Group investor reports and preserved financial copies in their existing dossier
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -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/);
|
||||
});
|
||||
Reference in New Issue
Block a user