import {test,expect} from '@playwright/test';import fs from 'node:fs';import path from 'node:path';import {execFileSync} from 'node:child_process';import {getRuntimeQaEnv} from '../../../../src/qa/env';
test.use({channel:'chrome',trace:'on',video:'off'});
const cases=[{date:'2026-09-29',accountId:162,name:'A_주홍석_지급계좌',corpAccountId:215,type:'NORMAL',count:2,amount:0,distribution:0},{date:'2026-10-02',accountId:148,name:'테스트 지급계좌',corpAccountId:219,type:'EARLY',count:3,amount:2001012,distribution:178993},{date:'2026-10-08',accountId:180,name:'A_주홍석_법인_지급계좌',corpAccountId:216,type:'EARLY',count:12,amount:0,distribution:0}];
for(const role of ['ADMIN','TA'])for(const e of cases)test(`[dev] / [${role}] role / Q07 Q08 ${e.date} 법인계좌 날짜별 집계·반복 동기화·엑셀`,async({browser},info)=>{test.setTimeout(160000);test.skip(process.env.QA4403_ALLOW_AGGREGATE_SYNC!=='1','BLOCKED: 승인된 개발계 집계 동기화 guard 필요');const env=getRuntimeQaEnv('dev','admin',role==='ADMIN'?'QA4373_ADMIN':role),ctx=await browser.newContext({storageState:env.storageState,acceptDownloads:true,viewport:{width:2200,height:1100}}),page=await ctx.newPage();page.setDefaultTimeout(12000);const dir=`qa-results/issues/4403/dev/private/${role}/distribution-${e.date}`;fs.mkdirSync(dir,{recursive:true});let syncs=0;const blockedWrites:string[]=[];await page.route('**/api/**',async route=>{const r=route.request(),p=new URL(r.url()).pathname;if(['POST','PUT','PATCH','DELETE'].includes(r.method())){if(r.method()==='POST'&&p.endsWith('/master-accounting/corporation-accounts/sync-distributions')&&new URL(r.url()).searchParams.get('date')===e.date){syncs++;return route.continue();}if(p.includes('/excel/'))return route.continue();blockedWrites.push(p);return route.abort('blockedbyclient');}return route.continue();});const isGet=(r:any)=>r.request().method()==='GET'&&new URL(r.url()).pathname.endsWith(`/disbursement-accounts/${e.accountId}/corporation-accounts/settlements`)&&new URL(r.url()).searchParams.get('date')===e.date;const flat=(d:any)=>d.payload?.content?.flatMap((g:any)=>g.corporationAccounts.map((x:any)=>({...x,pgName:g.pgName})))??[];const stable=(d:any)=>flat(d).map((r:any)=>({account:r.corporationAccountId,type:r.isEarlySettlement?'EARLY':'NORMAL',count:r.totalTransactionCount,amount:r.totalTransactionAmount,distribution:r.calculatedTransactionAmount})).sort((a:any,b:any)=>a.account-b.account||a.type.localeCompare(b.type));
try{await page.goto(`${env.baseUrl}/master-accounting/disbursement-accounts?tenantId=redacted}`);const active=page.locator('[data-route-keep-alive-active=true]');const transfer=active.getByRole('button',{name:'이체 ▾',exact:true});if(!await transfer.waitFor({state:'visible',timeout:20000}).then(()=>true).catch(()=>false)){await info.attach('permission-context.png',{body:await active.screenshot(),contentType:'image/png'});await info.attach('permission.json',{body:JSON.stringify({role,route:'/master-accounting/disbursement-accounts',readable:await active.isVisible(),transferPanelProvided:false}),contentType:'application/json'});test.skip(true,'BLOCKED: 기존 계정에 날짜별 분배 조회 패널 진입 권한 없음. 공유 프리셋 변경 안 함');}await transfer.click();await page.getByText('법인계좌 이체',{exact:true}).click();const form=page.getByRole('dialog');await expect(form).toBeVisible();await form.getByRole('combobox',{name:'지급계좌를 선택하세요',exact:true}).fill(e.name);const option=page.getByRole('option').filter({hasText:e.name});if(!await option.first().waitFor({state:'visible',timeout:10000}).then(()=>true).catch(()=>false)){await info.attach('account-scope.png',{body:await form.screenshot(),contentType:'image/png'});await info.attach('account-scope.json',{body:JSON.stringify({role,accountId:e.accountId,optionAvailable:false}),contentType:'application/json'});test.skip(true,'BLOCKED: 해당 지급계좌가 기존 역할 계정의 선택 범위에 없음');}await option.first().click();await form.getByRole('button',{name:'거래내역',exact:true}).click();const panel=form.locator('div.absolute.right-0.top-0').filter({has:page.getByRole('button',{name:'엑셀 다운로드',exact:true})});await expect(panel).toBeVisible();const beforeWait=page.waitForResponse(isGet);await panel.locator('input[type=date]').fill(e.date);await panel.locator('input[type=date]').press('Tab');const beforeResponse=await beforeWait;if(beforeResponse.status()===403){await info.attach('read-permission.json',{body:JSON.stringify({role,date:e.date,accountId:e.accountId,status:403,scope:'날짜별 집계 조회',permissionsChanged:false}),contentType:'application/json'});await info.attach('read-denied.png',{body:await panel.screenshot(),contentType:'image/png'});test.skip(true,'BLOCKED: 기존 계정 날짜별 법인계좌 집계 조회 API 403');}const before=await beforeResponse.json();if(!fs.existsSync(dir+'/initial-before.json'))fs.writeFileSync(dir+'/initial-before.json',JSON.stringify(before,null,2));fs.writeFileSync(dir+'/before.json',JSON.stringify(before,null,2));const responses:any[]=[];for(let n=1;n<=2;n++){const sync=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname.endsWith('/master-accounting/corporation-accounts/sync-distributions'));const refreshed=page.waitForResponse(isGet).then(r=>r.json()).catch(()=>null);await panel.getByRole('button',{name:'동기화',exact:true}).click();const r=await sync;if(r.status()===403){await info.attach('sync-permission.json',{body:JSON.stringify({role,date:e.date,accountId:e.accountId,status:403,syncSucceeded:false,permissionsChanged:false}),contentType:'application/json'});await info.attach('sync-denied.png',{body:await panel.screenshot(),contentType:'image/png'});test.skip(true,'BLOCKED: 기존 계정의 날짜별 법인계좌 집계 동기화 API 403. 권한 변경 없이 종료');}expect(r.status()).toBe(200);expect(new URL(r.url()).searchParams.get('date')).toBe(e.date);const after=await refreshed;expect(after,'동기화 후 실제 GET 응답').toBeTruthy();responses.push(after);fs.writeFileSync(dir+`/after-${n}.json`,JSON.stringify(after,null,2));await expect(panel.getByRole('button',{name:'동기화',exact:true})).toBeEnabled();}
expect(stable(responses[1]),'같은 날짜 동기화 재실행 후 모든 계좌·유형별 건수·금액 유지').toEqual(stable(responses[0]));const target=flat(responses[1]).find((r:any)=>r.corporationAccountId===e.corpAccountId);if(e.distribution===0){expect(target,'기존 정책: 분배 순금액 0원 집계는 저장/표시 제외').toBeUndefined();}else{expect(target,'원거래 정산 대상 법인계좌 집계 존재').toBeTruthy();expect(target.isEarlySettlement).toBe(e.type==='EARLY');expect(target.totalTransactionCount).toBe(e.count);expect(target.totalTransactionAmount).toBe(e.amount);expect(target.calculatedTransactionAmount).toBe(e.distribution);expect(flat(responses[1]).filter((r:any)=>r.corporationAccountId===e.corpAccountId&&r.isEarlySettlement!==(e.type==='EARLY'))).toHaveLength(0);const table=panel.locator('tr').filter({hasText:target.corporationAccountName});await expect(table.first()).toBeVisible();}await info.attach('distribution-after.png',{body:await panel.screenshot(),contentType:'image/png'});await info.attach('distribution-comparison.json',{body:JSON.stringify({role,expected:e,before:stable(before),afterFirst:stable(responses[0]),afterSecond:stable(responses[1]),syncs,blockedWrites,actualTransfersExecuted:0,sourceBasis:'원거래 계약ID/정산유형·거래라인 수수료·가맹 정산대상 금액. 현재 MID 정보 사용 안 함.'},null,2),contentType:'application/json'});
const excel=page.waitForResponse(r=>r.request().method()==='GET'&&new URL(r.url()).pathname.endsWith(`/excel/disbursement-accounts/${e.accountId}/corporation-accounts/settlements`));await panel.getByRole('button',{name:'엑셀 다운로드',exact:true}).click();const er=await excel;expect(er.status()).toBe(200);expect(new URL(er.url()).searchParams.get('date')).toBe(e.date);const excelId=(await er.json()).payload;const pop=page.locator('.swal2-popup:visible');if(await pop.count()){const ok=pop.getByRole('button',{name:/문서관리|확인/});if(await ok.count())await ok.first().click();}await page.goto(`${env.baseUrl}/document?tenantId=redacted}`);const a=()=>page.locator('[data-route-keep-alive-active=true]');const row=a().locator(`.MuiDataGrid-row[data-id="${excelId}"]`);await expect.poll(async()=>{if(await row.locator('a').count())return true;await page.reload();await page.waitForTimeout(1500);return await row.locator('a').count()>0;},{timeout:60000,intervals:[2000,3000]}).toBe(true);const dw=page.waitForEvent('download');await row.locator('a').click();const download=await dw;const file=path.join(dir,'distribution.xlsx');await download.saveAs(file);const parsed=JSON.parse(execFileSync('python3',['qa/issues/4403/dev/read-distribution-excel.py',file],{encoding:'utf8'}));fs.writeFileSync(dir+'/excel-parsed.json',JSON.stringify(parsed,null,2));await info.attach('distribution-excel-comparison.json',{body:JSON.stringify({role,expected:e,headers:parsed.headers,rows:parsed.rows}),contentType:'application/json'});for(const ui of flat(responses[1])){const excelRow=parsed.rows.find((r:any)=>r['법인계좌명']===ui.corporationAccountName&&r['자금 유형']===(ui.isEarlySettlement?'선정산 자금':'일반정산 자금'));expect(excelRow,'엑셀 계좌·자금유형별 행').toBeTruthy();expect(Number(excelRow['거래건수'])).toBe(ui.totalTransactionCount);expect(Number(excelRow['거래금액'])).toBe(ui.totalTransactionAmount);expect(Number(excelRow['분배금액'])).toBe(ui.calculatedTransactionAmount);}const publicFile=path.join(dir,'distribution-public.xlsx');execFileSync('python3',['qa/issues/4403/dev/read-distribution-excel.py',file,'--public',publicFile],{stdio:'ignore'});await info.attach('distribution-masked.xlsx',{path:publicFile,contentType:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});if(target)expect(parsed.rows.some((r:any)=>Object.values(r).includes(target.corporationAccountName)),'엑셀 원거래 대상 계좌').toBe(true);else expect(parsed.rows.filter((r:any)=>Object.values(r).some(v=>String(v).includes('A_주홍석')))).toHaveLength(0);
}finally{expect(blockedWrites,'실제 이체·분배·출금 쓰기 요청 없음').toEqual([]);await page.unrouteAll({behavior:'ignoreErrors'});await ctx.close();}});
