fix(offers): preserve description in authorised investment context

This commit is contained in:
2026-11-14 18:39:25 +02:00
parent ed2fd86f6b
commit 9a01426864
21 changed files with 1219 additions and 10 deletions
+11
View File
@@ -193,6 +193,17 @@ final class InvestorContext
$href = $published ? '/oportunidades/'.$o->slug : '/minhas-oportunidades/'.$o->id.'/view';
$version = $o->management_version;
$fields = ['project_title' => $title, 'project_summary' => $draft['canonical']['executive_summary'] ?? $o->summary];
if ($o->isPrivateOfferPreparation()) {
// Offers reuse Opportunity, but their preparation writes description,
// not the canonical executive summary. Keep this authorised private
// projection explicit: never copy private_offer (price/finances/contact).
$title = $draft['title'] ?? $o->title;
$summary = $fields['project_summary'];
$fields['project_title'] = $title;
$fields['project_summary'] = is_string($summary) && trim($summary) !== ''
? $summary : ($draft['description'] ?? null);
$href = '/minhas-oportunidades/ofertas/'.$o->id;
}
if ($o->sector) {
$fields['sector_id'] = (string) $o->sector_id;
$fields['sector'] = $o->sector->name;
@@ -10202,3 +10202,6 @@ Estado: cinco fases preparadas em QA; F1-F4 aceites tecnicamente com ressalvas,
# Auditoria de aceitação Ofertas — 14/11/2026, antes de instalar
Referência: `../v2/auditoria-portal/ofertas-aceitacao-final-20261114.md`. Candidata `528a2597`; commits e hashes F1–F5 conferidos, JUnit aceite 59 testes/780 asserções confirmado sem o apresentar como nova execução. Nova regressão dos componentes nativos: 14/227 aprovada. Novo teste de integração: 2/11, um aprovado e uma falha na reutilização da descrição da Oferta pelo contexto de investimento; recuperação financeira privada e isolamento passaram. A aceitação integral permanece bloqueada: falta demonstrar Oferta → dossier → viabilidade em função da localização → relatório → recuperação. Publicação efectiva não implementada/activada; NDA real, visual/móvel, imagens e entrega real de email mantêm os estados específicos do relatório. Plano de instalação preparado, zero novas migrações no delta, schema do destino e reversão compatível por conferir. Nenhuma instalação ou alteração funcional efectuada nesta auditoria. RF-MN-003 actualizado sem alterar os restantes requisitos.
# Correcção Ofertas → contexto de investimento — QA, 14/11/2026
Após `ed2fd86f`, foi autorizada exclusivamente a correcção da continuidade. `InvestorContext` passa a recuperar a descrição da Oferta quando não existe resumo, mantendo a mesma Opportunity, os identificadores e a autorização privada. Resumo existente e edições deliberadas no dossier prevalecem. Não se copiam dados empresariais confidenciais, contactos ou preço privado; NDA/autorização do vendedor não concedem acesso ao contexto privado. A prova sintética percorre criação, submissão, revisão/aprovação, projecção pública isolada de teste, contexto/dossier, localização, pressupostos financeiros declarados, gravação/recuperação e relatórios HTML/Word. Não representa publicação real nem rentabilidade garantida. Recibo e resultados em `../v2/auditoria-portal/ofertas-correcao-contexto-qa-20261114.md` e `outputs/offers-investment-continuity-20261114/qa-result.json`; RF-MN-003 e matriz final actualizados. Mantêm-se pendências jurídicas, visuais, de imagens e email real. Sem instalação, activação pública, migrações ou alteração de dados operacionais; aguarda nova autorização para instalar.
File diff suppressed because one or more lines are too long
@@ -4,6 +4,8 @@
**Decisão: aprovação técnica parcial com ressalvas; aceitação integral bloqueada.** As provas das cinco fases são válidas para os comportamentos que demonstram. Não demonstram uma publicação operacional nem a continuidade completa Oferta → dossier → viabilidade territorial. Foi reproduzida uma perda de reutilização da descrição nessa fronteira.
**Actualização posterior à auditoria `ed2fd86f`:** a correcção autorizada em QA está documentada em [Ofertas → Investimento](ofertas-correcao-contexto-qa-20261114.md). O saldo actualizado nas linhas abaixo substitui apenas o estado desses critérios; conserva-se a prova original da falha. O percurso controlado com hipóteses de localização e relatório foi demonstrado; publicação operacional e restantes dependências não foram activadas.
## Evidência e limites
- Confirmada a ancestralidade dos commits F1 `1f2990e4`, plano `6e302db6`, F2 `91c18573`, F3 `7691cab3`, F4 `e431de8b` e F5 `528a2597`.
@@ -30,23 +32,23 @@
| Denúncias e tratamento no Dashboard, auditoria e destaques administrativos | aprovado com ressalvas | F5 cobre isolamento, fundamento cifrado, tarefas existentes, versões e responsável. Destaque só rascunho/arquivo, sem promoção/pagamentos. Visual pendente. |
| Isolamento de utilizadores, páginas, APIs, anexos, mapas e CSV | aprovado com ressalvas | Suites F1–F5 e regressão nativa; leitura pública limitada à projecção, comprovativos internos, sem originais externos. Não equivale a teste de intrusão independente nem validação visual. |
| Preservação da versão pública durante edição | aprovado com ressalvas | Contrato sintético de Ofertas e nova regressão nativa aprovados; não existe ainda versão pública operacional de Ofertas para demonstrar o percurso real. |
| Escrever uma vez: recuperar a descrição no contexto de investimento | bloqueado | Novo teste reproduz `project_summary` ausente embora `description` esteja guardada. `InvestorContext::reference` lê resumo canónico/`summary`, sem aproveitar a descrição da Oferta. |
| Reutilização de valores financeiros autorizados na viabilidade, por localização e factores do negócio | não verificado | Os campos privados são recuperáveis, mas não foi localizado um contrato específico que os leve ao simulador, explicite pressupostos territoriais e preserve alterações deliberadas. Não inferir viabilidade a partir de receita/caixa/preço. Falta prova integrada e decisão de mapeamento quando necessária. |
| Consulta da Oferta → acção no mesmo dossier → simulação → relatório → recuperação | bloqueado | Detalhe público apenas liga a regresso/denúncia e mapa; consulta normal fechada. O contexto nativo admite referências privadas autorizadas, mas não demonstra este percurso completo de aquisição/investimento. |
| Escrever uma vez: recuperar a descrição no contexto de investimento | aprovado | Correcção QA específica da transformação; descrição recuperada, resumo existente e alterações deliberadas preservados, sem truncar a origem nem duplicar registos. Prova e recibo no relatório de correcção. |
| Contexto territorial e hipóteses económicas autorizadas até à viabilidade | aprovado com ressalvas | Localização persistida até ao rascunho, custos de renda/transporte declarados pelo investidor, cálculo e comparação sensível aos custos. Valores empresariais confidenciais não são transferidos; não constitui conclusão automática sobre rentabilidade nem valida todos os cenários fiscais. |
| Percurso controlado QA Oferta → mesmo dossier → simulação → relatório → recuperação | aprovado com ressalvas | Criação/submissão/revisão/aprovação reais no teste, projecção pública sintética isolada, referência privada autorizada, rascunho/resultados no dossier e exportadores HTML/Word existentes. Abertura pública operacional continua bloqueada e visual pendente. |
| Restantes oportunidades, pesquisa e recursos afectados | aprovado com ressalvas | Nova regressão 14/227 passou. Não constitui declaração de ausência de defeitos em todo o portal ou em serviços externos não ensaiados. |
| Desktop/móvel e acessibilidade funcional | não verificado | Última evidência do conector: `The browser access host is unavailable`; não foi repetido o ciclo de reinícios. HTML compilado não substitui conferência visual. |
| Entrega real de notificações por email | não verificado | Serviço existente e permissões foram testados com transporte de teste; SMTP/fila/recepção real não ensaiados. Não foram enviados emails operacionais. |
## Bloqueantes e ressalvas para decisão
1. **Integração funcional:** corrigir e repetir o teste da descrição; demonstrar, sem exposição pública, o percurso de contexto/dossier/viabilidade/relatório/recuperação. Definir o mapeamento autorizado de dados económicos e pressupostos da localização antes de transportar informação confidencial; preservar proveniência, moeda e escolhas deliberadas. Não confundir preço de aquisição com investimento total nem dados históricos com previsão.
1. **Integração funcional — corrigida em QA na passagem autorizada posterior:** o teste da descrição e a prova controlada contexto/dossier/viabilidade/relatório/recuperação passaram. A projecção limita-se a campos comuns autorizados; dados económicos confidenciais não são transferidos. Moeda e custos do investimento são hipóteses explícitas, sem confundir preço de aquisição com investimento total ou dados históricos com previsão. Qualquer novo contrato de transferência confidencial continua fora desta correcção.
2. **Publicação:** implementar e testar um publicador controlado, se autorizado em nova instrução. A aceitação do consumidor sintético não autoriza remover as barreiras existentes. Instalação privada de avaliação e abertura pública são decisões separadas.
3. **Jurídico:** faltam exemplar/versão/referência de aprovação do NDA, tipos admissíveis de comprovativo imobiliário, política final de validade e condições/avisos aplicáveis à divulgação. Não são motivos para invalidar as provas técnicas já aceites; bloqueiam os comportamentos dependentes.
4. **Imagens:** comprovar processamento seguro e derivadas numa instalação compatível, incluindo scan, metadados, acesso e limite de seis. Não instalar extensões nem declarar galeria pronta nesta auditoria.
5. **Visual e correio:** realizar conferência real desktop/móvel e entrega de email autorizada em ambiente de teste. Permanecem não verificados, sem solicitar que o utilizador repita autenticações nesta passagem.
6. **Reversão:** um retorno cego à F4 é inseguro como plano: a versão F4 de `PrivateOfferConfidentiality::approvedHash` não verifica `OfferAvailability`, enquanto a F5 o faz. Uma retirada de ciclo zero pode conservar o mesmo hash editorial. A reversão deve preservar a recusa de leitura de ofertas retiradas/expiradas e os históricos; não assumir que repor código antigo mantém as novas protecções.
Não foram corrigidas estas lacunas porque a instrução autoriza exclusivamente auditoria e preparação do plano. O teste falhado fica explícito como critério de aceitação por satisfazer; não foi convertido numa aprovação por caracterização da falha.
Na auditoria original não foram corrigidas lacunas por estar autorizada exclusivamente a auditoria. A falha histórica foi conservada; a autorização posterior permitiu a correcção delimitada agora comprovada, sem alterar os restantes bloqueios.
## Plano de instalação controlada — não executado
@@ -61,4 +63,4 @@ Não foram corrigidas estas lacunas porque a instrução autoriza exclusivamente
## Resultado e próxima acção
Auditoria documental, hashes e regressões delimitadas concluídos com achados. **O módulo não está integralmente aceite para o percurso de investimento solicitado.** Próxima acção recomendada: autorizar a correcção da continuidade Oferta → contexto/dossier e a sua prova de viabilidade, mantendo os bloqueios públicos; depois concluir as verificações dependentes e submeter o pacote à autorização de homologação. Nenhuma instalação efectuada.
Auditoria original concluída com achados; a continuidade Oferta → contexto/dossier e a prova controlada de viabilidade/relatório foram corrigidas em QA por autorização posterior. **O módulo mantém ressalvas e bloqueios de publicação operacional, jurídicos e verificações pendentes.** Próxima acção: apreciar o recibo de correcção e as dependências restantes antes de qualquer autorização de homologação. Nenhuma instalação efectuada.
@@ -0,0 +1,72 @@
# Ofertas → Investimento — correcção da continuidade em QA
14/11/2026. Base: auditoria `ed2fd86f`, preservando F1–F5 até `528a2597`. Autorização exclusiva para investigar/corrigir a continuidade e demonstrá-la em QA. Sem instalação, publicação efectiva, alterações operacionais, migrações ou intervenção em `mne`.
## Causa e correcção
A reprodução antes da alteração confirmou o resultado anterior: dois testes, um aprovado e um falhado; o contexto não tinha `project_summary`. A descrição estava correctamente persistida em `Opportunity.description`, no mesmo identificador. A autorização já permitia ao dono consultar a Oferta. A perda ocorria na **transformação/recuperação**: `InvestorContext::reference('opportunity:…')` só projectava `canonical.executive_summary` ou `summary`, enquanto a preparação privada das Ofertas escreve `description`.
O adaptador existente passa a usar a descrição guardada como alternativa para `project_summary`, **apenas para Ofertas privadas já autorizadas**, conservando qualquer resumo existente não vazio. O título vem dos valores correntes da mesma preparação e a referência aponta para a rota específica da Oferta. Não se criam registos, fontes, relações ou contratos paralelos. Não se trunca a descrição, não se grava uma cópia na Oportunidade e não se altera a origem ao confirmar dados no dossier.
O reconciliador existente continua a apresentar prévia e confirmação, com versões, hash, proveniência e preservação de edições deliberadas. O teste integrado confirma que um resumo deliberadamente alterado no dossier não é substituído numa nova prévia da mesma Oferta.
## Campos e confidencialidade
| Informação | Continuidade autorizada |
| --- | --- |
| Identificador/chave da Oportunidade, versão, hash, título e referência | Contrato existente `opportunity:<id>`; mesmo cadastro e dossier, sem duplicação. |
| Descrição | Alternativa para resumo apenas no contexto privado autorizado; resumo existente prevalece. Descrição longa preservada. Não é uma autorização de divulgação pública. |
| Referências administrativas e sector comum já cadastrado | Contratos existentes preservados; a localização do ensaio mantém o mesmo identificador até ao contexto e ao rascunho financeiro. Não se deduz cobertura, disponibilidade ou benefício fiscal. |
| Moeda, custos e pressupostos do investimento | Declarações explícitas do investidor no percurso existente. A moeda/preço da Oferta não se convertem automaticamente em moeda/investimento total do projecto. |
| `private_offer.business`, receitas, fluxo de caixa, preço privado, contacto, razões e comprovativos | Não são copiados pelo adaptador para o contexto, relatórios ou exportações. Os campos de sector guardados dentro da informação empresarial confidencial também não são promovidos ao sector comum por inferência. |
| Informação pública sintética | Apenas projecção pública separada e expressamente construída no teste. A descrição privada e os valores empresariais não passam para essa projecção. |
| Leitura por NDA e autorização do vendedor | Mantém o âmbito próprio: não concede acesso à referência privada do contexto. Novo teste confirma recusa 404 mesmo depois das duas condições satisfeitas; revogação e demais testes F4 permanecem válidos. |
Não se introduziu um novo percurso de transferência de dados confidenciais. Qualquer futura reutilização desses dados precisa de autorização e contrato próprios; esta correcção não alarga direitos de consulta ou exportação.
## Prova integrada reproduzível
`OfferInvestmentAcceptanceAuditTest` usa exclusivamente `offers_phase1_20261114_test`, loopback:55441, com guardas e rollback existentes. Cria Oferta de empresa e referência provincial sintéticas, grava/recupera, associa contacto autorizado, submete, efectua revisão/aprovação por outra conta e conserva `status=draft`/`published_at=null`.
O mesmo teste comprova catálogo normal vazio e injecta `OfferPublicCatalogue(true)` exclusivamente no contentor do teste, sujeito à guarda real da base isolada. A projecção aprovada/publicada sintética tem resumo público próprio; não se altera qualquer interruptor de publicação operacional. A referência privada continua sujeita a `PortalProfileAccess`.
O dono selecciona a mesma Oportunidade num dossier, confere a prévia e confirma. Descrição e referência provincial chegam ao prefill existente do simulador. O investidor declara moeda e pressupostos de renda/transporte para esse local; guarda o rascunho financeiro, recupera-o e devolve os resultados com confirmação da revisão do contexto. Uma nova autenticação da mesma conta recupera o mesmo dossier e rascunho; outra conta é recusada. O teste confirma uma Oportunidade e um dossier, sem duplicações.
O teste frontend consome o JSON produzido pela prova PHP, recupera o snapshot e executa os calculadores existentes: receita mensal sintética 10.000, renda 2.000 e transporte 1.000 produzem saldo operacional estimado de 7.000 MZN; custos alternativos expressamente declarados de 5.000 e 3.000 produzem 2.000 MZN. Custos fixos 3.000, preço unitário 100 e custo variável 50 dão ponto de equilíbrio de 60 unidades. Estas hipóteses demonstram sensibilidade aos custos do local; **não atribuem uma tarifa provincial, uma margem real da empresa ou rentabilidade garantida**. Receita histórica confidencial da Oferta não é usada como previsão.
O mesmo teste gera o relatório privado HTML e Word pelo exportador existente, reabre o Word e verifica descrição/localização, recuperação do snapshot e ausência dos valores confidenciais e contacto. Os artefactos são sintéticos, não documentos de negócio nem uma aceitação visual.
Durante a preparação da prova, a referência provincial inicial não tinha a proveniência exigida pelo catálogo público; foi corrigida apenas a fixture isolada, reutilizando o contrato de `OpportunitySearchFixture`. O pedido do simulador foi ajustado à lista de campos já aceite e à confirmação da revisão de contexto. Nenhuma guarda foi desactivada para passar o ensaio.
## Ficheiros e verificação
- Produto: `app/Services/InvestorContext.php`, projecção delimitada de descrição/título/referência.
- Testes PHP: `OfferInvestmentAcceptanceAuditTest.php`, persistência, resumo existente, descrição longa, dossier/rascunho/resultados e isolamento; `PrivateOfferConfidentialityTest.php`, acesso por NDA sem concessão de acesso ao contexto privado.
- Teste frontend: `tests/frontend/offer-investment-continuity.test.mjs`, cálculo com pressupostos territoriais declarados e relatório real com reabertura do Word.
- Documentação: matriz RF-MN-003, manual e matriz final de aceitação existentes; este recibo explica a correcção sem apagar a prova histórica da falha.
Resultados e comandos finais: `outputs/offers-investment-continuity-20261114/qa-result.json`. Não somar passagens sobrepostas nem apresentar os 59/780 históricos como executados novamente nesta correcção.
| Verificação final | Resultado |
| --- | --- |
| Continuidade Oferta/contexto e percurso integrado | 4 testes / 62 asserções aprovados |
| Confidencialidade, incluindo NDA sem acesso ao contexto privado | 11 / 161 aprovados |
| Percursos existentes de investimento/dossiers | 4 / 192 aprovados |
| Gestão nativa de Oportunidades e versão pública | 3 / 46 aprovados |
| **Passagem PHP integrada** | **22 testes / 461 asserções; zero erros/falhas** |
| Regressão frontend de plano financeiro e relatórios | 41 testes aprovados |
| Prova frontend ligada ao JSON da passagem PHP final | 1 teste aprovado; HTML e Word gerados/reabertos |
| PHPStan completo, TypeScript e Pint | Aprovados; zero erros PHPStan |
| Lint/formatação do novo teste frontend; diferenças Git | Aprovados |
Comando PHP final: `php vendor/bin/phpunit tests/Feature/OfferInvestmentAcceptanceAuditTest.php tests/Feature/PrivateOfferConfidentialityTest.php tests/Feature/InvestorPathTest.php tests/Feature/OpportunityManagementTest.php --log-junit outputs/offers-investment-continuity-20261114/regression.xml`, com a configuração sintética/guardada acima. Frontend: `node --test tests/frontend/investor-dossier-report.test.mjs tests/frontend/business-financial-plan.test.mjs`, seguido de `node --test tests/frontend/offer-investment-continuity.test.mjs`. Análise: `php vendor/bin/phpstan analyse --no-progress --memory-limit=1G`, `npm.cmd run types:check`, Pint nos três ficheiros PHP e `vp fmt --check`/`vp lint` no novo teste. A compilação QA anterior é reutilizada porque não mudou código frontend do produto; os testes PHP preservaram os assets existentes.
## Saldo para aceitação
**Corrigido em QA:** descrição na fronteira Oferta/contexto, preservação de resumo e edições deliberadas, recuperação no mesmo dossier, ensaio de localização/pressupostos → cálculo → relatórios HTML/Word → recuperação, sem passagem de dados financeiros confidenciais.
**Mantém-se bloqueado:** publicação efectiva operacional; NDA real enquanto faltar o exemplar aprovado; decisões jurídicas/prazo definitivos identificados na auditoria. A injecção de projecção sintética não substitui esse fecho.
**Por verificar:** conferência visual desktop/móvel, processamento de derivadas de imagens/galeria no ambiente compatível e entrega real de email. A simulação demonstrada é uma análise controlada com hipóteses declaradas, não uma prova de todos os cenários fiscais e factores de rentabilidade do portal.
Nenhuma instalação efectuada ou autorizada por este recibo. A eventual instalação aguarda nova autorização expressa, com o plano de cópia/restauro e reversão compatível já documentado.
@@ -0,0 +1 @@
{"tool":"phpunit","result":"passed","tests":4,"passed":4,"assertions":55,"duration_ms":25651}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="Tests\Feature\OfferInvestmentAcceptanceAuditTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" tests="4" assertions="55" errors="0" failures="0" skipped="0" time="25.647841">
<testcase name="test_saved_offer_reuses_its_description_in_the_existing_investment_context" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="41" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="8" time="6.981317"/>
<testcase name="test_native_context_options_do_not_disclose_another_owners_private_offer" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="55" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="7" time="2.323249"/>
<testcase name="test_private_description_is_not_truncated_and_does_not_replace_an_existing_summary" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="67" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="5" time="2.097139"/>
<testcase name="test_approved_offer_continues_in_same_dossier_location_analysis_saved_result_and_report_input" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="80" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="35" time="14.246136"/>
</testsuite>
</testsuites>
@@ -0,0 +1,49 @@
✔ financial forecast reconciles revenue, finance sources, working capital and monthly cash (25.013875ms)
✔ opening payable installments reduce cash without changing profit and retain unpaid balances (4.962292ms)
✔ opening payable schedule caps cent rounding at the remaining balance (0.817417ms)
✔ monthly unit schedules drive revenue, costs, inventory and scenario volume (11.337458ms)
✔ monthly unit schedules must match the horizon and contain valid quantities (2.604459ms)
✔ monthly prices and unit costs affect revenue, consumption and scenarios (4.282666ms)
✔ monthly money schedules cover the horizon and reject invalid amounts (2.09225ms)
✔ monthly operating and payroll expense schedules remain separate from payments (3.289125ms)
✔ monthly operating schedules require full horizon, exact cents and current snapshot version (18.074292ms)
✔ losses and negative closing cash remain visible instead of being clamped to zero (3.032334ms)
✔ principal repayments affect cash but are excluded from profit expenses (2.658625ms)
✔ depreciation allocation reconciles exactly over useful life (1.653209ms)
✔ missing currency, products, invalid numbers and invalid horizons are explicit errors (1.136708ms)
✔ versioned plan snapshots restore assumptions and reject unsupported or oversized data (3.091333ms)
✔ business plan serializer refuses snapshots above the server storage limit (0.349541ms)
✔ sales invoicing and receipts follow an optional declared collection lag (0.986292ms)
✔ scheduled collection percentage keeps unpaid invoices in receivables with exact rounding (0.648709ms)
✔ sales recovery applies only to overdue invoices and never exceeds open balances (1.041541ms)
✔ opening receivables affect cash only when a month and collection percentage are declared (0.967791ms)
✔ opening receivable installments reconcile to the initial balance across months (1.622958ms)
✔ opening receivable installments cap independently rounded cents at the available balance (2.369541ms)
✔ inventory purchases, consumption, closing stock and stock shortfall reconcile independently of payments (0.385875ms)
✔ inventory lead time separates monthly orders, receipts, shortages and goods still in transit (0.667ms)
✔ monthly inventory purchase schedule reconciles variable orders, lead times, receipts and transit (1.319042ms)
✔ monthly variable purchase payments affect cash independently from inventory orders and profit (0.722625ms)
✔ product inventory schedules reconcile independently and do not transfer stock between products (6.993292ms)
✔ supplier payment terms drive purchase cash flow and expose amounts due beyond the horizon (2.183292ms)
✔ supplier order installments reconcile exact cents, cash dates and unallocated balances (0.841625ms)
✔ business assumptions survive the existing encrypted-draft snapshot split and restore (0.779958ms)
✔ presentation currency conversion requires and reports its rate, date, and declared source (1.636458ms)
✔ CSV marks presentation currency conversion as an unverified user planning assumption (1.087958ms)
✔ business plan export carries its assumptions, formulas, period, currency, and matching result (0.447959ms)
✔ business plan CSV preserves declared monthly product schedules (0.446125ms)
✔ CSV export exposes the sales collection assumption and outstanding receivable (0.320583ms)
✔ business plan CSV neutralizes formula-like labels and preserves negative financial values (0.323792ms)
✔ financial scenarios expose user-declared changes without creating tax values (5.431667ms)
✔ one dossier groups stage evidence, SWOT, fiscal and all preserved APIEX versions without modifying sources (5.195167ms)
✔ cross-dossier or cross-opportunity preparation is rejected and an ambiguous scope requires selection (1.404625ms)
✔ financial results from a different opportunity do not enter the consolidated report or its index (0.736833ms)
✔ the consolidated report reuses loan, break-even, financing and budget outputs with currency and historical state (14.746791ms)
✔ the existing Word and print exporters retain dossier identity, pending stages, source and history, escaping active HTML (131.861583ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 964.394334
@@ -0,0 +1,663 @@
{
"path": {
"funding_selections": [],
"preparation_action": null,
"private_investment": null,
"case": {
"id": "f71a2833-c182-43ba-862d-234e4b64739b",
"title": "Synthetic acquisition acceptance",
"version": 5,
"closed": false
},
"journey_id": "a0851462-2b7f-47dc-bb0f-59c2a5004e36",
"definition_id": "61000919-0000-4000-8000-000000000001",
"definition_version": 1,
"source_sha256": "aa13c53138ec15cd808de43d9dc2fc6808da86943d09d3352c9dd0944bdae364",
"phases": {
"1": "Ideia e viabilidade",
"2": "Formaliza\u00e7\u00e3o e dossier",
"3": "Licenciamento e terra",
"4": "Financiamento",
"5": "Execu\u00e7\u00e3o e acompanhamento"
},
"steps": [
{
"code": "S01",
"phase": 1,
"title": "Sistematizar a ideia",
"branch": null,
"sequence": 1,
"dependencies": [],
"source_paragraph": 22,
"id": "9f12bd89-308a-4e94-9cc3-df4822dea011",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": false
},
{
"code": "S02",
"phase": 1,
"title": "An\u00e1lise preliminar",
"branch": null,
"sequence": 2,
"dependencies": [],
"source_paragraph": 23,
"id": "369a3135-119c-42d2-83d2-6b5e3ff9e659",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": true
},
{
"code": "S03",
"phase": 1,
"title": "Decis\u00e3o sobre a viabilidade",
"branch": null,
"sequence": 3,
"dependencies": [],
"source_paragraph": 24,
"id": "0480e4d9-2265-4921-8338-83a0f728d49f",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": true
},
{
"code": "S04",
"phase": 1,
"title": "Estruturar o investimento",
"branch": null,
"sequence": 4,
"dependencies": [],
"source_paragraph": 25,
"id": "882503f7-e30d-4c8d-b7ca-fb157bda2dc7",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": true
},
{
"code": "S05",
"phase": 2,
"title": "Constitui\u00e7\u00e3o e registo",
"branch": null,
"sequence": 5,
"dependencies": [],
"source_paragraph": 26,
"id": "c3c87f28-f26a-42d0-80e9-74b412ae2ca5",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": false
},
{
"code": "S06",
"phase": 2,
"title": "Preparar o dossier",
"branch": null,
"sequence": 6,
"dependencies": [],
"source_paragraph": 27,
"id": "57865d31-78f3-4961-a065-49efd4ff15e8",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": false
},
{
"code": "S07",
"phase": 2,
"title": "Submeter quando aplic\u00e1vel",
"branch": null,
"sequence": 7,
"dependencies": {
"submit": [
"S06"
]
},
"source_paragraph": 28,
"id": "18dd4fa9-c309-4031-88eb-f4c27ab69e47",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": {
"submit": [
"S06"
]
},
"context_review_required": false
},
{
"code": "S08",
"phase": 3,
"title": "Licenciamento pr\u00e9vio",
"branch": null,
"sequence": 8,
"dependencies": [],
"source_paragraph": 29,
"id": "65d81f1c-da8d-4f2b-af0d-a6a049cbe54b",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": true
},
{
"code": "S09",
"phase": 3,
"title": "Necessidade de terra",
"branch": null,
"sequence": 9,
"dependencies": [],
"source_paragraph": 30,
"id": "b1a99d72-5ca6-4cb1-8a0a-7e35f1890392",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": false
},
{
"code": "S10",
"phase": 3,
"title": "Regulariza\u00e7\u00e3o da terra",
"branch": "land_needed",
"sequence": 10,
"dependencies": [],
"source_paragraph": 31,
"id": "162637cf-d578-4a75-87b9-369996834843",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "undetermined",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": false
},
{
"code": "S11",
"phase": 3,
"title": "Confirmar regulariza\u00e7\u00e3o aplic\u00e1vel",
"branch": null,
"sequence": 11,
"dependencies": [],
"source_paragraph": 32,
"id": "2c7d86f2-7426-4c6c-8ed4-cae8b6e68c99",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": false
},
{
"code": "S12",
"phase": 4,
"title": "Necessidade e estrutura de financiamento",
"branch": null,
"sequence": 12,
"dependencies": [],
"source_paragraph": 33,
"id": "7bdb6b9b-324d-49e2-a133-5865b423c830",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": true
},
{
"code": "S13",
"phase": 4,
"title": "Dilig\u00eancia e negocia\u00e7\u00e3o financeira",
"branch": "external_funding",
"sequence": 13,
"dependencies": [],
"source_paragraph": 34,
"id": "60bdbf70-8bbb-4771-abda-4f82b3f3e53e",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "undetermined",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": true
},
{
"code": "S14",
"phase": 4,
"title": "Decis\u00e3o do financiador",
"branch": "external_funding",
"sequence": 14,
"dependencies": [],
"source_paragraph": 35,
"id": "69a00105-ed81-4679-a0de-536b995d6b3e",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "undetermined",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": true
},
{
"code": "S15",
"phase": 4,
"title": "Fecho e desembolso",
"branch": "external_funding",
"sequence": 15,
"dependencies": {
"execute": [
"S14"
]
},
"source_paragraph": 36,
"id": "4b70d527-bdfd-4792-9fd9-a3cc7c397f28",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "undetermined",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": {
"execute": [
"S14"
]
},
"context_review_required": true
},
{
"code": "S16",
"phase": 5,
"title": "Implementa\u00e7\u00e3o",
"branch": null,
"sequence": 16,
"dependencies": [],
"source_paragraph": 37,
"id": "9c838949-2537-4d81-b848-adf20be1b789",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": false
},
{
"code": "S17",
"phase": 5,
"title": "Inspec\u00e7\u00f5es, testes e licen\u00e7a de opera\u00e7\u00e3o",
"branch": null,
"sequence": 17,
"dependencies": [],
"source_paragraph": 38,
"id": "8864abd3-fc9a-40f8-ad5f-22a84d87e10a",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": true
},
{
"code": "S18",
"phase": 5,
"title": "In\u00edcio de opera\u00e7\u00e3o",
"branch": null,
"sequence": 18,
"dependencies": {
"operate": [
"S17"
]
},
"source_paragraph": 39,
"id": "dc259b0e-2c14-422d-8309-dbd817f17ca4",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": {
"operate": [
"S17"
]
},
"context_review_required": false
},
{
"code": "S19",
"phase": 5,
"title": "Acompanhamento recorrente",
"branch": null,
"sequence": 19,
"dependencies": [],
"source_paragraph": 40,
"id": "22afbc5a-daec-473b-bd69-d90605d6d604",
"version": 1,
"state": "not_started",
"saved_state": "not_started",
"applicability": "applicable",
"reason": "",
"detail": {
"state": "not_started"
},
"preparation_allowed": true,
"action_gaps": [],
"context_review_required": false
}
],
"facts": {
"land_needed": "unknown",
"external_funding": "unknown",
"viability": "unknown"
},
"fact_reasons": [],
"context_opportunity": null,
"opportunity_options": [],
"fintech": null,
"canWrite": true,
"canReview": false,
"next": {
"code": "S01",
"title": "Sistematizar a ideia",
"action": "Preparar",
"href": "#S01"
},
"investment_context": {
"opportunity_choices": [],
"preparation_documents": [],
"preparation_snapshot": null,
"revision": 1,
"selection": {
"opportunity": {
"key": "opportunity:4",
"kind": "opportunity",
"id": "4",
"title": "Synthetic acquisition acceptance",
"version": 5,
"href": "\/minhas-oportunidades\/ofertas\/4",
"fields": {
"project_title": "Synthetic acquisition acceptance",
"project_summary": "Existing company description to reuse once in investment preparation.",
"province": "Synthetic province",
"province_id": "22fd440c-f744-4647-86a6-348c01c194d7",
"location": "Synthetic province"
},
"documents": [],
"visibility": "private",
"state": "approved",
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
}
},
"fields": {
"project_title": "Synthetic acquisition acceptance",
"investment_regime": "general",
"special_zone_id": "",
"currency": "MZN",
"project_summary": "Existing company description to reuse once in investment preparation.",
"province": "Synthetic province",
"province_id": "22fd440c-f744-4647-86a6-348c01c194d7",
"location": "Synthetic province"
},
"provenance": {
"project_title": {
"kind": "dossier",
"id": "f71a2833-c182-43ba-862d-234e4b64739b",
"version": 2
},
"currency": {
"kind": "process_declaration",
"actor_id": 8,
"context_revision": 1
},
"project_summary": {
"key": "opportunity:4",
"kind": "opportunity",
"id": "4",
"title": "Synthetic acquisition acceptance",
"version": 5,
"href": "\/minhas-oportunidades\/ofertas\/4",
"documents": [],
"visibility": "private",
"state": "approved",
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
},
"province": {
"key": "opportunity:4",
"kind": "opportunity",
"id": "4",
"title": "Synthetic acquisition acceptance",
"version": 5,
"href": "\/minhas-oportunidades\/ofertas\/4",
"documents": [],
"visibility": "private",
"state": "approved",
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
},
"province_id": {
"key": "opportunity:4",
"kind": "opportunity",
"id": "4",
"title": "Synthetic acquisition acceptance",
"version": 5,
"href": "\/minhas-oportunidades\/ofertas\/4",
"documents": [],
"visibility": "private",
"state": "approved",
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
},
"location": {
"key": "opportunity:4",
"kind": "opportunity",
"id": "4",
"title": "Synthetic acquisition acceptance",
"version": 5,
"href": "\/minhas-oportunidades\/ofertas\/4",
"documents": [],
"visibility": "private",
"state": "approved",
"sha256": "b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e"
}
},
"review_steps": {
"S02": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
},
"S03": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
},
"S04": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
},
"S08": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
},
"S12": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
},
"S13": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
},
"S14": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
},
"S15": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
},
"S17": {
"revision": 1,
"reason": "O contexto mudou; reveja os pressupostos e a evid\u00eancia aplic\u00e1vel."
}
},
"dependency_hash": "1311d8539ea6a08237003a36712145618834461dd63b510658b5419e95af46dd",
"updated_at": "2026-11-14T16:35:56+00:00",
"updated_by": 8,
"requirements_assessment": {
"version": "context-requirements-20261023",
"requirements": [],
"fiscal_notice": "Localiza\u00e7\u00e3o, corredor, cluster ou projecto \u00e2ncora n\u00e3o activam benef\u00edcios fiscais. O simulador mant\u00e9m as condi\u00e7\u00f5es, fonte, per\u00edodo e \u00e2mbito pr\u00f3prios; condi\u00e7\u00f5es ausentes permanecem indeterminadas."
},
"complements": []
},
"results": [
{
"id": "a18f1c7a-73bd-41d1-a28b-6c19b1bf3fb8",
"source_key": "3b31db9e28c3d4b33f05812d6292408541b24e06f720861cbea1350cd9b0b1c2",
"draft_id": "38f76a31-44ea-4815-b523-d99046475518",
"draft_version": 1,
"snapshot_sha256": "aa09f1d3ea78bbc8eccd5336548bd39466946799030e5459fe1b42e89587d87b",
"financial_snapshot": "{\"version\":1,\"data\":{\"budgetPeriod\":\"Mensal\",\"loan\":{\"principal\":\"100000\",\"rate\":\"0\",\"months\":\"24\",\"rateType\":\"nominal\",\"periodMonths\":\"1\",\"gracePeriods\":\"0\",\"upfrontFee\":\"0\",\"recurringFee\":\"0\"},\"breakEven\":{\"fixed\":\"3000\",\"price\":\"100\",\"variable\":\"50\"},\"funding\":{\"total\":\"100000\",\"secured\":\"30000\"},\"incomes\":[{\"label\":\"Synthetic scenario revenue\",\"amount\":\"10000\"}],\"expenses\":[{\"label\":\"Declared rent at synthetic province\",\"amount\":\"2000\"},{\"label\":\"Declared transport at synthetic province\",\"amount\":\"1000\"}]}}",
"context_opportunity": null,
"currency": "MZN",
"engine": "financial-calculators:v1",
"kind": "local_calculation_inputs",
"context_state": "current",
"context_revision": 1,
"context_dependency_hash": "1311d8539ea6a08237003a36712145618834461dd63b510658b5419e95af46dd",
"recorded_at": "2026-11-14T16:35:59+00:00"
}
],
"requests": [],
"documents": [],
"fiscal_reports": [],
"land_report": null,
"preparation_report": null,
"company_reports": [],
"zone_contexts": [],
"sector_contexts": [],
"sector_references": [],
"content_sources": [],
"tasks": [],
"services": [],
"tools": {
"funding": "\/fundos?kind=institution&location_mode=physical&context=f71a2833-c182-43ba-862d-234e4b64739b",
"simulator": "\/simulador-fiscal?context=f71a2833-c182-43ba-862d-234e4b64739b&draft=38f76a31-44ea-4815-b523-d99046475518",
"preparation": null,
"company": "\/investimentos\/f71a2833-c182-43ba-862d-234e4b64739b\/forma-juridica",
"desk": "\/balcao-virtual\/dossiers\/f71a2833-c182-43ba-862d-234e4b64739b",
"guidance": "\/balcao-virtual\/dossiers\/f71a2833-c182-43ba-862d-234e4b64739b\/orientacao",
"library": "\/documentos?context=f71a2833-c182-43ba-862d-234e4b64739b",
"opportunities": "\/oportunidades?context=f71a2833-c182-43ba-862d-234e4b64739b",
"territory": "\/zonas?context=f71a2833-c182-43ba-862d-234e4b64739b",
"partners": "\/parcerias\/painel?context=f71a2833-c182-43ba-862d-234e4b64739b",
"land": "\/investidor\/terra-duat\/novo?context=f71a2833-c182-43ba-862d-234e4b64739b"
}
},
"saved": {
"project_title": "Synthetic acquisition acceptance",
"currency": "MZN",
"project_summary": "Existing company description to reuse once in investment preparation.",
"province_id": "22fd440c-f744-4647-86a6-348c01c194d7",
"journey_type": "investor",
"purpose": "simulate_only",
"context_applicability_snapshot": "{\"version\":1,\"context_revision\":1,\"consumer_opportunity\":null,\"portfolio_origin\":null,\"zone\":null,\"provenance\":{\"project_title\":{\"kind\":\"dossier\",\"id\":\"f71a2833-c182-43ba-862d-234e4b64739b\",\"version\":2},\"currency\":{\"kind\":\"process_declaration\",\"actor_id\":8,\"context_revision\":1},\"project_summary\":{\"key\":\"opportunity:4\",\"kind\":\"opportunity\",\"id\":\"4\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/4\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e\"},\"province\":{\"key\":\"opportunity:4\",\"kind\":\"opportunity\",\"id\":\"4\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/4\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e\"},\"province_id\":{\"key\":\"opportunity:4\",\"kind\":\"opportunity\",\"id\":\"4\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/4\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e\"},\"location\":{\"key\":\"opportunity:4\",\"kind\":\"opportunity\",\"id\":\"4\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5,\"href\":\"\\\/minhas-oportunidades\\\/ofertas\\\/4\",\"documents\":[],\"visibility\":\"private\",\"state\":\"approved\",\"sha256\":\"b175039f4c49e40278436403a6aea19f218e03c3d9cb602583f0394caecd1f4e\"}},\"opportunity_choices\":[],\"complements\":[],\"requirements_assessment\":{\"version\":\"context-requirements-20261023\",\"requirements\":[],\"fiscal_notice\":\"Localiza\u00e7\u00e3o, corredor, cluster ou projecto \u00e2ncora n\u00e3o activam benef\u00edcios fiscais. O simulador mant\u00e9m as condi\u00e7\u00f5es, fonte, per\u00edodo e \u00e2mbito pr\u00f3prios; condi\u00e7\u00f5es ausentes permanecem indeterminadas.\"},\"path\":{\"definition_id\":\"61000919-0000-4000-8000-000000000001\",\"definition_version\":1,\"source_sha256\":\"aa13c53138ec15cd808de43d9dc2fc6808da86943d09d3352c9dd0944bdae364\",\"steps\":[{\"code\":\"S10\",\"title\":\"Regulariza\u00e7\u00e3o da terra\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":false},{\"code\":\"S13\",\"title\":\"Dilig\u00eancia e negocia\u00e7\u00e3o financeira\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true},{\"code\":\"S14\",\"title\":\"Decis\u00e3o do financiador\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true},{\"code\":\"S15\",\"title\":\"Fecho e desembolso\",\"state\":\"not_started\",\"applicability\":\"undetermined\",\"reason\":\"\",\"context_review_required\":true}]},\"land_preparation\":{\"need\":\"unknown\",\"reason\":\"\"},\"field_provenance\":{\"project_title\":{\"kind\":\"dossier\",\"version\":2},\"currency\":{\"kind\":\"process_declaration\",\"context_revision\":1},\"project_summary\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"province\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"province_id\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5},\"location\":{\"kind\":\"opportunity\",\"title\":\"Synthetic acquisition acceptance\",\"version\":5}},\"preparation\":null,\"fields\":{\"investment_regime\":\"general\",\"currency\":\"MZN\"},\"notice\":\"Refer\u00eancias e declara\u00e7\u00f5es para prepara\u00e7\u00e3o; sem reconhecimento autom\u00e1tico de elegibilidade ou benef\u00edcio.\"}",
"financial_tools_snapshot": "{\"version\":1,\"data\":{\"budgetPeriod\":\"Mensal\",\"loan\":{\"principal\":\"100000\",\"rate\":\"0\",\"months\":\"24\",\"rateType\":\"nominal\",\"periodMonths\":\"1\",\"gracePeriods\":\"0\",\"upfrontFee\":\"0\",\"recurringFee\":\"0\"},\"breakEven\":{\"fixed\":\"3000\",\"price\":\"100\",\"variable\":\"50\"},\"funding\":{\"total\":\"100000\",\"secured\":\"30000\"},\"incomes\":[{\"label\":\"Synthetic scenario revenue\",\"amount\":\"10000\"}],\"expenses\":[{\"label\":\"Declared rent at synthetic province\",\"amount\":\"2000\"},{\"label\":\"Declared transport at synthetic province\",\"amount\":\"1000\"}]}}"
}
}
@@ -0,0 +1,24 @@
{
"scope": "Synthetic investor-declared costs at the selected location; no automatic profitability or fiscal entitlement",
"province_id": "22fd440c-f744-4647-86a6-348c01c194d7",
"budget": {
"incomeTotal": "10000.00",
"expenseTotal": "3000.00",
"operatingBalance": "7000.00",
"status": "estimated"
},
"breakEven": {
"contributionPerUnit": "50.00",
"breakEvenUnits": "60",
"breakEvenRevenue": "6000.00",
"status": "estimated"
},
"alternative": {
"incomeTotal": "10000.00",
"expenseTotal": "8000.00",
"operatingBalance": "2000.00",
"status": "estimated"
},
"source_offer": "opportunity:4",
"case_id": "f71a2833-c182-43ba-862d-234e4b64739b"
}
@@ -0,0 +1 @@
{"tool":"phpstan","result":"passed","errors":0}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,102 @@
{
"base": "ed2fd86f",
"phase5_preserved": "528a2597",
"environment": {
"database": "offers_phase1_20261114_test",
"host": "127.0.0.1",
"port": 55441,
"fixtures": "synthetic only"
},
"cause": "Transformation: description persisted, project_summary read only canonical/summary. Existing identifiers and permissions preserved.",
"php_suites": [
{
"name": "Tests\\Feature\\OfferInvestmentAcceptanceAuditTest",
"tests": 4,
"assertions": 62,
"errors": 0,
"failures": 0
},
{
"name": "Tests\\Feature\\PrivateOfferConfidentialityTest",
"tests": 11,
"assertions": 161,
"errors": 0,
"failures": 0
},
{
"name": "Tests\\Feature\\InvestorPathTest",
"tests": 4,
"assertions": 192,
"errors": 0,
"failures": 0
},
{
"name": "Tests\\Feature\\OpportunityManagementTest",
"tests": 3,
"assertions": 46,
"errors": 0,
"failures": 0
}
],
"php_totals": {
"tests": 22,
"assertions": 461,
"errors": 0,
"failures": 0
},
"frontend_regression": {
"tests": 41,
"passed": 41
},
"integrated_frontend_report": {
"tests": 1,
"passed": 1,
"artifacts": [
"private-integrated-report.html",
"private-integrated-report.docx",
"location-analysis.json"
]
},
"static": {
"phpstan": "full analysis passed, zero errors",
"types": "tsc --noEmit passed",
"pint": "affected PHP passed",
"frontend_lint_format": "new mjs passed",
"build": "unchanged frontend product sources: accepted QA build reused; no rebuild",
"git_diff_check": "passed"
},
"field_policy": "Common authorised title/description/administrative references only; no private_offer financial/contact/price/proof transfer. NDA grant does not grant private context access.",
"publication": "default consumer closed; injected synthetic consumer guarded by actual isolated database",
"matrix": "RF-MN-003 only; other rows unchanged",
"pending": [
"real NDA and legal decisions",
"effective public writer/activation",
"desktop/mobile visual acceptance",
"image processing derivatives",
"real email delivery"
],
"installations": false,
"operational_migrations": false,
"mne_changes": false,
"accepted_59_780_repeated": false,
"source_sha256": {
"app/Services/InvestorContext.php": "cdae33a63d796b7e9862c949711b2c83f2482990d3d77806591a594214313fff",
"tests/Feature/OfferInvestmentAcceptanceAuditTest.php": "d91112cfccc08f45afb9a6c317ead1a5d03f97221a606ad123ca590a4b401b5b",
"tests/Feature/PrivateOfferConfidentialityTest.php": "5beb77c609baf3c78768bfbe0906b5d5b8701cd3266ca9ead8c3198a30fb5bc6",
"tests/frontend/offer-investment-continuity.test.mjs": "b37acc9d1dbf900aa8a5555666142bc088e73245b60c5aa8f6d0f7baa1a7655a"
},
"evidence_sha256": {
"focused.log": "0943af2982f42d60848e480008f0e045ebda4da944d16408be0ae944b94428bc",
"focused.xml": "e57322452c4e009e40dc0b3d5bbfa1da1c3add6afb74a8006143efbc904ee2cb",
"frontend-regression.log": "ddbeb6fe3365cb70fd4c0e5eafaf4d7919ead5910415f6aed9167acfb43e315d",
"integrated-report-input.json": "60d8d8f46821da56f47183f57e6869cb64160cf5f5c2fdd03eab2acdd24c5014",
"location-analysis.json": "83729ad333ace4507e3827b97f84905327083682fb83eecbb05847e4f59a5dff",
"phpstan.log": "c10f74fb840de989bba7e9c2dfee7f8419b5d2ac0cc0daf18a4a187f6530aa43",
"private-integrated-report.docx": "df76a52f6f1d1ee990702aefa8b4c3d3604de2305be840be88c77fc3dbdf546e",
"private-integrated-report.html": "3221f94d4df400f4edf5e9b262dbf4a13d6f80d6b6fd58b6f8a09ed384141367",
"regression.log": "13480527b87e724e7c9687131ee0d0f611f1c9a6ac719612440dcfac8ccfd543",
"regression.xml": "2785f34fb05e01b945fb1b7a61ba3e9d6c061d5c7d3cf09b189188b58a1b2e37",
"report-test.log": "71d866136fca227770e0c73347750f6597224c9420672c6ab9dfcebd9f80c60d",
"types.log": "b59cdb3ff539a29c8ae6c37772ecc1a93a5266c49959d408f16b17487b7ac755"
}
}
@@ -0,0 +1 @@
{"tool":"phpunit","result":"passed","tests":22,"passed":22,"assertions":461,"duration_ms":135835}
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="CLI Arguments" tests="22" assertions="461" errors="0" failures="0" skipped="0" time="135.825495">
<testsuite name="Tests\Feature\OfferInvestmentAcceptanceAuditTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" tests="4" assertions="62" errors="0" failures="0" skipped="0" time="28.758459">
<testcase name="test_saved_offer_reuses_its_description_in_the_existing_investment_context" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="41" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="8" time="6.650455"/>
<testcase name="test_native_context_options_do_not_disclose_another_owners_private_offer" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="55" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="7" time="2.800221"/>
<testcase name="test_private_description_is_not_truncated_and_does_not_replace_an_existing_summary" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="67" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="5" time="2.095158"/>
<testcase name="test_approved_offer_continues_in_same_dossier_location_analysis_saved_result_and_report_input" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OfferInvestmentAcceptanceAuditTest.php" line="80" class="Tests\Feature\OfferInvestmentAcceptanceAuditTest" classname="Tests.Feature.OfferInvestmentAcceptanceAuditTest" assertions="42" time="17.212625"/>
</testsuite>
<testsuite name="Tests\Feature\PrivateOfferConfidentialityTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" tests="11" assertions="161" errors="0" failures="0" skipped="0" time="100.821485">
<testcase name="test_confirmed_specific_fields_save_recover_and_bind_the_editorial_version" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="109" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="9" time="7.598624"/>
<testcase name="test_property_fields_are_optional_and_categories_and_unknown_fields_are_enforced" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="120" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="7" time="5.093540"/>
<testcase name="test_six_photos_limit_includes_restoration_and_alternate_routes_and_cleans_failed_uploads" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="131" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="15" time="10.676252"/>
<testcase name="test_upload_validation_and_quarantine_do_not_grant_publication_or_read_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="173" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="9" time="7.387037"/>
<testcase name="test_proof_bytes_are_encrypted_and_only_a_scoped_internal_validator_can_read_them" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="187" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="14" time="5.185126"/>
<testcase name="test_nda_absence_keeps_access_closed_and_acceptance_alone_never_grants_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="209" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="13" time="8.880152"/>
<testcase name="test_both_conditions_exact_versions_revocation_and_audit_are_required_on_every_read" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="227" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="18" time="9.641431"/>
<testcase name="test_nda_version_changes_withdrawal_session_revocation_and_private_revision_close_access" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="259" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="35" time="21.375187"/>
<testcase name="test_nda_integrity_and_authority_loss_are_rechecked_without_exposing_the_company" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="299" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="14" time="9.466717"/>
<testcase name="test_compiled_confidential_page_has_no_company_data_before_both_conditions" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="323" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="11" time="6.025318"/>
<testcase name="test_photo_metadata_and_original_are_bound_to_the_editorial_fingerprint" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\PrivateOfferConfidentialityTest.php" line="337" class="Tests\Feature\PrivateOfferConfidentialityTest" classname="Tests.Feature.PrivateOfferConfidentialityTest" assertions="16" time="9.492101"/>
</testsuite>
<testsuite name="Tests\Feature\InvestorPathTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" tests="4" assertions="192" errors="0" failures="0" skipped="0" time="4.861569">
<testcase name="test_existing_empty_desk_draft_can_explicitly_reuse_current_contextual_summary_without_overwriting_data" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" line="53" class="Tests\Feature\InvestorPathTest" classname="Tests.Feature.InvestorPathTest" assertions="29" time="0.792451"/>
<testcase name="test_two_independent_investments_recover_tool_evidence_support_and_internal_tasks_without_external_calls" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" line="83" class="Tests\Feature\InvestorPathTest" classname="Tests.Feature.InvestorPathTest" assertions="107" time="2.252891"/>
<testcase name="test_permissions_stale_versions_and_real_dependencies_do_not_block_independent_preparation" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" line="131" class="Tests\Feature\InvestorPathTest" classname="Tests.Feature.InvestorPathTest" assertions="35" time="0.995810"/>
<testcase name="test_applicability_changes_preserve_old_branch_evidence_and_legacy_workflow" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\InvestorPathTest.php" line="154" class="Tests\Feature\InvestorPathTest" classname="Tests.Feature.InvestorPathTest" assertions="21" time="0.820416"/>
</testsuite>
<testsuite name="Tests\Feature\OpportunityManagementTest" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" tests="3" assertions="46" errors="0" failures="0" skipped="0" time="1.383981">
<testcase name="test_installation_requires_management_permission_and_current_proofs" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="20" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="6" time="0.332180"/>
<testcase name="test_incomplete_draft_recovers_and_soft_delete_restore_preserves_identity" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="32" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="21" time="0.720887"/>
<testcase name="test_closed_version_edits_do_not_change_public_values_classification_or_relations" file="C:\laragon\www\portal-investidor-qa-editorial-20261113\tests\Feature\OpportunityManagementTest.php" line="60" class="Tests\Feature\OpportunityManagementTest" classname="Tests.Feature.OpportunityManagementTest" assertions="19" time="0.330915"/>
</testsuite>
</testsuite>
</testsuites>
@@ -0,0 +1,9 @@
✔ persisted synthetic offer crosses the dossier boundary into location assumptions, calculation and private report (130.377542ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 596.953708
@@ -0,0 +1,3 @@
> types:check
> tsc --noEmit
@@ -5,17 +5,21 @@ namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\InvestorContext;
use App\Services\OfferPublicCatalogue;
use App\Services\OpportunityManagement;
use App\Services\PortalProfileAdministration;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Tests\TestCase;
/** Acceptance audit only: no change to the product's contracts or access rules. */
/** Synthetic acceptance regression for the existing investment-context boundary. */
final class OfferInvestmentAcceptanceAuditTest extends TestCase
{
use RefreshDatabase;
private function prepare(): array
private function prepare(array $location = []): array
{
$this->withoutVite();
config(['portal.profile_access_enabled' => true]);
@@ -28,7 +32,7 @@ final class OfferInvestmentAcceptanceAuditTest extends TestCase
'title' => 'Synthetic acquisition acceptance', 'description' => 'Existing company description to reuse once in investment preparation.',
'private_offer' => ['price_mode' => 'amount', 'amount' => '120000.00', 'currency' => 'MZN',
'business' => ['revenue' => '60000.00', 'cash_flow' => '20000.00', 'currency' => 'MZN']],
'reason' => 'Synthetic final acceptance integration audit only.'];
'reason' => 'Synthetic final acceptance integration audit only.', ...$location];
$record = $this->actingAs($owner)->postJson('/minhas-oportunidades/ofertas/rascunhos', $payload)->assertCreated()->json('record');
return [$owner, $record, $payload];
@@ -42,6 +46,10 @@ final class OfferInvestmentAcceptanceAuditTest extends TestCase
$this->assertSame($payload['title'], $reference['fields']['project_title']);
$this->assertSame($payload['description'], $reference['fields']['project_summary'] ?? null,
'Write once/read always: the existing description must not disappear at the investment-context boundary.');
$this->assertSame('/minhas-oportunidades/ofertas/'.$record['id'], $reference['href']);
$this->assertArrayNotHasKey('private_offer', $reference);
$this->assertArrayNotHasKey('revenue', $reference['fields']);
$this->assertArrayNotHasKey('currency', $reference['fields']);
}
public function test_native_context_options_do_not_disclose_another_owners_private_offer(): void
@@ -55,4 +63,113 @@ final class OfferInvestmentAcceptanceAuditTest extends TestCase
$this->assertNotContains('opportunity:'.$record['id'], array_column(app(InvestorContext::class)->options(), 'key'));
$this->getJson('/minhas-oportunidades/ofertas/'.$record['id'])->assertForbidden();
}
public function test_private_description_is_not_truncated_and_does_not_replace_an_existing_summary(): void
{
[$owner, $record, $payload] = $this->prepare();
$description = str_repeat('Synthetic complete description. ', 400);
$this->postJson('/minhas-oportunidades/ofertas/'.$record['id'].'/guardar', [
'etag' => $record['etag'], 'description' => $description, 'reason' => $payload['reason'],
])->assertOk();
$this->assertSame(trim($description), app(InvestorContext::class)->reference('opportunity:'.$record['id'])['fields']['project_summary']);
DB::table('opportunities')->where('id', $record['id'])->update(['summary' => 'Deliberate existing summary']);
$this->assertSame('Deliberate existing summary', app(InvestorContext::class)->reference('opportunity:'.$record['id'])['fields']['project_summary']);
$this->assertSame(trim($description), Opportunity::findOrFail($record['id'])->description);
}
public function test_approved_offer_continues_in_same_dossier_location_analysis_saved_result_and_report_input(): void
{
Mail::fake();
config(['editorial_workflow.enabled' => true, 'investor-path.enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
$country = (string) Str::uuid();
$province = (string) Str::uuid();
foreach ([[$country, 'country', null], [$province, 'province', $country]] as [$id, $type, $parent]) {
DB::table('geo_locations')->insert(['id' => $id, 'code' => 'SYNTHETIC-'.$id, 'name' => 'Synthetic '.$type,
'location_type' => $type, 'parent_id' => $parent, 'data_classification' => 'public',
// Same isolated reference contract as OpportunitySearchFixture; never an operational import.
'source_system' => 'cartografia-workbook',
'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id]);
}
[$owner, $record, $payload] = $this->prepare(['province_id' => $province]);
$contact = (string) Str::uuid();
DB::table('contacts')->insert(['id' => $contact, 'full_name' => 'Synthetic private contact', 'source_system' => 'portal-user-contact', 'external_id' => 'user:'.$owner->id]);
$url = '/minhas-oportunidades/ofertas/'.$record['id'];
$this->postJson($url.'/guardar', ['etag' => $record['etag'], 'private_offer' => [...$payload['private_offer'], 'contact_id' => $contact], 'reason' => $payload['reason']])->assertOk();
$this->postJson($url.'/submeter', ['etag' => app(OpportunityManagement::class)->etag(Opportunity::findOrFail($record['id'])), 'confirm' => true])->assertOk()->assertJsonPath('record.state', 'submitted');
$reviewer = User::factory()->withTwoFactor()->create();
app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), [
'user_id' => $reviewer->id, 'role' => 'validator', 'responsibilities' => ['validation_coordinator'],
'opportunity_types' => ['business_sale'], 'source' => 'synthetic-acceptance-audit',
]);
$this->actingAs($reviewer)->withSession(['portal.mfa_actor' => $reviewer->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $reviewer->fresh()->portal_access_revision]);
foreach (['start_review', 'approve'] as $action) {
$this->postJson($url.'/revisao', ['etag' => app(OpportunityManagement::class)->etag(Opportunity::findOrFail($record['id'])),
'action' => $action, 'reason' => 'Synthetic independent review for investment continuity.'])->assertOk();
}
$offer = Opportunity::findOrFail($record['id']);
$public = ['title' => 'Synthetic public acquisition', 'summary' => 'Approved synthetic public summary only.', 'category' => 'business_sale',
'province_id' => $province, 'district_id' => null, 'price_mode' => 'negotiable', 'amount' => null, 'currency' => null];
$hash = OfferPublicCatalogue::projectionHash($public);
$context = $offer->workflow_context;
$context['offer_publication'] = ['contract' => OfferPublicCatalogue::CONTRACT, 'qa_fixture' => true, 'revision' => 1, 'payload' => $public,
'approval' => ['state' => 'approved', 'hash' => $hash, 'actor_id' => $reviewer->id, 'at' => now()->subMinutes(2)->toIso8601String()],
'publication' => ['state' => 'published', 'hash' => $hash, 'actor_id' => User::factory()->create()->id, 'at' => now()->subMinute()->toIso8601String()]];
$offer->forceFill(['workflow_context' => $context])->save();
$this->getJson('/api/v1/offers')->assertOk()->assertJsonPath('results.total', 0);
$this->app->instance(OfferPublicCatalogue::class, new OfferPublicCatalogue(true));
$response = $this->getJson('/api/v1/offers/'.$offer->id)->assertOk()->assertJsonPath('data.summary', $public['summary']);
$this->assertStringNotContainsString($payload['description'], $response->getContent());
$this->assertStringNotContainsString('60000.00', $response->getContent());
$this->assertNull($offer->fresh()->published_at);
$this->actingAs($owner);
$case = $this->postJson('/investimentos', ['title' => $payload['title'], 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
$view = $this->getJson('/investimentos/'.$case.'/contexto')->assertOk()->json();
$body = ['version' => $view['case']['version'], 'selection' => ['opportunity' => 'opportunity:'.$offer->id], 'declarations' => ['currency' => 'MZN']];
$plan = $this->postJson('/investimentos/'.$case.'/contexto/previa', $body)->assertOk()->json();
$this->assertSame($payload['description'], $plan['fields']['project_summary']);
$this->postJson('/investimentos/'.$case.'/contexto/confirmar', [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk();
$view = $this->getJson('/investimentos/'.$case.'/contexto')->assertOk()->json();
$prefill = $view['prefill']['simulator'];
$this->assertSame($province, $prefill['province_id']);
$this->assertSame($payload['description'], $prefill['project_summary']);
$this->assertStringNotContainsString('60000.00', json_encode($view, JSON_THROW_ON_ERROR));
$financial = ['version' => 1, 'data' => ['budgetPeriod' => 'Mensal',
'loan' => ['principal' => '100000', 'rate' => '0', 'months' => '24', 'rateType' => 'nominal', 'periodMonths' => '1', 'gracePeriods' => '0', 'upfrontFee' => '0', 'recurringFee' => '0'],
'breakEven' => ['fixed' => '3000', 'price' => '100', 'variable' => '50'], 'funding' => ['total' => '100000', 'secured' => '30000'],
'incomes' => [['label' => 'Synthetic scenario revenue', 'amount' => '10000']],
'expenses' => [['label' => 'Declared rent at synthetic province', 'amount' => '2000'], ['label' => 'Declared transport at synthetic province', 'amount' => '1000']]]];
$draft = $this->postJson('/api/v1/simulation-drafts', ['context' => $case, 'personal_snapshot' => [],
'economic_snapshot' => [...array_intersect_key($prefill, array_flip(['purpose', 'journey_type', 'project_title', 'project_summary', 'currency', 'province_id', 'context_applicability_snapshot'])),
'financial_tools_snapshot' => json_encode($financial, JSON_THROW_ON_ERROR)]],
['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$saved = $this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$case)->assertOk()->json('data.economic_snapshot');
$this->assertSame($payload['description'], $saved['project_summary']);
$path = $this->getJson('/investimentos/'.$case)->assertOk()->json();
$this->postJson('/investimentos/'.$case.'/resultados', ['version' => $path['case']['version'], 'draft_id' => $draft, 'draft_version' => 1,
'context_revision' => $path['investment_context']['revision'], 'context_reviewed' => true])->assertOk();
auth()->logout();
$this->actingAs(User::findOrFail($owner->id));
$path = $this->getJson('/investimentos/'.$case)->assertOk()->json();
$this->assertSame($draft, $path['results'][0]['draft_id']);
$this->assertSame($payload['description'], $path['investment_context']['fields']['project_summary']);
$this->assertSame(1, Opportunity::count());
$this->assertSame(1, DB::table('desk_cases')->count());
$this->assertStringNotContainsString('60000.00', json_encode($path, JSON_THROW_ON_ERROR));
$output = base_path('outputs/offers-investment-continuity-20261114');
if (! is_dir($output)) {
mkdir($output, 0777, true);
}
file_put_contents($output.'/integrated-report-input.json', json_encode(['path' => $path, 'saved' => $saved], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$body = ['version' => $path['case']['version'], 'selection' => ['opportunity' => 'opportunity:'.$offer->id],
'declarations' => ['project_summary' => 'Deliberate investment-specific summary']];
$plan = $this->postJson('/investimentos/'.$case.'/contexto/previa', $body)->assertOk()->json();
$this->assertSame('Deliberate investment-specific summary', $plan['fields']['project_summary']);
$this->postJson('/investimentos/'.$case.'/contexto/confirmar', [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk();
$view = $this->getJson('/investimentos/'.$case.'/contexto')->assertOk()->json();
$this->postJson('/investimentos/'.$case.'/contexto/previa', ['version' => $view['case']['version'],
'selection' => ['opportunity' => 'opportunity:'.$offer->id]])->assertOk()->assertJsonPath('fields.project_summary', 'Deliberate investment-specific summary');
$this->assertSame($payload['description'], $offer->fresh()->description);
$this->actingAs(User::factory()->create())->getJson('/investimentos/'.$case)->assertForbidden();
$this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$case)->assertNotFound();
}
}
@@ -5,6 +5,7 @@ namespace Tests\Feature;
use App\Jobs\ScanDocumentForMalware;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\InvestorContext;
use App\Services\OpportunityManagement;
use App\Services\PortalProfileAdministration;
use App\Services\PrivateOfferConfidentiality;
@@ -19,6 +20,7 @@ use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\Support\CleanScannerFixture;
use Tests\TestCase;
@@ -238,6 +240,14 @@ final class PrivateOfferConfidentialityTest extends TestCase
$data = $this->login($interested)->getJson($url.'/confidencial')->assertOk()->assertJsonPath('business.revenue', '789.01')->getContent();
$this->assertStringNotContainsString('PRIVATE-CONTACT-CANARY', $data);
$this->assertStringNotContainsString('PRIVATE-DESCRIPTION-CANARY', $data);
// NDA permission is a bounded read, not permission to copy the private
// Opportunity into another investment context or its downstream reports.
try {
app(InvestorContext::class)->reference('opportunity:'.$record->id);
$this->fail('An NDA grant must not grant private investment-context access.');
} catch (HttpException $error) {
$this->assertSame(404, $error->getStatusCode());
}
$this->login($this->actor())->getJson($url.'/confidencial')->assertForbidden();
$this->login($owner)->postJson($url.'/confidencialidade', $this->command($record, $nda, 'revoke', ['interested_user_id' => $interested->id]))->assertOk();
$this->login($interested)->getJson($url.'/confidencial')->assertForbidden();
@@ -0,0 +1,94 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, writeFileSync } from 'node:fs';
import JSZip from 'jszip';
import {
calculateBudget,
calculateBreakEven,
restoreFinancialToolsSnapshot,
} from '../../resources/js/lib/financial-calculators.ts';
import { createInvestorDossierReport } from '../../resources/js/lib/investor-dossier-report.ts';
import {
createProjectDossierPrintHtml,
generateProjectDossierDocx,
} from '../../resources/js/lib/project-dossier-report.ts';
await test('persisted synthetic offer crosses the dossier boundary into location assumptions, calculation and private report', async () => {
// Produced by the guarded PHP integration test, never by an operational export.
const directory = new URL(
'../../outputs/offers-investment-continuity-20261114/',
import.meta.url,
);
const { path, saved } = JSON.parse(
readFileSync(
new URL('integrated-report-input.json', directory),
'utf8',
),
);
assert.equal(path.case.title, 'Synthetic acquisition acceptance');
assert.equal(
path.investment_context.fields.project_summary,
saved.project_summary,
);
assert.equal(path.investment_context.fields.province_id, saved.province_id);
const data = restoreFinancialToolsSnapshot(
path.results[0].financial_snapshot,
);
assert.ok(data);
assert.deepEqual(
data,
restoreFinancialToolsSnapshot(saved.financial_tools_snapshot),
);
const budget = calculateBudget(
data.incomes.map((row) => row.amount),
data.expenses.map((row) => row.amount),
);
const breakEven = calculateBreakEven(
data.breakEven.fixed,
data.breakEven.price,
data.breakEven.variable,
);
assert.equal(budget.operatingBalance, '7000.00');
assert.equal(breakEven.breakEvenUnits, '60');
// An explicit alternative location-cost assumption, not an inferred provincial tariff.
const alternative = calculateBudget(
data.incomes.map((row) => row.amount),
['5000', '3000'],
);
assert.equal(alternative.operatingBalance, '2000.00');
assert.deepEqual(
data.expenses.map((row) => row.amount),
['2000', '1000'],
);
const report = createInvestorDossierReport(path, '2026-11-14T12:00:00Z');
const html = await createProjectDossierPrintHtml(report, [], false);
assert.ok(html.includes(saved.project_summary));
assert.ok(html.includes('Synthetic province'));
assert.ok(!html.includes('60000.00'));
assert.ok(!html.includes('Synthetic private contact'));
const docx = await generateProjectDossierDocx(report, [], false);
const bytes = Buffer.from(await docx.arrayBuffer());
const zip = await JSZip.loadAsync(bytes);
const xml = await zip.file('word/document.xml').async('string');
assert.ok(xml.includes(saved.project_summary));
assert.ok(xml.includes('Synthetic province'));
assert.ok(!xml.includes('60000.00'));
writeFileSync(new URL('private-integrated-report.html', directory), html);
writeFileSync(new URL('private-integrated-report.docx', directory), bytes);
writeFileSync(
new URL('location-analysis.json', directory),
JSON.stringify(
{
scope: 'Synthetic investor-declared costs at the selected location; no automatic profitability or fiscal entitlement',
province_id: saved.province_id,
budget,
breakEven,
alternative,
source_offer: path.investment_context.selection.opportunity.key,
case_id: path.case.id,
},
null,
2,
),
);
});