feat(irpc): prepare compatible L6 schedules and scoped form mappings in QA
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-25 22:29:06 +02:00
parent 82d0d05887
commit a2de725901
16 changed files with 421 additions and 29 deletions
@@ -544,3 +544,17 @@ Esta discriminação actualiza **AUD-SIM-INT02 na mesma matriz**, com o âmbito
**Informação do caso, distinta de desenvolvimento em falta:** falta de documento, NUIT, completude anual, montante, prova de saldo ou qualificação dentro de uma regra já implementada mantém o caso indeterminado, mas não constitui por si só outra tarefa deL6. O mesmo vale para a ausência de uma nova prova de uma funcionalidade já aceite. Não exigir nova aprovação genérica das fontes oficiais, novo aprovador ou repetição deL3/ISPC/câmbio/L5. Anos não incluídos no recorte temporal não se tornam automaticamente um projecto de cobertura histórica integral.
**Ordem de continuação delimitada:** ajustamentos ordinários/juros financeiros e amortizações; depois variações/periodização e resultado negativo; ampliar apenas os casos normais concretamente abrangidos e concluir a representação/correspondência dos dados deL6. Cada fecho deve demonstrar o delta e substituir a pendência correspondente nesta mesma linha/nota. Não há estimativa nova nem declaração deL6 integralmente concluído nesta revisão documental.
## 25/10/2026 — L6: três conjuntos compatíveis em QA sobre 2f684e59
Actualização de **AUD-SIM-INT02**, na matriz existente. O percurso instalado em `2f684e59` permanece aceite: lucro6700 → perdas2300 → matéria colectável4400 → colecta1408MZN; saldo anual continua indeterminado emL8. Este incremento é **QA, sem instalação**. Não repete as provas de L3, ISPC, câmbio ou L5; conserva a fotografia4 e o histórico sem recalcular a aceitação anterior.
| Conjunto | Delta executável demonstrado | Efeito e prova nova | Pendência concreta que subsiste |
|---|---|---|---|
| 1 — ajustamentos, juros, variações e periodização | Acréscimos36(1)a/b/g com parcela incluída e origem reconciliada; juros financeiros22c de exploração documentados, sem capitalização/relações/limites especiais; entradas de capital21a e distribuições24c externas ao resultado sem efeito fiscal; variações positivas21 e negativas22/24 ordinárias; especialização18(1) de custo/proveito entre exercício corrente e seguinte. Documento/linha únicos entre mapas, sinal e soma bruta por tema conferidos. | Resultado financeiro6240 + acréscimos anteriores220 + imposto de terceiro250 + variação positiva200 − negativa50 + custo de2027 contabilizado em2026 de100 + diferença de amortização20 = lucro6980. Juros reconhecidos240, compensatórios acrescidos120, remanescente ordinário120; pagamentos continuam separados. Informação insuficiente mantém os efeitos independentes visíveis e impede somente os totais dependentes. | Correcções ordinárias ainda sem regra: encargos e limites36(1)e/f/h/i/n, provisões28/29, operações plurianuais19 e componentes de exercícios anteriores18(2). Juros capitalizáveis/relações especiais e limites exigem o tratamento próprio; não são aceites por simples selecção de juros ordinários. A tributação autónoma que possa coexistir pertence aL7. |
| 2 — amortizações e perda corrente | Decreto72/2013: quotas anuais constantes, custo/residual/acumulado, limite do saldo e vida útil; tabelaII/grupo3/item9 computadores25% e9.1 servidores20%, sem taxa específica da tabelaI prevalecente. Soma da depreciação contabilística por activo reconciliada com o plano; nenhum efeito de mapa incompleto aplicado. Perda corrente separada das perdas anteriores, sem auto-reporte. | Servidor500 ×20%=100 fiscal, depreciação contabilística120 → acréscimo20. Alterar quota/categoria, exceder vida útil ou não reconciliar a origem mantém o lucro dependente indeterminado. Outro exemplo: resultado fiscal−3820 → perda corrente3820 → base0/colecta0 apenas com suporte e conferência completos, sem dedução de perdas anteriores a si própria nem criação de saldo oficial. | Restante catálogo sectorial/genérico de activos, taxas reduzidas dentro dos limites, duodécimos, quotas decrescentes, bens usados/reavaliados, activos intangíveis e recuperação de quotas contabilizadas em anos anteriores. Não declarar toda a amortização fiscal concluída por estes dois itens. |
| 3 — âmbitos, representação e M20/M22 | EP no regime geral, resultado completo exclusivamente imputável ao estabelecimento; ano civil ou período anual diferente autorizado7(2)/comunicado7(3), correspondência dos12 meses e termo em2026/MZN. Valores racionais exactos, sem arredondamento fiscal inventado. M22 campos259/260,269/270,276/277/278 e300 conforme aplicação; M20A.1 quadro4 contas6.1.1/6.2/6.5/6.9 com valores contabilísticos próprios. |6980 − perdas2300 = base4680; ×32%=colecta1497,60MZN, não saldo a pagar. EP com período01/07/2025–30/06/2026 demonstrado; período curto incompatível fica pendente. API privada recupera os quatro casos calculado/falta/perda/EP e os relatórios respectivos. M20 inteiro e M22 ao cêntimo apenas quando a representação é exacta; fracções conservadas e marcadas sem transcrição pronta. | Períodos curtos/transição/liquidação e reconciliação fiscal anual de planos noutra moeda (motor cambial aceite preservado). Representação fiscal de fracções de unidade/cêntimo por corresponder. No M20 faltam discriminação vendas/serviços, fornecimentos/outros gastos, imposto e resultado líquido; no M22 faltam a reconciliação do campo200 e detalhe dos restantes ajustamentos. Divergência delimitada de edições: folha M20A.1 do XLSX remete ao campo201/quadro8 do M22; PDF inventariado utiliza200/quadro10. Bloqueia esse transporte, não os outros campos. |
**Provas apenas do delta:** 14 testes frontend; API privada1 teste/78 asserções com transacção revertida na base QA127.0.0.1:55441; geração de routes/actions pelo procedimento existente, TypeScript e build; interface real com edição, taxa/categoria incompatível, falta de juros, gravação/recuperação, download e perda corrente; relatórios/dossier construídos a partir dos dados recuperados. Recibo `docs/investmoz/simulations/irpc-l6-compatible-sets-qa-20261025.json`; pacote/bundle em `outputs/irpc-l6-sets`. Sem alteração de schema, API, configuração, permissões ou dependências.
**Âmbitos próprios preservados:** L7 conserva benefícios, isenções, taxas reduzidas, tributações autónomas e operações especiais; L8 conserva retenções, pagamentos, créditos e saldo final. M22 campo302 não é preenchido só com a parcela normal300, nem os campos309–316 são preenchidos com zeros presumidos. As dez dúvidas deL5 continuam na mesma matriz, sem nova pesquisa e bloqueando apenas dependências efectivas. L6 continua parcial exclusivamente pelas pendências concretas da tabela; nenhum desses pontos reabre o percurso instalado.
@@ -0,0 +1,145 @@
{
"status": "three_compatible_L6_sets_QA_prepared_not_installed",
"installed_baseline": "2f684e59846187f3fb8a338d9d6f6a08cadb4d0d",
"branch": "codex/irpc-l6-compatible-sets-qa-20261025",
"QA_workspace": "C:\\laragon\\www\\portal-investidor-irpc-l6-sets-qa-20261025",
"scope": "Ordinary documented rules, annual MZN periods ending2026, resident general or attributable EP result; no assumed fiscal rounding or final settlement.",
"demonstrated": {
"accounting_profit": "6240MZN",
"installed_addbacks": "220MZN",
"new_effects": {
"third_party_tax": 250,
"patrimonial_gain": 200,
"patrimonial_cost": -50,
"timing": 100,
"depreciation_difference": 20
},
"taxable_profit": "6980MZN",
"previous_losses_explicit_deduction": "2300MZN",
"taxable_base": "4680MZN",
"general_rate": "32%",
"colecta": "1497.60MZN",
"payable": null,
"recoverable": null,
"current_loss_case": "3820MZN loss;known zero base/colecta;no official carryforward",
"EP_case": "2025-07-01 to2026-06-30,12 monthly periods"
},
"new_proofs": {
"frontend_tests": 14,
"private_API_tests": 1,
"private_API_assertions": 78,
"database": "127.0.0.1:55441/simulator_fiscal_delta_20261021_test;DatabaseTransactions rollback",
"routes_actions_generated": true,
"typescript": "passed",
"build": "passed",
"actual_UI": "field editing;unsupported asset rate;missing interest;save/recovery;download;current loss",
"dossier": "from API-recovered calculated/missing/loss/EP JSON"
},
"preservation": {
"installed4_fixture_matches_accepted_sha256": true,
"versions1_to4_keys_unchanged_without_optin": true,
"other_runtime_changes": false,
"schema_API_config_permissions_dependencies_changed": false,
"L3_ISPC_FX_L5_accepted_proofs": "reused;not repeated",
"L5_ten_questions": "unchanged,only dependent cases blocked",
"active_data_changed": false
},
"runtime_delta": [
{
"path": "resources/js/components/simulation-irpc-normal.tsx",
"sha256_LF": "e33b3839aac53a1b7ca2a76eda0ec4c54015a6d1c75e386aaeb5d342943f5965"
},
{
"path": "resources/js/components/simulation-irpc-schedules.tsx",
"sha256_LF": "eebd6d9e4100920a5ebf91b800537ffe301fd6a994b86a3b080d673f4a9c9a42"
},
{
"path": "resources/js/lib/irpc-normal-assessment.ts",
"sha256_LF": "1dc6adc0835af033ef296d12cdc1e9ef354b2f54cd36e2349b2df66e369c1ee3"
},
{
"path": "resources/js/lib/irpc-normal-base.ts",
"sha256_LF": "85fa1eb78ce2b8a144a9f0dcda93b542083a7ff119cbc43f4334c87284bfd312"
},
{
"path": "resources/js/lib/irpc-normal-forms.ts",
"sha256_LF": "4e986addb9f2d74e7a80345f8f1e4d8e0b2f562c867fa6eee84880d9f72a3649"
},
{
"path": "resources/js/lib/irpc-normal-profit.ts",
"sha256_LF": "d5a2b52f7deb4cf35651188e04f103fcba950a007e3aa4de5ba7017e686614df"
},
{
"path": "resources/js/lib/irpc-normal-schedules.ts",
"sha256_LF": "b0045fd2c072998ebb290c2a8332c26d05a1f5f746b3beb1805508147497cac8"
},
{
"path": "resources/js/lib/irpc-normal.ts",
"sha256_LF": "62d20bbcebb523f77fde950f623933a13f03283238665cf22fec9390f7ba9333"
}
],
"source_exemplar_added": {
"document": "Decreto72/2013,BR102,2suplemento,23December2013",
"official_issue": "https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-suplemento-n%C2%BA-2-de-231213-pag-1074-3-24-br-n%C2%BA-102-boletim-da-rep%C3%BAblica",
"gazette_copy": "https://www.lexlink.eu/FileGet.aspx?FileId=1152468",
"local_original": "C:\\laragon\\www\\portal-investidor-irpc-l6-sets-qa-20261025\\outputs\\irpc-l6-sets\\decreto-72-2013.pdf",
"sha256": "922ef67cb0c1111405c3c0a092d8c302a74fba8a34d627b6e9e13223612e0659",
"visual_pages": [
1,
2,
17
],
"rate_table": "tableII/group3/items9=25%,9.1=20%,Gazettepage1074(21)/PDF17",
"consultation": "preserved in QA proof archive;no publication/import into active library",
"legal_application": "articles3–7,ordinary annual constant method,tableI precedence;other categories/methods visibly unsupported",
"scan_status": "not asserted"
},
"existing_form_sources": {
"M22": "4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88;three accepted page images reused",
"M20_workbook": "1ceca6f4c6d77853c1b2b445876599b42093175fa89eecf40590180956d83a9e;existing structure inventory reused;no third-party sample values or cached formulas",
"concrete_conflict": "M20A.1 refers result to M22 field201/Q8;PDF M22 uses200/Q10;automatic transfer blocked only for that mapping"
},
"remaining": "Concrete cases enumerated in same matrix/manual section25/10;cost limits/provisions/prior-year timing;remaining asset categories/methods;short periods/annual foreign-currency reconciliation;fractional fiscal representation;remaining result/form mapping. L7 benefits/special cases and L8 settlement remain own scopes.",
"L6_complete": false,
"installation_performed": false,
"next_delivery": "Joint Git bundle from installed2f684e59;fresh authenticated destination backup/restore and delta required only at installation,not performed in QA.",
"proof_sha256": {
"api.txt": "3c3142dd55febf17c5e16e3cfabf2266759be7290b8d5ac92823e0f71cbffa6a",
"build-exit.txt": "13bf7b3039c63bf5a50491fa3cfd8eb4e699d1ba1436315aef9cbe5711530354",
"build.txt": "ba955cab87896b78652787f188c092e79108fc0e5289f237be1ddf21d73dbbf3",
"calculated.json": "109426b86ae7c14463699b1d197064617f124ecec0c55085d9e81aeddc505a5a",
"decreto-72-2013.pdf": "922ef67cb0c1111405c3c0a092d8c302a74fba8a34d627b6e9e13223612e0659",
"decreto-72-2013.txt": "bbbf05f109201a6d31e153046576a088e6c0cca97ada07833d0499698d0afcfa",
"decreto-72-page1.png": "bb681e5df24a13d8484a521ae58e480c9dd23b4fd09de3ff11d5287ddd26a4f9",
"decreto-72-page17.png": "bb3468fde9f95576d8d6392a4c906ae904941ad35843c472d3671492c24f0945",
"decreto-72-page2.png": "9bf0f11a4724b079b494f640e905eee5bd278b729091e67aa03d0bfe71f5c273",
"dossier-calculated.json": "e9487c0325db125f8a0ff5266d137141916f329283857f2efc1f64732353454b",
"dossier-ep.json": "57b55c94118bd9767f06b4cd0673bcecbd8882ca1ec8681c393216a0fc3a7ac7",
"dossier-loss.json": "28eb77522fea39947a6cffd7419c24cd37795f361559050377f79d8b9f1cd1fb",
"dossier-missing.json": "bda0394cb059a1800a5dcf4728ba58a5b3ee975a87a7a938ebf5673e0ace1587",
"dossier.mjs": "9ab7bc1ad64ab8f04936068df0f0555749048d9edd9f2d48e8246542204257dc",
"download.html": "d084c44b3b527c9ed5f3f2c2e7ccd88229b0a19b9956e153e63b14618020acdb",
"ep.json": "9ae0b179c100ea5e642ce2ed9362b74e60bf1040bf6e7227592db84220babb23",
"frontend.txt": "92d096de8d364f04339178feeb6e77cf247cbc4974974bce56ba7728401bbdc5",
"index.html": "09941968500d2910121bdcb0029e326fc6d44bb025b01766654289a28caa62fa",
"loss.json": "8a0e85205db146a02fc9e06a2667cdce5a00e48ed8a7e7568a50d361e40671b2",
"missing.json": "1f52b06286d0724fe27d24ab91386732c873fc65064521c270faf357ac193135",
"preparation.html": "7ac15855048512bec05690170368df1ee4d4879f224a9d6c56749c3500d2d9c0",
"qa.tsx": "e2f5dc5e9a9c142b3fe65889c0cefcbfb03fb3b46957437af1108dbf20569647",
"recovered-calculated.html": "7ac15855048512bec05690170368df1ee4d4879f224a9d6c56749c3500d2d9c0",
"recovered-calculated.json": "db98c66ba9cf46a67ead4267d7b3d25b08275a4d8a11bbd316f4bec2b77644af",
"recovered-ep.html": "47df28ba65d7825192ec7fd3fad61998f378829046df2c1b3ab74b7fb32980ef",
"recovered-ep.json": "56f12435276c4cf9604f10e8c53bba61f1eb84dcaa5b911ac06242112d740764",
"recovered-loss.html": "a3f70ddcfe4a10a38110690b7583f1c89be6427fe347a321e9a395db6214fc6a",
"recovered-loss.json": "fe280337741a5004c13c83a41f83ca87c98c07f815fc87b617a0d5c8dbd03573",
"recovered-missing.html": "8989a835f9145e94434f31c90c96c801219494019fbc6167f1c8272d08c6debc",
"recovered-missing.json": "a4a24bc80dd6352c0883a1a555de9cd32a71335bd07b4826526ebe311700766f",
"types.txt": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"ui-calculated.png": "1578772454a4e5902d3ac8f607dbcda3b3c3b7e23d0870bbe233da036f15ba5e",
"ui-missing.png": "72d48f81bc67bd9342c5078d81044290912afacd305980806c42cd96b038a614",
"ui-proof.json": "7bd7a522b4307d8f72fb04317106c3b7cc53763fe8923017ae0f63a510b9bb9e",
"ui.cjs": "207eea61db65ac778f26f35beecd18387d6126b75e1effcf3e33c1e26e957215",
"ui.txt": "9b120d22b3e2539e8862c5ce5a973cce4fc2632a16680e7f997020f4e2cd0196",
"vite.config.ts": "11a4a8f0bf0394f380e731cfb30cf4a0f3de3c6f3734653491e1aa26f57315f1"
}
}
@@ -6061,3 +6061,17 @@ Esta discriminação actualiza **AUD-SIM-INT02 na mesma matriz**, com o âmbito
**Informação do caso, distinta de desenvolvimento em falta:** falta de documento, NUIT, completude anual, montante, prova de saldo ou qualificação dentro de uma regra já implementada mantém o caso indeterminado, mas não constitui por si só outra tarefa deL6. O mesmo vale para a ausência de uma nova prova de uma funcionalidade já aceite. Não exigir nova aprovação genérica das fontes oficiais, novo aprovador ou repetição deL3/ISPC/câmbio/L5. Anos não incluídos no recorte temporal não se tornam automaticamente um projecto de cobertura histórica integral.
**Ordem de continuação delimitada:** ajustamentos ordinários/juros financeiros e amortizações; depois variações/periodização e resultado negativo; ampliar apenas os casos normais concretamente abrangidos e concluir a representação/correspondência dos dados deL6. Cada fecho deve demonstrar o delta e substituir a pendência correspondente nesta mesma linha/nota. Não há estimativa nova nem declaração deL6 integralmente concluído nesta revisão documental.
## 25/10/2026 — L6: três conjuntos compatíveis em QA sobre 2f684e59
Actualização de **AUD-SIM-INT02**, na matriz existente. O percurso instalado em `2f684e59` permanece aceite: lucro6700 → perdas2300 → matéria colectável4400 → colecta1408MZN; saldo anual continua indeterminado emL8. Este incremento é **QA, sem instalação**. Não repete as provas de L3, ISPC, câmbio ou L5; conserva a fotografia4 e o histórico sem recalcular a aceitação anterior.
| Conjunto | Delta executável demonstrado | Efeito e prova nova | Pendência concreta que subsiste |
|---|---|---|---|
| 1 — ajustamentos, juros, variações e periodização | Acréscimos36(1)a/b/g com parcela incluída e origem reconciliada; juros financeiros22c de exploração documentados, sem capitalização/relações/limites especiais; entradas de capital21a e distribuições24c externas ao resultado sem efeito fiscal; variações positivas21 e negativas22/24 ordinárias; especialização18(1) de custo/proveito entre exercício corrente e seguinte. Documento/linha únicos entre mapas, sinal e soma bruta por tema conferidos. | Resultado financeiro6240 + acréscimos anteriores220 + imposto de terceiro250 + variação positiva200 − negativa50 + custo de2027 contabilizado em2026 de100 + diferença de amortização20 = lucro6980. Juros reconhecidos240, compensatórios acrescidos120, remanescente ordinário120; pagamentos continuam separados. Informação insuficiente mantém os efeitos independentes visíveis e impede somente os totais dependentes. | Correcções ordinárias ainda sem regra: encargos e limites36(1)e/f/h/i/n, provisões28/29, operações plurianuais19 e componentes de exercícios anteriores18(2). Juros capitalizáveis/relações especiais e limites exigem o tratamento próprio; não são aceites por simples selecção de juros ordinários. A tributação autónoma que possa coexistir pertence aL7. |
| 2 — amortizações e perda corrente | Decreto72/2013: quotas anuais constantes, custo/residual/acumulado, limite do saldo e vida útil; tabelaII/grupo3/item9 computadores25% e9.1 servidores20%, sem taxa específica da tabelaI prevalecente. Soma da depreciação contabilística por activo reconciliada com o plano; nenhum efeito de mapa incompleto aplicado. Perda corrente separada das perdas anteriores, sem auto-reporte. | Servidor500 ×20%=100 fiscal, depreciação contabilística120 → acréscimo20. Alterar quota/categoria, exceder vida útil ou não reconciliar a origem mantém o lucro dependente indeterminado. Outro exemplo: resultado fiscal−3820 → perda corrente3820 → base0/colecta0 apenas com suporte e conferência completos, sem dedução de perdas anteriores a si própria nem criação de saldo oficial. | Restante catálogo sectorial/genérico de activos, taxas reduzidas dentro dos limites, duodécimos, quotas decrescentes, bens usados/reavaliados, activos intangíveis e recuperação de quotas contabilizadas em anos anteriores. Não declarar toda a amortização fiscal concluída por estes dois itens. |
| 3 — âmbitos, representação e M20/M22 | EP no regime geral, resultado completo exclusivamente imputável ao estabelecimento; ano civil ou período anual diferente autorizado7(2)/comunicado7(3), correspondência dos12 meses e termo em2026/MZN. Valores racionais exactos, sem arredondamento fiscal inventado. M22 campos259/260,269/270,276/277/278 e300 conforme aplicação; M20A.1 quadro4 contas6.1.1/6.2/6.5/6.9 com valores contabilísticos próprios. |6980 − perdas2300 = base4680; ×32%=colecta1497,60MZN, não saldo a pagar. EP com período01/07/2025–30/06/2026 demonstrado; período curto incompatível fica pendente. API privada recupera os quatro casos calculado/falta/perda/EP e os relatórios respectivos. M20 inteiro e M22 ao cêntimo apenas quando a representação é exacta; fracções conservadas e marcadas sem transcrição pronta. | Períodos curtos/transição/liquidação e reconciliação fiscal anual de planos noutra moeda (motor cambial aceite preservado). Representação fiscal de fracções de unidade/cêntimo por corresponder. No M20 faltam discriminação vendas/serviços, fornecimentos/outros gastos, imposto e resultado líquido; no M22 faltam a reconciliação do campo200 e detalhe dos restantes ajustamentos. Divergência delimitada de edições: folha M20A.1 do XLSX remete ao campo201/quadro8 do M22; PDF inventariado utiliza200/quadro10. Bloqueia esse transporte, não os outros campos. |
**Provas apenas do delta:** 14 testes frontend; API privada1 teste/78 asserções com transacção revertida na base QA127.0.0.1:55441; geração de routes/actions pelo procedimento existente, TypeScript e build; interface real com edição, taxa/categoria incompatível, falta de juros, gravação/recuperação, download e perda corrente; relatórios/dossier construídos a partir dos dados recuperados. Recibo `docs/investmoz/simulations/irpc-l6-compatible-sets-qa-20261025.json`; pacote/bundle em `outputs/irpc-l6-sets`. Sem alteração de schema, API, configuração, permissões ou dependências.
**Âmbitos próprios preservados:** L7 conserva benefícios, isenções, taxas reduzidas, tributações autónomas e operações especiais; L8 conserva retenções, pagamentos, créditos e saldo final. M22 campo302 não é preenchido só com a parcela normal300, nem os campos309–316 são preenchidos com zeros presumidos. As dez dúvidas deL5 continuam na mesma matriz, sem nova pesquisa e bloqueando apenas dependências efectivas. L6 continua parcial exclusivamente pelas pendências concretas da tabela; nenhum desses pontos reabre o percurso instalado.
File diff suppressed because one or more lines are too long
@@ -1,3 +1,4 @@
import {SimulationIrpcSchedules} from '@/components/simulation-irpc-schedules';
import {SimulationIrpcAssessment} from '@/components/simulation-irpc-assessment';
import {useState} from 'react';
import {SimulationIrpcTaxableBase} from '@/components/simulation-irpc-taxable-base';
@@ -15,9 +16,9 @@ export function SimulationIrpcNormal({draft,onChange}:{draft:Record<string,unkno
return<section aria-label="IRPC normal — ajustamentos" className="space-y-3 rounded border p-4"><h4>L6 — IRPC normal: ajustamentos ao resultado financeiro</h4><p>Acréscimos de multas sem origem contratual e juros compensatórios incluídos nos custos do plano, artigo 36(1)c. A conferência anual pode determinar o lucro tributável simulado no recorte ordinário documentado. A matéria colectável exige conferência específica de deduções e perdas; a colecta tem cálculo próprio e o saldo final a pagar ou recuperar pertence aL8. Não reutiliza automaticamente juros simulados de L5.</p>{error&&<p role="alert">{error}</p>}
{source?<p>Origem: plano financeiro desta oportunidade; {source.periods} meses; {source.currency}; resultado antes de imposto: {source.accountingProfitBeforeTax}. Cobranças, pagamentos e reembolso de capital ficam fora do resultado.</p>:<p>Prepare o plano financeiro para reutilizar o resultado e os custos reconciliados.</p>}
<label className="block">Exercício fiscal<input value={b.year} onChange={e=>save({...b,year:e.target.value})}/></label>
<label className="block">Correspondência dos 12 meses do plano a Janeiro–Dezembro de 2026<input value={b.mappingReference} onChange={e=>save({...b,mappingReference:e.target.value})}/></label>
<label className="block">Âmbito<select value={b.scope} onChange={e=>save({...b,scope:e.target.value as NormalBook['scope']})}><option value="">Por identificar</option><option value="resident_general">Sujeito residente — regime geral, fora de transparência e regimes especiais</option><option value="other">Outro âmbito — por corresponder</option></select></label>
<label className="block">Suporte ou pressuposto do âmbito<input value={b.scopeReference} onChange={e=>save({...b,scopeReference:e.target.value})}/></label>
<label className="block">{b.schedules?'Correspondência dos12 meses do plano ao período anual documentado':'Correspondência dos 12 meses do plano a Janeiro–Dezembro de 2026'}<input value={b.mappingReference} onChange={e=>save({...b,mappingReference:e.target.value})}/></label>
<label className="block">Âmbito<select value={b.scope} onChange={e=>save({...b,scope:e.target.value as NormalBook['scope']})}><option value="">Por identificar</option><option value="resident_general">Sujeito residente — regime geral, fora de transparência e regimes especiais</option><option value="ep_general">Não residente com estabelecimento estável — regime geral, só resultado imputável ao EP (mapas complementares)</option><option value="other">Outro âmbito — por corresponder</option></select></label>
<label className="block">Suporte do âmbito e da imputação integral do resultado ao sujeito/EP<input value={b.scopeReference} onChange={e=>save({...b,scopeReference:e.target.value})}/></label>
<button type="button" onClick={()=>save({...b,rows:[...b.rows,newNormalAdjustment(crypto.randomUUID())]})}>Acrescentar ajustamento ao resultado</button>
{b.rows.map(r=><fieldset key={r.id} className="space-y-2 rounded border p-3"><legend>{r.document||'Ajustamento por identificar'}</legend><label className="block">Custo de origem na ponte<select value={r.sourceCode} onChange={e=>patch(r.id,{sourceCode:e.target.value})}><option value="">Seleccione</option>{source?.accountingLines.filter(l=>l.accountingEffect==='expense').map(l=><option key={l.code} value={l.code}>{l.origin}: {l.total} {source.currency}</option>)}</select></label>{field(r,'document','Documento ou pressuposto identificado')}{field(r,'line','Linha distinta do documento')}{field(r,'amount','Parcela do custo em MZN')}
<label className="block">Natureza documentada<select value={r.rule} onChange={e=>patch(r.id,{rule:e.target.value as NormalAdjustment['rule']})}><option value="">Por classificar</option><option value="noncontractual_fine">Multa/encargo por infracção sem origem contratual — 36(1)c</option><option value="compensatory_interest">Juros compensatórios — 36(1)c</option><option value="other">Outro ajustamento — por corresponder</option></select></label>
@@ -28,6 +29,7 @@ export function SimulationIrpcNormal({draft,onChange}:{draft:Record<string,unkno
<p>Depreciação positiva e juros financeiros não integralmente acrescidos exigem correspondência própria;uma declaração de ausência não apaga os valores do plano.</p>
{profitTopics.map(([k,label])=>{const t=b.profit!.topics[k];const change=(p:Partial<ProfitEvidence>)=>profit({topics:{...b.profit!.topics,[k]:{...t,...p}}});return<fieldset key={k} className="rounded border p-2"><legend>{label}</legend><label className="block">Estado — {label}<select aria-label={'Estado — '+label} value={t.state} onChange={e=>change({state:e.target.value as ProfitEvidence['state']})}><option value="">Informação em falta</option><option value="known">Valor conhecido</option><option value="absent">Ausência declarada com suporte</option></select></label>{(t.state==='known'||t.amount!=='')&&<label className="block">Montante conhecido — {label}<input value={t.amount} onChange={e=>change({amount:e.target.value})}/></label>}<label className="block">Suporte — {label}<input value={t.reference} onChange={e=>change({reference:e.target.value})}/></label></fieldset>;})}
</fieldset>}
<SimulationIrpcSchedules book={b} save={save}/>
<SimulationIrpcTaxableBase draft={draft} book={b} save={save}/>
<SimulationIrpcAssessment book={b} save={save}/>
<button type="button" onClick={()=>{try{save(recordNormal(draft,b));}catch(e){setError(String(e));}}}>Calcular e guardar ajustamentos L6</button>
@@ -0,0 +1,29 @@
import {newNormalSchedules,newScheduleRow,newNormalAsset,scheduleRules,type NormalSchedules,type ScheduleRow,type NormalAsset} from '@/lib/irpc-normal-schedules';
import type {NormalBook} from '@/lib/irpc-normal';
export function SimulationIrpcSchedules({book,save}:{book:NormalBook;save:(b:NormalBook)=>void}){
const s=book.schedules;if(!s)return<button type="button" onClick={()=>save({...book,schedules:newNormalSchedules()})}>Iniciar mapas complementares L6</button>;
const patch=(p:Partial<NormalSchedules>)=>save({...book,schedules:{...s,...p}});
const row=(id:string,p:Partial<ScheduleRow>)=>patch({rows:s.rows.map(r=>r.id===id?{...r,...p}:r)});
const asset=(id:string,p:Partial<NormalAsset>)=>patch({assets:s.assets.map(a=>a.id===id?{...a,...p}:a)});
return<fieldset aria-label="Mapas complementares L6" className="space-y-3 rounded border p-3"><legend>Mapas complementares — regras ordinárias documentadas</legend>
<p>Reutiliza o resultado e as linhas do plano. As somas brutas de cada tema devem corresponder à conferência do lucro tributável. Os efeitos com sinal ficam discriminados; uma dúvida não apaga as parcelas independentes. L7 conserva benefícios e casos especiais; L8 conserva o fecho anual.</p>
<fieldset><legend>Período anual e correspondência dos meses</legend><label>Tipo de período<select aria-label="Tipo de período L6" value={s.period.kind} onChange={e=>patch({period:{...s.period,kind:e.target.value as NormalSchedules['period']['kind']}})}><option value="">Por identificar</option><option value="calendar">Ano civil — artigo7(1)</option><option value="authorised_annual">Anual diferente — autorização artigo7(2)</option><option value="ep_annual">Anual diferente de EP — comunicação artigo7(3)</option></select></label>{(['from','to','reference']as const).map(k=><label key={k} className="block">{({from:'Início do período AAAA-MM-DD',to:'Termo do período AAAA-MM-DD',reference:'Suporte do período e da correspondência mensal'})[k]}<input aria-label={'Período L6 '+k} value={s.period[k]} onChange={e=>patch({period:{...s.period,[k]:e.target.value}})}/></label>)}<p>Período anual diferente: documente autorização do artigo7(2) ou comunicação do EP do artigo7(3), início de aplicação e manutenção por cinco exercícios. O plano representa o período completo com termo em2026, emMZN. Períodos curtos e conversão fiscal anual de outra moeda ficam por corresponder.</p></fieldset>
<button type="button" onClick={()=>patch({rows:[...s.rows,newScheduleRow(crypto.randomUUID())]})}>Acrescentar regra complementar</button>
{s.rows.map((r,n)=><fieldset key={r.id} className="rounded border p-2"><legend>Regra complementar {n+1}</legend>
<label>Tratamento<select aria-label={'Tratamento complementar '+(n+1)} value={r.rule} onChange={e=>row(r.id,{rule:e.target.value as ScheduleRow['rule']})}><option value="">Por identificar</option>{Object.entries(scheduleRules).map(([k,v])=><option key={k} value={k}>{v[0]} — {v[2]}</option>)}</select></label>
{([['amount','Montante MZN'],['sourceCode','Código da linha de origem (custos/proveitos já incluídos)'],['document','Documento'],['line','Linha distinta'],['reference','Suporte da regra e dos factos'],['fiscalYear','Exercício de imputação (periodização)']]as const).map(([k,label])=><label key={k} className="block">{label}<input aria-label={label+' complementar '+(n+1)} value={r[k]} onChange={e=>row(r.id,{[k]:e.target.value})}/></label>)}
<p>Entradas/distribuições de capital e variações patrimoniais são externas ao resultado. Variação negativa ordinária: indispensável à actividade sujeita, sem liberalidade, menos-valia latente ou distribuição; sem gratificações especiais. Periodização: operação ordinária e exercício de realização documentados; sem obra plurianual ou correcção de exercício anterior.</p>
<label>Condições demonstradas<select aria-label={'Condições complementar '+(n+1)} value={r.conditions} onChange={e=>row(r.id,{conditions:e.target.value as ''|'yes'})}><option value="">Informação em falta</option><option value="yes">Sim, nos documentos identificados</option></select></label>
<button type="button" onClick={()=>patch({rows:s.rows.filter(q=>q.id!==r.id)})}>Retirar regra {n+1} das entradas actuais</button></fieldset>)}
<fieldset><legend>Juros financeiros ordinários — artigo22c</legend><p>Somente capitais alheios aplicados na exploração, encargos documentados, não capitalizáveis, sem partes relacionadas, subcapitalização ou outra limitação especial. Identifique o remanescente após os juros compensatórios acrescidos no mapa anterior. Outros juros permanecem pendentes neste caso.</p>
<label className="block">Juros ordinários remanescentes MZN<input value={s.interest.amount} onChange={e=>patch({interest:{...s.interest,amount:e.target.value}})}/></label>
<label className="block">Suporte dos juros e condições<input value={s.interest.reference} onChange={e=>patch({interest:{...s.interest,reference:e.target.value}})}/></label>
<label>Condições dos juros<select aria-label="Condições dos juros ordinários" value={s.interest.conditions} onChange={e=>patch({interest:{...s.interest,conditions:e.target.value as ''|'yes'}})}><option value="">Informação em falta</option><option value="yes">Demonstradas no recorte ordinário</option></select></label></fieldset>
<fieldset><legend>Amortizações fiscais — quotas constantes anuais</legend><p>Decreto72/2013, artigos3–7. Activo próprio sujeito a deperecimento, custo histórico, uso no início de2026, método uniforme, vida útil fiscal não excedida. Documente que não há regra sectorial específica prevalecente ou limitação especial. A categoria exige qualificação documentada; os itens genéricos9/9.1 só se aplicam quando não há taxa específica da tabelaI. Duodécimos, reavaliação, bens usados e recuperação de quotas antes tributadas ficam separados.</p>
<label className="block">Reconciliação de todos os activos com a depreciação do plano<input value={s.assetsReference} onChange={e=>patch({assetsReference:e.target.value})}/></label>
<button type="button" onClick={()=>patch({assets:[...s.assets,newNormalAsset(crypto.randomUUID())]})}>Acrescentar activo fiscal</button>
{s.assets.map((a,n)=><fieldset key={a.id} className="rounded border p-2"><legend>Activo {n+1}</legend><label>Categoria fiscal<select aria-label={'Categoria activo '+(n+1)} value={a.category} onChange={e=>asset(a.id,{category:e.target.value as NormalAsset['category'],rate:e.target.value==='computer'?'25':e.target.value==='server'?'20':'',tableReference:e.target.value==='computer'?'Decreto72/2013 tabelaII grupo3 item9':e.target.value==='server'?'Decreto72/2013 tabelaII grupo3 item9.1':''})}><option value="">Por identificar</option><option value="computer">Computador, impressora, UPS — II/3/9 — 25%</option><option value="server">Servidor — II/3/9.1 — 20%</option><option value="other">Outra categoria — correspondência pendente</option></select></label>{([['description','Descrição'],['cost','Custo fiscal documentado MZN'],['residual','Valor residual MZN'],['rate','Taxa anual documentada %'],['accounting','Depreciação contabilística de2026 MZN'],['previousFiscal','Amortização fiscal acumulada anterior MZN'],['inUseFrom','Data de entrada em uso AAAA-MM-DD'],['tableReference','Diploma, tabela, código e taxa aplicáveis'],['reference','Documentos e condições do activo']]as const).map(([k,label])=><label key={k} className="block">{label}<input aria-label={label+' activo '+(n+1)} value={a[k]} onChange={e=>asset(a.id,{[k]:e.target.value})}/></label>)}
<label>Condições do activo<select aria-label={'Condições activo '+(n+1)} value={a.conditions} onChange={e=>asset(a.id,{conditions:e.target.value as ''|'yes'})}><option value="">Informação em falta</option><option value="yes">Demonstradas no recorte anual constante</option></select></label><button type="button" onClick={()=>patch({assets:s.assets.filter(q=>q.id!==a.id)})}>Retirar activo {n+1} das entradas actuais</button></fieldset>)}</fieldset>
<label className="block">Suporte da perda fiscal do próprio exercício, se apurada<input value={s.currentLossReference} onChange={e=>patch({currentLossReference:e.target.value})}/></label><p>Perda actual conhecida: matéria colectável e colecta zero apenas depois de conferidas as restantes condições. Não é dedução a si própria nem saldo oficial de reporte. Informação insuficiente mantém os totais dependentes indeterminados.</p>
</fieldset>;
}
+4 -3
View File
@@ -5,15 +5,16 @@ export type NormalAssessmentInput={version:1;treatment:''|'general_61_1'|'other'
export const newNormalAssessmentInput=():NormalAssessmentInput=>({version:1,treatment:'',reference:''});
export function validateNormalAssessment(i:NormalAssessmentInput){if(!i||i.version!==1||!['','general_61_1','other'].includes(i.treatment)||typeof i.reference!=='string')throw Error('Enquadramento da colecta não recuperável;original conservado.');}
export type NormalAssessmentModel={input:NormalAssessmentInput;period:{year:string;from:string;to:string};classification:string;rate:FiscalFraction|null;taxableBase:FiscalFraction|null;colecta:FiscalFraction|null;centExact:boolean|null;pending:string[];rule:string;source:string;finalBalance:{scope:'L8';status:'not_calculated';payable:null;recoverable:null;reason:string}};
export function calculateNormalAssessment(input:NormalAssessmentInput,base:FiscalFraction|null,context:{year:string;scope:string;currency:string}):NormalAssessmentModel{
export function calculateNormalAssessment(input:NormalAssessmentInput,base:FiscalFraction|null,context:{year:string;scope:string;currency:string;extended?:boolean;period?:{from:string;to:string}}):NormalAssessmentModel{
validateNormalAssessment(input);const pending:string[]=[],period={year:context.year,from:/^20\d{2}$/.test(context.year)?context.year+'-01-01':'',to:/^20\d{2}$/.test(context.year)?context.year+'-12-31':''};
if(context.year!=='2026')pending.push('Taxa geral correspondida neste incremento apenas ao exercício2026;sem taxa transportada para outro período.');
if(context.scope!=='resident_general'||context.currency!=='MZN')pending.push('Colecta delimitada ao sujeito residente no regime geral emMZN;outros âmbitos por corresponder.');
if(!(context.scope==='resident_general'||context.extended&&context.scope==='ep_general')||context.currency!=='MZN')pending.push('Colecta delimitada ao sujeito residente no regime geral emMZN;outros âmbitos por corresponder.');
if(input.treatment!=='general_61_1')pending.push(input.treatment==='other'?'Benefício,taxa reduzida,tributação autónoma ou outro caso especial:tratamento próprio;sem taxa geral aplicada por aproximação.':'Enquadramento da taxa geral por identificar;sem escolha presumida.');
if(!input.reference.trim())pending.push('Identifique suporte ou pressuposto da aplicação da taxa geral ao caso;uma actividade semelhante não prova o regime.');
if(base===null)pending.push('Matéria colectável actual indeterminada;lucro tributável não é substituto da base.');
else if(!/^-?\d{1,80}$/.test(base.numerator)||!/^\d{1,80}$/.test(base.denominator)||BigInt(base.denominator)===0n||BigInt(base.numerator)<0n)pending.push('Matéria colectável deve ser um valor conhecido não negativo e reconciliado.');
if(context.period){period.from=context.period.from;period.to=context.period.to;const start=new Date(period.from+'T00:00:00Z'),end=new Date(period.to+'T00:00:00Z');if(!Number.isFinite(+start)||!Number.isFinite(+end)||start.toISOString().slice(0,10)!==period.from||end.toISOString().slice(0,10)!==period.to||!period.from.endsWith('-01')||!period.to.startsWith('2026-'))pending.push('Período fiscal anual não reconciliado.');else{start.setUTCFullYear(start.getUTCFullYear()+1);start.setUTCDate(start.getUTCDate()-1);if(start.toISOString().slice(0,10)!==period.to)pending.push('Colecta anual: período de12 meses por reconciliar.');}}
const rate=input.treatment==='general_61_1'&&context.year==='2026'?fraction(32n):null;
const colecta=pending.length===0&&base!==null&&rate?fraction(BigInt(base.numerator)*32n,BigInt(base.denominator)*100n):null;
return{input:structuredClone(input),period,classification:input.treatment==='general_61_1'?'Regime normal — taxa geral do artigo61(1)':input.treatment==='other'?'Caso especial fora deste recorte':'Enquadramento por identificar',rate,taxableBase:base,colecta,centExact:colecta?(BigInt(colecta.numerator)*100n)%BigInt(colecta.denominator)===0n:null,pending,rule:'CIRPC61(1)-general-2026/1',source:'CIRPC Lei34/2007 art61(1),PDF17/p768(71):taxa geral32%. Lei4/2012 altera61(2);Lei20/2022 PDF1/p2731 mantém61(1),altera61(2) com termo31/12/2025;Lei12/2025 PDF18/20 não altera61(1),adita61-A para mais-valias,fora deste recorte. Exercício anual2026/MZN/residente geral,matéria colectável actual×32/100 em aritmética racional exacta. Sem arredondamento fiscal ou transcriçãoM20/M22 presumidos.',finalBalance:{scope:'L8',status:'not_calculated',payable:null,recoverable:null,reason:'Saldo final a pagar ou recuperar por determinar emL8:retenções,pagamentos e créditos do fecho anual não apurados nem abatidos à colecta neste incremento. Colecta não equivale ao saldo final.'}};
return{input:structuredClone(input),period,classification:input.treatment==='general_61_1'?'Regime normal — taxa geral do artigo61(1)':input.treatment==='other'?'Caso especial fora deste recorte':'Enquadramento por identificar',rate,taxableBase:base,colecta,centExact:colecta?(BigInt(colecta.numerator)*100n)%BigInt(colecta.denominator)===0n:null,pending,rule:'CIRPC61(1)-general-2026/1',source:context.extended?'CIRPC61(1), taxa geral32% mantida pelos diplomas inventariados até Lei12/2025; período anual documentado com termo em2026. CIRPC15(1)a/c,7(1–3): sujeito residente ou resultado imputável aEP no regime geral; ano civil, autorização ou comunicação expressamente identificadas. Sem arredondamento fiscal ou saldoL8 presumidos.':'CIRPC Lei34/2007 art61(1),PDF17/p768(71):taxa geral32%. Lei4/2012 altera61(2);Lei20/2022 PDF1/p2731 mantém61(1),altera61(2) com termo31/12/2025;Lei12/2025 PDF18/20 não altera61(1),adita61-A para mais-valias,fora deste recorte. Exercício anual2026/MZN/residente geral,matéria colectável actual×32/100 em aritmética racional exacta. Sem arredondamento fiscal ou transcriçãoM20/M22 presumidos.',finalBalance:{scope:'L8',status:'not_calculated',payable:null,recoverable:null,reason:'Saldo final a pagar ou recuperar por determinar emL8:retenções,pagamentos e créditos do fecho anual não apurados nem abatidos à colecta neste incremento. Colecta não equivale ao saldo final.'}};
}
+4 -4
View File
@@ -15,13 +15,13 @@ export type NormalBaseModel={input:NormalBaseInput;taxableProfit:FiscalFraction|
const zero=()=>fraction(0n);
function amount(s:string){if(!/^\d{1,18}(?:\.\d{1,2})?$/.test(s))throw Error();return decimalFraction(s);}
const units=(f:FiscalFraction)=>BigInt(f.numerator)*100n/BigInt(f.denominator);
export function calculateNormalBase(input:NormalBaseInput,profit:FiscalFraction|null,year:string):NormalBaseModel{
export function calculateNormalBase(input:NormalBaseInput,profit:FiscalFraction|null,year:string,currentLossReference?:string):NormalBaseModel{
validateNormalBase(input);const i=structuredClone(input),pending:string[]=[],losses:NormalBaseModel['losses']=[];
if(year!=='2026')pending.push('Matéria colectável delimitada ao exercício2026.');
if(!/^\d{9}$/.test(i.subjectNuit)||!i.identityReference.trim())pending.push('Identifique NUIT e suporte do sujeito passivo do lucro e das perdas.');
if(i.method!=='direct'||!i.methodReference.trim())pending.push('Método declarativo/directo e suporte por identificar;apuramento indirecto fora deste recorte (15(2),41(2)).');
if(!profit)pending.push('Lucro tributável actual indeterminado;nenhuma dedução transforma informação em falta em zero.');
else if(units(profit)<0n)pending.push('Resultado fiscal negativo conhecido;tratamento da perda do próprio exercício fora deste recorte,sem matéria colectável zero ou reporte automático.');
else if(units(profit)<0n&&!currentLossReference?.trim())pending.push('Resultado fiscal negativo conhecido;tratamento da perda do próprio exercício fora deste recorte,sem matéria colectável zero ou reporte automático.');
if(!i.lossesState||!i.lossesReference.trim())pending.push('Situação e completude das perdas anteriores em falta;sem ausência presumida.');
if(i.lossesState==='absent'&&i.losses.length)pending.push('Ausência declarada de perdas contradiz os registos existentes;reconcilie sem apagar histórico.');
if(i.lossesState==='known'&&!i.losses.length)pending.push('Perdas declaradas conhecidas sem discriminação por exercício.');
@@ -46,12 +46,12 @@ export function calculateNormalBase(input:NormalBaseInput,profit:FiscalFraction|
losses.push(r);
}
const requestedLosses=addFractions(losses.filter(l=>l.status==='eligible').map(l=>l.requestedAmount!));
if(profit&&units(profit)>=0n&&units(requestedLosses)>units(profit))pending.push('Utilizações propostas excedem o lucro tributável disponível;reparta explicitamente sem criar base negativa ou escolher uma ordem de perdas.');
if(profit&&units(requestedLosses)>(units(profit)>0n?units(profit):0n))pending.push('Utilizações propostas excedem o lucro tributável disponível;reparta explicitamente sem criar base negativa ou escolher uma ordem de perdas.');
let benefitDeduction:FiscalFraction|null=null;const benefit=i.benefits;
if(!benefit.state||!benefit.reference.trim())pending.push('Benefícios/deduções à matéria colectável por identificar (15(4));créditos à colecta e retenções não são esta dedução.');
else if(benefit.state==='absent'){if(benefit.amount.trim())pending.push('Ausência de benefícios contradiz montante preenchido.');else benefitDeduction=zero();}
else{try{const value=amount(benefit.amount);if(units(value)!==0n)pending.push('Benefício de montante conhecido:regra,condições,limite e ordem próprios por implementar;sem subtracção livre do valor.');else benefitDeduction=value;}catch{pending.push('Montante conhecido das deduções por preencher ou inválido.');}}
const ready=pending.length===0&&profit!==null,deductedLosses=ready?requestedLosses:null,taxableBase=ready?subtractFractions(subtractFractions(profit!,requestedLosses),benefitDeduction!):null;
const ready=pending.length===0&&profit!==null,deductedLosses=ready?requestedLosses:null,taxableBase=ready?subtractFractions(subtractFractions(units(profit!)<0n?zero():profit!,requestedLosses),benefitDeduction!):null;
for(const l of losses){if(l.available){l.deducted=ready?(l.status==='eligible'?l.requestedAmount!:zero()):null;l.remaining=l.deducted?subtractFractions(l.available,l.deducted):null;}}
return{input:i,taxableProfit:profit,requestedLosses,deductedLosses,benefitDeduction,taxableBase,losses,pending,source:'CIRPC15(1)a/15(2–4),41(1–7),Lei34/2007 PDF5/p768(59),PDF12/p768(66). Recorte2026,residente geral,método declarativo,perdas próprias em exercícios anuais ordinários;cinco exercícios posteriores,sem transferência41-A,regimes especiais ou benefícios positivos implementados. Pedido de utilização expresso e limitado ao saldo/lucro;semFIFO obrigatório ou limite percentual inventado. Matéria colectável simulada,não colecta/imposto a pagar,semM20/M22/submissão ou consumo persistente do saldo oficial.'};
}
+20
View File
@@ -0,0 +1,20 @@
import {fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export const normalFormSource='M/22, exemplar inventariado IRPC M-22- DECLARACAO DE RENDIMENTOS.pdf, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88, quadros10/11/12, três páginas conferidas. Correspondência parcial de preparação, sem ficheiro oficial ou submissão.';
export function normalFormFields(m:{profit?:{lines:Array<{code:string;amount:string;reference:string}>};taxableProfit:FiscalFraction|null;taxableBase:FiscalFraction|null;colecta?:FiscalFraction|null;base?:{deductedLosses:FiscalFraction|null;benefitDeduction:FiscalFraction|null}}){
const p=m.taxableProfit,negative=p!==null&&BigInt(p.numerator)<0n;
const values:Array<[string,string,FiscalFraction|null,string]>= [
['259','Prejuízo fiscal',negative?fraction(-BigInt(p!.numerator),BigInt(p!.denominator)):null,'Quadro10; apenas quando há perda conhecida.'],
['260','Lucro tributável',p&&!negative?p:null,'Quadro10; não preencher o campo de lucro com uma perda.'],
['269','Prejuízo fiscal — regime geral',negative?fraction(-BigInt(p!.numerator),BigInt(p!.denominator)):null,'Quadro11; não é uma perda anterior dedutível.'],
['270','Lucro tributável — regime geral',p&&!negative?p:null,'Quadro11; manter separados regimes reduzido/isento.'],
['276','Prejuízos fiscais deduzidos',m.base?.deductedLosses??null,'Quadro11; utilização simulada expressa.'],
['277','Benefícios fiscais a deduzir',m.base?.benefitDeduction??null,'Quadro11; benefícios positivos pertencem aL7.'],
['278','Matéria colectável — regime geral',m.taxableBase,'Quadro11; não é colecta.'],
['300','Imposto à taxa normal',m.colecta??null,'Quadro12; não é saldo final.'],
['200','Resultado do exercício',null,'Resultado financeiro antes de imposto não preenche automaticamente o resultado líquido do formulário; falta reconciliação própria.'],
['302','Colecta total',null,'Inclui eventual parcela de taxa reduzida; não presumir a ausência deL7.'],
['309–316','Liquidação e saldo final',null,'ÂmbitoL8; retenções, pagamentos, créditos e juros não preenchidos porL6.'],
];
const m20Fields=([['cost_of_sales','6.1.1','Custo dos inventários vendidos/consumidos'],['personnel','6.2','Gastos com pessoal'],['depreciation','6.5','Amortizações do período'],['interest_expense','6.9','Gastos financeiros — apenas juros da origem']]as const).map(([key,account,label])=>{const l=m.profit?.lines.find(l=>l.code===key);let value:FiscalFraction|null=null;if(l&&/^\d+(?:\.\d{1,2})?$/.test(l.amount)){const[n,f='']=l.amount.split('.');value=fraction(BigInt(n)*100n+BigInt(f.padEnd(2,'0')),100n);}return{account,label,value,display:value?fractionPresentation(value).decimal+' MZN':'Não preenchido',transcriptionReady:value!==null&&BigInt(value.numerator)%BigInt(value.denominator)===0n,reference:l?.reference??'',reason:'M20 A.1 quadro4, código contabilístico do exemplar, sem inventar número de campo. Valor contabilístico da origem; não é dedução fiscal. Exemplar exige meticais sem centavos; fracções conservadas, sem arredondamento presumido.'};});
return{source:normalFormSource,m20:{status:'partial',fields:m20Fields,source:'Cópia de IRPS-IMOP.xlsx, SHA256 1ceca6f4c6d77853c1b2b445876599b42093175fa89eecf40590180956d83a9e, folhas M,20 e M20A.1; rótulos reutilizados, sem valores de terceiros ou execução das fórmulas do exemplar.',reason:'M20 A.1: correspondência parcial dos custos contabilísticos. Vendas/serviços, fornecimentos/outros gastos, imposto sobre o rendimento e resultado líquido exigem discriminação própria. Divergência concreta: M20A.1 remete resultado para M22 campo201/quadro8; PDF M22 inventariado usa campo200/quadro10. Sem transporte automático entre essas edições; restantes campos independentes conservados.'},fields:values.map(([code,label,value,reason])=>{const centExact=value?(BigInt(value.numerator)*100n)%BigInt(value.denominator)===0n:null;return{code,label,value,centExact,display:value?fractionPresentation(value).decimal+' MZN':'Não preenchido',transcriptionReady:value!==null&&centExact===true,reason:reason+(centExact===false?' Fracção de cêntimo conservada exactamente; regra de representação fiscal por corresponder, sem arredondamento presumido.':'')};})};
}
+10 -5
View File
@@ -1,3 +1,4 @@
import type {SchedulesModel} from './irpc-normal-schedules.ts';
import {addFractions,decimalFraction,fraction,type FiscalFraction} from './fiscal-rational.ts';
export const normalProfitVersion='IRPC-normal-adjustments/2';
@@ -23,7 +24,7 @@ const zero=()=>fraction(0n,1n);
function money(s:string){if(!/^-?\d+(?:\.\d{1,2})?$/.test(s))throw Error('Montante inválido');const v=decimalFraction(s.startsWith('-')?s.slice(1):s);return s.startsWith('-')?fraction(-BigInt(v.numerator),BigInt(v.denominator)):v;}
const units=(f:FiscalFraction)=>BigInt(f.numerator)*100n/BigInt(f.denominator);
const difference=(a:FiscalFraction,b:FiscalFraction)=>fraction(BigInt(a.numerator)*BigInt(b.denominator)-BigInt(b.numerator)*BigInt(a.denominator),BigInt(a.denominator)*BigInt(b.denominator));
export function closeNormalProfit(input:NormalProfitInput,source:Source|null,rows:Array<{sourceCode:string;adjustment:FiscalFraction|null;status:string}>,accounting:FiscalFraction|null,additions:FiscalFraction,contextReady:boolean):NormalProfitModel{
export function closeNormalProfit(input:NormalProfitInput,source:Source|null,rows:Array<{sourceCode:string;adjustment:FiscalFraction|null;status:string}>,accounting:FiscalFraction|null,additions:FiscalFraction,contextReady:boolean,schedules?:SchedulesModel):NormalProfitModel{
validateNormalProfit(input);const pending:string[]=[],lines:NormalProfitModel['lines']=[],topics:NormalProfitModel['topics']=[];
if(!contextReady)pending.push('Âmbito,exercício,moeda ou correspondência anual ainda insuficientes.');
if(!input.completenessReference.trim())pending.push('Documente que o plano e os ajustamentos abrangem o sujeito passivo/exercício completo;uma oportunidade isolada não equivale automaticamente ao conjunto da empresa.');
@@ -35,8 +36,8 @@ export function closeNormalProfit(input:NormalProfitInput,source:Source|null,row
try{const amount=money(l.total);remainder=difference(amount,addedBack);if(units(amount)<0n||units(remainder)<0n)issues.push('Valor negativo ou acréscimos superiores à origem;reconcilie a linha sem compensação presumida.');
if(!state||!reference.trim())issues.push('Conferência fiscal e suporte da linha em falta.');
if(state==='absent'&&(units(amount)!==0n||units(addedBack)!==0n))issues.push('Ausência declarada contradiz o montante conhecido da origem.');
if(state==='known'&&l.code==='depreciation'&&units(amount)!==0n)issues.push('Depreciação contabilística conhecida;amortização fiscal por corresponder,não presumir igualdade.');
if(state==='known'&&l.code==='interest_expense'&&units(remainder)!==0n)issues.push('Juros financeiros remanescentes conhecidos;dedutibilidade e eventuais limites por corresponder. Só os juros compensatórios já acrescidos estão cobertos.');
if(state==='known'&&l.code==='depreciation'&&units(amount)!==0n&&schedules?.depreciation==null)issues.push('Depreciação contabilística conhecida;amortização fiscal por corresponder,não presumir igualdade.');
if(state==='known'&&l.code==='interest_expense'&&units(remainder)!==0n&&schedules?.interest==null)issues.push('Juros financeiros remanescentes conhecidos;dedutibilidade e eventuais limites por corresponder. Só os juros compensatórios já acrescidos estão cobertos.');
if(!['revenue','cost_of_sales','operating_expenses','personnel','depreciation','interest_expense'].includes(l.code))issues.push('Linha financeira sem correspondência neste recorte.');
}catch{issues.push('Montante da linha por reconciliar.');}
const reason=issues.join(' ')|| (state==='absent'?'Ausência expressamente declarada e compatível com a origem zero.':units(remainder??zero())===0n?'Valor conhecido,integralmente corrigido ou igual a zero;sem presumir ausência.':l.accountingEffect==='income'?'Proveito ordinário reconhecido no exercício,qualificado e documentado nos termos dos artigos18/20.':'Parcela remanescente qualificada como custo ordinário indispensável e documentado do artigo22,sem exclusões/limites adicionais declarados.');
@@ -48,8 +49,12 @@ export function closeNormalProfit(input:NormalProfitInput,source:Source|null,row
if(!t.state||!t.reference.trim())reason='Informação em falta;sem zero presumido.';
else if(t.state==='absent'){if(t.amount.trim())reason='Ausência declarada com montante preenchido;reconcilie os dois estados.';else{value=zero();reason='Ausência expressamente declarada com suporte;parcela zero apenas por esta declaração.';}}
else{try{value=money(t.amount);if(units(value)!==0n)reason='Montante conhecido,mas tratamento específico ainda por corresponder;conservado sem aplicação presumida.';else reason='Valor conhecido zero com suporte;distinto de ausência declarada.';}catch{reason='Montante conhecido por preencher ou inválido.';}}
if(value===null||units(value)!==0n||t.state==='absent'&&!!t.amount.trim())pending.push(label+': '+reason);
const mapped=schedules?.effects.filter(e=>e.topic===key)??[];
const covered=schedules&&mapped.length>0&&mapped.every(e=>e.effect!==null)&&value!==null&&t.state==='known'&&units(addFractions(mapped.flatMap(e=>e.amount?[e.amount]:[])))===units(value);
if(covered)reason='Montante reconciliado com as regras do mapa complementar; efeito fiscal discriminado por linha.';
if(value===null||units(value)!==0n&&!covered||t.state==='absent'&&!!t.amount.trim()||mapped.length>0&&!covered)pending.push(label+': '+(mapped.length&&!covered?'Mapa complementar não reconciliado com este estado/montante. ': '')+reason);
topics.push({...structuredClone(t),key,label,value,reason});
}
return{input:structuredClone(input),lines,topics,pending,taxableProfit:pending.length===0&&accounting?addFractions([accounting,additions]):null,source:'CIRPC17(1)/18/20/22/36(1)c;recorte anual2026,residente geral,MZN. Resultado antes de imposto + acréscimos36(1)c;demais correcções/variações sem efeito apenas quando ausência ou zero conhecidos expressamente suportados. Receita/custos ordinários classificados pelo utilizador;depreciação fiscal e juros financeiros remanescentes fora deste percurso. Lucro tributável simulado,não matéria colectável,colecta ou declaraçãoM20/22.'};
if(schedules)pending.push(...schedules.pending);
return{input:structuredClone(input),lines,topics,pending,taxableProfit:pending.length===0&&accounting?addFractions([accounting,additions,...(schedules?[schedules.net]:[])]):null,source:schedules?'CIRPC17/18/20/21/22/24/36; Decreto72/2013 art3–7. Mapa complementar de efeitos com sinal reconciliado por tema, juros financeiros ordinários e quotas anuais constantes documentadas. Simulação emMZN/2026; benefícios e casos especiais separados emL7, saldo anual emL8.':'CIRPC17(1)/18/20/22/36(1)c;recorte anual2026,residente geral,MZN. Resultado antes de imposto + acréscimos36(1)c;demais correcções/variações sem efeito apenas quando ausência ou zero conhecidos expressamente suportados. Receita/custos ordinários classificados pelo utilizador;depreciação fiscal e juros financeiros remanescentes fora deste percurso. Lucro tributável simulado,não matéria colectável,colecta ou declaraçãoM20/22.'};
}
+71
View File
@@ -0,0 +1,71 @@
import {addFractions,decimalFraction,fraction,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
export const normalSchedulesVersion='IRPC-normal-adjustments/5';
export const scheduleRules={
tax_own:['IRPC e impostos sobre o lucro contabilizados','expenseCorrections','36(1)a',1],
tax_other:['Impostos da responsabilidade de outrem','expenseCorrections','36(1)b',1],
undocumented:['Despesa não documentada','expenseCorrections','36(1)g',1],
capital_in:['Entrada de capital fora do resultado','positivePatrimonial','21(1)a',0],
patrimonial_gain:['Variação positiva ordinária fora do resultado','positivePatrimonial','21',1],
capital_out:['Distribuição aos titulares fora do resultado','negativePatrimonial','24(1)c',0],
patrimonial_cost:['Variação negativa indispensável, sem exclusões do artigo24','negativePatrimonial','22/24(1)',-1],
future_cost:['Custo contabilizado em2026 imputável ao exercício seguinte','timing','18(1)',1],
future_income:['Proveito contabilizado em2026 imputável ao exercício seguinte','timing','18(1)',-1],
current_cost:['Custo de2026 ainda não reflectido no resultado','timing','18(1)',-1],
current_income:['Proveito de2026 ainda não reflectido no resultado','timing','18(1)',1],
} as const;
export type ScheduleRule=keyof typeof scheduleRules;
export type ScheduleRow={id:string;rule:''|ScheduleRule;amount:string;sourceCode:string;document:string;line:string;reference:string;fiscalYear:string;conditions:''|'yes'};
export type NormalAsset={id:string;category:''|'computer'|'server'|'other';description:string;cost:string;residual:string;rate:string;accounting:string;previousFiscal:string;inUseFrom:string;tableReference:string;reference:string;conditions:''|'yes'};
export type NormalSchedules={version:1;period:{kind:''|'calendar'|'authorised_annual'|'ep_annual';from:string;to:string;reference:string};rows:ScheduleRow[];interest:{amount:string;reference:string;conditions:''|'yes'};assets:NormalAsset[];assetsReference:string;currentLossReference:string};
export const newNormalSchedules=():NormalSchedules=>({version:1,period:{kind:'',from:'',to:'',reference:''},rows:[],interest:{amount:'',reference:'',conditions:''},assets:[],assetsReference:'',currentLossReference:''});
export const newScheduleRow=(id:string):ScheduleRow=>({id,rule:'',amount:'',sourceCode:'',document:'',line:'',reference:'',fiscalYear:'',conditions:''});
export const newNormalAsset=(id:string):NormalAsset=>({id,category:'',description:'',cost:'',residual:'',rate:'',accounting:'',previousFiscal:'',inUseFrom:'',tableReference:'',reference:'',conditions:''});
export function validateSchedules(s:NormalSchedules){
if(!s||s.version!==1||!Array.isArray(s.rows)||!Array.isArray(s.assets)||!s.interest||typeof s.assetsReference!=='string'||typeof s.currentLossReference!=='string'||['amount','reference','conditions'].some(k=>typeof s.interest[k as 'amount']!=='string')||!['','yes'].includes(s.interest.conditions))throw Error('Mapas L6 não recuperáveis.');
for(const [rows,empty]of [[s.rows,newScheduleRow('')],[s.assets,newNormalAsset('')]]as const){if(new Set(rows.map(r=>r.id)).size!==rows.length)throw Error('Identificador duplicado no mapa L6.');for(const r of rows)if(!r||Object.keys(empty).some(k=>typeof (r as unknown as Record<string,unknown>)[k]!=='string')||!['','yes'].includes(r.conditions))throw Error('Linha L6 não recuperável.');}
if(s.rows.some(r=>r.rule!==''&&!Object.hasOwn(scheduleRules,r.rule)))throw Error('Regra L6 desconhecida.');
if(s.assets.some(a=>!['','computer','server','other'].includes(a.category)))throw Error('Categoria de activo desconhecida.');
if(!s.period||!['','calendar','authorised_annual','ep_annual'].includes(s.period.kind)||['from','to','reference'].some(k=>typeof s.period[k as 'from']!=='string'))throw Error('Período complementar não recuperável.');
}
export function normalSchedulePeriod(s:NormalSchedules,scope:string){
const p=s.period,pending:string[]=[];
const iso=(v:string)=>/^20\d{2}-\d{2}-\d{2}$/.test(v)&&Number.isFinite(Date.parse(v))&&new Date(v).toISOString().slice(0,10)===v;
if(!iso(p.from)||!iso(p.to)||!p.from.endsWith('-01')||!p.to.startsWith('2026-'))pending.push('Período anual por identificar: início no primeiro dia do mês e termo em2026, sem período curto ou alinhamento mensal presumido.');
else{const end=new Date(p.from+'T00:00:00Z');end.setUTCFullYear(end.getUTCFullYear()+1);end.setUTCDate(end.getUTCDate()-1);if(end.toISOString().slice(0,10)!==p.to)pending.push('Os12 meses do plano não correspondem ao período anual indicado.');}
if(!p.kind||!p.reference.trim())pending.push('Identifique tipo de período e correspondência dos12 meses; ausência de informação não é ano civil presumido.');
if(p.kind==='calendar'&&(p.from!=='2026-01-01'||p.to!=='2026-12-31'))pending.push('Datas incompatíveis com o ano civil2026.');
if(p.kind==='ep_annual'&&scope!=='ep_general')pending.push('Período do artigo7(3) exige EP e comunicação documentada; não aplicar a um residente.');
return{...structuredClone(p),pending};
}
const z=()=>fraction(0n);
export const fiscalMoney=(s:string)=>{if(!/^\d{1,18}(?:\.\d{1,2})?$/.test(s))throw Error('Montante não negativo com até dois decimais necessário.');return decimalFraction(s);};
const cmp=(a:FiscalFraction,b:FiscalFraction)=>BigInt(a.numerator)*BigInt(b.denominator)-BigInt(b.numerator)*BigInt(a.denominator);
export type SchedulesModel=ReturnType<typeof calculateSchedules>;
export function calculateSchedules(s:NormalSchedules,source:{accountingLines:Array<{code:string;total:string;accountingEffect:string}>}|null,oldRows:Array<{sourceCode:string;document:string;line:string;amount:string;adjustment:FiscalFraction|null}>){
validateSchedules(s);const pending:string[]=[],effects:Array<{id:string;topic:string;amount:FiscalFraction|null;effect:FiscalFraction|null;reason:string}>=[];
for(const r of s.rows){const issues:string[]=[],rule=r.rule?scheduleRules[r.rule]:null;let amount:FiscalFraction|null=null,effect:FiscalFraction|null=null;
if(!rule||!r.reference.trim()||!r.document.trim()||!r.line.trim()||r.conditions!=='yes')issues.push('Regra, documento/linha, suporte e condições por identificar.');
if(s.rows.some(q=>q!==r&&q.document.trim()===r.document.trim()&&q.line.trim()===r.line.trim())||oldRows.some(q=>q.document.trim()===r.document.trim()&&q.line.trim()===r.line.trim()))issues.push('Documento/linha duplicados entre mapas; sem dupla contagem.');
try{amount=fiscalMoney(r.amount);if(cmp(amount,z())<=0n)throw Error();}catch{issues.push('Parcela positiva por identificar.');}
const topic=rule?.[1]??'';const needsOrigin=topic==='expenseCorrections'||['future_cost','future_income'].includes(r.rule);
if(needsOrigin){const line=source?.accountingLines.find(l=>l.code===r.sourceCode);const wanted=r.rule==='future_income'?'income':'expense';if(!line||line.accountingEffect!==wanted)issues.push('Parcela deve estar incluída numa linha de origem compatível.');else try{const allocated=addFractions([...s.rows.filter(q=>(q.rule&&(['expenseCorrections'].includes(scheduleRules[q.rule][1])||['future_cost','future_income'].includes(q.rule)))&&q.sourceCode===r.sourceCode).map(q=>fiscalMoney(q.amount)),...oldRows.filter(q=>q.sourceCode===r.sourceCode).map(q=>fiscalMoney(q.amount))]);if(cmp(allocated,fiscalMoney(line.total))>0n)issues.push('Parcelas excedem a origem contabilística.');}catch{issues.push('Reconciliação da origem incompleta.');}}
else if(r.sourceCode)issues.push('Variação fora do resultado/custo ou proveito omitido não deve repetir linha já contabilizada.');
if(topic==='timing'&&r.fiscalYear!==(r.rule.startsWith('future_')?'2027':'2026'))issues.push('Exercício de imputação incompatível; este recorte cobre2026/seguinte, sem resolver exercícios anteriores.');
if(!issues.length&&amount&&rule)effect=fraction(BigInt(amount.numerator)*BigInt(rule[3]),BigInt(amount.denominator));
const reason=issues.join(' ')||'CIRPC'+rule?.[2]+'; classificação e condições documentadas pelo utilizador; projecção, sem alteração da contabilidade de origem.';if(issues.length)pending.push(r.document+': '+reason);effects.push({id:r.id,topic,amount,effect,reason});
}
let interest:FiscalFraction|null=null;const interestOrigin=source?.accountingLines.find(l=>l.code==='interest_expense');
try{const corrected=s.rows.filter(r=>r.sourceCode==='interest_expense'&&r.rule&&(scheduleRules[r.rule][1]==='expenseCorrections'||r.rule==='future_cost'));const corrections=corrected.map(r=>{const e=effects.find(e=>e.id===r.id);if(!e?.effect)throw Error();return e.effect;});const remaining=subtractFractions(fiscalMoney(interestOrigin?.total??''),addFractions([...oldRows.filter(r=>r.sourceCode==='interest_expense').flatMap(r=>r.adjustment?[r.adjustment]:[]),...corrections]));interest=fiscalMoney(s.interest.amount);if(cmp(interest,remaining)!==0n||!s.interest.reference.trim()||s.interest.conditions!=='yes')throw Error();}catch{interest=null;}
const assets=s.assets.map(a=>{const issues:string[]=[];let accounting:FiscalFraction|null=null,fiscal:FiscalFraction|null=null,adjustment:FiscalFraction|null=null;
try{const cost=fiscalMoney(a.cost),residual=fiscalMoney(a.residual),previous=fiscalMoney(a.previousFiscal),rate=fiscalMoney(a.rate);accounting=fiscalMoney(a.accounting);const depreciable=subtractFractions(cost,residual);if(cmp(depreciable,z())<0n||cmp(previous,depreciable)>0n||cmp(rate,z())<=0n||cmp(rate,fraction(100n))>0n)throw Error();const annual=fraction(BigInt(depreciable.numerator)*BigInt(rate.numerator),BigInt(depreciable.denominator)*BigInt(rate.denominator)*100n);const available=subtractFractions(depreciable,previous);fiscal=cmp(annual,available)>0n?available:annual;
if(cmp(fiscal,accounting)>0n)issues.push('Quota fiscal superior à contabilizada neste exercício; dedução de quotas anteriormente tributadas exige mapa próprio, sem presunção.');
}catch{issues.push('Custo, residual, taxa, acumulado fiscal e depreciação contabilizada por reconciliar.');}
if(!['computer','server'].includes(a.category)||a.rate!==(a.category==='computer'?'25':'20'))issues.push('Categoria/taxa não correspondida: este recorte cobre tabelaII, grupo3, itens9 (25%) e9.1 (20%), sem regra específica da tabelaI prevalecente.');
if(!/^20\d{2}-\d{2}-\d{2}$/.test(a.inUseFrom)||!Number.isFinite(Date.parse(a.inUseFrom))||new Date(a.inUseFrom).toISOString().slice(0,10)!==a.inUseFrom||a.inUseFrom>(s.period.from||'2026-01-01')||a.inUseFrom<'2014-01-01'||!a.description.trim()||!a.tableReference.trim()||!a.reference.trim()||a.conditions!=='yes')issues.push('Identifique activo próprio, em uso no início do período, tabela/taxa aplicável, vida útil e condições do método constante anual; casos especiais/duodécimos separados.');
if(['computer','server'].includes(a.category)&&/^20\d{2}-\d{2}-\d{2}$/.test(a.inUseFrom)){const limit=new Date(a.inUseFrom+'T00:00:00Z');if(Number.isFinite(+limit)){limit.setUTCFullYear(limit.getUTCFullYear()+(a.category==='computer'?8:10));if((s.period.from||'2026-01-01')>=limit.toISOString().slice(0,10))issues.push('Período máximo de vida útil excedido, artigo5; excepção específica não implementada.');}}
if(!issues.length&&accounting&&fiscal)adjustment=subtractFractions(accounting,fiscal);if(issues.length)pending.push(a.description+': '+issues.join(' '));return{...structuredClone(a),accountingValue:accounting,fiscal,adjustment,reason:issues.join(' ')||'Decreto72/2013 art3–7; quota constante anual documentada, limitada ao saldo fiscal depreciável; diferença para a depreciação contabilística.'};});
let depreciation:FiscalFraction|null=null;try{const total=addFractions(assets.map(a=>a.accountingValue??z()));if(s.assetsReference.trim()&&assets.every(a=>a.adjustment!==null)&&cmp(total,fiscalMoney(source?.accountingLines.find(l=>l.code==='depreciation')?.total??''))===0n)depreciation=addFractions(assets.map(a=>a.adjustment!));}catch{/* linha de origem desconhecida */}
if(assets.length&&(oldRows.some(r=>r.sourceCode==='depreciation')||s.rows.some(r=>r.sourceCode==='depreciation'))){depreciation=null;pending.push('Depreciação repartida entre mapas de ajustamentos e activos: reconcilie as parcelas sem sobreposição antes de aplicar a quota fiscal.');}
if(assets.length&&depreciation===null)pending.push('Mapa de activos sem reconciliação integral com a depreciação da origem; efeito fiscal não aplicado.');
return{input:structuredClone(s),effects,interest,depreciation,assets,pending,net: addFractions([...effects.flatMap(r=>r.effect?[r.effect]:[]),...(depreciation?[depreciation]:[])]),source:'CIRPC18/21/22/24/36; Decreto72/2013 art3–7. Entradas documentadas, sem taxas de activos sugeridas nem casos especiais presumidos.'};
}
+19 -13
View File
@@ -1,3 +1,5 @@
import {normalFormFields} from './irpc-normal-forms.ts';
import {normalSchedulesVersion,normalSchedulePeriod,validateSchedules,calculateSchedules,type NormalSchedules,type SchedulesModel} from './irpc-normal-schedules.ts';
import {normalAssessmentVersion,validateNormalAssessment,calculateNormalAssessment,type NormalAssessmentInput,type NormalAssessmentModel} from './irpc-normal-assessment.ts';
import {normalBaseVersion,validateNormalBase,calculateNormalBase,type NormalBaseInput,type NormalBaseModel} from './irpc-normal-base.ts';
import {normalProfitVersion,validateNormalProfit,closeNormalProfit,type NormalProfitInput,type NormalProfitModel} from './irpc-normal-profit.ts';
@@ -9,8 +11,8 @@ import {addFractions,decimalFraction,fraction,fractionPresentation,type FiscalFr
export const irpcNormalVersion='IRPC-normal-adjustments/1';
export const irpcNormalSource='CIRPC Lei34/2007 art17(1),PDF6/p768(60):resultado e correcções;art36(1)c,PDF10/p768(64):multas e encargos por infracções sem origem contratual,incluindo juros compensatórios. Lei4/2012 PDF2 altera17(6)/acrescenta36(1)o;Lei19/2013 PDF2 mantém36(1)a–o e acrescenta p/q. Lei12/2025 altera3/5/20/62/67/75,adita61-A e revoga39/47/75(2)/76;sem alteração deste recorte36(1)c. Não é apuramento integral nem Modelo20/22.';
export type NormalAdjustment={id:string;sourceCode:string;document:string;line:string;amount:string;rule:''|'noncontractual_fine'|'compensatory_interest'|'other';included:''|'yes'|'no';reference:string};
export type NormalBook={version:1;year:string;mappingReference:string;scope:''|'resident_general'|'other';scopeReference:string;rows:NormalAdjustment[];profit?:NormalProfitInput;base?:NormalBaseInput;assessment?:NormalAssessmentInput;result?:NormalRecord;history?:NormalRecord[]};
type NormalModel={assessment?:NormalAssessmentModel;colecta?:FiscalFraction|null;base?:NormalBaseModel;profit?:NormalProfitModel;version:string;year:string;currency:string;origin:'projected';accountingProfit:FiscalFraction|null;additions:FiscalFraction;partialAdjustedResult:FiscalFraction|null;taxableProfit:FiscalFraction|null;taxableBase:FiscalFraction|null;tax:null;rows:Array<NormalAdjustment&{status:'calculated'|'indeterminate'|'outside_scope';adjustment:FiscalFraction|null;reason:string}>;pending:string[];source:string};
export type NormalBook={version:1;year:string;mappingReference:string;scope:''|'resident_general'|'ep_general'|'other';scopeReference:string;rows:NormalAdjustment[];profit?:NormalProfitInput;base?:NormalBaseInput;assessment?:NormalAssessmentInput;schedules?:NormalSchedules;result?:NormalRecord;history?:NormalRecord[]};
type NormalModel={fiscalPeriod?:ReturnType<typeof normalSchedulePeriod>;schedules?:SchedulesModel;currentLoss?:FiscalFraction|null;assessment?:NormalAssessmentModel;colecta?:FiscalFraction|null;base?:NormalBaseModel;profit?:NormalProfitModel;version:string;year:string;currency:string;origin:'projected';accountingProfit:FiscalFraction|null;additions:FiscalFraction;partialAdjustedResult:FiscalFraction|null;taxableProfit:FiscalFraction|null;taxableBase:FiscalFraction|null;tax:null;rows:Array<NormalAdjustment&{status:'calculated'|'indeterminate'|'outside_scope';adjustment:FiscalFraction|null;reason:string}>;pending:string[];source:string};
type NormalRecord={key:string;createdAt:string;model:NormalModel};
export const newNormalBook=():NormalBook=>({version:1,year:'',mappingReference:'',scope:'',scopeReference:'',rows:[]});
export const newNormalAdjustment=(id:string):NormalAdjustment=>({id,sourceCode:'',document:'',line:'',amount:'',rule:'',included:'',reference:''});
@@ -21,29 +23,31 @@ export function normalFinancialSource(d:Record<string,unknown>){
}
function store(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books||typeof s.books!=='object'||Array.isArray(s.books))throw Error('Livros fiscais não recuperáveis;originais preservados.');return s;}
export function validateNormal(b:NormalBook){
if(b?.schedules!==undefined)validateSchedules(b.schedules);
if(b?.assessment!==undefined)validateNormalAssessment(b.assessment);
if(b?.base!==undefined)validateNormalBase(b.base);
if(b?.profit!==undefined)validateNormalProfit(b.profit);
if(!b||b.version!==1||['year','mappingReference','scopeReference'].some(k=>typeof b[k as 'year']!=='string')||!['','resident_general','other'].includes(b.scope)||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history))throw Error('Preparação IRPC normal não recuperável.');
if(!b||b.version!==1||['year','mappingReference','scopeReference'].some(k=>typeof b[k as 'year']!=='string')||!['','resident_general','ep_general','other'].includes(b.scope)||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history))throw Error('Preparação IRPC normal não recuperável.');
for(const r of b.rows)if(!r||Object.keys(newNormalAdjustment('')).some(k=>typeof r[k as keyof NormalAdjustment]!=='string')||!['','noncontractual_fine','compensatory_interest','other'].includes(r.rule)||!['','yes','no'].includes(r.included))throw Error('Ajustamento IRPC não recuperável;original conservado.');
if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identificador de ajustamento duplicado.');
for(const r of [b.result,...(b.history??[])])if(r&&(!r.model||![irpcNormalVersion,normalProfitVersion,normalBaseVersion,normalAssessmentVersion].includes(r.model.version)||typeof r.key!=='string'||typeof r.createdAt!=='string'||!Array.isArray(r.model.rows)||!Array.isArray(r.model.pending)))throw Error('Histórico IRPC normal não recuperável.');
for(const r of [b.result,...(b.history??[])])if(r&&(!r.model||![irpcNormalVersion,normalProfitVersion,normalBaseVersion,normalAssessmentVersion,normalSchedulesVersion].includes(r.model.version)||typeof r.key!=='string'||typeof r.createdAt!=='string'||!Array.isArray(r.model.rows)||!Array.isArray(r.model.pending)))throw Error('Histórico IRPC normal não recuperável.');
}
export function normalBook(d:Record<string,unknown>):NormalBook{
const m=store(d).irpcNormal;if(m===undefined)return newNormalBook();if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa IRPC normal não recuperável.');const b=m[acquisitionScope(d)]??newNormalBook();validateNormal(b);return b;
}
export function saveNormalBook(d:Record<string,unknown>,b:NormalBook){validateNormal(b);const s=store(d);if(s.irpcNormal!==undefined&&(!s.irpcNormal||typeof s.irpcNormal!=='object'||Array.isArray(s.irpcNormal)))throw Error('Mapa IRPC normal não recuperável.');return JSON.stringify({...s,irpcNormal:{...s.irpcNormal,[acquisitionScope(d)]:b}});}
function key(d:Record<string,unknown>,b:NormalBook){return JSON.stringify([b.assessment?normalAssessmentVersion:b.base?normalBaseVersion:b.profit?normalProfitVersion:irpcNormalVersion,acquisitionScope(d),d.business_plan_snapshot??'',b.year,b.mappingReference,b.scope,b.scopeReference,b.rows,...(b.profit?[b.profit]:[]),...(b.base?[b.base]:[]),...(b.assessment?[b.assessment]:[])]);}
function key(d:Record<string,unknown>,b:NormalBook){return JSON.stringify([b.schedules?normalSchedulesVersion:b.assessment?normalAssessmentVersion:b.base?normalBaseVersion:b.profit?normalProfitVersion:irpcNormalVersion,acquisitionScope(d),d.business_plan_snapshot??'',b.year,b.mappingReference,b.scope,b.scopeReference,b.rows,...(b.profit?[b.profit]:[]),...(b.base?[b.base]:[]),...(b.assessment?[b.assessment]:[]),...(b.schedules?[b.schedules]:[])]);}
export function normalCurrent(d:Record<string,unknown>,b:NormalBook){return !!b.result&&b.result.key===key(d,b);}
const zero=()=>fraction(0n,1n);
const cents=(s:string)=>{if(!/^-?\d+(?:\.\d{1,2})?$/.test(s))throw Error('Montante MZN inválido;máximo dois decimais.');const v=decimalFraction(s.startsWith('-')?s.slice(1):s);return s.startsWith('-')?fraction(-BigInt(v.numerator),BigInt(v.denominator)):v;};
const integerCents=(v:FiscalFraction)=>BigInt(v.numerator)*100n/BigInt(v.denominator);
export function calculateNormal(d:Record<string,unknown>,b:NormalBook):NormalModel{
validateNormal(b);const pending:string[]=[],source=normalFinancialSource(d);
validateNormal(b);const pending:string[]=[],source=normalFinancialSource(d),fiscalPeriod=b.schedules?normalSchedulePeriod(b.schedules,b.scope):undefined;
if(fiscalPeriod)pending.push(...fiscalPeriod.pending);
if(!source)pending.push('Falta plano financeiro reconciliado.');
if(source?.currency!=='MZN')pending.push('Ponte em MZN necessária;sem conversão fiscal anual presumida.');
if(!/^2026$/.test(b.year)||!b.mappingReference.trim()||source?.periods!==12)pending.push('Documente correspondência dos 12 meses do plano ao exercício2026;outros exercícios/horizontes permanecem separados.');
if(b.scope!=='resident_general'||!b.scopeReference.trim())pending.push('Identifique sujeito residente no regime geral,fora de transparência/regimes especiais;EP e outros âmbitos não abrangidos.');
if(!(b.scope==='resident_general'||b.schedules&&b.scope==='ep_general')||!b.scopeReference.trim())pending.push('Identifique sujeito residente no regime geral,fora de transparência/regimes especiais;EP e outros âmbitos não abrangidos.');
const rows:NormalModel['rows']=b.rows.map(r=>({...structuredClone(r),status:'indeterminate',adjustment:null,reason:''}));
const amounts=new Map<string,FiscalFraction>(),groups=new Map<string,NormalModel['rows']>();
for(const r of rows){
@@ -63,20 +67,22 @@ export function calculateNormal(d:Record<string,unknown>,b:NormalBook):NormalMod
for(const r of rows){if(!r.reason){r.status='calculated';r.adjustment=amounts.get(r.id)!;r.reason='Acréscimo integral de custo incluído no resultado de partida,qualificado pelo utilizador como '+(r.rule==='noncontractual_fine'?'multa/encargo de infracção sem origem contratual':'juros compensatórios')+';CIRPC36(1)c. Projecção pelos pressupostos do plano.';}else pending.push(r.document+': '+r.reason);}
const additions=addFractions(rows.flatMap(r=>r.adjustment?[r.adjustment]:[]));
const accountingProfit=source?.currency==='MZN'?cents(source.accountingProfitBeforeTax):null;
const contextReady=!!source&&source.currency==='MZN'&&source.periods===12&&b.year==='2026'&&!!b.mappingReference.trim()&&b.scope==='resident_general'&&!!b.scopeReference.trim();
const contextReady=(!fiscalPeriod||fiscalPeriod.pending.length===0)&&!!source&&source.currency==='MZN'&&source.periods===12&&b.year==='2026'&&!!b.mappingReference.trim()&&(b.scope==='resident_general'||!!b.schedules&&b.scope==='ep_general')&&!!b.scopeReference.trim();
if(rows.length===0&&!b.profit)pending.push('Sem ajustamentos documentados;ausência de registos não prova inexistência de correcções.');
if(!b.profit)pending.push('L6 parcial:resultado ajustado apenas destes acréscimos;restantes custos/variações patrimoniais,amortizações,perdas,deduções e colecta por tratar. Lucro tributável,matéria colectável e imposto indeterminados.');
const profit=b.profit?closeNormalProfit(b.profit,source,rows,accountingProfit,additions,contextReady):undefined;
const schedules=b.schedules?calculateSchedules(b.schedules,source,rows):undefined;
const profit=b.profit?closeNormalProfit(b.profit,source,rows,accountingProfit,additions,contextReady,schedules):undefined;
if(profit)pending.push(...profit.pending,...(!b.base?['Matéria colectável,perdas reportadas,deduções e imposto não apurados neste percurso.']:[]));
const base=b.base?calculateNormalBase(b.base,profit?.taxableProfit??null,b.year):undefined;
const base=b.base?calculateNormalBase(b.base,profit?.taxableProfit??null,b.year,b.schedules?.currentLossReference):undefined;
if(base)pending.push(...base.pending,...(!b.assessment?['Colecta/imposto a pagar,créditos anuais eM20/M22 não determinados;matéria colectável não é imposto.']:[]));
const assessment=b.assessment?calculateNormalAssessment(b.assessment,base?.taxableBase??null,{year:b.year,scope:b.scope,currency:source?.currency??''}):undefined;
const assessment=b.assessment?calculateNormalAssessment(b.assessment,base?.taxableBase??null,{year:b.year,scope:b.scope,extended:!!b.schedules,currency:source?.currency??'',...(fiscalPeriod?{period:{from:fiscalPeriod.from,to:fiscalPeriod.to}}:{})}):undefined;
if(assessment)pending.push(...assessment.pending,assessment.finalBalance.reason);
return{...(assessment?{assessment,colecta:assessment.colecta}:{}),...(base?{base}:{}),...(profit?{profit}:{}),version:b.assessment?normalAssessmentVersion:b.base?normalBaseVersion:b.profit?normalProfitVersion:irpcNormalVersion,year:b.year,currency:source?.currency??'',origin:'projected',accountingProfit,additions,partialAdjustedResult:contextReady&&rows.length>0&&rows.every(r=>r.status==='calculated')&&accountingProfit?addFractions([accountingProfit,additions]):null,taxableProfit:profit?.taxableProfit??null,taxableBase:base?.taxableBase??null,tax:null,rows,pending,source:irpcNormalSource};
const currentLoss=b.schedules&&profit?.taxableProfit&&BigInt(profit.taxableProfit.numerator)<0n?fraction(-BigInt(profit.taxableProfit.numerator),BigInt(profit.taxableProfit.denominator)):null;
return{...(schedules?{schedules,currentLoss,fiscalPeriod}:{}),...(assessment?{assessment,colecta:assessment.colecta}:{}),...(base?{base}:{}),...(profit?{profit}:{}),version:b.schedules?normalSchedulesVersion:b.assessment?normalAssessmentVersion:b.base?normalBaseVersion:b.profit?normalProfitVersion:irpcNormalVersion,year:b.year,currency:source?.currency??'',origin:'projected',accountingProfit,additions,partialAdjustedResult:contextReady&&rows.length>0&&rows.every(r=>r.status==='calculated')&&accountingProfit?addFractions([accountingProfit,additions]):null,taxableProfit:profit?.taxableProfit??null,taxableBase:base?.taxableBase??null,tax:null,rows,pending,source:irpcNormalSource+(b.schedules?' Mapas complementares: CIRPC18/21/22/24/36; Decreto72/2013 art3–7. EP: CIRPC15(1)c, apenas resultado imputável ao estabelecimento no regime geral, anual2026/MZN.':'')};
}
export function recordNormal(d:Record<string,unknown>,b:NormalBook,now=new Date().toISOString()):NormalBook{return{...b,history:b.result?[...(b.history??[]),structuredClone(b.result)]:b.history??[],result:{key:key(d,b),createdAt:now,model:calculateNormal(d,b)}};}
const money=(v:FiscalFraction|null)=>v?fractionPresentation(v).decimal+' MZN':'Indeterminado';
function normalBaseReport(m:NormalBaseModel):Array<[string,string,string,string]>{return[['IRPC normal — L6','Matéria colectável — deduções/perdas',`Lucro tributável:${money(m.taxableProfit)};perdas propostas elegíveis:${money(m.requestedLosses)};perdas deduzidas nesta simulação:${money(m.deductedLosses)};benefícios deduzidos:${money(m.benefitDeduction)};matéria colectável simulada:${money(m.taxableBase)};imposto a pagar:Indeterminado. ${m.pending.join(' | ')}`,`NUIT:${m.input.subjectNuit};${m.input.identityReference};método:${m.input.method==='direct'?'Declarativo/directo':m.input.method==='indirect'?'Indirecto':'Por identificar'};${m.input.methodReference};perdas:${m.input.lossesState==='absent'?'Ausência declarada':m.input.lossesState==='known'?'Conhecidas':'Informação em falta'};${m.input.lossesReference};objecto:${m.input.unchangedObject==='yes'?'Sem alteração declarada':m.input.unchangedObject==='no'?'Alteração declarada':'Por esclarecer'};${m.input.objectReference};${m.source}`],['IRPC normal — L6','Benefícios à matéria colectável',`Estado:${m.input.benefits.state==='known'?'Valor conhecido':m.input.benefits.state==='absent'?'Ausência declarada':'Informação em falta'};montante original:${m.input.benefits.amount||'Não preenchido'};dedução:${money(m.benefitDeduction)};não inclui créditos à colecta ou retenções sofridas.`,m.input.benefits.reference],...m.losses.map(l=>['IRPC normal — L6','Perda fiscal de '+l.year,`Estado:${l.status==='eligible'?'Elegível neste recorte':l.status==='excluded'?'Excluída neste exercício':'Indeterminada'};apurada:${l.assessed};utilizada antes:${l.previouslyUsed};saldo:${money(l.available)};utilização proposta:${l.requested};dedução nesta simulação:${money(l.deducted)};saldo aritmético posterior:${money(l.remaining)};último exercício ordinário:${l.lastOrdinaryYear??'Por determinar'};${l.reason}`,`NUIT:${l.subjectNuit};${l.reference};sem consumo do saldo oficial nem reporte automático.`]as[string,string,string,string])];}
function normalAssessmentReport(m:NormalAssessmentModel):Array<[string,string,string,string]>{return[['IRPC normal — L6','Colecta — regime normal',`Matéria colectável:${money(m.taxableBase)};taxa:${m.rate?m.rate.numerator+'%':'Por identificar'};colecta simulada:${money(m.colecta)};${m.classification};${m.centExact===false?'Valor racional exacto com fracção de cêntimo;sem arredondamento fiscal aplicado.':''}${m.pending.join(' | ')}`,`Exercício:${m.period.year};período:${m.period.from}–${m.period.to};${m.input.reference};${m.rule};${m.source}`],['IRPC normal — L6','Saldo final — âmbito L8','A pagar:Indeterminado;a recuperar:Indeterminado. '+m.finalBalance.reason,'Sequência:lucro tributável → matéria colectável → colecta → saldo final;retenções,pagamentos e créditos anuais reservados aL8.']];}
export function normalReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=normalBook(d),r=b.result;if(!r)return[];const m=r.model;return[['IRPC normal — L6','Ajustamentos ao resultado financeiro',`Exercício:${m.year};resultado contabilístico antes de imposto:${money(m.accountingProfit)};acréscimos demonstrados:${money(m.additions)};resultado parcial ajustado:${money(m.partialAdjustedResult)};${m.profit?'lucro tributável simulado:'+money(m.taxableProfit)+(m.base?';matéria colectável simulada:'+money(m.taxableBase)+(m.assessment?';colecta simulada:'+money(m.colecta??null)+';saldo final a pagar ou recuperar:Indeterminado (L8).':';imposto a pagar:Indeterminado.'):';matéria colectável/imposto:Indeterminados.'):'lucro tributável/matéria colectável/imposto:Indeterminados.'} Projecção com pressupostos;${m.pending.join(' | ')}`,`${normalCurrent(d,b)?'Entradas correspondentes':'Histórico;origem ou entradas alteradas'};${r.createdAt};${m.source}`],...(m.base?normalBaseReport(m.base):[]),...(m.assessment?normalAssessmentReport(m.assessment):[]),...m.rows.map(r=>['IRPC normal — L6',r.document+'/'+r.line,`${({calculated:'Calculado neste recorte',indeterminate:'Indeterminado',outside_scope:'Fora deste recorte'})[r.status]};montante:${r.amount};acréscimo:${money(r.adjustment)};${r.reason}`,`Origem:${({revenue:'Receitas',cost_of_sales:'Custo das vendas',operating_expenses:'Despesas operacionais',personnel:'Pessoal',depreciation:'Depreciação contabilística',interest_expense:'Juros reconhecidos'}as Record<string,string>)[r.sourceCode]??'Por corresponder'};incluído:${r.included==='yes'?'Sim':r.included==='no'?'Não':'Por demonstrar'};suporte:${r.reference}`]as[string,string,string,string]),...(m.profit?[...m.profit.lines.map(l=>['IRPC normal — L6','Conferência: '+l.label,`Estado:${l.state==='known'?'Valor conhecido':l.state==='absent'?'Ausência declarada':'Informação em falta'};origem:${l.amount};acréscimos:${money(l.addedBack)};remanescente:${money(l.remainder)};${l.reason}`,l.reference]as[string,string,string,string]),...m.profit.topics.map(t=>['IRPC normal — L6',t.label,`Estado:${t.state==='known'?'Valor conhecido':t.state==='absent'?'Ausência declarada':'Informação em falta'};montante:${money(t.value)};${t.reason}`,t.reference]as[string,string,string,string]),['IRPC normal — L6','Âmbito da conferência',m.profit.input.completenessReference,m.profit.source]as[string,string,string,string]]:[])];}catch(e){return[['IRPC normal — L6','Não recuperável',String(e),'Originais conservados.']];}}
export function normalReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=normalBook(d),r=b.result;if(!r)return[];const m=r.model;return[['IRPC normal — L6','Ajustamentos ao resultado financeiro',`Exercício:${m.year};resultado contabilístico antes de imposto:${money(m.accountingProfit)};acréscimos demonstrados:${money(m.additions)};resultado parcial ajustado:${money(m.partialAdjustedResult)};${m.profit?'lucro tributável simulado:'+money(m.taxableProfit)+(m.base?';matéria colectável simulada:'+money(m.taxableBase)+(m.assessment?';colecta simulada:'+money(m.colecta??null)+';saldo final a pagar ou recuperar:Indeterminado (L8).':';imposto a pagar:Indeterminado.'):';matéria colectável/imposto:Indeterminados.'):'lucro tributável/matéria colectável/imposto:Indeterminados.'} Projecção com pressupostos;${m.pending.join(' | ')}`,`${normalCurrent(d,b)?'Entradas correspondentes':'Histórico;origem ou entradas alteradas'};${r.createdAt};${m.source}`],...(m.base?normalBaseReport(m.base):[]),...(m.assessment?normalAssessmentReport(m.assessment):[]),...(m.schedules?[['IRPC normal — L6','Mapas complementares',`Efeito líquido adicional:${money(m.schedules.net)};perda fiscal do próprio exercício:${money(m.currentLoss??null)};sem reporte ou consumo automático. ${m.schedules.pending.join(' | ')}`,m.schedules.source]as[string,string,string,string],...m.schedules.effects.map(e=>{const i=m.schedules!.input.rows.find(r=>r.id===e.id)!;return['IRPC normal — L6',i.document+'/'+i.line,`Montante:${money(e.amount)};efeito:${money(e.effect)};${e.reason}`,`${i.reference};exercício de imputação:${i.fiscalYear||'Não aplicável'}`]as[string,string,string,string];}),...m.schedules.assets.map(a=>['IRPC normal — L6',a.description,`Depreciação contabilística:${money(a.accountingValue)};quota fiscal:${money(a.fiscal)};acréscimo:${money(a.adjustment)};${a.reason}`,`${a.tableReference};${a.reference}`]as[string,string,string,string]),['IRPC normal — L6','Juros financeiros ordinários',`Remanescente aceite no recorte:${money(m.schedules.interest)};não corresponde a juros de mora/compensatórios L5.`,m.schedules.input.interest.reference]as[string,string,string,string]]:[]),...(m.schedules?[...normalFormFields(m).fields.map(f=>['IRPC normal — L6','M22 campo '+f.code+' — '+f.label,`${f.display};exacto:${f.value?f.value.numerator+'/'+f.value.denominator:'Indeterminado'};transcrição:${f.transcriptionReady?'Preparação parcial com valor exacto ao cêntimo':'Pendente ou sem aplicação'};${f.reason}`,normalFormFields(m).source]as[string,string,string,string]),...normalFormFields(m).m20.fields.map(f=>['IRPC normal — L6','M20 A.1 — conta '+f.account+' — '+f.label,`${f.display};representação em meticais inteiros:${f.transcriptionReady?'Exacta':'Pendente'};${f.reason}`,`${f.reference};${normalFormFields(m).m20.source}`]as[string,string,string,string]),['IRPC normal — L6','M20 — correspondência',normalFormFields(m).m20.reason,normalFormFields(m).m20.source]as[string,string,string,string]]:[]),...m.rows.map(r=>['IRPC normal — L6',r.document+'/'+r.line,`${({calculated:'Calculado neste recorte',indeterminate:'Indeterminado',outside_scope:'Fora deste recorte'})[r.status]};montante:${r.amount};acréscimo:${money(r.adjustment)};${r.reason}`,`Origem:${({revenue:'Receitas',cost_of_sales:'Custo das vendas',operating_expenses:'Despesas operacionais',personnel:'Pessoal',depreciation:'Depreciação contabilística',interest_expense:'Juros reconhecidos'}as Record<string,string>)[r.sourceCode]??'Por corresponder'};incluído:${r.included==='yes'?'Sim':r.included==='no'?'Não':'Por demonstrar'};suporte:${r.reference}`]as[string,string,string,string]),...(m.profit?[...m.profit.lines.map(l=>['IRPC normal — L6','Conferência: '+l.label,`Estado:${l.state==='known'?'Valor conhecido':l.state==='absent'?'Ausência declarada':'Informação em falta'};origem:${l.amount};acréscimos:${money(l.addedBack)};remanescente:${money(l.remainder)};${l.reason}`,l.reference]as[string,string,string,string]),...m.profit.topics.map(t=>['IRPC normal — L6',t.label,`Estado:${t.state==='known'?'Valor conhecido':t.state==='absent'?'Ausência declarada':'Informação em falta'};montante:${money(t.value)};${t.reason}`,t.reference]as[string,string,string,string]),['IRPC normal — L6','Âmbito da conferência',m.profit.input.completenessReference,m.profit.source]as[string,string,string,string]]:[])];}catch(e){return[['IRPC normal — L6','Não recuperável',String(e),'Originais conservados.']];}}
export function normalHtml(d:Record<string,unknown>){const b=normalBook(d);if(!normalCurrent(d,b))throw Error('Calcule e guarde as entradas actuais antes de exportar.');const esc=(s:string)=>s.replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));return'<!doctype html><html lang="pt"><meta charset="utf-8"><title>IRPC normal — ajustamentos</title><h1>L6 — mapa parcial de ajustamentos</h1><p>Projecção pelos pressupostos do plano. Não é demonstração contabilística,liquidação ou Modelo20/22.</p>'+normalReportRows(d).map(r=>'<section><h2>'+esc(r[1])+'</h2><p>'+esc(r[2])+'</p><small>'+esc(r[3])+'</small></section>').join('')+'</html>';}
@@ -0,0 +1,35 @@
<?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 IrpcNormalSchedulesDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_complementary_rules_recover_calculated_missing_and_current_loss_without_settlement(): 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()]));
$id=null;$etag=null;
foreach(['calculated','missing','loss','ep'] as $case){
$d=json_decode(file_get_contents(base_path('outputs/irpc-l6-sets/'.$case.'.json')),true,flags:JSON_THROW_ON_ERROR);$p=['person_type'=>$d['person_type']];unset($d['person_type']);
if($id===null)$id=$this->postJson('/api/v1/simulation-drafts',['personal_snapshot'=>$p,'economic_snapshot'=>$d],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated()->json('data.id');
else $this->putJson('/api/v1/simulation-drafts/'.$id,['personal_snapshot'=>$p,'economic_snapshot'=>$d],['If-Match'=>$etag])->assertOk();
$r=$this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk();$etag=$r->headers->get('ETag');$back=$r->json('data.economic_snapshot');
$this->assertSame($d['modelo_a_snapshot'],$back['modelo_a_snapshot']);$this->assertSame($d['business_plan_snapshot'],$back['business_plan_snapshot']);
$s=json_decode($back['modelo_a_snapshot'],true,flags:JSON_THROW_ON_ERROR);$b=$s['irpcNormal'][$d['context_opportunity']];$m=$b['result']['model'];
$this->assertSame('IRPC-normal-adjustments/5',$m['version']);$this->assertNull($m['tax']);
$this->assertSame('2026',$m['assessment']['period']['year']);$this->assertSame($case==='ep'?'2025-07-01':'2026-01-01',$m['assessment']['period']['from']);$this->assertSame($case==='ep'?'2026-06-30':'2026-12-31',$m['assessment']['period']['to']);
$this->assertSame('L8',$m['assessment']['finalBalance']['scope']);$this->assertNull($m['assessment']['finalBalance']['payable']);$this->assertNull($m['assessment']['finalBalance']['recoverable']);
$this->assertContains('IRPC-normal-adjustments/4',array_column(array_column($b['history'],'model'),'version'));$this->assertArrayHasKey('schedules',$m);$this->assertArrayHasKey('irpcWithholdings',$s);
if(in_array($case,['calculated','ep'],true)){$this->assertSame('6980',$m['taxableProfit']['numerator']);$this->assertSame('4680',$m['taxableBase']['numerator']);$this->assertSame(['numerator'=>'7488','denominator'=>'5'],$m['colecta']);$this->assertSame('120',$m['schedules']['interest']['numerator']);}
elseif($case==='missing'){$this->assertNull($m['taxableProfit']);$this->assertNull($m['colecta']);$this->assertNull($m['schedules']['interest']);$this->assertSame('520',$m['schedules']['net']['numerator']);}
else{$this->assertSame('3820',$m['currentLoss']['numerator']);$this->assertSame('0',$m['taxableBase']['numerator']);$this->assertSame('0',$m['colecta']['numerator']);$this->assertSame([],$b['base']['losses']);}
file_put_contents(base_path('outputs/irpc-l6-sets/recovered-'.$case.'.json'),json_encode($back,JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE));
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {normalBook,normalCurrent,calculateNormal,recordNormal,saveNormalBook,normalHtml,normalFinancialSource,normalReportRows} from '../../resources/js/lib/irpc-normal.ts';
import {newNormalSchedules,newScheduleRow,newNormalAsset,calculateSchedules} from '../../resources/js/lib/irpc-normal-schedules.ts';
import {normalFormFields} from '../../resources/js/lib/irpc-normal-forms.ts';import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-assessment-preserved.json',import.meta.url),'utf8'));
function scenario(){const d=seed(),b=normalBook(d),p=JSON.parse(d.business_plan_snapshot);p.plan.monthlyInterestExpense='20';p.plan.assetDepreciation='120';d.business_plan_snapshot=JSON.stringify(p);b.schedules=newNormalSchedules();const s=b.schedules;s.period={kind:'calendar',from:'2026-01-01',to:'2026-12-31',reference:'QA doze meses do plano correspondem ao ano civil completo'};
s.interest={amount:'120',reference:'QA contrato bancário, exploração, sem relação especial, limites especiais ou capitalização; pagamentos separados',conditions:'yes'};
for(const [id,rule,amount,sourceCode,fiscalYear]of[['thirdparty','tax_other','250','operating_expenses',''],['gain','patrimonial_gain','200','',''],['cost','patrimonial_cost','50','',''],['timing','future_cost','100','operating_expenses','2027']])s.rows.push({...newScheduleRow(id),rule,amount,sourceCode,fiscalYear,document:'QA-'+id,line:'1',reference:'QA factos documentados no recorte ordinário, não é operação especial',conditions:'yes'});
for(const[k,amount]of Object.entries({expenseCorrections:'250',positivePatrimonial:'200',negativePatrimonial:'50',timing:'100'}))b.profit.topics[k]={state:'known',amount,reference:'QA reconciliação integral do tema com os documentos do mapa'};
b.profit.lines.find(l=>l.code==='depreciation').state='known';s.assetsReference='QA único activo; total contabilístico120 reconciliado com o plano';s.assets=[{...newNormalAsset('server'),category:'server',description:'Servidor QA',cost:'500',residual:'0',rate:'20',accounting:'120',previousFiscal:'0',inUseFrom:'2026-01-01',tableReference:'Decreto72/2013 tabelaII grupo3 item9.1, sem taxa específica da tabelaI',reference:'QA factura500, activo próprio, vida útil fiscal, sem regras especiais',conditions:'yes'}];return{d,b};}
const val=f=>f?Number(f.numerator)/Number(f.denominator):null;
test('new rules reconcile financial interest, patrimonial variations, timing and server quota through profit/base/colecta',()=>{const{d,b}=scenario(),m=calculateNormal(d,b);assert.equal(val(m.accountingProfit),6240);assert.equal(val(m.additions),220);assert.equal(val(m.schedules.interest),120);assert.equal(val(m.schedules.depreciation),20);assert.equal(val(m.schedules.net),520);assert.equal(val(m.taxableProfit),6980);assert.equal(val(m.taxableBase),4680);assert.equal(val(m.colecta),1497.6);assert.equal(m.tax,null);assert.equal(m.assessment.finalBalance.payable,null);assert.equal(m.schedules.assets[0].fiscal.numerator,'100');assert.equal(m.version,'IRPC-normal-adjustments/5');});
test('ordinary interest is qualified independently from compensatory interest and cash payments',()=>{const{d,b}=scenario();b.schedules.interest.amount='119';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(val(m.schedules.net),520);assert.equal(m.schedules.interest,null);assert.match(m.profit.pending.join(' '),/Juros financeiros/);const c=scenario();c.b.schedules.rows[0].sourceCode='interest_expense';c.b.schedules.rows[0].amount='20';c.b.profit.topics.expenseCorrections.amount='20';assert.equal(calculateNormal(c.d,c.b).taxableProfit,null);c.b.schedules.interest.amount='100';assert.equal(val(calculateNormal(c.d,c.b).schedules.interest),100);assert.notEqual(calculateNormal(c.d,c.b).taxableProfit,null);});
test('missing rule evidence blocks dependent totals while independent amounts remain calculated',()=>{const{d,b}=scenario();b.schedules.rows[0].reference='';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.colecta,null);assert.equal(val(m.schedules.effects.find(e=>e.id==='gain').effect),200);assert.equal(val(m.schedules.depreciation),20);assert.equal(val(m.accountingProfit),6240);});
test('duplicate documents, over-allocation, wrong source and contradictory topic absence are rejected',()=>{for(const mutate of[b=>{b.schedules.rows[0].document=b.rows[0].document+' ';b.schedules.rows[0].line=b.rows[0].line;},b=>{b.schedules.rows[0].amount='1201';b.profit.topics.expenseCorrections.amount='1201';},b=>b.schedules.rows[0].sourceCode='revenue',b=>b.profit.topics.positivePatrimonial={state:'absent',amount:'',reference:'QA declared absent'}]){const{d,b}=scenario();mutate(b);assert.equal(calculateNormal(d,b).taxableProfit,null);}});
test('capital contributions and distributions outside result are known amounts with zero tax effect, not ordinary gains/costs',()=>{const{d,b}=scenario();b.schedules.rows.find(r=>r.id==='gain').rule='capital_in';b.schedules.rows.find(r=>r.id==='cost').rule='capital_out';const m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),6830);assert.equal(val(m.schedules.effects.find(e=>e.id==='gain').effect),0);assert.equal(val(m.schedules.effects.find(e=>e.id==='cost').amount),50);});
test('timing belongs to documented year; previous years and inconsistent mapping stay pending',()=>{for(const y of['','2025','2026']){const{d,b}=scenario();b.schedules.rows.find(r=>r.id==='timing').fiscalYear=y;assert.equal(calculateNormal(d,b).taxableProfit,null);}});
test('asset quota uses remaining fiscal value; unsupported rates, short-year facts and incomplete reconciliation do not default to accounting',()=>{const{d,b}=scenario();b.schedules.assets[0].previousFiscal='450';let m=calculateNormal(d,b);assert.equal(val(m.schedules.assets[0].fiscal),50);assert.equal(val(m.schedules.depreciation),70);for(const patch of[{rate:'25'},{category:'other'},{inUseFrom:'2026-02-01'},{inUseFrom:'2025-02-31'},{inUseFrom:'2014-01-01'},{accounting:'119'},{conditions:''}]){const{d,b}=scenario();Object.assign(b.schedules.assets[0],patch);m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.schedules.depreciation,null);}});
test('known current loss gives zero base/colecta only with documented current-loss treatment and no self-deduction; no official carryforward created',()=>{const{d,b}=scenario(),p=JSON.parse(d.business_plan_snapshot);p.plan.monthlyOperatingExpenses='1000';d.business_plan_snapshot=JSON.stringify(p);b.base.losses=[];b.base.lossesState='absent';b.base.lossesReference='QA ausência expressa de perdas anteriores';let m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),-3820);assert.equal(m.taxableBase,null);b.schedules.currentLossReference='QA perda fiscal corrente apurada, sem dedução de perdas anteriores ou reporte automático';m=calculateNormal(d,b);assert.equal(val(m.currentLoss),3820);assert.equal(val(m.taxableBase),0);assert.equal(val(m.colecta),0);assert.deepEqual(b.base.losses,[]);assert.equal(normalFormFields(m).fields.find(f=>f.code==='269').value.numerator,'3820');assert.equal(normalFormFields(m).fields.find(f=>f.code==='270').value,null);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.mkdirSync('outputs/irpc-l6-sets',{recursive:true});fs.writeFileSync('outputs/irpc-l6-sets/loss.json',JSON.stringify(d));});
test('EP ordinary annual MZN uses its attributable financial result; unsupported currency/year remains case-specific',()=>{const{d,b}=scenario();b.scope='ep_general';b.scopeReference='QA único EP em Moçambique, imputação completa e exclusiva de custos/proveitos do EP, CIRPC15(1)c';assert.equal(val(calculateNormal(d,b).colecta),1497.6);b.year='2025';assert.equal(calculateNormal(d,b).colecta,null);});
test('form preparation keeps exact fractions and unmapped fields without provisional codes or fabricated final settlement',()=>{const{d,b}=scenario(),m=calculateNormal(d,b),f=normalFormFields(m);assert.equal(val(f.fields.find(f=>f.code==='278').value),4680);assert.equal(f.fields.find(f=>f.code==='300').transcriptionReady,true);for(const code of['200','302','309–316'])assert.equal(f.fields.find(f=>f.code===code).value,null);assert.equal(f.m20.status,'partial');assert.equal(val(f.m20.fields.find(f=>f.account==='6.5').value),120);assert.match(f.m20.reason,/campo201.*campo200/);const partial=normalFormFields({...m,colecta:fraction(2n,625n)}).fields.find(f=>f.code==='300');assert.deepEqual(partial.value,{numerator:'2',denominator:'625'});assert.equal(partial.transcriptionReady,false);assert.match(partial.reason,/sem arredondamento/);});
test('installed4 is current without recalculation; new inputs survive save/recovery/history/export/dossier and become stale on edit',()=>{const original=seed(),old=normalBook(original);assert.equal(normalCurrent(original,old),true);assert.equal(old.result.model.version,'IRPC-normal-adjustments/4');const{d,b}=scenario(),before=JSON.parse(d.modelo_a_snapshot);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-25T23:50:00Z'));const recovered=JSON.parse(JSON.stringify(d)),back=normalBook(recovered);assert.equal(normalCurrent(recovered,back),true);assert.deepEqual(back.history.at(-1),old.result);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).books,before.books);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,before.irpcWithholdings);assert.match(normalHtml(d),/1497.6/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/M22 campo 300/);assert.match(JSON.stringify(normalReportRows(d)),/M20/);fs.writeFileSync('outputs/irpc-l6-sets/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-sets/preparation.html',normalHtml(d));back.schedules.rows[0].amount='251';recovered.modelo_a_snapshot=saveNormalBook(recovered,back);assert.equal(normalCurrent(recovered,normalBook(recovered)),false);assert.throws(()=>normalHtml(recovered));const missing=normalBook(d);missing.schedules.interest.conditions='';d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,missing));fs.writeFileSync('outputs/irpc-l6-sets/missing.json',JSON.stringify(d));});
test('unknown schema/rules fail without silently replacing saved originals',()=>{const{d,b}=scenario();b.schedules.version=2;assert.throws(()=>saveNormalBook(d,b));b.schedules.version=1;b.schedules.rows[0].rule='fiction';assert.throws(()=>saveNormalBook(d,b));});
test('documented noncalendar EP year preserves12 monthly periods; short periods and absent legal basis stay case-specific',()=>{const{d,b}=scenario();b.scope='ep_general';b.schedules.period={kind:'ep_annual',from:'2025-07-01',to:'2026-06-30',reference:'QA comunicação expressa AT anterior, início e manutenção5exercícios, plano completo do EP'};b.schedules.assets[0].inUseFrom='2025-07-01';let m=calculateNormal(d,b);assert.equal(val(m.colecta),1497.6);assert.equal(m.assessment.period.from,'2025-07-01');assert.equal(m.assessment.period.to,'2026-06-30');d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.writeFileSync('outputs/irpc-l6-sets/ep.json',JSON.stringify(d));b.schedules.period.from='2026-01-01';assert.equal(calculateNormal(d,b).colecta,null);b.schedules.period.reference='';assert.equal(calculateNormal(d,b).taxableProfit,null);});
test('ordinary tax/undocumented addbacks and current income/cost timing apply their distinct signs without overwriting original accounting',()=>{for(const rule of['tax_own','undocumented']){const{d,b}=scenario();b.schedules.rows[0].rule=rule;assert.equal(val(calculateNormal(d,b).taxableProfit),6980);}for(const [rule,delta]of[['current_cost',-200],['current_income',0]]){const{d,b}=scenario(),r=b.schedules.rows.find(r=>r.id==='timing');r.rule=rule;r.sourceCode='';r.fiscalYear='2026';assert.equal(val(calculateNormal(d,b).taxableProfit),6980+delta);}const{d,b}=scenario(),r=b.schedules.rows.find(r=>r.id==='timing');r.rule='future_income';r.sourceCode='revenue';assert.equal(val(calculateNormal(d,b).taxableProfit),6780);assert.equal(normalFinancialSource(d).accountingProfitBeforeTax,'6240.00');});
+21
View File
@@ -0,0 +1,21 @@
<?php
$root=dirname(__DIR__);
$operation=$argv[1]??'';
if(!in_array($operation,['routes','test'],true)||basename($root)!=='portal-investidor-irpc-l6-sets-qa-20261025')throw new RuntimeException('Explicit reliefs QA workspace and routes/test operation required.');
foreach(['storage/framework/views','storage/framework/cache','storage/framework/sessions','bootstrap/cache','storage/logs','outputs/irpc-l6-sets']as$dir)if(!is_dir($root.'/'.$dir))mkdir($root.'/'.$dir,0700,true);
foreach(['APP_BASE_PATH'=>$root,'APP_ENV'=>'testing','APP_KEY'=>'base64:'.base64_encode(str_repeat('Q',32)),'DB_CONNECTION'=>'pgsql','DB_HOST'=>'127.0.0.1','DB_PORT'=>'55441','DB_DATABASE'=>'simulator_fiscal_delta_20261021_test','DB_USERNAME'=>'postgres','DB_PASSWORD'=>'','DB_URL'=>'','CACHE_STORE'=>'array','SESSION_DRIVER'=>'array','QUEUE_CONNECTION'=>'sync','MAIL_MAILER'=>'array'] as $k=>$v){putenv($k.'='.$v);$_ENV[$k]=$_SERVER[$k]=$v;}
$loader=require $root.'/vendor/autoload.php';
spl_autoload_register(function($class)use($root){foreach(['App\\'=>'app/','Tests\\'=>'tests/','Database\\Factories\\'=>'database/factories/']as$prefix=>$dir){if(str_starts_with($class,$prefix)){$file=$root.'/'.$dir.str_replace('\\','/',substr($class,strlen($prefix))).'.php';if(is_file($file)){require $file;return;}}}},true,true);
$app=require $root.'/bootstrap/app.php';
(new Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables)->bootstrap($app);
(new Illuminate\Foundation\Bootstrap\LoadConfiguration)->bootstrap($app);
$c=config('database.connections.pgsql');
if($app->configurationIsCached()||$c['host']!=='127.0.0.1'||(int)$c['port']!==55441||$c['database']!=='simulator_fiscal_delta_20261021_test'||!empty($c['url']))throw new RuntimeException('QA guard');
$pdo=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$c['database'],$c['username'],$c['password']);
if($pdo->query('select current_database()')->fetchColumn()!==$c['database'])throw new RuntimeException('QA identity');
if($operation==='routes'){
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$status=Illuminate\Support\Facades\Artisan::call('wayfinder:generate',['--with-form'=>true]);
echo Illuminate\Support\Facades\Artisan::output();exit($status);
}
$argv=['phpunit','-c','phpunit.investmoz.xml','tests/Feature/IrpcNormalSchedulesDraftTest.php'];$argc=count($argv);$_SERVER['argv']=$argv;$_SERVER['argc']=$argc;require $root.'/vendor/phpunit/phpunit/phpunit';