Prepare L3 D refund lifecycle and grouped C D QA delivery with preserved credits

This commit is contained in:
2026-10-25 14:47:03 +02:00
parent b7a745e64f
commit 9e2d90c880
17 changed files with 466 additions and 13 deletions
@@ -83,8 +83,9 @@ class StoreSimulationFormDraftRequest extends FormRequest
'economic_snapshot.published_iva_scope' => ['nullable', 'string', 'in:yes,no'],
'economic_snapshot.monthly_iva_settings' => ['nullable', 'string', 'max:2000'],
'economic_snapshot.monthly_iva_snapshot' => ['nullable', 'string', 'max:60000'],
// Preserve operation books and explicit history as the draft grows.
'economic_snapshot.modelo_a_snapshot' => ['nullable', 'string', 'json', 'max:4000000'],
// Preserve accepted books and explicit history through the L3 refund increment.
// This is a bounded transport choice, not a business limit on fiscal records.
'economic_snapshot.modelo_a_snapshot' => ['nullable', 'string', 'json', 'max:6000000'],
'economic_snapshot.project_team_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.project_execution_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.project_risk_responses_snapshot' => ['nullable', 'string', 'max:30000'],
@@ -26775,5 +26775,90 @@
],
"schema_changes": false,
"active_writes": false
},
"l3_d_qa_20261025": {
"status": "L3-D concluído em QA no âmbito operacional demonstrado;preparado comC,sem instalação",
"matrix_anchor": "AUD-SIM-INT02",
"installed_revision": "109cb0fe",
"installed_receipt": "3aa851a4",
"C_QA_preserved": "b7a745e6",
"implemented": {
"allocation": "Reutiliza25de resultadoAB congelado e afectação explícita:26=25−27. Parcela27positiva,não excede25;apenas26 segue para o próximo mês por adopção expressa. Origem antiga/histórica não financia um novo pedido se a afectação actual mudou. Reportes dependentes indeterminados precisam de reconciliação;reporte efectivo já usado em declaração posterior não é reafectado retroactivamente.",
"lifecycle": "Preparação→apresentado fora doInvesteMoz→decisão pendente/integral/parcial/indeferida→recebimentos identificados. Referência/data/entidade e âmbito do montante da decisão preservados. Pedido não cria reconhecimento;decisão não cria recebimento.",
"basis": "21(6):quatro meses desde período de início e crédito superior100000;21(7a):superior500000;21(7b):cessação identificada. Origem/data nascimento/suporte e créditos mais antigos declarados.21(15):limite10anos com datas fornecidas. Compatibilidade de factos não equivale a concessão. Decisão externa documentada é registada independentemente de fundamento ainda não automatizado,com divergência visível.",
"no_duplicate": "Um pedido por parcela/âmbito recuperado;mesma origem não repetida por nova versão ou após apagar preparação actual. Parcela apresentada/decidida/recebida bloqueia retorno silencioso a26. Recibos repetidos,anteriores à decisão ou montantes incompatíveis exigem reconciliação;sem criar caixa/crédito20 por inferência.",
"F": "800000de crédito=200000reporte+600000pedido;550000reconhecidos;450000recebidos;100000aguardam recebimento;50000não deferidos. Valores distintos na gravação/recuperação/relatório. Fracções exactas:799681/3de crédito−200000de pedido=199681/3de reporte.",
"preservation": "Mesmo snapshot/API e relatório existentes;ivaRefunds acrescido sem alterar mapas anteriores. Cada cálculo congela entradas/resultados e acrescenta histórico. C continua actual eAB72/30/42 intacto.",
"technical_limit": "Escolha técnica identificada:limite de transporte do snapshot4M→6Mcaracteres;amostra preservada4055677caracteres excedia o anterior. Sem truncar/apagar históricos;novo limite mantido e rejeição acima dele demonstrada."
},
"source_cotejo": {
"CIVA2016": "Lei13/2016 PDF15–16,p1426(273–274),art21(5/6/7),fundamentos e reporte.",
"CIVA2022": "Lei22/2022 PDF3–4,p2252(27–28),art21.",
"CIVA2025": "Lei10/2025,BR248suplemento1,PDF13/p2180(13),21(7a),revogação21(8),150dias21(10),suspensões21(13),10anos21(15).",
"exemplar": "VAT Return.pdf SHA2569c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741,p2/quadro11. Remissãoart20 no formulário conservada como divergência,sem a transpor para regime2026.",
"regulation": "78/2017,30/2022,52/2025 identificados;formalidades não automatizadas,sem transformar pista/resumo em parâmetro.",
"bounded_lookup": [
"https://www.at.gov.mz/legislacao/fiscal",
"https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-suplemento-n%C2%BA-3-de-281217-pag-2602-9-16-br-n%C2%BA-202-boletim-da-rep%C3%BAblica"
],
"all17originals_unchanged": true
},
"validation": {
"frontend": "33dirigidos passaram:9AB+11C+13D;incluem limites estritos,fracções,duplicação,transporte efectivo já consumido,decisão independente da avaliação automática e preservação completa.",
"API": "1teste/109asserções passou;simulator_fiscal_delta_20261021_test/55441,DatabaseTransactions rollback;modelo_a_snapshot recuperado byte a byte. Teste do limite6M:1teste/5asserções,rejeição acima do limite.",
"types": "npm.cmd run types:check passou",
"build": "npm.cmd run build passou",
"UI": "Componente real:recebimento excedente assinala divergência;repor valores600000/550000/450000/100000;gravação→recuperação→relatório conferidos. Todos os contextos anterioresAPI/UI idênticos;exemploD mantém mesmos valores.",
"proof": "outputs/iva-l3-d-20261025/ui-recovered-refund.png",
"logs": [
"outputs/iva-l3-d-20261025/frontend-final.txt",
"outputs/iva-l3-d-20261025/api-final.txt",
"outputs/iva-l3-d-20261025/api-boundary.txt",
"outputs/iva-l3-d-20261025/types-final.txt",
"outputs/iva-l3-d-20261025/build-final.txt"
]
},
"limits": [
"Âmbito demonstrado2026/MZN,primeira declaração em prazo com parcela expressa27 e saldo26;D não submete pedido nem executa recebimento bancário. C conserva o seu âmbito demonstrado,sem cobertura fiscal integral.",
"Fundamentos21(6),21(7a) e21(7b) avaliados com factos e origem declarados/documentados. Composição cronológica completa dos créditos/extractos regulamentares não gerada automaticamente;ordem dos mais antigos confirmada expressamente,sem conta corrente centralAT ou novos aprovadores.",
"Regulamento78/2017,30/2022 e52/2025 identificados;exemplares não localizados no acervo17PDF nem obtidos na consulta delimitadaAT/INM. Formulários,anexos,garantias,compensações,formalidades,suspensões e calendário completo não automatizados. PropostaCTA/resumos não activam regras. Decisão externa documentada pode ser registada sem a avaliação automática do fundamento se tornar autoridade adicional.",
"150dias é prazo textual de reembolso devido,não previsão automática de caixa;10anos de21(15) avaliado pelas datas declaradas,com divergência visível. Obrigação50%/12meses revogada porLei10/2025,não aplicada.",
"Controlo de duplicação sobre livros e históricos recuperados neste rascunho/âmbitos efectivos. Não é reconciliação global entre rascunhos privados diferentes. Projecções de oportunidades diferentes não são consumos fiscais efectivos.",
"Afetação de preparação não altera declaração apresentada. Reporte efectivo já usado numa declaração posterior não pode ser reafectado retroactivamente;usar saldo disponível reconciliado. Pedido apresentado/decisão/recebimento preservam a parcela,inclusive após remoção da preparação actual. Indeferimento/parte não deferida não libertam crédito automaticamente;retorno exige procedimento documentado e reconciliação,sem criar20automaticamente.",
"Recebimento é registo declarado;divergências de valor/data/documento ficam identificadas,sem execução bancária,sincronização silenciosa de caixa ou eliminação do documento.",
"Erestituição de bebidas não demonstrado nesta ronda epermanece pendente,sem bloquearC/D. L1mantém129fechos+P058/P084/P085+P001=133;L2mantém18(6)+mista,sucessivas/plurianuais,erros/incobráveis. Estatísticas/IA adiadas."
],
"runtime_delta": [
{
"path": "app/Http/Requests/StoreSimulationFormDraftRequest.php",
"sha256_LF": "61c79fd1735f54d8d1d964d521ec28613184e363dd07a95d619932ba30e9de68"
},
{
"path": "resources/js/lib/iva-refund.ts",
"sha256_LF": "a2f4c0567a0633ad4926c4b57d44c167eae3fc2d9ceb167ce000ffd945ba0bbb"
},
{
"path": "resources/js/components/acquisition-refund.tsx",
"sha256_LF": "72b0706f2d338f4a06a28f0f2a2fa1a29bbeff343b919727fa34465881ed6622"
},
{
"path": "resources/js/lib/iva-closure.ts",
"sha256_LF": "0a916c7e36bb76b853c65839aa18e597bc617efe7c4a003126e44eab4243a2cd"
},
{
"path": "resources/js/components/acquisition-closure.tsx",
"sha256_LF": "321883d3fcf6d761310c896ae9b0536f2b6a802f54c052462692931e14723cc3"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "ed0563fb4f39b1d71310e7b28a2d2aa570fae14ba97a14d55159f3fe96c42901"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "27893e0c9c393a217ece3401ab3e9f839aabdb26f1b62a1130399c2b8666d40d"
}
],
"schema_changes": false,
"active_writes": false
}
}
@@ -1,5 +1,5 @@
{
"status": "QA_prepared_not_installed",
"status": "included_in_prepared_CD_group_not_installed",
"candidate": "commit que contém este manifesto",
"baseline_installed": "109cb0fe9e1530dd09b8277a422ae9c5e68e0674",
"baseline_receipt": "3aa851a4",
@@ -41,5 +41,6 @@
"backup/restauro autenticados antes da instalação",
"Git existente,conferência só do delta eprovasQA reutilizadas"
],
"active_writes": false
"active_writes": false,
"group_manifest": "docs/investmoz/simulations/l3-cd-prepared-delivery-20261025.json"
}
@@ -0,0 +1,73 @@
{
"status": "L3_CD_prepared_for_grouped_delivery_not_installed",
"candidate": "commit que contém este manifesto",
"baseline_installed": "109cb0fe9e1530dd09b8277a422ae9c5e68e0674",
"baseline_receipt": "3aa851a4",
"accepted_C_candidate": "b7a745e6",
"reports": [
"docs/investmoz/simulations/l3-c-substitution-qa-20261025.json",
"docs/investmoz/simulations/l3-d-refund-qa-20261025.json"
],
"coverage": "Csubstituições eDreembolso no âmbito operacional demonstrado;formalidades externas,E e residuaisL1/L2 permanecem explícitos",
"compatibility_at_preparation": {
"origin_main": "109cb0fe9e1530dd09b8277a422ae9c5e68e0674",
"main_is_ancestor": true,
"checked_utc": "2026-10-25T12:46:36.801875+00:00"
},
"runtime_delta": [
{
"path": "app/Http/Requests/StoreSimulationFormDraftRequest.php",
"sha256_LF": "61c79fd1735f54d8d1d964d521ec28613184e363dd07a95d619932ba30e9de68"
},
{
"path": "resources/js/lib/iva-refund.ts",
"sha256_LF": "a2f4c0567a0633ad4926c4b57d44c167eae3fc2d9ceb167ce000ffd945ba0bbb"
},
{
"path": "resources/js/components/acquisition-refund.tsx",
"sha256_LF": "72b0706f2d338f4a06a28f0f2a2fa1a29bbeff343b919727fa34465881ed6622"
},
{
"path": "resources/js/lib/iva-closure.ts",
"sha256_LF": "0a916c7e36bb76b853c65839aa18e597bc617efe7c4a003126e44eab4243a2cd"
},
{
"path": "resources/js/components/acquisition-closure.tsx",
"sha256_LF": "321883d3fcf6d761310c896ae9b0536f2b6a802f54c052462692931e14723cc3"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "ed0563fb4f39b1d71310e7b28a2d2aa570fae14ba97a14d55159f3fe96c42901"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "27893e0c9c393a217ece3401ab3e9f839aabdb26f1b62a1130399c2b8666d40d"
},
{
"path": "resources/js/lib/iva-substitution.ts",
"sha256_LF": "0ea914bf952ff053f7e6912705f80526aaed5a8b9f4b3bd5a27f737c4aef3977"
},
{
"path": "resources/js/components/acquisition-substitution.tsx",
"sha256_LF": "6a61f59bcbd493ca45e60a14fe3c67375f581581f9cbe882426fdbffa98399c2"
}
],
"limits": [
"Âmbito demonstrado2026/MZN,primeira declaração em prazo com parcela expressa27 e saldo26;D não submete pedido nem executa recebimento bancário. C conserva o seu âmbito demonstrado,sem cobertura fiscal integral.",
"Fundamentos21(6),21(7a) e21(7b) avaliados com factos e origem declarados/documentados. Composição cronológica completa dos créditos/extractos regulamentares não gerada automaticamente;ordem dos mais antigos confirmada expressamente,sem conta corrente centralAT ou novos aprovadores.",
"Regulamento78/2017,30/2022 e52/2025 identificados;exemplares não localizados no acervo17PDF nem obtidos na consulta delimitadaAT/INM. Formulários,anexos,garantias,compensações,formalidades,suspensões e calendário completo não automatizados. PropostaCTA/resumos não activam regras. Decisão externa documentada pode ser registada sem a avaliação automática do fundamento se tornar autoridade adicional.",
"150dias é prazo textual de reembolso devido,não previsão automática de caixa;10anos de21(15) avaliado pelas datas declaradas,com divergência visível. Obrigação50%/12meses revogada porLei10/2025,não aplicada.",
"Controlo de duplicação sobre livros e históricos recuperados neste rascunho/âmbitos efectivos. Não é reconciliação global entre rascunhos privados diferentes. Projecções de oportunidades diferentes não são consumos fiscais efectivos.",
"Afetação de preparação não altera declaração apresentada. Reporte efectivo já usado numa declaração posterior não pode ser reafectado retroactivamente;usar saldo disponível reconciliado. Pedido apresentado/decisão/recebimento preservam a parcela,inclusive após remoção da preparação actual. Indeferimento/parte não deferida não libertam crédito automaticamente;retorno exige procedimento documentado e reconciliação,sem criar20automaticamente.",
"Recebimento é registo declarado;divergências de valor/data/documento ficam identificadas,sem execução bancária,sincronização silenciosa de caixa ou eliminação do documento.",
"Erestituição de bebidas não demonstrado nesta ronda epermanece pendente,sem bloquearC/D. L1mantém129fechos+P058/P084/P085+P001=133;L2mantém18(6)+mista,sucessivas/plurianuais,erros/incobráveis. Estatísticas/IA adiadas."
],
"required_at_delivery": [
"fetchmain fresco e reconciliação de concorrência",
"backup novo/restauro isolado autenticados antes de instalar",
"uma entregaGit do conjuntoC/D,sem migrações/importações",
"conferir só delta instalado e reutilizar provas válidas",
"manterE e residuaisL1/L2 na mesma matriz"
],
"active_writes": false
}
@@ -0,0 +1,85 @@
{
"status": "L3-D concluído em QA no âmbito operacional demonstrado;preparado comC,sem instalação",
"matrix_anchor": "AUD-SIM-INT02",
"installed_revision": "109cb0fe",
"installed_receipt": "3aa851a4",
"C_QA_preserved": "b7a745e6",
"implemented": {
"allocation": "Reutiliza25de resultadoAB congelado e afectação explícita:26=25−27. Parcela27positiva,não excede25;apenas26 segue para o próximo mês por adopção expressa. Origem antiga/histórica não financia um novo pedido se a afectação actual mudou. Reportes dependentes indeterminados precisam de reconciliação;reporte efectivo já usado em declaração posterior não é reafectado retroactivamente.",
"lifecycle": "Preparação→apresentado fora doInvesteMoz→decisão pendente/integral/parcial/indeferida→recebimentos identificados. Referência/data/entidade e âmbito do montante da decisão preservados. Pedido não cria reconhecimento;decisão não cria recebimento.",
"basis": "21(6):quatro meses desde período de início e crédito superior100000;21(7a):superior500000;21(7b):cessação identificada. Origem/data nascimento/suporte e créditos mais antigos declarados.21(15):limite10anos com datas fornecidas. Compatibilidade de factos não equivale a concessão. Decisão externa documentada é registada independentemente de fundamento ainda não automatizado,com divergência visível.",
"no_duplicate": "Um pedido por parcela/âmbito recuperado;mesma origem não repetida por nova versão ou após apagar preparação actual. Parcela apresentada/decidida/recebida bloqueia retorno silencioso a26. Recibos repetidos,anteriores à decisão ou montantes incompatíveis exigem reconciliação;sem criar caixa/crédito20 por inferência.",
"F": "800000de crédito=200000reporte+600000pedido;550000reconhecidos;450000recebidos;100000aguardam recebimento;50000não deferidos. Valores distintos na gravação/recuperação/relatório. Fracções exactas:799681/3de crédito−200000de pedido=199681/3de reporte.",
"preservation": "Mesmo snapshot/API e relatório existentes;ivaRefunds acrescido sem alterar mapas anteriores. Cada cálculo congela entradas/resultados e acrescenta histórico. C continua actual eAB72/30/42 intacto.",
"technical_limit": "Escolha técnica identificada:limite de transporte do snapshot4M→6Mcaracteres;amostra preservada4055677caracteres excedia o anterior. Sem truncar/apagar históricos;novo limite mantido e rejeição acima dele demonstrada."
},
"source_cotejo": {
"CIVA2016": "Lei13/2016 PDF15–16,p1426(273–274),art21(5/6/7),fundamentos e reporte.",
"CIVA2022": "Lei22/2022 PDF3–4,p2252(27–28),art21.",
"CIVA2025": "Lei10/2025,BR248suplemento1,PDF13/p2180(13),21(7a),revogação21(8),150dias21(10),suspensões21(13),10anos21(15).",
"exemplar": "VAT Return.pdf SHA2569c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741,p2/quadro11. Remissãoart20 no formulário conservada como divergência,sem a transpor para regime2026.",
"regulation": "78/2017,30/2022,52/2025 identificados;formalidades não automatizadas,sem transformar pista/resumo em parâmetro.",
"bounded_lookup": [
"https://www.at.gov.mz/legislacao/fiscal",
"https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-suplemento-n%C2%BA-3-de-281217-pag-2602-9-16-br-n%C2%BA-202-boletim-da-rep%C3%BAblica"
],
"all17originals_unchanged": true
},
"validation": {
"frontend": "33dirigidos passaram:9AB+11C+13D;incluem limites estritos,fracções,duplicação,transporte efectivo já consumido,decisão independente da avaliação automática e preservação completa.",
"API": "1teste/109asserções passou;simulator_fiscal_delta_20261021_test/55441,DatabaseTransactions rollback;modelo_a_snapshot recuperado byte a byte. Teste do limite6M:1teste/5asserções,rejeição acima do limite.",
"types": "npm.cmd run types:check passou",
"build": "npm.cmd run build passou",
"UI": "Componente real:recebimento excedente assinala divergência;repor valores600000/550000/450000/100000;gravação→recuperação→relatório conferidos. Todos os contextos anterioresAPI/UI idênticos;exemploD mantém mesmos valores.",
"proof": "outputs/iva-l3-d-20261025/ui-recovered-refund.png",
"logs": [
"outputs/iva-l3-d-20261025/frontend-final.txt",
"outputs/iva-l3-d-20261025/api-final.txt",
"outputs/iva-l3-d-20261025/api-boundary.txt",
"outputs/iva-l3-d-20261025/types-final.txt",
"outputs/iva-l3-d-20261025/build-final.txt"
]
},
"limits": [
"Âmbito demonstrado2026/MZN,primeira declaração em prazo com parcela expressa27 e saldo26;D não submete pedido nem executa recebimento bancário. C conserva o seu âmbito demonstrado,sem cobertura fiscal integral.",
"Fundamentos21(6),21(7a) e21(7b) avaliados com factos e origem declarados/documentados. Composição cronológica completa dos créditos/extractos regulamentares não gerada automaticamente;ordem dos mais antigos confirmada expressamente,sem conta corrente centralAT ou novos aprovadores.",
"Regulamento78/2017,30/2022 e52/2025 identificados;exemplares não localizados no acervo17PDF nem obtidos na consulta delimitadaAT/INM. Formulários,anexos,garantias,compensações,formalidades,suspensões e calendário completo não automatizados. PropostaCTA/resumos não activam regras. Decisão externa documentada pode ser registada sem a avaliação automática do fundamento se tornar autoridade adicional.",
"150dias é prazo textual de reembolso devido,não previsão automática de caixa;10anos de21(15) avaliado pelas datas declaradas,com divergência visível. Obrigação50%/12meses revogada porLei10/2025,não aplicada.",
"Controlo de duplicação sobre livros e históricos recuperados neste rascunho/âmbitos efectivos. Não é reconciliação global entre rascunhos privados diferentes. Projecções de oportunidades diferentes não são consumos fiscais efectivos.",
"Afetação de preparação não altera declaração apresentada. Reporte efectivo já usado numa declaração posterior não pode ser reafectado retroactivamente;usar saldo disponível reconciliado. Pedido apresentado/decisão/recebimento preservam a parcela,inclusive após remoção da preparação actual. Indeferimento/parte não deferida não libertam crédito automaticamente;retorno exige procedimento documentado e reconciliação,sem criar20automaticamente.",
"Recebimento é registo declarado;divergências de valor/data/documento ficam identificadas,sem execução bancária,sincronização silenciosa de caixa ou eliminação do documento.",
"Erestituição de bebidas não demonstrado nesta ronda epermanece pendente,sem bloquearC/D. L1mantém129fechos+P058/P084/P085+P001=133;L2mantém18(6)+mista,sucessivas/plurianuais,erros/incobráveis. Estatísticas/IA adiadas."
],
"runtime_delta": [
{
"path": "app/Http/Requests/StoreSimulationFormDraftRequest.php",
"sha256_LF": "61c79fd1735f54d8d1d964d521ec28613184e363dd07a95d619932ba30e9de68"
},
{
"path": "resources/js/lib/iva-refund.ts",
"sha256_LF": "a2f4c0567a0633ad4926c4b57d44c167eae3fc2d9ceb167ce000ffd945ba0bbb"
},
{
"path": "resources/js/components/acquisition-refund.tsx",
"sha256_LF": "72b0706f2d338f4a06a28f0f2a2fa1a29bbeff343b919727fa34465881ed6622"
},
{
"path": "resources/js/lib/iva-closure.ts",
"sha256_LF": "0a916c7e36bb76b853c65839aa18e597bc617efe7c4a003126e44eab4243a2cd"
},
{
"path": "resources/js/components/acquisition-closure.tsx",
"sha256_LF": "321883d3fcf6d761310c896ae9b0536f2b6a802f54c052462692931e14723cc3"
},
{
"path": "resources/js/components/simulation-acquisitions.tsx",
"sha256_LF": "ed0563fb4f39b1d71310e7b28a2d2aa570fae14ba97a14d55159f3fe96c42901"
},
{
"path": "resources/js/lib/project-dossier.ts",
"sha256_LF": "27893e0c9c393a217ece3401ab3e9f839aabdb26f1b62a1130399c2b8666d40d"
}
],
"schema_changes": false,
"active_writes": false
}
@@ -1,5 +1,28 @@
# Rastreabilidade, utilização e formação
## 25/10/2026 — L3-D emQA operacional;C/D preparado para entrega conjunta
Homologação109cb0fe,recibo3aa851a4;C QAb7a745e6 preservado. Sem instalação,migração/importação ou escrita activa. L3 permanece aberto:formalidades de submissão eErestituição de bebidas pendentes;L1129fechos+P058/P084/P085+P001=133 eL2três famílias pendentes mantidos. Estatísticas/IAadiadas.
No fecho existente,escolher «Destinar parcela ao pedido27 — acompanhar emD» e indicar montante. Calcular afectação explícita25=26+27;27positiva e não superior25. Apenas26pode seguir para o mês seguinte. Origem/reportes dependentes alterados exigem adopção expressa e reconciliação;reporte efectivo já utilizado em declaração posterior não é reafectado retroactivamente. Afetação preparada não altera uma declaração apresentada.
Abrir «L3-D — pedido,decisão e recebimento do reembolso». Reutiliza afectação congelada actual de27,âmbito,período e natureza. Pedir apenas fundamento/origem/data nascimento,suporte e referência/data em falta.21(6):quatro meses e crédito superior100000;21(7a):superior500000 e considerar créditos mais antigos;21(7b):cessação identificada. A origem e ordem são factos declarados/documentados,não conta correnteAT reconstruída nem extracto regulamentar automático.21(15)usa datas declaradas para limite10anos. Não aplicar antiga obrigação50%/12meses,revogada pelaLei10/2025.
Preparação→pedido apresentado fora doInvesteMoz→decisão pendente/integral/parcial/indeferida→recebimento identificado. Pedido não gera decisão;decisão não gera caixa. Guardar número/data/entidade da decisão e respectivo montante;registar recibos/data/montante separadamente. A avaliação automática do fundamento fica independente da decisão externa documentada:fundamento desconhecido/não automatizado não cria aprovador adicional nem impede registar documento recebido. Divergência fica explícita;sem presumir direito numa preparação sem fundamento.
Mesma parcela não pode sustentar dois pedidos. Depois de pedido apresentado/decisão/recebimento,a parcela continua reservada,inclusive ao remover a preparação actual;histórico preservado. Indeferimento e parte não deferida não libertam crédito automaticamente:retorno exige procedimento documentado e reconciliação,sem criar20porinferência. Duplicações,recebimentos sem decisão/data ou acima do reconhecido ficam indeterminados;documentos conservados. Sem executar banco ou alterar caixa silenciosamente.
F acompanhaD:800000MZNdecrédito=200000reporte+600000pedido;550000reconhecidos;450000recebidos;100000aguardam recebimento;50000não deferidos. Relatório guarda cada contexto e as mesmas referências/valores. Fracção exacta799681/3menos200000depedido=199681/3de reporte. Gravar→recuperar→relatório conferido no componente real;AB72/30/42 eC intactos.
Cotejo:Lei13/2016 PDF15–16;Lei22/2022 PDF3–4;Lei10/2025 PDF13,art21;exemplarModeloA quadro11. Remissãoart20 do formulário conservada como divergência. Regulamentos78/2017,30/2022 e52/2025 identificados,mas respectivos exemplares não obtidos no acervo/consulta delimitadaAT/INM nesta ronda;propostas/resumos não activam regras. Formulários,anexos,garantias,compensações,suspensões e calendário completos continuam não automatizados e externos.150dias não constitui previsão de caixa;E não foi demonstrado nesta ronda e não bloqueiaC/D.
Escolha técnica delimitada:transporte do snapshot4M→6Mcaracteres para preservar todos os livros/históricos aceites ao acrescentarD;sem truncagem. API109asserções recupera exactamente a amostra;teste5asserções mantém rejeição acima6M.33dirigidosAB/C/D,tipos/build eUI passaram. Prova:outputs/iva-l3-d-20261025/ui-recovered-refund.png. Relatório:docs/investmoz/simulations/l3-d-refund-qa-20261025.json;manifesto conjunto:docs/investmoz/simulations/l3-cd-prepared-delivery-20261025.json.
## 25/10/2026 — inventário anterior aL3-D
AB instalado109cb0fe/3aa851a4 eC QAb7a745e6 preservados,sem perdas confirmadas. L1129fechos/quatroresiduais,L2três famílias eErestituição de bebidas mantidos. D reutiliza crédito congelado,separando26reporte/27pedido,pedido/decisão/recebimento. Cotejo limitadoCIVA21 comredacção2025 eModeloA11;regra50%/12meses revogada,não activar. Formalidades regulamentares externas não automatizadas,não reinventar aprovadores/certificados.
## 25/10/2026 — L3-C emQA no âmbito demonstrado;homologaçãoAB preservada
AB entregue109cb0fe,recibo3aa851a4;72calculados/30pagos registados/42porliquidar intactos. C não está instalado. L3 continua aberto,Dreembolso geral eErestituição de bebidas pendentes;L1 mantém129fechos+P058/P084/P085+P001=133 eL2 mantém18(6)+mista,sucessivas/plurianuais,erros/incobráveis. Estatísticas/IAadiadas.
File diff suppressed because one or more lines are too long
@@ -10,7 +10,7 @@ export function AcquisitionClosure({draft,onChange}:{draft:Record<string,unknown
const choice=(r:ClosureInput,key:'firstDeclaration'|'creditsComplete',label:string)=><label className="block">{label}<select value={r[key]} onChange={e=>change(r.id,{[key]:e.target.value})}><option value="">Por confirmar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>;
return<details className="rounded border p-3"><summary>L3-A/B — crédito,fecho e pagamentos por período</summary>
<p>Reutiliza resultados congelados de L2. Crédito anterior,saldo calculado,pagamento registado e transporte são distintos. Identifique o âmbito fiscal da entidade,sem consolidar oportunidades implicitamente.</p>
<p>Primeira declaração em prazo,2026/MZN e reporte integral. SubstituiçõesC,reembolsoD e restituição de bebidasE continuam próprios. Prazo/data são declarados,não calculados pelo portal. Uma simulação não submete declarações nem efectua pagamentos.</p>
<p>Primeira declaração em prazo,2026/MZN e reporte integral ou afectação de parcela ao pedidoD. SubstituiçõesC,acompanhamento de reembolsoD e restituição de bebidasE continuam próprios. A parcela preparada para pedido é separada do reporte,sem concessão automática. Prazo/data são declarados,não calculados pelo portal. Uma simulação não submete declarações nem efectua pagamentos.</p>
{error&&<p role="alert">{error}</p>}
<button type="button" onClick={()=>save({...b,rows:[...b.rows,newClosure(crypto.randomUUID(),sources.length===1?sources[0]:null,entities.length===1?entities[0]:String(draft.fiscal_entity_ref??draft.organization_name??''))]})}>Preparar fecho de outro período</button>
{b.rows.map(r=><fieldset className="rounded border p-3" key={r.id}><legend>Fecho — {r.period||'período por escolher'}</legend>
@@ -35,8 +35,10 @@ export function AcquisitionClosure({draft,onChange}:{draft:Record<string,unknown
<label>Juros23<select value={r.interest} onChange={e=>change(r.id,{interest:e.target.value as ClosureInput['interest']})}><option value="">Indeterminados</option><option value="none">Sem juros aplicáveis — motivo identificado</option><option value="documented">Montante identificado,sem cálculo de taxa pelo portal</option></select></label>
{r.interest==='documented'&&<label>Montante de juros (MZN)<input value={r.interestAmount} onChange={e=>change(r.id,{interestAmount:e.target.value})}/></label>}
{r.interest&&<label>Fonte/motivo dos juros ou da sua inexistência<input value={r.interestRef} onChange={e=>change(r.id,{interestRef:e.target.value})}/></label>}
<label>Destino do crédito25<select value={r.destination} onChange={e=>change(r.id,{destination:e.target.value as ClosureInput['destination']})}><option value="">Por escolher</option><option value="report">Reportar integralmente26;sem pedido27 neste percurso</option><option value="refund">Preparar reembolso — percursoD pendente</option></select></label>
<label>Destino do crédito25<select value={r.destination} onChange={e=>change(r.id,{destination:e.target.value as ClosureInput['destination']})}><option value="">Por escolher</option><option value="report">Reportar integralmente26;sem pedido27 neste percurso</option><option value="refund">Destinar parcela ao pedido27 — acompanhar emD</option></select></label>
{r.destination==='refund'&&<label>Parcela do cr?dito destinada ao pedido27 (MZN)<input value={r.refundAmount??''} onChange={e=>change(r.id,{refundAmount:e.target.value})}/></label>}
<h5>Pagamentos registados,distintos do saldo calculado</h5>
{r.destination==='refund'&&<label>Parcela do crédito destinada ao pedido27 (MZN)<input value={r.refundAmount??''} onChange={e=>change(r.id,{refundAmount:e.target.value})}/></label>}
{r.payments.map(p=><fieldset className="rounded border p-2" key={p.id}>{(['reference','date','amount'] as const).map((k,i)=><label key={k}>{['Referência do pagamento','Data do pagamento','Montante do pagamento (MZN)'][i]}<input type={k==='date'?'date':'text'} value={p[k]} onChange={e=>change(r.id,{payments:r.payments.map(v=>v.id===p.id?{...v,[k]:e.target.value}:v)})}/></label>)}<button type="button" onClick={()=>change(r.id,{payments:r.payments.filter(v=>v.id!==p.id)})}>Remover pagamento desta preparação</button></fieldset>)}
<button type="button" onClick={()=>change(r.id,{payments:[...r.payments,{id:crypto.randomUUID(),reference:'',date:'',amount:''}]})}>Registar referência de pagamento</button>
<button type="button" onClick={()=>save({...b,rows:b.rows.filter(v=>v.id!==r.id)})}>Remover fecho em preparação (histórico conservado)</button></fieldset>)}
@@ -0,0 +1,34 @@
import {useState} from 'react';
import {refundBook,refundSources,newRefund,saveRefundBook,recordRefund,refundReportRows,type RefundBook,type RefundInput} from '@/lib/iva-refund';
export function AcquisitionRefund({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');let b:RefundBook;try{b=refundBook(draft);}catch(e){return<p role="alert">{String(e)}</p>;}
const save=(book:RefundBook)=>{try{onChange('modelo_a_snapshot',saveRefundBook(draft,book));setError('');}catch(e){setError(String(e));}};
const change=(id:string,p:Partial<RefundInput>)=>save({...b,rows:b.rows.map(r=>r.id===id?{...r,...p}:r)}),sources=refundSources(draft);
const choice=(r:RefundInput,k:'factsConfirmed'|'oldestFirst',label:string)=><label>{label}<select value={r[k]} onChange={e=>change(r.id,{[k]:e.target.value})}><option value="">Por confirmar</option><option value="yes">Sim — documentado ou pressuposto explícito</option><option value="no">Não</option></select></label>;
return<details className="rounded border p-3"><summary>L3-D — pedido,decisão e recebimento do reembolso</summary>
<p>Primeiro,no fecho do período,escolha a parcela destinada ao pedido27 e calcule a afectação:25=26+27. Apenas26 continua em reporte. Reutilize a afectação abaixo;crédito já transportado não é uma nova parcela disponível.</p>
<p>Preparar ou apresentar o pedido não é deferimento;deferimento não é recebimento. Submissão,formulários regulamentares,suspensões e pagamentos bancários ocorrem fora doInvesteMoz. O prazo legal não é previsão de caixa. Não se aplica a antiga obrigação50%/12meses,revogada em2026.</p>
{error&&<p role="alert">{error}</p>}
<button type="button" onClick={()=>save({...b,rows:[...b.rows,newRefund(crypto.randomUUID(),sources.length===1?sources[0]:null)]})}>Preparar pedido de reembolso geral</button>
{b.rows.map(r=><fieldset className="rounded border p-3" key={r.id}><legend>Pedido de reembolso — {r.source?.period||'período por escolher'}</legend>
<label>Afectação congelada do crédito27<select value={sources.findIndex(s=>s.allocationKey===r.source?.allocationKey)} onChange={e=>{const s=sources[Number(e.target.value)];if(s)change(r.id,{source:structuredClone(s)});}}><option value={-1}>Escolha a parcela actual</option>{sources.map((s,i)=><option key={s.allocationKey} value={i}>{s.period} — {s.entityRef} — fecho{s.closureId}</option>)}</select></label>
<label>Fundamento do pedido<select value={r.ground} onChange={e=>change(r.id,{ground:e.target.value as RefundInput['ground']})}><option value="">Por identificar</option><option value="four_months">21(6) — quatro meses e crédito superior100000MZN</option><option value="high_credit">21(7a) — crédito superior500000MZN</option><option value="cessation">21(7b) — cessação de actividade</option></select></label>
<label>Período de início do excesso de crédito<input type="month" value={r.originPeriod} onChange={e=>change(r.id,{originPeriod:e.target.value})}/></label>
<label>Data de nascimento do direito à dedução<input type="date" value={r.entitlementDate} onChange={e=>change(r.id,{entitlementDate:e.target.value})}/></label>
{r.ground==='cessation'&&<label>Data da cessação de actividade<input type="date" value={r.cessationDate} onChange={e=>change(r.id,{cessationDate:e.target.value})}/></label>}
<label>Suporte dos factos e origem do crédito<textarea value={r.groundRef} onChange={e=>change(r.id,{groundRef:e.target.value})}/></label>
{choice(r,'factsConfirmed','Factos do fundamento identificados no suporte')}{choice(r,'oldestFirst','Considerei sequencialmente os créditos mais antigos')}
<label>Referência do pedido ou preparação<input value={r.requestReference} onChange={e=>change(r.id,{requestReference:e.target.value})}/></label>
<label>Data do pedido ou prevista<input type="date" value={r.requestDate} onChange={e=>change(r.id,{requestDate:e.target.value})}/></label>
<label>Situação do pedido<select value={r.stage} onChange={e=>change(r.id,{stage:e.target.value as RefundInput['stage']})}><option value="preparation">Em preparação — sem submissão</option><option value="submitted">Apresentado fora doInvesteMoz — registo declarado</option></select></label>
<label>Decisão recebida<select value={r.decision} onChange={e=>change(r.id,{decision:e.target.value as RefundInput['decision']})}><option value="pending">Ainda não identificada</option><option value="approved">Deferimento integral</option><option value="partial">Deferimento parcial</option><option value="rejected">Indeferimento</option></select></label>
{r.decision!=='pending'&&<><label>Número ou referência da decisão<input value={r.decisionReference} onChange={e=>change(r.id,{decisionReference:e.target.value})}/></label><label>Data da decisão<input type="date" value={r.decisionDate} onChange={e=>change(r.id,{decisionDate:e.target.value})}/></label><label>Entidade emitente da decisão<input value={r.decisionIssuer} onChange={e=>change(r.id,{decisionIssuer:e.target.value})}/></label><label>Montante reconhecido na decisão (MZN)<input value={r.approvedAmount} onChange={e=>change(r.id,{approvedAmount:e.target.value})}/></label></>}
<h5>Recebimentos registados</h5>
{r.receipts.map(p=><fieldset key={p.id}><legend>Recebimento de reembolso</legend>{(['reference','date','amount'] as const).map(k=><label key={k}>{k==='reference'?'Referência do recebimento':k==='date'?'Data do recebimento':'Montante recebido (MZN)'}<input type={k==='date'?'date':'text'} value={p[k]} onChange={e=>change(r.id,{receipts:r.receipts.map(v=>v.id===p.id?{...v,[k]:e.target.value}:v)})}/></label>)}<button type="button" onClick={()=>change(r.id,{receipts:r.receipts.filter(v=>v.id!==p.id)})}>Remover recebimento desta preparação</button></fieldset>)}
<button type="button" onClick={()=>change(r.id,{receipts:[...r.receipts,{id:crypto.randomUUID(),reference:'',date:'',amount:''}]})}>Registar recebimento identificado</button>
<button type="button" onClick={()=>save({...b,rows:b.rows.filter(v=>v.id!==r.id)})}>Remover pedido desta preparação</button>
</fieldset>)}
{b.rows.length>0&&<button type="button" onClick={()=>{try{save(recordRefund(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar acompanhamento D</button>}
{refundReportRows(draft).map((r,i)=><p key={i}><strong>{r[1]}:</strong> {r[2]}<br/>{r[3]}</p>)}
</details>;
}
@@ -1,6 +1,7 @@
import { useState } from 'react';
import {AcquisitionClosure} from './acquisition-closure';
import {AcquisitionSubstitution} from './acquisition-substitution';
import {AcquisitionRefund} from './acquisition-refund';
import {AcquisitionRegularizations} from './acquisition-regularizations';
import { AcquisitionOperationLink } from './acquisition-operation-link';
import { AcquisitionImportsExclusions } from './acquisition-imports-exclusions';
@@ -87,6 +88,7 @@ export function SimulationAcquisitions({draft,onChange}:{draft:Record<string,unk
<p>O apuramento01–18 conserva o seu resultado antes dos créditos. O fechoA/B abaixo reutiliza a versão congelada,sem ordem de pagamento,submissãoAT ou promessa de reembolso.</p>
<AcquisitionClosure draft={draft} onChange={onChange}/>
<AcquisitionSubstitution draft={draft} onChange={onChange}/>
<AcquisitionRefund draft={draft} onChange={onChange}/>
{book.result&&<><p role="status">{acquisitionCurrent(draft,book)?'Resultado correspondente às entradas guardadas.':'Entradas ou origem alteradas: resultado histórico preservado; actualizar o cálculo.'}</p><dl>{acquisitionReportRows(draft).map(([,label,value,origin],i)=><div key={i}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</section>;
}
+23 -5
View File
@@ -7,7 +7,7 @@ type Choice=''|'yes'|'no';
export type ClosureSource={revision:number;calculatedAt:string;fingerprint:string;opportunity:string;currency:string;period:string;location:string;projected:boolean;debit:FiscalFraction|null;credit:FiscalFraction|null};
export type ClosureCredit={id:string;kind:'opening'|'communication'|'previous';period:string;reference:string;date:string;amount:string;previousId:string;carryKey:string};
export type ClosurePayment={id:string;reference:string;date:string;amount:string};
export type ClosureInput={id:string;period:string;entityRef:string;origin:'projected'|'effective';source:ClosureSource|null;firstDeclaration:Choice;filing:''|'timely'|'late';submissionDate:string;filingRef:string;creditsComplete:Choice;credits:ClosureCredit[];interest:''|'none'|'documented';interestAmount:string;interestRef:string;destination:''|'report'|'refund';payments:ClosurePayment[]};
export type ClosureInput={id:string;period:string;entityRef:string;origin:'projected'|'effective';source:ClosureSource|null;firstDeclaration:Choice;filing:''|'timely'|'late';submissionDate:string;filingRef:string;creditsComplete:Choice;credits:ClosureCredit[];interest:''|'none'|'documented';interestAmount:string;interestRef:string;destination:''|'report'|'refund';refundAmount?:string;payments:ClosurePayment[]};
export type ClosureResult={id:string;period:string;entityRef:string;origin:ClosureInput['origin'];status:'determined'|'indeterminate';reason:string;inputKey:string;financialKey:string;carryKey:string;fields:Record<string,FiscalFraction|null>;paid:FiscalFraction|null;outstanding:FiscalFraction|null;overpayment:FiscalFraction|null;source:string};
export type ClosureBook={version:1;rows:ClosureInput[];result?:{calculatedAt:string;key:string;inputs:ClosureInput[];rows:ClosureResult[]};history?:Array<NonNullable<ClosureBook['result']>>};
const scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general');
@@ -18,7 +18,7 @@ function maps(d:Record<string,unknown>):Record<string,ClosureBook>{const v=store
function validate(b:ClosureBook):void{
if(!b||b.version!==1||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history)||b.result!==undefined&&(!Array.isArray(b.result.rows)||!Array.isArray(b.result.inputs)||typeof b.result.key!=='string'))throw Error('FechoL3 não recuperável;originais preservados.');
if(b.result?.rows.some(r=>!r||typeof r.id!=='string'||!r.fields||typeof r.fields!=='object'||!['determined','indeterminate'].includes(r.status)||Object.values(r.fields).some(v=>v!==null&&(!v||!/^\d{1,80}$/.test(v.numerator)||!/^\d{1,80}$/.test(v.denominator)||BigInt(v.denominator)===0n))))throw Error('ResultadoL3 não recuperável;originais preservados.');
for(const r of b.rows){if(!r||!['id','period','entityRef','submissionDate','filingRef','interestAmount','interestRef'].every(k=>typeof r[k as keyof ClosureInput]==='string')||!['projected','effective'].includes(r.origin)||!['','yes','no'].includes(r.firstDeclaration)||!['','yes','no'].includes(r.creditsComplete)||!['','timely','late'].includes(r.filing)||!['','none','documented'].includes(r.interest)||!['','report','refund'].includes(r.destination)||!Array.isArray(r.credits)||!Array.isArray(r.payments)||r.credits.some(c=>!c||!['opening','communication','previous'].includes(c.kind)||!['id','period','reference','date','amount','previousId','carryKey'].every(k=>typeof c[k as keyof ClosureCredit]==='string'))||r.payments.some(p=>!p||!['id','reference','date','amount'].every(k=>typeof p[k as keyof ClosurePayment]==='string'))||r.source!==null&&(!r.source||!Number.isInteger(r.source.revision)||!['calculatedAt','fingerprint','opportunity','currency','period','location'].every(k=>typeof r.source![k as keyof ClosureSource]==='string')))throw Error('EntradasL3 não recuperáveis;originais preservados,sem substituir por zero.');}
for(const r of b.rows){if(!r||!['id','period','entityRef','submissionDate','filingRef','interestAmount','interestRef'].every(k=>typeof r[k as keyof ClosureInput]==='string')||!['projected','effective'].includes(r.origin)||!['','yes','no'].includes(r.firstDeclaration)||!['','yes','no'].includes(r.creditsComplete)||!['','timely','late'].includes(r.filing)||!['','none','documented'].includes(r.interest)||!['','report','refund'].includes(r.destination)||r.refundAmount!==undefined&&typeof r.refundAmount!=='string'||!Array.isArray(r.credits)||!Array.isArray(r.payments)||r.credits.some(c=>!c||!['opening','communication','previous'].includes(c.kind)||!['id','period','reference','date','amount','previousId','carryKey'].every(k=>typeof c[k as keyof ClosureCredit]==='string'))||r.payments.some(p=>!p||!['id','reference','date','amount'].every(k=>typeof p[k as keyof ClosurePayment]==='string'))||r.source!==null&&(!r.source||!Number.isInteger(r.source.revision)||!['calculatedAt','fingerprint','opportunity','currency','period','location'].every(k=>typeof r.source![k as keyof ClosureSource]==='string')))throw Error('EntradasL3 não recuperáveis;originais preservados,sem substituir por zero.');}
}
export function closureBook(d:Record<string,unknown>):ClosureBook{const b=maps(d)[scope(d)]??{version:1,rows:[]};validate(b);return b;}
export function saveClosureBook(d:Record<string,unknown>,b:ClosureBook):string{validate(b);const s=store(d);return JSON.stringify({...s,ivaClosures:{...maps(d),[scope(d)]:b}});}
@@ -38,6 +38,16 @@ function substitutionPayments(d:Record<string,unknown>):string[]{const m=store(d
function bookKey(d:Record<string,unknown>,b:ClosureBook){const sources=closureSources(d);const values:unknown[]=[b.rows,b.rows.map(r=>sources.some(s=>JSON.stringify(s)===JSON.stringify(r.source))),otherUses(d,b).map(r=>[normalize(r.entityRef),r.origin,r.period,r.credits.map(c=>creditIdentity(r,c)),r.payments.map(p=>paymentIdentity(r,p))])];const reserved=substitutionPayments(d),conflicts=b.rows.flatMap(r=>r.payments.map(p=>paymentIdentity(r,p))).filter(k=>reserved.includes(k));if(conflicts.length)values.push(conflicts);return closureFingerprint(values);}
export function closureCurrent(d:Record<string,unknown>,b:ClosureBook):boolean{return !!b.result&&b.result.key===bookKey(d,b)&&b.result.rows.length===b.rows.length&&b.rows.every(r=>b.result!.rows.filter(x=>x.id===r.id&&x.inputKey===inputKey(r)).length===1);}
export function adoptPreviousCredit(c:ClosureCredit,previous:ClosureResult):ClosureCredit{if(previous.status!=='determined'||!previous.fields['26'])throw Error('Fecho anterior indeterminado;sem crédito presumido.');return{...c,kind:'previous',period:previous.period,reference:`Fecho ${previous.id}`,previousId:previous.id,carryKey:previous.carryKey,amount:fractionPresentation(previous.fields['26']).decimal,date:''};}
function refundLockConflict(d:Record<string,unknown>,row:ClosureInput,fields:Record<string,FiscalFraction|null>):boolean{
const books=store(d).ivaRefunds as Record<string,{rows:Array<{source:{closureId:string;entityRef:string;origin:string;period:string;credit:FiscalFraction;carry:FiscalFraction;requested:FiscalFraction}|null;stage:string;decision:string;receipts:unknown[]}>;result?:{inputs:Array<{source:{closureId:string;entityRef:string;origin:string;period:string;credit:FiscalFraction;carry:FiscalFraction;requested:FiscalFraction}|null;stage:string;decision:string;receipts:unknown[]}>};history?:Array<{inputs:Array<{source:{closureId:string;entityRef:string;origin:string;period:string;credit:FiscalFraction;carry:FiscalFraction;requested:FiscalFraction}|null;stage:string;decision:string;receipts:unknown[]}>}>}>|undefined;
return Object.entries(books??{}).some(([context,book])=>[...book.rows,...[...(book.history??[]),...(book.result?[book.result]:[])].flatMap(c=>c.inputs)].some(r=>r.source&&(context===scope(d)||r.source.origin==='effective')&&r.source.closureId===row.id&&normalize(r.source.entityRef)===normalize(row.entityRef)&&r.source.origin===row.origin&&r.source.period===row.period&&(r.stage==='submitted'||r.decision!=='pending'||r.receipts.length>0)&&[['25',r.source.credit],['26',r.source.carry],['27',r.source.requested]].some(([f,v])=>JSON.stringify(fields[f as string])!==JSON.stringify(v))));
}
function effectiveCarryAlreadyUsed(d:Record<string,unknown>,row:ClosureInput,fields:Record<string,FiscalFraction|null>):boolean{
if(row.origin!=='effective'||row.destination!=='refund')return false;
const books=maps(d),own=books[scope(d)];if(!own)return false;
const prior=[...(own.history??[]),...(own.result?[own.result]:[])].flatMap(c=>c.rows).filter(r=>r.id===row.id&&r.origin==='effective'&&r.fields['26']&&fields['26']&&BigInt(subtractFractions(r.fields['26'],fields['26']).numerator)>0n);
return prior.some(p=>Object.values(books).some(b=>[...(b.history??[]),...(b.result?[b.result]:[])].some(c=>c.inputs.some(input=>input.origin==='effective'&&normalize(input.entityRef)===normalize(row.entityRef)&&input.period>row.period&&input.credits.some(credit=>credit.kind==='previous'&&credit.previousId===p.id&&credit.carryKey===p.carryKey)&&c.rows.some(r=>r.id===input.id&&r.status==='determined')))));
}
export function recordClosure(d:Record<string,unknown>,b:ClosureBook,now=new Date().toISOString()):ClosureBook{
validate(b);if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identidade de fecho repetida.');
const contexts=b.rows.map(r=>JSON.stringify([normalize(r.entityRef),r.origin,r.period]));if(new Set(contexts).size!==contexts.length)throw Error('Mesmo âmbito/período repetido neste cenário;sem duplicar o fecho.');
@@ -78,21 +88,29 @@ export function recordClosure(d:Record<string,unknown>,b:ClosureBook,now=new Dat
else if(row.interest==='documented'&&row.interestRef.trim()){try{fields['23']=decimalFraction(row.interestAmount);}catch{/* Preserve unknown. */}}
fields['24']=fields['23']?addFractions([fields['22'],fields['23']]):null;
if(row.destination==='report'){fields['26']=fields['25'];fields['27']=zero();}
if(row.destination!=='report'){stop('Escolha reporte integral para o percursoA/B;pedido de reembolso/alocação27 segueL3-D e não é presumido.');continue;}
else if(row.destination==='refund'){
let amount:FiscalFraction;try{amount=decimalFraction(row.refundAmount??'');}catch{stop('Identifique a parcela destinada ao pedido;sem montante presumido.');continue;}
if(BigInt(amount.numerator)===0n||BigInt(subtractFractions(fields['25'],amount).numerator)<0n){stop('Parcela do pedido deve ser positiva e não exceder o crédito25;sem dupla utilização.');continue;}
fields['27']=amount;fields['26']=subtractFractions(fields['25'],amount);r.source+=' D:afectação expressa26+27=25,sem concessão/recebimento;CIVA21 eModeloA11.';
}else{stop('Escolha reporte integral ou parcela para preparação de reembolsoD;sem destino presumido.');continue;}
if(refundLockConflict(d,row,fields)){stop('Crédito ligado a pedido apresentado/decisão/recebimento;conserve a afectação e reconcilie o procedimento,sem o voltar a reportar ou alterar silenciosamente.');continue;}
if(effectiveCarryAlreadyUsed(d,row,fields)){stop('Reporte efectivo já utilizado numa declaração posterior recuperada;prepare o pedido sobre o saldo disponível do período reconciliado,sem reafectar retroactivamente crédito consumido.');continue;}
if(!fields['24']){stop('Juros23 por identificar:indique montante documentado/pressuposto ou inexistência expressa com motivo;sem taxa presumida.');continue;}
r.status='determined';r.carryKey=closureFingerprint([r.id,r.financialKey,fields['26'],scope(d)]);
const payments:FiscalFraction[]=[];
for(const p of row.payments){if(!p.reference.trim()||!validDate(p.date)||(puses.get(paymentIdentity(row,p))??0)>1||substitutionPayments(d).includes(paymentIdentity(row,p))){invalid='Pagamento sem referência/data ou repetido noutro período/declaração;conserve o documento e reconcilie a imputação.';break;}try{payments.push(decimalFraction(p.amount));}catch{invalid='Montante de pagamento por identificar;sem baixa presumida.';break;}}
if(invalid){stop(invalid);continue;}
r.paid=addFractions(payments);r.outstanding=positiveFraction(subtractFractions(fields['24'],r.paid));r.overpayment=positiveFraction(subtractFractions(r.paid,fields['24']));
r.status='determined';r.carryKey=closureFingerprint([r.id,r.financialKey,fields['26'],scope(d)]);r.reason='Saldo calculado, pagamentos registados e crédito transportado distintos. Pagamento não altera22/25/26;excedente pago não cria crédito20 sem comunicação.21 não aplicável à primeira declaração;27zero pela opção expressa de reporte integral. '+(row.origin==='projected'?'Pressupostos de simulação,sem declaração ou pagamento efectuados pelo portal.':'Dados efectivos declarados;registo não certifica cobrança nem submissãoAT.');results.push(r);
r.status='determined';r.carryKey=closureFingerprint([r.id,r.financialKey,fields['26'],scope(d)]);r.reason='Saldo calculado, pagamentos registados e crédito transportado distintos. Pagamento não altera22/25/26;excedente pago não cria crédito20 sem comunicação.21 não aplicável à primeira declaração;'+(row.destination==='refund'?'26/27 separados pela afectação;27 é parcela destinada a pedidoD,sem concessão nem recebimento. ':'27zero pela opção expressa de reporte integral. ')+(row.origin==='projected'?'Pressupostos de simulação,sem declaração ou pagamento efectuados pelo portal.':'Dados efectivos declarados;registo não certifica cobrança nem submissãoAT.');results.push(r);
}
const result={calculatedAt:now,key:bookKey(d,b),inputs:structuredClone(b.rows),rows:results};return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};
}
export function closureReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{
try{
const b=closureBook(d);if(!b.rows.length)return[];
const out:Array<[string,string,string,string]>=[['IVA mensal','L3-A/B — fecho por período',closureCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever entradas/origens antes de utilizar',closureSource]];
const hasRefund=b.result?.rows.some(r=>r.fields['27']&&BigInt(r.fields['27'].numerator)>0n);
const description=hasRefund?closureSource.replace('Sem substituição21,reembolso27 ou restituição13(2).','ExtensãoD:26+27=25;27é parcela destinada a pedido,não decisão/recebimento. Substituição21 e restituição13(2) mantêm percursos próprios.'):closureSource;
const out:Array<[string,string,string,string]>=[['IVA mensal','L3-A/B — fecho por período',closureCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever entradas/origens antes de utilizar',description]];
for(const r of b.result?.rows??[]){
const input=b.result?.inputs.find(x=>x.id===r.id),source=input?.source;
const context=`${r.entityRef};${scope(d)};MZN;${r.origin==='projected'?'Pressuposto de simulação':'Dados efectivos declarados'};versão guardada${b.result!.calculatedAt}`;
+63
View File
@@ -0,0 +1,63 @@
import {closureBook,closureCurrent,closureFingerprint} from './iva-closure.ts';
import {addFractions,subtractFractions,positiveFraction,decimalFraction,fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export const refundSource='L3-D:CIVA21(5/6/7),Lei13/2016 PDF15–16,p1426(273–274);Lei22/2022 PDF3–4;Lei10/2025 PDF13/p2180(13),21(7a/8revogado/10/13/15). ModeloA/VAT Return9c0b99a8,p2/quadro11:26+27=25. Pedido,decisão e recebimento separados. Formalidades doRegulamento78/2017/30/2022/52/2025 não automatizadas;sem concessão automática.';
type Choice=''|'yes'|'no';
export type RefundSource={closureId:string;period:string;entityRef:string;origin:'projected'|'effective';allocationKey:string;calculatedAt:string;credit:FiscalFraction;carry:FiscalFraction;requested:FiscalFraction};
export type RefundReceipt={id:string;reference:string;date:string;amount:string};
export type RefundInput={id:string;source:RefundSource|null;ground:''|'four_months'|'high_credit'|'cessation';originPeriod:string;entitlementDate:string;cessationDate:string;groundRef:string;factsConfirmed:Choice;oldestFirst:Choice;stage:'preparation'|'submitted';requestReference:string;requestDate:string;decision:'pending'|'approved'|'partial'|'rejected';decisionReference:string;decisionDate:string;decisionIssuer:string;approvedAmount:string;receipts:RefundReceipt[]};
export type RefundResult={id:string;status:'determined'|'indeterminate';reason:string;inputKey:string;eligibility:'compatible'|'not_demonstrated'|'incompatible';requested:FiscalFraction|null;reported:FiscalFraction|null;approved:FiscalFraction|null;received:FiscalFraction|null;awaitingReceipt:FiscalFraction|null;notGranted:FiscalFraction|null;source:string};
export type RefundCalculation={calculatedAt:string;key:string;inputs:RefundInput[];rows:RefundResult[]};
export type RefundBook={version:1;rows:RefundInput[];result?:RefundCalculation;history?:RefundCalculation[]};
const scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general'),norm=(s:string)=>s.trim().toLocaleLowerCase(),zero=()=>fraction(0n);
const date=(s:string)=>/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(s)&&!Number.isNaN(Date.parse(s))&&new Date(s).toISOString().slice(0,10)===s;
const period=(s:string)=>/^20\d{2}-(0[1-9]|1[0-2])$/.test(s);
const months=(a:string,b:string)=>(Number(b.slice(0,4))-Number(a.slice(0,4)))*12+Number(b.slice(5))-Number(a.slice(5));
function store(d:Record<string,unknown>):Record<string,unknown>{const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books)throw Error('Livros não recuperáveis;originais preservados.');return s;}
function maps(d:Record<string,unknown>):Record<string,RefundBook>{const m=store(d).ivaRefunds;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa de reembolsos não recuperável;sem o substituir.');return m as Record<string,RefundBook>;}
function validate(b:RefundBook):void{if(!b||b.version!==1||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history)||b.result!==undefined&&(!Array.isArray(b.result.rows)||!Array.isArray(b.result.inputs)))throw Error('Livro de reembolsos não recuperável;originais preservados.');for(const r of b.rows)if(!r||!['id','originPeriod','entitlementDate','cessationDate','groundRef','requestReference','requestDate','decisionReference','decisionDate','decisionIssuer','approvedAmount'].every(k=>typeof r[k as keyof RefundInput]==='string')||!['','four_months','high_credit','cessation'].includes(r.ground)||!['','yes','no'].includes(r.factsConfirmed)||!['','yes','no'].includes(r.oldestFirst)||!['preparation','submitted'].includes(r.stage)||!['pending','approved','partial','rejected'].includes(r.decision)||!Array.isArray(r.receipts)||r.receipts.some(p=>!p||!['id','reference','date','amount'].every(k=>typeof p[k as keyof RefundReceipt]==='string')))throw Error('EntradasD não recuperáveis;sem montantes presumidos.');}
export function refundBook(d:Record<string,unknown>):RefundBook{const b=maps(d)[scope(d)]??{version:1,rows:[]};validate(b);return b;}
export function saveRefundBook(d:Record<string,unknown>,b:RefundBook):string{validate(b);const s=store(d);return JSON.stringify({...s,ivaRefunds:{...maps(d),[scope(d)]:b}});}
export function refundSources(d:Record<string,unknown>):RefundSource[]{const b=closureBook(d);if(!closureCurrent(d,b))return[];return(b.result?.rows??[]).flatMap(r=>{const p=r.fields['27'],c=r.fields['25'],carry=r.fields['26'];if(r.status!=='determined'||!p||!c||!carry||BigInt(p.numerator)<=0n)return[];const dependentUnreconciled=b.rows.some(input=>input.credits.some(credit=>credit.kind==='previous'&&credit.previousId===r.id)&&!b.result!.rows.some(result=>result.id===input.id&&result.status==='determined'));if(dependentUnreconciled)return[];return[{closureId:r.id,period:r.period,entityRef:r.entityRef,origin:r.origin,allocationKey:closureFingerprint([r.id,r.entityRef,r.origin,r.period,c,carry,p]),calculatedAt:b.result!.calculatedAt,credit:c,carry,requested:p}];});}
export function newRefund(id:string,source:RefundSource|null):RefundInput{return{id,source:source?structuredClone(source):null,ground:'',originPeriod:'',entitlementDate:'',cessationDate:'',groundRef:'',factsConfirmed:'',oldestFirst:'',stage:'preparation',requestReference:'',requestDate:'',decision:'pending',decisionReference:'',decisionDate:'',decisionIssuer:'',approvedAmount:'',receipts:[]};}
// A new calculation timestamp alone does not change an identical allocation.
function available(d:Record<string,unknown>,r:RefundInput):boolean{return !!r.source&&refundSources(d).some(s=>s.allocationKey===r.source!.allocationKey&&s.closureId===r.source!.closureId&&s.entityRef===r.source!.entityRef&&s.origin===r.source!.origin&&s.period===r.source!.period&&JSON.stringify([s.credit,s.carry,s.requested])===JSON.stringify([r.source!.credit,r.source!.carry,r.source!.requested]));}
function identity(r:RefundInput):string{return JSON.stringify([norm(r.source?.entityRef??''),r.source?.origin,r.source?.period,r.source?.closureId]);}
function uses(d:Record<string,unknown>,b:RefundBook):RefundInput[]{return[...b.rows,...Object.entries(maps(d)).filter(([id])=>id!==scope(d)).flatMap(([,v])=>{validate(v);return v.rows.filter(r=>r.source?.origin==='effective');})];}
function receiptIdentity(r:RefundInput,p:RefundReceipt):string{return JSON.stringify([norm(r.source?.entityRef??''),r.source?.origin,norm(p.reference)]);}
function past(d:Record<string,unknown>,b:RefundBook):RefundInput[]{return Object.entries({...maps(d),[scope(d)]:b}).flatMap(([context,v])=>[...(v.history??[]),...(v.result?[v.result]:[])].flatMap(c=>c.inputs.filter(r=>context===scope(d)||r.source?.origin==='effective')));}
function key(d:Record<string,unknown>,b:RefundBook):string{const all=uses(d,b),old=past(d,b);return closureFingerprint([b.rows,b.rows.map(r=>available(d,r)),all.map(r=>[r.id,identity(r),r.receipts.map(p=>receiptIdentity(r,p))]),b.rows.map(r=>old.filter(v=>v.id!==r.id&&(v.stage==='submitted'||v.decision!=='pending'||v.receipts.length)).some(v=>identity(v)===identity(r)||r.receipts.some(p=>v.receipts.some(q=>receiptIdentity(v,q)===receiptIdentity(r,p)))))]);}
export function refundCurrent(d:Record<string,unknown>,b:RefundBook):boolean{return!!b.result&&b.result.key===key(d,b)&&b.result.rows.length===b.rows.length&&b.rows.every(r=>b.result!.rows.some(v=>v.id===r.id&&v.inputKey===closureFingerprint(r)));}
export function recordRefund(d:Record<string,unknown>,b:RefundBook,now=new Date().toISOString()):RefundBook{
validate(b);if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identidade de pedido repetida.');const all=uses(d,b),old=past(d,b);
const rows=b.rows.map(row=>{
const r:RefundResult={id:row.id,status:'indeterminate',reason:'',inputKey:closureFingerprint(row),eligibility:'not_demonstrated',requested:null,reported:null,approved:null,received:null,awaitingReceipt:null,notGranted:null,source:refundSource};const stop=(reason:string)=>({...r,reason});const s=row.source;
if(!s||!available(d,row)||d.currency!=='MZN'||!/^2026-(0[1-9]|1[0-2])$/.test(s.period))return stop('Escolha afectação26/27 actual e recuperável do mesmo período2026/MZN;crédito já reportado não é pedido disponível.');
if(all.filter(v=>identity(v)===identity(row)).length>1||old.some(v=>v.id!==row.id&&identity(v)===identity(row)&&(v.stage==='submitted'||v.decision!=='pending'||v.receipts.length)))return stop('Parcela já ligada a outro pedido no âmbito recuperado;sem pedir ou consumir duas vezes o mesmo crédito.');
r.requested=s.requested;r.reported=s.carry;
let eligibilityReason='';
if(!row.ground||!period(row.originPeriod)||row.originPeriod>s.period||!date(row.entitlementDate)||row.entitlementDate.slice(0,7)!==row.originPeriod||!row.groundRef.trim()||row.factsConfirmed!=='yes'||row.oldestFirst!=='yes')eligibilityReason='Fundamento/origem/nascimento do crédito,suporte ou consideração dos créditos mais antigos ainda não demonstrados;sem direito presumido.';
if(!date(row.requestDate)||row.requestDate.slice(0,7)<s.period||!row.requestReference.trim())return stop('Identifique referência e data do pedido ou preparação;separar da decisão.');
if(!eligibilityReason){
const lastYear=Number(row.entitlementDate.slice(0,4))+10,lastMonth=Number(row.entitlementDate.slice(5,7)),lastDay=Math.min(Number(row.entitlementDate.slice(8)),new Date(Date.UTC(lastYear,lastMonth,0)).getUTCDate());const anniversary=`${lastYear}-${String(lastMonth).padStart(2,'0')}-${String(lastDay).padStart(2,'0')}`;
const above=(n:bigint)=>BigInt(subtractFractions(s.credit,fraction(n)).numerator)>0n;
if(row.requestDate<row.entitlementDate||row.requestDate>anniversary){r.eligibility='incompatible';eligibilityReason='Datas declaradas incompatíveis com nascimento do direito/limite10anos de21(15),Lei10/2025;conservar documentos e identificar eventual excepção.';}
else if(row.ground==='four_months'&&(months(row.originPeriod,s.period)<4||!above(100000n))||row.ground==='high_credit'&&!above(500000n)||row.ground==='cessation'&&(!date(row.cessationDate)||row.cessationDate>row.requestDate)){r.eligibility='incompatible';eligibilityReason='Factos declarados não demonstram21(6),quatro meses/crédito superior100000;21(7a),superior500000;ou21(7b),cessação identificada.';}
else r.eligibility='compatible';
}
if(row.stage==='preparation'&&(row.decision!=='pending'||row.receipts.length))return stop('Preparação não é pedido apresentado;registe apresentação externa antes de decisão/recebimento.');
if(row.decision==='pending'){
if(row.receipts.length)return stop('Recebimento sem decisão identificada;conservar registo sem o presumir reconhecido.');
if(eligibilityReason)return stop(eligibilityReason+' Pedido/estado conservados;formalidades regulamentares não automatizadas.');
r.status='determined';r.reason=row.stage==='preparation'?'Pedido em preparação;parcela separada do reporte,sem submissão,decisão ou recebimento.':'Pedido apresentado fora doInvesteMoz;decisão e recebimento ainda não identificados.';return r;
}
if(!row.decisionReference.trim()||!row.decisionIssuer.trim()||!date(row.decisionDate)||row.decisionDate<row.requestDate)return stop('Identifique decisão recebida,número/data e entidade emitente;pedido não equivale a deferimento.');
let approved:FiscalFraction;try{approved=decimalFraction(row.approvedAmount);}catch{return stop('Montante reconhecido por identificar;indeferimento exigezero expresso,sem transformar desconhecido emzero.');}
if(BigInt(subtractFractions(approved,s.requested).numerator)>0n||row.decision==='approved'&&JSON.stringify(approved)!==JSON.stringify(s.requested)||row.decision==='rejected'&&BigInt(approved.numerator)!==0n||row.decision==='partial'&&(BigInt(approved.numerator)<=0n||BigInt(subtractFractions(s.requested,approved).numerator)<=0n))return stop('Montante reconhecido incompatível com decisão/pedido;conservar o âmbito exacto,sem generalizar o deferimento.');
r.approved=approved;r.notGranted=subtractFractions(s.requested,approved);const receipts:FiscalFraction[]=[];
for(const p of row.receipts){const id=receiptIdentity(row,p);if(!p.reference.trim()||!date(p.date)||p.date<row.decisionDate||all.flatMap(v=>v.receipts.filter(q=>receiptIdentity(v,q)===id)).length>1||old.some(v=>v.id!==row.id&&v.receipts.some(q=>receiptIdentity(v,q)===id)))return stop('Recebimento sem referência/data posterior à decisão ou já registado noutro pedido;sem duplicar caixa.');try{const v=decimalFraction(p.amount);if(BigInt(v.numerator)<=0n)return stop('Recebimento exige montante positivo identificado.');receipts.push(v);}catch{return stop('Montante recebido por identificar;decisão não cria recebimento.');}}
r.received=addFractions(receipts);if(BigInt(subtractFractions(r.received,approved).numerator)>0n)return stop('Recebimentos excedem o montante reconhecido;reconcilie sem criar crédito ou caixa adicional.');r.awaitingReceipt=positiveFraction(subtractFractions(approved,r.received));r.status='determined';r.reason=(eligibilityReason?'Decisão externa documentada registada independentemente da avaliação automática do fundamento: '+eligibilityReason+' ':'')+'Pedido,decisão e recebimento separados;parcela27 não é reutilizada em26. Parte não deferida/indeferimento não libertam crédito automaticamente;eventual retorno exige procedimento documentado e reconciliação,sem duplicar20. Recebimento é registo declarado,sem execução bancária ou alteração silenciosa de caixa. Formalidades e suspensões não automatizadas;150dias não são promessa de recebimento.';return r;
});
const result={calculatedAt:now,key:key(d,b),inputs:structuredClone(b.rows),rows};return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};
}
export function refundReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=refundBook(d);if(!b.rows.length)return[];const out:Array<[string,string,string,string]>=[['IVA mensal','L3-D — reembolso geral',refundCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever entradas/afectação',refundSource]],fmt=(v:FiscalFraction|null|undefined)=>v?fractionPresentation(v).decimal:'Indeterminado';for(const r of b.result?.rows??[]){const input=b.result!.inputs.find(v=>v.id===r.id),s=input?.source;out.push(['IVA mensal',`Reembolso — ${s?.period??'por identificar'}`,`Fundamento:${r.eligibility==='compatible'?'Compatível com factos declarados':r.eligibility==='incompatible'?'Divergência nos factos — ver suporte':'Não demonstrado automaticamente'};Pedido27:${fmt(r.requested)};reporte26 separado:${fmt(r.reported)};reconhecido:${fmt(r.approved)};recebido:${fmt(r.received)};aguarda recebimento:${fmt(r.awaitingReceipt)};não deferido:${fmt(r.notGranted)}`,`${s?.entityRef??''};${scope(d)};MZN;${s?.origin==='effective'?'Dados efectivos declarados':'Pressuposto de simulação'};${input?.stage==='submitted'?'Apresentado fora doInvesteMoz':'Em preparação'};pedido ${input?.requestReference??''},${input?.requestDate??''};origem${input?.originPeriod??''},nascimento${input?.entitlementDate??''};${input?.groundRef??''};${r.reason}`]);out.push(['IVA mensal',`Decisão e recebimento — ${s?.period??''}`,`Decisão:${input?.decision==='approved'?'Deferido':input?.decision==='partial'?'Deferimento parcial':input?.decision==='rejected'?'Indeferido':'Pendente'};documento${input?.decisionReference??''};${input?.decisionDate??''};${input?.decisionIssuer??''};recebimentos:${input?.receipts.map(p=>`${p.reference};${p.date};${p.amount}MZN`).join(' | ')||'nenhum registado'}`,`Afectação do fecho${s?.closureId??''},versão${s?.calculatedAt??''};25:${fmt(s?.credit)}=26:${fmt(s?.carry)}+27:${fmt(s?.requested)};âmbito apenas desta parcela;versão guardada${b.result!.calculatedAt};${refundSource}`]);}return out;}catch{return[['IVA mensal','L3-D','Pedido não recuperável','Originais preservados;sem concessão/recebimento presumidos.']];}}
+2
View File
@@ -17,6 +17,7 @@ import { modeloAPurchaseReferences } from './iva-modelo-a.ts';
import { acquisitionReportRows } from './iva-acquisitions.ts';
import {closureReportRows} from './iva-closure.ts';
import {substitutionReportRows} from './iva-substitution.ts';
import {refundReportRows} from './iva-refund.ts';
import { importExemptionReportRows } from './iva-import-exemptions.ts';
import { publicBaseReportRows } from './iva-public-authority-base.ts';
import { commissionBaseReportRows } from './iva-commission-base.ts';
@@ -650,6 +651,7 @@ export function buildProjectDossierRows(
rows.push(...acquisitionReportRows(draft));
rows.push(...closureReportRows(draft));
rows.push(...substitutionReportRows(draft));
rows.push(...refundReportRows(draft));
rows.push(...importExemptionReportRows(draft));
rows.push(...publicBaseReportRows(draft));
rows.push(...commissionBaseReportRows(draft));
+1 -1
View File
@@ -18,7 +18,7 @@ final class IvaImportSuppliesDraftTest extends TestCase
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$snapshot = json_encode(['padding' => str_repeat('x', 4000000)], JSON_THROW_ON_ERROR);
$snapshot = json_encode(['padding' => str_repeat('x', 6000000)], JSON_THROW_ON_ERROR);
$this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => ['modelo_a_snapshot' => $snapshot]], ['Idempotency-Key' => (string) Str::uuid()])
->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot.modelo_a_snapshot');
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
final class IvaRefundDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_refund_recovers_allocation_request_decision_receipts_history_and_all_prior_maps(): void
{
$target=DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test',$target->name);$this->assertSame(55441,(int)$target->port);
config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false]);$this->actingAs(User::factory()->create(['email_verified_at'=>now()]));
$draft=json_decode(file_get_contents(base_path('outputs/iva-l3-d-20261025/calculated.json')),true,flags:JSON_THROW_ON_ERROR);
$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>[],'economic_snapshot'=>$draft],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
$recovered=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'],$recovered['modelo_a_snapshot']);
$s=json_decode($recovered['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['ivaRefunds']['opportunity:374'];$this->assertCount(1,$b['rows']);$this->assertCount(1,$b['history']);
$r=$b['result']['rows'][0];$this->assertSame('determined',$r['status']);$this->assertSame('compatible',$r['eligibility']);
foreach(['requested'=>'600000','reported'=>'200000','approved'=>'550000','received'=>'450000','awaitingReceipt'=>'100000','notGranted'=>'50000']as $f=>$v)$this->assertSame(['numerator'=>$v,'denominator'=>'1'],$r[$f]);
$input=$b['result']['inputs'][0];$this->assertSame('SYNTHETIC QA pedidoR1',$input['requestReference']);$this->assertSame('2026-02-15',$input['requestDate']);$this->assertSame('partial',$input['decision']);$this->assertSame('SYNTHETIC QA decisãoD1',$input['decisionReference']);$this->assertCount(2,$input['receipts']);
$allocation=$s['ivaClosures']['opportunity:374']['result']['rows'][0];foreach(['25'=>'800000','26'=>'200000','27'=>'600000']as $f=>$v)$this->assertSame(['numerator'=>$v,'denominator'=>'1'],$allocation['fields'][(string)$f]);
$accepted=json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-l3d-preserved.json')),true,flags:JSON_THROW_ON_ERROR);$accepted=json_decode($accepted['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);
foreach($accepted as $key=>$map){if($key==='version')continue;foreach($map as $context=>$original)$this->assertSame($original,$s[$key][$context]);}
$frozen=json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-l3ab-preserved.json')),true,flags:JSON_THROW_ON_ERROR);
foreach(json_decode($frozen['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR)as $key=>$map){if($key==='version')continue;foreach($map as $context=>$original)$this->assertSame($original,$s[$key][$context]);}
$old=$s['ivaClosures']['opportunity:373']['result']['rows'][2];$this->assertSame(['numerator'=>'72','denominator'=>'1'],$old['fields']['24']);$this->assertSame(['numerator'=>'30','denominator'=>'1'],$old['paid']);$this->assertSame(['numerator'=>'42','denominator'=>'1'],$old['outstanding']);
file_put_contents(base_path('outputs/iva-l3-d-20261025/recovered.json'),json_encode($recovered,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {recordSales,saveSalesBook} from '../../resources/js/lib/iva-operations.ts';
import {recordAcquisitions,saveAcquisitionBook,acquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {closureBook,closureSources,newClosure,recordClosure,saveClosureBook,adoptPreviousCredit,newClosureCredit,closureCurrent} from '../../resources/js/lib/iva-closure.ts';
import {substitutionBook,substitutionCurrent} from '../../resources/js/lib/iva-substitution.ts';
import {refundBook,refundSources,newRefund,recordRefund,saveRefundBook,refundCurrent,refundReportRows} from '../../resources/js/lib/iva-refund.ts';
const seed=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l3d-preserved.json',import.meta.url),'utf8')),older=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-l3ab-preserved.json',import.meta.url),'utf8'));
function fixture(){const d=structuredClone(seed),base=JSON.parse(older.modelo_a_snapshot),s=JSON.parse(d.modelo_a_snapshot);for(const[k,v]of Object.entries(base))if(k!=='version')s[k]={...v,...s[k]};d.modelo_a_snapshot=JSON.stringify(s);d.context_opportunity='opportunity:374';
const sales=structuredClone(s.salesOperations['opportunity:373']);d.modelo_a_snapshot=saveSalesBook(d,recordSales(d,sales));const b=structuredClone(s.books['opportunity:373']);b.rows[0].net='5001000';b.rows[0].vat='800160';b.rows[0].gross='5801160';delete b.result;delete b.history;d.modelo_a_snapshot=saveAcquisitionBook(d,recordAcquisitions(d,b,'2026-10-25T13:00:00Z'));
const row={...newClosure('refund-Jan',closureSources(d).find(s=>s.period==='2026-01'),'SYNTHETIC QA entidade374'),firstDeclaration:'yes',filing:'timely',submissionDate:'2026-02-15',filingRef:'SYNTHETIC QA prazo declarado',creditsComplete:'yes',interest:'none',interestRef:'SYNTHETIC QA sem juros',destination:'refund',refundAmount:'600000'};
const ab=recordClosure(d,{version:1,rows:[row]},'2026-10-25T13:01:00Z');d.modelo_a_snapshot=saveClosureBook(d,ab);assert.equal(ab.result.rows[0].status,'determined',ab.result.rows[0].reason);
const input={...newRefund('refund1',refundSources(d)[0]),ground:'high_credit',originPeriod:'2026-01',entitlementDate:'2026-01-10',groundRef:'SYNTHETIC QA extractos/origem800000 e créditos mais antigos',factsConfirmed:'yes',oldestFirst:'yes',requestReference:'SYNTHETIC QA pedidoR1',requestDate:'2026-02-15',stage:'submitted',decision:'partial',decisionReference:'SYNTHETIC QA decisãoD1',decisionDate:'2026-03-01',decisionIssuer:'SYNTHETIC QA entidade emitente declarada',approvedAmount:'550000',receipts:[{id:'receipt1',reference:'SYNTHETIC QA recebimentoR1a',date:'2026-03-05',amount:'200000'},{id:'receipt2',reference:'SYNTHETIC QA recebimentoR1b',date:'2026-03-10',amount:'250000'}]};return{d,b:{version:1,rows:[input]}};
}
const n=v=>v&&`${v.numerator}/${v.denominator}`;
test('25 is allocated once between26 and27;requested,approved,received and waiting remain distinct',()=>{const{d,b}=fixture(),ab=closureBook(d).result.rows[0];assert.equal(n(ab.fields['25']),'800000/1');assert.equal(n(ab.fields['26']),'200000/1');assert.equal(n(ab.fields['27']),'600000/1');const r=recordRefund(d,b).result.rows[0];assert.equal(r.status,'determined',r.reason);assert.equal(r.eligibility,'compatible');assert.equal(n(r.requested),'600000/1');assert.equal(n(r.reported),'200000/1');assert.equal(n(r.approved),'550000/1');assert.equal(n(r.received),'450000/1');assert.equal(n(r.awaitingReceipt),'100000/1');assert.equal(n(r.notGranted),'50000/1');});
test('preparation,presented pending,approval and rejection do not automatically create receipt or release credit',()=>{const{d,b}=fixture();for(const stage of ['preparation','submitted']){const v=structuredClone(b);Object.assign(v.rows[0],{stage,decision:'pending',receipts:[]});const r=recordRefund(d,v).result.rows[0];assert.equal(r.status,'determined');assert.equal(r.approved,null);assert.equal(r.received,null);}
const full=structuredClone(b);Object.assign(full.rows[0],{decision:'approved',approvedAmount:'600000',receipts:[]});let r=recordRefund(d,full).result.rows[0];assert.equal(n(r.received),'0/1');assert.equal(n(r.awaitingReceipt),'600000/1');const rejected=structuredClone(b);Object.assign(rejected.rows[0],{decision:'rejected',approvedAmount:'0',receipts:[]});r=recordRefund(d,rejected).result.rows[0];assert.equal(r.status,'determined');assert.equal(n(r.notGranted),'600000/1');assert.equal(n(closureBook(d).result.rows[0].fields['26']),'200000/1');});
test('source,request and decision details are required;unknown never becomes entitlement or cash',()=>{const{d,b}=fixture();for(const patch of [{source:null},{requestReference:''},{requestDate:''},{decisionReference:''},{decisionDate:'2026-02-01'},{decisionIssuer:''},{approvedAmount:''},{stage:'preparation'}]){const v=structuredClone(b);Object.assign(v.rows[0],patch);assert.equal(recordRefund(d,v).result.rows[0].status,'indeterminate',JSON.stringify(patch));}
const forged=structuredClone(b);forged.rows[0].source.requested={numerator:'700000',denominator:'1'};assert.equal(recordRefund(d,forged).result.rows[0].requested,null);});
test('a documented external decision is recorded without turning an unimplemented/unknown basis into a new approval gate',()=>{const{d,b}=fixture();for(const patch of [{ground:''},{factsConfirmed:''},{oldestFirst:''},{entitlementDate:''},{groundRef:''}]){const v=structuredClone(b);Object.assign(v.rows[0],patch);const r=recordRefund(d,v).result.rows[0];assert.equal(r.status,'determined',r.reason);assert.equal(r.eligibility,'not_demonstrated');assert.equal(n(r.approved),'550000/1');assert.equal(n(r.received),'450000/1');v.rows[0].decision='pending';v.rows[0].receipts=[];const pending=recordRefund(d,v).result.rows[0];assert.equal(pending.status,'indeterminate');assert.equal(pending.approved,null);}});
test('current four-month/high-credit conditions and ten-year limit are applied without obsolete50percent rule',()=>{const{d,b}=fixture();const v=structuredClone(b);Object.assign(v.rows[0],{ground:'four_months',originPeriod:'2025-09',entitlementDate:'2025-09-10'});assert.equal(recordRefund(d,v).result.rows[0].eligibility,'compatible');v.rows[0].originPeriod='2025-10';v.rows[0].entitlementDate='2025-10-10';assert.equal(recordRefund(d,v).result.rows[0].eligibility,'incompatible');v.rows[0].originPeriod='2015-01';v.rows[0].entitlementDate='2015-01-10';assert.equal(recordRefund(d,v).result.rows[0].eligibility,'incompatible');
const cessation=structuredClone(b);Object.assign(cessation.rows[0],{ground:'cessation',cessationDate:'2026-01-31'});assert.equal(recordRefund(d,cessation).result.rows[0].eligibility,'compatible');cessation.rows[0].cessationDate='2026-03-01';assert.equal(recordRefund(d,cessation).result.rows[0].eligibility,'incompatible');});
test('thresholds are strictly greater;fractional credit remains exact after allocation',()=>{for(const [vat,request,ground,expected]of [['500160','100000','high_credit','incompatible'],['100160','50000','four_months','incompatible'],['500161','100000','high_credit','compatible']]){const{d,b}=fixture(),acq=acquisitionBook(d);acq.rows[0].vat=vat;acq.rows[0].gross=String(Number(acq.rows[0].net)+Number(vat));d.modelo_a_snapshot=saveAcquisitionBook(d,recordAcquisitions(d,acq));const ab=closureBook(d);ab.rows[0].source=closureSources(d).filter(s=>s.period==='2026-01').at(-1);ab.rows[0].refundAmount=request;d.modelo_a_snapshot=saveClosureBook(d,recordClosure(d,ab));Object.assign(b.rows[0],{source:refundSources(d)[0],ground,originPeriod:'2025-09',entitlementDate:'2025-09-10',stage:'preparation',decision:'pending',receipts:[]});assert.equal(recordRefund(d,b).result.rows[0].eligibility,expected);}
const{d}=fixture(),acq=acquisitionBook(d);acq.rows[0].vat='800161';acq.rows[0].gross='5801161';acq.rows[0].d.numerator='1';acq.rows[0].d.denominator='3';d.modelo_a_snapshot=saveAcquisitionBook(d,recordAcquisitions(d,acq));const ab=closureBook(d);ab.rows[0].source=closureSources(d).filter(s=>s.period==='2026-01').at(-1);ab.rows[0].refundAmount='200000';const result=recordClosure(d,ab);assert.equal(n(result.result.rows[0].fields['25']),'799681/3');assert.equal(n(result.result.rows[0].fields['26']),'199681/3');assert.equal(n(result.result.rows[0].fields['27']),'200000/1');});
test('request/receipt duplication,over-receipt and decisions beyond request are refused',()=>{const{d,b}=fixture();const copy=structuredClone(b);copy.rows.push({...structuredClone(copy.rows[0]),id:'copy'});assert.ok(recordRefund(d,copy).result.rows.every(r=>r.requested===null));for(const patch of [{approvedAmount:'600001'},{decision:'approved',approvedAmount:'550000'},{decision:'rejected',approvedAmount:'1'},{decision:'pending'}]){const v=structuredClone(b);Object.assign(v.rows[0],patch);assert.equal(recordRefund(d,v).result.rows[0].status,'indeterminate');}const receipt=structuredClone(b);receipt.rows[0].receipts.push({...receipt.rows[0].receipts[0],id:'copy'});assert.equal(recordRefund(d,receipt).result.rows[0].received,null);const over=structuredClone(b);over.rows[0].receipts[0].amount='400000';assert.equal(recordRefund(d,over).result.rows[0].status,'indeterminate');});
test('submitted refund locks the allocation even after current preparation is removed;histories are not erased',()=>{const{d,b}=fixture();const book=recordRefund(d,b);d.modelo_a_snapshot=saveRefundBook(d,book);const ab=closureBook(d);ab.rows[0].destination='report';assert.match(recordClosure(d,ab).result.rows[0].reason,/Crédito ligado/);const removed=recordRefund(d,{...book,rows:[]});d.modelo_a_snapshot=saveRefundBook(d,removed);assert.match(recordClosure(d,ab).result.rows[0].reason,/Crédito ligado/);assert.ok(removed.history.length);});
test('only residual26 travels to next month;old whole-credit carry requires explicit adoption',()=>{const{d,b}=fixture();let ab=closureBook(d);const full=structuredClone(ab);full.rows[0].destination='report';const before=recordClosure(d,full);const feb={...newClosure('refund-Feb',closureSources(d).find(s=>s.period==='2026-02'),'SYNTHETIC QA entidade374'),firstDeclaration:'yes',filing:'timely',submissionDate:'2026-03-15',filingRef:'SYNTHETIC QA prazo',creditsComplete:'yes',interest:'none',interestRef:'SYNTHETIC QA sem juros',destination:'report',credits:[adoptPreviousCredit(newClosureCredit('carry','previous'),before.result.rows[0])]};ab=recordClosure(d,{...ab,rows:[...ab.rows,feb]});assert.equal(ab.result.rows[1].status,'indeterminate');d.modelo_a_snapshot=saveClosureBook(d,ab);assert.deepEqual(refundSources(d),[]);ab.rows[1].credits[0]=adoptPreviousCredit(ab.rows[1].credits[0],ab.result.rows[0]);ab=recordClosure(d,ab);assert.equal(n(ab.result.rows[1].fields['19']),'200000/1');assert.equal(n(ab.result.rows[1].fields['26']),'199920/1');d.modelo_a_snapshot=saveClosureBook(d,ab);assert.equal(recordRefund(d,b).result.rows[0].status,'determined');});
test('effective carry already used in a later declaration cannot be reallocated retroactively for a refund',()=>{const{d}=fixture(),acq=acquisitionBook(d);acq.rows[0].origin='effective';d.modelo_a_snapshot=saveAcquisitionBook(d,recordAcquisitions(d,acq));let ab=closureBook(d);Object.assign(ab.rows[0],{origin:'effective',source:closureSources(d).filter(s=>s.period==='2026-01').at(-1),destination:'report'});ab=recordClosure(d,ab);assert.equal(ab.result.rows[0].status,'determined');const feb={...structuredClone(ab.rows[0]),id:'actual-Feb',period:'2026-02',source:closureSources(d).filter(s=>s.period==='2026-02').at(-1),submissionDate:'2026-03-15',credits:[adoptPreviousCredit(newClosureCredit('actual-carry','previous'),ab.result.rows[0])]};ab=recordClosure(d,{...ab,rows:[...ab.rows,feb]});assert.equal(ab.result.rows[1].status,'determined');d.modelo_a_snapshot=saveClosureBook(d,ab);ab.rows[0].destination='refund';ab.rows[0].refundAmount='600000';const changed=recordClosure(d,ab);assert.match(changed.result.rows[0].reason,/Reporte efectivo já utilizado/);assert.equal(changed.result.rows[0].status,'indeterminate');});
test('unsubmitted allocation may be changed explicitly,but old reported credit cannot fund the old request',()=>{const{d,b}=fixture();b.rows[0].stage='preparation';b.rows[0].decision='pending';b.rows[0].receipts=[];let book=recordRefund(d,b);d.modelo_a_snapshot=saveRefundBook(d,book);const ab=closureBook(d);ab.rows[0].destination='report';d.modelo_a_snapshot=saveClosureBook(d,recordClosure(d,ab));assert.equal(refundCurrent(d,book),false);assert.equal(recordRefund(d,book).result.rows[0].requested,null);});
test('save/recover preserves all prior L1/L2/AB/C maps and frozen report;recalculation appends history',()=>{const{d,b}=fixture(),before=JSON.parse(d.modelo_a_snapshot);let book=recordRefund(d,b);d.modelo_a_snapshot=saveRefundBook(d,book);const after=JSON.parse(d.modelo_a_snapshot);for(const[k,v]of Object.entries(before))assert.deepEqual(after[k],v,k);assert.ok(refundCurrent(d,refundBook(d)));const cDraft={...d,context_opportunity:'opportunity:373'};assert.ok(substitutionCurrent(cDraft,substitutionBook(cDraft)));assert.ok(closureCurrent(cDraft,closureBook(cDraft)));book.rows[0].requestReference='deliberate edit';assert.equal(refundCurrent(d,book),false);d.modelo_a_snapshot=saveRefundBook(d,book);assert.doesNotMatch(refundReportRows(d).map(r=>r.join(' ')).join(' '),/deliberate edit/);const prior=structuredClone(book.result);book=recordRefund(d,book);assert.deepEqual(book.history.at(-1),prior);});
test('export synthetic frozen refund reconciliation for guardedAPI and actual component proof',()=>{const{d,b}=fixture();const first=recordRefund(d,b,'2026-10-25T13:02:00Z'),book=recordRefund(d,first,'2026-10-25T13:03:00Z');d.modelo_a_snapshot=saveRefundBook(d,book);assert.ok(refundCurrent(d,book));if(process.env.IVA_L3D_CALCULATED)fs.writeFileSync(process.env.IVA_L3D_CALCULATED,JSON.stringify(d));});