feat: prioritise missing preparation documents and retain associations

This commit is contained in:
2026-10-29 22:56:02 +02:00
parent d662e63469
commit 181a9883b3
5 changed files with 296 additions and 49 deletions
@@ -2119,3 +2119,14 @@ Incremento de apresentação sobre `fb9a363e`, nos requisitos existentes V2-07/V
Conferência dirigida: seis testes frontend existentes passaram; 22 verificações SSR cobriram os três modos, rótulos de dados em falta, preparação inactiva/ausente, ligação de continuação, histórico preservado e imutabilidade da origem. Tipos, formatação, lint e compilação QA passaram. Provas anteriores reutilizadas. Recibo no campo `adaptive_summary_increment` de `local-validation-20261029.json`.
**Conferência visual pendente pela equipa:** na candidata, abrir o resumo de preparação activo, conferir situação/próxima acção/dados em falta sem expandir detalhes, continuar pelo formulário e consultar histórico/fontes; verificar também preparação inactiva e ausência de dados em falta. Registar revisão, perfil, percurso e resultado nesta matriz. SSR não comprova apresentação no navegador. Sem instalação ou avanço de main; este incremento não encerra os requisitos transversais.
### 29/10/2026 — percurso adaptativo: documentos em falta e recuperação das associações
Continuação sobre `d662e634`, nos requisitos existentes V2-07/V2-10 e na decisão adaptativa da Tempo de 23/10. O resumo distingue documentos obrigatórios por associar, documentos associados cujo conteúdo continua por conferir e exigências documentais ainda por determinar; o histórico sem aplicação não é contado como pendência. Com dados completos e âmbito/período cobertos, a acção de continuação conduz à secção existente de documentos. Nos restantes estados mantém o destino do formulário. São decisões de apresentação/navegação; não criam novas condições de negócio.
No editor, os documentos ainda por associar mantêm-se visíveis; os já associados ficam em «Documentos já associados — consultar ou alterar». A retirada deliberada permanece por confirmar na comparação de impacto e não é substituída pelo identificador guardado. O agrupamento usa o estado recuperado, conservando o controlo durante a edição. Dados, documentos originais, histórico, regras fiscais, backend, schema e permissões preservados.
Passaram oito testes frontend dirigidos: seis existentes e dois novos para encaminhamento conforme contexto, distinção entre associação/aprovação/indeterminação, controlos recuperados editáveis, retirada explícita, recuperação sem mutação do original e estado desactivado dos controlos. Tipos, lint, formatação e compilação QA passaram. Recibo e hashes no campo `adaptive_documents_increment` de `local-validation-20261029.json`; provas integrais aceites não foram repetidas.
**Visual continua pendente, execução pela equipa:** na candidata, conferir contagens e destino com/sem dados em falta; abrir documentos já associados, alterar/retirar uma associação, comparar impacto antes de confirmar e verificar recuperação. Incluir também âmbito/período não cobertos e exigências indeterminadas, sem interpretar associação como aprovação. Registar revisão, perfil, percurso, esperado/observado e evidência nesta matriz. Sem instalação ou avanço de main; requisitos transversais continuam parciais.
@@ -1216,5 +1216,43 @@
"baseline_evidence": "Reused accepted PHP/frontend baseline; no full-suite rerun.",
"visual_acceptance": "Pending team execution and recording against candidate; SSR does not replace browser acceptance.",
"installation": "Not triggered; main and active release unchanged."
},
"adaptive_documents_increment": {
"recorded_at": "2026-10-29T20:56:01.348797+00:00",
"base_commit": "d662e6346942400e18bedffbfed0d92e017428f9",
"requirements": [
"V2-07",
"V2-10",
"Tempo adaptive journey decision 2026-10-23"
],
"behavior": [
"Preparation summary separately counts missing required documents, associated required documents with content still to review, and requirements whose applicability remains undetermined; not-applicable history is excluded from these counts.",
"When enabled context is complete within its covered period and scope, missing required documents lead directly to the existing document section. Otherwise the existing question-section destination is retained.",
"Editor prioritises required documents still to associate and groups saved associated documents in editable details. Explicit null detachment overrides the saved link until impact comparison; no original association is mutated."
],
"checks": {
"directed_frontend_tests": 8,
"new_cases": 2,
"existing_cases_reused": 6,
"types": "passed",
"lint": "passed",
"format": "passed",
"qa_build": "passed"
},
"source_sha256": {
"resources/js/components/private-investment-preparation.tsx": "b1e55f3d80d0ae5cba4ab822ce9b564880973d2d8ebfc35bac0d91acaeea5d72",
"tests/frontend/investor-usability-components.test.mjs": "fc07a7ff63e122392250825f9825ec6f1e0e09b5c62c6fb900d63846d07f2d3d"
},
"evidence": {
"outputs/validation-20261029/adaptive-documents-tests.log": "cbf08ccb0897a7432a9153ee6488490a1b5c2a59c80465f459c3786765c0ee6f",
"outputs/validation-20261029/adaptive-documents-types.log": "82962651e47f564de653ad2883f8fd34e7dae4b4468e4ac459cb3331962a3879",
"outputs/validation-20261029/adaptive-documents-lint.log": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"outputs/validation-20261029/adaptive-documents-format-check.log": "3996b5359cbd4c71d9610a4af35fdc8af9aa6fb4ab4bfcec9a15df2190b9c0cd",
"outputs/validation-20261029/adaptive-documents-build.log": "e76ce07b405e5dc28e9529f0d49b82bf8d4b9fa0e2ccf0574897b888369c4525",
"outputs/validation-20261029/adaptive-documents-build-exit.json": "124fd50cb6d223cdd18fedc166d00af7254199b6e6dedf9be82f71db87af6d01"
},
"preservation": "Presentation and navigation only. Existing impact comparison, document data/history, backend, schema, permissions and fiscal rules unchanged. Historical accepted suites reused, not rerun in full.",
"visual_acceptance": "Pending team execution and recording on candidate; component tests do not constitute visual acceptance.",
"installation": "Not triggered; main and active release unchanged."
}
}
@@ -7656,3 +7656,14 @@ Incremento de apresentação sobre `fb9a363e`, nos requisitos existentes V2-07/V
Conferência dirigida: seis testes frontend existentes passaram; 22 verificações SSR cobriram os três modos, rótulos de dados em falta, preparação inactiva/ausente, ligação de continuação, histórico preservado e imutabilidade da origem. Tipos, formatação, lint e compilação QA passaram. Provas anteriores reutilizadas. Recibo no campo `adaptive_summary_increment` de `local-validation-20261029.json`.
**Conferência visual pendente pela equipa:** na candidata, abrir o resumo de preparação activo, conferir situação/próxima acção/dados em falta sem expandir detalhes, continuar pelo formulário e consultar histórico/fontes; verificar também preparação inactiva e ausência de dados em falta. Registar revisão, perfil, percurso e resultado nesta matriz. SSR não comprova apresentação no navegador. Sem instalação ou avanço de main; este incremento não encerra os requisitos transversais.
### 29/10/2026 — percurso adaptativo: documentos em falta e recuperação das associações
Continuação sobre `d662e634`, nos requisitos existentes V2-07/V2-10 e na decisão adaptativa da Tempo de 23/10. O resumo distingue documentos obrigatórios por associar, documentos associados cujo conteúdo continua por conferir e exigências documentais ainda por determinar; o histórico sem aplicação não é contado como pendência. Com dados completos e âmbito/período cobertos, a acção de continuação conduz à secção existente de documentos. Nos restantes estados mantém o destino do formulário. São decisões de apresentação/navegação; não criam novas condições de negócio.
No editor, os documentos ainda por associar mantêm-se visíveis; os já associados ficam em «Documentos já associados — consultar ou alterar». A retirada deliberada permanece por confirmar na comparação de impacto e não é substituída pelo identificador guardado. O agrupamento usa o estado recuperado, conservando o controlo durante a edição. Dados, documentos originais, histórico, regras fiscais, backend, schema e permissões preservados.
Passaram oito testes frontend dirigidos: seis existentes e dois novos para encaminhamento conforme contexto, distinção entre associação/aprovação/indeterminação, controlos recuperados editáveis, retirada explícita, recuperação sem mutação do original e estado desactivado dos controlos. Tipos, lint, formatação e compilação QA passaram. Recibo e hashes no campo `adaptive_documents_increment` de `local-validation-20261029.json`; provas integrais aceites não foram repetidas.
**Visual continua pendente, execução pela equipa:** na candidata, conferir contagens e destino com/sem dados em falta; abrir documentos já associados, alterar/retirar uma associação, comparar impacto antes de confirmar e verificar recuperação. Incluir também âmbito/período não cobertos e exigências indeterminadas, sem interpretar associação como aprovação. Registar revisão, perfil, percurso, esperado/observado e evidência nesta matriz. Sem instalação ou avanço de main; requisitos transversais continuam parciais.
@@ -64,6 +64,23 @@ export function PreparationSummary({
caseId?: string;
}) {
if (!data) return null;
const missingDocuments = data.requirements.filter(
(requirement) =>
requirement.required && requirement.state === 'missing',
).length;
const associatedDocuments = data.requirements.filter(
(requirement) =>
requirement.required && requirement.state === 'associated',
).length;
const undeterminedDocuments = data.requirements.filter(
(requirement) => requirement.state === 'indeterminate',
).length;
const continueWithDocuments =
data.enabled &&
data.period_covered &&
data.scope_covered &&
data.missing_fields.length === 0 &&
missingDocuments > 0;
return (
<section
id="private-investment-preparation"
@@ -109,15 +126,32 @@ export function PreparationSummary({
.
</p>
)}
{missingDocuments > 0 && (
<p>Documentos por associar: {missingDocuments}.</p>
)}
{associatedDocuments > 0 && (
<p>
Documentos associados: {associatedDocuments} —
conteúdo por conferir.
</p>
)}
{undeterminedDocuments > 0 && (
<p>
Exigências documentais por determinar:{' '}
{undeterminedDocuments}.
</p>
)}
</div>
)}
{caseId && (
<p>
<Link
className="inline-block rounded border px-4 py-2 font-semibold"
href={`/investimentos/${caseId}/contexto/editar#preparation-questions`}
href={`/investimentos/${caseId}/contexto/editar#${continueWithDocuments ? 'preparation-documents' : 'preparation-questions'}`}
>
Preencher ou continuar o formulário
{continueWithDocuments
? 'Associar documentos do dossier'
: 'Preencher ou continuar o formulário'}
</Link>
</p>
)}
@@ -336,6 +370,39 @@ export function PreparationEditor({
data.mode !== 'authorization' ||
data.missing_fields.includes(q.key)),
);
const renderDocument = (r: Preparation['requirements'][number]) => (
<label className="grid gap-1" key={r.key}>
{r.label}
<select
className={input}
disabled={disabled}
value={
Object.hasOwn(links, r.key)
? (links[r.key] ?? '')
: (r.document_id ?? '')
}
onChange={(e) => onLink(r.key, e.target.value || null)}
>
<option value="">Por associar</option>
{documents.map((doc) => (
<option key={doc.document_id} value={doc.document_id}>
{doc.file_name} ·{' '}
{{
quarantined: 'Em quarentena',
active: 'Disponível',
deleted: 'Removido',
}[doc.status] ?? 'Estado por conferir'}
</option>
))}
</select>
<span>
{r.reason}{' '}
<a className="underline" href={r.source.url}>
{r.source.reference}
</a>
</span>
</label>
);
return (
<section
id="preparation-questions"
@@ -385,53 +452,29 @@ export function PreparationEditor({
substitui a análise do pedido.
</p>
{data.requirements
.filter((r) => r.required)
.map((r) => (
<label className="grid gap-1" key={r.key}>
{r.label}
<select
className={input}
disabled={disabled}
value={
Object.hasOwn(links, r.key)
? (links[r.key] ?? '')
: (r.document_id ?? '')
}
onChange={(e) =>
onLink(
r.key,
e.target.value || null,
)
}
>
<option value="">Por associar</option>
{documents.map((doc) => (
<option
key={doc.document_id}
value={doc.document_id}
>
{doc.file_name} ·{' '}
{{
quarantined:
'Em quarentena',
active: 'Disponível',
deleted: 'Removido',
}[doc.status] ??
'Estado por conferir'}
</option>
))}
</select>
<span>
{r.reason}{' '}
<a
className="underline"
href={r.source.url}
>
{r.source.reference}
</a>
</span>
</label>
))}
.filter(
(r) => r.required && r.state !== 'associated',
)
.map(renderDocument)}
{data.requirements.some(
(r) => r.required && r.state === 'associated',
) && (
<details>
<summary>
Documentos já associados — consultar ou
alterar
</summary>
<div className="space-y-3">
{data.requirements
.filter(
(r) =>
r.required &&
r.state === 'associated',
)
.map(renderDocument)}
</div>
</details>
)}
</div>
</>
)}
@@ -358,3 +358,147 @@ void test('investment stages expose readable next actions while internal codes a
assert.doesNotMatch(html, /<summary[^>]*>S01/);
assert.match(html, /Fonte e referência desta etapa/);
});
function preparationFixture() {
const requirement = (key, state, required = true) => ({
key,
label: key,
state,
required,
document_id: state === 'associated' ? 'saved-document' : null,
document: null,
reason: 'Synthetic QA',
source: { url: '/source', reference: 'Synthetic QA' },
});
return {
version: 'private-investment-preparation-2026-v1',
enabled: true,
mode: 'registration',
period_covered: true,
scope_covered: true,
missing_fields: [],
questions: [
{
key: 'investment_preparation',
label: 'Preparar?',
source: 'Synthetic QA',
},
],
next: 'Preparar documentos',
reason: [],
authority_source: { url: '/source' },
source: { url: '/source' },
requirements: [
requirement('MISSING', 'missing'),
requirement('ASSOCIATED', 'associated'),
requirement('UNDETERMINED', 'indeterminate', false),
requirement('HISTORICAL', 'not_applicable', false),
],
};
}
const preparationMocks = {
'@inertiajs/react': {
Link: ({ href, children }) =>
React.createElement('a', { href }, children),
},
'@/pages/investment/shared': { card: 'card', input: 'input' },
};
void test('preparation directs complete context to missing documents without treating association or uncertainty as approval', () => {
const Summary = load(
'../../resources/js/components/private-investment-preparation.tsx',
preparationMocks,
'PreparationSummary',
);
const data = preparationFixture();
const before = JSON.stringify(data);
const render = (overrides = {}) =>
renderToStaticMarkup(
React.createElement(Summary, {
data: { ...data, ...overrides },
caseId: 'synthetic',
}),
);
const visible = render().split('<details>')[0];
assert.match(visible, /Documentos por associar: 1/);
assert.match(visible, /Documentos associados: 1 — conteúdo por conferir/);
assert.match(visible, /Exigências documentais por determinar: 1/);
assert.match(visible, /#preparation-documents/);
assert.match(visible, /Associar documentos do dossier/);
for (const overrides of [
{ missing_fields: ['currency'] },
{ scope_covered: false },
{ period_covered: false },
{ enabled: false },
{
requirements: data.requirements.filter(
(r) => r.state !== 'missing',
),
},
]) {
assert.match(
render(overrides).split('<details>')[0],
/#preparation-questions/,
);
}
assert.equal(JSON.stringify(data), before);
});
void test('associated document controls remain editable in details and deliberate detachment survives rerender', () => {
const Editor = load(
'../../resources/js/components/private-investment-preparation.tsx',
preparationMocks,
'PreparationEditor',
);
const data = preparationFixture();
const links = {};
const props = {
data,
fields: { investment_preparation: 'yes' },
changes: {},
documents: [
{
document_id: 'saved-document',
file_name: 'ORIGINAL.pdf',
status: 'active',
},
],
links,
onChange() {},
onLink(key, value) {
links[key] = value;
},
disabled: false,
};
const tree = Editor(props);
const details = flat(tree).find(
(node) =>
node.type === 'details' &&
flat(node).some(
(child) =>
child.type === 'summary' &&
child.props.children ===
'Documentos já associados — consultar ou alterar',
),
);
assert.ok(details);
assert.ok(!details.props.open);
const existingSelect = flat(details).find((node) => node.type === 'select');
assert.equal(existingSelect.props.value, 'saved-document');
assert.equal(flat(tree).filter((node) => node.type === 'select').length, 2);
existingSelect.props.onChange({ target: { value: '' } });
assert.deepEqual(links, { ASSOCIATED: null });
const rerender = Editor(props);
assert.equal(
flat(rerender)
.filter((node) => node.type === 'select')
.every((node) => node.props.value === ''),
true,
);
assert.equal(data.requirements[1].document_id, 'saved-document');
const disabled = Editor({ ...props, disabled: true });
assert.equal(
flat(disabled)
.filter((node) => node.type === 'select')
.every((node) => node.props.disabled),
true,
);
});