Merge branch 'main' of https://git.tempo.co.mz/nala/Investidor-V2
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-11-13 14:43:34 +02:00
42 changed files with 4400 additions and 10 deletions
@@ -0,0 +1,311 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import JSZip from 'jszip';
import {
createInvestorDossierReport,
investorDossierReportIndex,
} from '../../resources/js/lib/investor-dossier-report.ts';
import {
createProjectDossierPrintHtml,
generateProjectDossierDocx,
} from '../../resources/js/lib/project-dossier-report.ts';
import { serializeFinancialToolsSnapshot } from '../../resources/js/lib/financial-calculators.ts';
function fixture() {
return {
case: {
id: 'case-a',
title: 'Investimento QA <script>alert(1)</script>',
version: 9,
},
context_opportunity: 'op-a',
opportunity_options: ['op-a', 'op-b'],
investment_context: {
revision: 3,
fields: { project_title: 'Projecto QA', province: 'Manica' },
},
phases: { 1: 'Viabilidade', 2: 'Preparação' },
steps: [
{
code: 'S03',
title: 'Viabilidade',
phase: 1,
version: 2,
state: 'preparing',
reason: 'Conferir pressupostos',
detail: {},
},
{
code: 'S08',
title: 'Licenciamento prévio',
phase: 2,
version: 4,
state: 'not_started',
reason: '',
detail: { document_id: 'doc-a' },
},
],
results: [],
land_report: {
id: 'land-a',
version: 3,
updated_at: '2026-11-13',
href: '/investidor/terra-duat/land-a/imprimir',
},
documents: [
{
id: 'link-a',
document_id: 'doc-a',
file_name: 'Estudo QA',
status: 'active',
malware_scan_status: 'clean',
version_no: 2,
sha256: 'a'.repeat(64),
},
],
preparation_report: {
case_id: 'case-a',
opportunity: 'op-a',
version: 7,
context_revision: 3,
href: '/investimentos/case-a/preparacao?opportunity=op-a',
api_state: 'delivery_recorded',
api_issues: [],
exports: [
{
id: 'export-old',
at: '2026-11-12',
context_revision: 2,
historical: true,
fingerprint: 'old',
href: '/investimentos/case-a/preparacao/exportar/export-old?opportunity=op-a',
},
{
id: 'export-new',
at: '2026-11-13',
context_revision: 3,
historical: false,
fingerprint: 'new',
href: '/investimentos/case-a/preparacao/exportar/export-new?opportunity=op-a',
},
],
external: [
{
kind: 'delivery',
date: '2026-11-13',
reference: 'QA-ENTREGA',
export_id: 'export-new',
},
],
swot_review_required: true,
swot: {
object_key: 'investment:case-a',
scope: 'Produção QA',
rows: [
{
id: 's1',
dimension: 'strength',
text: 'Equipa experiente',
reference: 'Fonte QA',
date: '2026-11-12',
classification: 'hypothesis',
},
],
review: null,
actions: [],
},
},
fiscal_reports: [
{
simulation_id: 'simulation-a',
report_id: 'report-a',
report_version_no: 2,
report_sha256: 'b'.repeat(64),
consented_at: '2026-11-13',
},
],
company_reports: {
comparison: { revision: 2, at: '2026-11-12' },
guides: {
energy: {
slug: 'energia',
title: 'Guia de energia',
revision: 1,
at: '2026-11-12',
},
},
},
};
}
void test('one dossier groups stage evidence, SWOT, fiscal and all preserved APIEX versions without modifying sources', () => {
const input = fixture();
input.documents.push({ ...input.documents[0], id: 'second-link' });
const before = JSON.stringify(input);
const index = investorDossierReportIndex(input);
assert.equal(
index.filter((entry) => entry.id.startsWith('apiex:')).length,
2,
);
assert.equal(
index.find((entry) => entry.id === 'document:link-a').stage,
'S08',
);
assert.equal(
index.filter((entry) => entry.id.startsWith('document:')).length,
1,
);
assert.match(
index.find((entry) => entry.id === 'apiex:export-old').state,
/histórica/,
);
assert.equal(
index.find((entry) => entry.id === 'swot').state,
'Revisão necessária',
);
assert.match(
index.find((entry) => entry.id === 'land:land-a').state,
/não é título DUAT/,
);
const report = createInvestorDossierReport(input, '2026-11-13T12:00:00Z');
assert.match(report.version, /case-a.*v9.*contexto v3.*op-a/);
assert.match(
report.sections.find((s) => s.id === 'execution').rows[1].value,
/Por iniciar.*versão 4/,
);
assert.match(
report.sections.find((s) => s.id === 'market').rows[0].value,
/Hipótese/,
);
assert.match(
report.sections.find((s) => s.id === 'sources').paragraphs.join(' '),
/Entrega externa registada.*não equivale a aprovação/,
);
assert.equal(JSON.stringify(input), before);
});
void test('cross-dossier or cross-opportunity preparation is rejected and an ambiguous scope requires selection', () => {
const input = fixture();
input.preparation_report.case_id = 'case-b';
assert.throws(() => createInvestorDossierReport(input), /outro dossier/);
input.preparation_report.case_id = 'case-a';
input.preparation_report.opportunity = 'op-b';
assert.throws(() => createInvestorDossierReport(input), /oportunidade/);
input.context_opportunity = null;
assert.throws(() => investorDossierReportIndex(input), /Seleccione/);
});
void test('financial results from a different opportunity do not enter the consolidated report or its index', () => {
const input = fixture();
input.results = [
{
id: 'other',
context_opportunity: 'op-b',
financial_snapshot: 'OTHER-PRIVATE-INPUTS',
draft_id: 'other-draft',
draft_version: 8,
recorded_at: '2026-11-13',
engine: 'unknown',
currency: 'MZN',
},
];
assert.ok(
!JSON.stringify(createInvestorDossierReport(input)).includes(
'OTHER-PRIVATE-INPUTS',
),
);
assert.ok(
!investorDossierReportIndex(input).some(
(entry) => entry.id === 'finance:other',
),
);
assert.match(
createInvestorDossierReport(input).sections.find(
(s) => s.id === 'finance',
).paragraphs[0],
/Nenhum resultado/,
);
});
void test('the consolidated report reuses loan, break-even, financing and budget outputs with currency and historical state', () => {
const input = fixture();
const financial_snapshot = serializeFinancialToolsSnapshot({
budgetPeriod: 'Mensal',
loan: {
principal: '100000',
rate: '0',
months: '12',
rateType: 'nominal',
periodMonths: '1',
gracePeriods: '0',
upfrontFee: '0',
recurringFee: '0',
},
breakEven: { fixed: '1000', price: '100', variable: '50' },
funding: { total: '100000', secured: '30000' },
incomes: [{ label: 'Receita QA', amount: '100' }],
expenses: [{ label: 'Custo QA', amount: '50' }],
});
input.results = [
{
id: 'finance-a',
context_opportunity: 'op-a',
context_revision: 2,
context_state: 'review_required',
financial_snapshot,
draft_id: 'draft-a',
draft_version: 5,
recorded_at: '2026-11-12',
engine: 'financial-calculators:v1',
currency: 'MZN',
},
];
const report = createInvestorDossierReport(input);
const rows = report.sections.find(
(section) => section.id === 'finance',
).rows;
assert.equal(
rows.find(
(item) => item.field === 'Financiamento — Diferença por financiar',
).value,
'70000.00 MZN',
);
assert.equal(
rows.find(
(item) => item.field === 'Ponto de equilíbrio — Unidades estimadas',
).value,
'20',
);
assert.equal(
rows.find(
(item) => item.field === 'Orçamento — Saldo operacional estimado',
).value,
'50.00 MZN',
);
assert.ok(
rows.some((item) => item.field.startsWith('Empréstimo educativo')),
);
assert.match(rows[0].classification, /Histórico/);
assert.equal(input.results[0].financial_snapshot, financial_snapshot);
});
void test('the existing Word and print exporters retain dossier identity, pending stages, source and history, escaping active HTML', async () => {
const report = createInvestorDossierReport(
fixture(),
'2026-11-13T12:00:00Z',
);
const html = await createProjectDossierPrintHtml(report, [], false);
assert.ok(!html.includes('<script>alert(1)</script>'));
assert.match(html, /case-a/);
assert.match(html, /Fonte QA/);
assert.match(html, /export-old/);
assert.ok(html.includes('a'.repeat(64)));
const docx = await generateProjectDossierDocx(report, [], false);
const zip = await JSZip.loadAsync(await docx.arrayBuffer());
const xml = await zip.file('word/document.xml').async('string');
assert.match(xml, /case-a/);
assert.match(xml, /Licenciamento prévio/);
assert.match(xml, /Por iniciar/);
assert.match(xml, /Fonte QA/);
assert.match(xml, /export-old/);
});