Files
Investidor-V2/tools/portal-functional-lease-workflow-qa.cjs
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

53 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Only the remaining lease branch; reuse prior sale/materiality/coediting/A/B/C37 evidence.
const {start,assert,DIR,fs,path,sanitized}=require('./portal-functional-qa-lib.cjs');
const crypto=require('node:crypto');
void(async()=>{
const q=await start('functional-lease-workflow-chromium-20261011'),sessions=new Map();let page,p;
const id=54,title='Arrendamento — SIMULAÇÃO QA 11/10/2026, sem efeito jurídico';
const plan=JSON.parse(fs.readFileSync(path.join(DIR,'portal-stability-latest.json'),'utf8'));
const source=fs.readFileSync(path.join(plan.source,'resources/js/components/opportunity-canonical-fields.tsx'),'utf8');
const labelBlock=source.slice(source.indexOf('const labels:'),source.indexOf('function',source.indexOf('const labels:')));
const labels=Object.fromEntries([...labelBlock.matchAll(/^\s*(\w+):\s*'([^']+)'/gm)].map(m=>[m[1],m[2]]));
q.proof.opportunity_id=id;q.proof.documents=q.prior.documents||{};q.proof.location=q.prior.location;q.proof.checks={...(q.prior.checks||{})};q.proof.transitions=q.prior.transitions||[];
q.proof.reused=['Sale case31','6-category materiality/conditions','coediting QA22','multiple-role self-approval rejection','A/B and C37 closures'];
async function actor(role,fresh=false){if(fresh&&sessions.has(role)){await sessions.get(role).ctx.close();sessions.delete(role);}if(!sessions.has(role)){const s=await q.context(role);sessions.set(role,s);await q.login(s.page,role);}page=sessions.get(role).page;}
async function open(route=`/minhas-oportunidades/${id}/view`){const r=await q.open(page,route);assert.equal(r.status(),200);const html=await r.text(),a=html.match(/data-page="([^"]+)"/);p=JSON.parse(a&&a[1].startsWith('{')?a[1].replace(/&quot;/g,'"').replace(/&amp;/g,'&').replace(/&#039;/g,"'"):html.match(/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/)[1]).props;await page.locator('main').first().waitFor();return p;}
async function rent(stage){await open();assert.equal(p.values.canonical.real_estate.rent,4500);assert.equal(p.values.canonical.real_estate.rent_periodicity,'monthly');assert.equal(p.values.canonical.currency,'MZN');const api=await q.json(page,`/minhas-oportunidades/${id}/response`);assert.equal(api.status,200);q.proof.transitions.push({stage,state:p.workflowState,rent:4500,periodicity:'monthly',currency:'MZN',response_status:200,version:p.canonicalResponse.data.version});q.save();}
async function action(name,gate){await open();assert(p.profileActions.includes(name),'Authorized '+name);const box=page.getByRole('region',{name:'Workflow interno',exact:true});await box.getByLabel('Fundamentação',{exact:true}).fill('SIMULAÇÃO QA54 de '+name+': preservar renda 4500 MZN/mês, documentos próprios e revisão independente; sem direito real.');
if(name==='request_correction')await box.getByLabel('Campos ou documentos a corrigir, um por linha',{exact:true}).fill('canonical.real_estate.title_holder_name — explicitar que o titular e o documento são simulações QA sem efeito jurídico.');
if(gate){await box.getByRole('combobox',{name:/^Revisão/}).selectOption(gate);await box.getByRole('combobox',{name:/^Decisão/}).selectOption('approved');await box.getByLabel(q.proof.documents[{property_review:'valuation',registry_urban_review:'title',building_compliance_review:'license'}[gate]].title,{exact:true}).check();}
if(name==='publish')await box.getByLabel(/Confirmei a pré-visualização/).check();
const names={submit:'Submeter para validação',start_review:'Iniciar validação',request_correction:'Solicitar correções',review_gate:'Guardar revisão especializada',approve:'Aprovar versão',publish:'Disponibilizar com acesso restrito'};
const pending=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname===`/minhas-oportunidades/${id}/workflow`);await box.getByRole('button',{name:names[name],exact:true}).click();const r=await pending,body=await r.json();assert.equal(r.status(),200,JSON.stringify(body));await rent(name+(gate?':'+gate:''));
}
async function field(path_,value){const el=page.locator(`[id="${path_}"]`);await el.waitFor({state:'attached'});const tag=await el.evaluate(e=>e.tagName);if(tag==='SELECT')await el.selectOption(String(value));else await el.fill(String(value));}
async function values(data,prefix='canonical'){
for(const [key,v]of Object.entries(data)){if(['opportunity_category','promoter_id'].includes(key))continue;const here=prefix+'.'+key;
if(prefix==='canonical'&&['sector_id','subsector_id','economic_activity_id','internal_contact_id','public_contact_id'].includes(key)){await page.getByRole('combobox',{name:new RegExp('^'+labels[key])}).selectOption(String(v));continue;}
if(prefix==='canonical'&&['documents','locations','images'].includes(key)){const rows=key==='locations'?[q.proof.location]:Object.values(q.proof.documents);for(const row of rows)await page.getByLabel(row.title,{exact:true}).check();continue;}
if(Array.isArray(v)){const label=labels[key]??key.replaceAll('_',' '),box=page.locator('fieldset').filter({has:page.locator('legend').filter({hasText:new RegExp('^'+label.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'$')})}).filter({has:page.getByRole('button',{name:'Adicionar linha',exact:true})}).last();
for(let i=0;i<v.length;i++){const probe=typeof v[i]==='object'?Object.keys(v[i])[0]:null;const exists=await page.locator(`[id="${here}.${i}${probe?'.'+probe:''}"]`).count();if(!exists)await box.locator(':scope > button').filter({hasText:'Adicionar linha'}).click();if(typeof v[i]==='object')await values(v[i],here+'.'+i);else await field(here+'.'+i,v[i]);}continue;}
if(v&&typeof v==='object'){await values(v,here);continue;}await field(here,typeof v==='boolean'?String(v):v);
}
}
async function save(){await page.getByLabel('Motivo da alteração',{exact:true}).fill('SIMULAÇÃO QA54: arrendamento com documentos próprios; conservar a renda 4500 MZN/mês e os IDs existentes.');const pending=page.waitForResponse(r=>r.request().method()==='PUT'&&new URL(r.url()).pathname===`/minhas-oportunidades/${id}/draft`);await page.getByRole('button',{name:'Guardar contrato no rascunho',exact:true}).click();const r=await pending;assert.equal(r.status(),200,JSON.stringify(await r.json()));}
async function binary(url){return page.evaluate(async u=>{const r=await fetch(u);const bytes=await r.arrayBuffer();const hash=await crypto.subtle.digest('SHA-256',bytes);return{status:r.status,sha256:[...new Uint8Array(hash)].map(b=>b.toString(16).padStart(2,'0')).join('')};},url);}
try{
await actor('promoter');q.phase('Lease own documents and real QA scanning');
for(const kind of ['title','license','valuation']){if(q.proof.documents[kind])continue;await open(`/minhas-oportunidades/${id}/resources`);await page.getByRole('button',{name:'Documentos',exact:true}).click();const docTitle='SIMULAÇÃO QA54 — '+{title:'título e registo fictícios',license:'licença de utilização fictícia',valuation:'avaliação indicativa fictícia'}[kind];const file=path.join(DIR,`functional-lease-${kind}-20261011.pdf`),sha=crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');await page.getByLabel('Título',{exact:true}).fill(docTitle);await page.getByLabel('Classificação documental',{exact:true}).selectOption('restricted');await page.getByLabel('Âmbito de consulta',{exact:true}).selectOption('internal');await page.locator('input[type="file"]').setInputFiles(file);await page.getByLabel('Motivo (20–1000 caracteres)',{exact:true}).fill('SIMULAÇÃO QA54 sem efeito jurídico: ficheiro próprio para verificar a condição documental de arrendamento.');const pending=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname.endsWith('/resources/documents'));await page.getByRole('button',{name:'Guardar recurso',exact:true}).click();const r=await pending;assert.equal(r.status(),201);const resource=(await r.json()).id;q.proof.documents[kind]={id:resource,title:docTitle,sha256:sha};q.save();const url=`/minhas-oportunidades/${id}/resources/documents/${resource}/download`;let found;for(let i=0;i<40;i++){found=await binary(url);if(found.status===200)break;assert.equal(found.status,423);await page.waitForTimeout(1000);}assert.equal(found.status,200);assert.equal(found.sha256,sha);q.proof.documents[kind].clean_download_verified=true;q.save();}
if(!q.proof.location){q.phase('Lease own location and administrative map');await open(`/minhas-oportunidades/${id}/resources`);const choose=async(label,re)=>{const s=page.getByRole('combobox',{name:label,exact:true}),o=s.locator('option').filter({hasText:re}).first();await o.waitFor({state:'attached'});await s.selectOption(await o.getAttribute('value'));};await choose('Província',/^Cabo Delgado/i);await choose('Distrito',/^Ancuabe/i);await page.getByLabel('Papel da localização',{exact:true}).selectOption('storage');await page.getByLabel('Natureza da relação territorial',{exact:true}).selectOption('context');await page.getByLabel('Localização principal',{exact:true}).check();await page.getByLabel('Título',{exact:true}).fill('SIMULAÇÃO QA54 — contexto administrativo Cabo Delgado/Ancuabe');await page.getByLabel('Motivo (20–1000 caracteres)',{exact:true}).fill('SIMULAÇÃO QA54: contexto administrativo sem atribuição de localização real nem direito territorial.');const pending=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname.endsWith('/resources/locations'));await page.getByRole('button',{name:'Guardar recurso',exact:true}).click();const r=await pending;assert.equal(r.status(),201);q.proof.location={id:(await r.json()).id,title:'SIMULAÇÃO QA54 — contexto administrativo Cabo Delgado/Ancuabe'};q.save();}
if(!q.proof.checks.complete_contract_saved){q.phase('Lease canonical fields through the editor');await open(`/minhas-oportunidades/${id}/edit`);const baseline=JSON.parse(fs.readFileSync(path.join(DIR,'functional-lease-context-qa-20261011.json'),'utf8'))['31'].canonical;
const data={...baseline,official_title:title,executive_summary:'SIMULAÇÃO QA de arrendamento sem efeito jurídico. '.repeat(10),demand_statement:'Pretensão fictícia exclusivamente para verificar o workflow independente de arrendamento. '.repeat(3),review_due_date:'2027-01-01',documents:Object.values(q.proof.documents).map(d=>d.id),locations:[q.proof.location.id],evidences:Object.entries(q.proof.documents).map(([kind,d])=>({title:d.title,evidence_type:'qa-synthetic-'+kind,document_id:d.id})),real_estate:{...baseline.real_estate,transaction_type:'lease',rent:4500,rent_periodicity:'monthly',registration_number:'SIMULAÇÃO-QA54',title_holder_name:'Titular fictício da organização QA',title_document_id:q.proof.documents.title.id,use_license_document_id:q.proof.documents.license.id,valuation:{date:'2026-10-11',value:1000,method:'qa-simulation-only',appraiser:'SIMULAÇÃO QA54',document_id:q.proof.documents.valuation.id}}};delete data.real_estate.price;await values(data);await save();q.proof.checks.complete_contract_saved=true;q.save();}
await actor('promoter',true);await rent('complete_draft_new_session');for(const d of Object.values(q.proof.documents)){assert.equal((await binary(`/minhas-oportunidades/${id}/resources/documents/${d.id}/download`)).sha256,d.sha256);}q.proof.checks.documents_recovered_new_session=true;
if(!q.proof.checks.first_submission){q.phase('Lease submission');await action('submit');q.proof.checks.first_submission=true;q.save();}
if(!q.proof.checks.correction_requested){await actor('validator');q.phase('Independent validator requests correction');await action('start_review');await action('request_correction');assert.equal(p.workflowState,'needs_information');q.proof.checks.correction_requested=true;q.save();}
if(!q.proof.checks.corrected_and_resubmitted){await actor('promoter',true);await rent('correction_recovered_new_session');await open(`/minhas-oportunidades/${id}/edit`);await field('canonical.real_estate.title_holder_name','SIMULAÇÃO QA54 — titular fictício sem direito real atribuído');await save();await rent('corrected_draft');await action('submit');q.proof.checks.corrected_and_resubmitted=true;q.save();}
if(!q.proof.checks.approved){await actor('validator');await open();if(p.workflowState==='submitted')await action('start_review');for(const gate of ['property_review','registry_urban_review','building_compliance_review']){if(p.profileContext.reviews?.[gate]?.decision!=='approved')await action('review_gate',gate);}await action('approve');assert.equal(p.profileContext.approval.actor_id,24);q.proof.checks.approved=true;q.save();}
if(!q.proof.checks.published_restricted){await actor('publisher');q.phase('Explicit restricted publication, no public catalogue');await action('publish');assert.equal(p.workflowState,'published');assert.equal(p.publicationState.public_catalog,false);assert.equal(p.profileContext.publication.actor_id,25);q.proof.checks.published_restricted=true;await q.shot(page,'lease-publication-restricted',false);q.save();}
await actor('promoter',true);await rent('published_recovered_new_session');assert.equal(p.workflowState,'published');assert(!p.profileActions.includes('approve'));assert(!p.profileActions.includes('publish'));assert.equal(p.values.canonical.real_estate.title_document_id,q.proof.documents.title.id);assert.equal(p.values.canonical.real_estate.use_license_document_id,q.proof.documents.license.id);await q.shot(page,'lease-published-recovered',false);q.proof.checks.creator_review_buttons_denied=true;
await actor('outsider');assert.equal((await q.open(page,`/minhas-oportunidades/${id}/edit`)).status(),403);const denied=await binary(`/minhas-oportunidades/${id}/resources/documents/${q.proof.documents.title.id}/download`);assert([403,404].includes(denied.status));q.proof.checks.outside_organization_denied=true;
q.proof.result='passed';q.save();console.log(JSON.stringify({result:'passed',id,checks:q.proof.checks,transitions:q.proof.transitions.length,active_writes:0}));
}catch(e){q.proof.result='failed';q.proof.failure=sanitized(e);if(page&&!page.isClosed()){q.proof.last_route=new URL(page.url()).pathname;q.proof.visible_alerts=await page.getByRole('alert').allTextContents();}q.save();console.log(JSON.stringify({result:'failed',failure:q.proof.failure,route:q.proof.last_route,alerts:q.proof.visible_alerts}));process.exitCode=1;}
finally{for(const s of sessions.values())await s.ctx.close();await q.browser.close();}
})();