import test from 'node:test'; import assert from 'node:assert/strict'; import JSZip from 'jszip'; import { serializeFinancialToolsSnapshot } from '../../resources/js/lib/financial-calculators.ts'; import { createProjectDossierPrintHtml, createProjectDossierReport, dossierReportSections, generateProjectDossierDocx, generateProjectDossierPackage, classifyDossierAttachment, validateDossierAttachments, } from '../../resources/js/lib/project-dossier-report.ts'; void test('report separates inapplicable documents from missing and unknown requirements without changing the saved snapshot', async () => { const requirements = [ { label: 'Título DUAT', state: 'not_applicable', reason: 'Arrendamento declarado', }, { label: 'Identificação', state: 'missing', reason: 'Pedido em preparação', }, { label: 'Capital externo', state: 'indeterminate', reason: 'Origem por determinar', }, ]; const snapshot = JSON.stringify({ version: 1, context_revision: 2, preparation: { enabled: true, mode: 'registration', version: 'QA', next: 'Completar identificação', calculation_notice: 'Cálculo separado', notice: 'Preparação', requirements, }, }); const draft = { context_applicability_snapshot: snapshot }; const report = createProjectDossierReport(draft, { selectedSections: new Set(['sources']), }); const section = report.sections.find((section) => section.id === 'sources'); assert.ok(section); assert.equal( section.rows.some((row) => row.field === 'Título DUAT'), false, ); assert.match( section.paragraphs.join(' '), /Não exigido neste contexto: Título DUAT \(Arrendamento declarado\)/, ); assert.equal( section.rows.find((row) => row.field === 'Identificação').value, 'Documento por associar', ); assert.equal( section.rows.find((row) => row.field === 'Capital externo').value, 'Informação em falta para decidir', ); const print = await createProjectDossierPrintHtml(report, [], false); assert.match(print, /Arrendamento declarado/); assert.equal(draft.context_applicability_snapshot, snapshot); }); void test('executive summary exposes the same financial result and period without replacing missing data by zero', () => { const draft = { currency: 'MZN', financial_tools_snapshot: serializeFinancialToolsSnapshot({ budgetPeriod: 'Mensal', loan: { principal: '1200', rate: '0', months: '12', rateType: 'nominal', periodMonths: '1', gracePeriods: '0', upfrontFee: '0', recurringFee: '0', }, breakEven: { fixed: '100.01', price: '30', variable: '20' }, funding: { total: '1000', secured: '800' }, incomes: [{ label: 'Receita QA', amount: '1000' }], expenses: [{ label: 'Despesa QA', amount: '1100' }], }), }; const report = createProjectDossierReport(draft, { selectedSections: new Set(['summary', 'finance']), }); const summary = report.sections.find((s) => s.id === 'summary'); const detail = report.sections.find((s) => s.id === 'finance'); assert.equal( summary.rows.find((r) => r.field.startsWith('Saldo operacional')).value, detail.rows.find((r) => r.field === 'Saldo operacional estimado').value, ); assert.match( summary.rows.find((r) => r.field.startsWith('Saldo operacional')).field, /Mensal/, ); const missing = createProjectDossierReport( {}, { selectedSections: new Set(['summary']) }, ); assert.equal( missing.sections[0].rows.some((r) => r.field.startsWith('Saldo operacional'), ), false, ); }); import { serializeBusinessPlanSnapshot, forecastBusinessPlan, } from '../../resources/js/lib/business-financial-plan.ts'; const allSections = new Set(dossierReportSections.map((section) => section.id)); function simplePlan() { return { currency: 'MZN', openingCash: '0', products: [ { label: 'Pão', monthlyUnits: '100', unitPrice: '50', unitVariableCost: '20', }, ], monthlyCollections: '100', monthlyVariableCostPayments: '100', monthlyOperatingExpenses: '500', monthlyOperatingPayments: '500', monthlyPayrollExpense: '0', monthlyPayrollPayments: '0', assetInvestment: '0', inventory: '0', receivables: '0', payables: '0', equity: '10000', shareholderLoans: '0', bankLoanProceeds: '0', grants: '0', assetDepreciation: '0', depreciationLifeMonths: 12, monthlyInterestExpense: '0', monthlyInterestPayment: '0', monthlyPrincipalRepayment: '0', periods: 12, }; } void test('incomplete dossier identifies unknowns without inventing zeroes', () => { const report = createProjectDossierReport( {}, { selectedSections: allSections, generatedAt: '2026-10-04T10:00:00.000Z', }, ); assert.equal(report.title, 'Não informado'); assert.equal( report.sections.find((section) => section.id === 'summary').rows[0] .value, 'Não informado', ); assert.match( report.sections.find((section) => section.id === 'resources').rows[0] .value, /Informação insuficiente/, ); assert.doesNotMatch(JSON.stringify(report), /"value":"0(\.00)?"/); }); void test('report sections are selected explicitly and preserve declared versus estimated provenance', () => { const selected = new Set(['summary', 'market', 'sources']); const report = createProjectDossierReport( { project_title: 'Moagem', market_assumption: 'procura crescente', market_source: 'entrevista própria; 2026-09-01', }, { selectedSections: selected, version: 'Revisão 3' }, ); assert.deepEqual( report.sections.map((section) => section.id), ['summary', 'market', 'sources'], ); const market = report.sections.find((section) => section.id === 'market'); assert.ok( market.rows.some( (row) => row.value === 'procura crescente' && row.classification === 'Informação declarada, não verificada', ), ); assert.equal(report.version, 'Revisão 3'); }); void test('business plan values in the dossier equal the existing simulator forecast', () => { const plan = simplePlan(); const expected = forecastBusinessPlan(plan); const snapshot = serializeBusinessPlanSnapshot(plan); const report = createProjectDossierReport( { project_title: 'Padaria', currency: 'MZN', business_plan_snapshot: snapshot, }, { selectedSections: allSections }, ); const finance = report.sections.find((section) => section.id === 'finance'); const row = (field) => finance.rows.find((item) => item.field === field).value; const numeric = (value) => { assert.match(value, /MZN$/); return value.replace(/\s|MZN/g, '').replace(',', '.'); }; assert.equal( numeric(row('Facturação no horizonte')), expected.totalRevenue, ); assert.equal( numeric(row('Resultado antes de impostos')), expected.totalProfitBeforeTax, ); assert.equal(numeric(row('Caixa final projectada')), expected.endingCash); assert.equal(expected.fundingStatus, 'surplus'); assert.equal( row('Reconciliação do financiamento'), 'Fontes superiores aos usos previstos', ); }); void test('an attached simulator execution keeps its exact snapshot result, explanation and version', () => { const report = createProjectDossierReport( { project_title: 'Pequena indústria' }, { selectedSections: allSections, currentCalculation: { status: 'completed', version: 4, updatedAt: '2026-10-03T12:00:00Z', reportChecksum: 'a'.repeat(64), snapshot: { contract_version: 1, indicative_total: '1250.75', formula_version: 'v2', base: '2000', rate_percent: '75', reduction_percent: '16.6', currency: 'MZN', context: { reference_date: '2026-10-03', engine_version: 'engine-4', scenario_version: 4, }, result: { status: 'calculated', value: '1250.75', unit: null, explanation: 'Cálculo indicativo guardado.', missing_inputs: [], source_ids: [], }, }, }, }, ); const finance = report.sections.find((section) => section.id === 'finance'); assert.equal( finance.rows.find((row) => row.field === 'Resultado indicativo').value, '1250.75', ); assert.equal( finance.rows.find((row) => row.field === 'Explicação e dados em falta') .value, 'Cálculo indicativo guardado.', ); assert.equal( finance.rows.find((row) => row.field === 'Revisão do resultado').value, '4', ); assert.equal( finance.rows.find( (row) => row.field === 'Hash SHA-256 do relatório associado', ).value, 'a'.repeat(64), ); }); void test('DOCX is a readable OpenXML package with Portuguese accents, tables and page fields', async () => { const report = createProjectDossierReport( { project_title: 'Produção de café', project_summary: 'Ideia com acção e localização.', }, { selectedSections: allSections, version: 'Revisão 2' }, ); const blob = await generateProjectDossierDocx(report, [], true); const bytes = new Uint8Array(await blob.arrayBuffer()); assert.deepEqual(Array.from(bytes.subarray(0, 2)), [0x50, 0x4b]); const zip = await JSZip.loadAsync(bytes); const documentXml = await zip.file('word/document.xml').async('string'); const footerXml = await zip.file('word/footer1.xml').async('string'); assert.match(documentXml, /Produção de café/); assert.match(documentXml, /Classificação/); assert.match(documentXml, /projecto/); assert.match(footerXml, /PAGE/); assert.ok(zip.file('[Content_Types].xml')); }); void test('CSV and plain-text annexes are embedded, while other originals get a ZIP index', async () => { const csv = new File(['Produto;Quantidade\r\nCafé;12'], 'tabela.csv', { type: 'text/csv', }); const pdf = new File(['%PDF-1.7 example'], '../apoio.pdf', { type: 'application/pdf', }); assert.equal(classifyDossierAttachment(csv), 'csv'); assert.equal(classifyDossierAttachment(pdf), 'original-only'); const attachments = [ { file: csv, include: true, caption: '' }, { file: pdf, include: true, caption: '' }, ]; const report = createProjectDossierReport( { project_title: 'Projecto de café' }, { selectedSections: allSections }, ); const docx = await generateProjectDossierDocx(report, attachments, true); const docxZip = await JSZip.loadAsync(await docx.arrayBuffer()); const xml = await docxZip.file('word/document.xml').async('string'); assert.match(xml, /Café/); assert.match(xml, /Este formato não foi incorporado/); const bundle = await generateProjectDossierPackage(docx, attachments, true); const bundleZip = await JSZip.loadAsync(await bundle.arrayBuffer()); assert.ok(bundleZip.file('Anexos/tabela.csv')); assert.ok(bundleZip.file('Anexos/apoio.pdf')); const index = await bundleZip.file('indice-anexos.txt').async('string'); assert.match(index, /Conteúdo não incorporado/); assert.match(index, /Conteúdo não incorporado; original preservado/); }); void test('PNG photographs are embedded in the Word package with an image relationship and caption', async () => { const pngBytes = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/aZkAAAAASUVORK5CYII=', 'base64', ); const photo = new File([pngBytes], 'local.jpg', { type: 'image/png' }); const report = createProjectDossierReport( { project_title: 'Projecto com fotografia' }, { selectedSections: allSections }, ); const docx = await generateProjectDossierDocx( report, [{ file: photo, include: true, caption: 'Localização declarada' }], true, ); const zip = await JSZip.loadAsync(await docx.arrayBuffer()); const mediaPath = Object.keys(zip.files).find((path) => /^word\/media\/.+\.png$/.test(path), ); assert.ok(mediaPath); assert.match( await zip.file('word/document.xml').async('string'), /Localização declarada/, ); assert.match( await zip.file('word/_rels/document.xml.rels').async('string'), new RegExp(`Target="media/${mediaPath.split('/').at(-1)}"`), ); }); void test('print/PDF HTML escapes user content and explains omitted annexes', async () => { const report = createProjectDossierReport( { project_title: '', project_summary: 'Acentuação: localização.', }, { selectedSections: new Set(['summary', 'annexes']) }, ); const pdf = await createProjectDossierPrintHtml(report, [], false); assert.doesNotMatch(pdf, /', }, [], false, ); assert.doesNotMatch(escapedVersion, /