feat: prioritise missing preparation documents and retain associations
This commit is contained in:
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user