From 1d11a5e57bcf23b5f1a8ba4c3db4cd192fad383c Mon Sep 17 00:00:00 2001 From: nala Date: Wed, 11 Nov 2026 09:00:17 +0200 Subject: [PATCH] commit melhorias sgf --- AGENTS.md | 135 ++ README.md | 4 + app/Console/Commands/PopulateDimTempo.php | 47 + .../Api/V1/SgcIntegrationController.php | 175 +++ .../Api/V1/SgcInvoiceIngressController.php | 28 + .../Middleware/AuthenticateSgcInvoice.php | 70 + app/Http/Requests/Sgc/StoreInvoiceRequest.php | 34 + app/Models/Sgc/Fatura.php | 69 + app/Models/Sgc/OrdemCompra.php | 67 + app/Modules/Accounting/CashLedgerSource.php | 22 + app/Modules/Accounting/ComposedLedger.php | 79 ++ .../Accounting/ComposedLedgerCandidates.php | 27 + .../Accounting/ComposedLedgerController.php | 82 ++ .../Accounting/ComposedMaterialIsolation.php | 34 + .../Accounting/CostLedgerController.php | 1 + app/Modules/Accounting/CostLedgerSource.php | 22 +- .../Accounting/GeneralLedgerController.php | 9 + .../Accounting/IndustrialLedgerController.php | 3 +- .../Accounting/IndustrialLedgerSource.php | 16 +- .../Accounting/InternalStatementTotals.php | 20 + .../InternalStatementsController.php | 88 ++ .../LedgerCompositionController.php | 1 + .../Accounting/SalesLedgerController.php | 6 +- app/Modules/Accounting/SalesLedgerSource.php | 23 +- app/Modules/Accounting/SourceBook.php | 22 + app/Modules/Finance/BankLedgerSource.php | 18 + app/Modules/Finance/BankReconciliation.php | 86 ++ app/Modules/Finance/CashWorkspace.php | 89 ++ .../Finance/FinancialIndicatorSource.php | 13 + app/Modules/Finance/FinancialIndicators.php | 71 + .../Http/BankReconciliationController.php | 13 + app/Modules/Finance/Http/CashController.php | 12 + .../Http/FinancialIndicatorsController.php | 14 + .../Http/PersonnelController.php | 12 + .../Http/RemunerationsController.php | 12 + app/Modules/HumanResources/Personnel.php | 112 ++ app/Modules/HumanResources/PersonnelTime.php | 31 + app/Modules/HumanResources/Remunerations.php | 85 ++ .../Identity/Services/IdentityWorkflow.php | 18 +- .../Inventory/CompositionStockRead.php | 9 +- .../Inventory/StockCatalogueController.php | 78 ++ .../Inventory/StockCompletionController.php | 101 ++ .../Inventory/StockFinalController.php | 140 ++ app/Modules/Inventory/StockInactiveReport.php | 23 + .../Inventory/StockOperationsController.php | 150 +++ app/Modules/Purchasing/ComposedFinance.php | 32 + .../Purchasing/ComposedFinanceController.php | 37 + app/Modules/Purchasing/PayableAge.php | 19 + .../Purchasing/PayableDueController.php | 64 + .../PayablePositionReadController.php | 33 + app/Modules/Purchasing/ReceiptRuntime.php | 35 +- app/Modules/Purchasing/SgcInvoiceIngress.php | 54 + app/Modules/Services/BillingRegister.php | 41 + app/Modules/Services/CommercialQuotes.php | 55 + app/Modules/Services/DocumentPeriod.php | 12 + app/Modules/Services/FiscalAssessment.php | 99 ++ app/Modules/Services/FiscalCoverage.php | 32 + app/Modules/Services/FiscalDocumentMath.php | 18 + app/Modules/Services/FiscalObligations.php | 88 ++ app/Modules/Services/FiscalRuleExpression.php | 205 +++ app/Modules/Services/FiscalRules.php | 114 ++ .../Http/CommercialQuotesController.php | 13 + .../Http/FiscalAssessmentController.php | 13 + .../Http/FiscalObligationsController.php | 11 + .../Services/Http/FiscalRulesController.php | 14 + .../Http/ReceivablePositionReadController.php | 8 + .../Http/ServiceDocumentsController.php | 14 + .../Services/ReceivablePositionRead.php | 32 + app/Modules/Services/ServiceCalculation.php | 5 +- app/Modules/Services/ServiceDocuments.php | 143 ++ app/Modules/Services/ServiceFiscal.php | 4 +- app/Modules/Services/ServiceInvoicing.php | 6 + app/Modules/Services/ServiceReceivables.php | 42 +- app/Modules/Services/ServiceWorkflow.php | 1 + .../Workshop/ComposedWorkshopStockBook.php | 41 + .../Http/WorkshopEquipmentController.php | 9 + .../Http/WorkshopOrderReadController.php | 8 + .../Http/WorkshopScheduleController.php | 13 + .../Workshop/Http/WorkshopStockController.php | 5 +- app/Modules/Workshop/WorkshopBilling.php | 20 +- app/Modules/Workshop/WorkshopEquipment.php | 20 + app/Modules/Workshop/WorkshopOrderRead.php | 45 + app/Modules/Workshop/WorkshopSchedule.php | 88 ++ app/Modules/Workshop/WorkshopStock.php | 91 +- app/Providers/AppServiceProvider.php | 1 + app/Requests/Sgc/StoreInvoiceRequest.php | 43 + app/Requests/Sgc/StorePaymentRequest.php | 28 + app/Services/Sgc/BudgetCommitmentService.php | 90 ++ app/Services/Sgc/CalendarDimension.php | 29 + app/Services/Sgc/IntegrationService.php | 224 ++++ app/Services/Sgc/ThreeWayMatchService.php | 97 ++ app/Shared/Context/ModuleNavigation.php | 26 +- .../Installation/CompositionRuntime.php | 29 + app/Shared/Installation/CompositionSchema.php | 102 ++ .../Installation/CompositionSchemaBuilder.php | 30 + .../CompositionSchemaConnection.php | 28 + .../Installation/HumanResourcesProfiles.php | 25 + app/Shared/Installation/PresentationEntry.php | 75 ++ app/Shared/Installation/SyntheticProfiles.php | 84 ++ app/Shared/Testing/TestingDatabaseGuard.php | 1 + bootstrap/app.php | 1 + config/analysis_qa.php | 2 + config/bank_qa.php | 2 + config/cash_qa.php | 2 + config/commercial_quotes.php | 2 + config/fiscal_obligations.php | 2 + config/fiscal_qa.php | 2 + config/fiscal_rules_qa.php | 2 + config/hr_personnel.php | 2 + config/human_resources.php | 2 + config/internal_statements.php | 2 + config/payable_due.php | 2 + config/receipt_qa.php | 2 +- config/remunerations.php | 2 + config/service_documents.php | 2 + config/sgc_invoice.php | 9 + config/sgf_remaining_functions.php | 3 + config/sgf_single_installation.php | 4 + config/stock_catalogue.php | 2 + config/stock_completion.php | 2 + config/stock_operations.php | 4 + config/workshop_catalogue.php | 2 + config/workshop_schedule.php | 2 + config/workshop_stock.php | 2 +- ...9_000001_create_sgc_integration_tables.php | 184 +++ ..._09_000002_create_sgc_analytics_tables.php | 212 +++ database/sql/bank_reconciliation.sql | 31 + database/sql/cash.sql | 24 + database/sql/commercial_quotes.sql | 26 + database/sql/financial_indicators.sql | 25 + database/sql/fiscal_assessment.sql | 25 + database/sql/fiscal_obligations.sql | 25 + database/sql/fiscal_rules.sql | 27 + database/sql/hr_personnel.sql | 28 + database/sql/internal_statements.sql | 39 + database/sql/payable_due.sql | 25 + database/sql/remunerations.sql | 28 + database/sql/service_documents.sql | 35 + database/sql/stock_catalogue.sql | 22 + database/sql/workshop_schedule.sql | 32 + docs/SGC_8085_INTEGRATION_PREPARATION.md | 73 + docs/SGC_INVOICE_ENDPOINT.md | 60 + docs/SGC_INVOICE_WORKSPACE_DELIVERY.md | 68 + docs/SGC_PREPARATION_20261110.md | 37 + docs/SGF_8085_NEXT_AUTHORIZATION.md | 36 + docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md | 32 + docs/SGF_BANK_RECONCILIATION_DELIVERY.md | 51 + docs/SGF_BANK_RECONCILIATION_SCOPE.md | 36 + docs/SGF_BILLING_REGISTER_DELIVERY.md | 18 + docs/SGF_CASH_DELIVERY.md | 46 + docs/SGF_CASH_NEXT_SCOPE.md | 39 + docs/SGF_CLIENT_ASSET_DELIVERY.md | 9 + docs/SGF_CURRENT_DELIVERY_20261110.md | 86 ++ docs/SGF_DOCUMENT_READ_ACCESS.md | 16 + docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md | 51 + docs/SGF_FINANCIAL_INDICATORS_SCOPE.md | 38 + docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md | 58 + docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md | 58 + docs/SGF_FISCAL_COVERAGE_DELIVERY.md | 30 + docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md | 48 + docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md | 33 + docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md | 52 + docs/SGF_FISCAL_RULES_COMMON_SCOPE.md | 33 + docs/SGF_GUIAO_APRESENTACAO.md | 74 ++ docs/SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md | 31 + docs/SGF_HUMAN_RESOURCES_DELIVERY.md | 34 + docs/SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md | 61 + docs/SGF_HUMAN_RESOURCES_PREPARATION.md | 30 + docs/SGF_IVA_ASSESSMENT_DELIVERY.md | 46 + docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md | 34 + docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md | 34 + docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md | 33 + docs/SGF_NEXT_BLOCK_RIGHTS.md | 49 + docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md | 40 + docs/SGF_PAYABLE_AGING_DELIVERY.md | 22 + docs/SGF_PAYABLE_DUE_DELIVERY.md | 39 + docs/SGF_PAYABLE_DUE_SCOPE.md | 28 + docs/SGF_PAYABLE_POSITIONS_DELIVERY.md | 24 + docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md | 22 + docs/SGF_REMUNERATIONS_SCOPE.md | 41 + docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md | 50 + docs/SGF_SINGLE_INTEGRATION_20261110.md | 49 + docs/SGF_SIX_MODULE_CLOSURE_20261110.md | 62 + docs/SGF_SOLUTION_DELIVERY.md | 46 + docs/SGF_STOCK_CATALOGUE_DELIVERY.md | 48 + docs/SGF_STOCK_CATALOGUE_SCOPE.md | 30 + docs/SGF_STOCK_FINAL_SCOPE.md | 53 + docs/SGF_STOCK_INACTIVITY_DELIVERY.md | 29 + docs/SGF_STOCK_OPERATIONS_DELIVERY.md | 41 + docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md | 23 + docs/SGF_USO_LOCAL.md | 29 + docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md | 15 + .../SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md | 16 + docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md | 18 + .../integration/sgf-8085-access-proposal.json | 761 +++++++++++ ...f-bank-reconciliation-access-proposal.json | 89 ++ .../integration/sgf-cash-access-proposal.json | 84 ++ .../sgf-document-read-access-proposal.json | 82 ++ ...-financial-indicators-access-proposal.json | 81 ++ .../sgf-fiscal-expression-port.json | 12 + ...gf-fiscal-obligations-access-proposal.json | 65 + .../sgf-fiscal-rules-access-proposal.json | 81 ++ .../sgf-human-resources-access-proposal.json | 102 ++ ...n-resources-personnel-access-proposal.json | 114 ++ .../sgf-iva-assessment-access-proposal.json | 132 ++ .../sgf-knowledge-inventory-20261110.json | 103 ++ .../sgf-multi-line-included-proposal.json | 31 + .../sgf-payable-due-access-proposal.json | 44 + ...f-remaining-functions-access-proposal.json | 169 +++ .../sgf-remunerations-access-proposal.json | 69 + docs/integration/sgf-solution-contract.json | 600 +++++++++ .../sgf-statements-access-proposal.json | 45 + .../sgf-stock-catalogue-access-proposal.json | 41 + .../sgf-stock-final-access-proposal.json | 25 + ...8085-integration-preparation-20261110.json | 104 ++ .../proofs/sgc-invoice-endpoint-20261110.json | 237 ++++ .../sgc-invoice-workspace-20261110.json | 225 ++++ .../sgc-invoice-workspace-final-20261110.json | 234 ++++ docs/proofs/sgc-preparation-20261110.json | 75 ++ .../sgf-accounting-reports-20261110.json | 63 + .../sgf-bank-reconciliation-20261110.json | 138 ++ .../proofs/sgf-billing-register-20261110.json | 41 + docs/proofs/sgf-cash-20261110.json | 70 + .../sgf-composition-footprint-20261110.json | 380 ++++++ .../sgf-financial-indicators-20261110.json | 128 ++ docs/proofs/sgf-fiscal-coverage-20261110.json | 81 ++ .../sgf-fiscal-obligations-20261110.json | 58 + docs/proofs/sgf-fiscal-rules-20261110.json | 137 ++ docs/proofs/sgf-human-resources-20261111.json | 78 ++ ...ources-personnel-preparation-20261111.json | 28 + ...-human-resources-preparation-20261111.json | 53 + docs/proofs/sgf-iva-assessment-20261110.json | 116 ++ .../sgf-multi-line-included-20261110.json | 97 ++ .../sgf-next-block-progress-20261110.json | 126 ++ .../sgf-operational-information-20261110.json | 122 ++ docs/proofs/sgf-payable-aging-20261110.json | 27 + docs/proofs/sgf-payable-due-20261110.json | 49 + .../sgf-payable-positions-20261110.json | 29 + .../sgf-receivable-positions-20261110.json | 75 ++ .../sgf-single-integration-20261110.json | 109 ++ ...ingle-integration-pagination-20261110.json | 28 + .../sgf-six-module-composition-20261110.json | 249 ++++ ...six-module-footprint-current-20261110.json | 374 ++++++ ...f-six-module-footprint-final-20261110.json | 450 +++++++ ...six-module-functional-review-20261110.json | 254 ++++ ...gf-solution-closure-baseline-20261110.json | 378 ++++++ ...ution-closure-baseline-final-20261110.json | 376 ++++++ .../sgf-solution-structure-v10-20261110.json | 690 ++++++++++ .../sgf-solution-structure-v11-20261110.json | 717 ++++++++++ .../sgf-solution-structure-v12-20261110.json | 729 ++++++++++ .../sgf-solution-structure-v13-20261110.json | 847 ++++++++++++ .../sgf-solution-structure-v14-20261110.json | 869 ++++++++++++ .../sgf-solution-structure-v15-20261110.json | 896 +++++++++++++ .../sgf-solution-structure-v16-20261110.json | 928 +++++++++++++ .../sgf-solution-structure-v17-20261110.json | 955 ++++++++++++++ .../sgf-solution-structure-v18-20261110.json | 982 ++++++++++++++ .../sgf-solution-structure-v19-20261110.json | 994 ++++++++++++++ .../sgf-solution-structure-v20-20261110.json | 1031 +++++++++++++++ .../sgf-solution-structure-v21-20261110.json | 1043 +++++++++++++++ .../sgf-solution-structure-v22-20261111.json | 1090 +++++++++++++++ .../sgf-solution-structure-v23-20261111.json | 1107 ++++++++++++++++ .../sgf-solution-structure-v24-20261111.json | 1107 ++++++++++++++++ .../sgf-solution-structure-v25-20261111.json | 1169 +++++++++++++++++ ...solution-structure-v25-final-20261111.json | 1169 +++++++++++++++++ .../sgf-solution-structure-v6-20261110.json | 542 ++++++++ .../sgf-solution-structure-v7-20261110.json | 613 +++++++++ .../sgf-solution-structure-v8-20261110.json | 635 +++++++++ .../sgf-solution-structure-v9-20261110.json | 662 ++++++++++ docs/proofs/sgf-statements-20261111.json | 75 ++ docs/proofs/sgf-stock-catalogue-20261110.json | 173 +++ .../proofs/sgf-stock-inactivity-20261110.json | 72 + .../proofs/sgf-stock-operations-20261110.json | 97 ++ ...gf-workshop-equipment-orders-20261111.json | 39 + docs/proofs/sgf-workshop-period-20261110.json | 27 + packages/general-ledger/bootstrap.php | 1 + .../src/CostCorrectionSchema.php | 2 +- .../src/IndustrialCorrectionSchema.php | 2 +- packages/general-ledger/src/LedgerCycle.php | 20 +- packages/general-ledger/src/LedgerSales.php | 4 +- packages/general-ledger/src/LedgerSchema.php | 8 + .../general-ledger/src/PayableJournal.php | 26 +- packages/general-ledger/src/SalesSchema.php | 2 +- .../src/StockSupplierReturnEvidence.php | 31 + .../src/StockTransitEvidence.php | 44 + .../general-ledger/src/StockValuation.php | 54 +- .../releases/0.1.0/bootstrap.php | 2 + .../releases/0.1.0/manifest.json | 8 + .../0.1.0/src/IssueMovementWriter.php | 40 + .../releases/0.1.0/bootstrap.php | 2 + .../releases/0.1.0/manifest.json | 8 + .../0.1.0/src/ReceiptMovementWriter.php | 74 ++ .../sgc-invoice-client/src/InvoiceClient.php | 130 ++ public/sgf-bank.mjs | 18 + public/sgf-cash.mjs | 15 + public/sgf-composed-ledger.mjs | 15 + public/sgf-entry-menu.mjs | 3 + public/sgf-finance-payables.mjs | 13 + public/sgf-fiscal-rules.mjs | 25 + public/sgf-indicators.mjs | 20 + public/sgf-iva.mjs | 17 + public/sgf-service-documents.mjs | 18 + public/sgf-stock-catalogue.mjs | 15 + public/sgf-stock-completion.mjs | 22 + public/sgf-stock-final.mjs | 44 + public/stock-operations-workspace.mjs | 22 + resources/css/tempo-theme.css | 12 + resources/data/bank-statement-qa.csv | 3 + resources/data/fiscal-rule-qa.json | 90 ++ resources/js/AccountingMenuWorkspace.jsx | 5 +- resources/js/BillingMenuWorkspace.jsx | 6 +- resources/js/BillingRegisterWorkspace.jsx | 14 + resources/js/CommercialQuotesWorkspace.jsx | 23 + resources/js/ComposedAccountingWorkspace.jsx | 16 + resources/js/FiscalCoverage.jsx | 9 + resources/js/FiscalObligationsWorkspace.jsx | 29 + resources/js/InternalStatementsWorkspace.jsx | 31 + resources/js/LedgerReportsWorkspace.jsx | 17 + resources/js/ModuleNavigation.jsx | 22 +- resources/js/PayableDueWorkspace.jsx | 23 + resources/js/PayablePositionsWorkspace.jsx | 16 + resources/js/PersonnelWorkspace.jsx | 32 + resources/js/ReceivablePositionsWorkspace.jsx | 12 + resources/js/RemunerationsWorkspace.jsx | 35 + resources/js/ServiceFiscalWorkspace.jsx | 10 +- resources/js/ServiceInvoiceWorkspace.jsx | 2 +- resources/js/ServiceReceivablesWorkspace.jsx | 11 +- resources/js/StockInactiveReportWorkspace.jsx | 16 + resources/js/StockMenuWorkspace.jsx | 25 +- resources/js/WorkshopBillingWorkspace.jsx | 9 +- resources/js/WorkshopEquipmentWorkspace.jsx | 24 + resources/js/WorkshopMenuWorkspace.jsx | 20 +- resources/js/WorkshopOrderList.jsx | 12 + resources/js/WorkshopScheduleWorkspace.jsx | 32 + resources/js/WorkshopStockWorkspace.jsx | 29 +- resources/js/WorkshopWorkspace.jsx | 6 +- resources/js/WorkspaceHome.jsx | 27 +- resources/js/WorkspaceNavigation.jsx | 10 +- resources/js/app.jsx | 64 +- resources/js/context-transition.js | 2 +- resources/js/fiscal-obligations-view.js | 4 + resources/js/internal-statements-view.js | 3 + resources/js/ledger-report-view.js | 27 + resources/js/payable-position-view.js | 17 + resources/js/presentation-landing.js | 5 + resources/js/receivable-position-view.js | 11 + resources/js/statement-download.js | 16 + resources/js/workshop-report-view.js | 12 + resources/js/workshop-stock-operation.js | 16 + resources/js/workspace-overview.js | 46 + .../accounting/composed-ledger.blade.php | 17 + .../views/commercial/documents.blade.php | 4 + .../views/commercial/quote-print.blade.php | 1 + resources/views/finance/bank.blade.php | 4 + resources/views/finance/cash.blade.php | 5 + .../views/finance/composed-payables.blade.php | 16 + resources/views/finance/indicators.blade.php | 4 + resources/views/fiscal/rules.blade.php | 5 + resources/views/fiscal/workspace.blade.php | 7 + .../views/sgf-presentation-entry.blade.php | 21 + .../views/stock-catalogue-workspace.blade.php | 6 + .../stock-completion-workspace.blade.php | 6 + .../views/stock-final-workspace.blade.php | 8 + .../stock-operations-workspace.blade.php | 17 + routes/api.php | 37 + routes/sgc.php | 11 + routes/web.php | 108 +- scripts/abrir-sgf.ps1 | 7 + scripts/check-sgf-solution.mjs | 56 + scripts/launch-sgf.mjs | 14 + scripts/prepare-sgc-8085.mjs | 65 + scripts/receipt-composition-http.php | 8 +- scripts/sgc-dispatch-pg-bootstrap.php | 38 + scripts/sgc-dispatch-pg-proof.php | 53 + scripts/sgc-dispatch-receiver.php | 9 + scripts/sgc-invoice-authority-proof.php | 22 + scripts/sgc-invoice-check.mjs | 23 + scripts/sgc-invoice-client-kernel-proof.php | 33 + scripts/sgc-invoice-context-proof.php | 21 + scripts/sgc-invoice-proof-bootstrap.php | 38 + scripts/sgc-invoice-proof.php | 135 ++ scripts/sgc-invoice-security-proof.php | 45 + scripts/sgf-accounting-continuity-http.mjs | 10 + scripts/sgf-accounting-report-boundaries.php | 5 + scripts/sgf-accounting-reports-http.mjs | 4 + scripts/sgf-accounting-reports-ui.mjs | 3 + scripts/sgf-analysis-access-readonly.php | 2 + scripts/sgf-analysis-backup.php | 4 + scripts/sgf-analysis-boundaries.php | 7 + scripts/sgf-analysis-concurrency.mjs | 4 + scripts/sgf-analysis-concurrency.php | 7 + scripts/sgf-analysis-dom-http.mjs | 5 + scripts/sgf-analysis-http.mjs | 9 + scripts/sgf-analysis-preservation.php | 4 + scripts/sgf-analysis-proof-bootstrap.php | 13 + scripts/sgf-analysis-proof.php | 12 + scripts/sgf-analysis-setup.php | 4 + scripts/sgf-analysis-sources-readonly.php | 2 + scripts/sgf-assets-http.mjs | 2 + scripts/sgf-assets-routing-proof.php | 4 + scripts/sgf-authorized-profiles.php | 10 + scripts/sgf-bank-access-readonly.php | 2 + scripts/sgf-bank-backup.php | 4 + scripts/sgf-bank-boundaries.php | 8 + scripts/sgf-bank-concurrency.mjs | 4 + scripts/sgf-bank-concurrency.php | 7 + scripts/sgf-bank-dom-http.mjs | 5 + scripts/sgf-bank-guard-repair.php | 2 + scripts/sgf-bank-http.mjs | 7 + scripts/sgf-bank-origin-readonly.php | 2 + scripts/sgf-bank-preservation.php | 4 + scripts/sgf-bank-proof-bootstrap.php | 13 + scripts/sgf-bank-proof.php | 14 + scripts/sgf-bank-report-http.mjs | 2 + scripts/sgf-bank-setup.php | 4 + scripts/sgf-billing-register-http.mjs | 7 + scripts/sgf-billing-register-proof.php | 20 + scripts/sgf-cash-access.php | 3 + scripts/sgf-cash-backup.php | 2 + scripts/sgf-cash-concurrency.mjs | 2 + scripts/sgf-cash-concurrency.php | 12 + scripts/sgf-cash-dom-http.mjs | 6 + scripts/sgf-cash-http.mjs | 12 + scripts/sgf-cash-period-proof.php | 19 + scripts/sgf-cash-preservation.php | 6 + scripts/sgf-cash-proof-bootstrap.php | 9 + scripts/sgf-cash-proof.php | 25 + scripts/sgf-cash-read-boundaries.php | 8 + scripts/sgf-cash-rights-readonly.php | 2 + scripts/sgf-cash-rule-policy.php | 2 + scripts/sgf-cash-setup.php | 6 + scripts/sgf-commercial-quotes-http.mjs | 10 + scripts/sgf-commercial-quotes-proof.php | 20 + scripts/sgf-commercial-quotes-schema.php | 2 + scripts/sgf-commercial-workflow.php | 29 + scripts/sgf-compose-install.php | 60 + scripts/sgf-composed-direct-cost-proof.php | 31 + scripts/sgf-composed-industrial-proof.php | 28 + scripts/sgf-composed-ledger-proof.php | 26 + scripts/sgf-composed-payments-proof.php | 29 + scripts/sgf-composed-period-proof.php | 18 + scripts/sgf-composed-sales-ledger-proof.php | 23 + scripts/sgf-composition-bootstrap.php | 13 + scripts/sgf-composition-flow-proof.php | 46 + scripts/sgf-composition-http-check.mjs | 25 + scripts/sgf-composition-proof.php | 33 + scripts/sgf-document-cancellation-proof.php | 25 + scripts/sgf-document-cancellation-schema.php | 16 + scripts/sgf-document-closed-period-check.php | 4 + scripts/sgf-document-inputs.php | 15 + scripts/sgf-document-ledger-setup.php | 2 + scripts/sgf-document-math-backup.php | 4 + scripts/sgf-document-math-concurrency.mjs | 4 + scripts/sgf-document-math-concurrency.php | 7 + scripts/sgf-document-math-dom-http.mjs | 3 + scripts/sgf-document-math-http.mjs | 6 + scripts/sgf-document-math-preservation.php | 4 + scripts/sgf-document-math-proof-bootstrap.php | 4 + scripts/sgf-document-math-proof.php | 13 + scripts/sgf-document-math-recovery.php | 3 + scripts/sgf-document-period-proof.php | 18 + scripts/sgf-document-read-access.php | 4 + scripts/sgf-document-read-rights.php | 5 + scripts/sgf-documents-concurrency.mjs | 2 + scripts/sgf-documents-concurrency.php | 11 + scripts/sgf-equipment-orders-preservation.php | 5 + scripts/sgf-finance-ledger-client-check.mjs | 13 + scripts/sgf-finance-lock-proof.php | 12 + scripts/sgf-fiscal-context-ui.mjs | 8 + scripts/sgf-fiscal-coverage-http.mjs | 4 + scripts/sgf-fiscal-coverage-preservation.php | 3 + scripts/sgf-fiscal-coverage-proof.php | 12 + scripts/sgf-fiscal-coverage-ui.mjs | 4 + scripts/sgf-fiscal-entry-http.mjs | 3 + scripts/sgf-fiscal-expression-parity.php | 7 + scripts/sgf-fiscal-map-download-proof.mjs | 3 + scripts/sgf-fiscal-next-access-check.php | 4 + ...sgf-fiscal-obligations-access-readonly.php | 7 + scripts/sgf-fiscal-obligations-backup.php | 4 + scripts/sgf-fiscal-obligations-boundaries.php | 14 + .../sgf-fiscal-obligations-concurrency.mjs | 2 + .../sgf-fiscal-obligations-concurrency.php | 8 + scripts/sgf-fiscal-obligations-entry-http.mjs | 5 + scripts/sgf-fiscal-obligations-http.mjs | 9 + .../sgf-fiscal-obligations-preservation.php | 7 + ...sgf-fiscal-obligations-proof-bootstrap.php | 11 + scripts/sgf-fiscal-obligations-proof.php | 14 + ...f-fiscal-obligations-regression-source.php | 2 + scripts/sgf-fiscal-obligations-sample.php | 2 + scripts/sgf-fiscal-obligations-setup.php | 3 + scripts/sgf-fiscal-obligations-ui.mjs | 10 + scripts/sgf-fiscal-rules-access-readonly.php | 2 + scripts/sgf-fiscal-rules-backup.php | 4 + scripts/sgf-fiscal-rules-boundaries.php | 10 + scripts/sgf-fiscal-rules-concurrency.mjs | 4 + scripts/sgf-fiscal-rules-concurrency.php | 7 + scripts/sgf-fiscal-rules-dom-http.mjs | 8 + scripts/sgf-fiscal-rules-http.mjs | 9 + scripts/sgf-fiscal-rules-input-proof.php | 2 + scripts/sgf-fiscal-rules-preservation.php | 4 + scripts/sgf-fiscal-rules-proof-bootstrap.php | 11 + scripts/sgf-fiscal-rules-proof.php | 18 + scripts/sgf-fiscal-rules-setup.php | 3 + scripts/sgf-guided-menu-http.mjs | 12 + scripts/sgf-hr-personnel-backup.php | 6 + scripts/sgf-hr-personnel-boundaries.php | 16 + scripts/sgf-hr-personnel-concurrency.mjs | 2 + scripts/sgf-hr-personnel-concurrency.php | 7 + scripts/sgf-hr-personnel-fixture.php | 2 + scripts/sgf-hr-personnel-proof-bootstrap.php | 6 + scripts/sgf-hr-personnel-proof.php | 13 + scripts/sgf-hr-personnel-setup.php | 4 + scripts/sgf-human-resources-boundaries.php | 21 + scripts/sgf-human-resources-concurrency.mjs | 2 + scripts/sgf-human-resources-concurrency.php | 10 + scripts/sgf-human-resources-final-entry.mjs | 1 + scripts/sgf-human-resources-finalize.php | 2 + scripts/sgf-human-resources-http.mjs | 12 + ...an-resources-personnel-access-readonly.php | 3 + ...f-human-resources-personnel-time-proof.php | 5 + .../sgf-human-resources-preauthorization.php | 4 + ...man-resources-preparation-preservation.php | 5 + scripts/sgf-human-resources-prepared-ui.mjs | 12 + scripts/sgf-human-resources-preservation.php | 6 + scripts/sgf-human-resources-setup.php | 6 + scripts/sgf-included-policy-preview.php | 6 + scripts/sgf-industrial-workflow.php | 25 + scripts/sgf-install-accounting-continuity.php | 6 + scripts/sgf-install-accounting-reports.php | 9 + scripts/sgf-install-analysis.php | 3 + scripts/sgf-install-bank.php | 3 + scripts/sgf-install-billing-register.php | 3 + scripts/sgf-install-cash.php | 3 + scripts/sgf-install-commercial-quotes.php | 3 + scripts/sgf-install-document-math.php | 3 + scripts/sgf-install-equipment-orders.php | 8 + scripts/sgf-install-fiscal-coverage.php | 7 + scripts/sgf-install-fiscal-obligations.php | 9 + scripts/sgf-install-fiscal-rules.php | 4 + scripts/sgf-install-human-resources.php | 11 + scripts/sgf-install-iva.php | 4 + scripts/sgf-install-overview.php | 10 + scripts/sgf-install-payable-aging.php | 8 + scripts/sgf-install-payable-due.php | 10 + scripts/sgf-install-payable-positions.php | 9 + scripts/sgf-install-receivable-positions.php | 7 + scripts/sgf-install-service-documents.php | 6 + scripts/sgf-install-single-workspace.php | 24 + scripts/sgf-install-statements.php | 9 + scripts/sgf-install-stock-catalogue.php | 6 + scripts/sgf-install-stock-completion.php | 9 + scripts/sgf-install-stock-final.php | 11 + scripts/sgf-install-stock-inactive.php | 9 + scripts/sgf-install-stock-prepared.mjs | 22 + scripts/sgf-install-workshop-commercial.php | 6 + scripts/sgf-install-workshop-period.php | 8 + scripts/sgf-install-workshop-schedule.php | 6 + scripts/sgf-install-workshop-stock.php | 10 + scripts/sgf-integration-access.php | 16 + scripts/sgf-iva-backup.php | 4 + scripts/sgf-iva-boundaries.php | 9 + scripts/sgf-iva-concurrency.mjs | 4 + scripts/sgf-iva-concurrency.php | 12 + scripts/sgf-iva-dom-http.mjs | 8 + scripts/sgf-iva-http.mjs | 6 + scripts/sgf-iva-preservation.php | 4 + scripts/sgf-iva-proof-bootstrap.php | 10 + scripts/sgf-iva-proof.php | 23 + scripts/sgf-iva-setup.php | 5 + scripts/sgf-ledger-candidate-pages-proof.php | 12 + scripts/sgf-ledger-proof-bootstrap.php | 17 + scripts/sgf-material-isolation-proof.php | 14 + scripts/sgf-material-perimeter.php | 9 + scripts/sgf-module-dropdown-check.mjs | 4 + scripts/sgf-native-bindings.php | 12 + scripts/sgf-native-ledger-setup.php | 54 + scripts/sgf-next-block-backup.php | 2 + scripts/sgf-next-block-preservation.php | 6 + scripts/sgf-overview-entry-http.mjs | 3 + scripts/sgf-overview-final-http.mjs | 2 + scripts/sgf-overview-http.mjs | 7 + scripts/sgf-overview-ui.mjs | 7 + scripts/sgf-payable-aging-http.mjs | 3 + scripts/sgf-payable-aging-preservation.php | 5 + scripts/sgf-payable-aging-proof.php | 7 + scripts/sgf-payable-aging-ui.mjs | 4 + scripts/sgf-payable-due-access-readonly.php | 7 + scripts/sgf-payable-due-backup.php | 4 + scripts/sgf-payable-due-concurrency.mjs | 2 + scripts/sgf-payable-due-concurrency.php | 7 + scripts/sgf-payable-due-final-http.mjs | 7 + scripts/sgf-payable-due-http.mjs | 16 + scripts/sgf-payable-due-identity-proof.php | 12 + scripts/sgf-payable-due-list-proof.php | 6 + scripts/sgf-payable-due-pagination-proof.php | 6 + scripts/sgf-payable-due-preservation.php | 7 + scripts/sgf-payable-due-proof-bootstrap.php | 7 + scripts/sgf-payable-due-proof.php | 26 + scripts/sgf-payable-due-setup.php | 6 + scripts/sgf-payable-due-ui.mjs | 14 + scripts/sgf-payable-positions-http.mjs | 25 + .../sgf-payable-positions-preservation.php | 5 + scripts/sgf-payable-positions-proof.php | 6 + scripts/sgf-payable-positions-ui.mjs | 3 + scripts/sgf-presentation-controller-proof.php | 11 + scripts/sgf-profiles-check.php | 18 + scripts/sgf-quotes-concurrency.mjs | 2 + scripts/sgf-quotes-concurrency.php | 12 + scripts/sgf-receivable-positions-http.mjs | 3 + scripts/sgf-receivable-positions-proof.php | 15 + scripts/sgf-receivable-positions-ui.mjs | 5 + scripts/sgf-receivables-workflow.php | 31 + scripts/sgf-remaining-access.php | 7 + scripts/sgf-remaining-rights-readonly.php | 4 + scripts/sgf-remunerations-access-readonly.php | 6 + scripts/sgf-remunerations-backup.php | 5 + scripts/sgf-remunerations-proof-bootstrap.php | 16 + scripts/sgf-remunerations-proof.php | 17 + scripts/sgf-remunerations-retract-finance.php | 5 + scripts/sgf-remunerations-setup.php | 3 + scripts/sgf-repair-new-industrial-fixture.php | 11 + scripts/sgf-restart-proof.mjs | 10 + scripts/sgf-runtime-snapshot.php | 4 + scripts/sgf-selected-assets.php | 9 + scripts/sgf-service-documents-dom-http.mjs | 7 + scripts/sgf-service-documents-http.mjs | 17 + .../sgf-service-documents-proof-bootstrap.php | 7 + scripts/sgf-service-documents-proof.php | 39 + scripts/sgf-service-documents-schema.php | 4 + scripts/sgf-service-documents-status.php | 5 + scripts/sgf-single-bootstrap.php | 19 + scripts/sgf-single-boundaries-http.mjs | 13 + scripts/sgf-single-corrections-http.mjs | 19 + scripts/sgf-single-financial-http.mjs | 18 + scripts/sgf-single-http.mjs | 8 + scripts/sgf-single-read-http.mjs | 11 + scripts/sgf-single-stock-final-http.mjs | 20 + scripts/sgf-single-stock-http.mjs | 16 + .../sgf-single-workshop-commercial-http.mjs | 12 + scripts/sgf-statements-access-readonly.php | 7 + scripts/sgf-statements-backup.php | 4 + scripts/sgf-statements-boundaries.php | 15 + scripts/sgf-statements-concurrency.mjs | 2 + scripts/sgf-statements-concurrency.php | 7 + scripts/sgf-statements-download-proof.mjs | 6 + scripts/sgf-statements-final-http.mjs | 6 + scripts/sgf-statements-http.mjs | 9 + scripts/sgf-statements-preservation.php | 7 + scripts/sgf-statements-proof-bootstrap.php | 8 + scripts/sgf-statements-proof.php | 13 + scripts/sgf-statements-setup.php | 6 + scripts/sgf-statements-snapshot-failure.php | 6 + scripts/sgf-statements-source-dates.php | 2 + scripts/sgf-statements-source-readonly.mjs | 1 + scripts/sgf-statements-ui-sample.php | 2 + scripts/sgf-statements-ui.mjs | 7 + .../sgf-stock-catalogue-access-readonly.php | 2 + scripts/sgf-stock-catalogue-assets.mjs | 2 + scripts/sgf-stock-catalogue-backup.php | 4 + scripts/sgf-stock-catalogue-boundaries.php | 4 + scripts/sgf-stock-catalogue-concurrency.mjs | 2 + scripts/sgf-stock-catalogue-concurrency.php | 6 + scripts/sgf-stock-catalogue-dom-http.mjs | 2 + scripts/sgf-stock-catalogue-http.mjs | 6 + scripts/sgf-stock-catalogue-metadata.php | 2 + scripts/sgf-stock-catalogue-navigation.mjs | 2 + scripts/sgf-stock-catalogue-preservation.php | 9 + .../sgf-stock-catalogue-proof-bootstrap.php | 7 + scripts/sgf-stock-catalogue-proof.php | 15 + scripts/sgf-stock-catalogue-setup.php | 6 + scripts/sgf-stock-completion-binding.php | 8 + scripts/sgf-stock-completion-concurrency.mjs | 2 + scripts/sgf-stock-completion-concurrency.php | 18 + scripts/sgf-stock-completion-dom-http.mjs | 8 + scripts/sgf-stock-completion-http.mjs | 14 + scripts/sgf-stock-completion-ledger-setup.php | 19 + .../sgf-stock-completion-proof-bootstrap.php | 11 + scripts/sgf-stock-completion-proof.php | 55 + scripts/sgf-stock-disposition-input.php | 23 + scripts/sgf-stock-final-backup.php | 5 + scripts/sgf-stock-final-boundaries-http.mjs | 9 + scripts/sgf-stock-final-client-check.mjs | 5 + scripts/sgf-stock-final-concurrency.mjs | 8 + scripts/sgf-stock-final-concurrency.php | 22 + scripts/sgf-stock-final-dom-http.mjs | 7 + scripts/sgf-stock-final-install.php | 52 + scripts/sgf-stock-final-ledger-setup.php | 19 + scripts/sgf-stock-final-preservation.php | 16 + scripts/sgf-stock-final-proof-bootstrap.php | 10 + scripts/sgf-stock-final-proof.php | 52 + scripts/sgf-stock-final-recovery-proof.php | 15 + scripts/sgf-stock-inactive-http.mjs | 8 + scripts/sgf-stock-inactive-preservation.php | 4 + scripts/sgf-stock-inactive-proof.php | 49 + scripts/sgf-stock-inactive-ui.mjs | 9 + scripts/sgf-stock-lot-input.php | 24 + scripts/sgf-stock-physical-input.php | 25 + scripts/sgf-stock-rate-controller-proof.php | 27 + scripts/sgf-stock-remaining-rights-check.php | 6 + scripts/sgf-stock-transit-ledger-proof.php | 15 + scripts/sgf-workshop-commercial-proof.php | 17 + scripts/sgf-workshop-direct-workflow.php | 36 + scripts/sgf-workshop-equipment-http.mjs | 6 + .../sgf-workshop-equipment-orders-http.mjs | 5 + .../sgf-workshop-equipment-orders-proof.php | 12 + scripts/sgf-workshop-equipment-orders-ui.mjs | 8 + scripts/sgf-workshop-equipment-proof.php | 17 + scripts/sgf-workshop-equipment-schema.php | 2 + scripts/sgf-workshop-orders-http.mjs | 2 + scripts/sgf-workshop-orders-proof.php | 24 + scripts/sgf-workshop-orders-ui.mjs | 2 + scripts/sgf-workshop-period-http.mjs | 3 + scripts/sgf-workshop-period-preservation.php | 5 + scripts/sgf-workshop-period-proof.php | 13 + scripts/sgf-workshop-period-ui.mjs | 4 + scripts/sgf-workshop-read-lock.mjs | 2 + scripts/sgf-workshop-read-lock.php | 4 + scripts/sgf-workshop-schedule-concurrency.mjs | 2 + scripts/sgf-workshop-schedule-concurrency.php | 12 + scripts/sgf-workshop-schedule-http.mjs | 8 + scripts/sgf-workshop-schedule-proof.php | 27 + scripts/sgf-workshop-schedule-schema.php | 6 + scripts/sgf-workshop-stock-bindings.php | 9 + scripts/sgf-workshop-stock-bridge-proof.php | 34 + scripts/sgf-workshop-stock-client-check.mjs | 9 + scripts/sgf-workshop-stock-input.php | 11 + scripts/sgf-workshop-stock-schema.php | 14 + scripts/stock-operations-client-check.mjs | 53 + scripts/stock-operations-proof.php | 115 ++ scripts/stop-selected-sgf-server.ps1 | 15 + scripts/summarize-sgc-dispatch.php | 35 + scripts/summarize-sgc-invoice.php | 33 + scripts/summarize-sgf-single-integration.mjs | 8 + scripts/summarize-sgf-six-modules.mjs | 11 + tests/Unit/CalendarDimensionTest.php | 56 + tests/Unit/SgcInvoiceClientTest.php | 133 ++ tests/payloads/invoice_test.json | 22 + 736 files changed, 40746 insertions(+), 210 deletions(-) create mode 100644 app/Console/Commands/PopulateDimTempo.php create mode 100644 app/Http/Controllers/Api/V1/SgcIntegrationController.php create mode 100644 app/Http/Controllers/Api/V1/SgcInvoiceIngressController.php create mode 100644 app/Http/Middleware/AuthenticateSgcInvoice.php create mode 100644 app/Http/Requests/Sgc/StoreInvoiceRequest.php create mode 100644 app/Models/Sgc/Fatura.php create mode 100644 app/Models/Sgc/OrdemCompra.php create mode 100644 app/Modules/Accounting/CashLedgerSource.php create mode 100644 app/Modules/Accounting/ComposedLedger.php create mode 100644 app/Modules/Accounting/ComposedLedgerCandidates.php create mode 100644 app/Modules/Accounting/ComposedLedgerController.php create mode 100644 app/Modules/Accounting/ComposedMaterialIsolation.php create mode 100644 app/Modules/Accounting/InternalStatementTotals.php create mode 100644 app/Modules/Accounting/InternalStatementsController.php create mode 100644 app/Modules/Accounting/SourceBook.php create mode 100644 app/Modules/Finance/BankLedgerSource.php create mode 100644 app/Modules/Finance/BankReconciliation.php create mode 100644 app/Modules/Finance/CashWorkspace.php create mode 100644 app/Modules/Finance/FinancialIndicatorSource.php create mode 100644 app/Modules/Finance/FinancialIndicators.php create mode 100644 app/Modules/Finance/Http/BankReconciliationController.php create mode 100644 app/Modules/Finance/Http/CashController.php create mode 100644 app/Modules/Finance/Http/FinancialIndicatorsController.php create mode 100644 app/Modules/HumanResources/Http/PersonnelController.php create mode 100644 app/Modules/HumanResources/Http/RemunerationsController.php create mode 100644 app/Modules/HumanResources/Personnel.php create mode 100644 app/Modules/HumanResources/PersonnelTime.php create mode 100644 app/Modules/HumanResources/Remunerations.php create mode 100644 app/Modules/Inventory/StockCatalogueController.php create mode 100644 app/Modules/Inventory/StockCompletionController.php create mode 100644 app/Modules/Inventory/StockFinalController.php create mode 100644 app/Modules/Inventory/StockInactiveReport.php create mode 100644 app/Modules/Inventory/StockOperationsController.php create mode 100644 app/Modules/Purchasing/ComposedFinance.php create mode 100644 app/Modules/Purchasing/ComposedFinanceController.php create mode 100644 app/Modules/Purchasing/PayableAge.php create mode 100644 app/Modules/Purchasing/PayableDueController.php create mode 100644 app/Modules/Purchasing/PayablePositionReadController.php create mode 100644 app/Modules/Purchasing/SgcInvoiceIngress.php create mode 100644 app/Modules/Services/BillingRegister.php create mode 100644 app/Modules/Services/CommercialQuotes.php create mode 100644 app/Modules/Services/DocumentPeriod.php create mode 100644 app/Modules/Services/FiscalAssessment.php create mode 100644 app/Modules/Services/FiscalCoverage.php create mode 100644 app/Modules/Services/FiscalDocumentMath.php create mode 100644 app/Modules/Services/FiscalObligations.php create mode 100644 app/Modules/Services/FiscalRuleExpression.php create mode 100644 app/Modules/Services/FiscalRules.php create mode 100644 app/Modules/Services/Http/CommercialQuotesController.php create mode 100644 app/Modules/Services/Http/FiscalAssessmentController.php create mode 100644 app/Modules/Services/Http/FiscalObligationsController.php create mode 100644 app/Modules/Services/Http/FiscalRulesController.php create mode 100644 app/Modules/Services/Http/ReceivablePositionReadController.php create mode 100644 app/Modules/Services/Http/ServiceDocumentsController.php create mode 100644 app/Modules/Services/ReceivablePositionRead.php create mode 100644 app/Modules/Services/ServiceDocuments.php create mode 100644 app/Modules/Workshop/ComposedWorkshopStockBook.php create mode 100644 app/Modules/Workshop/Http/WorkshopEquipmentController.php create mode 100644 app/Modules/Workshop/Http/WorkshopOrderReadController.php create mode 100644 app/Modules/Workshop/Http/WorkshopScheduleController.php create mode 100644 app/Modules/Workshop/WorkshopEquipment.php create mode 100644 app/Modules/Workshop/WorkshopOrderRead.php create mode 100644 app/Modules/Workshop/WorkshopSchedule.php create mode 100644 app/Requests/Sgc/StoreInvoiceRequest.php create mode 100644 app/Requests/Sgc/StorePaymentRequest.php create mode 100644 app/Services/Sgc/BudgetCommitmentService.php create mode 100644 app/Services/Sgc/CalendarDimension.php create mode 100644 app/Services/Sgc/IntegrationService.php create mode 100644 app/Services/Sgc/ThreeWayMatchService.php create mode 100644 app/Shared/Installation/CompositionRuntime.php create mode 100644 app/Shared/Installation/CompositionSchema.php create mode 100644 app/Shared/Installation/CompositionSchemaBuilder.php create mode 100644 app/Shared/Installation/CompositionSchemaConnection.php create mode 100644 app/Shared/Installation/HumanResourcesProfiles.php create mode 100644 app/Shared/Installation/PresentationEntry.php create mode 100644 app/Shared/Installation/SyntheticProfiles.php create mode 100644 config/analysis_qa.php create mode 100644 config/bank_qa.php create mode 100644 config/cash_qa.php create mode 100644 config/commercial_quotes.php create mode 100644 config/fiscal_obligations.php create mode 100644 config/fiscal_qa.php create mode 100644 config/fiscal_rules_qa.php create mode 100644 config/hr_personnel.php create mode 100644 config/human_resources.php create mode 100644 config/internal_statements.php create mode 100644 config/payable_due.php create mode 100644 config/remunerations.php create mode 100644 config/service_documents.php create mode 100644 config/sgc_invoice.php create mode 100644 config/sgf_remaining_functions.php create mode 100644 config/sgf_single_installation.php create mode 100644 config/stock_catalogue.php create mode 100644 config/stock_completion.php create mode 100644 config/stock_operations.php create mode 100644 config/workshop_catalogue.php create mode 100644 config/workshop_schedule.php create mode 100644 database/migrations/2026_11_09_000001_create_sgc_integration_tables.php create mode 100644 database/migrations/2026_11_09_000002_create_sgc_analytics_tables.php create mode 100644 database/sql/bank_reconciliation.sql create mode 100644 database/sql/cash.sql create mode 100644 database/sql/commercial_quotes.sql create mode 100644 database/sql/financial_indicators.sql create mode 100644 database/sql/fiscal_assessment.sql create mode 100644 database/sql/fiscal_obligations.sql create mode 100644 database/sql/fiscal_rules.sql create mode 100644 database/sql/hr_personnel.sql create mode 100644 database/sql/internal_statements.sql create mode 100644 database/sql/payable_due.sql create mode 100644 database/sql/remunerations.sql create mode 100644 database/sql/service_documents.sql create mode 100644 database/sql/stock_catalogue.sql create mode 100644 database/sql/workshop_schedule.sql create mode 100644 docs/SGC_8085_INTEGRATION_PREPARATION.md create mode 100644 docs/SGC_INVOICE_ENDPOINT.md create mode 100644 docs/SGC_INVOICE_WORKSPACE_DELIVERY.md create mode 100644 docs/SGC_PREPARATION_20261110.md create mode 100644 docs/SGF_8085_NEXT_AUTHORIZATION.md create mode 100644 docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md create mode 100644 docs/SGF_BANK_RECONCILIATION_DELIVERY.md create mode 100644 docs/SGF_BANK_RECONCILIATION_SCOPE.md create mode 100644 docs/SGF_BILLING_REGISTER_DELIVERY.md create mode 100644 docs/SGF_CASH_DELIVERY.md create mode 100644 docs/SGF_CASH_NEXT_SCOPE.md create mode 100644 docs/SGF_CLIENT_ASSET_DELIVERY.md create mode 100644 docs/SGF_CURRENT_DELIVERY_20261110.md create mode 100644 docs/SGF_DOCUMENT_READ_ACCESS.md create mode 100644 docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md create mode 100644 docs/SGF_FINANCIAL_INDICATORS_SCOPE.md create mode 100644 docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md create mode 100644 docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md create mode 100644 docs/SGF_FISCAL_COVERAGE_DELIVERY.md create mode 100644 docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md create mode 100644 docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md create mode 100644 docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md create mode 100644 docs/SGF_FISCAL_RULES_COMMON_SCOPE.md create mode 100644 docs/SGF_GUIAO_APRESENTACAO.md create mode 100644 docs/SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md create mode 100644 docs/SGF_HUMAN_RESOURCES_DELIVERY.md create mode 100644 docs/SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md create mode 100644 docs/SGF_HUMAN_RESOURCES_PREPARATION.md create mode 100644 docs/SGF_IVA_ASSESSMENT_DELIVERY.md create mode 100644 docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md create mode 100644 docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md create mode 100644 docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md create mode 100644 docs/SGF_NEXT_BLOCK_RIGHTS.md create mode 100644 docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md create mode 100644 docs/SGF_PAYABLE_AGING_DELIVERY.md create mode 100644 docs/SGF_PAYABLE_DUE_DELIVERY.md create mode 100644 docs/SGF_PAYABLE_DUE_SCOPE.md create mode 100644 docs/SGF_PAYABLE_POSITIONS_DELIVERY.md create mode 100644 docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md create mode 100644 docs/SGF_REMUNERATIONS_SCOPE.md create mode 100644 docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md create mode 100644 docs/SGF_SINGLE_INTEGRATION_20261110.md create mode 100644 docs/SGF_SIX_MODULE_CLOSURE_20261110.md create mode 100644 docs/SGF_SOLUTION_DELIVERY.md create mode 100644 docs/SGF_STOCK_CATALOGUE_DELIVERY.md create mode 100644 docs/SGF_STOCK_CATALOGUE_SCOPE.md create mode 100644 docs/SGF_STOCK_FINAL_SCOPE.md create mode 100644 docs/SGF_STOCK_INACTIVITY_DELIVERY.md create mode 100644 docs/SGF_STOCK_OPERATIONS_DELIVERY.md create mode 100644 docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md create mode 100644 docs/SGF_USO_LOCAL.md create mode 100644 docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md create mode 100644 docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md create mode 100644 docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md create mode 100644 docs/integration/sgf-8085-access-proposal.json create mode 100644 docs/integration/sgf-bank-reconciliation-access-proposal.json create mode 100644 docs/integration/sgf-cash-access-proposal.json create mode 100644 docs/integration/sgf-document-read-access-proposal.json create mode 100644 docs/integration/sgf-financial-indicators-access-proposal.json create mode 100644 docs/integration/sgf-fiscal-expression-port.json create mode 100644 docs/integration/sgf-fiscal-obligations-access-proposal.json create mode 100644 docs/integration/sgf-fiscal-rules-access-proposal.json create mode 100644 docs/integration/sgf-human-resources-access-proposal.json create mode 100644 docs/integration/sgf-human-resources-personnel-access-proposal.json create mode 100644 docs/integration/sgf-iva-assessment-access-proposal.json create mode 100644 docs/integration/sgf-knowledge-inventory-20261110.json create mode 100644 docs/integration/sgf-multi-line-included-proposal.json create mode 100644 docs/integration/sgf-payable-due-access-proposal.json create mode 100644 docs/integration/sgf-remaining-functions-access-proposal.json create mode 100644 docs/integration/sgf-remunerations-access-proposal.json create mode 100644 docs/integration/sgf-solution-contract.json create mode 100644 docs/integration/sgf-statements-access-proposal.json create mode 100644 docs/integration/sgf-stock-catalogue-access-proposal.json create mode 100644 docs/integration/sgf-stock-final-access-proposal.json create mode 100644 docs/proofs/sgc-8085-integration-preparation-20261110.json create mode 100644 docs/proofs/sgc-invoice-endpoint-20261110.json create mode 100644 docs/proofs/sgc-invoice-workspace-20261110.json create mode 100644 docs/proofs/sgc-invoice-workspace-final-20261110.json create mode 100644 docs/proofs/sgc-preparation-20261110.json create mode 100644 docs/proofs/sgf-accounting-reports-20261110.json create mode 100644 docs/proofs/sgf-bank-reconciliation-20261110.json create mode 100644 docs/proofs/sgf-billing-register-20261110.json create mode 100644 docs/proofs/sgf-cash-20261110.json create mode 100644 docs/proofs/sgf-composition-footprint-20261110.json create mode 100644 docs/proofs/sgf-financial-indicators-20261110.json create mode 100644 docs/proofs/sgf-fiscal-coverage-20261110.json create mode 100644 docs/proofs/sgf-fiscal-obligations-20261110.json create mode 100644 docs/proofs/sgf-fiscal-rules-20261110.json create mode 100644 docs/proofs/sgf-human-resources-20261111.json create mode 100644 docs/proofs/sgf-human-resources-personnel-preparation-20261111.json create mode 100644 docs/proofs/sgf-human-resources-preparation-20261111.json create mode 100644 docs/proofs/sgf-iva-assessment-20261110.json create mode 100644 docs/proofs/sgf-multi-line-included-20261110.json create mode 100644 docs/proofs/sgf-next-block-progress-20261110.json create mode 100644 docs/proofs/sgf-operational-information-20261110.json create mode 100644 docs/proofs/sgf-payable-aging-20261110.json create mode 100644 docs/proofs/sgf-payable-due-20261110.json create mode 100644 docs/proofs/sgf-payable-positions-20261110.json create mode 100644 docs/proofs/sgf-receivable-positions-20261110.json create mode 100644 docs/proofs/sgf-single-integration-20261110.json create mode 100644 docs/proofs/sgf-single-integration-pagination-20261110.json create mode 100644 docs/proofs/sgf-six-module-composition-20261110.json create mode 100644 docs/proofs/sgf-six-module-footprint-current-20261110.json create mode 100644 docs/proofs/sgf-six-module-footprint-final-20261110.json create mode 100644 docs/proofs/sgf-six-module-functional-review-20261110.json create mode 100644 docs/proofs/sgf-solution-closure-baseline-20261110.json create mode 100644 docs/proofs/sgf-solution-closure-baseline-final-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v10-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v11-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v12-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v13-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v14-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v15-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v16-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v17-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v18-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v19-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v20-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v21-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v22-20261111.json create mode 100644 docs/proofs/sgf-solution-structure-v23-20261111.json create mode 100644 docs/proofs/sgf-solution-structure-v24-20261111.json create mode 100644 docs/proofs/sgf-solution-structure-v25-20261111.json create mode 100644 docs/proofs/sgf-solution-structure-v25-final-20261111.json create mode 100644 docs/proofs/sgf-solution-structure-v6-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v7-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v8-20261110.json create mode 100644 docs/proofs/sgf-solution-structure-v9-20261110.json create mode 100644 docs/proofs/sgf-statements-20261111.json create mode 100644 docs/proofs/sgf-stock-catalogue-20261110.json create mode 100644 docs/proofs/sgf-stock-inactivity-20261110.json create mode 100644 docs/proofs/sgf-stock-operations-20261110.json create mode 100644 docs/proofs/sgf-workshop-equipment-orders-20261111.json create mode 100644 docs/proofs/sgf-workshop-period-20261110.json create mode 100644 packages/general-ledger/src/StockSupplierReturnEvidence.php create mode 100644 packages/general-ledger/src/StockTransitEvidence.php create mode 100644 packages/inventory-issue/releases/0.1.0/bootstrap.php create mode 100644 packages/inventory-issue/releases/0.1.0/manifest.json create mode 100644 packages/inventory-issue/releases/0.1.0/src/IssueMovementWriter.php create mode 100644 packages/inventory-write/releases/0.1.0/bootstrap.php create mode 100644 packages/inventory-write/releases/0.1.0/manifest.json create mode 100644 packages/inventory-write/releases/0.1.0/src/ReceiptMovementWriter.php create mode 100644 packages/sgc-invoice-client/src/InvoiceClient.php create mode 100644 public/sgf-bank.mjs create mode 100644 public/sgf-cash.mjs create mode 100644 public/sgf-composed-ledger.mjs create mode 100644 public/sgf-entry-menu.mjs create mode 100644 public/sgf-finance-payables.mjs create mode 100644 public/sgf-fiscal-rules.mjs create mode 100644 public/sgf-indicators.mjs create mode 100644 public/sgf-iva.mjs create mode 100644 public/sgf-service-documents.mjs create mode 100644 public/sgf-stock-catalogue.mjs create mode 100644 public/sgf-stock-completion.mjs create mode 100644 public/sgf-stock-final.mjs create mode 100644 public/stock-operations-workspace.mjs create mode 100644 resources/data/bank-statement-qa.csv create mode 100644 resources/data/fiscal-rule-qa.json create mode 100644 resources/js/BillingRegisterWorkspace.jsx create mode 100644 resources/js/CommercialQuotesWorkspace.jsx create mode 100644 resources/js/ComposedAccountingWorkspace.jsx create mode 100644 resources/js/FiscalCoverage.jsx create mode 100644 resources/js/FiscalObligationsWorkspace.jsx create mode 100644 resources/js/InternalStatementsWorkspace.jsx create mode 100644 resources/js/LedgerReportsWorkspace.jsx create mode 100644 resources/js/PayableDueWorkspace.jsx create mode 100644 resources/js/PayablePositionsWorkspace.jsx create mode 100644 resources/js/PersonnelWorkspace.jsx create mode 100644 resources/js/ReceivablePositionsWorkspace.jsx create mode 100644 resources/js/RemunerationsWorkspace.jsx create mode 100644 resources/js/StockInactiveReportWorkspace.jsx create mode 100644 resources/js/WorkshopEquipmentWorkspace.jsx create mode 100644 resources/js/WorkshopOrderList.jsx create mode 100644 resources/js/WorkshopScheduleWorkspace.jsx create mode 100644 resources/js/fiscal-obligations-view.js create mode 100644 resources/js/internal-statements-view.js create mode 100644 resources/js/ledger-report-view.js create mode 100644 resources/js/payable-position-view.js create mode 100644 resources/js/presentation-landing.js create mode 100644 resources/js/receivable-position-view.js create mode 100644 resources/js/statement-download.js create mode 100644 resources/js/workshop-report-view.js create mode 100644 resources/js/workshop-stock-operation.js create mode 100644 resources/js/workspace-overview.js create mode 100644 resources/views/accounting/composed-ledger.blade.php create mode 100644 resources/views/commercial/documents.blade.php create mode 100644 resources/views/commercial/quote-print.blade.php create mode 100644 resources/views/finance/bank.blade.php create mode 100644 resources/views/finance/cash.blade.php create mode 100644 resources/views/finance/composed-payables.blade.php create mode 100644 resources/views/finance/indicators.blade.php create mode 100644 resources/views/fiscal/rules.blade.php create mode 100644 resources/views/fiscal/workspace.blade.php create mode 100644 resources/views/sgf-presentation-entry.blade.php create mode 100644 resources/views/stock-catalogue-workspace.blade.php create mode 100644 resources/views/stock-completion-workspace.blade.php create mode 100644 resources/views/stock-final-workspace.blade.php create mode 100644 resources/views/stock-operations-workspace.blade.php create mode 100644 routes/api.php create mode 100644 routes/sgc.php create mode 100644 scripts/abrir-sgf.ps1 create mode 100644 scripts/check-sgf-solution.mjs create mode 100644 scripts/launch-sgf.mjs create mode 100644 scripts/prepare-sgc-8085.mjs create mode 100644 scripts/sgc-dispatch-pg-bootstrap.php create mode 100644 scripts/sgc-dispatch-pg-proof.php create mode 100644 scripts/sgc-dispatch-receiver.php create mode 100644 scripts/sgc-invoice-authority-proof.php create mode 100644 scripts/sgc-invoice-check.mjs create mode 100644 scripts/sgc-invoice-client-kernel-proof.php create mode 100644 scripts/sgc-invoice-context-proof.php create mode 100644 scripts/sgc-invoice-proof-bootstrap.php create mode 100644 scripts/sgc-invoice-proof.php create mode 100644 scripts/sgc-invoice-security-proof.php create mode 100644 scripts/sgf-accounting-continuity-http.mjs create mode 100644 scripts/sgf-accounting-report-boundaries.php create mode 100644 scripts/sgf-accounting-reports-http.mjs create mode 100644 scripts/sgf-accounting-reports-ui.mjs create mode 100644 scripts/sgf-analysis-access-readonly.php create mode 100644 scripts/sgf-analysis-backup.php create mode 100644 scripts/sgf-analysis-boundaries.php create mode 100644 scripts/sgf-analysis-concurrency.mjs create mode 100644 scripts/sgf-analysis-concurrency.php create mode 100644 scripts/sgf-analysis-dom-http.mjs create mode 100644 scripts/sgf-analysis-http.mjs create mode 100644 scripts/sgf-analysis-preservation.php create mode 100644 scripts/sgf-analysis-proof-bootstrap.php create mode 100644 scripts/sgf-analysis-proof.php create mode 100644 scripts/sgf-analysis-setup.php create mode 100644 scripts/sgf-analysis-sources-readonly.php create mode 100644 scripts/sgf-assets-http.mjs create mode 100644 scripts/sgf-assets-routing-proof.php create mode 100644 scripts/sgf-authorized-profiles.php create mode 100644 scripts/sgf-bank-access-readonly.php create mode 100644 scripts/sgf-bank-backup.php create mode 100644 scripts/sgf-bank-boundaries.php create mode 100644 scripts/sgf-bank-concurrency.mjs create mode 100644 scripts/sgf-bank-concurrency.php create mode 100644 scripts/sgf-bank-dom-http.mjs create mode 100644 scripts/sgf-bank-guard-repair.php create mode 100644 scripts/sgf-bank-http.mjs create mode 100644 scripts/sgf-bank-origin-readonly.php create mode 100644 scripts/sgf-bank-preservation.php create mode 100644 scripts/sgf-bank-proof-bootstrap.php create mode 100644 scripts/sgf-bank-proof.php create mode 100644 scripts/sgf-bank-report-http.mjs create mode 100644 scripts/sgf-bank-setup.php create mode 100644 scripts/sgf-billing-register-http.mjs create mode 100644 scripts/sgf-billing-register-proof.php create mode 100644 scripts/sgf-cash-access.php create mode 100644 scripts/sgf-cash-backup.php create mode 100644 scripts/sgf-cash-concurrency.mjs create mode 100644 scripts/sgf-cash-concurrency.php create mode 100644 scripts/sgf-cash-dom-http.mjs create mode 100644 scripts/sgf-cash-http.mjs create mode 100644 scripts/sgf-cash-period-proof.php create mode 100644 scripts/sgf-cash-preservation.php create mode 100644 scripts/sgf-cash-proof-bootstrap.php create mode 100644 scripts/sgf-cash-proof.php create mode 100644 scripts/sgf-cash-read-boundaries.php create mode 100644 scripts/sgf-cash-rights-readonly.php create mode 100644 scripts/sgf-cash-rule-policy.php create mode 100644 scripts/sgf-cash-setup.php create mode 100644 scripts/sgf-commercial-quotes-http.mjs create mode 100644 scripts/sgf-commercial-quotes-proof.php create mode 100644 scripts/sgf-commercial-quotes-schema.php create mode 100644 scripts/sgf-commercial-workflow.php create mode 100644 scripts/sgf-compose-install.php create mode 100644 scripts/sgf-composed-direct-cost-proof.php create mode 100644 scripts/sgf-composed-industrial-proof.php create mode 100644 scripts/sgf-composed-ledger-proof.php create mode 100644 scripts/sgf-composed-payments-proof.php create mode 100644 scripts/sgf-composed-period-proof.php create mode 100644 scripts/sgf-composed-sales-ledger-proof.php create mode 100644 scripts/sgf-composition-bootstrap.php create mode 100644 scripts/sgf-composition-flow-proof.php create mode 100644 scripts/sgf-composition-http-check.mjs create mode 100644 scripts/sgf-composition-proof.php create mode 100644 scripts/sgf-document-cancellation-proof.php create mode 100644 scripts/sgf-document-cancellation-schema.php create mode 100644 scripts/sgf-document-closed-period-check.php create mode 100644 scripts/sgf-document-inputs.php create mode 100644 scripts/sgf-document-ledger-setup.php create mode 100644 scripts/sgf-document-math-backup.php create mode 100644 scripts/sgf-document-math-concurrency.mjs create mode 100644 scripts/sgf-document-math-concurrency.php create mode 100644 scripts/sgf-document-math-dom-http.mjs create mode 100644 scripts/sgf-document-math-http.mjs create mode 100644 scripts/sgf-document-math-preservation.php create mode 100644 scripts/sgf-document-math-proof-bootstrap.php create mode 100644 scripts/sgf-document-math-proof.php create mode 100644 scripts/sgf-document-math-recovery.php create mode 100644 scripts/sgf-document-period-proof.php create mode 100644 scripts/sgf-document-read-access.php create mode 100644 scripts/sgf-document-read-rights.php create mode 100644 scripts/sgf-documents-concurrency.mjs create mode 100644 scripts/sgf-documents-concurrency.php create mode 100644 scripts/sgf-equipment-orders-preservation.php create mode 100644 scripts/sgf-finance-ledger-client-check.mjs create mode 100644 scripts/sgf-finance-lock-proof.php create mode 100644 scripts/sgf-fiscal-context-ui.mjs create mode 100644 scripts/sgf-fiscal-coverage-http.mjs create mode 100644 scripts/sgf-fiscal-coverage-preservation.php create mode 100644 scripts/sgf-fiscal-coverage-proof.php create mode 100644 scripts/sgf-fiscal-coverage-ui.mjs create mode 100644 scripts/sgf-fiscal-entry-http.mjs create mode 100644 scripts/sgf-fiscal-expression-parity.php create mode 100644 scripts/sgf-fiscal-map-download-proof.mjs create mode 100644 scripts/sgf-fiscal-next-access-check.php create mode 100644 scripts/sgf-fiscal-obligations-access-readonly.php create mode 100644 scripts/sgf-fiscal-obligations-backup.php create mode 100644 scripts/sgf-fiscal-obligations-boundaries.php create mode 100644 scripts/sgf-fiscal-obligations-concurrency.mjs create mode 100644 scripts/sgf-fiscal-obligations-concurrency.php create mode 100644 scripts/sgf-fiscal-obligations-entry-http.mjs create mode 100644 scripts/sgf-fiscal-obligations-http.mjs create mode 100644 scripts/sgf-fiscal-obligations-preservation.php create mode 100644 scripts/sgf-fiscal-obligations-proof-bootstrap.php create mode 100644 scripts/sgf-fiscal-obligations-proof.php create mode 100644 scripts/sgf-fiscal-obligations-regression-source.php create mode 100644 scripts/sgf-fiscal-obligations-sample.php create mode 100644 scripts/sgf-fiscal-obligations-setup.php create mode 100644 scripts/sgf-fiscal-obligations-ui.mjs create mode 100644 scripts/sgf-fiscal-rules-access-readonly.php create mode 100644 scripts/sgf-fiscal-rules-backup.php create mode 100644 scripts/sgf-fiscal-rules-boundaries.php create mode 100644 scripts/sgf-fiscal-rules-concurrency.mjs create mode 100644 scripts/sgf-fiscal-rules-concurrency.php create mode 100644 scripts/sgf-fiscal-rules-dom-http.mjs create mode 100644 scripts/sgf-fiscal-rules-http.mjs create mode 100644 scripts/sgf-fiscal-rules-input-proof.php create mode 100644 scripts/sgf-fiscal-rules-preservation.php create mode 100644 scripts/sgf-fiscal-rules-proof-bootstrap.php create mode 100644 scripts/sgf-fiscal-rules-proof.php create mode 100644 scripts/sgf-fiscal-rules-setup.php create mode 100644 scripts/sgf-guided-menu-http.mjs create mode 100644 scripts/sgf-hr-personnel-backup.php create mode 100644 scripts/sgf-hr-personnel-boundaries.php create mode 100644 scripts/sgf-hr-personnel-concurrency.mjs create mode 100644 scripts/sgf-hr-personnel-concurrency.php create mode 100644 scripts/sgf-hr-personnel-fixture.php create mode 100644 scripts/sgf-hr-personnel-proof-bootstrap.php create mode 100644 scripts/sgf-hr-personnel-proof.php create mode 100644 scripts/sgf-hr-personnel-setup.php create mode 100644 scripts/sgf-human-resources-boundaries.php create mode 100644 scripts/sgf-human-resources-concurrency.mjs create mode 100644 scripts/sgf-human-resources-concurrency.php create mode 100644 scripts/sgf-human-resources-final-entry.mjs create mode 100644 scripts/sgf-human-resources-finalize.php create mode 100644 scripts/sgf-human-resources-http.mjs create mode 100644 scripts/sgf-human-resources-personnel-access-readonly.php create mode 100644 scripts/sgf-human-resources-personnel-time-proof.php create mode 100644 scripts/sgf-human-resources-preauthorization.php create mode 100644 scripts/sgf-human-resources-preparation-preservation.php create mode 100644 scripts/sgf-human-resources-prepared-ui.mjs create mode 100644 scripts/sgf-human-resources-preservation.php create mode 100644 scripts/sgf-human-resources-setup.php create mode 100644 scripts/sgf-included-policy-preview.php create mode 100644 scripts/sgf-industrial-workflow.php create mode 100644 scripts/sgf-install-accounting-continuity.php create mode 100644 scripts/sgf-install-accounting-reports.php create mode 100644 scripts/sgf-install-analysis.php create mode 100644 scripts/sgf-install-bank.php create mode 100644 scripts/sgf-install-billing-register.php create mode 100644 scripts/sgf-install-cash.php create mode 100644 scripts/sgf-install-commercial-quotes.php create mode 100644 scripts/sgf-install-document-math.php create mode 100644 scripts/sgf-install-equipment-orders.php create mode 100644 scripts/sgf-install-fiscal-coverage.php create mode 100644 scripts/sgf-install-fiscal-obligations.php create mode 100644 scripts/sgf-install-fiscal-rules.php create mode 100644 scripts/sgf-install-human-resources.php create mode 100644 scripts/sgf-install-iva.php create mode 100644 scripts/sgf-install-overview.php create mode 100644 scripts/sgf-install-payable-aging.php create mode 100644 scripts/sgf-install-payable-due.php create mode 100644 scripts/sgf-install-payable-positions.php create mode 100644 scripts/sgf-install-receivable-positions.php create mode 100644 scripts/sgf-install-service-documents.php create mode 100644 scripts/sgf-install-single-workspace.php create mode 100644 scripts/sgf-install-statements.php create mode 100644 scripts/sgf-install-stock-catalogue.php create mode 100644 scripts/sgf-install-stock-completion.php create mode 100644 scripts/sgf-install-stock-final.php create mode 100644 scripts/sgf-install-stock-inactive.php create mode 100644 scripts/sgf-install-stock-prepared.mjs create mode 100644 scripts/sgf-install-workshop-commercial.php create mode 100644 scripts/sgf-install-workshop-period.php create mode 100644 scripts/sgf-install-workshop-schedule.php create mode 100644 scripts/sgf-install-workshop-stock.php create mode 100644 scripts/sgf-integration-access.php create mode 100644 scripts/sgf-iva-backup.php create mode 100644 scripts/sgf-iva-boundaries.php create mode 100644 scripts/sgf-iva-concurrency.mjs create mode 100644 scripts/sgf-iva-concurrency.php create mode 100644 scripts/sgf-iva-dom-http.mjs create mode 100644 scripts/sgf-iva-http.mjs create mode 100644 scripts/sgf-iva-preservation.php create mode 100644 scripts/sgf-iva-proof-bootstrap.php create mode 100644 scripts/sgf-iva-proof.php create mode 100644 scripts/sgf-iva-setup.php create mode 100644 scripts/sgf-ledger-candidate-pages-proof.php create mode 100644 scripts/sgf-ledger-proof-bootstrap.php create mode 100644 scripts/sgf-material-isolation-proof.php create mode 100644 scripts/sgf-material-perimeter.php create mode 100644 scripts/sgf-module-dropdown-check.mjs create mode 100644 scripts/sgf-native-bindings.php create mode 100644 scripts/sgf-native-ledger-setup.php create mode 100644 scripts/sgf-next-block-backup.php create mode 100644 scripts/sgf-next-block-preservation.php create mode 100644 scripts/sgf-overview-entry-http.mjs create mode 100644 scripts/sgf-overview-final-http.mjs create mode 100644 scripts/sgf-overview-http.mjs create mode 100644 scripts/sgf-overview-ui.mjs create mode 100644 scripts/sgf-payable-aging-http.mjs create mode 100644 scripts/sgf-payable-aging-preservation.php create mode 100644 scripts/sgf-payable-aging-proof.php create mode 100644 scripts/sgf-payable-aging-ui.mjs create mode 100644 scripts/sgf-payable-due-access-readonly.php create mode 100644 scripts/sgf-payable-due-backup.php create mode 100644 scripts/sgf-payable-due-concurrency.mjs create mode 100644 scripts/sgf-payable-due-concurrency.php create mode 100644 scripts/sgf-payable-due-final-http.mjs create mode 100644 scripts/sgf-payable-due-http.mjs create mode 100644 scripts/sgf-payable-due-identity-proof.php create mode 100644 scripts/sgf-payable-due-list-proof.php create mode 100644 scripts/sgf-payable-due-pagination-proof.php create mode 100644 scripts/sgf-payable-due-preservation.php create mode 100644 scripts/sgf-payable-due-proof-bootstrap.php create mode 100644 scripts/sgf-payable-due-proof.php create mode 100644 scripts/sgf-payable-due-setup.php create mode 100644 scripts/sgf-payable-due-ui.mjs create mode 100644 scripts/sgf-payable-positions-http.mjs create mode 100644 scripts/sgf-payable-positions-preservation.php create mode 100644 scripts/sgf-payable-positions-proof.php create mode 100644 scripts/sgf-payable-positions-ui.mjs create mode 100644 scripts/sgf-presentation-controller-proof.php create mode 100644 scripts/sgf-profiles-check.php create mode 100644 scripts/sgf-quotes-concurrency.mjs create mode 100644 scripts/sgf-quotes-concurrency.php create mode 100644 scripts/sgf-receivable-positions-http.mjs create mode 100644 scripts/sgf-receivable-positions-proof.php create mode 100644 scripts/sgf-receivable-positions-ui.mjs create mode 100644 scripts/sgf-receivables-workflow.php create mode 100644 scripts/sgf-remaining-access.php create mode 100644 scripts/sgf-remaining-rights-readonly.php create mode 100644 scripts/sgf-remunerations-access-readonly.php create mode 100644 scripts/sgf-remunerations-backup.php create mode 100644 scripts/sgf-remunerations-proof-bootstrap.php create mode 100644 scripts/sgf-remunerations-proof.php create mode 100644 scripts/sgf-remunerations-retract-finance.php create mode 100644 scripts/sgf-remunerations-setup.php create mode 100644 scripts/sgf-repair-new-industrial-fixture.php create mode 100644 scripts/sgf-restart-proof.mjs create mode 100644 scripts/sgf-runtime-snapshot.php create mode 100644 scripts/sgf-selected-assets.php create mode 100644 scripts/sgf-service-documents-dom-http.mjs create mode 100644 scripts/sgf-service-documents-http.mjs create mode 100644 scripts/sgf-service-documents-proof-bootstrap.php create mode 100644 scripts/sgf-service-documents-proof.php create mode 100644 scripts/sgf-service-documents-schema.php create mode 100644 scripts/sgf-service-documents-status.php create mode 100644 scripts/sgf-single-bootstrap.php create mode 100644 scripts/sgf-single-boundaries-http.mjs create mode 100644 scripts/sgf-single-corrections-http.mjs create mode 100644 scripts/sgf-single-financial-http.mjs create mode 100644 scripts/sgf-single-http.mjs create mode 100644 scripts/sgf-single-read-http.mjs create mode 100644 scripts/sgf-single-stock-final-http.mjs create mode 100644 scripts/sgf-single-stock-http.mjs create mode 100644 scripts/sgf-single-workshop-commercial-http.mjs create mode 100644 scripts/sgf-statements-access-readonly.php create mode 100644 scripts/sgf-statements-backup.php create mode 100644 scripts/sgf-statements-boundaries.php create mode 100644 scripts/sgf-statements-concurrency.mjs create mode 100644 scripts/sgf-statements-concurrency.php create mode 100644 scripts/sgf-statements-download-proof.mjs create mode 100644 scripts/sgf-statements-final-http.mjs create mode 100644 scripts/sgf-statements-http.mjs create mode 100644 scripts/sgf-statements-preservation.php create mode 100644 scripts/sgf-statements-proof-bootstrap.php create mode 100644 scripts/sgf-statements-proof.php create mode 100644 scripts/sgf-statements-setup.php create mode 100644 scripts/sgf-statements-snapshot-failure.php create mode 100644 scripts/sgf-statements-source-dates.php create mode 100644 scripts/sgf-statements-source-readonly.mjs create mode 100644 scripts/sgf-statements-ui-sample.php create mode 100644 scripts/sgf-statements-ui.mjs create mode 100644 scripts/sgf-stock-catalogue-access-readonly.php create mode 100644 scripts/sgf-stock-catalogue-assets.mjs create mode 100644 scripts/sgf-stock-catalogue-backup.php create mode 100644 scripts/sgf-stock-catalogue-boundaries.php create mode 100644 scripts/sgf-stock-catalogue-concurrency.mjs create mode 100644 scripts/sgf-stock-catalogue-concurrency.php create mode 100644 scripts/sgf-stock-catalogue-dom-http.mjs create mode 100644 scripts/sgf-stock-catalogue-http.mjs create mode 100644 scripts/sgf-stock-catalogue-metadata.php create mode 100644 scripts/sgf-stock-catalogue-navigation.mjs create mode 100644 scripts/sgf-stock-catalogue-preservation.php create mode 100644 scripts/sgf-stock-catalogue-proof-bootstrap.php create mode 100644 scripts/sgf-stock-catalogue-proof.php create mode 100644 scripts/sgf-stock-catalogue-setup.php create mode 100644 scripts/sgf-stock-completion-binding.php create mode 100644 scripts/sgf-stock-completion-concurrency.mjs create mode 100644 scripts/sgf-stock-completion-concurrency.php create mode 100644 scripts/sgf-stock-completion-dom-http.mjs create mode 100644 scripts/sgf-stock-completion-http.mjs create mode 100644 scripts/sgf-stock-completion-ledger-setup.php create mode 100644 scripts/sgf-stock-completion-proof-bootstrap.php create mode 100644 scripts/sgf-stock-completion-proof.php create mode 100644 scripts/sgf-stock-disposition-input.php create mode 100644 scripts/sgf-stock-final-backup.php create mode 100644 scripts/sgf-stock-final-boundaries-http.mjs create mode 100644 scripts/sgf-stock-final-client-check.mjs create mode 100644 scripts/sgf-stock-final-concurrency.mjs create mode 100644 scripts/sgf-stock-final-concurrency.php create mode 100644 scripts/sgf-stock-final-dom-http.mjs create mode 100644 scripts/sgf-stock-final-install.php create mode 100644 scripts/sgf-stock-final-ledger-setup.php create mode 100644 scripts/sgf-stock-final-preservation.php create mode 100644 scripts/sgf-stock-final-proof-bootstrap.php create mode 100644 scripts/sgf-stock-final-proof.php create mode 100644 scripts/sgf-stock-final-recovery-proof.php create mode 100644 scripts/sgf-stock-inactive-http.mjs create mode 100644 scripts/sgf-stock-inactive-preservation.php create mode 100644 scripts/sgf-stock-inactive-proof.php create mode 100644 scripts/sgf-stock-inactive-ui.mjs create mode 100644 scripts/sgf-stock-lot-input.php create mode 100644 scripts/sgf-stock-physical-input.php create mode 100644 scripts/sgf-stock-rate-controller-proof.php create mode 100644 scripts/sgf-stock-remaining-rights-check.php create mode 100644 scripts/sgf-stock-transit-ledger-proof.php create mode 100644 scripts/sgf-workshop-commercial-proof.php create mode 100644 scripts/sgf-workshop-direct-workflow.php create mode 100644 scripts/sgf-workshop-equipment-http.mjs create mode 100644 scripts/sgf-workshop-equipment-orders-http.mjs create mode 100644 scripts/sgf-workshop-equipment-orders-proof.php create mode 100644 scripts/sgf-workshop-equipment-orders-ui.mjs create mode 100644 scripts/sgf-workshop-equipment-proof.php create mode 100644 scripts/sgf-workshop-equipment-schema.php create mode 100644 scripts/sgf-workshop-orders-http.mjs create mode 100644 scripts/sgf-workshop-orders-proof.php create mode 100644 scripts/sgf-workshop-orders-ui.mjs create mode 100644 scripts/sgf-workshop-period-http.mjs create mode 100644 scripts/sgf-workshop-period-preservation.php create mode 100644 scripts/sgf-workshop-period-proof.php create mode 100644 scripts/sgf-workshop-period-ui.mjs create mode 100644 scripts/sgf-workshop-read-lock.mjs create mode 100644 scripts/sgf-workshop-read-lock.php create mode 100644 scripts/sgf-workshop-schedule-concurrency.mjs create mode 100644 scripts/sgf-workshop-schedule-concurrency.php create mode 100644 scripts/sgf-workshop-schedule-http.mjs create mode 100644 scripts/sgf-workshop-schedule-proof.php create mode 100644 scripts/sgf-workshop-schedule-schema.php create mode 100644 scripts/sgf-workshop-stock-bindings.php create mode 100644 scripts/sgf-workshop-stock-bridge-proof.php create mode 100644 scripts/sgf-workshop-stock-client-check.mjs create mode 100644 scripts/sgf-workshop-stock-input.php create mode 100644 scripts/sgf-workshop-stock-schema.php create mode 100644 scripts/stock-operations-client-check.mjs create mode 100644 scripts/stock-operations-proof.php create mode 100644 scripts/stop-selected-sgf-server.ps1 create mode 100644 scripts/summarize-sgc-dispatch.php create mode 100644 scripts/summarize-sgc-invoice.php create mode 100644 scripts/summarize-sgf-single-integration.mjs create mode 100644 scripts/summarize-sgf-six-modules.mjs create mode 100644 tests/Unit/CalendarDimensionTest.php create mode 100644 tests/Unit/SgcInvoiceClientTest.php create mode 100644 tests/payloads/invoice_test.json diff --git a/AGENTS.md b/AGENTS.md index a63cfae..b23a9a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,140 @@ # SGF Tempo — instruções locais +- Autorização expressa11/11: «Autorizar o bloco e as30 concessões QA apresentadas», docs/SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md/JSON. Implementar todo o bloco contratos/assiduidade/férias/conferência e ligação contratual explícita às novas preparações salariais apenas em regressão.9direitos/15grants por livro existente/30total, mesmos3perfisRH, sem novas identidades/pins. Preservar6versões/recibos salariais8085,seis registos/cache/GL27/contas19 e exemplos. Sem demos/SGC/efeitos financeiros/modelos legais/publicação/checkpoint. Indicações anteriores por autorizar substituídas; não pedir por campo/operação/teste. + +- Pedido11/11 para contratos/assiduidade/férias no RH autónomo. Âmbito completo preparado `docs/SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md`/JSON: contrato QA10000, planos09/10Nov480min, presença09, orçamento fictício960min/pedido-gozo10de480/saldo480, conferência parcial por pessoa e ligação contratual explícita só em futuras preparações salariais (prova regressão), preservando6versões/recibos salariais8085. Novos9direitos human_resources.contracts/attendance/leave × view/prepare/review; consultas3perfis, preparehr_prepare/reviewhr_review,15grants/livro30total nos perfis existentes, **por autorizar/não concedidos**; SELECT-only confirma ausência nos dois livros. Zero novas identidades/roles/pins alterados. PersonnelTime puro local22checks intervalos/desconhecidos/conflitos/saldo; não instalado, sem writers/schema/dados alterados. Não declarar funções novas entregues. Regra de novos poderes requer decisão antes de conceder; pedido de implementar não contém lista nova aprovada. Preservar8085/seis registos/cache/provas/GL27/contas19/Caixa950/fontes; sem demos/SGC/dívida/pagamento/GL/salário/contrato real/modelos legais/publicação/checkpoint. Não presumir férias legais960min nem classificar faltas automaticamente. Após aprovação, implementar todo bloco sem pedir por campo/rubrica/teste. + + +- RH autónomo entregue11/11, `docs/SGF_HUMAN_RESOURCES_DELIVERY.md`, mesma8085 `/sgf`→Recursos Humanos. Três perfis próprios QA/livro,11grants/livro22total, namespace human_resources.remunerations.*, registo separado SHA, sem alterar10perfis/pins anteriores; antigosgrants Financeiros remunerations.* permanecem ausentes. Cadastro/regras/processamento de QA-COLABORADOR-01/2026-11 revistos por pessoas distintas: bruto10000, descontos300+800, encargo400, líquido8900/custo10400. São parâmetros artificiais, não cálculo legal IRPS/INSS.6versões/6recibos na8085; não repetir comandos, progresso privado hr-http-progress-private.json.40reg perfisHR reais+23fronteiras finais+16handlers API-simulada reutilizados+43HTTP+10leituras finais=132, mais concorrência5,20s503 hostempresa FOR UPDATE/transactionid pg_blocking_pids→409 pós-libertação/replay200 sem duplicação, e71preservação/ACL.55conjuntos originais iguais+grants antigos iguais com11aprovados,3bindings/GL27/contas19/Caixa950/seis registos/cache preservados.27fixtures paginação+1estrangeira/falha parcial revertidas;6versões suporte concorrência ficaram só na regressão. JSmain/lazy/shared MIME/SHA, flags root off/privada8085 on, companycontract só1:2 incluiRH. Matrizv25 tem7módulos; produto amplo incompleto. Contratos/assiduidade/férias/legalIRPS-INSS/ligação salarial ainda faltam. Sem dívida/pagamento/GL/salário real/modelo oficial/demos/SGC/publicação/candidata/checkpoint/aceitação; visual/carga/implantação separados. Indicações históricas de acessos/instalação por autorizar foram substituídas. + + +- Autorização expressa11/11: «Autorizar os perfis QA próprios de RH e as concessões apresentadas», `docs/SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md`/JSON. Criar hr_prepare/hr_review/hr_read,11grants por livro QA existente/22total, sem conceder aos Financeiros. Concluir todo o percurso sintético autorizado no módulo autónomo RH na mesma8085, provas dirigidas positivas/isolamento/concorrência/recuperação e preservação. Não pedir por campo/rubrica. Preservar perfis/pins anteriores,27GL,seis registos/cache; sem demos/SGC/dívida/pagamento/salário real/GL novo/modelos legais/publicação/checkpoint. Indicações anteriores por autorizar foram substituídas. + +- Directiva posterior11/11: RH deve ser AUTÓNOMO, perfis/permissões distintos. `docs/SGF_HUMAN_RESOURCES_PREPARATION.md`: código/rotas/menu local preparado (não copiado8085), motor em App/Modules/HumanResources, proprietário human_resources, namespace human_resources.remunerations.*, registo HR separado SHA sem alterar os10perfis/pins aceites. Retiradas integralmente as7concessões novas a Financeiros13–17 por livro/14total, com auditoria; nenhum processamento8085 persistido. Três tabelas rm_* técnicas já criadas sob primeira autorização, versões/recibos vazios. Matriz v24 conserva6entregues e7ºRH planeado, produto incompleto. Próxima decisão `docs/SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md`/JSON: hr_prepare/hr_review/hr_read novos, view3+prepare1+review1 e company.view/unit.view para cada,11grants/livro22total/seisidentidades QA; **por autorizar/não concedidos/não criados**. Regra de acessos exige decisão da nova lista, autonomia do módulo já foi ordenada. Instalador novo falha sem aprovação, antigo bloqueado por status superseded.16handlersAPI-simulada,11preauth/cálculo em cada livro,65preservação/56conjuntos+3bindings intactos.40funcionais iniciais são anteriores à separação, fixtures revertidas; não equivalem a integração RH nova. Percurso efectivo/concorrência/falha parcial/HTTP RH pendentes até autorização; não declarar concluído. Contratos/assiduidade/férias/legalIRPS-INSS/ligação salarial ainda faltam.27GL/seis registos/cache preservados; sem demos/SGC/publicação/candidata/checkpoint/aceitação. Indicações anteriores de Financeiro e concessões autorizadas13–17 são históricas e foram substituídas pela directiva de autonomia. + + +- Autorização expressa11/11: «Autorizar o bloco QA e os três direitos apresentados», `docs/SGF_REMUNERATIONS_SCOPE.md`/JSON. Implementar todo o percurso sintético descrito: cadastro/regras/processamento/revisão/correcção/cancelamento/mapa/SHA/recuperação. remunerations.view13–17/prepare13/review14, sete concessões por livro existente/14total. Não pedir por rubrica. Sem salários/dívida/pagamento/GL/modelos legais novos; preservar27GL/seis registos/cache/mesma8085, sem demos/SGC/publicação/checkpoint. A indicação histórica por autorizar foi substituída por esta decisão. + +- Próxima decisão11/11 preparada `docs/SGF_REMUNERATIONS_SCOPE.md`/JSON: processamento interno completo de remunerações QA, cadastro fictício/regras versionadas/10000 bruto, parâmetros artificiais3%/4%/8%, descontos300+800, líquido8900/custo10400, revisão distinta/mapa conservado/SHA/correcção/cancelamento/recibos. Não são taxas legais nem salários reais; parâmetros em falta não viram zero. Três direitos remunerations.view13–17/prepare13/review14, sete concessões por QA existente/14total, **por autorizar/não implementados/não concedidos**, SELECT-only confirmou ausência nos dois livros. Sem novos GL/contas/pagamentos/dívida/modelos oficiais/submissão/SGC/demos; preservar27GL/fontes/seis registos/cache. Não inferir poder salarial dos direitos de Caixa/IVA/GL; não pedir por rubrica após autorização do bloco. + + +- Ordens por equipamento entregues11/11, `docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md`, mesma8085 Oficinas→Equipamentos. Escolha explícita, metadados actuais, filtro SQL antes25/100, pesquisa/estado/período, CSV/página e ordem exacta; regresso conserva catálogo. Reutiliza direitos/contexto/atribuição; fonte catálogo e ordens na mesma leitura consistente, sem writer/mutex/schema/grants novos.23reg (28ordens/um equipamento fixtures SQL revertidas),12handlers simulados,17HTTP efectivo=52.56conjuntos/binding intactos incluindo fs_* recentes e27GL; seis registos/cache/provas/trabalho alheio preservados. JSprincipal/lazy MIME/SHA, cópias anteriores privadas. Matriz v23 amplo produto incompleto; visual/carga/implantação separados. Sem demos/SGC/publicação/candidata/checkpoint/aceitação. + + +- Balanço/resultados internos parciais QA entregues11/11, `docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md`, mesma8085 Contabilidade.19 correspondências explícitas propostas preparadas15/revistas16, nove por classificar; resultado10/11 conservado19linhas sobre27lançamentos, quatro versões/recibos/eventos, um snapshot/SHA. Activos2155/fornecedor100/rendimentos2586,21/gastos2200/subtotal386,21; capital/resultado completo indeterminados, sem modelo legal. Motor/writer/contas/lançamentos originais intactos; original trialBalance por páginas25/100, grupos exactos/linhas conservadas, CSV por lotes100 e tempstream1MiB, não a cache bloqueada. Consulta/expor antigo preserva bytes, fonte nova marca histórico, razão recebe conta/datas. Dois direitos aprovados prepare15/review16, duas concessões por QA existente/quatro total, sem SQL/PUBLIC/roles/contas reais.152 verificações centrais mais65preservação/ACL/concorrência:38reg (livro vazio sem inserir GL),17fronteiras (double128linhas),4falha parcial(snapshot19linhas revertido sem contas/GL),23handlersUI,46HTTP sobre19contas/27GL efectivos,11descarga,13entrada/assets finais. Não somar passagens18/19UI ou16fronteiras substituídas. Concorrência503 após5,13s hostempresa FOR UPDATE/transactionid e pg_blocking_pids; pós-libertação409 e replay200/uma revisão só na regressão. Retoma corrigiu descarga API sem X-SGF-Context: botão com leitura autenticada/geração/SHA antes de guardar, token nunca URL; guardas originais mantidas. Mesmo atalho do mapa IVA corrigido, cinco compatibilidade e22UI existentes reexecutadas (prova antiga SHA igual), sem recriar mapa/decisões.49 conjuntos iguais; linhas antigas de permissões/auditoria nativa iguais+2grants/+4eventos, três bindings/GL27/contas19/Caixa950/IVA4/cálculos5/fo/pd/stock/seis registos/cache preservados. Progresso/chaves privados: não repetir quatro comandos8085. Config root desligada, privada8085 habilitada; JSmain/lazy/partilhado MIME/SHA conferidos. Sem demos/SGC/efeitos financeiros/fecho/reabertura/publicação/candidata/aceitação/checkpoint. Matriz v22 produto amplo incompleto; visual/carga/implantação separados. Proposta anterior por autorizar é histórica. + + +- Autorização expressa11/11: «Autorizar o bloco QA e os dois direitos apresentados», `docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md`/JSON. Implementar bloco inteiro de correspondências revistas e balanço/resultados internos parciais QA, prepare15/review16, duas concessões por QA existente/quatro total.19 contas propostas/nove por classificar, sem capital fabricado; snapshots/SHA/histórico/recibo/provas. Reutilizar motor/writer/fontes originais; preservar27lançamentos/dados/seis registos/cache, sem demos/SGC/modelo legal/publicação/candidata/checkpoint/aceitação. Não pedir por campo/teste; indicação anterior por autorizar é histórica. + + +- Antiguidade de fornecedores entregue10/11, `docs/SGF_PAYABLE_AGING_DELIVERY.md`, mesma8085 Financeiro. Referência explícita, saldo original/condição administrativa revista da página, faixas0/1–30/31–60/61–90/>90/desconhecido; moeda/centavos exactos, desconhecido separado e sem reconhecimento/indeterminado/liquidado excluídos das faixas monetárias. Original due_on/days_overdue preservados.52 verificações (26reg/12UI/14HTTP), JS main/lazy MIME/SHA;51 conjuntos/binding intactos incluindo fo_* quatro versões/recibos recentes, seis registos/cache/provas/trabalho alheio. Zero comandos/direitos/schema/índices novos; mesmos mutex/paginação25/100, sem totais globais/corte histórico/carga/implantação alegados. Matriz v21 produto amplo incompleto; visual separado; sem demos/SGC/publicação/candidata/aceitação/checkpoint. Próxima decisão preparada `docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md`/JSON: correspondências explícitas revistas para balanço/resultados internos parciais QA,19 contas/27lançamentos existentes, nove contas por classificar e nenhum capital fabricado. Dois direitos general_ledger.statements.prepare15/review16, duas concessões por QA existente/quatro total, **por autorizar/não concedidos**, SELECT-only confirma. Consulta reutiliza general_ledger.view; sem alterar motor/writer/contas/lançamentos ou declarar modelos legais. + + +- Obrigações/prazos administrativos QA entregues10/11, `docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md`, mesma8085 Contabilidade. Uma origem mapa IVA já revisto, referência/responsável explícitos, prazo artificial15/12, preparação13/revisão14 e entrega APENAS simulada13/14 com referência/SHA exportação; quatro versões/recibos/eventos. Correcção, desconhecido, recusa, cancelamento, histórico/impacto/recibo e SQL25/100/filtros/CSV. Três direitos aprovados view13–17/prepare13/review14, sete concessões por QA existente/14total, sem SQL/PUBLIC/roles/contas reais.41 regressão/19 fronteiras/22 handlers simulados/29 HTTP/sete entrada=118, mais concorrência503 após5,64s pelo FOR UPDATE empresa (pg_blocking_pids/transactionid), pós-libertação409/replay200 e fixture cancelada por dois comandos adicionais só na regressão. Reg havia livro revisto mas não mapa persistente: dois comandos necessários prepararam/reviram esse mapa, sem repetir livro/origem financeira;8085 mapa intacto.61 preservação/ACL:47 conjuntos iguais, linhas antigas de permissões iguais+sete concessões, três bindings/diário27/Caixa950/cálculos5/IVA4/condições/artigos/seis registos/cache preservados. Auditoria comum usa allowlist existente/refversão; impacto integral em fo_versions append-only. Config root desligada, privada8085 habilitada; JS main/lazy MIME/SHA conferidos. Sem demos/SGC/efeitos financeiros/AT/prazo legal/modelo oficial/publicação/candidata/aceitação/checkpoint. Matriz v20 produto amplo incompleto; visual/carga/implantação separados. Proposta anterior por autorizar é histórica; não repetir quatro comandos da8085 (progresso/chaves privados). + + +- Autorização expressa10/11: «Autorizar o bloco QA e os três direitos apresentados», `docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md`/JSON. Implementar bloco inteiro administrativo ligado ao mapa IVA revisto, fiscal_obligations.view13–17/prepare13/review14, sete concessões por QA existente/14total. Prazo15/12 artificial, entrega simulada por referência/SHA do export interno; sem prazos legais/AT/imposto/dívida/pagamento/lançamentos. Preservar fontes, seis registos/cache e demais limites; não pedir por campo/teste. Proposta anterior por autorizar é histórica. + + +- Relatório por abertura de ordens entregue10/11, `docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md`, mesma8085 Oficinas→Relatórios. Datas explícitas, dia local Africa/Johannesburg convertido em UTC início incluso/fim exclusivo, SQL antes25/100; apontamentos/identificação actuais, não fecho histórico. CSV só página/aplicação de filtros/escapagem, zero comandos/direitos/schema.21 regressão30fixtures revertidas/13 UI/15 HTTP=49, MIME/SHA de main/lazy;48 conjuntos actuais/binding intactos, diário27/Caixa950/cálculos5/condições de vencimento/seis registos/cache preservados. Provas anteriores reutilizadas, não repetir workflows; leitor sem mutex anterior preservado sem nova carga alegada. Matriz v19 produto amplo incompleto; visual/carga/implantação separados. Sem demos/SGC/publicação/candidata/checkpoint/aceitação. Próximo bloco concreto `docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md`/JSON: controlo administrativo de prazo/obrigação/declaração de entrega simulada ligado ao mapa IVA interno revisto, três direitos novos fiscal_obligations.view/prepare/review, sete concessões por QA existente/14total, **por autorizar/não concedidos** (SELECT-only confirma ausência). Sem calendário legal, imposto/dívida/pagamento/submissão externa; prazo15/12 artificial explicitamente. Não confundir agenda de Oficina já implementada com esta lacuna fiscal. + + +- Vencimentos administrativos de fornecedores entregues10/11, `docs/SGF_PAYABLE_DUE_DELIVERY.md`, mesma8085 Financeiro→Posições→Condições/histórico. Preparação13/revisão14, evidência/fundamento/prévia/recusa/correcção desconhecido/versões/recibo; origem/saldo/versão/direitos/contexto/pessoa revalidados. Dois direitos aprovados, duas concessões por QA existente/quatro total, sem SQL/PUBLIC/roles novos. Exemplo obrigação100/data artificial30/11, duas versões/recibos e dois eventos; original pa_obligations.due_on/factura/saldo/diário intactos.40 regressão final (não contar original40 outra vez)/nove identidade/quatro paginação29fixtures revertidas/seis lista/23HTTP/10finais/21handlers simulados=113; concorrência503 após5,12s pelo FOR UPDATE empresa, pg_blocking_pids/transactionid observado, pós-libertação409 e replay200/uma revisão apenas na regressão.58 preservação/ACL:43 conjuntos iguais, dois com linhas antigas iguais e append autorizado2, três bindings/diário27/Caixa950/cálculos5/seis registos/cache intactos. Identidade humana gravada em preparações novas; duas versões iniciais8085 preservadas sem reescrever. Lista projecta condições aprovadas da página e CSV distingue origem/administrativa; referência explícita no detalhe, não saldo histórico. Config root desligada, QA seleccionada habilitada. Matriz v18 produto amplo incompleto; visual/carga/implantação separados. Sem demos/SGC/efeitos financeiros/acessos reais/publicação/candidata/aceitação/checkpoint. Proposta anterior por autorizar é histórica. + + +- Autorização expressa10/11: «Autorizar o percurso QA e os dois direitos apresentados», `docs/SGF_PAYABLE_DUE_SCOPE.md`/JSON. Implementar percurso inteiro de vencimentos administrativos revistos, due.prepare13/due.review14, duas concessões por QA existente/quatro total, sem pedir por campo/teste. Exemplo100MZN/data artificial30/11, sidecar sem mutação de factura/obrigação/saldo/diário, demais limites do âmbito mantidos. A indicação anterior por autorizar é histórica. + + +- Próxima decisão concreta: `docs/SGF_PAYABLE_DUE_SCOPE.md`/JSON, vencimento administrativo revisto de obrigação individual, sidecar append-only sem alterar pa_obligations/factura/saldo. Dois novos poderes due.prepare13/due.review14 sob supplier_payables, duas concessões por livro existente/quatro total: por autorizar/não concedidas. SELECT-only confirma ausência nas duas bases. Exemplo proposto obrigação100MZN com evidência artificial e data30/11, atraso apenas por referência explícita; sem juros/pagamentos/propagação. Leitura não confere estes poderes. Preservar todos os exemplos, seis registos/cache; sem demos/SGC/publicação/candidata/checkpoint/aceitação/implantação. + + +- Posições de fornecedores entregues10/11, `docs/SGF_PAYABLE_POSITIONS_DELIVERY.md`, mesma8085 Financeiro. SQL25/100 por factura/fornecedor e estado documental, engine.positionsPage só IDs da página; fonte ambígua409, saldo desconhecido não zero, moedas separadas/resumo e CSV só página. Detalhe conserva document_id explícito e recusa403 sem fallback; command financeiro original idêntico após normalizar finais de linha.23 regressão/15 UI/19 HTTP=57, assets JS SHA/MIME incluídos; zero POSTs de negócio/direitos/schema.45 conjuntos actuais preservados incluindo catálogo novo/recibos, diário27/Caixa950/cálculos5; seis registos/cache/provas/trabalho alheio preservados. Vencimento/atraso continuam desconhecidos pelo motor original; não inferir prazos. Leitor composto conserva mutex; não alegar carga/optimização integral, visual ou implantação. Matriz v17 produto amplo incompleto; sem demos/SGC/publicação/candidata/checkpoint/aceitação. + + +- Cadastro de artigos entregue10/11, `docs/SGF_STOCK_CATALOGUE_DELIVERY.md`, mesma8085 Stock → Artigos e revisão do catálogo. Intenção/revisão distintas; criação no catálogo canónico sem saldo/movimento; código/unidade imutáveis e correcção de designação só dos novos, histórico/recibo atómicos. Duas concessões aprovadas por QA existente (stock.items.prepare2/review12), quatro total; sem novas concessões SQL/PUBLIC/roles.34 regressão/11 limites/22 HTTP/15 DOM→HTTP sem confirmações adicionais/três navegação=85, mais concorrência503 em5,22s→409/um artigo e replay200; dois checksums finais. Um QA-CATALOGO-INTEGRADO/UN, duas versões/recibos;38 conjuntos originais/três bindings e diário27/Caixa950/cálculos5 intactos, seis registos/cache preservados. Descoberta falha transversal: scripts novos devolviam HTML200 no router. Corrigida entrega de dez nomes exactos só8085, dependência sgf-stock-final incluída;17 limites/comparação corpo original e13 HTTP MIME/SHA/404. Ver `docs/SGF_CLIENT_ASSET_DELIVERY.md`. API/DOM anteriores não provavam entrega de scripts nem visual; não declarar ecrã validado por esses níveis. Router partilhado conserva comportamento das outras composições. Sem reinício/cache/demos/SGC/novos efeitos financeiros/publicação/candidata/checkpoint/aceitação/implantação; visual/carga separados. Matriz v16 produto amplo incompleto. + +- Autorização expressa10/11: «Autorizar o cadastro QA e os dois direitos apresentados», `docs/SGF_STOCK_CATALOGUE_SCOPE.md`/JSON. Criar/rever artigo e corrigir designação apenas dos novos, código/unidade imutáveis; stock.items.prepare QA2 e stock.items.review QA12, duas concessões por livro QA existente/quatro total. Um QA-CATALOGO-INTEGRADO/UN sem saldo inicial na8085. Sem artigos anteriores/movimentos/lançamentos/lotes/preços/SGC/demos/acessos reais/seis registos/cache/publicação/candidata/checkpoint/aceitação/implantação. Proposta anterior por autorizar é histórica; implementar/provar percurso inteiro sem pedir por campo/teste. + +- Continuação10/11: `docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md`, Financeiro → Posições de clientes/vencimentos na mesma8085. SQL25/100, pesquisa/estado/vencimento, efeitos confirmados actuais e totais por obrigação, proposta sem saldo reconhecido, desconhecido sem atraso zero; referência não é corte histórico. Passagem ao detalhe conserva ID explícito e não substitui por outro se indisponível; CSV da página e marcador entre páginas.30 regressão/16 UI/15 HTTP, correcções de vencimento revertidas, zero POST de negócio8085/direitos novos; dados/binding/sr_* intactos, diário27/Caixa950/cálculos5/seis registos/cache preservados. Cobertura fiscal v2 corrige descrições antigas sem activar motores. Matriz v15 produto incompleto. Próxima decisão concreta: cadastro/revisão de artigos, `docs/SGF_STOCK_CATALOGUE_SCOPE.md`/JSON; stock.items.prepare QA2 e stock.items.review QA12, duas concessões por livro existente/quatro total, **ausentes/por autorizar**, nada concedido. Escrita SQL nativa existente não é autorização de negócio; ligação SELECT-only permanece igual. Sem demos/SGC/acessos reais/publicação/candidata/checkpoint/aceitação/implantação; visual/carga separados. + +- Continuação10/11, `docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md`, mesma8085: Contabilidade tem submenus distintos e Análise balancete/razão com datas explícitas, conta escolhida por linha,25/100, saldo progressivo/totais originais e CSV apenas da página/escapagem. Mudança de marcador entre páginas exige nova leitura; impressão é nova consulta, não arquivo imutável. Adaptador HTTP corrige conta ausente500→403 e argumento inválido→422; engine/writers nativos intactos. Cinco regressão/19 HTTP/14 UI,32 conjuntos/binding preservados; zero direitos/efeitos financeiros/SGC/demos/seis registos/cache/publicação/candidata/checkpoint/aceitação/implantação. Código/testes antes de actualizar apresentação; não acoplar cada passo à8085. Leitor composto conserva mutex anterior; sem alegação de optimização de carga. Matriz v14 produto amplo incompleto, visual separado. + +- Continuação executiva10/11: início operacional e lista paginada de ordens entregues na mesma8085; `docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md`. Entrada descreve seis módulos; Início mostra percursos/evidências/funções autorizadas e consulta explícita das fontes existentes, sem totais inventados ou consultas entre módulos. Oficinas painel/equipa/relatórios usam novo GET `/api/workshop/orders`, SQL25/100 por pesquisa/estado, cliente/equipamento/responsável, apontamentos actuais/minutos, direito existente workshop.view e atribuição actual. Sem mutex de escrita durante esta leitura; teste regressão terminou0,093s enquanto primeira transacção mantinha empresa bloqueada. Writers/triggers e detalhe antigo preservados (detalhe ainda materializa universo).50 HTTP fontes/28 UI/11 entrada/três assets finais;29 regressão/11 HTTP/12 UI de ordens, fixtures27 revertidas, sem ordens novas na8085.32 conjuntos originais/binding intactos, diário27/Caixa950/cálculos5; zero concessões/efeitos financeiros/SGC/demos/seis registos/cache/publicação/candidata/checkpoint/aceitação/implantação. Matriz v13 mantém produto amplo incompleto, visual/carga próprios. + +- Cálculo QA por linhas/imposto incluído entregue10/11, `docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md`, mesma8085 `/fiscal/rules`.1–25linhas, modalidade/regra explícitas, imposto truncado por linha/base incluída residual, exactos/diferenças preservados. Três cálculos novos1000+160=1160 (sem/incluído) e86,21+13,79=100; dois antigos intactos.34 regressão/três recuperação/29 HTTP/16 DOM→HTTP=80 verificações, mais concorrência mesma chave503 após5,17s→200 replay/um cálculo; retirada de regra recusa409.26 conjuntos originais/binding e concessões intactos; diário27/Caixa950/IVA4/regras4/bancos7/indicadores4+2. Zero direitos/esquema/efeitos financeiros novos. Config root desligada, apenas QA aprovado na8085. Sem emissão/dívida/pagamento/SGC/demos/seis registos/cache/publicação/candidata/checkpoint/aceitação/implantação; visual/carga/fiscalidade geral separados. Indicações anteriores por autorizar/não implementado são históricas. + +- Autorização expressa posterior10/11: «Autorizar a modalidade QA e a política apresentada», `docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md`/JSON.1–25linhas, preço sem/com imposto, truncagem do imposto por linha, base documental incluída=bruto−imposto documental, exactos/diferenças racionais preservados, soma documental de linhas.100 incluído16%→86,21+13,79, diferenças9/2900; base não-cent no preço acrescido e bruto não-cent incluído ficam pendentes. Reutilizar fiscal_rules.view/prepare/review sem concessões; só cálculo/mapa interno nas mesmas8085/regressão, sem emissão/dívida/pagamento/lançamento/activação real/SGC/demos. Preservar27 lançamentos/Caixa950/IVA4/regras4/dois cálculos antigos/bancos7/indicadores4+2/seis registos/cache/provas/trabalho alheio. Não pedir aprovação por campo/teste do bloco. Sem aceitação/checkpoint/publicação/implantação. Indicação anterior por autorizar é histórica. + +- Próxima decisão preparada após indicadores: `docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md`/JSON, preço por1–25linhas e modo imposto incluído, reutilizando direitos fiscais existentes (zero concessões). Propõe truncagem de imposto por linha, base documental incluída=bruto−imposto documental, exactos/diferenças racionais preservados e soma de linhas. Casos400+600→base1000/IVA160/1160;464+696 incluídos idem;100 incluído→86,21+13,79 com diferenças9/2900. **Política/modalidade por autorizar, não implementada no endpoint**; protótipo matemático sem base apenas demonstra números. Não inferir esta decomposição do antigo caso de base simples. Sem emissão/dívida/pagamento/lançamento/SGC/demos/acessos reais/seis registos/cache, preservar todos os exemplos. + +- Indicadores QA entregues10/11, `docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md`, mesma8085 `/finance/indicators`: quatro eventos de critério13/14 e dois resultados conservados,950/100=19/2→9,50vezes;50/1000=1/20→5,00%. Fonte composta efectiva Caixa fechada/diário e saldo credor explícito, sem valor absoluto/critério automático/parecer real.41 regressão/12 SQL e fronteiras (origem artificial só no harness),29 HTTP efectivo/16 DOM→HTTP zero confirmações =98 verificações, mais concorrência503→409/uma revisão.23 conjuntos originais/binding e27 lançamentos/Caixa950/IVA4/regras4+cálculos2/bancos7 intactos. Sete concessões por QA exactas aplicadas. Rootflag desligada. Matriz v11 mantém critérios reais/rácios gerais/séries/modelos/motores legais pendentes. Sem novas transacções/efeitos financeiros/SGC/demos/acessos reais/seis registos/cache/reabertura/publicação/candidata/aceitação/checkpoint/implantação; progresso HTTP guardado e provas válidas reutilizadas. + +- Autorização expressa posterior10/11: «Autorizar os indicadores QA e os direitos apresentados», `docs/SGF_FINANCIAL_INDICATORS_SCOPE.md`/JSON de acesso. Três direitos financial_analysis.view/prepare/review, sete concessões por QA existente aos perfis13–17. Critérios13/revisão14/cálculo explícito/fontes/histórico/relatório dos indicadores limitados950/100=9,5 e50/1000=5%; não rácios gerais ou recomendação real. Origem artificial apenas no harness da regressão, HTTP8085 fontes efectivas. Implementar/provar/disponibilizar limitadamente no mesmo livro sem pedir por campo/teste. Preservar27 lançamentos/Caixa950/IVA4/regras4+cálculos2/bancos7/seis registos/cache/trabalho alheio. Sem novas transacções/efeitos financeiros, critérios/acessos reais, SGC/demos/bases/portas, reabertura/publicação/candidata/checkpoint/aceitação/implantação. Indicação anterior por autorizar é histórica. + +- Próxima lacuna preparada após bancos: `docs/SGF_FINANCIAL_INDICATORS_SCOPE.md`/JSON de acesso, critérios/indicadores QA950/100=9,5 vezes (Caixa/credor QA-FORNECEDOR) e50/1000=5% (despesa/entradas da Caixa), retratos/fontes/revisão distinta/histórico. Três direitos financial_analysis.view/prepare/review, sete concessões por QA existente aos perfis13–17: **por autorizar/não aplicados**. Não presumir poder de configurar critérios dos direitos de Caixa/bancos/fiscal/GL, nem adequação contabilística real ou rácio geral de liquidez/margem. Preservar27 lançamentos/Caixa950/IVA4/regras4+cálculos2/bancos7/seis registos/cache/demos; sem SGC/novos efeitos financeiros/operações ou acessos reais/reabertura/publicação. Origem artificial no harness da regressão explicitada; HTTP8085 provará fontes reais do QA. + +- Conferência bancária QA entregue10/11, `docs/SGF_BANK_RECONCILIATION_DELIVERY.md`, mesma8085 `/finance/bank`: um CSV artificial0−20+20=0, duas linhas de extracto, sete eventos conservados, correspondências13→14 individuais com linhas GL4/5 dos diários2/3 já existentes. Leitor nativo sem fallback; não declara QA-LIQUIDACAO conta bancária real.37 regressão/11 SQL e fronteiras (origem artificial injectada só no harness),28 HTTP nativo/15 DOM→HTTP zeroPOSTs/três relatório =94 verificações, mais concorrência503→409/uma revisão.20 conjuntos originais/binding e27 lançamentos/Caixa950/IVA4/regras4+cálculos2 intactos. Oito concessões por QA exactas aplicadas. Rootflag desligada, binding privado. Extracto finalizado sem reabertura, relatório derivado/checksum; importador geral/correcção de extracto recusado/arquivo binário legal não entregues. Matriz v10 mantém produto amplo incompleto. Sem novas bases/portas/demos/SGC/pagamentos/lançamentos/acessos reais/seis registos/cache/publicação/candidata/aceitação/checkpoint/implantação. Progresso HTTP guardado; não repetir confirmações válidas. + +- Autorização expressa posterior10/11: «Autorizar a conferência bancária QA e os direitos apresentados», `docs/SGF_BANK_RECONCILIATION_SCOPE.md`/JSON de acesso. Quatro direitos bank_reconciliation.view/import/prepare/review, oito concessões por QA existente aos perfis13–17. Extracto artificial0−20+20=0, importação/revisão distinta/correspondências individuais com duas linhas QA existentes20/20 na8085/relatório. Origem artificial no harness da regressão explicitada, leitor de produção sem fallback; HTTP8085 prova fontes efectivas. Implementar/provar/disponibilizar limitadamente sem pedir por campo/teste. Preservar27 lançamentos/Caixa950/IVA4/regras4+cálculos2/seis registos/cache/trabalho alheio. Sem novos pagamentos/lançamentos, natureza bancária real da conta de liquidação, SGC/bancos/APIs/credenciais reais, demos/bases/portas, reabertura, publicação/candidata/checkpoint/aceitação/implantação. Indicação anterior por autorizar é histórica. + +- Próxima lacuna preparada após regras: `docs/SGF_BANK_RECONCILIATION_SCOPE.md`/JSON de acesso, conferência de extracto artificial0−20+20=0 pelas duas linhas de liquidação já existentes (diários2/3, contaQA-LIQUIDACAO; não se declara conta bancária real). Quatro direitos bank_reconciliation.view/import/prepare/review, oito concessões por QA existente aos perfis13–17: **por autorizar/não aplicados**. Importação, revisão distinta, correspondências individuais/fundamento e relatório; sem novo pagamento/lançamento ou associação automática. Preservar27 lançamentos/Caixa950/IVA4/regras4+cálculos2/seis registos/cache/demos; sem SGC/bancos reais/reabertura/publicação. Não inferir estes poderes de Caixa/recebíveis/regras. + +- Núcleo comum de regras QA entregue10/11, `docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md`, mesma8085 `/fiscal/rules`. Quatro eventos de regra e dois cálculos conservados:1000×16%=160/1160 em30/11 e sucessora artificial17%=170/1170 em01/12; sem alteração legal. Avaliador declarativo do portal reutilizado localmente por SHA, limites24/8 e base16/8, sem dependência runtime. Só dois DAGs QA fixados executam; referências legais pendentes não se activam.39 regressão/oito paridade/14 SQL e fronteiras/24 HTTP/14 DOM→HTTP/três entrada,102 verificações, mais concorrência503→409/uma revisão.17 conjuntos originais/binding e27 lançamentos/Caixa950/quatro versões IVA intactos. Sete concessões exactas por QA aplicadas após aprovação. Rootflag desligada; sourceSHA privada. Sem demos/SGC/novos efeitos financeiros/acessos reais/seis registos/cache/publicação/candidata/aceitação/checkpoint/implantação. Reutilizar provas, não repetir confirmações; progresso HTTP guardado. Restantes motores fiscais/modelos/calendário/remunerações/reconciliação/rácios pendentes na matriz v9. + +- Autorização expressa posterior10/11: «Autorizar o núcleo comum QA e os direitos apresentados», `docs/SGF_FISCAL_RULES_COMMON_SCOPE.md`/JSON de acesso. Três direitos fiscal_rules.view/prepare/review, sete concessões por QA existente aos perfis13–17 discriminados. Catálogo/revisão distinta/cálculo explicável por versão/vigência, caso artificial1000×16%=160 até30/11 e sucessor artificial17%=170 desde01/12, sem mudança legal presumida. Implementar/provar/disponibilizar limitadamente na mesma8085/regressão; sem novo pedido por campo/fronteira/teste do contrato. Preservar27 lançamentos/Caixa950/quatro versões IVA/documentos/seis registos/cache/trabalho alheio. Sem outros motores legais/folha salarial/acessos reais/novos efeitos financeiros/SGC/demos/bases/portas/publicação/candidata/checkpoint/aceitação ou capacidade de implantação. Indicação anterior por autorizar é histórica. + +- Próximo bloco preparado após IVA QA: `docs/SGF_FISCAL_RULES_COMMON_SCOPE.md`/JSON de acesso, núcleo comum de fontes/regras versionadas e cálculo artificial1000×16%=160 até30/11, sucessor artificial17%=170 desde01/12, sem afirmação legal ou alteração de documento existente. Três direitos fiscal_rules.view/prepare/review, sete concessões por QA existente aos perfis13–17: **por autorizar e não aplicados**. Não inferir da autorização de livro/mapa poder de manter regras transversais. A proposta não conclui motores IRPS/IRPC/ISPC/ICE/INSS/modelos oficiais/remunerações. Mesma8085/regressão, preservar27 lançamentos/Caixa950/quatro versões IVA/seis registos/cache, sem demos/SGC/novos efeitos financeiros/publicação. + +- Livro/mapa IVA QA entregue10/11, `docs/SGF_IVA_ASSESSMENT_DELIVERY.md`, mesma8085 `/fiscal/iva`: FR existente1000/base862,07/IVA137,93 escolhido, livro/mapa preparados13 e revistos14; quatro versões conservadas/CSV congelado/SHA, dedução e imposto a pagar indeterminados.38 regressão revertidas, concorrência503→409/uma revisão (harness POST corrigido, evidência inicial conservada só na regressão), dez SQL/paginação revertidas,23 HTTP/quatro confirmações e14 DOM→HTTP/zero confirmações.15 conjuntos originais e27 lançamentos intactos; Caixa950 fechada/período aberto.14 concessões exactas por QA aplicadas conforme aprovação; rootflag desligada. Matriz v8 continua produto amplo incompleto: mapa interno não é Modelo A oficial, IVA geral/IRPS/IRPC/ISPC/ICE/INSS/calendário/reconciliação/rácios pendentes. Sem seis registos/cache/demos/SGC/novos efeitos financeiros/direitos reais/publicação/candidata/checkpoint/aceitação ou implantação. Não repetir provas válidas nem confirmações; progresso HTTP guardado. + +- Autorização expressa posterior10/11: «Autorizar o livro/mapa IVA QA e os direitos apresentados», conforme `docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md`/JSON de acesso. Seis direitos fiscal_assessments.view/prepare/review e fiscal_forms.view/prepare/review,14 concessões por base QA existente aos perfis13–17 discriminados. Ciclo completo origem FR1000/base862,07/imposto137,93 → livro revisto distintamente → mapa interno QA preparado/revisto/recuperável, sem apuramento completo/declaração oficial. Implementar/provar e disponibilizar limitadamente na8085/regressão existentes, sem repetir autorização por campo/teste; preservar27 lançamentos, Caixa950 fechada, seis registos/cache/provas/trabalho alheio. Sem novos motores fiscais, lançamentos, acessos/operações reais, SGC, demos/bases/portas, publicação/candidata/checkpoint/aceitação ou capacidade de implantação. A indicação anterior «por autorizar» é histórica. + +- Continuação10/11: conferência do documento de impostos identifica backlog amplo FIS-SGF13–19 por concluir, separado do fecho QA dos seis módulos. `docs/SGF_FISCAL_COVERAGE_DELIVERY.md`: painel de cobertura no leitor fiscal existente, protecção de respostas atrasadas/contexto e rodapé corrigido; duas funções Financeiras já autorizadas visíveis no dropdown Contabilidade, sem concessões.17 regressão/10 HTTP/sete renderização/três contexto/três entrada;11 tabelas e diário27 intactos. Inventário12 fontes do portal por hash sem transferência/activação legal nem dependência runtime. Matriz v7 explicita motores/modelos/reconciliação/rácios pendentes. Próximo bloco `docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md`, seis direitos/14 concessões por base QA existente conforme JSON de acesso: **por autorizar, não aplicado**. Sem novos direitos/schema/demos/SGC/cache/associações/publicação/checkpoint. Uma tentativa da aba integrada por URL conhecido terminou em timeout30s; não repetir ou contornar, visual pendente. + +- Continuação executiva10/11: relatório Produtos sem movimento entregue na mesma8085, `docs/SGF_STOCK_INACTIVITY_DELIVERY.md`. Corte UTC/dias explícitos, pares com história conhecida, quantidade exacta, filtro literal/armazém e SQL25/100; cinco SELECTs de projecção, revisão/contexto600s.23 provas de regressão (fixtures SQL de relatório, não workflows),15 HTTP SELECT-only e sete cliente; metadados/movimentos/diário27 da8085 intactos. Sem novos direitos/roles/bases/demos/índices/writers/SGC/cache/publicação/candidata/aceitação/checkpoint. Seis registos preservados. Carga/visual/implantação separados. Matriz v6 acrescenta consulta sem alterar aceitação histórica. + +- Caixa QA entregue na mesma8085 após aprovação, `docs/SGF_CASH_DELIVERY.md`: abertura0, custódia explicitamente classificada do recebimento existente1000, despesa50, contagem/revisão/fecho950; nove eventos append-only, declarante13/revisor14 distintos. Nenhuma factura/recebível/afectação nova ou modificada. Contabilidade15/16 reclassifica1000 e reconhece50 em duas origens individuais no mesmo writer; diário27 equilibrado e25 anteriores idênticos.23 verificações revertidas, concorrência503→409/uma classificação só na regressão, sete fronteiras/intervalos,21 HTTP incluindo duas páginas25+2 do diário e recibo recuperado noutra sessão,14 DOM→HTTP exclusivamente consulta/recuperação (zero POSTs), oito de período com38 factos/zero pendentes e fecho em rollback; Caixa conservada fechada, período QA aberto sem reabertura. Quatro direitos/oito concessões aplicados exactamente nas duas bases QA existentes aos perfis13/14/15/16/17. Configuração root desligada; só8085 seleccionada disponibilizada após provas. Backups/dados privados fora do repositório, comparação inclui documentos/movimentos/SR/binding. Seis registos/cache/demos/trabalho alheio intactos; sem SGC, dinheiro/acessos reais, bancos/POS, ajuste automático/reabertura/publicação/candidata/checkpoint/aceitação ou capacidade de implantação. Referências de que Caixa aguarda autorização/não implementada são históricas. Visual/carga/conformidade/implantação continuam separados. + +- Autorização expressa posterior10/11/2026: «Autorizar o ciclo Caixa QA e os direitos apresentados», `docs/SGF_CASH_NEXT_SCOPE.md`/`docs/integration/sgf-cash-access-proposal.json`. Quatro direitos cash.view/prepare/review/close, oito concessões por base QA existente aos perfis13/14/15/16/17 discriminados; sem nova função nativa/PGrole/acesso real. Ciclo0+classificação explícita de recebimento fictício existente1000−despesa50=contagem/fecho950, preparação/revisão distintas, duas origens no mesmo diário e contas QA-CAIXA/QA-DESPESA-CAIXA. Implementar/provar e disponibilizar limitadamente na mesma8085; preservar25 lançamentos, seis registos/cache/trabalho alheio. Sem novas demos/bases/portas, dinheiro real/banco/SGC, ajuste automático, reabertura, publicação/candidata/checkpoint/aceitação. Não repetir pedido por campo/teste deste bloco. + +- Continuação executiva10/11/2026: registo unificado entregue na8085, `docs/SGF_BILLING_REGISTER_DELIVERY.md`. Comercial → Todos os documentos/Facturas inclui seis famílias autorizadas, factura-recibo sem duplicar factura técnica, filtros anteriores no servidor, páginas25/100, bases canónicas e saldos apenas da página/com direito financeiro; sem somar documentos heterogéneos.33 verificações em regressão com127 metadados e uma fixture estrangeira revertidos;16 HTTP, diário25 intacto, nenhum comando de negócio/direito/schema/índice novo. Componentes carregados diferidamente; visual não presumido. Próxima lacuna identificada: Caixa, não há motor/concessões existentes. `docs/SGF_CASH_NEXT_SCOPE.md`/`docs/integration/sgf-cash-access-proposal.json` apresentam quatro direitos/oito concessões QA por base e ciclo0+1000−50=950, duas origens no mesmo diário; **aguarda autorização, não implementado nem concedido**. Não presumir aprovação a partir da directiva executiva; apresentar novos direitos antes de aplicar. Mesma8085/regressão, seis registos/cache/provas/trabalho alheio preservados; sem demos/SGC/publicação/candidata/checkpoint/aceitação. + +- Continuação após as dez consultas autorizadas10/11/2026: `docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md` entregue na mesma8085. Recibo ligado à afectação efectiva400, crédito nominal100 sobre factura nova1000 deixando500, factura-recibo de outra origem1000/recebimento/afectação atómicos deixando0; PDFs congelados/SHA e três versões por documento. Mantém emissor/recebíveis originais, declarante13 e executor14 distintos sem trocar identidade. Diário25 equilibrado (cinco origens novas),29 factos/zero pendências no período; fecho apenas em rollback, instalação aberta. Anulação técnica preparada/revista distintamente: reverte afectação ou redução sem apagar factura/PDF, sem reembolso; recusa período fechado, novos efeitos exigem seus lançamentos.17 verificações de reversão em rollback,20 documentos/diário, concorrência503→409/uma emissão,21 HTTP,19 DOM→HTTP com confirmação simulada e oito de período. Exemplos emitidos conservados; zero anulações confirmadas na8085. Dez consultas aplicadas só nos dois perfis QA das duas bases existentes; bloqueio de direitos anterior ultrapassado. Originais18/clientes/facturas/movimentos/binding comparados intactos; seis registos/cache não tocados. Sem SGC, nova demo, publicação/candidata, direitos/operações reais, aceitação/checkpoint ou capacidade de implantação presumidos. Visual/carga/conformidade real permanecem separados. Notas anteriores de documentos candidatos/bloqueio são históricas. + +- Autorização posterior10/11/2026: «Autorizar as10 concessões de consulta QA», conforme `docs/integration/sgf-document-read-access-proposal.json`. Apenas os perfis Financeiros QA13/14 existentes nas duas bases QA existentes, dez concessões/seis nomes de consulta discriminados, sem edição fiscal/novos perfis/acessos reais/PUBLIC. Retomar e validar a factura-recibo do bloco já autorizado; não voltar a pedir esta decisão. Mesma8085, sem novas demos/SGC/publicação, seis registos/cache preservados. + +- Continuação10/11/2026, após a autorização do bloco: agenda/reagendamento e cadastro/reutilização de equipamentos entregues na8085 com direitos existentes; orçamentos Comercial1000/Oficina500 com revisão distinta, aceitação fictícia documentada e conversão explícita em rascunhos, sem emissão por aceitar. Ver `docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md`. Políticas Stock e devolução ao fornecedor entregues na8085, ver `docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md`: artigo QA novo5, entrada10UN/200, saída2UN/40 pelo writer nativo, saldo8UN/160. Diário passa18→20 equilibrado;18 lançamentos/movimentos/clientes/facturas originais comparados e intactos.22 provas Stock revertidas, concorrência503 durante lock de tenant nativo e409 após libertação sem duplicação,11 HTTP e17 DOM→HTTP (confirmação simulada, prévia real revertida); não confundir DOM com visual. Direitos do bloco exacto aplicados apenas nas duas bases QA existentes. Preservar antigos livros/seis registos/cache e trabalho alheio; sem publicação, SGC, nova demo ou checkpoint/aceitação presumidos. + +- Documentos adicionais desse bloco continuam candidatos **não instalados nem activados na8085**: Recibo400 e redução100 da nova factura1000 deixando500 passaram16 verificações em rollback na regressão, incluindo versões/permissões/duplicação/falha e recuperação; factura-recibo ainda sem prova positiva/HTTP/UI/ligação integral ao diário. Bloqueio concreto:10 concessões de consulta existentes ausentes nos perfis Financeiros13/14, fora da lista exacta anteriormente aprovada; ver `docs/SGF_DOCUMENT_READ_ACCESS.md` e `docs/integration/sgf-document-read-access-proposal.json` (approved=false). Não conceder implicitamente, não contornar as guardas documentais nem usar identidade de outro actor; implementação de finalização preparada ainda não validada e desligada. Revisão visual, carga sustentada e implantação continuam separados. + +- Autorização executiva posterior10/11/2026 «Autorizar o bloco QA e os direitos discriminados», `docs/SGF_NEXT_BLOCK_RIGHTS.md`/`docs/integration/sgf-remaining-functions-access-proposal.json`: orçamentos Comercial/Oficina com revisão/aceitação documentada/conversão explícita, recibo/nota de crédito/factura-recibo de teste e respectivas ligações ao recebível/mesmo diário, políticas/mínimos Stock e devolução física ao fornecedor pelos motores originais.15 novos direitos host, service_invoices.issue adicional a finance_review, oito concessões nativas (seis nomes existentes) apenas nos perfis QA discriminados e empresa2/unidade2; sem warehouse genérico ao revisor. Novas fontes fictícias: quoteComercial1000/Oficina500; nova factura1000/recebimento400/crédito100/saldo500; outra factura-recibo1000; artigo novo10UN/taxa20/devolução2UN40/saldo8UN160. Mesma8085 e regressão existente, sem novas demos/bases/portas/perfis/rolesPG. Preservar18 lançamentos/exemplos anteriores/seis registos/cache/trabalho alheio; sem direitos/operações reais, fiscalidade certificada/AT/banco/envio de mensagens, reabertura, publicação/candidata/checkpoint/aceitação presumidos. Abrange implementação/provas e disponibilização limitada após testes; não pedir nova aprovação por cada campo/teste deste mesmo bloco. + +- Directiva de navegação posterior10/11/2026: utilizador pediu módulos horizontalmente e «use o dropdown para melhor facilidade». Entrada8085 `/sgf` reorganizada em seis dropdowns, consulta directa e funções QA por módulo;17 perfis originais preservados em secção separada. ShellReact também tem dropdown por módulo com funções autorizadas, Início/opções gerais separados; escolha de função muda explicitamente para o módulo escolhido, inclusive cadastros partilhados, pelo mesmo ContextTransition/POST de contexto e guardas de formulários/confirmações. Sem novas concessões ou comandos de negócio. Utilizador disse «agora consigo entrar» após a dificuldade inicial; não equivale a aceitação visual dos cinco módulos ou dos novos dropdowns. Guião actualizado; conferir prova dirigida de menus, não reexecutar percursos financeiros/Compras por alteração de navegação. + +- Consolidação final funcional10/11/2026, `docs/SGF_SIX_MODULE_CLOSURE_20261110.md` e `docs/SGF_GUIAO_APRESENTACAO.md`: Oficina–Stock/devolução/valorização100/50/25 e Oficina–Comercial500 efectivas na8085; trânsito1UN recebido0,4/0,6 distintamente, propriedade1,5UN/75 conservada; novo artigo de lotes4→3,5UN/70, diferença10 e entrada80 pelo mesmo diário.18 lançamentos/15 contas;16 anteriores idênticos, Stock145MZN. Fecho QA completo18factos/zero pendentes executado com rollback, período aberto sem reabertura.27 provas trânsito/lotes, concorrência dois processos503 durante bloqueio da linha de autor host→409 versão antiga após libertação→nova conferência restante sem duplicação;6 reconciliação,6 recuperação/retorno,12 preservação,20 fronteirasHTTP,10DOM→HTTP (confirmação simulada)/9 cliente. Não somar passagens repetidas ou antigas. Modo nativo de perfis QA activado com funções legadas preservadas, sete concessões e dois direitos apenas QA aprovados; sem warehouse genérico ao revisor, PGroles/PUBLIC/acessos reais. Arranque/reinício PHP29tabelas anterior reutilizado; não repetir. Utilizador confirmou «Abre e está legível» para novo Stock; isso não é revisão dos restantes ecrãs nem aceitação integral. Controlo de navegador interrompido por URL indeterminado, não contornar. Preservar seis registos/cache/Compras/provas/trabalho alheio; sem demos/SGC/publicação/candidata/checkpoint. Carga sustentada/implantação/reinícioOS e funcionalidades opcionais fora da matriz conservam limites próprios; não tratar as funcionalidades acima como por implementar. + +- Autorização expressa de10/11/2026 «Autorizar este fecho QA de Stock», `docs/SGF_STOCK_FINAL_SCOPE.md` e `docs/integration/sgf-stock-final-access-proposal.json`: concluir trânsito com recepção distinta e lotes/contagem/ajuste valorizado na mesma8085 e regressão existente. Dois novos direitos SGF apenas QA (`stock.transfers.receive` destino1 e `stock.lots.review` novo artigo/armazém3) para workshop_verify, sete funções nativas discriminadas para actores6/3, sem função genérica warehouse para revisor. Transferir1UN mantendo propriedade1,5UN/75; novo artigo4UN→3,5UN, taxa20, valor80→70 e diferença10. Mesmo livro físico/diário, revisão distinta e fecho com rollback; preservar16 lançamentos anteriores, C01–C10, seis registos/cache/provas/trabalho alheio. Sem novas demos/bases/portas/rolesPG/acessos reais/SGC/publicação/candidataCompras/checkpoint. Autoriza implementação/concessões QA/dados/regras e disponibilização limitada após provas, sem nova aprovação por campo/teste. Visual continua separado e bloqueio do controlo de navegador não será contornado. + +- Adenda da mesma continuação10/11/2026: catálogo de origens do diário8085 agora paginado25/100 no servidor, filtro por família e escolha individual, `ComposedLedgerCandidates`; três ficheiros instalados com backup.127 fixtures de metadados e uma estrangeira revertidas na regressão, oito páginas/dois SELECTs por página,275 asserções (não275 percursos), duas leituras HTTP da8085 aprovadas. Não repetir provas anteriores por rotina. Stock filtra tipo após página física, podendo ter página vazia. FechoQA/ligação física Oficinas–Stock/devolução/valorização seleccionada e demais limitações do relatório continuam pendentes; a autorização vigente abrange continuar esses trabalhos sem nova decisão por campo/teste. + +- Continuação autorizada de10/11/2026, `docs/SGF_SINGLE_INTEGRATION_20261110.md`: entrada única8085 `/sgf`, dez perfis QA mais sete Compras preservados, flags SGF limitadas activadas após provas dirigidas. Comercial1000, recebível400/600/reversão400; obrigação nativa original100→80→100 pela afectação/reversão20; custo275→295 e industrial1800→1820, residual300 separado.13 lançamentos equilibrados no mesmo livro nativo, âmbito SGF1/2/2 explicitamente ligado à origem financeira1/0/0, sem dívida no host. Operações financeiras e13 lançamentos por HTTP8085 com identidades distintas; preparação das fontes pelos workflows, não revisão visual.92 verificações dirigidas indexadas; provas anteriores reutilizadas. Autoload privado corrigido;52 ficheiros/build privado instalados com backup, bootstrapSGC/pacote fixado Compras intactos. Stock operações disponível, mas Oficinas→mesmo livro físico/devolução e cenário2/-1/+0,5/valorização ainda pendentes; não presumir essa ligação nem fechoQA. Origens materiais artificiais separadas por perímetro privado fixado; desconhecidas/partilhadas bloqueiam. Catálogo de origens contabilísticas além25 e atalhos antigos ainda exigem continuidade, visual/arranque/implantação separados. Preservar seis registos/cache/exemplos/provas/trabalho alheio; sem novos acessos reais/rolesPG/bases/portas/demos/publicação/candidataCompras/SGC/checkpoint. A autorização integral anterior continua vigente; não pedir outra decisão por cada campo/teste do mesmo recorte. + +- Autorização expressa de10/11/2026 «autorizo» ao recorte integral `docs/SGF_8085_NEXT_AUTHORIZATION.md` e matriz `docs/integration/sgf-8085-access-proposal.json`: dez perfis exclusivamente de aplicação QA na empresa2/unidade2; novos direitos Stock propostos e restantes direitos existentes discriminados, segundo armazém/artigo sintéticos; preparação dos documentos de teste; continuação da obrigação original100MZN com liquidação/afectação20, reversão e Contabilidade; cenários Comercial1000/400+600, custo275→295, industrial1800→1820 e stock2UN/100,-1UN/50,+0,5UN/25 na mesma8085. Um diário comum no livro financeiro nativo existente com âmbito SGF explícito, sem copiar dívida e sem serviço SGC. Contas/período/revisão/fecho QA e activação limitada após provas afectadas, sem presumir aceitação. Substitui as exclusões anteriores apenas neste recorte sintético; não pedir nova aprovação por cada campo/teste do mesmo recorte. Preservar seis registos, cache bloqueada, provas, exemplos anteriores e trabalho alheio; sem roles PostgreSQL/contas/acessos reais, dinheiro/banco/emissão fiscal, direitos públicos, reabertura, nova base/porta/demo, publicação ou candidata de Compras. Visual e capacidade de implantação exigem evidência própria. + +- Pedido expresso de10/11/2026 «concluir e integrar os seis módulos na instalação8085»:70 tabelas aditivas instaladas no host existentecc50a276c10c por `scripts/sgf-compose-install.php`, com dump/cópias privados, preservação de linhas/sequências/permissões/associações;44 funções novas sem PUBLIC EXECUTE. Código/guardas instalados com `sgf_single_installation.enabled=false`; nenhuma conta/concessão/efeito financeiro criado. Stock código instalado inactivo145dfc19fd0971639778, acrescentando contagem sem lotes/aplicação de diferença/equipamento;38PG/15DOM e seis HTTP dirigidos, sem repetir percurso de negócio.32 provas de schema e nove de ligação Comercial→Oficinas reutilizadas; livro de regressãob409 também composto sem base/servidor novo.8085 estava parada e foi novamente iniciada na mesma porta; `/compras`200, recepção e obrigação100MZN preservadas. Build privado válido reutilizado, cache não limpa. Guardas SQL presas ao DB/role/cluster exactos; resets/destruição recusados por evento antes de executar comando. Ver `docs/proofs/sgf-six-module-composition-20261110.json`. **Pendente de decisão**, não presumir aprovação: `docs/SGF_8085_NEXT_AUTHORIZATION.md`/matriz `docs/integration/sgf-8085-access-proposal.json`: dez perfis de aplicação QA, dois direitos novosStock, segundo armazém/artigo de ensaio e continuação sintética financeira/contabilística da obrigação100MZN antes excluída. Diário único deve conservar obrigação nativa, sem copiar dívida para host ou exigir serviço SGC. Seis registos/cache/provas/direitos reais preservados; sem activação/publicação/checkpoint. Stock por lotes/devoluções, composição financeira, visual e capacidade de implantação não declarados concluídos. + +- Continuação de10/11/2026: código de saídas/transferências SGF preparado, `docs/SGF_STOCK_OPERATIONS_DELIVERY.md`. Mesmo `StockWorkflow`/writer original fixado; prévia revertida, saldo/contexto/impacto revalidados, confirmação e recibo auditado atómicos, recuperação sem duplicação.30 verificações PG no livro existenteb4093c6f6707, todas as linhas repostas por rollback, sequências apenas QA podem avançar;11 DOM dirigidas, sem revisão visual. Nenhuma alteração/instalação/activação na8085, base/porta/demo nova ou acesso real. Dois direitos novos **apenas propostos**: `stock.operations.prepare` e `stock.operations.confirm`, por empresa/unidade/armazém; testados só em transacção revertida, sem concessão persistida. Flag `stock_operations.enabled=false`, mapa adicional opcional fixado por SHA sem alterar bindings Compras. Transferência imediata atómica não é recepção em trânsito. Contagens/ajustes/devoluções/selector de equipamento e composição integral continuam lacunas explícitas; teste concorrente de dois processos, carga/visual/implantação não declarados. Preservar seis registos/cache/provas/trabalho alheio e não retomar o incidente de navegador Portal como dependência do SGF. + +- Directiva prioritária de10/11/2026: **concluir todos os seis módulos SGF e entregar uma solução autónoma para apresentar a potenciais clientes**, `docs/SGF_SOLUTION_DELIVERY.md` e contrato `docs/integration/sgf-solution-contract.json`. Não retomar SGC/conector como prioridade ou dependência do produto. Uma instalação, uma entrada, mesma empresa/unidade, Comercial/Compras/Stock/Oficinas/Financeiro/Contabilidade funcionais e percursos completos; menus/catálogos/fontes/testes isolados não equivalem a fecho. Baseline READ ONLY de8085 confirmou estruturas de host em falta em quatro módulos e UI/operações de Stock/Contabilidade incompletas; só a prova `sgf-solution-closure-baseline-final-20261110.json` é válida (primeira projecção tinha nomes de tabela/prova incorrectos, corrigidos contra declarações reais). `scripts/check-sgf-solution.mjs` é gate interno e devolve2 sem aceitação integral, nunca uma declaração automática de implantação. Prioridade: composição/schema/guardas/perfis → fechar funcionalidades → percursos integrados → visual/arranque/recuperação/guião → aceitação única. Preservar seis registos/cache/provas/trabalho alheio/direitos reais, sem novas demonstrações/publicação/activação implícita. Meta não é mais uma entrega parcial de conector nem apenas documentação. + +- Continuação de10/11/2026 expressamente autorizada: sourceSGC ecrã/persistência auditados entregues, `docs/SGC_INVOICE_WORKSPACE_DELIVERY.md`; novo pacote inactivo3bb99e866f52965dbb7d (8SGF/15cliente-SGC), antigo preservado.20 casos únicos/142 asserções SQLite+14 verificações PG entre sourcebookb4093c6f6707 e receiverda77bb730751 existentes. Duas facturas pendentes por writer original, recuperação após commit/resposta perdida e concorrência uma decisão/envio+409; nenhum consumo/stock/pagamento extra. SóQA recebeu duas tabelas de rascunho/eventos; migraçãoSGC000001 de10/11 NÃO aplicada no piloto. Sourcewriter exclui documento reservado, intenção/decisão/cut persistidos antes da rede, lease120s e auditappend-only. Flagsworkspace/transportefalse, mapeamentos/principais não escolhidos, sem novos direitos reais/creds/demo/portas/bases.8085 código/estado/dados/sequências, seis registos/cache/provas/trabalho alheio preservados. Tipos/compilaçãoTSXprivada/Blade passaram. Não reabrir ecrã/persistência como lacuna: próximos passos são instalação exacta/configuração/activação aprovadas e ensaioTCP na mesma8085, visual/carga/implantação separados. Não declarar todoSGF concluído ou activar/publicar/criar checkpoint por inferência. + +- Preparação expressa de10/11/2026 da ligação SGC→8085, ver `docs/SGC_8085_INTEGRATION_PREPARATION.md`: cliente portátil/gateway SGC desligados, mapeamentos/principal não escolhidos e pacote de revisão `d09aac9d09da98e61df5` fora da aplicação (oito ficheiros de endpoint/três do cliente). Não instalado/activado; `prepare-sgc-8085.mjs` só check/prepare, sem aplicar.16 Unit/46 asserções novas e seis verificações de contrato kernel no livro de regressão existente;78/63 provas anteriores reutilizadas. Nenhuma base/role/demo/porta/credencial/concessão nova nem escrita na instalação8085/pilotoSGC. Só proposta de origem `unregistered_draft`/autoridade única SGF; `Invoice` SGC existentes excluídas. Ecrã/persistência auditada da intenção/decisão/recibo, mapeamentos/contas privados aprovados, instalação desligada e teste de rede nessa mesma8085 continuam pendentes antes de uso. Preservar seis registos/cache/provas/trabalho alheio; sem publicação, candidata ou checkpoint presumidos. + +- Pedido de10/11/2026: endpoint de facturas SGC implementado, ver `docs/SGC_INVOICE_ENDPOINT.md` e `docs/proofs/sgc-invoice-endpoint-20261110.json`. `bootstrap/app.php` monta apenas `routes/sgc.php`: GET da ordem/versão e POST preview/confirm. `config/sgc_invoice.php` permanece `enabled=false`, `principals=[]`; sem activação/credenciais/concessões reais. Middleware fixa pessoa/contexto por principal, revalida filiação/módulo/direitos existentes e usa sessão só em memória. Factura pendente pelo mesmo InvoiceController/ReceiptRuntime/workflow/writer; sem sincronização automática de fornecedor, espelho SGC, revisão/consumo/obrigação/pagamento/lançamento. UUID serializado por advisory lock, recibo nativo atómico, conteúdo/origem diferentes409, bloqueio5s503/recuperação200, operador nativo e manutenção revalidados também na repetição.78 verificações kernel HTTP/PostgreSQL/processos PHP (42+20+10+6), sem repetir suites antigas. Livro de regressão existente `da77bb730751`, sem novo servidor/porta/base/role/demo; fixture SQL de origem marcada como tal e quatro facturas pendentes pelo writer.8085: todos os dados/sequências e código preservados; seis registos/cache/provas/trabalho alheio intactos. Serviço/rotas legados de pagamentos/analytics não montados. Fonte continua QA-only; activação/TLS operacional/visual/carga/implantação separados. `node scripts/sgc-invoice-check.mjs` só lê a prova válida; não usar `--new` sem alteração/falha que justifique outro ensaio. Sem publicação/checkpoint. + +- Pedido expresso de10/11/2026 (migrations/calendário/cache/teste SGC): ver `docs/SGC_PREPARATION_20261110.md`. Apenas migrations SGC000001/000002 de09/11 aplicadas em `sgf_tempo_dev`/55432, lotes18/19, com backup privado e86 tabelas anteriores de dados/concessões/cache idênticas. Migration antiga130000 continua pendente; não executar migrate global por consequência. Calendário `sgc:populate-dim-tempo` implementado,2026–2035/3652 dias, repetição0 inserções, quatro testes/17 asserções; feriados não certificados. `config:clear`/`route:clear` explicitamente pedidos e executados; não confundir com autorização de `cache:clear`/cache de dados. POST SGC404, rota não registada, payload e classes ausentes, autenticação/isolamento/idempotência incompletos: integração NÃO activada nem teste positivo declarado. Sem pagamentos, concessões explícitas novas, candidatas, publicação ou novas demos.8085/percurso nativo/seis registos/provas/trabalho alheio preservados; esta preparação não substitui o motor existente. + - Decisão expressa de04/11/2026: concluir Compras numa única instalação e **não criar mais demonstrações isoladas**. Instalação de trabalho escolhida e actualizada no lugar:8085, entrada `http://127.0.0.1:8085/compras`, estado privado `receipt-composition-cc50a276c10c`. Continuar aí; não abrir outra porta ou base de demonstração por incremento. Ver `docs/PURCHASING_COMPLETION.md` e `docs/COMPRAS_USO_LOCAL.md`. C01–C10 concluídos no percurso aquisição → obrigação: fornecedor novo/revisão, requisição/submissão/aprovação, três propostas/seis avaliações, adjudicação/aprovação, ordem, recepção, factura/validação, preparação/reconhecimento distinto. ExemploREQ-2026-000003/OC-2026-000001, facturaFACT-COMPRA-COMPLETA-20261104, principal/saldo100MZN e movimento1UN. Entrada com funções existentes/CSRF/guardas físicas, painel de documentos, prévias legíveis, retenção da origem ao trocar função e protecção de campos/confirmações em curso.63 verificações dirigidas:13 percurso UI→HTTP real,22 leituras/entrada,10 cliente,12 dados canónicos READ ONLY, duas comparações com dump inicial, três passagens de função e uma observação do mesmo movimento na API de Stock; sem repetir comandos concluídos ou suites anteriores. DOM simulado não é revisão visual. Writers/controllers/políticas/pacotes fixados preservados; orçamento do host e concessões idênticos ao backup. Código/dumps privados fora do repositório; sem novas bases/roles/portas/demos.8085 agora contém o exemplo concluído; isso substitui a sua condição inicial vazia, sem alterar8084/anteriores. Preservar seis registos/cache/trabalho alheio; sem implantação/concessões reais, pagamentos/contabilidade desta origem, emissão fiscal, candidata de Compras, publicação, aceitação/checkpoint presumidos. `scripts/abrir-compras.ps1` reutiliza a instalação; foi conferido com8085 já activa, não com reinício completo do sistema. Visual/carga sustentada/implantação permanecem separados. - Continuação de 03/11/2026: `SGF-STOCK-PERIOD-REPORT-QA-v1` entregue; ver `docs/SGF_STOCK_PERIOD_REPORT.md`. Stock → Relatórios → Movimentos: resumo físico por artigo/armazém, abertura/entradas/saídas/fecho, período explícito por registo UTC, metadados actuais, sem total entre unidades. SQL/páginas25/100, referências/contexto/revisão, direitos e fonte SELECT-only existentes; flag desligada por omissão, sem direito novo.52 verificações (28 API/SQL,12 UI, duas de consultas, três guardas directas, sete demo).105 pares/114 movimentos iniciais são fixtures SQL de relatório, não operações do workflow; um append adicional só na regressão para revisão. Cinco SELECTs na projecção para25 e100 linhas, excluindo host/middleware/guardas; não é carga/memória/implantação. Demo8085 nova vazia;8084/anteriores, seis registos/cache/provas/trabalho alheio preservados. Sem writer/efeito financeiro novo, índices no destino, concessões reais, instalação, activação, publicação, aceitação/checkpoint. UTC da origem, unidades coerentes/histórico, exportação e restantes relatórios exigem seus recortes; visual/carga sustentada/implantação separados. diff --git a/README.md b/README.md index 802629e..0af91e1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # SGF TEMPO +**Entrada dos seis módulos na mesma instalação:** http://127.0.0.1:8085/sgf · [Guião de apresentação](docs/SGF_GUIAO_APRESENTACAO.md) · [Estado funcional e limitações](docs/SGF_SIX_MODULE_CLOSURE_20261110.md). Comercial, Compras, Stock, Oficinas, Financeiro e Contabilidade integrados no recorte QA aprovado,18 lançamentos equilibrados, sem SGC ou novas demonstrações. Abertura/legibilidade do novo Stock confirmada pelo utilizador; aceitação integral, revisão visual restante e capacidade de implantação continuam separadas. + +**Directiva actual — conclusão do SGF autónomo:** fechar os seis módulos numa única instalação e entregar um produto apresentável a potenciais clientes. [Escopo, diagnóstico e critérios únicos de entrega](docs/SGF_SOLUTION_DELIVERY.md). O conector SGC é opcional e permanece desligado; as provas e demonstrações históricas abaixo não representam a conclusão dos seis módulos. + **Compras — entrada única actual:** **http://127.0.0.1:8085/compras**. [Guia de utilização](docs/COMPRAS_USO_LOCAL.md) · [percurso concluído e prova](docs/PURCHASING_COMPLETION.md). A instalação existente reúne fornecedores, requisições, propostas/adjudicação, ordens, recepções, facturas e reconhecimento de obrigações. Há um exemplo completo conservado, com principal100MZN e movimento1UN. Funções sintéticas explícitas; nenhuma instalação empresarial ou operação real foi activada. Para reutilizar a aplicação local: `scripts/abrir-compras.ps1`. As referências de demonstrações abaixo são históricas. [Comprovativos de fornecedor integrados na consulta contabilística](docs/FINANCIAL_RECEIPT_RECOVERY_DELIVERY.md): domínio explícito de reconhecimento ou liquidação, direitos próprios, sem reexecutar operações.43 verificações dirigidas em base QA nova; nenhum pagamento criado. Revisão visual, carga e implantação pendentes. diff --git a/app/Console/Commands/PopulateDimTempo.php b/app/Console/Commands/PopulateDimTempo.php new file mode 100644 index 0000000..fd31284 --- /dev/null +++ b/app/Console/Commands/PopulateDimTempo.php @@ -0,0 +1,47 @@ +option('start-year') ?? now('Africa/Maputo')->year); + $years = (string) $this->option('years'); + if (!ctype_digit($start) || !ctype_digit($years) || (int)$start < 1 || (int)$years < 1 || (int)$years > 100 || (int)$start + (int)$years > 10000) { + $this->error('Indique um ano válido e entre 1 e 100 anos, até ao ano 9999.'); + return self::INVALID; + } + $count = 0; + if ($this->option('dry-run')) { + foreach (CalendarDimension::days((int)$start, (int)$years) as $day) $count++; + $this->info("Simulação: {$count} dias; {$start}-01-01 até ".((int)$start+(int)$years-1).'-12-31. Nenhuma escrita.'); + return self::SUCCESS; + } + $connection = DB::connection(); + if (!$connection->getSchemaBuilder()->hasTable('analytics_dim_tempo')) { + $this->error('Falta analytics_dim_tempo; aplique primeiro a migration analítica.'); + return self::FAILURE; + } + $inserted = $connection->transaction(function () use ($connection, $start, $years, &$count) { + $batch = []; $inserted = 0; + foreach (CalendarDimension::days((int)$start, (int)$years) as $day) { + $batch[] = $day; $count++; + if (count($batch) === 250) { + $inserted += $connection->table('analytics_dim_tempo')->insertOrIgnore($batch); $batch = []; + } + } + if ($batch) $inserted += $connection->table('analytics_dim_tempo')->insertOrIgnore($batch); + return $inserted; + }); + $this->info("Calendário: {$count} dias; {$inserted} inseridos; restantes preservados. Feriados não classificados; dia útil significa apenas segunda a sexta."); + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Api/V1/SgcIntegrationController.php b/app/Http/Controllers/Api/V1/SgcIntegrationController.php new file mode 100644 index 0000000..e8c7731 --- /dev/null +++ b/app/Http/Controllers/Api/V1/SgcIntegrationController.php @@ -0,0 +1,175 @@ +integrationService = $integrationService; + } + + /** + * POST /api/v1/integration/sgc/invoices + * Recebe fatura do SGC + */ + public function storeInvoice(StoreInvoiceRequest $request): JsonResponse + { + $idempotencyKey = $request->header('X-Idempotency-Key'); + + if (!$idempotencyKey) { + return response()->json([ + 'status' => 'REJECTED', + 'error_code' => 'MISSING_IDEMPOTENCY_KEY', + 'message' => 'Header X-Idempotency-Key é obrigatório', + ], 400); + } + + // Verificar assinatura HMAC (em produção) + // $this->verificarAssinatura($request); + + $resultado = $this->integrationService->receberFatura( + $request->validated(), + $idempotencyKey + ); + + $statusCode = match($resultado['status']) { + 'ACCEPTED' => 201, + 'DUPLICATE' => 409, + 'REJECTED' => 400, + default => 500, + }; + + return response()->json($resultado, $statusCode); + } + + /** + * POST /api/v1/integration/sgc/payments + * Recebe confirmação de pagamento do SGC + */ + public function storePayment(StorePaymentRequest $request): JsonResponse + { + $idempotencyKey = $request->header('X-Idempotency-Key'); + + if (!$idempotencyKey) { + return response()->json([ + 'status' => 'REJECTED', + 'error_code' => 'MISSING_IDEMPOTENCY_KEY', + 'message' => 'Header X-Idempotency-Key é obrigatório', + ], 400); + } + + $resultado = $this->integrationService->receberPagamento( + $request->validated(), + $idempotencyKey + ); + + $statusCode = match($resultado['status']) { + 'ACCEPTED' => 201, + 'DUPLICATE' => 409, + 'REJECTED' => 400, + default => 500, + }; + + return response()->json($resultado, $statusCode); + } + + /** + * GET /api/v1/integration/sgc/invoices/{sgfDocNumber}/status + * Consulta estado de uma fatura + */ + public function getInvoiceStatus(string $sgfDocNumber): JsonResponse + { + $fatura = \App\Models\Sgc\Fatura::where('sgf_doc_number', $sgfDocNumber) + ->with(['ordemCompra', 'fornecedor']) + ->first(); + + if (!$fatura) { + return response()->json([ + 'status' => 'NOT_FOUND', + 'message' => 'Fatura não encontrada', + ], 404); + } + + // Construir lifecycle + $lifecycle = [ + ['event' => 'RECEBIDA', 'at' => $fatura->created_at], + ]; + + if ($fatura->status_3_way_match !== 'Pendente') { + $lifecycle[] = [ + 'event' => '3WAY_MATCH', + 'at' => $fatura->updated_at, + 'result' => $fatura->status_3_way_match, + ]; + } + + if ($fatura->status_pagamento === 'Pago') { + $lifecycle[] = [ + 'event' => 'PAGA', + 'at' => $fatura->data_pagamento, + 'reference' => $fatura->numero_comprovativo, + ]; + } + + return response()->json([ + 'sgf_doc_number' => $fatura->sgf_doc_number, + 'current_status' => $fatura->status_pagamento, + 'lifecycle' => $lifecycle, + ]); + } + + /** + * GET /api/v1/integration/sgc/stats + * Estatísticas de integração + */ + public function getStats(): JsonResponse + { + $stats = [ + 'faturas' => [ + 'total' => \App\Models\Sgc\Fatura::count(), + 'conformes' => \App\Models\Sgc\Fatura::conformes()->count(), + 'com_excecao' => \App\Models\Sgc\Fatura::comExcecao()->count(), + 'pagas' => \App\Models\Sgc\Fatura::pagas()->count(), + ], + 'ordens_compra' => [ + 'total' => \App\Models\Sgc\OrdemCompra::count(), + 'cabimentadas' => \App\Models\Sgc\OrdemCompra::cabimentadas()->count(), + ], + 'integracao' => [ + 'aceites' => DB::table('sgc_integration_log') + ->where('status', 'ACCEPTED')->count(), + 'rejeitadas' => DB::table('sgc_integration_log') + ->where('status', 'REJECTED')->count(), + 'duplicadas' => DB::table('sgc_integration_log') + ->where('status', 'DUPLICATE')->count(), + ], + ]; + + return response()->json($stats); + } + + /** + * Verifica assinatura HMAC (em produção) + */ + private function verificarAssinatura(Request $request): bool + { + $signature = $request->header('X-SGC-Signature'); + $secret = config('services.sgc.webhook_secret'); + + $payload = json_encode($request->all(), JSON_UNESCAPED_SLASHES); + $expectedSignature = hash_hmac('sha256', $payload, $secret); + + return hash_equals($expectedSignature, $signature); + } +} diff --git a/app/Http/Controllers/Api/V1/SgcInvoiceIngressController.php b/app/Http/Controllers/Api/V1/SgcInvoiceIngressController.php new file mode 100644 index 0000000..3e0b669 --- /dev/null +++ b/app/Http/Controllers/Api/V1/SgcInvoiceIngressController.php @@ -0,0 +1,28 @@ +show($order, $runtime)->getData(true); + return response()->json(['order'=>$state['order'], 'revision'=>$state['revision'], 'receipt_progress'=>$state['receipt_progress']]); + } + + public function __invoke(StoreInvoiceRequest $request, SgcInvoiceIngress $ingress) + { + try { return $ingress->receive($request); } + catch (QueryException $error) { + if (in_array($error->getCode(), ['55P03','40P01','40001'], true)) { + return response()->json(['error'=>'RETRY_SAME_KEY', 'message'=>'Operação concorrente; recuperar com a mesma chave e conteúdo.'], 503)->header('Retry-After', '1'); + } + if ($error->getCode() === '23505') return response()->json(['error'=>'DOCUMENT_CONFLICT'], 409); + throw $error; + } + } +} diff --git a/app/Http/Middleware/AuthenticateSgcInvoice.php b/app/Http/Middleware/AuthenticateSgcInvoice.php new file mode 100644 index 0000000..dbec4eb --- /dev/null +++ b/app/Http/Middleware/AuthenticateSgcInvoice.php @@ -0,0 +1,70 @@ +environment('budget-testing'), 403); + abort_unless($request->isSecure() || in_array($request->ip(), ['127.0.0.1', '::1'], true), 403); + abort_if(strlen($request->getContent()) > 131072, 413); + if ($request->isMethod('GET')) abort_if($request->query->count() || strlen($request->getContent()), 422); + else abort_unless($request->isJson(), 415); + $client = (string)$request->header('X-SGC-Client'); + $principals = config('sgc_invoice.principals', []); + $principal = preg_match('/^[a-zA-Z0-9_-]{1,64}$/D', $client) ? ($principals[$client] ?? null) : null; + $token = $request->bearerToken(); + abort_unless(is_array($principal) && ($principal['enabled'] ?? false) === true + && is_string($token) && strlen($token) >= 32 && strlen($token) <= 512 + && preg_match('/^[a-f0-9]{64}$/D', $principal['token_sha256'] ?? '') + && hash_equals($principal['token_sha256'], hash('sha256', $token)), 401); + foreach (['user_id', 'auth_version', 'tenant_id', 'company_id', 'unit_id'] as $key) { + abort_unless(is_int($principal[$key] ?? null) && $principal[$key] > 0, 403); + } + $version = $principal['context_version'] ?? null; + abort_unless(is_string($version) && preg_match('/^[a-f0-9]{64}$/D', $version) + && hash_equals($version, (string)$request->header('X-SGC-Context')), 409); + $user = User::find($principal['user_id']); + abort_unless($user && $user->auth_version === $principal['auth_version'], 401); + $context = app(CompanyContext::class); + $guard = Auth::getDefaultDriver(); + $oldUser = Auth::guard($guard)->user(); + $oldResolver = $request->getUserResolver(); + $oldSession = $request->hasSession() ? $request->session() : null; + try { + // Context and person come exclusively from the server-side principal, never JSON/cookies. + $context->selectFor($user->id, $principal['tenant_id'], $principal['company_id'], $principal['unit_id']); + Auth::guard($guard)->setUser($user); + $request->setUserResolver(fn () => $user); + $session = new Store('sgc_invoice_ephemeral', new ArraySessionHandler(10)); + $session->start(); + $session->put('company_context', ['tenant_id'=>$principal['tenant_id'], 'company_id'=>$principal['company_id'], + 'unit_id'=>$principal['unit_id'], 'module_id'=>'purchasing', 'token'=>$version]); + $request->setLaravelSession($session); + $request->headers->set('X-SGF-Context', $version); + app(RequireModuleContext::class)->check($request, $context); + $response = $next($request); + $response->headers->set('Cache-Control', 'no-store, private'); + return $response; + } finally { + $context->clear(); + if (isset($session)) $session->flush(); + Auth::forgetGuards(); Auth::shouldUse($guard); + if ($oldUser) Auth::guard($guard)->setUser($oldUser); + $request->setUserResolver($oldResolver); + if ($oldSession) $request->setLaravelSession($oldSession); + } + } +} diff --git a/app/Http/Requests/Sgc/StoreInvoiceRequest.php b/app/Http/Requests/Sgc/StoreInvoiceRequest.php new file mode 100644 index 0000000..6c18c91 --- /dev/null +++ b/app/Http/Requests/Sgc/StoreInvoiceRequest.php @@ -0,0 +1,34 @@ +user() !== null; } + + public function rules(): array + { + $base = ['phase'=>'required|in:preview,confirm', 'order_id'=>'required|integer|min:1']; + if ($this->input('phase') === 'confirm') return $base + ['preview'=>'required|string|max:131072']; + return $base + ['revision'=>'required|string|regex:/^[a-f0-9]{64}$/D', 'reason'=>'required|string|min:10|max:2000', + 'data'=>'required|array:number,issued_on,subtotal,tax,total,items', 'data.number'=>'required|string|max:80', + 'data.issued_on'=>'required|date_format:Y-m-d', 'data.subtotal'=>'required|string|regex:/^\d{1,12}\.\d{2}$/D', + 'data.tax'=>'required|string|regex:/^\d{1,12}\.\d{2}$/D', 'data.total'=>'required|string|regex:/^\d{1,12}\.\d{2}$/D', + 'data.items'=>'required|array|min:1|max:50', 'data.items.*'=>'required|array:purchase_order_item_id,quantity,unit,unit_price,tax_rate', + 'data.items.*.purchase_order_item_id'=>'required|integer|min:1|distinct', + 'data.items.*.quantity'=>'required|string|regex:/^\d{1,12}(\.\d{1,3})?$/D', + 'data.items.*.unit'=>'required|string|max:20', 'data.items.*.unit_price'=>'required|string|regex:/^\d{1,12}\.\d{2}$/D', + 'data.items.*.tax_rate'=>'required|string|regex:/^\d{1,3}(\.\d{1,2})?$/D']; + } + + public function after(): array + { + return [function ($validator) { + $allowed = $this->input('phase') === 'confirm' ? ['phase','order_id','preview'] : ['phase','order_id','revision','reason','data']; + if ($this->query->count() || array_diff(array_keys($this->json()->all()), $allowed)) $validator->errors()->add('payload', 'Campos ou parâmetros não permitidos.'); + if (!preg_match('/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[1-8][a-fA-F0-9]{3}-[89aAbB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$/D', (string)$this->header('X-Idempotency-Key'))) $validator->errors()->add('idempotency_key', 'X-Idempotency-Key UUID obrigatório.'); + }]; + } +} diff --git a/app/Models/Sgc/Fatura.php b/app/Models/Sgc/Fatura.php new file mode 100644 index 0000000..4b0b3e9 --- /dev/null +++ b/app/Models/Sgc/Fatura.php @@ -0,0 +1,69 @@ + 'date', + 'data_vencimento' => 'date', + 'data_pagamento' => 'date', + 'valor_fatura' => 'decimal:2', + 'valor_pago' => 'decimal:2', + 'desconto_obtido' => 'decimal:2', + 'iva_valor' => 'decimal:2', + 'taxa_cambio' => 'decimal:6', + 'sgf_sync_timestamp' => 'datetime', + ]; + + public function ordemCompra(): BelongsTo + { + return $this->belongsTo(OrdemCompra::class); + } + + public function fornecedor(): BelongsTo + { + return $this->belongsTo(Fornecedor::class); + } + + public function scopeConformes($query) + { + return $this->where('status_3_way_match', 'Conforme'); + } + + public function scopePagas($query) + { + return $this->where('status_pagamento', 'Pago'); + } + + public function scopeComExcecao($query) + { + return $this->where('status_3_way_match', 'Com Excecao'); + } +} diff --git a/app/Models/Sgc/OrdemCompra.php b/app/Models/Sgc/OrdemCompra.php new file mode 100644 index 0000000..25b9ac3 --- /dev/null +++ b/app/Models/Sgc/OrdemCompra.php @@ -0,0 +1,67 @@ + 'date', + 'data_entrega_prevista' => 'date', + 'valor_comprometido' => 'decimal:2', + 'taxa_cambio' => 'decimal:6', + 'cabimentado' => 'boolean', + ]; + + public function fornecedor(): BelongsTo + { + return $this->belongsTo(Fornecedor::class); + } + + public function centroCusto(): BelongsTo + { + return $this->belongsTo(CentroCusto::class); + } + + public function faturas(): HasMany + { + return $this->hasMany(Fatura::class, 'ordem_compra_id'); + } + + public function recebimentos(): HasMany + { + return $this->hasMany(Recebimento::class, 'ordem_compra_id'); + } + + public function scopeEmitidas($query) + { + return $query->where('status', 'Emitida'); + } + + public function scopeCabimentadas($query) + { + return $query->where('cabimentado', true); + } +} diff --git a/app/Modules/Accounting/CashLedgerSource.php b/app/Modules/Accounting/CashLedgerSource.php new file mode 100644 index 0000000..1cc996a --- /dev/null +++ b/app/Modules/Accounting/CashLedgerSource.php @@ -0,0 +1,22 @@ +scope($u->id,'view',true);$b=$cash->binding();$row=DB::table('cash_events')->where($s)->where(['id'=>$id,'session_key'=>$b['session_key'],'action'=>'review','state'=>'approved','kind'=>$kind==='cash.receipt_classified'?'receipt':'expense'])->first()??throw new \DomainException('Origem de Caixa fora do âmbito.'); + $p=DB::table('cash_events')->where($s)->where(['id'=>$row->proposal_id,'action'=>'prepare','kind'=>$row->kind])->first();$data=json_decode($row->data,true);$deps=json_decode($row->dependencies,true);$receipt=$cash->receipt($s,$b);$hash=hash('sha256',Payables::canonical($receipt)); + if(!$p||$p->actor_id===$row->actor_id||Payables::canonical(json_decode($p->data,true))!==Payables::canonical($data)||($deps['source']??null)!==$hash||!($deps['open']??false)||$data['amount_minor']!==($row->kind==='receipt'?100000:5000))throw new \DomainException('Caixa sem proposta, origem e revisão distintas.'); + if($row->kind==='receipt'&&!SourceBook::table('gl_journals')->where($s)->where('snapshot->source->effect_type','receivable.allocated')->where('snapshot->source->source_id',$receipt['allocation']['id'])->exists())throw new \DomainException('Contabilize primeiro a afectação original; a Caixa só reclassifica a liquidez já reconhecida.'); + return ['effect_type'=>$kind,'source_id'=>$id,'obligation_id'=>$id,'origin_key'=>hash('sha256',Payables::canonical([$s,$b['session_key']])),'amount_minor'=>$data['amount_minor'],'principal_minor'=>$data['amount_minor'],'tax_minor'=>0,'currency'=>'MZN','compatible'=>true,'history_imported'=>false,'cash_effect_on'=>$data['date'],'permitted_period_key'=>$b['period_key'],'recognition_revision'=>hash('sha256',Payables::canonical(['event'=>(array)$row,'proposal'=>(array)$p,'source'=>$receipt])),'cash_event'=>(array)$row]; + } + public function period(User $u,array $period):array + { + if(config('cash_qa.enabled')!==true)return ['facts'=>0,'pending'=>0,'hash'=>hash('sha256','cash-disabled')];$cash=app(CashWorkspace::class);$s=$cash->scope($u->id,'view',true);$b=$cash->binding();$facts=0;$pending=0;$h=hash_init('sha256');$q=DB::table('cash_events')->where($s); + foreach((clone$q)->orderBy('id')->cursor() as $row){$facts++;hash_update($h,Payables::canonical((array)$row));if($row->session_key!==$b['session_key']){$pending++;continue;}if($row->action==='prepare'&&!(clone$q)->where('proposal_id',$row->id)->where('action','review')->exists())$pending++; + if($row->action==='review'&&$row->state==='approved'&&in_array($row->kind,['receipt','expense'],true)){try{$type=$row->kind==='receipt'?'cash.receipt_classified':'cash.expense_confirmed';$source=$this->source($u,$type,$row->id);if(!SourceBook::table('gl_journals')->where($s)->where('snapshot->source->effect_type',$type)->where('snapshot->source->source_id',$row->id)->where('snapshot->source->recognition_revision',$source['recognition_revision'])->exists())$pending++;}catch(\DomainException|\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface){$pending++;}} + } + if($facts>0&&!(clone$q)->where(['session_key'=>$b['session_key'],'state'=>'closed'])->exists())$pending++;return ['facts'=>$facts,'pending'=>$pending,'hash'=>hash_final($h)]; + } +} diff --git a/app/Modules/Accounting/ComposedLedger.php b/app/Modules/Accounting/ComposedLedger.php new file mode 100644 index 0000000..e82ce06 --- /dev/null +++ b/app/Modules/Accounting/ComposedLedger.php @@ -0,0 +1,79 @@ +environment(); + DB::setDefaultConnection($host->getName());Schema::clearResolvedInstance('db.schema');Auth::shouldUse('web');Auth::guard('web')->setUser($user);app()->instance('env','budget-testing'); + try{return $work();}finally{DB::setDefaultConnection($default);Schema::clearResolvedInstance('db.schema');Auth::shouldUse($guard);if($nativeUser)Auth::guard($guard)->setUser($nativeUser);app()->instance('env',$environment);} + } + private function source(object $actor,array $binding,array $financialScope,int $id):array + { + $o=DB::table('pa_obligations')->where($financialScope)->where('id',$id)->first()??throw new \DomainException('Obrigação fora da origem financeira autorizada.'); + $s=DB::table('pa_sources')->where($financialScope)->where('id',$o->source_id)->first()??throw new \DomainException('Origem financeira ausente.'); + $orders=PurchaseOrder::visibleTo($actor)->where(fn($q)=>$q->whereIn('id',$binding['order_ids']??[])->orWhereIn('requisition_id',RequisitionAccess::ids($binding))); + abort_unless(Invoice::where('tenant_id',$actor->tenant_id)->whereIn('purchase_order_id',$orders->select('id'))->where('id',$s->local_source_id)->exists(),403); + $service=app(SupplierPayables::class);$service->readContext($actor);$document=$service->document($actor,(int)$s->local_source_id);$position=$service->engine($actor)->positionForSource((int)$s->local_source_id); + if(!$position||(int)$position['id']!==$id||$position['source_conflict'])throw new \DomainException('A fonte financeira mudou; exige nova conferência.'); + $proposal=DB::table('pa_proposals')->where($financialScope)->where('id',$o->proposal_id)->first()??throw new \DomainException('Reconhecimento sem conferência.'); + return ['effect_type'=>'supplier_payable.recognised','origin_key'=>$s->source_key,'obligation_id'=>$id,'local_source_id'=>(int)$s->local_source_id,'document_revision'=>$o->source_revision,'recognition_revision'=>hash('sha256',Payables::canonical(['obligation'=>(array)$o,'recognition_snapshot'=>$proposal->snapshot])),'principal_minor'=>(int)$o->principal_minor,'currency'=>$o->currency,'due_on'=>$o->due_on,'tax_minor'=>$document['tax_minor'],'compatible'=>true,'history_imported'=>$position['history_imported']]; + } + + public function run(string $action,\Closure $work):mixed + { + CompositionRuntime::assert();abort_unless(config('sgf_single_installation.ledger_enabled')===true,404); + abort_unless(request()->session()->get('company_context.module_id')==='accounting',409,'Seleccione Contabilidade.'); + $context=app(CompanyContext::class);$scope=$context->identifiers()+['unit_id'=>$context->unitId()];$hostUser=$context->userId();$host=DB::connection();$hostIdentity=CompositionRuntime::physical($host); + $generation=request()->session()->get('company_context.token');$hostActor=\App\Models\User::findOrFail($hostUser); + try{return $host->transaction(function()use($action,$work,$context,$scope,$hostUser,$hostActor,$host,$hostIdentity,$generation){ + $host->statement("SET LOCAL lock_timeout='5s'");$host->table('companies')->where('tenant_id',$scope['tenant_id'])->where('id',$scope['company_id'])->lockForUpdate()->firstOrFail(); + return app(ReceiptRuntime::class)->run('general_ledger.'.$action,function($actor,$binding,$revision,$origin)use($action,$work,$context,$scope,$hostUser,$hostActor,$host,$hostIdentity,$generation){ + $native=DB::connection();LedgerSchema::assertSynthetic($native);$r=$native->table('gl_sgf_composition')->where('tenant_id',$actor->tenant_id)->first(); + abort_unless($r&&$r->host_database===$hostIdentity['database']&&(int)$r->company_id===$scope['company_id']&&(int)$r->unit_id===$scope['unit_id'],403,'Livro financeiro ligado a outro contexto SGF.'); + $ledgerScope=['tenant_id'=>(int)$actor->tenant_id,'company_id'=>$scope['company_id'],'unit_id'=>$scope['unit_id']];$financialScope=['tenant_id'=>(int)$actor->tenant_id,'company_id'=>0,'unit_id'=>0]; + // The legacy payable book has no SGF unit column. Never expose its + // listings or settlement facts until every obligation is qualified. + $orders=PurchaseOrder::visibleTo($actor)->where(fn($q)=>$q->whereIn('id',$binding['order_ids']??[])->orWhereIn('requisition_id',RequisitionAccess::ids($binding))); + $unqualified=$native->table('pa_obligations as o')->where(['o.tenant_id'=>$actor->tenant_id,'o.company_id'=>0,'o.unit_id'=>0])->whereNotExists(function($q)use($orders,$actor){ + $q->selectRaw('1')->from('pa_sources as s')->join('invoices as i','i.id','=','s.local_source_id')->whereColumn('s.id','o.source_id')->whereColumn('s.tenant_id','o.tenant_id')->whereColumn('s.company_id','o.company_id')->whereColumn('s.unit_id','o.unit_id')->where('i.tenant_id',$actor->tenant_id)->whereIn('i.purchase_order_id',$orders->select('id')); + })->exists(); + abort_if($unqualified,503,'Há origens financeiras sem unidade conferida; a partição exige decisão explícita.'); + $identity=app(SupplierPayables::class)->readContext($actor);$person=$identity['person']; + $authorize=function(string $right)use($host,$hostUser,$scope,$context,$actor,$person,$ledgerScope,$generation){ + abort_unless($context->userId()===$hostUser&&$context->identifiers()+['unit_id'=>$context->unitId()]===$scope&&request()->session()->get('company_context.token')===$generation,409,'Contexto SGF alterado.'); + foreach(['general_ledger.view','general_ledger.'.$right] as $permission)abort_unless($host->table('permission_grants')->where($scope+['user_id'=>$hostUser,'permission'=>$permission])->exists(),403); + $fresh=app(CurrentActor::class)->lock($actor,['finance']);abort_if(app(OrganizationAccess::class)->maintenanceOnly($fresh),403); + abort_unless(DB::table('pa_access')->where($ledgerScope)->where(['user_id'=>$actor->id,'action'=>'general_ledger.'.$right,'person'=>$person])->exists(),403); + }; + $authorize('view'); + $isolation=fn()=>app(ComposedMaterialIsolation::class)->check($host,$native,$ledgerScope); + $stock=new StockValuation($native,$ledgerScope,$actor->id,$person,$authorize,fn()=>$authorize('view'),$isolation); + $additional=function($kind,$id)use($host,$hostActor,$stock,$isolation){ + if(str_starts_with($kind,'stock.'))return $stock->source($kind,$id); + if(str_starts_with($kind,'workshop.')||str_starts_with($kind,'industry.'))$isolation(); + return $this->withHost($host,$hostActor,function()use($hostActor,$kind,$id){ + if(str_starts_with($kind,'cash.'))return app(CashLedgerSource::class)->source($hostActor,$kind,$id);if(str_starts_with($kind,'workshop.'))return $kind==='workshop.direct_cost.validated'?app(CostLedgerSource::class)->source($hostActor,$id):app(CostLedgerSource::class)->correctionSource($hostActor,$kind,$id); + if(str_starts_with($kind,'industry.'))return $kind==='industry.cost.validated'?app(IndustrialLedgerSource::class)->source($hostActor,$id):app(IndustrialLedgerSource::class)->correctionSource($hostActor,$kind,$id); + return app(SalesLedgerSource::class)->source($hostActor,$kind,$id); + }); + }; + $period=function($p)use($host,$hostActor,$stock){$extra=$this->withHost($host,$hostActor,function()use($p,$hostActor){$facts=0;$pending=0;$h=hash_init('sha256');foreach([SalesLedgerSource::class,CostLedgerSource::class,IndustrialLedgerSource::class,CashLedgerSource::class] as $class){$v=app($class)->period($hostActor,$p);$facts+=$v['facts'];$pending+=$v['pending'];hash_update($h,$v['hash']);}return ['facts'=>$facts,'pending'=>$pending,'hash'=>hash_final($h)];});$physical=$stock->period($p);return ['facts'=>$extra['facts']+$physical['facts'],'pending'=>$extra['pending']+$physical['pending'],'hash'=>hash('sha256',$extra['hash'].$physical['hash'])];}; + $engine=new PayableJournal($native,$ledgerScope,$actor->id,$person,$authorize,fn()=>$authorize('view'),fn(int $id)=>$this->source($actor,$binding,$financialScope,$id),null,$additional,$period,$financialScope); + return $work($engine,$actor,$binding,$origin+['sgf_host_user'=>$hostUser,'sgf_host_scope'=>$scope,'binding_revision'=>$revision],$ledgerScope,$stock,fn(\Closure $read)=>$this->withHost($host,$hostActor,$read)); + }); + });}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Livro ocupado. Recupere a mesma operação, sem gerar outra chave.'],503)->header('Retry-After','5')->header('Cache-Control','no-store');} + } +} diff --git a/app/Modules/Accounting/ComposedLedgerCandidates.php b/app/Modules/Accounting/ComposedLedgerCandidates.php new file mode 100644 index 0000000..7e0e41d --- /dev/null +++ b/app/Modules/Accounting/ComposedLedgerCandidates.php @@ -0,0 +1,27 @@ +$kind==='all'||$kind===$k; + $append=function($query,$type,$columns,\Closure $map)use(&$sources,&$last,&$totals,$page,$size,$accept){if(!$accept($type))return;$p=$query->paginate($size,$columns,'source_page',$page);$last=max($last,$p->lastPage());$totals[$type]=$p->total();foreach($p->items() as $row)$sources[]=['kind'=>$type]+$map($row);}; + $financial=['tenant_id'=>$actor->tenant_id,'company_id'=>0,'unit_id'=>0]; + $append(DB::table('pa_obligations as o')->join('pa_sources as s','s.id','=','o.source_id')->join('invoices as i','i.id','=','s.local_source_id')->where(['o.tenant_id'=>$actor->tenant_id,'o.company_id'=>0,'o.unit_id'=>0])->orderBy('o.id'),'supplier_payable.recognised',['o.id','o.principal_minor','i.number'],fn($o)=>['id'=>$o->id,'label'=>'Compra '.$o->number,'amount'=>$o->principal_minor]); + foreach(['ps_allocations'=>'allocation.confirmed','ps_reversals'=>'allocation.reversed'] as $table=>$type)$append(DB::table($table)->where($financial)->orderBy('id'),$type,['id','amount_minor'],fn($o)=>['id'=>$o->id,'label'=>($type==='allocation.confirmed'?'Afectação':'Reversão').' fornecedor #'.$o->id,'amount'=>$o->amount_minor]); + $hostRead(function()use($scope,$append){ + $append(ModuleRecords::query('si_invoices')->where($scope)->orderBy('id'),'sales.invoice',['id','series_code','number'],fn($i)=>['id'=>$i->id,'label'=>'Venda '.$i->series_code.'/'.$i->number,'amount'=>null]); + foreach(['allocate'=>'receivable.allocated','reverse_allocation'=>'receivable.reversed','reduction'=>'receivable.reduced','reverse_reduction'=>'receivable.reduction_reversed'] as $event=>$type)$append(DB::table('sr_effects as e')->join('sr_records as r','r.id','=','e.record_id')->where(['r.tenant_id'=>$scope['tenant_id'],'r.company_id'=>$scope['company_id'],'r.unit_id'=>$scope['unit_id']])->whereIn('r.invoice_id',ModuleRecords::query('si_invoices')->select('id'))->where('e.kind',$event)->orderBy('e.id'),$type,['e.id'],fn($e)=>['id'=>$e->id,'label'=>'Afectação/reversão cliente #'.$e->id,'amount'=>null]); + if(config('cash_qa.enabled')===true)foreach(['receipt'=>'cash.receipt_classified','expense'=>'cash.expense_confirmed'] as $kind=>$type)$append(DB::table('cash_events')->where($scope)->where(['action'=>'review','state'=>'approved','kind'=>$kind])->orderBy('id'),$type,['id'],fn($r)=>['id'=>$r->id,'label'=>'Caixa QA #'.$r->id,'amount'=>null]); + }); + foreach(['gl_cost_sources'=>['order_id','workshop.direct_cost.validated','Custo de oficina'],'gl_industrial_sources'=>['lot_id','industry.cost.validated','Custo industrial']] as $table=>[$column,$type,$label])$append(DB::table($table)->where($scope)->orderBy('id'),$type,[$column],fn($o)=>['id'=>$o->$column,'label'=>$label.' #'.$o->$column,'amount'=>null]); + $stockData=[];if($kind==='all'||str_starts_with($kind,'stock.'))foreach(DB::table('gl_stock_books')->where($scope)->orderBy('id')->cursor() as $book){$value=$stock->page($book->id,$page,$size);$stockData[]=$value;$last=max($last,(int)ceil($value['pagination']['total']/$size));foreach($value['rows'] as $movement){$m=$movement['movement'];$type=\Tempo\Finance\GeneralLedger\StockValuation::TYPES[$m['movement_type']]??null;if($type&&$accept($type))$sources[]=['kind'=>$type,'id'=>$m['id'],'label'=>'Movimento físico #'.$m['id'],'amount'=>null];}} + return ['sources'=>$sources,'stock'=>$stockData,'source_pagination'=>['page'=>$page,'last_page'=>$last,'per_page'=>$size,'kind'=>$kind,'totals_by_family'=>$totals,'independent_family_pages'=>true]]; + } +} diff --git a/app/Modules/Accounting/ComposedLedgerController.php b/app/Modules/Accounting/ComposedLedgerController.php new file mode 100644 index 0000000..8feb28d --- /dev/null +++ b/app/Modules/Accounting/ComposedLedgerController.php @@ -0,0 +1,82 @@ +['invoice_id','sales_invoice_id','sales.invoice'],'cost'=>['order_id','cost_order_id','workshop.direct_cost.validated'],'industrial'=>['lot_id','industrial_lot_id','industry.cost.validated']}; + $d=$r->validate([$alias=>'sometimes|required|integer|min:1','page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);$query=['source_kind'=>$kind];if(isset($d[$alias]))$query[$target]=(int)$d[$alias];if(isset($d['page']))$query['page']=(int)$d['page'];$r->query->replace($query);return $this->page($r,app(ComposedLedger::class)); + } + public function receipt(Request $r,string $key,?string $domain=null) + { + abort_if($r->query()!==[],422);abort_unless($domain===null||in_array($domain,['recognition','settlement'],true),422); + try{return app(ComposedLedger::class)->run('view',function($engine,$actor)use($key,$domain){if($domain===null)$data=$engine->receipt($key);else{$service=app(\Tempo\Purchasing\Embedded\Services\SupplierPayables::class);$service->readContext($actor);$marker=DB::table('gl_sgf_composition')->where('tenant_id',$actor->tenant_id)->first();$financialScope=['tenant_id'=>(int)$actor->tenant_id,'company_id'=>(int)$marker->financial_company_id,'unit_id'=>(int)$marker->financial_unit_id];$data=\Tempo\Finance\Payables\ReceiptReader::read(DB::connection(),$domain,$financialScope,(int)$actor->id,$key,fn()=>$service->readContext($actor));}abort_if($data===null,404,'Comprovativo não encontrado para esta pessoa e contexto.');return response()->json($data)->header('Cache-Control','no-store');});}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(403,$e->getMessage());} + } + public function flow(Request $r) + { + $d=$r->validate(['source'=>'required|integer|min:1']);abort_if(array_diff(array_keys($r->query()),['source']),422); + return app(ComposedLedger::class)->run('view',function($engine,$actor)use($d){$id=DB::table('pa_obligations as o')->join('pa_sources as s','s.id','=','o.source_id')->where(['o.tenant_id'=>$actor->tenant_id,'o.company_id'=>0,'o.unit_id'=>0,'s.local_source_id'=>(int)$d['source']])->value('o.id');abort_unless($id,404,'Origem sem obrigação reconhecida neste contexto.');return redirect('/general-ledger?obligation_id='.$id);}); + } + private function rights():array + { + $ctx=app(CompanyContext::class);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId(),'user_id'=>$ctx->userId()];$permissions=DB::table('permission_grants')->where($scope)->pluck('permission')->all();$out=[]; + foreach(['view','prepare','validate','post','correct_prepare','correct_validate','correct_post','period_prepare','period_validate','period_close'] as $right)$out[$right]=in_array('general_ledger.'.$right,$permissions,true);return $out; + } + + public function page(Request $r,ComposedLedger $ledger) + { + $d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000','accounts_page'=>'sometimes|integer|min:1|max:100000','accounts_per_page'=>'sometimes|integer|in:25,100','account_q'=>'sometimes|nullable|string|max:80','source_page'=>'sometimes|integer|min:1|max:100000','source_per_page'=>'sometimes|integer|in:25,100','source_kind'=>'sometimes|in:'.implode(',',ComposedLedgerCandidates::KINDS),'period_key'=>'sometimes|string|max:80','account_id'=>'sometimes|nullable|integer|min:1','obligation_id'=>'sometimes|integer|min:1','sales_invoice_id'=>'sometimes|integer|min:1','cost_order_id'=>'sometimes|integer|min:1','industrial_lot_id'=>'sometimes|integer|min:1']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);if(($d['account_id']??null)===null)unset($d['account_id']);$rights=$this->rights(); + return $ledger->run('view',function($engine,$actor,$binding,$origin,$scope,$stock,$hostRead)use($r,$d,$rights){ + if(isset($d['sales_invoice_id']))abort_unless($hostRead(fn()=>\App\Shared\Context\ModuleRecords::query('si_invoices')->where($scope)->where('id',$d['sales_invoice_id'])->exists()),403,'Documento fora do contexto e módulo.'); + $data=$engine->listing(array_diff_key($d,['source_page'=>true,'source_per_page'=>true,'source_kind'=>true,'accounts_page'=>true,'accounts_per_page'=>true,'account_q'=>true]));$data['cycle']=$engine->cycleListing((int)($d['page']??1));$data['permissions']=$rights;$data['current_actor']=$actor->id;$data['sgf_scope']=$origin['sgf_host_scope']; + $data=array_merge($data,app(ComposedLedgerCandidates::class)->read($d,$actor,$scope,$stock,$hostRead)); + $accounts=DB::table('gl_accounts')->where($scope);if(($d['account_q']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['account_q']).'%';$accounts->where(fn($q)=>$q->whereRaw("code ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("label ILIKE ? ESCAPE '!'",[$needle]));} + $chart=$accounts->orderBy('code')->paginate((int)($d['accounts_per_page']??25),['id','code','label','currency','is_movement'],'accounts_page',(int)($d['accounts_page']??1));$data['chart_accounts']=$chart->items();$data['chart_pagination']=['page'=>$chart->currentPage(),'last_page'=>$chart->lastPage(),'per_page'=>$chart->perPage(),'total'=>$chart->total()]; + if($r->is('api/*'))return response()->json($data)->header('Cache-Control','no-store'); + return response()->view('accounting.composed-ledger',['data'=>$data,'context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()])->header('Cache-Control','no-store'); + }); + } + + public function command(Request $r,string $action,ComposedLedger $ledger) + { + $allowed=['prepare','validate','post','effect-prepare','correct-prepare','correct-validate','correct-post','period-prepare','period-validate','period-close'];abort_unless(in_array($action,$allowed,true),404); + $right=$action==='effect-prepare'?'prepare':str_replace('-','_',$action);$key=strtolower((string)$r->header('Idempotency-Key'));abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$data=$r->all();abort_if(strlen(json_encode($data))>65536,422); + try{return $ledger->run($right,function($engine,$actor,$binding,$origin)use($action,$data,$key){$hash=hash('sha256',Payables::canonical([$action,$data]));$old=AuditLog::where('tenant_id',$actor->tenant_id)->where('action','sgf.ledger.command')->where('after->operation_key',$key)->first();if($old)abort_unless(($old->after['sgf_context_key']??null)===$origin['sgf_context_key']&&($old->after['request_hash']??null)===$hash,409,'Chave utilizada noutro contexto/conteúdo.');$result=$engine->command($action,$data,$key);if(!$old)app(RequisitionWorkflow::class)->audit($actor,'sgf.ledger.command',null,null,$origin+['operation_key'=>$key,'request_hash'=>$hash,'command'=>$action,'reason'=>$data['reason']??'','result'=>$result]);return response()->json($result)->header('Cache-Control','no-store');});}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(409,$e->getMessage());} + } + + public function report(Request $r,string $kind,ComposedLedger $ledger) + { + abort_unless(in_array($kind,['trial-balance','account-ledger'],true),404);$d=$r->validate(['from_on'=>'required|date_format:Y-m-d','to_on'=>'required|date_format:Y-m-d|after_or_equal:from_on','account_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422); + try{return $ledger->run('view',function($engine)use($r,$kind,$d){$data=$kind==='trial-balance'?$engine->trialBalance($d):$engine->accountLedger($d);return $r->is('api/*')?response()->json($data)->header('Cache-Control','no-store'):response(ReportsWorkspace::render($data,['host'=>'SGF · livro integrado','return_url'=>'/general-ledger']))->header('Cache-Control','no-store');});} + catch(\InvalidArgumentException $e){abort(422,$e->getMessage());} + catch(\DomainException $e){abort(403,$e->getMessage());} + } + public function stockCommand(Request $r,string $action,ComposedLedger $ledger) + { + abort_unless(in_array($action,['rate-prepare','rate-review'],true),404);$right=$action==='rate-prepare'?'prepare':'validate';$key=strtolower((string)$r->header('Idempotency-Key'));abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422); + $data=$r->all();abort_if(strlen(json_encode($data))>65536,422); + try{return $ledger->run($right,function($e,$actor,$b,$origin,$s,$stock)use($action,$data,$key){ + $hash=hash('sha256',Payables::canonical([$action,$data])); + $old=AuditLog::where('tenant_id',$actor->tenant_id)->where('action','sgf.ledger.stock-command')->where('after->operation_key',$key)->first(); + if($old)abort_unless(($old->after['sgf_context_key']??null)===$origin['sgf_context_key']&&($old->after['request_hash']??null)===$hash,409,'Chave utilizada noutro contexto/conteúdo.'); + // The public review label maps to the original engine's validation command. + $result=$stock->command($action==='rate-review'?'rate-validate':$action,$data,$key); + if(!$old)app(RequisitionWorkflow::class)->audit($actor,'sgf.ledger.stock-command',null,null,$origin+['operation_key'=>$key,'request_hash'=>$hash,'command'=>$action,'reason'=>$data['reason']??'','result'=>$result]); + return response()->json($result)->header('Cache-Control','no-store'); + });}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(409,$e->getMessage());} + } +} + + + + + diff --git a/app/Modules/Accounting/ComposedMaterialIsolation.php b/app/Modules/Accounting/ComposedMaterialIsolation.php new file mode 100644 index 0000000..e6baf40 --- /dev/null +++ b/app/Modules/Accounting/ComposedMaterialIsolation.php @@ -0,0 +1,34 @@ +getDatabaseName()||($p['composition']??null)!==$physical['composition'])throw new \DomainException('Perímetro pertence a outro contexto.'); + $stocks=$native->table('gl_stock_books')->where($scope)->orderBy('id')->get(['stock_item_id','warehouse_id']); + $actual=$stocks->map(fn($r)=>(array)$r)->all();if(Payables::canonical($actual)!==Payables::canonical($p['valued_stock']))throw new \DomainException('Artigo/armazém de valorização mudou; nova conferência obrigatória.'); + $codes=$native->table('stock_items')->where('tenant_id',$scope['tenant_id'])->whereIn('id',$stocks->pluck('stock_item_id'))->pluck('code')->all(); + foreach($native->table('gl_cost_sources')->where($scope)->orderBy('id')->cursor() as $map){ + $claim=$p['direct_costs'][(string)$map->order_id]??null; + if(!$claim||$claim['material_origin']!==$map->material_origin)throw new \DomainException('Custo sem declaração individual de exclusão do Stock valorizado.'); + if(!$host->table('wo_orders')->where($scope)->where('id',$map->order_id)->exists())throw new \DomainException('Ordem de custo fora da empresa/unidade.'); + $entry=$host->table('wo_entries')->where('order_id',$map->order_id)->where('origin',$map->material_origin)->orderByDesc('version')->first();$data=$entry?json_decode($entry->data,true):null; + if(!$entry||$entry->kind!=='material'||($data['resource']??null)!==$claim['resource']||in_array($claim['resource'],$codes,true)||!str_starts_with($claim['resource'],'QA-MATERIAL-DIRECTO-')||isset($data['stock_item_id'])||isset($data['delivery_movement_id']))throw new \DomainException('Material directo partilhado, alterado ou sem fronteira física explícita.'); + } + foreach($native->table('gl_industrial_sources')->where($scope)->orderBy('id')->cursor() as $map){ + $claim=$p['industrial_costs'][(string)$map->lot_id]??null;$origins=json_decode($map->origins,true); + if(!$claim||$claim['material_origin']!==($origins['material']??null)||in_array($claim['material_origin'],$codes,true))throw new \DomainException('Material industrial sem exclusão individual do Stock valorizado.'); + $entry=$host->table('ic_inputs')->where($scope)->where('lot_id',$map->lot_id)->where('kind','material')->where('source_ref',$claim['material_origin'])->orderByDesc('version')->first(); + if(!$entry||!str_starts_with($entry->source_ref,'IND-TEST-MATERIAL-'))throw new \DomainException('Origem industrial fora do perímetro sintético seleccionado.'); + } + } +} diff --git a/app/Modules/Accounting/CostLedgerController.php b/app/Modules/Accounting/CostLedgerController.php index f8c574f..5ba7d5f 100644 --- a/app/Modules/Accounting/CostLedgerController.php +++ b/app/Modules/Accounting/CostLedgerController.php @@ -6,6 +6,7 @@ final class CostLedgerController extends Controller { public function index(Request $r,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->focused($r,'cost'); $d=$r->validate(['order_id'=>'sometimes|required|integer|min:1','page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),array_keys($d))!==[],422); try{$data=$ledger->costs($r->user(),isset($d['order_id'])?(int)$d['order_id']:null,(int)($d['page']??1));}catch(\DomainException $e){abort(403,$e->getMessage());}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());} return $r->is('api/*')?response()->json($data)->header('Cache-Control','no-store'):response()->view('accounting.cost-ledger',['data'=>$data])->header('Cache-Control','no-store'); diff --git a/app/Modules/Accounting/CostLedgerSource.php b/app/Modules/Accounting/CostLedgerSource.php index 9dc752a..7c1e966 100644 --- a/app/Modules/Accounting/CostLedgerSource.php +++ b/app/Modules/Accounting/CostLedgerSource.php @@ -16,7 +16,7 @@ final class CostLedgerSource abort_unless(config('cost_ledger.synthetic_enabled')===true,403);DirectCosts::guard();$ctx=app(CompanyContext::class); abort_unless(auth()->id()===$u->id&&$ctx->userId()===$u->id&&$ctx->unitId(),403);$s=$ctx->identifiers()+['unit_id'=>$ctx->unitId()]; foreach(['workshop.view','direct_costs.view'] as $p)app(Permissions::class)->require($u->id,$s['tenant_id'],$s['company_id'],$p,$s['unit_id']); - foreach(['gl_cost_sources','wo_orders','wo_entries','wo_decisions','dc_rates','dc_calculations','dc_decisions'] as $t)abort_unless(DB::getSchemaBuilder()->hasTable($t),503,'Origem de custo não composta neste ensaio.');return $s; + foreach(['gl_cost_sources','wo_orders','wo_entries','wo_decisions','dc_rates','dc_calculations','dc_decisions'] as $t)abort_unless(str_starts_with($t,'gl_')?SourceBook::has($t):DB::getSchemaBuilder()->hasTable($t),503,'Origem de custo não composta neste ensaio.');return $s; } private function hash(array $v):string{return hash('sha256',Payables::canonical($v));} private function facts(User $u,array $s,object $map):array @@ -30,39 +30,39 @@ final class CostLedgerSource if(!$c['calculation']||!$c['current']||!$decision||$decision['decision']!=='approved'||$decision['actor_id']===$c['calculation']['actor_id']||$snap['currency']!=='MZN'||$snap['incomplete']||$snap['selected_direct_total_exact']===null)throw new \DomainException('Cálculo actual completo e validado distintamente obrigatório; custo conhecido parcial não é total.'); $origins=[];foreach($snap['lines'] as $line)$origins[$line['kind']]=$line['origin']; if($origins!==['time'=>$m['time_origin'],'material'=>$m['material_origin']]&&$origins!==['material'=>$m['material_origin'],'time'=>$m['time_origin']])throw new \DomainException('Perímetro seleccionado mudou; não atribuir outra origem automaticamente.'); - if(config('industrial_ledger.synthetic_enabled')===true){abort_unless(DB::getSchemaBuilder()->hasTable('gl_industrial_sources'),503);if(DB::table('ic_inputs as i')->join('gl_industrial_sources as m','m.lot_id','=','i.lot_id')->where('i.tenant_id',$m['tenant_id'])->where('i.company_id',$m['company_id'])->whereIn('i.source_ref',array_values($origins))->exists())throw new \DomainException('Origem já composta em lote industrial; não reconhecer novamente.');} + if(config('industrial_ledger.synthetic_enabled')===true){abort_unless(SourceBook::has('gl_industrial_sources'),503);if(SourceBook::table('gl_industrial_sources')->where('tenant_id',$m['tenant_id'])->where('company_id',$m['company_id'])->whereRaw("EXISTS(SELECT FROM jsonb_each_text(origins::jsonb) AS o WHERE o.value IN (?,?))",array_values($origins))->exists())throw new \DomainException('Origem já composta em lote industrial; não reconhecer novamente.');} try{$amount=BigRational::of($snap['selected_direct_total_exact'])->multipliedBy(100)->toBigInteger()->toInt();}catch(\Brick\Math\Exception\MathException $e){throw new \DomainException('Custo exacto não representável em cêntimos; não arredondar.',0,$e);} if($amount<=0)throw new \DomainException('Este ensaio exige custo seleccionado positivo.'); return ['effect_type'=>'workshop.direct_cost.validated','source_id'=>$m['order_id'],'cost_order_id'=>$m['order_id'],'obligation_id'=>$m['order_id'],'origin_key'=>$this->hash(['scope'=>array_intersect_key($m,array_flip(['tenant_id','company_id','unit_id'])),'order'=>$m['order_id'],'material_origin'=>$m['material_origin'],'time_origin'=>$m['time_origin']]),'recognition_revision'=>$this->hash($facts),'principal_minor'=>$amount,'currency'=>'MZN','tax_minor'=>0,'compatible'=>true,'history_imported'=>false,'permitted_period_key'=>$m['period_key'],'calculation_id'=>$c['calculation']['id'],'calculation_version'=>$c['calculation']['version'],'document_revision'=>$c['calculation']['fingerprint'],'cost_evidence'=>$facts,'label'=>'Custo directo conhecido do perímetro seleccionado; não custo completo, margem ou valorização de inventário']; } public function source(User $u,int $order):array { - $s=$this->scope($u);$m=DB::table('gl_cost_sources')->where($s)->where('order_id',$order)->first()??throw new \DomainException('Ordem/perímetro de custo não composto neste âmbito.');return $this->eligible($this->facts($u,$s,$m)); + $s=$this->scope($u);$m=SourceBook::table('gl_cost_sources')->where($s)->where('order_id',$order)->first()??throw new \DomainException('Ordem/perímetro de custo não composto neste âmbito.');return $this->eligible($this->facts($u,$s,$m)); } public function period(User $u,array $period):array { $s=$this->scope($u);$h=hash_init('sha256');$facts=0;$pending=0; - foreach(DB::table('gl_cost_sources')->where($s)->where('period_key',$period['period_key'])->orderBy('id')->cursor() as $m){$v=$this->facts($u,$s,$m);hash_update($h,Payables::canonical($v));$facts++;try{$source=$this->eligible($v);$j=DB::table('gl_journals')->where($s)->where('snapshot->source->effect_type','workshop.direct_cost.validated')->where('snapshot->source->cost_order_id',$m->order_id)->first();if(!$j||!$this->resolved($s,$j,$source))$pending++;}catch(\DomainException){$pending++;}} + foreach(SourceBook::table('gl_cost_sources')->where($s)->where('period_key',$period['period_key'])->orderBy('id')->cursor() as $m){$v=$this->facts($u,$s,$m);hash_update($h,Payables::canonical($v));$facts++;try{$source=$this->eligible($v);$j=SourceBook::table('gl_journals')->where($s)->where('snapshot->source->effect_type','workshop.direct_cost.validated')->where('snapshot->source->cost_order_id',$m->order_id)->first();if(!$j||!$this->resolved($s,$j,$source))$pending++;}catch(\DomainException){$pending++;}} return ['facts'=>$facts,'pending'=>$pending,'hash'=>hash_final($h)]; } public function correctionSource(User $u,string $kind,int $journal):array { - abort_unless(config('cost_ledger.correction_enabled')===true,403,'Correcção de custo não composta neste ensaio.');$s=$this->scope($u);abort_unless(DB::getSchemaBuilder()->hasTable('gl_cost_correction_capability'),503,'Schema de correcção de custo não instalado.'); - if(!in_array($kind,['workshop.direct_cost.reversal','workshop.direct_cost.replacement'],true))throw new \DomainException('Tipo de correcção fora do recorte.');$j=DB::table('gl_journals')->where($s)->where('id',$journal)->first()??throw new \DomainException('Original fora do âmbito.');$original=json_decode($j->snapshot,true,128,JSON_THROW_ON_ERROR);if($original['source']['effect_type']!=='workshop.direct_cost.validated')throw new \DomainException('Só o custo original admite esta correcção.'); + abort_unless(config('cost_ledger.correction_enabled')===true,403,'Correcção de custo não composta neste ensaio.');$s=$this->scope($u);abort_unless(SourceBook::has('gl_cost_correction_capability'),503,'Schema de correcção de custo não instalado.'); + if(!in_array($kind,['workshop.direct_cost.reversal','workshop.direct_cost.replacement'],true))throw new \DomainException('Tipo de correcção fora do recorte.');$j=SourceBook::table('gl_journals')->where($s)->where('id',$journal)->first()??throw new \DomainException('Original fora do âmbito.');$original=json_decode($j->snapshot,true,128,JSON_THROW_ON_ERROR);if($original['source']['effect_type']!=='workshop.direct_cost.validated')throw new \DomainException('Só o custo original admite esta correcção.'); $base=$this->source($u,(int)$original['source']['cost_order_id']);if($base['origin_key']!==$original['source']['origin_key']||$base['permitted_period_key']!==$original['period']['period_key'])throw new \DomainException('Perímetro/período original incompatível.');if($base['recognition_revision']===$original['source']['recognition_revision'])throw new \DomainException('Não há nova versão validada para substituir o original.'); return array_replace($base,['effect_type'=>$kind,'source_id'=>$journal,'amount_minor'=>$kind==='workshop.direct_cost.reversal'?(int)$j->debit_minor:$base['principal_minor'],'replacement_source_revision'=>$base['recognition_revision'],'recognition_revision'=>$this->hash(['current_source'=>$base,'original'=>(array)$j,'kind'=>$kind]),'original_journal_id'=>$journal,'original_journal_on'=>$j->journal_on,'original_journal_sha256'=>$this->hash((array)$j),'original_cost_source'=>$original['source']]); } private function resolved(array $s,object $original,array $source):bool { if(json_decode($original->snapshot,true)['source']['recognition_revision']===$source['recognition_revision'])return true; - if(config('cost_ledger.correction_enabled')!==true||!DB::getSchemaBuilder()->hasTable('gl_cost_correction_capability'))return false; - $r=DB::table('gl_correction_results')->where($s)->where('original_journal_id',$original->id)->first();if(!$r)return false;$j=DB::table('gl_journals')->where($s)->where('id',$r->replacement_journal_id)->first();if(!$j)return false;$current=json_decode($j->snapshot,true)['source'];return $current['effect_type']==='workshop.direct_cost.replacement'&&($current['cost_order_id']??null)===$source['cost_order_id']&&($current['replacement_source_revision']??null)===$source['recognition_revision']; + if(config('cost_ledger.correction_enabled')!==true||!SourceBook::has('gl_cost_correction_capability'))return false; + $r=SourceBook::table('gl_correction_results')->where($s)->where('original_journal_id',$original->id)->first();if(!$r)return false;$j=SourceBook::table('gl_journals')->where($s)->where('id',$r->replacement_journal_id)->first();if(!$j)return false;$current=json_decode($j->snapshot,true)['source'];return $current['effect_type']==='workshop.direct_cost.replacement'&&($current['cost_order_id']??null)===$source['cost_order_id']&&($current['replacement_source_revision']??null)===$source['recognition_revision']; } public function workspace(User $u,?int $order,int $page):array { - $s=$this->scope($u);$q=DB::table('gl_cost_sources')->where($s);$p=$q->orderBy('id')->paginate(25,['*'],'page',$page);$chosen=null;$source=null;$pending=null; - if($order!==null){$m=DB::table('gl_cost_sources')->where($s)->where('order_id',$order)->first()??abort(403,'Ordem fora do âmbito composto.');$chosen=$this->facts($u,$s,$m);try{$source=$this->eligible($chosen);}catch(\DomainException $e){$pending=$e->getMessage();}} - $original=$order!==null?DB::table('gl_journals')->where($s)->where('snapshot->source->effect_type','workshop.direct_cost.validated')->where('snapshot->source->cost_order_id',$order)->first():null;$state=$original?($source&&$this->resolved($s,$original,$source)?'current':'requires_correction'):'unposted'; + $s=$this->scope($u);$q=SourceBook::table('gl_cost_sources')->where($s);$p=$q->orderBy('id')->paginate(25,['*'],'page',$page);$chosen=null;$source=null;$pending=null; + if($order!==null){$m=SourceBook::table('gl_cost_sources')->where($s)->where('order_id',$order)->first()??abort(403,'Ordem fora do âmbito composto.');$chosen=$this->facts($u,$s,$m);try{$source=$this->eligible($chosen);}catch(\DomainException $e){$pending=$e->getMessage();}} + $original=$order!==null?SourceBook::table('gl_journals')->where($s)->where('snapshot->source->effect_type','workshop.direct_cost.validated')->where('snapshot->source->cost_order_id',$order)->first():null;$state=$original?($source&&$this->resolved($s,$original,$source)?'current':'requires_correction'):'unposted'; return ['scope'=>$s,'orders'=>$p->items(),'pagination'=>['page'=>$page,'total'=>$p->total(),'last_page'=>$p->lastPage()],'order_id'=>$order,'facts'=>$chosen,'source'=>$source,'pending'=>$pending,'original_journal'=>$original,'accounting_state'=>$state]; } } diff --git a/app/Modules/Accounting/GeneralLedgerController.php b/app/Modules/Accounting/GeneralLedgerController.php index 4b359f5..4e9aa63 100644 --- a/app/Modules/Accounting/GeneralLedgerController.php +++ b/app/Modules/Accounting/GeneralLedgerController.php @@ -7,14 +7,17 @@ final class GeneralLedgerController extends Controller { public function financialReceipt(Request $r,string $domain,string $key,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->receipt($r,$key,$domain); abort_if($r->query()!==[],422);try{$data=$ledger->financialReceipt($r->user(),$domain,$key);}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(403,$e->getMessage());}abort_if($data===null,404,'Comprovativo não encontrado neste domínio, contexto e captura.');return response()->json($data)->header('Cache-Control','no-store'); } public function receipt(Request $r,string $key,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->receipt($r,$key); abort_if($r->query()!==[],422);try{$data=$ledger->receipt($r->user(),$key);}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(403,$e->getMessage());}abort_if($data===null,404,'Comprovativo não encontrado neste contexto e nesta captura.');return response()->json($data)->header('Cache-Control','no-store'); } public function report(Request $r,string $kind,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->report($r,$kind,app(ComposedLedger::class)); $d=$r->validate(['from_on'=>'required|date_format:Y-m-d','to_on'=>'required|date_format:Y-m-d|after_or_equal:from_on','account_id'=>'sometimes|required|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']); abort_if(array_diff(array_keys($r->query()),array_keys($d))!==[],422); try{$data=$ledger->report($r->user(),$kind,$d);} @@ -30,16 +33,19 @@ final class GeneralLedgerController extends Controller } public function index(Request $r,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->page($r,app(ComposedLedger::class)); try{return response()->json($ledger->listing($r->user(),$this->options($r)))->header('Cache-Control','no-store');} catch(\DomainException $e){abort(403,$e->getMessage());} } public function page(Request $r,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->page($r,app(ComposedLedger::class)); try{return response()->view('accounting.general-ledger',['data'=>$ledger->listing($r->user(),$this->options($r))])->header('Cache-Control','no-store');} catch(\DomainException $e){abort(403,$e->getMessage());} } public function command(Request $r,string $action,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->command($r,$action,app(ComposedLedger::class)); if(!in_array($action,['prepare','validate','post'],true))abort_unless(config('general_ledger.cycle_enabled')===true,403); try{return response()->json($ledger->engine($r->user())->command($action,$r->all(),(string)$r->header('Idempotency-Key')))->header('Cache-Control','no-store');} catch(\InvalidArgumentException $e){abort(422,$e->getMessage());} @@ -47,17 +53,20 @@ final class GeneralLedgerController extends Controller } public function cycle(Request $r,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->page($r,app(ComposedLedger::class)); $d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),['page'])!==[],422); $page=(int)($d['page']??1);$data=$ledger->cycle($r->user(),$page); return response(\Tempo\Finance\GeneralLedger\CycleWorkspace::render($data,['host'=>'ERP SGF','csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token'),'scope'=>$r->session()->get('company_context'),'financial_url'=>'/supplier-payments','return_url'=>'/general-ledger','page_url'=>'/general-ledger/cycle']))->header('Cache-Control','no-store'); } public function cycleData(Request $r,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->page($r,app(ComposedLedger::class)); $d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),['page'])!==[],422); return response()->json($ledger->cycle($r->user(),(int)($d['page']??1)))->header('Cache-Control','no-store'); } public function flow(Request $r,GeneralLedger $ledger) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->flow($r); $d=$r->validate(['source'=>'required|integer|min:1']); abort_if(array_diff(array_keys($r->query()),['source'])!==[],422); return response()->view('accounting.payable-flow',['flow'=>$ledger->flow($r->user(),(int)$d['source'])])->header('Cache-Control','no-store'); diff --git a/app/Modules/Accounting/IndustrialLedgerController.php b/app/Modules/Accounting/IndustrialLedgerController.php index 61676ba..da9ede9 100644 --- a/app/Modules/Accounting/IndustrialLedgerController.php +++ b/app/Modules/Accounting/IndustrialLedgerController.php @@ -3,5 +3,6 @@ namespace App\Modules\Accounting; use App\Http\Controllers\Controller; use Illuminate\Http\Request; final class IndustrialLedgerController extends Controller{ - public function index(Request $r,GeneralLedger $ledger){$d=$r->validate(['lot_id'=>'sometimes|required|integer|min:1','page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);try{$data=$ledger->industrial($r->user(),isset($d['lot_id'])?(int)$d['lot_id']:null,(int)($d['page']??1));}catch(\DomainException $e){abort(403,$e->getMessage());}return $r->is('api/*')?response()->json($data)->header('Cache-Control','no-store'):response()->view('accounting.industrial-ledger',['data'=>$data])->header('Cache-Control','no-store');} + public function index(Request $r,GeneralLedger $ledger){if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->focused($r,'industrial');$d=$r->validate(['lot_id'=>'sometimes|required|integer|min:1','page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);try{$data=$ledger->industrial($r->user(),isset($d['lot_id'])?(int)$d['lot_id']:null,(int)($d['page']??1));}catch(\DomainException $e){abort(403,$e->getMessage());}return $r->is('api/*')?response()->json($data)->header('Cache-Control','no-store'):response()->view('accounting.industrial-ledger',['data'=>$data])->header('Cache-Control','no-store');} } + diff --git a/app/Modules/Accounting/IndustrialLedgerSource.php b/app/Modules/Accounting/IndustrialLedgerSource.php index ba04dcd..9592fa0 100644 --- a/app/Modules/Accounting/IndustrialLedgerSource.php +++ b/app/Modules/Accounting/IndustrialLedgerSource.php @@ -10,7 +10,7 @@ use Tempo\Finance\Payables\Payables; final class IndustrialLedgerSource { private function scope(User $u):array{ - abort_unless(config('industrial_ledger.synthetic_enabled')===true,403);IndustrialCosts::guard();$c=app(CompanyContext::class);abort_unless(auth()->id()===$u->id&&$c->userId()===$u->id&&$c->unitId(),403);$s=$c->identifiers()+['unit_id'=>$c->unitId()];app(Permissions::class)->require($u->id,$s['tenant_id'],$s['company_id'],'industrial_costs.view',$s['unit_id']);foreach(['gl_industrial_sources','ic_lots','ic_results','ic_inputs','ic_decisions'] as $t)abort_unless(DB::getSchemaBuilder()->hasTable($t),503,'Origem industrial indisponível.');return $s; + abort_unless(config('industrial_ledger.synthetic_enabled')===true,403);IndustrialCosts::guard();$c=app(CompanyContext::class);abort_unless(auth()->id()===$u->id&&$c->userId()===$u->id&&$c->unitId(),403);$s=$c->identifiers()+['unit_id'=>$c->unitId()];app(Permissions::class)->require($u->id,$s['tenant_id'],$s['company_id'],'industrial_costs.view',$s['unit_id']);foreach(['gl_industrial_sources','ic_lots','ic_results','ic_inputs','ic_decisions'] as $t)abort_unless(str_starts_with($t,'gl_')?SourceBook::has($t):DB::getSchemaBuilder()->hasTable($t),503,'Origem industrial indisponível.');return $s; } private function facts(User $u,object $map):array{return ['mapping'=>(array)$map,'conference'=>app(IndustrialCosts::class)->conference($map->lot_id,$u->id)];} private function eligible(array $s,array $facts):array{ @@ -19,24 +19,24 @@ final class IndustrialLedgerSource $origins=[];foreach(['material','labour','pool','machine','production'] as $kind)$origins[$kind]=$snap['origins'][$kind]['source_ref'];if(Payables::canonical($origins)!==Payables::canonical(json_decode($m['origins'],true)))throw new \DomainException('Perímetro industrial alterado; nova conferência necessária.'); if(count(array_unique($origins))!==5)throw new \DomainException('Origem industrial ambígua.'); if(DB::table('ic_inputs')->where('tenant_id',$s['tenant_id'])->where('company_id',$s['company_id'])->where('lot_id','<>',$m['lot_id'])->whereIn('source_ref',array_values($origins))->exists())throw new \DomainException('Origem reutilizada entre lotes.'); - if(DB::getSchemaBuilder()->hasTable('gl_cost_sources')&&DB::table('gl_cost_sources')->where('tenant_id',$s['tenant_id'])->where('company_id',$s['company_id'])->where(fn($q)=>$q->whereIn('material_origin',array_values($origins))->orWhereIn('time_origin',array_values($origins)))->exists())throw new \DomainException('Origem já composta como custo directo; não reconhecer novamente.'); + if(SourceBook::has('gl_cost_sources')&&SourceBook::table('gl_cost_sources')->where('tenant_id',$s['tenant_id'])->where('company_id',$s['company_id'])->where(fn($q)=>$q->whereIn('material_origin',array_values($origins))->orWhereIn('time_origin',array_values($origins)))->exists())throw new \DomainException('Origem já composta como custo directo; não reconhecer novamente.'); try{$amount=BigRational::of($snap['selected_total_exact'])->multipliedBy(100)->toBigInteger()->toInt();}catch(\Brick\Math\Exception\MathException $e){throw new \DomainException('Montante industrial não representável em cêntimos; sem arredondamento.',0,$e);}if($amount<=0)throw new \DomainException('Montante seleccionado deve ser positivo.'); return ['effect_type'=>'industry.cost.validated','source_id'=>$m['lot_id'],'industrial_lot_id'=>$m['lot_id'],'obligation_id'=>$m['lot_id'],'origin_key'=>hash('sha256',Payables::canonical(['scope'=>$s,'lot'=>$m['lot_id'],'origins'=>$origins])),'recognition_revision'=>hash('sha256',Payables::canonical($facts)),'principal_minor'=>$amount,'currency'=>'MZN','tax_minor'=>0,'compatible'=>true,'history_imported'=>false,'permitted_period_key'=>$m['period_key'],'period_start'=>$c['lot']['period_start'],'period_end'=>$c['lot']['period_end'],'result_id'=>$r['id'],'result_version'=>$r['version'],'document_revision'=>$r['fingerprint'],'good_units'=>$snap['good_units'],'unit_cost_exact'=>$snap['unit_cost_exact'],'residual_exact'=>$snap['residual_exact'],'industrial_evidence'=>$facts,'label'=>'Resultado industrial seleccionado de ensaio; residual separado, sem stock ou salários contabilizados']; } - public function source(User $u,int $lot):array{$s=$this->scope($u);$m=DB::table('gl_industrial_sources')->where($s)->where('lot_id',$lot)->first()??throw new \DomainException('Lote fora do âmbito composto.');return $this->eligible($s,$this->facts($u,$m));} + public function source(User $u,int $lot):array{$s=$this->scope($u);$m=SourceBook::table('gl_industrial_sources')->where($s)->where('lot_id',$lot)->first()??throw new \DomainException('Lote fora do âmbito composto.');return $this->eligible($s,$this->facts($u,$m));} public function correctionSource(User $u,string $kind,int $id):array{ - abort_unless(config('industrial_ledger.correction_enabled')===true,403);$s=$this->scope($u);abort_unless(DB::getSchemaBuilder()->hasTable('gl_industrial_correction_capability'),503);if(!in_array($kind,['industry.cost.reversal','industry.cost.replacement'],true))throw new \DomainException('Tipo de correcção industrial inválido.');$j=DB::table('gl_journals')->where($s)->where('id',$id)->first()??throw new \DomainException('Original fora do âmbito.');$old=json_decode($j->snapshot,true);if($old['source']['effect_type']!=='industry.cost.validated')throw new \DomainException('Apenas o original industrial admite esta correcção.');$base=$this->source($u,(int)$old['source']['industrial_lot_id']);if($base['origin_key']!==$old['source']['origin_key']||$base['permitted_period_key']!==$old['period']['period_key']||$base['recognition_revision']===$old['source']['recognition_revision'])throw new \DomainException('Origem/perímetro/período sem sucessora compatível.');return array_replace($base,['effect_type'=>$kind,'source_id'=>$id,'amount_minor'=>$kind==='industry.cost.reversal'?(int)$j->debit_minor:$base['principal_minor'],'replacement_source_revision'=>$base['recognition_revision'],'recognition_revision'=>hash('sha256',Payables::canonical(['current_source'=>$base,'original'=>(array)$j,'kind'=>$kind])),'original_journal_id'=>$id,'original_journal_on'=>$j->journal_on,'original_journal_sha256'=>hash('sha256',Payables::canonical((array)$j)),'original_industrial_source'=>$old['source']]); + abort_unless(config('industrial_ledger.correction_enabled')===true,403);$s=$this->scope($u);abort_unless(SourceBook::has('gl_industrial_correction_capability'),503);if(!in_array($kind,['industry.cost.reversal','industry.cost.replacement'],true))throw new \DomainException('Tipo de correcção industrial inválido.');$j=SourceBook::table('gl_journals')->where($s)->where('id',$id)->first()??throw new \DomainException('Original fora do âmbito.');$old=json_decode($j->snapshot,true);if($old['source']['effect_type']!=='industry.cost.validated')throw new \DomainException('Apenas o original industrial admite esta correcção.');$base=$this->source($u,(int)$old['source']['industrial_lot_id']);if($base['origin_key']!==$old['source']['origin_key']||$base['permitted_period_key']!==$old['period']['period_key']||$base['recognition_revision']===$old['source']['recognition_revision'])throw new \DomainException('Origem/perímetro/período sem sucessora compatível.');return array_replace($base,['effect_type'=>$kind,'source_id'=>$id,'amount_minor'=>$kind==='industry.cost.reversal'?(int)$j->debit_minor:$base['principal_minor'],'replacement_source_revision'=>$base['recognition_revision'],'recognition_revision'=>hash('sha256',Payables::canonical(['current_source'=>$base,'original'=>(array)$j,'kind'=>$kind])),'original_journal_id'=>$id,'original_journal_on'=>$j->journal_on,'original_journal_sha256'=>hash('sha256',Payables::canonical((array)$j)),'original_industrial_source'=>$old['source']]); } private function resolved(array $s,object $j,array $source):bool{ - if($j->source_revision===$source['recognition_revision'])return true;if(config('industrial_ledger.correction_enabled')!==true||!DB::getSchemaBuilder()->hasTable('gl_industrial_correction_capability'))return false;$r=DB::table('gl_correction_results')->where($s)->where('original_journal_id',$j->id)->first();if(!$r)return false;$replacement=DB::table('gl_journals')->where($s)->where('id',$r->replacement_journal_id)->first();if(!$replacement)return false;$v=json_decode($replacement->snapshot,true)['source'];return $v['effect_type']==='industry.cost.replacement'&&($v['industrial_lot_id']??null)===$source['industrial_lot_id']&&($v['replacement_source_revision']??null)===$source['recognition_revision']; + if($j->source_revision===$source['recognition_revision'])return true;if(config('industrial_ledger.correction_enabled')!==true||!SourceBook::has('gl_industrial_correction_capability'))return false;$r=SourceBook::table('gl_correction_results')->where($s)->where('original_journal_id',$j->id)->first();if(!$r)return false;$replacement=SourceBook::table('gl_journals')->where($s)->where('id',$r->replacement_journal_id)->first();if(!$replacement)return false;$v=json_decode($replacement->snapshot,true)['source'];return $v['effect_type']==='industry.cost.replacement'&&($v['industrial_lot_id']??null)===$source['industrial_lot_id']&&($v['replacement_source_revision']??null)===$source['recognition_revision']; } public function period(User $u,array $period):array{ - $s=$this->scope($u);$h=hash_init('sha256');$facts=0;$pending=0;foreach(DB::table('gl_industrial_sources')->where($s)->where('period_key',$period['period_key'])->orderBy('id')->cursor() as $m){$v=$this->facts($u,$m);hash_update($h,Payables::canonical($v));$facts++;try{$source=$this->eligible($s,$v);$j=DB::table('gl_journals')->where($s)->where('snapshot->source->effect_type','industry.cost.validated')->where('snapshot->source->industrial_lot_id',$m->lot_id)->first();if(!$j||!$this->resolved($s,$j,$source)||$source['period_start']<$period['starts_on']||$source['period_end']>$period['ends_on'])$pending++;}catch(\DomainException){$pending++;}}return ['facts'=>$facts,'pending'=>$pending,'hash'=>hash_final($h)]; + $s=$this->scope($u);$h=hash_init('sha256');$facts=0;$pending=0;foreach(SourceBook::table('gl_industrial_sources')->where($s)->where('period_key',$period['period_key'])->orderBy('id')->cursor() as $m){$v=$this->facts($u,$m);hash_update($h,Payables::canonical($v));$facts++;try{$source=$this->eligible($s,$v);$j=SourceBook::table('gl_journals')->where($s)->where('snapshot->source->effect_type','industry.cost.validated')->where('snapshot->source->industrial_lot_id',$m->lot_id)->first();if(!$j||!$this->resolved($s,$j,$source)||$source['period_start']<$period['starts_on']||$source['period_end']>$period['ends_on'])$pending++;}catch(\DomainException){$pending++;}}return ['facts'=>$facts,'pending'=>$pending,'hash'=>hash_final($h)]; } public function workspace(User $u,?int $lot,int $page):array{ - $s=$this->scope($u);$p=DB::table('gl_industrial_sources')->where($s)->orderBy('lot_id')->paginate(25,['lot_id','period_key'],'page',$page);$facts=null;$source=null;$pending=null;$journal=null;if($lot!==null){$m=DB::table('gl_industrial_sources')->where($s)->where('lot_id',$lot)->first()??abort(403,'Lote fora do âmbito.');$facts=$this->facts($u,$m);try{$source=$this->eligible($s,$facts);}catch(\DomainException $e){$pending=$e->getMessage();}$journal=DB::table('gl_journals')->where($s)->where('snapshot->source->effect_type','industry.cost.validated')->where('snapshot->source->industrial_lot_id',$lot)->first();}return ['scope'=>$s,'lots'=>$p->items(),'pagination'=>['page'=>$page,'total'=>$p->total(),'last_page'=>$p->lastPage()],'lot_id'=>$lot,'facts'=>$facts,'source'=>$source,'pending'=>$pending,'journal'=>$journal,'accounting_state'=>$journal?($source&&$this->resolved($s,$journal,$source)?'current':'requires_future_correction'):'unposted']; + $s=$this->scope($u);$p=SourceBook::table('gl_industrial_sources')->where($s)->orderBy('lot_id')->paginate(25,['lot_id','period_key'],'page',$page);$facts=null;$source=null;$pending=null;$journal=null;if($lot!==null){$m=SourceBook::table('gl_industrial_sources')->where($s)->where('lot_id',$lot)->first()??abort(403,'Lote fora do âmbito.');$facts=$this->facts($u,$m);try{$source=$this->eligible($s,$facts);}catch(\DomainException $e){$pending=$e->getMessage();}$journal=SourceBook::table('gl_journals')->where($s)->where('snapshot->source->effect_type','industry.cost.validated')->where('snapshot->source->industrial_lot_id',$lot)->first();}return ['scope'=>$s,'lots'=>$p->items(),'pagination'=>['page'=>$page,'total'=>$p->total(),'last_page'=>$p->lastPage()],'lot_id'=>$lot,'facts'=>$facts,'source'=>$source,'pending'=>$pending,'journal'=>$journal,'accounting_state'=>$journal?($source&&$this->resolved($s,$journal,$source)?'current':'requires_future_correction'):'unposted']; } public function cards(User $u,int $page):array{ - $s=$this->scope($u);$p=DB::table('gl_industrial_sources')->where($s)->orderBy('lot_id')->paginate(25,['lot_id','period_key'],'industrial_page',$page);$ids=array_column($p->items(),'lot_id');$results=DB::table('ic_results as r')->where(array_combine(array_map(fn($k)=>'r.'.$k,array_keys($s)),array_values($s)))->whereIn('r.lot_id',$ids)->whereNotExists(fn($q)=>$q->selectRaw('1')->from('ic_results as n')->whereColumn('n.previous_id','r.id'))->get(['r.id','r.lot_id','r.version'])->keyBy('lot_id');$decisions=DB::table('ic_decisions')->whereIn('result_id',$results->pluck('id')->all())->get(['result_id','decision'])->keyBy('result_id');$posted=DB::table('gl_journals')->where($s)->where('snapshot->source->effect_type','industry.cost.validated')->whereIn('snapshot->source->industrial_lot_id',$ids)->select('id')->selectRaw("snapshot->'source'->>'industrial_lot_id' AS lot_id")->get()->keyBy('lot_id');$rows=[];foreach($p->items() as $m){$r=$results->get($m->lot_id);$rows[]=['lot_id'=>$m->lot_id,'period_key'=>$m->period_key,'result_id'=>$r?->id,'result_version'=>$r?->version,'recorded_decision'=>$r?$decisions->get($r->id)?->decision:null,'original_journal_id'=>$posted->get($m->lot_id)?->id,'current_dependency_conference_required'=>true];}return ['allowed'=>true,'rows'=>$rows,'pagination'=>['page'=>$page,'last_page'=>$p->lastPage(),'total'=>$p->total(),'per_page'=>25]]; + $s=$this->scope($u);$p=SourceBook::table('gl_industrial_sources')->where($s)->orderBy('lot_id')->paginate(25,['lot_id','period_key'],'industrial_page',$page);$ids=array_column($p->items(),'lot_id');$results=DB::table('ic_results as r')->where(array_combine(array_map(fn($k)=>'r.'.$k,array_keys($s)),array_values($s)))->whereIn('r.lot_id',$ids)->whereNotExists(fn($q)=>$q->selectRaw('1')->from('ic_results as n')->whereColumn('n.previous_id','r.id'))->get(['r.id','r.lot_id','r.version'])->keyBy('lot_id');$decisions=DB::table('ic_decisions')->whereIn('result_id',$results->pluck('id')->all())->get(['result_id','decision'])->keyBy('result_id');$posted=SourceBook::table('gl_journals')->where($s)->where('snapshot->source->effect_type','industry.cost.validated')->whereIn('snapshot->source->industrial_lot_id',$ids)->select('id')->selectRaw("snapshot->'source'->>'industrial_lot_id' AS lot_id")->get()->keyBy('lot_id');$rows=[];foreach($p->items() as $m){$r=$results->get($m->lot_id);$rows[]=['lot_id'=>$m->lot_id,'period_key'=>$m->period_key,'result_id'=>$r?->id,'result_version'=>$r?->version,'recorded_decision'=>$r?$decisions->get($r->id)?->decision:null,'original_journal_id'=>$posted->get($m->lot_id)?->id,'current_dependency_conference_required'=>true];}return ['allowed'=>true,'rows'=>$rows,'pagination'=>['page'=>$page,'last_page'=>$p->lastPage(),'total'=>$p->total(),'per_page'=>25]]; } } diff --git a/app/Modules/Accounting/InternalStatementTotals.php b/app/Modules/Accounting/InternalStatementTotals.php new file mode 100644 index 0000000..975338a --- /dev/null +++ b/app/Modules/Accounting/InternalStatementTotals.php @@ -0,0 +1,20 @@ +groups[$currency][$c]??=['accounts'=>0,'opening_net_minor'=>'0','debit_minor'=>'0','credit_minor'=>'0','closing_net_minor'=>'0','inverted_accounts'=>0];$g=&$this->groups[$currency][$category];$g['accounts']++; + foreach(['opening_net_minor','debit_minor','credit_minor','closing_net_minor']as$key)$g[$key]=(string)BigInteger::of($g[$key])->plus($row[$key]); + if(($category==='asset'&&BigInteger::of($row['closing_net_minor'])->isNegative())||($category==='liability'&&BigInteger::of($row['closing_net_minor'])->isPositive()))$g['inverted_accounts']++; + } + public function result():array + { + $out=[];foreach($this->groups as$currency=>$groups){foreach($groups as$category=>&$g){$net=BigInteger::of($g['debit_minor'])->minus($g['credit_minor']);$g['period_net_minor']=(string)$net;$g['presentation_minor']=$g['accounts']===0?null:(string)(match($category){'liability'=>BigInteger::of($g['closing_net_minor'])->negated(),'income'=>$net->negated(),'expense'=>$net,default=>BigInteger::of($g['closing_net_minor'])});}unset($g);$known=BigInteger::of($groups['income']['presentation_minor']??'0')->minus($groups['expense']['presentation_minor']??'0');$out[$currency]=['groups'=>$groups,'known_result_subtotal_minor'=>(string)$known,'complete_result_minor'=>$groups['unclassified']['accounts']===0?(string)$known:null,'equity_minor'=>null,'complete_balance_sheet'=>false,'unclassified_accounts'=>$groups['unclassified']['accounts']];}return$out; + } +} diff --git a/app/Modules/Accounting/InternalStatementsController.php b/app/Modules/Accounting/InternalStatementsController.php new file mode 100644 index 0000000..8d2f830 --- /dev/null +++ b/app/Modules/Accounting/InternalStatementsController.php @@ -0,0 +1,88 @@ +validate();} + private function run(Request $r,string $action,\Closure $fn):mixed + { + abort_unless(config('internal_statements.enabled')===true,404);$host=DB::connection();$hostId=(int)$r->user()->id;$identity=$host->table('users')->where('id',$hostId)->first(['id','email','name']);$identityHash=$this->hash((array)$identity);$generation=$this->hash($r->session()->get('company_context',[])); + try{return app(ComposedLedger::class)->run('view',function($engine,$actor,$binding,$origin,$scope)use($action,$fn,$host,$hostId,$identityHash,$generation){$meta=DB::table('fs_meta')->where('policy',self::POLICY)->first();abort_unless($meta&&DB::connection()->getDatabaseName()==='compras_receipts_test_'.$meta->composition&&hash_equals($meta->source_hash,(string)config('internal_statements.schema_sha256')),503,'Schema de demonstrações não conferido.');if($action!=='view')abort_unless($hostId===($action==='prepare'?15:16)&&$host->table('permission_grants')->where($origin['sgf_host_scope']+['user_id'=>$hostId,'permission'=>'general_ledger.statements.'.$action])->lockForUpdate()->first(),403);$person=DB::table('pa_access')->where($scope+['user_id'=>$actor->id,'action'=>'general_ledger.view'])->lockForUpdate()->value('person');abort_unless(is_string($person)&&$person!=='',403);$freshIdentity=$host->table('users')->where('id',$hostId)->lockForUpdate()->first(['id','email','name']);abort_unless($freshIdentity,403);$identityHash=$this->hash((array)$freshIdentity);return$fn($engine,$actor,$scope,$hostId,$person,$identityHash,$generation,$this->hash([$origin['binding_revision'],$origin['sgf_host_scope']]),$host,$origin);});}catch(\InvalidArgumentException$e){abort(422,$e->getMessage());}catch(\DomainException$e){abort(403,$e->getMessage());} + } + private function rows(array $s){return DB::table('fs_versions')->where($s);} + private function row(?object $r):?array{if(!$r)return null;$v=(array)$r;$v['data']=json_decode($v['data'],true,64,JSON_THROW_ON_ERROR);return$v;} + private function latest(array $s,string $kind,bool $approved=false):?array{$q=$this->rows($s)->where('kind',$kind);if($approved)$q->where('state','approved');return$this->row($q->orderByDesc('version')->first());} + private function plan(array $s):array{$v=DB::table('gl_accounts')->where($s)->selectRaw('count(*) accounts,coalesce(max(id),0) last_id')->first();return(array)$v;} + private function mapping(array $s,array $entries):array + { + $ids=array_column($entries,'account_id');$accounts=DB::table('gl_accounts')->where($s)->whereIn('id',$ids)->get(['id','code','label','currency'])->keyBy('id');abort_unless(count($ids)===$accounts->count(),403,'Conta fora do livro autorizado.');$out=[];foreach($entries as$e){$a=(array)$accounts[$e['account_id']];$out[]=['account_id'=>$e['account_id'],'category'=>$e['category'],'account'=>$a,'account_hash'=>$this->hash($a)];}usort($out,fn($a,$b)=>$a['account_id']<=>$b['account_id']);return$out; + } + private function validateMapping(array $s,array $mapping):void + { + $fresh=$this->mapping($s,array_map(fn($e)=>array_intersect_key($e,array_flip(['account_id','category'])),$mapping));abort_unless($this->hash($fresh)===$this->hash($mapping),409,'Conta ou correspondência alterada; exige revisão.'); + } + private function input(array $v):array + { + if(isset($v['reason'])&&is_string($v['reason']))$v['reason']=trim($v['reason']);$d=$this->exact($v,['kind'=>'required|in:mapping,result','action'=>'required|in:prepare,review','version'=>'required|integer|min:0','reason'=>'required|string|min:10|max:2000','data'=>'present|array']);$d['version']=(int)$d['version']; + if($d['action']==='review')$d['data']=$this->exact($d['data'],['decision'=>'required|in:approved,refused']);elseif($d['kind']==='mapping'){$d['data']=$this->exact($d['data'],['entries'=>'required|array|min:1|max:100','evidence'=>'required|string|min:10|max:2000']);$seen=[];foreach($d['data']['entries']as&$e){$e=$this->exact($e,['account_id'=>'required|integer|min:1','category'=>'required|in:asset,liability,income,expense,unclassified']);$e['account_id']=(int)$e['account_id'];abort_if(isset($seen[$e['account_id']]),422,'Conta repetida.');$seen[$e['account_id']]=true;}unset($e);}else$d['data']=$this->exact($d['data'],['mapping_id'=>'required|integer|min:1','from_on'=>'required|date_format:Y-m-d','to_on'=>'required|date_format:Y-m-d|after_or_equal:from_on','per_page'=>'required|integer|in:25,100','evidence'=>'required|string|min:10|max:2000']);return$d; + } + private function basis(array $s,string $kind,string $identity):array{return ['last'=>$this->latest($s,$kind),'mapping'=>$this->latest($s,'mapping',true),'mapping_latest'=>$this->latest($s,'mapping'),'plan'=>$this->plan($s),'identity'=>$identity];} + private function capture($engine,array $s,array $mapping,array $d,?string $persistKey=null):array + { + $this->validateMapping($s,$mapping['data']['entries']);$map=collect($mapping['data']['entries'])->keyBy('account_id');$totals=new InternalStatementTotals();$digest=hash_init('sha256');$first=null;$query=array_intersect_key($d,array_flip(['from_on','to_on','per_page']));$plan=$this->plan($s);$count=0; + for($page=1;;$page++){$report=$engine->trialBalance($query+['page'=>$page]);if(!$first)$first=$report;else abort_unless($this->hash($first['basis'])===$this->hash($report['basis'])&&$this->hash($first['totals'])===$this->hash($report['totals']),409,'Livro mudou entre páginas.');foreach($report['rows']as$row){$entry=$map->get($row['account_id']);$category=$entry['category']??'unclassified';$totals->add($row,$category);$line=$row+['category'=>$category];foreach(['opening_net_minor','debit_minor','credit_minor','closing_net_minor']as$key)$line[$key]=(string)$line[$key];hash_update($digest,Payables::canonical($line)."\n");if($persistKey)DB::table('fs_result_lines')->insert($s+['snapshot_key'=>$persistKey]+$line);$count++;}if($page>=$report['pagination']['last_page'])break;} + $current=DB::table('gl_journals')->where($s)->where('journal_on','<=',$d['to_on'])->selectRaw('count(*) journals,coalesce(max(id),0) last_journal_id')->first();abort_unless((int)$current->journals===$first['basis']['journals']&&(int)$current->last_journal_id===$first['basis']['last_journal_id']&&$this->hash($plan)===$this->hash($this->plan($s))&&$count===$first['totals']['accounts'],409,'Fonte mudou durante a preparação.'); + $summary=['query'=>['from_on'=>$d['from_on'],'to_on'=>$d['to_on']],'mapping_id'=>$mapping['id'],'mapping_version'=>$mapping['version'],'basis'=>$first['basis'],'original_totals'=>array_map('strval',$first['totals']),'currencies'=>$totals->result(),'accounts'=>$count,'plan'=>$plan,'rows_sha256'=>hash_final($digest),'synthetic_only'=>true,'legal_model'=>false];return['summary'=>$summary,'source_basis'=>$this->hash($summary)]; + } + private function csv(array $s,string $key,array $data):array + { + $stream=fopen('php://temp/maxmemory:1048576','w+');$h=hash_init('sha256');$write=function(array $row)use($stream,$h){$f=fopen('php://memory','w+');fputcsv($f,$row,';','"','',"\r\n");rewind($f);$bytes=stream_get_contents($f);fclose($f);fwrite($stream,$bytes);hash_update($h,$bytes);}; + $write(['DEMONSTRACOES INTERNAS PARCIAIS QA — SEM MODELO LEGAL']);$write(['Snapshot',$key,'Versao de resultado',$data['result_version'],'Observacao',$data['frozen_at']]);$write(['Datas',$data['summary']['query']['from_on'],$data['summary']['query']['to_on'],'Correspondencias',$data['summary']['mapping_version']]);$write(['Capital proprio','INDETERMINADO','Nao presumir zero ou completar equacao patrimonial']);$write(['Rubricas/pendencias',Payables::canonical($data['summary']['currencies'])]);$write(['Fundamento',"'".$data['evidence']]);$write(['Conta','Descricao','Moeda','Rubrica','Abertura liquida minor','Debito minor','Credito minor','Fecho liquido minor']); + DB::table('fs_result_lines')->where($s+['snapshot_key'=>$key])->orderBy('code')->orderBy('account_id')->chunk(100,function($rows)use($write){foreach($rows as$r)$write(["'".$r->code,"'".$r->label,$r->currency,$r->category,$r->opening_net_minor,$r->debit_minor,$r->credit_minor,$r->closing_net_minor]);});rewind($stream);return['stream'=>$stream,'sha256'=>hash_final($h)]; + } + private function execute($engine,$actor,array $s,int $hostId,string $person,string $identity,array $d,array $b,string $key,string $stamp,$host,array $origin):array + { + $last=$b['last'];abort_unless(($last['version']??0)===$d['version'],409,'Versão alterada; confira novamente.');$version=$d['version']+1;$prepared=$hostId;$native=$actor->id;$preparedPerson=$person;$preparedIdentity=$identity; + if($d['action']==='prepare'){ + if($d['kind']==='mapping'){$entries=$this->mapping($s,$d['data']['entries']);$data=['entries'=>$entries,'evidence'=>$d['data']['evidence']];$source=$this->hash(['entries'=>$entries,'plan'=>$b['plan']]);} + else{$m=$b['mapping'];abort_unless($m&&$m['id']===(int)$d['data']['mapping_id'],409,'Escolha explicitamente as correspondências revistas actuais.');$captured=$this->capture($engine,$s,$m,$d['data'],$key);$data=['snapshot_key'=>$key,'result_version'=>$version,'frozen_at'=>$stamp,'evidence'=>$d['data']['evidence'],'summary'=>$captured['summary']];$csv=$this->csv($s,$key,$data);$data['csv_sha256']=$csv['sha256'];fclose($csv['stream']);$source=$captured['source_basis'];DB::table('fs_results')->insert($s+['snapshot_key'=>$key,'data'=>Payables::canonical($data),'csv_sha256'=>$data['csv_sha256'],'source_basis'=>$source,'created_at'=>$stamp]);} + $state='prepared'; + }else{ + abort_unless($last&&$last['state']==='prepared'&&$last['prepared_host_id']!==$hostId&&$last['prepared_native_id']!==$actor->id&&$last['prepared_person']!==$person,403,'Preparação actual e pessoa distinta obrigatórias.');$prepared=$last['prepared_host_id'];$native=$last['prepared_native_id'];$preparedPerson=$last['prepared_person'];$preparedIdentity=$last['preparer_identity'];$u=$host->table('users')->where('id',$prepared)->lockForUpdate()->first(['id','email','name']);abort_unless($u&&hash_equals($preparedIdentity,$this->hash((array)$u))&&$host->table('company_memberships')->where(['tenant_id'=>$origin['sgf_host_scope']['tenant_id'],'company_id'=>$origin['sgf_host_scope']['company_id'],'user_id'=>$prepared])->lockForUpdate()->first(),409,'Identidade/vínculo do preparador mudou.');abort_unless($host->table('permission_grants')->where($origin['sgf_host_scope']+['user_id'=>$prepared,'permission'=>'general_ledger.statements.prepare'])->lockForUpdate()->first(),403);$freshPerson=DB::table('pa_access')->where($s+['user_id'=>$native,'action'=>'general_ledger.view'])->lockForUpdate()->value('person');abort_unless($freshPerson===$preparedPerson,409,'Pessoa do preparador mudou.');$data=$last['data']; + if($d['kind']==='mapping'){$this->validateMapping($s,$data['entries']);$source=$this->hash(['entries'=>$data['entries'],'plan'=>$b['plan']]);}else{$m=$b['mapping'];abort_unless($m&&$m['id']===$data['summary']['mapping_id'],409,'Correspondências mudaram desde a preparação.');$source=$this->capture($engine,$s,$m,$data['summary']['query']+['per_page'=>100])['source_basis'];$csv=$this->csv($s,$data['snapshot_key'],$data);abort_unless(hash_equals($data['csv_sha256'],$csv['sha256']),409,'Snapshot congelado incompatível.');fclose($csv['stream']);}abort_unless(hash_equals($last['source_basis'],$source),409,'Contas, livro ou dependências mudaram desde a preparação.');$state=$d['data']['decision']; + } + $impact=['before'=>['version'=>$d['version'],'state'=>$last['state']??null],'after'=>['version'=>$version,'state'=>$state],'kind'=>$d['kind'],'data'=>$data,'new_journals'=>0,'accounts_changed'=>false,'legal_model'=>false];$id=DB::table('fs_versions')->insertGetId($s+['kind'=>$d['kind'],'version'=>$version,'previous_id'=>$last['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_host_id'=>$prepared,'prepared_native_id'=>$native,'prepared_person'=>$preparedPerson,'preparer_identity'=>$preparedIdentity,'host_actor_id'=>$hostId,'actor_id'=>$actor->id,'data'=>Payables::canonical($data),'source_basis'=>$source,'reason'=>$d['reason'],'created_at'=>now()->utc()]);return['id'=>$id,'kind'=>$d['kind'],'version'=>$version,'state'=>$state,'impact'=>$impact]; + } + public function preview(Request $r) + { + $d=$this->input($r->all());$key=strtolower((string)$r->header('Idempotency-Key'));$this->exact(['key'=>$key],['key'=>'required|uuid']);return$this->run($r,$d['action'],function($e,$a,$s,$h,$p,$identity,$g,$binding,$host,$origin)use($d,$key){$b=$this->basis($s,$d['kind'],$identity);$stamp=now()->utc()->toIso8601String();$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction();try{$out=$this->execute($e,$a,$s,$h,$p,$identity,$d,$b,$key,$stamp,$host,$origin);}finally{while($db->transactionLevel()>$level)$db->rollBack();}$cut=['input'=>$d,'actor'=>$h,'person'=>$p,'generation'=>$g,'binding'=>$binding,'key'=>$key,'stamp'=>$stamp,'observed'=>time(),'basis'=>$this->hash($b),'impact'=>$out['impact']];return response()->json(['preview'=>Crypt::encryptString(Payables::canonical($cut)),'impact'=>$out['impact']])->header('Cache-Control','no-store');}); + } + public function confirm(Request $r) + { + $v=$this->exact($r->all(),['preview'=>'required|string|max:131072']);try{$c=json_decode(Crypt::decryptString($v['preview']),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}abort_unless(is_array($c)&&is_array($c['input']??null),409);$d=$this->input($c['input']);return$this->run($r,$d['action'],function($e,$a,$s,$h,$p,$identity,$g,$binding,$host,$origin)use($c,$d){abort_unless($c['actor']===$h&&$c['person']===$p&&$c['generation']===$g&&$c['binding']===$binding&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Pessoa/contexto/prévia alterados.');$command=$s+['host_actor_id'=>$h,'command_key'=>$c['key']];$old=DB::table('fs_commands')->where($command)->first();if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d))&&hash_equals($old->binding_hash,$binding),409,'Chave usada com outros dados.');return response()->json(json_decode($old->result,true)+['replayed'=>true])->header('Cache-Control','no-store');}$b=$this->basis($s,$d['kind'],$identity);abort_unless(hash_equals($c['basis'],$this->hash($b)),409,'Versão, identidade ou correspondências mudaram.');$out=$this->execute($e,$a,$s,$h,$p,$identity,$d,$b,$c['key'],$c['stamp'],$host,$origin);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409,'Impacto mudou.');app(RequisitionWorkflow::class)->audit($a,'sgf.statements.command',null,['version'=>$d['version']],['host_actor_id'=>$h,'operation_key'=>$c['key'],'reason'=>$d['reason'],'after'=>$out]);DB::table('fs_commands')->insert($command+['actor_id'=>$a->id,'request_hash'=>$this->hash($d),'binding_hash'=>$binding,'input'=>Payables::canonical($d),'result'=>Payables::canonical($out),'created_at'=>now()->utc()]);return response()->json($out+['replayed'=>false],201)->header('Cache-Control','no-store');}); + } + public function receipt(Request $r,string $key) + { + abort_if($r->query()!==[],422);$this->exact(['key'=>$key],['key'=>'required|uuid']);return$this->run($r,'view',function($e,$a,$s,$h,$p,$identity,$g,$binding)use($key){$old=DB::table('fs_commands')->where($s+['host_actor_id'=>$h,'command_key'=>$key])->first();abort_unless($old,404);abort_unless(hash_equals($old->binding_hash,$binding),409);return response()->json(['result'=>json_decode($old->result,true),'created_at'=>$old->created_at,'reason'=>json_decode($old->input,true)['reason'],'operation_executed'=>false,'historical_receipt_only'=>true])->header('Cache-Control','no-store');}); + } + public function index(Request $r) + { + $d=$this->exact($r->query(),['view'=>'sometimes|in:history,accounts,result','kind'=>'sometimes|in:mapping,result','version_id'=>'required_if:view,result|prohibited_unless:view,result|integer|min:1','search'=>'sometimes|nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);return$this->run($r,'view',function($e,$a,$s,$h,$p,$identity,$g,$binding,$host,$origin)use($d){$view=$d['view']??'history';$size=(int)($d['per_page']??25);$page=(int)($d['page']??1);$lastMapping=$this->latest($s,'mapping');$mapping=$this->latest($s,'mapping',true);$lastResult=$this->latest($s,'result');$approvedResult=$this->latest($s,'result',true);$snapshot=null;$matches=null; + if($view==='accounts'){$q=DB::table('gl_accounts')->where($s);if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($q)=>$q->whereRaw("code ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("label ILIKE ? ESCAPE '!'",[$needle]));}$rows=$q->orderBy('code')->orderBy('id')->paginate($size,['id','code','label','currency'],'page',$page);} + elseif($view==='result'){$v=$this->row($this->rows($s)->where(['id'=>$d['version_id'],'kind'=>'result','state'=>'approved'])->first());abort_unless($v,403,'Resultado revisto fora do contexto.');$snapshot=$v['data'];$q=DB::table('fs_result_lines')->where($s+['snapshot_key'=>$snapshot['snapshot_key']]);$rows=$q->orderBy('code')->orderBy('account_id')->paginate($size,['*'],'page',$page);$current=$mapping&&$mapping['id']===$snapshot['summary']['mapping_id'];if($current){try{$now=$this->capture($e,$s,$mapping,$snapshot['summary']['query']+['per_page'=>100]);$current=hash_equals($v['source_basis'],$now['source_basis']);}catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$ex){if($ex->getStatusCode()!==409)throw$ex;$current=false;}}$matches=$current;} + else{$q=$this->rows($s);if(isset($d['kind']))$q->where('kind',$d['kind']);$rows=$q->orderByDesc('id')->paginate($size,['*'],'page',$page);$rows->setCollection($rows->getCollection()->map(fn($v)=>$this->row($v)));} + $can=fn($action,$expected)=>$h===$expected&&$host->table('permission_grants')->where($origin['sgf_host_scope']+['user_id'=>$h,'permission'=>'general_ledger.statements.'.$action])->exists();return response()->json(['policy'=>self::POLICY,'view'=>$view,'records'=>$rows,'mapping_latest'=>$lastMapping,'mapping_approved'=>$mapping,'result_latest'=>$lastResult,'result_approved'=>$approvedResult,'snapshot'=>$snapshot,'current_source_matches'=>$matches,'capabilities'=>['prepare'=>$can('prepare',15),'review'=>$can('review',16)],'notice'=>'Demonstrações internas parciais QA; classificação pendente visível, capital não presumido. Não é modelo legal.'])->header('Cache-Control','no-store');}); + } + public function export(Request $r,int $id) + { + abort_if($r->query()!==[],422);return$this->run($r,'view',function($e,$a,$s)use($id){$v=$this->row($this->rows($s)->where(['id'=>$id,'kind'=>'result','state'=>'approved'])->first());abort_unless($v,403,'Resultado não revisto ou fora do âmbito.');$data=$v['data'];$saved=DB::table('fs_results')->where($s+['snapshot_key'=>$data['snapshot_key']])->first();abort_unless($saved&&hash_equals($saved->csv_sha256,$data['csv_sha256']),409);$csv=$this->csv($s,$data['snapshot_key'],$data);if(!hash_equals($data['csv_sha256'],$csv['sha256'])){fclose($csv['stream']);abort(409,'Artefacto alterado.');}$stream=$csv['stream'];return response()->stream(function()use($stream){try{fpassthru($stream);}finally{fclose($stream);}},200,['Content-Type'=>'text/csv; charset=UTF-8','Content-Disposition'=>'attachment; filename="sgf-demonstracoes-internas-QA.csv"','X-Content-SHA256'=>$csv['sha256'],'Cache-Control'=>'no-store, private']);}); + } +} diff --git a/app/Modules/Accounting/LedgerCompositionController.php b/app/Modules/Accounting/LedgerCompositionController.php index ce30bd2..c10326e 100644 --- a/app/Modules/Accounting/LedgerCompositionController.php +++ b/app/Modules/Accounting/LedgerCompositionController.php @@ -6,6 +6,7 @@ final class LedgerCompositionController extends Controller { public function index(Request $r,LedgerComposition $workspace) { + if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->page($r,app(ComposedLedger::class)); $rules=['period_key'=>'sometimes|required|string|max:80'];foreach(['industrial_page','invoices_page','costs_page','suppliers_page','journals_page','checks_page'] as $k)$rules[$k]='sometimes|integer|min:1|max:100000';$d=$r->validate($rules);abort_if(array_diff(array_keys($r->query()),array_keys($d))!==[],422); try{$data=$workspace->workspace($r->user(),$d);}catch(\DomainException $e){abort(403,$e->getMessage());}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());} return $r->is('api/*')?response()->json($data)->header('Cache-Control','no-store'):response()->view('accounting.ledger-composition',['data'=>$data])->header('Cache-Control','no-store'); diff --git a/app/Modules/Accounting/SalesLedgerController.php b/app/Modules/Accounting/SalesLedgerController.php index 4c6c43c..531701e 100644 --- a/app/Modules/Accounting/SalesLedgerController.php +++ b/app/Modules/Accounting/SalesLedgerController.php @@ -11,6 +11,8 @@ final class SalesLedgerController extends Controller catch(\DomainException $e){abort(403,$e->getMessage());} catch(\InvalidArgumentException $e){abort(422,$e->getMessage());} } - public function page(Request $r,GeneralLedger $ledger){return response()->view('accounting.sales-ledger',['data'=>$this->data($r,$ledger)])->header('Cache-Control','no-store');} - public function index(Request $r,GeneralLedger $ledger){return response()->json($this->data($r,$ledger))->header('Cache-Control','no-store');} + public function page(Request $r,GeneralLedger $ledger){if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->focused($r,'sales');return response()->view('accounting.sales-ledger',['data'=>$this->data($r,$ledger)])->header('Cache-Control','no-store');} + public function index(Request $r,GeneralLedger $ledger){if(config('sgf_single_installation.ledger_enabled')===true)return app(ComposedLedgerController::class)->focused($r,'sales');return response()->json($this->data($r,$ledger))->header('Cache-Control','no-store');} } + + diff --git a/app/Modules/Accounting/SalesLedgerSource.php b/app/Modules/Accounting/SalesLedgerSource.php index d647a49..142cdf6 100644 --- a/app/Modules/Accounting/SalesLedgerSource.php +++ b/app/Modules/Accounting/SalesLedgerSource.php @@ -13,7 +13,7 @@ final class SalesLedgerSource { private function scope(User $u):array { - abort_unless(config('sales_ledger.synthetic_enabled')===true,403);ServiceReceivables::guard();foreach(['si_invoices','sr_records','sr_effects','gl_sales_capability'] as $table)abort_unless(DB::getSchemaBuilder()->hasTable($table),503,'Composição comercial contabilística não instalada.');$ctx=app(CompanyContext::class); + abort_unless(config('sales_ledger.synthetic_enabled')===true,403);ServiceReceivables::guard();foreach(['si_invoices','sr_records','sr_effects','gl_sales_capability'] as $table)abort_unless(str_starts_with($table,'gl_')?SourceBook::has($table):DB::getSchemaBuilder()->hasTable($table),503,'Composição comercial contabilística não instalada.');$ctx=app(CompanyContext::class); abort_unless(auth()->id()===$u->id&&$ctx->userId()===$u->id&&$ctx->unitId(),403);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId()]; foreach(['service_invoices.view','service_receivables.view'] as $p)app(Permissions::class)->require($u->id,$scope['tenant_id'],$scope['company_id'],$p,$scope['unit_id']);return $scope; } @@ -37,27 +37,38 @@ final class SalesLedgerSource public function source(User $u,string $kind,int $id):array { $scope=$this->scope($u);if($kind==='sales.invoice')return $this->invoice($u,$scope,$id); - $expected=match($kind){'receivable.allocated'=>'allocate','receivable.reversed'=>'reverse_allocation',default=>throw new \DomainException('Efeito comercial não suportado.')}; + $expected=match($kind){'receivable.allocated'=>'allocate','receivable.reversed'=>'reverse_allocation','receivable.reduced'=>'reduction','receivable.reduction_reversed'=>'reverse_reduction',default=>throw new \DomainException('Efeito comercial não suportado.')}; $e=$this->effectQuery($scope)->where('e.id',$id)->where('e.kind',$expected)->first()??throw new \DomainException('Afectação/reversão confirmada fora do âmbito.'); $ob=DB::table('sr_records')->where($scope)->where('id',$e->obligation_id)->where('kind','obligation')->first()??throw new \DomainException('Obrigação a receber indisponível.'); $base=$this->invoice($u,$scope,(int)$ob->invoice_id); + if($kind==='receivable.reduction_reversed'){ + $target=$this->effectQuery($scope)->where(['e.id'=>$e->target_effect_id,'e.kind'=>'reduction','e.obligation_id'=>$ob->id])->first();$r=DB::table('sr_records')->where($scope)->where(['id'=>$e->record_id,'kind'=>'correction'])->first();$cancel=DB::table('sd_cancellations')->where($scope)->where(['effect_id'=>$e->id,'state'=>'cancelled'])->first(); + if(!$target||!$r||!$cancel||$r->actor_id===$e->actor_id||$target->amount!==$e->amount||!SourceBook::table('gl_journals')->where($scope)->where('snapshot->source->effect_type','receivable.reduced')->where('snapshot->source->source_id',(int)$target->id)->exists())throw new \DomainException('Reversão sem crédito original contabilizado e anulação distinta.'); + return array_replace($base,['effect_type'=>$kind,'source_id'=>$id,'amount_minor'=>$this->minor($e->amount),'tax_minor'=>0,'recognition_revision'=>$this->hash(['invoice'=>$base['recognition_revision'],'target'=>(array)$target,'effect'=>(array)$e,'record'=>(array)$r,'cancellation'=>(array)$cancel]),'financial_effect'=>(array)$e]); + } + if($kind==='receivable.reduced'){ + $r=DB::table('sr_records')->where($scope)->where(['id'=>$e->record_id,'kind'=>'correction','invoice_id'=>$ob->invoice_id,'obligation_id'=>$ob->id])->first(); + $doc=DB::getSchemaBuilder()->hasTable('sd_outputs')?DB::table('sd_outputs')->where($scope)->where(['effect_id'=>$e->id,'invoice_id'=>$ob->invoice_id,'kind'=>'credit_note'])->first():null; + if(!$r||!$doc||$r->actor_id===$e->actor_id||$r->amount!==$e->amount||!hash_equals($doc->pdf_sha256,hash('sha256',base64_decode($doc->pdf_base64,true)))||!SourceBook::table('gl_journals')->where($scope)->where('snapshot->source->effect_type','sales.invoice')->where('snapshot->source->source_id',(int)$ob->invoice_id)->exists())throw new \DomainException('Crédito sem documento, decisão distinta ou factura original contabilizada.'); + return array_replace($base,['effect_type'=>$kind,'source_id'=>$id,'amount_minor'=>$this->minor($e->amount),'tax_minor'=>0,'invoice_tax_minor'=>$base['tax_minor'],'recognition_revision'=>$this->hash(['invoice'=>$base['recognition_revision'],'effect'=>(array)$e,'record'=>(array)$r,'credit_document'=>(array)$doc]),'financial_effect'=>(array)$e,'credit_document_id'=>$doc->id,'operational_nominal_reduction'=>true]); + } $receipt=DB::table('sr_records')->where($scope)->where('id',$e->receipt_id)->where(['kind'=>'receipt','obligation_id'=>$ob->id,'invoice_id'=>$ob->invoice_id])->first(); $confirmation=$receipt?$this->effectQuery($scope)->where('e.record_id',$receipt->id)->where('e.kind','receipt_confirm')->first():null; if(!$receipt||!$confirmation||$receipt->actor_id===$confirmation->actor_id)throw new \DomainException('Recebimento não confirmado independentemente.'); - if(!DB::table('gl_journals')->where($scope)->where('snapshot->source->effect_type','sales.invoice')->where('snapshot->source->source_id',(int)$ob->invoice_id)->exists())throw new \DomainException('Contabilize primeiro a factura original.'); + if(!SourceBook::table('gl_journals')->where($scope)->where('snapshot->source->effect_type','sales.invoice')->where('snapshot->source->source_id',(int)$ob->invoice_id)->exists())throw new \DomainException('Contabilize primeiro a factura original.'); if($kind==='receivable.reversed'){ $target=$this->effectQuery($scope)->where('e.id',$e->target_effect_id)->where('e.kind','allocate')->where('e.obligation_id',$ob->id)->first(); - if(!$target||$target->amount!==$e->amount||!DB::table('gl_journals')->where($scope)->where('snapshot->source->effect_type','receivable.allocated')->where('snapshot->source->source_id',(int)$target->id)->exists())throw new \DomainException('Reversão exige a afectação original contabilizada integralmente.'); + if(!$target||$target->amount!==$e->amount||!SourceBook::table('gl_journals')->where($scope)->where('snapshot->source->effect_type','receivable.allocated')->where('snapshot->source->source_id',(int)$target->id)->exists())throw new \DomainException('Reversão exige a afectação original contabilizada integralmente.'); } return array_replace($base,['effect_type'=>$kind,'source_id'=>$id,'amount_minor'=>$this->minor($e->amount),'tax_minor'=>0,'invoice_tax_minor'=>$base['tax_minor'],'recognition_revision'=>$this->hash(['invoice'=>$base['recognition_revision'],'effect'=>(array)$e,'receipt'=>(array)$receipt,'confirmation'=>(array)$confirmation]),'financial_effect'=>(array)$e,'receipt_confirmation_id'=>$confirmation->id]); } public function period(User $u,array $period):array { $scope=$this->scope($u);$h=hash_init('sha256');$facts=0;$pending=0; - foreach(DB::table('si_invoices')->where($scope)->orderBy('id')->cursor() as $i){$facts++;hash_update($h,$i->snapshot);hash_update($h,$i->pdf_sha256);if(!DB::table('gl_journals')->where($scope)->where('snapshot->source->effect_type','sales.invoice')->where('snapshot->source->source_id',(int)$i->id)->exists())$pending++;} + foreach(DB::table('si_invoices')->where($scope)->orderBy('id')->cursor() as $i){$facts++;hash_update($h,$i->snapshot);hash_update($h,$i->pdf_sha256);if(!SourceBook::table('gl_journals')->where($scope)->where('snapshot->source->effect_type','sales.invoice')->where('snapshot->source->source_id',(int)$i->id)->exists())$pending++;} foreach(DB::table('sr_records')->where($scope)->orderBy('id')->cursor() as $r){hash_update($h,Payables::canonical((array)$r));if(!$this->effectQuery($scope)->where('e.record_id',$r->id)->where('e.kind','<>','allocate')->exists())$pending++;} foreach($this->effectQuery($scope)->orderBy('e.id')->cursor() as $e){$facts++;hash_update($h,Payables::canonical((array)$e)); - if(in_array($e->kind,['allocate','reverse_allocation'],true)){if(!DB::table('gl_journals')->where($scope)->where('snapshot->source->effect_type',$e->kind==='allocate'?'receivable.allocated':'receivable.reversed')->where('snapshot->source->source_id',(int)$e->id)->exists())$pending++;} + if(in_array($e->kind,['allocate','reverse_allocation','reduction','reverse_reduction'],true)){if(!SourceBook::table('gl_journals')->where($scope)->where('snapshot->source->effect_type',match($e->kind){'allocate'=>'receivable.allocated','reduction'=>'receivable.reduced','reverse_reduction'=>'receivable.reduction_reversed',default=>'receivable.reversed'})->where('snapshot->source->source_id',(int)$e->id)->exists())$pending++;} elseif(!in_array($e->kind,['open','receipt_confirm','refused'],true))$pending++; // Unsupported financial changes are visible blockers, never ignored. } return ['facts'=>$facts,'pending'=>$pending,'hash'=>hash_final($h)]; diff --git a/app/Modules/Accounting/SourceBook.php b/app/Modules/Accounting/SourceBook.php new file mode 100644 index 0000000..63e106e --- /dev/null +++ b/app/Modules/Accounting/SourceBook.php @@ -0,0 +1,22 @@ +environment('budget-testing')&&config('sgf_single_installation.enabled')===true&&config('sgf_single_installation.ledger_enabled')===true))return DB::connection(); + CompositionRuntime::assert();$host=CompositionRuntime::physical();$ctx=app(CompanyContext::class);$scope=$ctx->identifiers();$native=DB::connection('receipt_native');LedgerSchema::assertSynthetic($native); + $binding=$native->table('gl_sgf_composition')->where('tenant_id',$scope['tenant_id'])->first(); + abort_unless($binding&&$binding->host_database===$host['database']&&(int)$binding->company_id===$scope['company_id']&&(int)$binding->unit_id===$ctx->unitId(),403); + return $native; + } + public static function table(string $table){if(!preg_match('/^gl_[a-z_]+$/D',$table))throw new \LogicException('Ledger metadata only');return self::connection()->table($table);} + public static function has(string $table):bool{if(!preg_match('/^gl_[a-z_]+$/D',$table))throw new \LogicException('Ledger metadata only');return self::connection()->getSchemaBuilder()->hasTable($table);} +} diff --git a/app/Modules/Finance/BankLedgerSource.php b/app/Modules/Finance/BankLedgerSource.php new file mode 100644 index 0000000..aa0393a --- /dev/null +++ b/app/Modules/Finance/BankLedgerSource.php @@ -0,0 +1,18 @@ +$c]); + return $this->read($scope,$binding); + } + private function read(array $s,array $b):array + { + $rows=SourceBook::table('gl_lines')->where($s)->whereIn('id',$b['ledger_line_ids'])->orderBy('id')->get();abort_unless($rows->count()===2,409,'As duas fontes contabilísticas do exercício não estão disponíveis.');$out=[]; + foreach($rows as$r){$j=SourceBook::table('gl_journals')->where($s)->where('id',$r->journal_id)->first();$a=SourceBook::table('gl_accounts')->where($s)->where('id',$r->account_id)->first();abort_unless($j&&$a&&$a->code==='QA-LIQUIDACAO'&&$j->currency==='MZN'&&$j->journal_on==='2026-11-10'&&max($r->debit_minor,$r->credit_minor)===2000,409,'Linha contabilística incompatível com o exercício autorizado.');$out[]=['id'=>$r->id,'journal_id'=>$r->journal_id,'account_code'=>$a->code,'date'=>$j->journal_on,'currency'=>'MZN','debit_minor'=>$r->debit_minor,'credit_minor'=>$r->credit_minor,'journal_sha256'=>hash('sha256',$j->snapshot)];} + return ['kind'=>'native_qa_ledger','lines'=>$out,'hash'=>hash('sha256',\Tempo\Finance\Payables\Payables::canonical($out))]; + } +} diff --git a/app/Modules/Finance/BankReconciliation.php b/app/Modules/Finance/BankReconciliation.php new file mode 100644 index 0000000..a8a530d --- /dev/null +++ b/app/Modules/Finance/BankReconciliation.php @@ -0,0 +1,86 @@ +json($a));} + public function scope(int $actor,string $right='view'):array + { + abort_unless(config('bank_qa.enabled')===true,404);CompositionRuntime::assert();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2]&&ModuleRecords::active()==='finance',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(array_unique(['view',$right])as$r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'bank_reconciliation.'.$r,$s['unit_id']);abort_unless($this->binding()['scope']===$s,403);return $s; + } + public function binding():array + { + $p=CompositionRuntime::physical();$file='C:/laragon/data/sgf-tempo/run/modular-regression-'.getenv('SGF_ISOLATED_REGRESSION_RUN').'/private/receipt-composition-'.$p['composition'].'/bank-binding.json';$pin=config('bank_qa.binding_sha256');abort_unless(is_string($pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503,'Vínculo do exercício bancário não conferido.');$b=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);abort_unless($b['composition']===$p['composition']&&$b['scope']===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2]&&$b['ledger_line_ids']===[4,5],403);return $b; + } + private function statements(array $s,array $b){return DB::table('br_statements')->where($s)->where('statement_key',$b['statement_key']);} + private function events(array $s,int $id){return DB::table('br_events')->where($s)->where('statement_id',$id);} + private function row(object $r):array{$a=(array)$r;foreach(['entries','data']as$k)if(isset($a[$k]))$a[$k]=json_decode($a[$k],true,32,JSON_THROW_ON_ERROR);return $a;} + private function basis(array $s,array $b):array + { + $source=$this->source->snapshot($s,$b);$header=$this->statements($s,$b)->first();$last=$header?$this->events($s,$header->id)->orderByDesc('version')->first():null; + abort_if($header&&$header->source_hash!==$source['hash'],409,'A origem contabilística mudou. Confira o exercício; não há ajuste automático.');return ['binding'=>$this->hash($b),'source'=>$source,'header'=>$header?$this->row($header):null,'last'=>$last?$this->row($last):null]; + } + private function exact(array $d,array $rules):array{abort_if(array_diff(array_keys($d),array_keys($rules)),422,'Campo fora do exercício autorizado.');return Validator::make($d,$rules)->validate();} + private function right(string $a):string{return match($a){'import'=>'import','prepare_match'=>'prepare',default=>'review'};} + private function lock(int $actor,array $s):void + { + DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->lockForUpdate()->first(),403);DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->orderBy('id')->lockForUpdate()->get(); + } + public function preview(int $actor,array $input,string $key):array + { + abort_if(strlen(json_encode($input))>32768,422);$d=$this->exact($input,['action'=>'required|in:import,review_statement,prepare_match,review_match,finalize','version'=>'required|integer|min:0','proposal_id'=>'nullable|required_if:action,review_match|integer|min:1','data'=>'present|array','reason'=>'required|string|min:10|max:4000']);$d['version']=(int)$d['version'];abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$s=$this->scope($actor,$this->right($d['action']));$b=$this->binding();return DB::transaction(function()use($actor,$s,$b,$d,$key){$this->lock($actor,$s);$this->scope($actor,$this->right($d['action']));$basis=$this->basis($s,$b);$level=DB::transactionLevel();DB::beginTransaction();try{$out=$this->execute($actor,$s,$b,$d,$basis);}finally{while(DB::transactionLevel()>$level)DB::rollBack();}$cut=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'observed'=>time(),'key'=>$key,'input'=>$d,'basis'=>$this->hash($basis),'impact'=>$out['impact']];return ['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];}); + } + public function confirm(int $actor,string $preview):array + { + try{$c=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$c['input'];$s=$this->scope($actor,$this->right($d['action']));$b=$this->binding();return DB::transaction(function()use($actor,$s,$b,$d,$c){$this->lock($actor,$s);$this->scope($actor,$this->right($d['action']));abort_unless($c['actor']===$actor&&$c['scope']==$s&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Contexto ou prévia expirados.');$old=DB::table('br_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409);return json_decode($old->result,true)+['replayed'=>true,'historical_receipt_only'=>true];}$basis=$this->basis($s,$b);abort_unless($this->hash($basis)===$c['basis'],409,'Extracto, origem ou versão mudaram. Confira novamente.');$out=$this->execute($actor,$s,$b,$d,$basis);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409);DB::table('br_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('bank_qa.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'version'=>$out['version'],'before_revision'=>$d['version'],'reason'=>$d['reason'],'review_hash'=>$basis['source']['hash']]);return $out+['replayed'=>false];}); + } + private function parse(array $d,array $b):array + { + $d=$this->exact($d,['account_ref'=>'required|in:QA-BANCO-LOCAL','currency'=>'required|in:MZN','period'=>'required|in:2026-11','opening'=>'required|in:0.00','closing'=>'required|in:0.00','csv'=>'required|string|max:8192']);$fp=fopen('php://temp','w+');fwrite($fp,$d['csv']);rewind($fp);abort_unless(fgetcsv($fp,0,',','"','')===['date','reference','debit','credit','balance'],422,'Cabeçalho CSV inválido.');$rows=[];$balance=0;while(($row=fgetcsv($fp,0,',','"',''))!==false){if($row===[null])continue;abort_unless(count($row)===5&&count($rows)<2,422,'Este exercício exige exactamente duas linhas.');[$date,$ref,$debit,$credit,$closing]=$row;$expected=count($rows)===0?['2026-11-10','QA-OUT-20','20.00','0.00','-20.00']:['2026-11-10','QA-IN-20','0.00','20.00','0.00'];abort_unless($row===$expected,422,'Data, referência, sinal, valor ou saldo fora do exercício artificial autorizado.');$out=$debit==='20.00'?2000:0;$in=$credit==='20.00'?2000:0;$balance+=$in-$out;abort_unless(($closing==='-20.00'?-2000:0)===$balance,422);$rows[]=['date'=>$date,'reference'=>$ref,'debit_minor'=>$out,'credit_minor'=>$in,'balance_minor'=>$balance];}fclose($fp);abort_unless(count($rows)===2&&$balance===0,422,'Fecho/linhas incompletos.');return $d+['entries'=>$rows,'csv_sha256'=>hash('sha256',$d['csv'])]; + } + private function reviewed(array $s,int $id,string $state):bool{return $this->events($s,$id)->where('state',$state)->exists();} + private function checkPreparer(int $actor,int $prepared,array $s,string $permission):void + { + abort_unless($actor!==$prepared,403,'Revisão por pessoa distinta obrigatória.');DB::table('users')->where('id',$prepared)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$prepared])->lockForUpdate()->first(),403);abort_unless(DB::table('permission_grants')->where($s+['user_id'=>$prepared,'permission'=>$permission])->lockForUpdate()->first(),403,'Preparador sem autoridade actual.'); + } + private function execute(int $actor,array $s,array $b,array $d,array $basis):array + { + $h=$basis['header'];$last=$basis['last'];abort_unless(($last['version']??0)===$d['version'],409,'Versão alterada.');$prepared=$actor;$proposal=null;$data=[];$state='';$id=$h['id']??null; + if($d['action']==='import'){ + abort_if(isset($d['proposal_id']),422);$parsed=$this->parse($d['data'],$b);if($h){abort_unless($parsed['csv_sha256']===$h['csv_sha256'],409,'Conserve o extracto importado; não pode substituir a fotografia.');return ['id'=>$last['id'],'statement_id'=>$id,'version'=>$last['version'],'state'=>$last['state'],'already_imported'=>true,'impact'=>['action'=>'import','before_version'=>$d['version'],'after_version'=>$d['version'],'state'=>$last['state'],'existing_statement'=>true,'new_journals'=>0,'bank_operations'=>false]];} + $id=DB::table('br_statements')->insertGetId($s+['statement_key'=>$b['statement_key'],'account_ref'=>$parsed['account_ref'],'currency'=>$parsed['currency'],'period'=>$parsed['period'],'csv_sha256'=>$parsed['csv_sha256'],'csv_bytes'=>$parsed['csv'],'opening_minor'=>0,'closing_minor'=>0,'entries'=>$this->json($parsed['entries']),'source_hash'=>$basis['source']['hash'],'actor_id'=>$actor,'reason'=>$d['reason'],'created_at'=>now()]);$state='imported';$data=['csv_sha256'=>$parsed['csv_sha256'],'opening_minor'=>0,'closing_minor'=>0,'rows'=>2]; + }else{ + abort_unless($h,409,'Importe e confira o extracto primeiro.');abort_if($this->reviewed($s,$id,'finalized'),409,'Conferência finalizada, sem reabertura.'); + if($d['action']==='review_statement'){$decision=$this->exact($d['data'],['decision'=>'required|in:reviewed,refused']);abort_if($this->events($s,$id)->where('action','review_statement')->exists(),409,'O extracto já recebeu decisão.');$prepared=$h['actor_id'];$this->checkPreparer($actor,$prepared,$s,'bank_reconciliation.import');$state=$decision['decision']==='reviewed'?'statement_reviewed':'statement_refused';$data=['csv_sha256'=>$h['csv_sha256'],'opening_minor'=>$h['opening_minor'],'closing_minor'=>$h['closing_minor']];} + else{ + abort_unless($this->reviewed($s,$id,'statement_reviewed'),409,'Extracto sem revisão favorável.'); + if($d['action']==='prepare_match'){ + abort_if(isset($d['proposal_id']),422);$data=$this->exact($d['data'],['row_reference'=>'required|in:QA-OUT-20,QA-IN-20','ledger_line_id'=>'required|integer|min:1']);$row=null;$line=null;foreach($h['entries']as$r)if($r['reference']===$data['row_reference'])$row=$r;foreach($basis['source']['lines']as$l)if($l['id']===(int)$data['ledger_line_id'])$line=$l;abort_unless($row&&$line,403,'Escolha individual da linha de extracto e origem autorizadas.');abort_unless($row['debit_minor']===$line['credit_minor']&&$row['credit_minor']===$line['debit_minor']&&$row['date']===$line['date'],422,'Sinal/data/valor não correspondem.'); + $used=$this->events($s,$id)->where('state','match_reviewed')->where(fn($q)=>$q->where('data->row_reference',$data['row_reference'])->orWhere('data->ledger_line_id',$data['ledger_line_id']))->exists();abort_if($used,409,'Linha já conferida; não pode ser usada novamente.');$pending=$this->events($s,$id)->where('state','match_prepared')->where('data->row_reference',$data['row_reference'])->whereNotExists(fn($q)=>$q->selectRaw('1')->from('br_events as r')->whereColumn('r.proposal_id','br_events.id'))->exists();abort_if($pending,409,'Já existe uma proposta por rever nesta linha.');$state='match_prepared';$data=['row_reference'=>$row['reference'],'ledger_line_id'=>$line['id'],'journal_id'=>$line['journal_id'],'debit_minor'=>$row['debit_minor'],'credit_minor'=>$row['credit_minor'],'source_hash'=>$basis['source']['hash']]; + }elseif($d['action']==='review_match'){ + $decision=$this->exact($d['data'],['decision'=>'required|in:reviewed,refused']);$p=$this->events($s,$id)->where(['id'=>$d['proposal_id'],'state'=>'match_prepared'])->first();abort_unless($p,403);abort_if($this->events($s,$id)->where('proposal_id',$p->id)->exists(),409,'Proposta já revista.');$prepared=$p->prepared_by;$this->checkPreparer($actor,$prepared,$s,'bank_reconciliation.prepare');$proposal=$p->id;$data=json_decode($p->data,true);abort_unless($data['source_hash']===$basis['source']['hash'],409);$state=$decision['decision']==='reviewed'?'match_reviewed':'match_refused'; + }else{ + $this->exact($d['data'],[]);abort_if(isset($d['proposal_id']),422);$prepared=$h['actor_id'];$this->checkPreparer($actor,$prepared,$s,'bank_reconciliation.import');abort_unless($this->events($s,$id)->where('state','match_reviewed')->count()===2,409,'Há linhas por conferir.');abort_if($this->events($s,$id)->where('state','match_prepared')->whereNotExists(fn($q)=>$q->selectRaw('1')->from('br_events as r')->whereColumn('r.proposal_id','br_events.id'))->exists(),409,'Há propostas por rever.');$state='finalized';$data=['opening_minor'=>0,'closing_minor'=>0,'difference_minor'=>0,'matched_rows'=>2,'source_hash'=>$basis['source']['hash']]; + } + } + } + $version=$d['version']+1;$event=DB::table('br_events')->insertGetId($s+['statement_id'=>$id,'version'=>$version,'previous_id'=>$last['id']??null,'proposal_id'=>$proposal,'action'=>$d['action'],'state'=>$state,'prepared_by'=>$prepared,'actor_id'=>$actor,'data'=>$this->json($data),'reason'=>$d['reason'],'created_at'=>now()]);return ['id'=>$event,'statement_id'=>$id,'version'=>$version,'state'=>$state,'impact'=>['action'=>$d['action'],'before_version'=>$d['version'],'after_version'=>$version,'before_state'=>$last['state']??null,'state'=>$state,'data'=>$data,'new_journals'=>0,'bank_operations'=>false]]; + } + public function listing(int $actor,array $input):array + { + $s=$this->scope($actor);$d=$this->exact($input,['view'=>'sometimes|in:rows,history','status'=>'sometimes|in:unmatched,pending,matched','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);$b=$this->binding();return DB::transaction(function()use($actor,$s,$d,$b){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$basis=null;$blocked=null;try{$basis=$this->basis($s,$b);}catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){if($e->getStatusCode()!==409)throw $e;$blocked=$e->getMessage();}$h=$basis['header']??($this->statements($s,$b)->first()?->id? $this->row($this->statements($s,$b)->first()):null);$rows=[];$pending=[];$matches=[];$history=null;$statementReviewed=false;$final=false; + if($h){$matches=$this->events($s,$h['id'])->where('state','match_reviewed')->get()->map(fn($r)=>$this->row($r))->all();$pending=$this->events($s,$h['id'])->where('state','match_prepared')->whereNotExists(fn($q)=>$q->selectRaw('1')->from('br_events as r')->whereColumn('r.proposal_id','br_events.id'))->get()->map(fn($r)=>$this->row($r))->all();foreach($h['entries']as$r){$r['status']='unmatched';foreach($matches as$m)if($m['data']['row_reference']===$r['reference']){$r['status']='matched';$r['match']=$m;}foreach($pending as$p)if($p['data']['row_reference']===$r['reference']){$r['status']='pending';$r['proposal']=$p;}if(!isset($d['status'])||$d['status']===$r['status'])$rows[]=$r;}$q=$this->events($s,$h['id']);$history=$q->orderByDesc('version')->paginate($d['per_page']??25,['*'],'page',$d['page']??1);$history->setCollection($history->getCollection()->map(fn($r)=>$this->row($r)));$statementReviewed=$this->reviewed($s,$h['id'],'statement_reviewed');$final=$this->reviewed($s,$h['id'],'finalized');unset($h['csv_bytes']);} + return ['synthetic_only'=>true,'account_is_real_bank'=>false,'blocked'=>$blocked,'source_kind'=>$basis['source']['kind']??null,'source_lines'=>$basis['source']['lines']??[],'statement'=>$h,'version'=>$basis['last']['version']??0,'statement_reviewed'=>$statementReviewed,'finalized'=>$final,'rows'=>$rows,'pending_proposals'=>$pending,'history'=>$history?->toArray(),'report_available'=>$final&&!$blocked,'capabilities'=>array_combine(['import','prepare','review'],array_map(fn($r)=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'bank_reconciliation.'.$r,$s['unit_id']),['import','prepare','review'])),'template_csv'=>file_get_contents(base_path('resources/data/bank-statement-qa.csv')),'new_journals'=>0];}); + } + public function receipt(int $actor,string $key):array + { + $s=$this->scope($actor);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$r=DB::table('br_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$key])->first();abort_unless($r,404);return ['result'=>json_decode($r->result,true),'operation_executed'=>false,'historical_receipt_only'=>true,'created_at'=>$r->created_at]; + } + public function report(int $actor):array + { + $s=$this->scope($actor);$b=$this->binding();return DB::transaction(function()use($actor,$s,$b){$this->lock($actor,$s);$basis=$this->basis($s,$b);$h=$basis['header'];abort_unless($h&&$this->reviewed($s,$h['id'],'finalized'),409,'Conclua a conferência antes do relatório.');$matches=$this->events($s,$h['id'])->where('state','match_reviewed')->orderBy('id')->get();$fp=fopen('php://temp','w+');$rows=[['CONFERENCIA INTERNA QA — NAO E EXTRACTO BANCARIO REAL'],['Conta de exercicio',$h['account_ref']],['Abertura','0.00'],['Fecho','0.00'],['Diferenca','0.00'],['Linha extracto','Linha contabilistica','Diario','Debito extracto MZN','Credito extracto MZN']];foreach($matches as$m){$d=json_decode($m->data,true);$rows[]=[$d['row_reference'],(string)$d['ledger_line_id'],(string)$d['journal_id'],(string)\Brick\Math\BigDecimal::of($d['debit_minor'])->dividedBy('100',2),(string)\Brick\Math\BigDecimal::of($d['credit_minor'])->dividedBy('100',2)];}foreach($rows as$row)fputcsv($fp,$row,';','"','',"\r\n");rewind($fp);$bytes=stream_get_contents($fp);Audit::record('bank_qa.report_downloaded',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$h['id'],'version'=>$basis['last']['version'],'review_hash'=>hash('sha256',$bytes)]);return ['bytes'=>$bytes,'sha256'=>hash('sha256',$bytes)];}); + } +} diff --git a/app/Modules/Finance/CashWorkspace.php b/app/Modules/Finance/CashWorkspace.php new file mode 100644 index 0000000..a24193a --- /dev/null +++ b/app/Modules/Finance/CashWorkspace.php @@ -0,0 +1,89 @@ +json($v));} + public function scope(int $actor,string $right='view',bool $ledger=false):array + { + abort_unless(config('cash_qa.enabled')===true,404);CompositionRuntime::assert();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&in_array(ModuleRecords::active(),$ledger?['finance','accounting']:['finance'],true),403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(['view',$right] as $r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'cash.'.$r,$s['unit_id']);abort_unless($this->binding()['scope']==$s,403);return $s; + } + public function binding():array + { + $p=CompositionRuntime::physical();$file='C:/laragon/data/sgf-tempo/run/modular-regression-'.getenv('SGF_ISOLATED_REGRESSION_RUN').'/private/receipt-composition-'.$p['composition'].'/cash-binding.json';$pin=config('cash_qa.binding_sha256');abort_unless(is_string($pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503);$b=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);abort_unless($b['composition']===$p['composition']&&$b['scope']===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],403);return $b; + } + private function events(array $s,array $b){return DB::table('cash_events')->where($s)->where('session_key',$b['session_key']);} + public function receipt(array $s,array $b):array + { + $r=DB::table('sr_records')->where($s)->where(['id'=>$b['receipt_id'],'invoice_id'=>$b['invoice_id'],'kind'=>'receipt'])->first();$confirm=$r?DB::table('sr_effects')->where(['record_id'=>$r->id,'kind'=>'receipt_confirm'])->first():null;$doc=DB::table('sd_outputs')->where($s)->where(['id'=>$b['source_output_id'],'kind'=>'invoice_receipt','invoice_id'=>$b['invoice_id']])->first();$allocation=$doc?DB::table('sr_effects')->where(['id'=>$doc->effect_id,'receipt_id'=>$b['receipt_id'],'kind'=>'allocate'])->first():null; + abort_unless($r&&$confirm&&$doc&&$allocation&&$r->actor_id!==$confirm->actor_id&&ServiceReceivables::money($r->amount)==='1000.00'&&ServiceReceivables::money($allocation->amount)==='1000.00'&&!DB::table('sr_effects')->where('receipt_id',$r->id)->where('kind','reverse_receipt')->exists()&&!DB::table('sr_effects')->where('target_effect_id',$allocation->id)->exists(),409,'A origem foi alterada/revertida ou não está confirmada. Confira a custódia; não há reembolso automático.'); + abort_unless(hash_equals($doc->pdf_sha256,hash('sha256',base64_decode($doc->pdf_base64,true))),409);return ['receipt'=>(array)$r,'confirmation'=>(array)$confirm,'allocation'=>(array)$allocation,'document'=>['id'=>$doc->id,'invoice_id'=>$doc->invoice_id,'sha256'=>$doc->pdf_sha256]]; + } + public function basis(array $s,array $b):array + { + $q=$this->events($s,$b);$balance=(int)(clone$q)->where(['action'=>'review','state'=>'approved'])->selectRaw("coalesce(sum(CASE kind WHEN 'receipt' THEN (data->>'amount_minor')::bigint WHEN 'expense' THEN -(data->>'amount_minor')::bigint ELSE 0 END),0) balance")->value('balance');$fingerprint=(clone$q)->where(['action'=>'review','state'=>'approved'])->whereIn('kind',['receipt','expense'])->orderBy('id')->pluck('id')->all(); + return ['binding'=>$this->hash($b),'revision'=>(int)(clone$q)->max('version'),'open'=>(clone$q)->where(['kind'=>'open','action'=>'review','state'=>'approved'])->exists(),'closed'=>(clone$q)->where('state','closed')->exists(),'received'=>(clone$q)->where(['kind'=>'receipt','action'=>'review','state'=>'approved'])->exists(),'expense_recorded'=>(clone$q)->where(['kind'=>'expense','action'=>'review','state'=>'approved'])->exists(),'balance_minor'=>$balance,'movements_hash'=>$this->hash($fingerprint),'source'=>$this->hash($this->receipt($s,$b))]; + } + private function lock(int $actor,array $s):void + { + DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['user_id'=>$actor,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->lockForUpdate()->first(),403);DB::table('permission_grants')->where(['user_id'=>$actor,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->where(fn($q)=>$q->whereNull('unit_id')->orWhere('unit_id',$s['unit_id']))->orderBy('id')->lockForUpdate()->get(); + } + public function preview(int $actor,array $input,string $key):array + { + abort_if(strlen(json_encode($input,JSON_THROW_ON_ERROR))>65536,422);$rules=['action'=>'required|in:prepare,review,close','kind'=>'required|in:open,receipt,expense,count','proposal_id'=>'nullable|integer|min:1','version'=>'required|integer|min:0','data'=>'present|array','reason'=>'required|string|min:10|max:4000'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$s=$this->scope($actor,$d['action']);$b=$this->binding(); + return DB::transaction(function()use($actor,$s,$d,$key,$b){$this->lock($actor,$s);$this->scope($actor,$d['action']);$basis=$this->basis($s,$b);abort_unless($basis['revision']===$d['version'],409,'Versão da Caixa alterada.');$level=DB::transactionLevel();DB::beginTransaction();try{$out=$this->execute($actor,$s,$b,$d,$basis);}finally{while(DB::transactionLevel()>$level)DB::rollBack();}$cut=['input'=>$d,'key'=>$key,'actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'basis'=>$this->hash($basis),'impact'=>$out['impact'],'observed'=>time()];return ['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];}); + } + public function confirm(int $actor,string $preview):array + { + try{$c=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$c['input'];$s=$this->scope($actor,$d['action']);$b=$this->binding();return DB::transaction(function()use($actor,$s,$d,$c,$b){$this->lock($actor,$s);$this->scope($actor,$d['action']);abort_unless($c['actor']===$actor&&$c['scope']==$s&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Contexto ou prévia expirados.');$old=DB::table('cash_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409);return json_decode($old->result,true)+['replayed'=>true];}$basis=$this->basis($s,$b);abort_unless($this->hash($basis)===$c['basis'],409,'Caixa, origem, estado ou versão mudaram. Confira novamente.');$out=$this->execute($actor,$s,$b,$d,$basis);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409,'Impacto alterado.');DB::table('cash_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('cash.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'version'=>$out['version'],'reason'=>$d['reason']]);return $out+['replayed'=>false];}); + } + private function exact(array $data,array $rules):array{abort_if(array_diff(array_keys($data),array_keys($rules)),422);return Validator::make($data,$rules)->validate();} + private function execute(int $actor,array $s,array $b,array $d,array $basis):array + { + abort_if($basis['closed'],409,'Caixa fechada; não há reabertura.');$period=DocumentPeriod::open($s);$q=$this->events($s,$b);$data=[];$source=null;$proposal=null;$state='proposed';$kind=$d['kind'];$change=0; + if($d['action']==='prepare'){ + abort_if($d['proposal_id']??null,422);abort_unless($kind==='open'?!$basis['open']:$basis['open'],409,'Confirme a abertura primeiro.');$common=['date'=>'required|date_format:Y-m-d|before_or_equal:today','evidence'=>'required|string|min:10|max:1000']; + $data=$this->exact($d['data'],match($kind){'open'=>$common+['label'=>'required|string|min:3|max:100','opening'=>'required|string'],'receipt'=>$common+['receipt_id'=>'required|integer|min:1','medium'=>'required|in:cash'],'expense'=>$common+['amount'=>'required|string','beneficiary'=>'required|string|min:3|max:150','description'=>'required|string|min:10|max:1000'],'count'=>$common+['counted'=>'required|string']}); + if($kind==='open'){abort_unless(ServiceReceivables::money($data['opening'],false)==='0.00',422,'Abertura QA explicitamente zero.');$data['amount_minor']=0;} + if($kind==='receipt'){abort_unless($data['receipt_id']===$b['receipt_id'],403,'Escolha a origem QA autorizada.');abort_if((clone$q)->where(['kind'=>'receipt','action'=>'review','state'=>'approved'])->exists(),409,'Recebimento já classificado.');$source=$b['receipt_id'];$data['amount_minor']=$b['amount_minor'];} + if($kind==='expense'){abort_unless(ServiceReceivables::money($data['amount'])==='50.00'&&$basis['balance_minor']>=5000,422,'Despesa QA50 exige saldo suficiente.');abort_if((clone$q)->where(['kind'=>'expense','action'=>'review','state'=>'approved'])->exists(),409,'Despesa QA já confirmada.');$data['amount_minor']=5000;} + if($kind==='count'){$data['counted_minor']=(int)\Brick\Math\BigDecimal::of(ServiceReceivables::money($data['counted'],false))->multipliedBy(100)->toInt();abort_if($data['counted_minor']<0,422);} + if($kind!=='open'){$opened=(clone$q)->where(['kind'=>'open','action'=>'review','state'=>'approved'])->firstOrFail();abort_unless($data['date']>=json_decode($opened->data,true)['date'],422,'O facto não pode anteceder a abertura.');} + if($kind==='receipt'){$original=$this->receipt($s,$b);$on=json_decode($original['receipt']['data'],true)['date']??null;abort_unless(is_string($on)&&$data['date']>=$on,422,'A classificação não pode anteceder o recebimento.');} + if(in_array($kind,['expense','count'],true)){$lastDate=(clone$q)->where(['action'=>'review','state'=>'approved'])->whereIn('kind',['receipt','expense'])->max('data->date');abort_unless($lastDate===null||$data['date']>=$lastDate,422,'O facto não pode anteceder os movimentos confirmados.');} + }elseif($d['action']==='review'){ + $choice=$this->exact($d['data'],['decision'=>'required|in:approved,refused']);$proposal=(clone$q)->where(['id'=>$d['proposal_id']??0,'action'=>'prepare','kind'=>$kind])->firstOrFail();abort_if($proposal->actor_id===$actor,403,'Revisão exige pessoa distinta.');abort_if((clone$q)->where('proposal_id',$proposal->id)->exists(),409,'Proposta já decidida.');$data=json_decode($proposal->data,true);$deps=json_decode($proposal->dependencies,true);$state=$choice['decision'];$source=$proposal->source_receipt_id; + if($state==='approved'){ + DB::table('users')->where('id',$proposal->actor_id)->lockForUpdate()->firstOrFail();$this->permissions->require($proposal->actor_id,$s['tenant_id'],$s['company_id'],'cash.prepare',$s['unit_id']);abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$proposal->actor_id])->lockForUpdate()->first(),403); + abort_unless($deps['source']===$basis['source'],409,'Origem alterada desde a preparação.'); + if($kind==='open')abort_if($basis['open'],409);else abort_unless($basis['open'],409); + if($kind==='receipt'){abort_if((clone$q)->where(['kind'=>'receipt','action'=>'review','state'=>'approved'])->exists(),409);$change=100000;} + if($kind==='expense'){abort_unless($basis['balance_minor']>=5000,409,'Saldo insuficiente.');abort_if((clone$q)->where(['kind'=>'expense','action'=>'review','state'=>'approved'])->exists(),409);$change=-5000;} + if($kind==='count')abort_unless($deps['movements_hash']===$basis['movements_hash']&&$data['counted_minor']===$basis['balance_minor'],409,'Contagem divergente ou movimentos alterados; sem ajuste automático.'); + } + }else{ + abort_unless($basis['received']&&$basis['expense_recorded']&&$basis['balance_minor']===95000,409,'Complete a classificação1000, despesa50 e contagem950 deste recorte QA.'); + abort_unless($kind==='count'&&$d['data']===[]&&$basis['open'],422);$proposal=(clone$q)->where(['id'=>$d['proposal_id']??0,'kind'=>'count','action'=>'review','state'=>'approved'])->firstOrFail();$count=(clone$q)->where('id',$proposal->proposal_id)->firstOrFail();abort_if($count->actor_id===$actor,403,'Quem contou não pode fechar.');$data=json_decode($proposal->data,true);$deps=json_decode($proposal->dependencies,true);abort_unless($data['counted_minor']===$basis['balance_minor']&&$deps['movements_hash']===$basis['movements_hash'],409,'Movimentos mudaram após a contagem.'); + abort_if((clone$q)->where('action','prepare')->whereNotIn('id',(clone$q)->where('action','review')->whereNotNull('proposal_id')->select('proposal_id'))->exists(),409,'Decisões pendentes impedem o fecho.'); + foreach((clone$q)->where(['action'=>'review','state'=>'approved'])->whereIn('kind',['receipt','expense'])->get(['id','kind']) as $fact){$type=$fact->kind==='receipt'?'cash.receipt_classified':'cash.expense_confirmed';abort_unless(SourceBook::table('gl_journals')->where($s)->where('snapshot->source->effect_type',$type)->where('snapshot->source->source_id',$fact->id)->exists(),409,'Valorizações de Caixa pendentes no diário.');}$state='closed'; + } + $id=DB::table('cash_events')->insertGetId($s+['session_key'=>$b['session_key'],'version'=>$basis['revision']+1,'action'=>$d['action'],'kind'=>$kind,'state'=>$state,'proposal_id'=>$proposal?->id,'source_receipt_id'=>$source,'actor_id'=>$actor,'data'=>$this->json($data),'dependencies'=>$this->json($basis+['period'=>$period]),'reason'=>$d['reason'],'created_at'=>now()]); + return ['id'=>$id,'version'=>$basis['revision']+1,'state'=>$state,'impact'=>['action'=>$d['action'],'kind'=>$kind,'state'=>$state,'before_minor'=>$basis['balance_minor'],'after_minor'=>$basis['balance_minor']+$change,'change_minor'=>$change,'counted_minor'=>$data['counted_minor']??null,'money_real'=>false,'new_invoice_or_receipt'=>false,'journal_posted'=>false]]; + } + public function listing(int $actor,array $input):array + { + $s=$this->scope($actor);return DB::transaction(function()use($actor,$input,$s){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$d=$this->exact($input,['page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','kind'=>'sometimes|in:open,receipt,expense,count','from'=>'sometimes|date_format:Y-m-d','to'=>'sometimes|date_format:Y-m-d']);abort_if(isset($d['from'],$d['to'])&&$d['from']>$d['to'],422);$b=$this->binding();$q=$this->events($s,$b);foreach(['kind'=>'kind'] as $k=>$column)if(isset($d[$k]))$q->where($column,$d[$k]);if(isset($d['from']))$q->where('data->date','>=',$d['from']);if(isset($d['to']))$q->where('data->date','<=',$d['to']); + $basis=null;$blocked=null;try{$basis=$this->basis($s,$b);}catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e){if($e->getStatusCode()!==409)throw $e;$blocked=$e->getMessage();} + $p=$q->orderByDesc('version')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1));$decisions=$this->events($s,$b)->whereIn('proposal_id',$p->pluck('id')->all())->where('action','review')->get(['id','proposal_id','state'])->keyBy('proposal_id');$commandReferences=DB::table('cash_commands')->where($s)->where('actor_id',$actor)->whereIn('result->id',$p->pluck('id')->all())->get(['command_key','result'])->keyBy(fn($r)=>json_decode($r->result,true)['id']);foreach($p->items() as $row){$row->command_reference=$commandReferences->get($row->id)?->command_key;$row->data=json_decode($row->data,true);$row->dependencies=json_decode($row->dependencies,true);$row->decision=$decisions->get($row->id);}$caps=[];foreach(['prepare','review','close'] as $r)$caps[$r]=$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'cash.'.$r,$s['unit_id']); + $period=null;if($basis!==null){$from=$d['from']??'0001-01-01';$to=$d['to']??'9999-12-31';$period=(array)$this->events($s,$b)->where(['action'=>'review','state'=>'approved'])->whereIn('kind',['receipt','expense'])->selectRaw("coalesce(sum(CASE WHEN data->>'date' < ? THEN CASE kind WHEN 'receipt' THEN (data->>'amount_minor')::bigint ELSE -(data->>'amount_minor')::bigint END ELSE 0 END),0) opening_minor,coalesce(sum((data->>'amount_minor')::bigint) FILTER(WHERE kind='receipt' AND data->>'date'>=? AND data->>'date'<=?),0) receipts_minor,coalesce(sum((data->>'amount_minor')::bigint) FILTER(WHERE kind='expense' AND data->>'date'>=? AND data->>'date'<=?),0) expenses_minor",[$from,$from,$to,$from,$to])->first();foreach($period as $k=>$v)$period[$k]=(int)$v;$period['closing_minor']=$period['opening_minor']+$period['receipts_minor']-$period['expenses_minor'];} + return ['basis'=>$basis,'blocked'=>$blocked,'events'=>$p,'period'=>$period,'binding'=>array_intersect_key($b,array_flip(['session_key','receipt_id','invoice_id','amount_minor','expense_minor'])),'capabilities'=>$caps,'actor_id'=>$actor,'simulation_only'=>true]; + }); + } + public function commandReceipt(int $actor,string $key):array + { + $s=$this->scope($actor);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$row=DB::table('cash_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$key])->first();abort_unless($row,404);$result=json_decode($row->result,true,32,JSON_THROW_ON_ERROR);abort_unless($this->events($s,$this->binding())->where('id',$result['id'])->exists(),403);return ['result'=>$result,'created_at'=>$row->created_at,'command_key'=>$key,'historical_receipt_only'=>true,'operation_executed'=>false]; + } +} diff --git a/app/Modules/Finance/FinancialIndicatorSource.php b/app/Modules/Finance/FinancialIndicatorSource.php new file mode 100644 index 0000000..45f65a1 --- /dev/null +++ b/app/Modules/Finance/FinancialIndicatorSource.php @@ -0,0 +1,13 @@ +listing($actor,['from'=>'2026-11-10','to'=>'2026-11-10','per_page'=>25]);abort_unless(!$cash['blocked']&&$cash['basis']&&$cash['basis']['closed']&&$cash['period'],409,'Caixa não conciliada/fechada ou origem alterada.');$period=DocumentPeriod::open($scope);$balances=[]; + foreach(['QA-CAIXA','QA-FORNECEDOR']as$code){$a=SourceBook::table('gl_accounts')->where($scope)->where('code',$code)->first();abort_unless($a&&$a->currency==='MZN',409,'Conta/moeda da fonte QA não conferida.');$q=SourceBook::table('gl_lines')->join('gl_journals as j',function($join){$join->on('j.id','=','gl_lines.journal_id');foreach(['tenant_id','company_id','unit_id']as$k)$join->on('j.'.$k,'=','gl_lines.'.$k);})->where(array_combine(array_map(fn($k)=>'gl_lines.'.$k,array_keys($scope)),array_values($scope)))->where('gl_lines.account_id',$a->id)->where('j.journal_on','<=','2026-11-10');$r=$q->selectRaw('coalesce(sum(gl_lines.debit_minor-gl_lines.credit_minor),0)::text net_debit_minor,count(*)::int line_count,max(gl_lines.id) last_line_id')->first();$balances[$code]=['account_id'=>$a->id,'code'=>$a->code,'currency'=>$a->currency]+(array)$r;} + abort_unless($balances['QA-CAIXA']['net_debit_minor']===(string)$cash['period']['closing_minor'],409,'Caixa e diário não conciliam.');$v=['available'=>true,'kind'=>'composed_qa_sources','scope'=>$scope,'currency'=>'MZN','unit'=>'money','as_of'=>'2026-11-10','cash_balance_minor'=>(string)$cash['period']['closing_minor'],'cash_expenses_minor'=>(string)$cash['period']['expenses_minor'],'cash_receipts_minor'=>(string)$cash['period']['receipts_minor'],'supplier_credit_minor'=>(string)\Brick\Math\BigInteger::of($balances['QA-FORNECEDOR']['net_debit_minor'])->negated(),'cash_revision'=>$cash['basis']['revision'],'cash_source_hash'=>$cash['basis']['source'],'cash_session_key'=>$cash['binding']['session_key'],'ledger_accounts'=>$balances,'period'=>$period];$v['hash']=hash('sha256',\Tempo\Finance\Payables\Payables::canonical($v));return $v; + } +} diff --git a/app/Modules/Finance/FinancialIndicators.php b/app/Modules/Finance/FinancialIndicators.php new file mode 100644 index 0000000..36e45b0 --- /dev/null +++ b/app/Modules/Finance/FinancialIndicators.php @@ -0,0 +1,71 @@ +json($v));} + public static function templates():array{return ['cash_coverage_single_payable'=>['label'=>'Cobertura QA de uma obrigação por Caixa','numerator'=>'cash_balance','denominator'=>'supplier_credit_balance','denominator_direction'=>'credit','currency'=>'MZN','representation'=>'times_2','as_of'=>'2026-11-10'],'cash_expense_share'=>['label'=>'Despesa/entradas da Caixa QA','numerator'=>'cash_expenses','denominator'=>'cash_receipts','denominator_direction'=>'inflow','currency'=>'MZN','representation'=>'percent_2','as_of'=>'2026-11-10']];} + public function scope(int $actor,string $right='view'):array + { + abort_unless(config('analysis_qa.enabled')===true,404);CompositionRuntime::assert();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2]&&ModuleRecords::active()==='finance',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(array_unique(['view',$right])as$r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'financial_analysis.'.$r,$s['unit_id']);return $s; + } + private function criteria(array $s){return DB::table('an_criteria')->where($s);} + private function row(object $r):array{$a=(array)$r;foreach(['definition','input','result','source_snapshot']as$k)if(isset($a[$k]))$a[$k]=json_decode($a[$k],true,64,JSON_THROW_ON_ERROR);return $a;} + private function currentCriteria(array $s){$q=$this->criteria($s);return $q->whereNotExists(function($sub){$sub->selectRaw('1')->from('an_criteria as newer');foreach(['tenant_id','company_id','unit_id','metric']as$k)$sub->whereColumn('newer.'.$k,'an_criteria.'.$k);$sub->whereColumn('newer.version','>','an_criteria.version');});} + private function snapshot(int $actor,array $s):array + { + try{return $this->source->snapshot($actor,$s);}catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){if($e->getStatusCode()!==409)throw $e;$v=['available'=>false,'kind'=>'unavailable','pending'=>[$e->getMessage()],'scope'=>$s];$v['hash']=$this->hash($v);return $v;} + } + private function basis(int $actor,array $s,array $d):array{$source=$this->snapshot($actor,$s);$q=$this->criteria($s);$latest=isset($d['metric'])?(clone$q)->where('metric',$d['metric'])->orderByDesc('version')->first():null;$selected=isset($d['data']['criterion_id'])?(clone$q)->where('id',$d['data']['criterion_id'])->first():null;return ['source'=>$source,'latest'=>$latest?$this->row($latest):null,'selected'=>$selected?$this->row($selected):null,'criteria_count'=>(clone$q)->count(),'criteria_last_id'=>(int)(clone$q)->max('id')];} + private function exact(array $d,array $rules):array{abort_if(array_diff(array_keys($d),array_keys($rules)),422,'Campo fora dos indicadores aprovados.');return Validator::make($d,$rules)->validate();} + private function lock(int $actor,array $s):void + { + DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->lockForUpdate()->first(),403);DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->orderBy('id')->lockForUpdate()->get(); + } + private function right(string $action):string{return $action==='review'?'review':'prepare';} + public function preview(int $actor,array $input,string $key):array + { + abort_if(strlen(json_encode($input))>16384,422);$d=$this->exact($input,['action'=>'required|in:prepare,review,calculate','metric'=>'nullable|required_unless:action,calculate|in:'.implode(',',self::METRICS),'version'=>'required|integer|min:0','data'=>'present|array','reason'=>'required|string|min:10|max:4000']);$d['version']=(int)$d['version'];abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$s=$this->scope($actor,$this->right($d['action']));return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($actor,$s);$this->scope($actor,$this->right($d['action']));$b=$this->basis($actor,$s,$d);$level=DB::transactionLevel();DB::beginTransaction();try{$out=$this->execute($actor,$s,$d,$b);}finally{while(DB::transactionLevel()>$level)DB::rollBack();}$cut=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'observed'=>time(),'key'=>$key,'input'=>$d,'basis'=>$this->hash($b),'impact'=>$out['impact']];return ['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];}); + } + public function confirm(int $actor,string $preview):array + { + try{$c=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$c['input'];$s=$this->scope($actor,$this->right($d['action']));return DB::transaction(function()use($actor,$s,$d,$c){$this->lock($actor,$s);$this->scope($actor,$this->right($d['action']));abort_unless($c['actor']===$actor&&$c['scope']==$s&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Contexto ou prévia expirados.');$old=DB::table('an_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409);return json_decode($old->result,true)+['replayed'=>true,'historical_receipt_only'=>true];}$b=$this->basis($actor,$s,$d);abort_unless($this->hash($b)===$c['basis'],409,'Critérios, fontes ou versão mudaram. Exige nova conferência.');$out=$this->execute($actor,$s,$d,$b);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409);DB::table('an_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('financial_analysis.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'version'=>$out['version'],'before_revision'=>$d['version'],'reason'=>$d['reason'],'review_hash'=>$b['source']['hash']]);return $out+['replayed'=>false];}); + } + private function execute(int $actor,array $s,array $d,array $b):array + { + if($d['action']==='calculate'){ + abort_if(!empty($d['metric'])||$d['version']!==0,422);$in=$this->exact($d['data'],['criterion_id'=>'nullable|integer|min:1','as_of'=>'required|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31']);abort_if(isset($in['criterion_id'])&&!$b['selected'],403,'Critério fora do contexto.');$result=$this->evaluate($s,$b['source'],$b['selected'],$in);$id=DB::table('an_results')->insertGetId($s+['actor_id'=>$actor,'criterion_id'=>$b['selected']['id']??null,'input'=>$this->json($in),'result'=>$this->json($result),'source_snapshot'=>$this->json($b['source']),'reason'=>$d['reason'],'created_at'=>now()]);return ['id'=>$id,'type'=>'result','version'=>0,'state'=>$result['state'],'impact'=>['action'=>'calculate','result'=>$result,'new_financial_effects'=>0,'real_company_criterion'=>false]]; + } + $last=$b['latest'];abort_unless(($last['version']??0)===$d['version'],409,'Versão do critério alterada.');$prepared=$actor; + if($d['action']==='prepare'){ + $template=self::templates()[$d['metric']];$def=$this->exact($d['data'],['numerator'=>'required|string','denominator'=>'required|string','denominator_direction'=>'required|string','currency'=>'required|in:MZN','representation'=>'required|string','as_of'=>'required|in:2026-11-10','scope_description'=>'required|string|min:10|max:2000','evidence'=>'required|string|min:10|max:2000']);foreach($template as$k=>$value)if($k!=='label')abort_unless(($def[$k]??null)===$value,422,'Fontes/direcção/fórmula fora dos dois indicadores QA aprovados.');$def['label']=$template['label'];$state='prepared'; + }else{ + $review=$this->exact($d['data'],['decision'=>'required|in:reviewed,refused']);abort_unless($last&&$last['state']==='prepared'&&$last['prepared_by']!==$actor,403,'Preparação actual e revisor distinto obrigatórios.');$prepared=$last['prepared_by'];DB::table('users')->where('id',$prepared)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$prepared])->lockForUpdate()->first(),403);abort_unless(DB::table('permission_grants')->where($s+['user_id'=>$prepared,'permission'=>'financial_analysis.prepare'])->lockForUpdate()->first(),403,'Preparador sem autoridade actual.');abort_unless($last['source_hash']===$b['source']['hash'],409,'Fontes mudaram depois da preparação.');abort_unless($b['source']['available'],409,'Fontes não conciliadas; não podem ser aprovadas por inferência.');$def=$last['definition'];$state=$review['decision']; + } + $version=$d['version']+1;$id=DB::table('an_criteria')->insertGetId($s+['metric'=>$d['metric'],'version'=>$version,'previous_id'=>$last['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_by'=>$prepared,'actor_id'=>$actor,'definition'=>$this->json($def),'source_hash'=>$b['source']['hash'],'reason'=>$d['reason'],'created_at'=>now()]);return ['id'=>$id,'type'=>'criterion','version'=>$version,'state'=>$state,'impact'=>['action'=>$d['action'],'before_version'=>$d['version'],'before_state'=>$last['state']??null,'after_version'=>$version,'state'=>$state,'definition'=>$def,'sources'=>$b['source'],'new_financial_effects'=>0,'real_company_criterion'=>false]]; + } + private function evaluate(array $s,array $source,?array $criterion,array $in):array + { + $pending=[];$def=$criterion['definition']??null;if(!$criterion)$pending[]='Escolha explícita de critério em falta.';else{if($criterion['state']!=='reviewed'||$this->criteria($s)->where('metric',$criterion['metric'])->orderByDesc('version')->value('id')!==$criterion['id'])$pending[]='Critério não revisto ou versão antiga.';if($criterion['source_hash']!==$source['hash'])$pending[]='Fontes mudaram; exige nova conferência do critério.';} + if(!$source['available'])$pending[]='Fonte ausente/não conciliada; não é zero.';if(($source['currency']??null)!=='MZN')$pending[]='Moeda incompatível ou desconhecida.';if(($source['unit']??null)!=='money')$pending[]='Dimensão incompatível.';if($in['as_of']!=='2026-11-10'||($source['as_of']??null)!==$in['as_of'])$pending[]='Retrato temporal não disponível para essa data.'; + $num=null;$den=null;if($criterion&&$source['available']){if($criterion['metric']==='cash_coverage_single_payable'){$num=$source['cash_balance_minor']??null;$den=$source['supplier_credit_minor']??null;}else{$num=$source['cash_expenses_minor']??null;$den=$source['cash_receipts_minor']??null;}if($num===null||$den===null)$pending[]='Numerador/denominador sem fonte conhecida.';elseif(BigRational::of($den)->isLessThanOrEqualTo(0))$pending[]='Denominador nulo ou sem direcção positiva/credora comprovada.';elseif(BigRational::of($num)->isLessThan(0))$pending[]='Numerador com sinal fora do critério aprovado.';} + if($pending)return ['state'=>'pending','pending'=>$pending,'value'=>null,'exact_fraction'=>null,'numerator_minor'=>null,'denominator_minor'=>null,'synthetic_only'=>true,'real_company_criterion'=>false,'previous_period'=>null,'trend'=>null]; + $ratio=BigRational::of($num)->dividedBy($den)->simplified();$percent=$criterion['metric']==='cash_expense_share';$represented=$ratio->multipliedBy($percent?100:1);$display=$represented->toScale(2,RoundingMode::Down);return ['state'=>'determined_qa','label'=>$def['label'],'metric'=>$criterion['metric'],'criterion_id'=>$criterion['id'],'criterion_version'=>$criterion['version'],'as_of'=>$in['as_of'],'numerator_minor'=>(string)$num,'denominator_minor'=>(string)$den,'currency'=>'MZN','exact_fraction'=>(string)$ratio,'value'=>(string)$display,'unit'=>$percent?'percent':'times','representation_difference_fraction'=>(string)$represented->minus(BigRational::of((string)$display))->simplified(),'formula'=>$percent?'despesa / entradas ×100':'Caixa / saldo credor QA-FORNECEDOR','source_hash'=>$source['hash'],'previous_period'=>null,'trend'=>null,'notes'=>['Indicador limitado ao exercício QA; não rácio geral de liquidez/margem ou recomendação de pagamento.','Sem série anterior validada; não se apresenta variação zero.'],'pending'=>[],'synthetic_only'=>true,'real_company_criterion'=>false]; + } + public function listing(int $actor,array $input):array + { + $s=$this->scope($actor);$d=$this->exact($input,['view'=>'sometimes|in:criteria,history,results','metric'=>'sometimes|in:'.implode(',',self::METRICS),'state'=>'sometimes|in:prepared,reviewed,refused','search'=>'nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);$view=$d['view']??'criteria';abort_if($view==='history'&&!isset($d['metric']),422);abort_if($view==='results'&&array_intersect(array_keys($d),['metric','state','search']),422); + return DB::transaction(function()use($actor,$s,$d,$view){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$q=$view==='results'?DB::table('an_results')->where($s):($view==='history'?$this->criteria($s):$this->currentCriteria($s));if(isset($d['metric']))$q->where('metric',$d['metric']);if(isset($d['state']))$q->where('state',$d['state']);if(!empty($d['search'])){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($w)=>$w->whereRaw("definition->>'label' ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("definition->>'evidence' ILIKE ? ESCAPE '!'",[$needle]));}$page=$q->orderByDesc('id')->paginate($d['per_page']??25,['*'],'page',$d['page']??1);$page->setCollection($page->getCollection()->map(fn($r)=>$this->row($r)));return ['templates'=>self::templates(),'view'=>$view,'records'=>$page->toArray(),'metric_version'=>isset($d['metric'])?(int)$this->criteria($s)->where('metric',$d['metric'])->max('version'):null,'source'=>$this->snapshot($actor,$s),'capabilities'=>array_combine(['prepare','review'],array_map(fn($r)=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'financial_analysis.'.$r,$s['unit_id']),['prepare','review'])),'real_company_criterion'=>false];}); + } + public function detail(int $actor,int $id,string $type='criterion'):array + { + $s=$this->scope($actor);$row=DB::table($type==='result'?'an_results':'an_criteria')->where($s)->where('id',$id)->first();abort_unless($row,404);$a=$this->row($row);if($type==='criterion')$a['current_version']=(int)$this->criteria($s)->where('metric',$a['metric'])->max('version');else{$source=$this->snapshot($actor,$s);$criterion=$a['criterion_id']?$this->criteria($s)->where('id',$a['criterion_id'])->first():null;$a['current_evaluation']=$this->evaluate($s,$source,$criterion?$this->row($criterion):null,$a['input']);$a['historical_snapshot_unchanged']=true;}return $a; + } + public function receipt(int $actor,string $key):array{$s=$this->scope($actor);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$r=DB::table('an_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$key])->first();abort_unless($r,404);return ['result'=>json_decode($r->result,true),'created_at'=>$r->created_at,'historical_receipt_only'=>true,'operation_executed'=>false];} + public function report(int $actor,int $id):array + { + $r=$this->detail($actor,$id,'result');$v=$r['result'];$fp=fopen('php://temp','w+');$rows=[['INDICADOR INTERNO QA — NAO E RACIO GERAL OU RECOMENDACAO'],['Estado historico',$v['state']],['Estado actual',$r['current_evaluation']['state']],['Data do retrato',$r['input']['as_of']],['Indicador',$v['label']??'Indeterminado'],['Numerador MZN',isset($v['numerator_minor'])?(string)\Brick\Math\BigDecimal::of($v['numerator_minor'])->dividedBy('100',2):'INDETERMINADO'],['Denominador MZN',isset($v['denominator_minor'])?(string)\Brick\Math\BigDecimal::of($v['denominator_minor'])->dividedBy('100',2):'INDETERMINADO'],['Fraccao exacta',$v['exact_fraction']??'INDETERMINADO'],['Representacao',$v['value']??'INDETERMINADO'],['Unidade',$v['unit']??'INDETERMINADO'],['Versao do criterio',(string)($v['criterion_version']??0)],['Serie anterior','INDETERMINADA'],['Tendencia','INDETERMINADA']];foreach($rows as$row)fputcsv($fp,$row,';','"','',"\r\n");rewind($fp);$bytes=stream_get_contents($fp);$s=$this->scope($actor);Audit::record('financial_analysis.report_downloaded',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'version'=>0,'review_hash'=>hash('sha256',$bytes)]);return ['bytes'=>$bytes,'sha256'=>hash('sha256',$bytes)]; + } +} diff --git a/app/Modules/Finance/Http/BankReconciliationController.php b/app/Modules/Finance/Http/BankReconciliationController.php new file mode 100644 index 0000000..892edf4 --- /dev/null +++ b/app/Modules/Finance/Http/BankReconciliationController.php @@ -0,0 +1,13 @@ +json($out)->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException $e){$state=$e->errorInfo[0]??null;if($state==='23505')return response()->json(['message'=>'Linha/referência já utilizada; confira a versão actual.'],409);if(!in_array($state,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Conferência ocupada. Recupere o mesmo pedido; versão alterada exige nova conferência.'],503)->header('Retry-After','5');}} + public function index(Request $r,BankReconciliation $b){return $this->response(fn()=>$b->listing($r->user()->id,$r->query()));} + public function preview(Request $r,BankReconciliation $b){return $this->response(fn()=>$b->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,BankReconciliation $b){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return $this->response(fn()=>$b->confirm($r->user()->id,$d['preview']));} + public function receipt(Request $r,BankReconciliation $b,string $key){abort_if($r->query()!==[],422);return $this->response(fn()=>$b->receipt($r->user()->id,strtolower($key)));} + public function screen(Request $r,BankReconciliation $b){abort_if($r->query()!==[],422);$b->scope($r->user()->id);return response()->view('finance.bank',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store');} + public function report(Request $r,BankReconciliation $b){abort_if($r->query()!==[],422);return $this->response(function()use($r,$b){$data=$b->report($r->user()->id);return response($data['bytes'])->header('Content-Type','text/csv; charset=UTF-8')->header('Content-Disposition','attachment; filename="conferencia-extracto-QA.csv"')->header('X-Content-SHA256',$data['sha256'])->header('Cache-Control','no-store, private');});} +} diff --git a/app/Modules/Finance/Http/CashController.php b/app/Modules/Finance/Http/CashController.php new file mode 100644 index 0000000..12f3334 --- /dev/null +++ b/app/Modules/Finance/Http/CashController.php @@ -0,0 +1,12 @@ +json($fn())->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Caixa ocupada. Recupere a mesma confirmação.'],503)->header('Retry-After','5');}} + public function index(Request $r,CashWorkspace $s){return $this->response(fn()=>$s->listing($r->user()->id,$r->query()));} + public function receipt(Request $r,CashWorkspace $s,string $key){abort_if($r->query()!==[],422);return $this->response(fn()=>$s->commandReceipt($r->user()->id,strtolower($key)));} + public function screen(Request $r,CashWorkspace $s){abort_if($r->query()!==[],422);$s->scope($r->user()->id);return response()->view('finance.cash',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store');} + public function preview(Request $r,CashWorkspace $s){return $this->response(fn()=>$s->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,CashWorkspace $s){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return $this->response(fn()=>$s->confirm($r->user()->id,$d['preview']));} +} diff --git a/app/Modules/Finance/Http/FinancialIndicatorsController.php b/app/Modules/Finance/Http/FinancialIndicatorsController.php new file mode 100644 index 0000000..4b7292d --- /dev/null +++ b/app/Modules/Finance/Http/FinancialIndicatorsController.php @@ -0,0 +1,14 @@ +json($out)->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Critérios/indicadores ocupados. Recupere o mesmo pedido; versão alterada exige nova conferência.'],503)->header('Retry-After','5');}} + public function screen(Request $r,FinancialIndicators $a){abort_if($r->query()!==[],422);$a->scope($r->user()->id);return response()->view('finance.indicators',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store');} + public function index(Request $r,FinancialIndicators $a){return $this->response(fn()=>$a->listing($r->user()->id,$r->query()));} + public function detail(Request $r,FinancialIndicators $a,int $id){$d=$r->validate(['type'=>'sometimes|in:criterion,result']);abort_if(array_diff(array_keys($r->query()),['type']),422);return $this->response(fn()=>$a->detail($r->user()->id,$id,$d['type']??'criterion'));} + public function preview(Request $r,FinancialIndicators $a){return $this->response(fn()=>$a->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,FinancialIndicators $a){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return $this->response(fn()=>$a->confirm($r->user()->id,$d['preview']));} + public function receipt(Request $r,FinancialIndicators $a,string $key){abort_if($r->query()!==[],422);return $this->response(fn()=>$a->receipt($r->user()->id,strtolower($key)));} + public function report(Request $r,FinancialIndicators $a,int $id){abort_if($r->query()!==[],422);return $this->response(function()use($r,$a,$id){$data=$a->report($r->user()->id,$id);return response($data['bytes'])->header('Content-Type','text/csv; charset=UTF-8')->header('Content-Disposition','attachment; filename="indicador-QA-'.$id.'.csv"')->header('X-Content-SHA256',$data['sha256'])->header('Cache-Control','no-store, private');});} +} diff --git a/app/Modules/HumanResources/Http/PersonnelController.php b/app/Modules/HumanResources/Http/PersonnelController.php new file mode 100644 index 0000000..027b708 --- /dev/null +++ b/app/Modules/HumanResources/Http/PersonnelController.php @@ -0,0 +1,12 @@ +json($fn())->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException$e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw$e;return response()->json(['message'=>'Conferência de pessoal ocupada. Recupere a mesma confirmação.'],503)->header('Retry-After','5')->header('Cache-Control','no-store');}} + public function index(Request $r,Personnel $f){return$this->response(fn()=>$f->listing($r->user()->id,$r->query()));} + public function preview(Request $r,Personnel $f){abort_if(strlen(json_encode($r->all()))>16384,422);return$this->response(fn()=>$f->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,Personnel $f){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return$this->response(fn()=>$f->confirm($r->user()->id,$d['preview']));} + public function receipt(Request $r,Personnel $f,string $key){abort_if($r->query()!==[],422);return$this->response(fn()=>$f->receipt($r->user()->id,strtolower($key)));} + public function export(Request $r,Personnel $f,int $id){abort_if($r->query()!==[],422);$out=$f->export($r->user()->id,$id);return response($out['bytes'])->header('Content-Type','text/csv; charset=utf-8')->header('Content-Disposition','attachment; filename="sgf-rh-pessoal-interno-QA.csv"')->header('X-Content-SHA256',$out['sha256'])->header('Cache-Control','no-store, private');} +} diff --git a/app/Modules/HumanResources/Http/RemunerationsController.php b/app/Modules/HumanResources/Http/RemunerationsController.php new file mode 100644 index 0000000..ceede5d --- /dev/null +++ b/app/Modules/HumanResources/Http/RemunerationsController.php @@ -0,0 +1,12 @@ +json($fn())->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException$e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw$e;return response()->json(['message'=>'Processamento de remunerações ocupado. Recupere a mesma confirmação.'],503)->header('Retry-After','5')->header('Cache-Control','no-store');}} + public function index(Request $r,Remunerations $f){return$this->response(fn()=>$f->listing($r->user()->id,$r->query()));} + public function preview(Request $r,Remunerations $f){abort_if(strlen(json_encode($r->all()))>16384,422);return$this->response(fn()=>$f->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,Remunerations $f){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return$this->response(fn()=>$f->confirm($r->user()->id,$d['preview']));} + public function receipt(Request $r,Remunerations $f,string $key){abort_if($r->query()!==[],422);return$this->response(fn()=>$f->receipt($r->user()->id,strtolower($key)));} + public function export(Request $r,Remunerations $f,int $id){abort_if($r->query()!==[],422);$out=$f->export($r->user()->id,$id);return response($out['bytes'])->header('Content-Type','text/csv; charset=utf-8')->header('Content-Disposition','attachment; filename="sgf-remuneracao-interna-QA.csv"')->header('X-Content-SHA256',$out['sha256'])->header('Cache-Control','no-store, private');} +} diff --git a/app/Modules/HumanResources/Personnel.php b/app/Modules/HumanResources/Personnel.php new file mode 100644 index 0000000..f1e7d6e --- /dev/null +++ b/app/Modules/HumanResources/Personnel.php @@ -0,0 +1,112 @@ +json($a));} + private function exact(array $a,array $rules):array{abort_if(array_diff(array_keys($a),array_keys($rules)),422);return Validator::make($a,$rules)->validate();} + private function area(string $kind):string{return match($kind){'contract'=>'contracts','entitlement','leave'=>'leave','day','report'=>'attendance',default=>abort(422)};} + public function scope(int $actor,string $kind,string $action='view'):array{ + abort_unless(config('hr_personnel.enabled')===true,404);CompositionRuntime::assert();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&ModuleRecords::active()==='human_resources',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);HumanResourcesProfiles::assertActor($actor,$s,$action); + foreach(array_unique(['view',$action])as$r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'human_resources.'.$this->area($kind).'.'.$r,$s['unit_id']);$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'human_resources.remunerations.view',$s['unit_id']); + if($kind==='report')foreach(['contracts','leave']as$a)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'human_resources.'.$a.'.view',$s['unit_id']); + $p=json_decode(file_get_contents(base_path('docs/integration/sgf-human-resources-personnel-access-proposal.json')),true);abort_unless(($p['approved']??false)===true&&($p['authorization']['decision']??null)==='Autorizar o bloco e as30 concessões QA apresentadas'&&hash_equals((string)config('hr_personnel.access_sha256'),$this->hash(['scope'=>$p['scope'],'grants'=>$p['grants_per_book']])),503,'Acesso de pessoal não conferido.');$m=DB::table('hp_meta')->where('policy',self::POLICY)->first();abort_unless($m&&$m->composition===CompositionRuntime::physical()['composition']&&hash_equals($m->source_hash,(string)config('hr_personnel.schema_sha256')),503,'Schema de pessoal não conferido.');return$s; + } + private function identity(int $actor,array $s):string{$u=DB::table('users')->where('id',$actor)->first(['id','name','email']);abort_unless($u&&DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->exists(),403);return$this->hash((array)$u);} + private function lock(int $actor,array $s):void{DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->lockForUpdate()->first();DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->orderBy('id')->lockForUpdate()->get();} + private function rows(array $s){return DB::table('hp_versions')->where($s);} + private function row(?object $r):?array{if(!$r)return null;$a=(array)$r;foreach(['data','impact']as$k)$a[$k]=json_decode($a[$k],true,64,JSON_THROW_ON_ERROR);return$a;} + private function latest(array $s,string $key,bool $approved=false):?array{$q=$this->rows($s)->where('record_key',$key);if($approved)$q->where('state','approved');return$this->row($q->orderByDesc('version')->first());} + private function effective(array $s,string $kind){return$this->rows($s)->where('kind',$kind)->where('state','approved')->whereNotExists(function($n){$n->selectRaw('1')->from('hp_versions as newer');foreach(['tenant_id','company_id','unit_id','record_key']as$k)$n->whereColumn('newer.'.$k,'hp_versions.'.$k);$n->where('newer.state','approved')->whereColumn('newer.version','>','hp_versions.version');});} + private function personQuery($q,string $worker){return$q->whereRaw("data->'input'->>'worker_key'=?",[$worker]);} + private function worker(array $s,string $key):array{$q=DB::table('rm_versions')->where($s)->where(['record_key'=>$key,'kind'=>'worker']);$last=(clone$q)->orderByDesc('version')->first();$a=(clone$q)->where('state','approved')->orderByDesc('version')->first();abort_unless($a&&$last,409,'Escolha um cadastro RH revisto deste âmbito.');$data=json_decode($a->data,true);return['record_key'=>$key,'latest_id'=>$last->id,'approved_id'=>$a->id,'version'=>$a->version,'code'=>$data['input']['code'],'status'=>$data['status']];} + private function input(array $v):array{ + if(is_string($v['reason']??null))$v['reason']=trim($v['reason']);$d=$this->exact($v,['kind'=>'required|in:contract,day,entitlement,leave,report','action'=>'required|in:prepare,review','record_key'=>'required|uuid','version'=>'required|integer|min:0','reason'=>'required|string|min:10|max:2000','data'=>'present|array']);$d['version']=(int)$d['version'];$d['record_key']=strtolower($d['record_key']); + if($d['action']==='review'){$d['data']=$this->exact($d['data'],['decision'=>'required|in:approved,refused']);return$d;} + $base=['operation'=>'required|in:establish,amend,terminate,cancel,take','worker_key'=>'required|uuid','evidence'=>'required|string|min:10|max:1000'];$date='required|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2100-12-31'; + $rules=match($d['kind']){ + 'contract'=>['reference'=>'required|string|regex:/^QA-[A-Z0-9-]{2,60}$/D','mode'=>'required|in:fixed,open,unknown_end','valid_from'=>$date,'valid_to'=>'present|nullable|date_format:Y-m-d|after_or_equal:valid_from','gross'=>'required|string|regex:/^[0-9]{1,9}\.[0-9]{2}$/D'], + 'day'=>['contract_key'=>'required|uuid','date'=>$date,'reference_on'=>$date,'planned'=>'present|nullable|array|max:8','actual'=>'present|nullable|array|max:8','leave_conferred'=>'required|boolean'], + 'entitlement'=>['contract_key'=>'required|uuid','valid_from'=>$date,'valid_to'=>$date.'|after_or_equal:valid_from','minutes'=>'present|nullable|integer|min:0|max:527040'], + 'leave'=>['contract_key'=>'required|uuid','entitlement_key'=>'required|uuid','day_key'=>'required|uuid','date'=>$date,'reference_on'=>$date,'intervals'=>'required|array|min:1|max:8'], + 'report'=>['period'=>'required|date_format:Y-m']}; + $d['data']=$this->exact($d['data'],$base+$rules);foreach(['worker_key','contract_key','entitlement_key','day_key']as$k)if(isset($d['data'][$k]))$d['data'][$k]=strtolower($d['data'][$k]);$op=$d['data']['operation'];abort_if($op==='take'&&$d['kind']!=='leave'||$op==='terminate'&&$d['kind']!=='contract',422); + if($d['kind']==='contract'){abort_unless((int)str_replace('.','',$d['data']['gross'])>0,422);abort_unless(($d['data']['mode']==='fixed')===($d['data']['valid_to']!==null),422,'Prazo exige fim explícito; sem termo/fim desconhecido conserva fim nulo.');} + if($d['kind']==='day'){foreach(['planned','actual']as$k)if($d['data'][$k]!==null)$d['data'][$k]=PersonnelTime::intervals($d['data'][$k]);$d['data']['leave_conferred']=(bool)$d['data']['leave_conferred'];} + if($d['kind']==='leave')$d['data']['intervals']=PersonnelTime::intervals($d['data']['intervals']);if($d['kind']==='entitlement'&&$d['data']['minutes']!==null)$d['data']['minutes']=(int)$d['data']['minutes'];return$d; + } + private function basis(int $actor,array $s,array $d):array{ + $last=$this->latest($s,$d['record_key']);$effective=$this->latest($s,$d['record_key'],true);$in=$d['action']==='prepare'?$d['data']:($last['data']['input']??[]);abort_unless(isset($in['worker_key']),409);$worker=$this->worker($s,$in['worker_key']);$facts=$this->personQuery($this->rows($s),$in['worker_key'])->where('record_key','<>',$d['record_key'])->where('kind','<>','report')->where('state','approved')->selectRaw('count(*) count,coalesce(max(id),0) last_id')->first();$pay=DB::table('rm_versions')->where($s)->where('kind','payroll')->whereRaw("data->'input'->>'worker_key'=?",[$in['worker_key']])->selectRaw('count(*) count,coalesce(max(id),0) last_id')->first();$refs=[];foreach(['contract_key','day_key','entitlement_key']as$k)if(isset($in[$k])){$x=$this->latest($s,$in[$k]);$refs[$k]=['key'=>$in[$k],'latest_id'=>$x['id']??null];}$deps=['worker'=>$worker,'personnel_marker'=>(array)$facts,'referenced_versions'=>$refs,'payroll_marker'=>(array)$pay];return['last'=>$last,'effective'=>$effective,'dependencies'=>$deps,'actor_identity'=>$this->identity($actor,$s)]; + } + private function source(array $s,string $key,string $kind,string $worker):array{$r=$this->latest($s,$key,true);abort_unless($r&&$r['kind']===$kind&&$r['data']['input']['worker_key']===$worker&&$r['data']['status']!=='cancelled',409,'Fonte revista da pessoa/contexto deixou de estar disponível.');return$r;} + private function covers(array $contract,string $from,string $to):bool{$d=$contract['data']['input'];return$contract['data']['status']!=='cancelled'&&$d['mode']!=='unknown_end'&&$d['valid_from']<=$from&&($d['valid_to']===null||$d['valid_to']>=$to);} + private function leaveForDay(array $s,string $worker,string $date,?string $except=null):array{$q=$this->personQuery($this->effective($s,'leave'),$worker)->whereRaw("data->'input'->>'date'=? AND data->>'status'<>'cancelled'",[$date]);if($except)$q->where('record_key','<>',$except);$ranges=[];foreach($q->cursor()as$r)foreach(json_decode($r->data,true)['input']['intervals']as$range)$ranges[]=$range;abort_if(count($ranges)>64,409,'Mais de64 intervalos: requer consolidação individual dos pedidos.');return$this->union($ranges);} + private function union(array $ranges):array{usort($ranges,fn($a,$b)=>$a['from']<=>$b['from']);$out=[];foreach($ranges as$r){$i=count($out)-1;if($i>=0){abort_if($r['from']<$out[$i]['to'],409,'Pedidos de férias sobrepostos.');if($r['from']===$out[$i]['to']){$out[$i]['to']=$r['to'];continue;}}$out[]=$r;}abort_if(count($out)>8,409,'Mais de oito intervalos efectivos num dia.');return$out;} + private function balance(array $s,string $entitlement,?string $except=null,?array $candidate=null):array{$e=$this->latest($s,$entitlement,true);abort_unless($e&&$e['kind']==='entitlement'&&$e['data']['status']!=='cancelled',409,'Condição de férias revista obrigatória.');$q=$this->effective($s,'leave')->whereRaw("data->'input'->>'entitlement_key'=?",[$entitlement]);if($except)$q->where('record_key','<>',$except);$tot=$q->selectRaw("coalesce(sum((data->'result'->>'minutes')::bigint) FILTER(WHERE data->>'status'='reserved'),0) reserved,coalesce(sum((data->'result'->>'minutes')::bigint) FILTER(WHERE data->>'status'='taken'),0) taken")->first();$reserved=(int)$tot->reserved;$taken=(int)$tot->taken;if($candidate){if($candidate['status']==='reserved')$reserved+=$candidate['minutes'];if($candidate['status']==='taken')$taken+=$candidate['minutes'];}$initial=$e['data']['input']['minutes'];$available=$initial===null?null:$initial-$reserved-$taken;abort_if($available!==null&&$available<0,409,'Saldo de férias insuficiente.');return['entitlement_key'=>$entitlement,'entitlement_version'=>$e['version'],'entitlement_minutes'=>$initial,'reserved_minutes'=>$reserved,'taken_minutes'=>$taken,'available_minutes'=>$available,'legal_entitlement_validated'=>false,'salary_effect'=>false];} + + private function plan(int $actor,array $s,array $d,array $b):array{ + $last=$b['last'];$eff=$b['effective'];abort_unless(($last['version']??0)===$d['version']&&(!$last||$last['kind']===$d['kind']),409,'Versão/tipo alterados. Confira novamente.');$old=$eff['data']??null; + if($d['action']==='prepare'){$in=$d['data'];$prepared=$actor;$identity=$this->identity($actor,$s);}else{abort_unless($last&&$last['state']==='prepared'&&$last['prepared_by']!==$actor,403,'Preparação alheia actual obrigatória.');$prepared=$last['prepared_by'];$identity=$last['preparer_identity'];HumanResourcesProfiles::assertActor($prepared,$s,'prepare');abort_unless(hash_equals($identity,$this->identity($prepared,$s)),409,'Identidade do preparador mudou.');foreach(['view','prepare']as$a)$this->permissions->require($prepared,$s['tenant_id'],$s['company_id'],'human_resources.'.$this->area($d['kind']).'.'.$a,$s['unit_id']);$in=$last['data']['input'];abort_unless($this->hash($last['data']['dependencies'])===$this->hash($b['dependencies']),409,'Fontes mudaram desde a preparação. Prepare novamente.');} + $op=$in['operation'];$kind=$d['kind'];$worker=$in['worker_key'];if($op==='establish')abort_if($eff,409,'Já revisto. Use correcção neste registo.');else abort_unless($old&&$old['status']!=='cancelled',409,'Operação exige origem efectiva não cancelada.');if($last)abort_unless($worker===$last['data']['input']['worker_key'],409,'Pessoa imutável neste registo.');if($op!=='cancel')abort_unless($b['dependencies']['worker']['status']==='active',409,'Cadastro suspenso.'); + if(in_array($op,['cancel','take'],true)){foreach($old['input']as$k=>$v)if(!in_array($k,['operation','evidence','reference_on'],true))abort_unless($this->hash(['v'=>$in[$k]??null])===$this->hash(['v'=>$v]),409,'Esta decisão conserva a origem e os valores revistos.');} + $status=$op==='cancel'?'cancelled':'active';$result=[]; + if($kind==='contract'){ + if($old)foreach(['reference','valid_from']as$k)abort_unless($in[$k]===$old['input'][$k],409,'Referência/início estáveis; não mudar identidade contratual.'); + abort_if($this->rows($s)->where('kind','contract')->where('record_key','<>',$d['record_key'])->whereRaw("data->'input'->>'reference'=?",[$in['reference']])->exists(),409,'Referência já utilizada.'); + if($op==='terminate'){abort_unless($in['mode']==='fixed'&&$in['valid_to']!==null&&$in['gross']===$old['input']['gross']&&($old['input']['valid_to']===null||$in['valid_to']<=$old['input']['valid_to']),409,'Terminação exige fim explícito sem alterar bruto.');$status='terminated';} + if($status!=='cancelled')abort_if($this->personQuery($this->effective($s,'contract'),$worker)->where('record_key','<>',$d['record_key'])->whereRaw("data->>'status'<>'cancelled' AND data->'input'->>'valid_from'<=? AND coalesce(data->'input'->>'valid_to','2100-12-31')>=?",[$in['valid_to']??'2100-12-31',$in['valid_from']])->exists(),409,'Contrato efectivo sobreposto ou fim desconhecido.'); + $dependents=0;foreach(['day','entitlement','leave']as$k){$q=$this->effective($s,$k)->whereRaw("data->'input'->>'contract_key'=? AND data->>'status'<>'cancelled'",[$d['record_key']]);$dependents+=(clone$q)->count();if($status==='cancelled')abort_if((clone$q)->exists(),409,'Há dependentes efectivos; conferir/cancelar individualmente antes do contrato.');else{$lo=$k==='entitlement'?'valid_from':'date';$hi=$k==='entitlement'?'valid_to':'date';abort_if($q->where(function($x)use($in,$lo,$hi){$x->whereRaw("data->'input'->>? < ?",[$lo,$in['valid_from']]);if($in['valid_to']!==null)$x->orWhereRaw("data->'input'->>? > ?",[$hi,$in['valid_to']]);if($in['mode']==='unknown_end')$x->orWhereRaw('true');})->exists(),409,'Alteração exclui datas de dependentes revistos.');}} + $bound=DB::table('rm_versions')->where($s)->where('kind','payroll')->where('state','approved')->whereRaw("data->'input'->>'contract_key'=?",[$d['record_key']])->count();if($status==='cancelled')abort_if($bound>0,409,'Processamento contratual revisto exige conferência própria antes de cancelar.');$result=['dependent_records'=>$dependents,'historical_bound_payroll_versions'=>$bound,'mode'=>$in['mode'],'legal_contract_validated'=>false]; + }else{ + if($kind!=='report'){$contract=$this->source($s,$in['contract_key'],'contract',$worker);$from=$in['valid_from']??$in['date'];$to=$in['valid_to']??$in['date'];if($op!=='cancel')abort_unless($this->covers($contract,$from,$to),409,'Contrato não cobre a vigência/dia explicitamente.');if($old)abort_unless($in['contract_key']===$old['input']['contract_key'],409,'Contrato de origem imutável.');} + if($kind==='day'){ + if($last)abort_unless($in['date']===$last['data']['input']['date'],409,'Dia imutável.');abort_if($this->personQuery($this->rows($s)->where('kind','day'),$worker)->where('record_key','<>',$d['record_key'])->whereRaw("data->'input'->>'date'=?",[$in['date']])->exists(),409,'Dia já registado; reutilize a origem.'); + abort_unless($in['reference_on']<=(new \DateTimeImmutable('today',new \DateTimeZone(PersonnelTime::ZONE)))->format('Y-m-d'),422,'Referência futura não comprova declaração.');if($in['actual']!==null)abort_unless($in['date']<=$in['reference_on'],422,'Não declarar trabalho futuro.');$leaves=$this->leaveForDay($s,$worker,$in['date']);if($status==='cancelled')abort_if($leaves!==[],409,'Há férias efectivas neste dia.');$result=PersonnelTime::day($in['date'],$in['planned'],$in['actual'],$in['leave_conferred']?$leaves:null);if($leaves!==[])PersonnelTime::day($in['date'],$in['planned'],$in['actual'],$leaves); + } + if($kind==='entitlement'){ + if($op!=='cancel')abort_if($this->personQuery($this->effective($s,'entitlement'),$worker)->where('record_key','<>',$d['record_key'])->whereRaw("data->>'status'<>'cancelled' AND data->'input'->>'valid_from'<=? AND data->'input'->>'valid_to'>=?",[$in['valid_to'],$in['valid_from']])->exists(),409,'Condições de férias sobrepostas.'); + $q=$this->effective($s,'leave')->whereRaw("data->'input'->>'entitlement_key'=? AND data->>'status'<>'cancelled'",[$d['record_key']]);if($op==='cancel')abort_if((clone$q)->exists(),409,'Reservas/gozos efectivos exigem conferência individual primeiro.');else abort_if((clone$q)->where(fn($x)=>$x->whereRaw("data->'input'->>'date'orWhereRaw("data->'input'->>'date'>?",[$in['valid_to']]))->exists(),409,'Nova vigência exclui pedido efectivo.'); + $tot=$q->selectRaw("coalesce(sum((data->'result'->>'minutes')::bigint) FILTER(WHERE data->>'status'='reserved'),0) reserved,coalesce(sum((data->'result'->>'minutes')::bigint) FILTER(WHERE data->>'status'='taken'),0) taken")->first();$avail=$in['minutes']===null?null:$in['minutes']-(int)$tot->reserved-(int)$tot->taken;abort_if($avail!==null&&$avail<0,409,'Condição insuficiente para pedidos efectivos.');$result=['entitlement_minutes'=>$in['minutes'],'reserved_minutes'=>(int)$tot->reserved,'taken_minutes'=>(int)$tot->taken,'available_minutes'=>$avail,'legal_entitlement_validated'=>false]; + } + if($kind==='leave'){ + $ent=$this->source($s,$in['entitlement_key'],'entitlement',$worker);$day=$this->source($s,$in['day_key'],'day',$worker);abort_unless($ent['data']['input']['contract_key']===$in['contract_key']&&$day['data']['input']['contract_key']===$in['contract_key']&&$day['data']['input']['date']===$in['date'],409,'Pedido tem de usar plano/condição do mesmo contrato e dia.');abort_unless($in['date']>=$ent['data']['input']['valid_from']&&$in['date']<=$ent['data']['input']['valid_to'],409,'Pedido fora da condição de férias.'); + if($last)foreach(['date','entitlement_key','day_key']as$k)abort_unless($in[$k]===$last['data']['input'][$k],409,'Origem e dia imutáveis; corrija os intervalos.');$status=$op==='cancel'?'cancelled':($op==='take'?'taken':($old['status']??'reserved'));if($op==='take')abort_unless($old['status']==='reserved'&&$in['date']<=$in['reference_on'],409,'Só declarar gozo de uma reserva já revista na data aplicável.');abort_unless($in['reference_on']<=(new \DateTimeImmutable('today',new \DateTimeZone(PersonnelTime::ZONE)))->format('Y-m-d'),422); + $ranges=$op==='cancel'?[]:$in['intervals'];$others=$this->leaveForDay($s,$worker,$in['date'],$d['record_key']);$union=$this->union([...$others,...$ranges]);abort_unless($day['data']['input']['planned']!==null,409,'Plano explícito obrigatório.');PersonnelTime::day($in['date'],$day['data']['input']['planned'],$day['data']['input']['actual'],$union);$minutes=array_sum(array_map(fn($r)=>$r['to']-$r['from'],$in['intervals']));$balance=$this->balance($s,$in['entitlement_key'],$d['record_key'],['status'=>$status,'minutes'=>$minutes]);abort_unless($balance['available_minutes']!==null||$status==='cancelled',409,'Saldo inicial desconhecido: decisão de férias bloqueada.');$result=['minutes'=>$minutes,'balance_after'=>$balance,'plan_version'=>$day['version'],'contract_version'=>$contract['version'],'legal_leave_validated'=>false]; + } + if($kind==='report'){if($last)abort_unless($in['period']===$last['data']['input']['period'],409,'Período imutável.');abort_if($this->personQuery($this->rows($s)->where('kind','report'),$worker)->where('record_key','<>',$d['record_key'])->whereRaw("data->'input'->>'period'=?",[$in['period']])->exists(),409,'Conferência deste período já tem origem.');$result=$this->period($actor,$s,$worker,$in['period']);} + } + $data=['input'=>$in,'status'=>$status,'dependencies'=>$b['dependencies'],'result'=>$result,'synthetic_only'=>true];$state=$d['action']==='prepare'?'prepared':$d['data']['decision'];$version=$d['version']+1;if($state==='approved'){$csv=$this->csv($kind,$d['record_key'],$version,$data);$data['artifact_csv']=$csv;$data['artifact_sha256']=hash('sha256',$csv);} + $impact=['before'=>['version'=>$last['version']??0,'state'=>$last['state']??null,'effective'=>$old],'after'=>['version'=>$version,'state'=>$state,'proposal'=>$data,'effective'=>$state==='approved'?$data:$old],'payments'=>0,'debt_created'=>false,'journals'=>0,'legal_payroll'=>false];return['kind'=>$kind,'record_key'=>$d['record_key'],'version'=>$version,'previous_id'=>$last['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_by'=>$prepared,'preparer_identity'=>$identity,'data'=>$data,'impact'=>$impact]; + } + public function period(int $actor,array $s,string $worker,string $period):array{ + $this->scope($actor,'report');$from=$period.'-01';$to=(new \DateTimeImmutable($from))->modify('last day of this month')->format('Y-m-d');$q=$this->personQuery($this->effective($s,'day'),$worker)->whereRaw("data->>'status'<>'cancelled' AND data->'input'->>'date'>=? AND data->'input'->>'date'<=?",[$from,$to])->orderByRaw("data->'input'->>'date'");$days=[];$known=0;$unknown=0;$unaccounted=0;$contracts=[];$entitlements=[]; + foreach($q->cursor()as$r){abort_if(count($days)>=31,409,'Mais de um registo efectivo por dia exige conferência.');$row=$this->row($r);$i=$row['data']['input'];$leaves=$this->leaveForDay($s,$worker,$i['date']);$calc=PersonnelTime::day($i['date'],$i['planned'],$i['actual'],$i['leave_conferred']?$leaves:null);$days[]=['record_key'=>$r->record_key,'version'=>$r->version,'contract_key'=>$i['contract_key'],'calculation'=>$calc];if($calc['unaccounted_minutes']===null)$unknown++;else{$known++;$unaccounted+=$calc['unaccounted_minutes'];}$contracts[$i['contract_key']]=true;} + $contractSources=[];foreach(array_keys($contracts)as$key){$c=$this->source($s,$key,'contract',$worker);$contractSources[]=['record_key'=>$key,'version'=>$c['version'],'reference'=>$c['data']['input']['reference'],'status'=>$c['data']['status']];} + $entQ=$this->personQuery($this->effective($s,'entitlement'),$worker)->whereRaw("data->>'status'<>'cancelled' AND data->'input'->>'valid_from'<=? AND data->'input'->>'valid_to'>=?",[$to,$from]);foreach($entQ->cursor()as$e){abort_if(count($entitlements)>=31,409,'Condições efectivas incompatíveis.');$entitlements[]=$this->balance($s,$e->record_key);} + $pay=DB::table('rm_versions')->where($s)->where(['kind'=>'payroll','state'=>'approved'])->whereRaw("data->'input'->>'worker_key'=? AND data->'input'->>'period'=?",[$worker,$period])->whereNotExists(function($n){$n->selectRaw('1')->from('rm_versions as next');foreach(['tenant_id','company_id','unit_id','record_key']as$k)$n->whereColumn('next.'.$k,'rm_versions.'.$k);$n->where('next.state','approved')->whereColumn('next.version','>','rm_versions.version');});$payroll=[];foreach($pay->cursor()as$r){abort_if(count($payroll)>=1,409,'Mais de um processamento efectivo.');$v=json_decode($r->data,true);$payroll[]=['record_key'=>$r->record_key,'version'=>$r->version,'status'=>$v['status'],'net_cents'=>$v['calculation']['net_cents'],'contract_link_proven'=>isset($v['input']['contract_key']),'contract_key'=>$v['input']['contract_key']??null];} + return['period'=>$period,'worker'=>$this->worker($s,$worker),'days'=>$days,'contracts'=>$contractSources,'leave_balances'=>$entitlements,'payroll'=>$payroll,'known_days'=>count($days),'known_conferred_days'=>$known,'days_with_unknown_conference'=>$unknown,'other_calendar_days_unknown'=>(int)substr($to,-2)-count($days),'unaccounted_known_minutes'=>$unaccounted,'complete_month_proven'=>false,'basis'=>'explicit_days_only_not_legal_calendar','salary_effect'=>false]; + } + private function csv(string $kind,string $key,int $version,array $data):string{$rows=[['SGF QA - RH INTERNO - NAO E DOCUMENTO LEGAL'],['Tipo',$kind,'Registo',$key,'Versao',$version],['Pessoa ficticia',$data['dependencies']['worker']['code']],['Estado',$data['status']],['Sem salario/pagamento/lancamento automaticos'],['Input',$this->json($data['input'])],['Resultado',$this->json($data['result'])],['Fontes',$this->json($data['dependencies'])]];return implode("\r\n",array_map(fn($r)=>implode(';',array_map(fn($v)=>'"'.str_replace('"','""',"'".(string)$v).'"',$r)),$rows))."\r\n";} + public function export(int $actor,int $id):array{$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];$r=$this->row($this->rows($s)->where(['id'=>$id,'state'=>'approved'])->first());abort_unless($r,403);$this->scope($actor,$r['kind']);$d=$r['data'];abort_unless(hash_equals($d['artifact_sha256'],hash('sha256',$d['artifact_csv'])),409,'Exportação conservada não confere.');return['bytes'=>$d['artifact_csv'],'sha256'=>$d['artifact_sha256']];} + public function payrollContract(int $actor,string $key,string $worker,string $period,string $gross):array{$s=$this->scope($actor,'contract');$r=$this->source($s,$key,'contract',$worker);$from=$period.'-01';$to=(new \DateTimeImmutable($from))->modify('last day of this month')->format('Y-m-d');abort_unless($this->covers($r,$from,$to)&&$r['data']['input']['gross']===$gross,409,'Contrato revisto não cobre o mês/bruto escolhidos.');return['record_key'=>$key,'version'=>$r['version'],'id'=>$r['id'],'latest_id'=>$this->latest($s,$key)['id'],'reference'=>$r['data']['input']['reference'],'gross'=>$gross,'valid_from'=>$r['data']['input']['valid_from'],'valid_to'=>$r['data']['input']['valid_to']];} + public function listing(int $actor,array $input):array{ + $d=$this->exact($input,['kind'=>'required|in:contract,day,entitlement,leave,report','worker_key'=>'sometimes|uuid','record_key'=>'sometimes|uuid','period'=>'sometimes|date_format:Y-m','date'=>'sometimes|date_format:Y-m-d','state'=>'sometimes|in:prepared,approved,refused','search'=>'sometimes|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);$s=$this->scope($actor,$d['kind']); + return DB::transaction(function()use($actor,$s,$d){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$worker=isset($d['worker_key'])?$this->worker($s,strtolower($d['worker_key'])):null;$q=$this->rows($s)->where('kind',$d['kind']);if($worker)$q=$this->personQuery($q,$worker['record_key']);if(isset($d['record_key'])){$q->where('record_key',$d['record_key']);abort_unless((clone$q)->exists(),403);}else$q->whereNotExists(function($n){$n->selectRaw('1')->from('hp_versions as next');foreach(['tenant_id','company_id','unit_id','record_key']as$k)$n->whereColumn('next.'.$k,'hp_versions.'.$k);$n->whereColumn('next.version','>','hp_versions.version');}); + if(isset($d['period']))$q->whereRaw("coalesce(data->'input'->>'period',substring(data->'input'->>'date' from 1 for 7))=?",[$d['period']]);if(isset($d['date']))$q->whereRaw("data->'input'->>'date'=?",[$d['date']]);if(isset($d['state']))$q->where('state',$d['state']);if(isset($d['search'])){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->whereRaw("(coalesce(data->'input'->>'reference','')||' '||coalesce(data->'dependencies'->'worker'->>'code','')) ILIKE ? ESCAPE '!'",[$needle]);} + $page=$q->orderByDesc('id')->paginate($d['per_page']??25,['*'],'page',$d['page']??1);$keys=$page->pluck('record_key')->all();$effective=$this->effective($s,$d['kind'])->whereIn('record_key',$keys)->get()->keyBy('record_key');$items=[];foreach($page->items()as$r){$a=$this->row($r);$a['effective']=$this->row($effective->get($r->record_key));$items[]=$a;}$page->setCollection(collect($items));$area=$this->area($d['kind']);return['policy'=>self::POLICY,'kind'=>$d['kind'],'worker'=>$worker,'records'=>$page->toArray(),'capabilities'=>['prepare'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'human_resources.'.$area.'.prepare',$s['unit_id']),'review'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'human_resources.'.$area.'.review',$s['unit_id'])],'synthetic_only'=>true,'salary_effect'=>false,'legal_framework_validated'=>false];}); + } + public function preview(int $actor,array $input,string $key):array{$d=$this->input($input);$this->exact(['key'=>$key],['key'=>'required|uuid']);$s=$this->scope($actor,$d['kind'],$d['action']);return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($actor,$s);$this->scope($actor,$d['kind'],$d['action']);$b=$this->basis($actor,$s,$d);$out=$this->plan($actor,$s,$d,$b);$c=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>strtolower($key),'basis'=>$this->hash($b),'impact'=>$out['impact'],'observed'=>time()];return['preview'=>Crypt::encryptString($this->json($c)),'impact'=>$out['impact']];});} + public function confirm(int $actor,string $token):array{ + try{$c=json_decode(Crypt::decryptString($token),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}abort_unless(is_array($c)&&is_array($c['input']??null),409);$d=$this->input($c['input']);$s=$this->scope($actor,$d['kind'],$d['action']);return DB::transaction(function()use($actor,$s,$d,$c){$this->lock($actor,$s);$this->scope($actor,$d['kind'],$d['action']);abort_unless($c['actor']===$actor&&$this->hash($c['scope'])===$this->hash($s)&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Pessoa/contexto/prévia alterados.');$q=$s+['actor_id'=>$actor,'command_key'=>$c['key']];$old=DB::table('hp_commands')->where($q)->first();if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d)),409,'Chave usada com outros dados.');return json_decode($old->result,true)+['replayed'=>true];}$b=$this->basis($actor,$s,$d);abort_unless(hash_equals($c['basis'],$this->hash($b)),409,'Origem/versão alterada. Confira novamente.');$out=$this->plan($actor,$s,$d,$b);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409,'Impacto mudou.');$row=$out;unset($row['impact'],$row['data']);$id=DB::table('hp_versions')->insertGetId($s+$row+['data'=>$this->json($out['data']),'impact'=>$this->json($out['impact']),'source_basis'=>$this->hash($b['dependencies']),'actor_id'=>$actor,'reason'=>$d['reason'],'created_at'=>now()->utc()]);$result=['id'=>$id,'record_key'=>$d['record_key'],'version'=>$out['version'],'state'=>$out['state'],'impact'=>$out['impact']];DB::table('hp_commands')->insert($q+['request_hash'=>$this->hash($d),'input'=>$this->json($d),'result'=>$this->json($result),'created_at'=>now()->utc()]);Audit::record('human_resources.remunerations.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'version'=>$out['version'],'reason'=>$d['reason']]);return$result+['replayed'=>false];}); + } + public function receipt(int $actor,string $key):array{$this->exact(['key'=>$key],['key'=>'required|uuid']);$s=$this->scope($actor,'report');$old=DB::table('hp_commands')->where($s+['actor_id'=>$actor,'command_key'=>strtolower($key)])->first();abort_unless($old,404);return['result'=>json_decode($old->result,true),'created_at'=>$old->created_at,'reason'=>json_decode($old->input,true)['reason'],'historical_receipt_only'=>true,'operation_executed'=>false];} + +} diff --git a/app/Modules/HumanResources/PersonnelTime.php b/app/Modules/HumanResources/PersonnelTime.php new file mode 100644 index 0000000..43b5eec --- /dev/null +++ b/app/Modules/HumanResources/PersonnelTime.php @@ -0,0 +1,31 @@ +8,422,'Máximo de oito intervalos por dia.');$result=[]; + foreach($input as$r){abort_unless(is_array($r)&&count($r)===2&&!array_diff(array_keys($r),['from','to']),422,'Intervalo deve indicar apenas início/fim em minutos locais.');abort_unless(is_int($r['from'])&&is_int($r['to'])&&$r['from']>=0&&$r['from']<1440&&$r['to']>0&&$r['to']<=1440&&$r['from']<$r['to'],422,'Intervalo fora do dia ou sem duração positiva.');$result[]=$r;} + usort($result,fn($a,$b)=>$a['from']<=>$b['from']);$out=[];foreach($result as$r){$i=count($out)-1;if($i>=0){abort_if($r['from']<$out[$i]['to'],409,'Intervalos sobrepostos exigem correcção explícita.');if($r['from']===$out[$i]['to']){$out[$i]['to']=$r['to'];continue;}}$out[]=$r;}return$out; + } + private static function minutes(array $ranges):int{return array_sum(array_map(fn($r)=>$r['to']-$r['from'],$ranges));} + private static function intersection(array $a,array $b):int{$sum=0;foreach($a as$x)foreach($b as$y)$sum+=max(0,min($x['to'],$y['to'])-max($x['from'],$y['from']));return$sum;} + public static function day(string $date,?array $planned,?array $actual,?array $approvedLeave):array + { + $zone=new \DateTimeZone(self::ZONE);$day=\DateTimeImmutable::createFromFormat('!Y-m-d',$date,$zone);abort_unless($day&&$day->format('Y-m-d')===$date,422,'Data local inválida.'); + $p=$planned===null?null:self::intervals($planned);$a=$actual===null?null:self::intervals($actual);$l=$approvedLeave===null?null:self::intervals($approvedLeave); + if($a!==null&&$l!==null)abort_if(self::intersection($a,$l)>0,409,'Trabalho e férias sobrepostos exigem nova conferência.'); + if($p!==null&&$l!==null)abort_unless(self::intersection($p,$l)===self::minutes($l),409,'Férias têm de corresponder a intervalos previstos explicitamente.'); + $unaccounted=($p===null||$a===null||$l===null)?null:self::minutes($p)-self::intersection($p,$a)-self::intersection($p,$l); + $utc=$p===null?null:array_map(fn($r)=>['from'=>$day->modify('+'.$r['from'].' minutes')->setTimezone(new \DateTimeZone('UTC'))->format(DATE_ATOM),'to'=>$day->modify('+'.$r['to'].' minutes')->setTimezone(new \DateTimeZone('UTC'))->format(DATE_ATOM)],$p); + return['date'=>$date,'zone'=>self::ZONE,'planned'=>$p,'actual'=>$a,'approved_leave'=>$l,'planned_utc'=>$utc,'planned_minutes'=>$p===null?null:self::minutes($p),'declared_minutes'=>$a===null?null:self::minutes($a),'leave_minutes'=>$l===null?null:self::minutes($l),'outside_plan_minutes'=>($a===null||$p===null)?null:self::minutes($a)-self::intersection($p,$a),'unaccounted_minutes'=>$unaccounted,'salary_effect'=>false,'overtime_authorized'=>false,'legal_classification'=>null]; + } + public static function leaveBalance(?int $entitlement,array $requests):array + { + abort_if($entitlement!==null&&$entitlement<0,422);$seen=[];$reserved=0;$taken=0; + foreach($requests as$r){abort_unless(is_array($r)&&isset($r['key'],$r['minutes'],$r['state'])&&is_string($r['key'])&&$r['key']!==''&&is_int($r['minutes'])&&$r['minutes']>0&&in_array($r['state'],['reserved','taken','cancelled'],true),422);abort_if(isset($seen[$r['key']]),409,'Cada pedido contribui uma única vez pelo estado efectivo.');$seen[$r['key']]=true;if($r['state']==='reserved')$reserved+=$r['minutes'];if($r['state']==='taken')$taken+=$r['minutes'];} + $available=$entitlement===null?null:$entitlement-$reserved-$taken;abort_if($available!==null&&$available<0,409,'Saldo de férias insuficiente.');return['entitlement_minutes'=>$entitlement,'reserved_minutes'=>$reserved,'taken_minutes'=>$taken,'available_minutes'=>$available,'unit'=>'minutes','legal_entitlement_validated'=>false,'salary_effect'=>false]; + } +} diff --git a/app/Modules/HumanResources/Remunerations.php b/app/Modules/HumanResources/Remunerations.php new file mode 100644 index 0000000..01b41e4 --- /dev/null +++ b/app/Modules/HumanResources/Remunerations.php @@ -0,0 +1,85 @@ +json($a));} + private function exact(array $a,array $rules):array{abort_if(array_diff(array_keys($a),array_keys($rules)),422);return Validator::make($a,$rules)->validate();} + public function scope(int $actor,string $action='view'):array{ + abort_unless(config('remunerations.enabled')===true,404);CompositionRuntime::assert();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&ModuleRecords::active()==='human_resources',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']); + foreach(array_unique(['view',$action])as$r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'human_resources.remunerations.'.$r,$s['unit_id']); + \App\Shared\Installation\HumanResourcesProfiles::assertActor($actor,$s,$action); + $m=DB::table('rm_meta')->where('policy',self::POLICY)->first();abort_unless($m&&$m->composition===CompositionRuntime::physical()['composition']&&hash_equals($m->source_hash,(string)config('remunerations.schema_sha256')),503,'Schema de remunerações não conferido.');return$s; + } + private function identity(int $actor,array $s):string{$u=DB::table('users')->where('id',$actor)->first(['id','name','email']);abort_unless($u&&DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->exists(),403);return$this->hash((array)$u);} + private function lock(int $actor,array $s):void{DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->lockForUpdate()->first();DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->orderBy('id')->lockForUpdate()->get();} + private function rows(array $s){return DB::table('rm_versions')->where($s);} + private function row(?object $r):?array{if(!$r)return null;$a=(array)$r;foreach(['data','impact']as$k)$a[$k]=json_decode($a[$k],true,64,JSON_THROW_ON_ERROR);return$a;} + private function latest(array $s,string $key,bool $approved=false):?array{$q=$this->rows($s)->where('record_key',$key);if($approved)$q->where('state','approved');return$this->row($q->orderByDesc('version')->first());} + private function input(array $v):array{ + if(is_string($v['reason']??null))$v['reason']=trim($v['reason']);$d=$this->exact($v,['action'=>'required|in:prepare,review','kind'=>'required|in:worker,rules,payroll','record_key'=>'required|uuid','version'=>'required|integer|min:0','reason'=>'required|string|min:10|max:2000','data'=>'present|array']);$d['version']=(int)$d['version'];$d['record_key']=strtolower($d['record_key']); + if($d['action']==='review'){$d['data']=$this->exact($d['data'],['decision'=>'required|in:approved,refused']);return$d;} + $common=['operation'=>'required|in:establish,amend,cancel']; + $rules=match($d['kind']){ + 'worker'=>['code'=>'required|string|regex:/^QA-[A-Z0-9-]{2,60}$/D','status'=>'required|in:active,suspended'], + 'rules'=>['label'=>'required|string|min:5|max:100','valid_from'=>'required|date_format:Y-m-d','valid_to'=>'required|date_format:Y-m-d|after_or_equal:valid_from','worker_rate'=>'present|nullable|numeric|min:0|max:100|regex:/^[0-9]{1,3}(?:\.[0-9]{1,4})?$/D','employer_rate'=>'present|nullable|numeric|min:0|max:100|regex:/^[0-9]{1,3}(?:\.[0-9]{1,4})?$/D','retention_rate'=>'present|nullable|numeric|min:0|max:100|regex:/^[0-9]{1,3}(?:\.[0-9]{1,4})?$/D','source'=>'required|string|min:10|max:500'], + 'payroll'=>['contract_key'=>'sometimes|uuid','worker_key'=>'required|uuid','rules_key'=>'required|uuid','period'=>'required|date_format:Y-m','gross'=>'required|string|regex:/^[0-9]{1,9}\.[0-9]{2}$/D']}; + $d['data']=$this->exact($d['data'],$common+$rules);if($d['kind']!=='payroll')abort_if($d['data']['operation']==='cancel',422,'Use o estado do cadastro ou uma versão de regras, sem apagar a origem.'); + if($d['kind']==='payroll'){foreach(['worker_key','rules_key','contract_key']as$k)if(isset($d['data'][$k]))$d['data'][$k]=strtolower($d['data'][$k]);abort_unless((int)str_replace('.','',$d['data']['gross'])>0,422);} + return$d; + } + private function dependency(array $s,string $key,string $kind):array{$last=$this->latest($s,$key);$a=$this->latest($s,$key,true);abort_unless($last&&$a&&$last['kind']===$kind&&$a['kind']===$kind,409,'Origem escolhida sem revisão actual disponível.');return['latest_id'=>$last['id'],'approved_id'=>$a['id'],'version'=>$a['version'],'record_key'=>$key,'data'=>$a['data']];} + private function basis(int $actor,array $s,array $d):array{ + $last=$this->latest($s,$d['record_key']);$effective=$this->latest($s,$d['record_key'],true);$data=$d['action']==='prepare'?$d['data']:($last['data']['input']??[]);$deps=[]; + if($d['kind']==='payroll'){abort_unless(isset($data['worker_key'],$data['rules_key']),409);$deps=['worker'=>$this->dependency($s,$data['worker_key'],'worker'),'rules'=>$this->dependency($s,$data['rules_key'],'rules')];if(isset($data['contract_key']))$deps['contract']=app(Personnel::class)->payrollContract($actor,$data['contract_key'],$data['worker_key'],$data['period'],$data['gross']);} + return['last'=>$last,'effective'=>$effective,'dependencies'=>$deps,'actor_identity'=>$this->identity($actor,$s)]; + } + public static function calculate(string $gross,array $rates):array{ + $base=\Brick\Math\BigInteger::of(str_replace('.','',$gross));$lines=[];$sum=\Brick\Math\BigInteger::zero();$complete=true; + foreach(['worker_rate','employer_rate','retention_rate']as$key){$rate=$rates[$key]??null;if($rate===null){$complete=false;$lines[$key]=['rate'=>null,'cents'=>null,'exact'=>null,'difference'=>null];continue;}$r=\Brick\Math\BigRational::of($rate)->dividedBy(100)->multipliedBy($base);$c=$r->getNumerator()->quotient($r->getDenominator());$lines[$key]=['rate'=>(string)$rate,'cents'=>(string)$c,'exact'=>(string)$r->simplified(),'difference'=>(string)$r->minus($c)->simplified()];if($key!=='employer_rate')$sum=$sum->plus($c);} + $net=$complete?$base->minus($sum):null;abort_if($net&&$net->isNegative(),422,'Descontos excedem o bruto.');return['gross_cents'=>(string)$base,'rubrics'=>$lines,'complete'=>$complete,'deductions_cents'=>$complete?(string)$sum:null,'net_cents'=>$net===null?null:(string)$net,'employer_cost_cents'=>$lines['employer_rate']['cents']===null?null:(string)$base->plus($lines['employer_rate']['cents']),'policy'=>'truncate_each_rubric_to_cents_preserve_exact_difference','synthetic_only'=>true,'legal_calculation_validated'=>false]; + } + private function plan(int $actor,array $s,array $d,array $b):array{ + $last=$b['last'];$effective=$b['effective'];abort_unless(($last['version']??0)===$d['version']&&(!$last||$last['kind']===$d['kind']),409,'Versão/tipo alterados. Confira novamente.');$old=$effective['data']??null; + if($d['action']==='prepare'){$input=$d['data'];$prepared=$actor;$identity=$this->identity($actor,$s);}else{abort_unless($last&&$last['state']==='prepared'&&$last['prepared_by']!==$actor,403,'Preparação alheia actual obrigatória.');$prepared=$last['prepared_by'];$identity=$last['preparer_identity'];abort_unless(hash_equals($identity,$this->identity($prepared,$s)),409,'Identidade do preparador alterada.');$this->permissions->require($prepared,$s['tenant_id'],$s['company_id'],'human_resources.remunerations.prepare',$s['unit_id']);$input=$last['data']['input'];abort_unless($this->hash($last['data']['dependencies'])===$this->hash($b['dependencies']),409,'Cadastro ou regras mudaram. Prepare novamente.');} + $op=$input['operation'];if($op==='establish')abort_if($effective,409,'Registo já aprovado. Use correcção.');else abort_unless($old&&$old['status']!=='cancelled',409,'Operação exige registo activo já aprovado.'); + $result=null;$status='active';$kind=$d['kind']; + if($kind==='worker'){$status=$input['status'];if($old)abort_unless($input['code']===$old['input']['code'],409,'Código imutável.');$duplicate=$this->rows($s)->where('kind','worker')->where('record_key','<>',$d['record_key'])->whereRaw("data->'input'->>'code'=?",[$input['code']])->exists();abort_if($duplicate,409,'Código já usado; reutilize o cadastro.');} + if($kind==='payroll'){ + $w=$b['dependencies']['worker'];$r=$b['dependencies']['rules'];$begin=$input['period'].'-01';$end=(new \DateTimeImmutable($begin))->modify('last day of this month')->format('Y-m-d'); + if($op!=='cancel'){abort_unless($w['data']['status']==='active',409,'Cadastro suspenso.');abort_unless($r['data']['input']['valid_from']<=$begin&&$r['data']['input']['valid_to']>=$end,409,'Regras não cobrem o período inteiro.');} + if($old){foreach(['worker_key','period']as$k)abort_unless($input[$k]===$old['input'][$k],409,'Pessoa/período imutáveis; corrija neste registo.');} + abort_if($this->rows($s)->where('kind','payroll')->where('record_key','<>',$d['record_key'])->whereRaw("data->'input'->>'worker_key'=? AND data->'input'->>'period'=?",[$input['worker_key'],$input['period']])->exists(),409,'Pessoa/período já registados. Reutilize a origem.'); + if($op==='cancel'){foreach(['rules_key','gross']as$k)abort_unless($input[$k]===$old['input'][$k],409,'Cancelamento conserva os valores aprovados.');$result=$old['calculation'];$status='cancelled';}else$result=self::calculate($input['gross'],$r['data']['input']); + if($d['action']==='review'&&$d['data']['decision']==='approved'&&$op!=='cancel')abort_unless($result['complete'],409,'Parâmetros ausentes: líquido indeterminado; aprovação completa bloqueada.'); + } + $data=['input'=>$input,'status'=>$status,'dependencies'=>$b['dependencies'],'calculation'=>$result,'synthetic_only'=>true];$state=$d['action']==='prepare'?'prepared':$d['data']['decision'];$version=$d['version']+1; + if($kind==='payroll'&&$state==='approved'){$csv=$this->csv($data,$d['record_key'],$version);$data['artifact_csv']=$csv;$data['artifact_sha256']=hash('sha256',$csv);} + $impact=['before'=>['version'=>$last['version']??0,'state'=>$last['state']??null,'effective'=>$old],'after'=>['version'=>$version,'state'=>$state,'proposal'=>$data,'effective'=>$state==='approved'?$data:$old],'payments'=>0,'debt_created'=>false,'journals'=>0,'legal_payroll'=>false];return['kind'=>$kind,'record_key'=>$d['record_key'],'version'=>$version,'previous_id'=>$last['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_by'=>$prepared,'preparer_identity'=>$identity,'data'=>$data,'impact'=>$impact]; + } + private function csv(array $d,string $key,int $version):string{ + $c=$d['calculation'];$rows=[['SGF QA - REMUNERACOES INTERNAS - NAO E RECIBO SALARIAL LEGAL'],['Registo',$key,'Versao',$version],['Estado interno',$d['status']],['Codigo ficticio',"'".$d['dependencies']['worker']['data']['input']['code']],['Periodo',$d['input']['period']],['Origem regras',"'".$d['dependencies']['rules']['data']['input']['source'],'Versao',$d['dependencies']['rules']['version']],['Rubrica','Base em centavos','Percentagem artificial','Valor em centavos','Exacto em centavos','Diferenca em centavos'],['Bruto',$c['gross_cents']]]; + foreach($c['rubrics']as$k=>$v)$rows[]=[$k,$c['gross_cents'],$v['rate']??'Indeterminado',$v['cents']??'Indeterminado',$v['exact']??'Indeterminado',$v['difference']??'Indeterminado'];$rows[]=['Contrato explicitamente escolhido',isset($d['dependencies']['contract'])?($d['dependencies']['contract']['reference'].' v'.$d['dependencies']['contract']['version']):'Sem ligacao contratual historica comprovada'];$rows[]=['Descontos',$c['deductions_cents']??'Indeterminado'];$rows[]=['Liquido interno',$c['net_cents']??'Indeterminado'];$rows[]=['Custo interno',$c['employer_cost_cents']??'Indeterminado'];return implode("\r\n",array_map(fn($r)=>implode(';',array_map(fn($v)=>'"'.str_replace('"','""',(string)$v).'"',$r)),$rows))."\r\n"; + } + public function preview(int $actor,array $input,string $key):array{$d=$this->input($input);$this->exact(['key'=>$key],['key'=>'required|uuid']);$s=$this->scope($actor,$d['action']);return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($actor,$s);$this->scope($actor,$d['action']);$b=$this->basis($actor,$s,$d);$out=$this->plan($actor,$s,$d,$b);$c=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>strtolower($key),'basis'=>$this->hash($b),'impact'=>$out['impact'],'observed'=>time()];return['preview'=>Crypt::encryptString($this->json($c)),'impact'=>$out['impact']];});} + public function confirm(int $actor,string $token):array{ + try{$c=json_decode(Crypt::decryptString($token),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}abort_unless(is_array($c)&&is_array($c['input']??null),409);$d=$this->input($c['input']);$s=$this->scope($actor,$d['action']);return DB::transaction(function()use($actor,$s,$d,$c){$this->lock($actor,$s);$this->scope($actor,$d['action']);abort_unless($c['actor']===$actor&&$this->hash($c['scope'])===$this->hash($s)&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Pessoa/contexto/prévia alterados.');$q=$s+['actor_id'=>$actor,'command_key'=>$c['key']];$old=DB::table('rm_commands')->where($q)->first();if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d)),409,'Chave usada com outros dados.');return json_decode($old->result,true)+['replayed'=>true];}$b=$this->basis($actor,$s,$d);abort_unless(hash_equals($c['basis'],$this->hash($b)),409,'Origem/versão alterada. Confira novamente.');$out=$this->plan($actor,$s,$d,$b);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409,'Impacto mudou.');$row=$out;unset($row['impact'],$row['data']);$id=DB::table('rm_versions')->insertGetId($s+$row+['data'=>$this->json($out['data']),'impact'=>$this->json($out['impact']),'source_basis'=>$this->hash($b['dependencies']),'actor_id'=>$actor,'reason'=>$d['reason'],'created_at'=>now()->utc()]);$result=['id'=>$id,'record_key'=>$d['record_key'],'version'=>$out['version'],'state'=>$out['state'],'impact'=>$out['impact']];DB::table('rm_commands')->insert($q+['request_hash'=>$this->hash($d),'input'=>$this->json($d),'result'=>$this->json($result),'created_at'=>now()->utc()]);Audit::record('human_resources.remunerations.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'version'=>$out['version'],'reason'=>$d['reason']]);return$result+['replayed'=>false];}); + } + public function receipt(int $actor,string $key):array{$this->exact(['key'=>$key],['key'=>'required|uuid']);$s=$this->scope($actor);$old=DB::table('rm_commands')->where($s+['actor_id'=>$actor,'command_key'=>strtolower($key)])->first();abort_unless($old,404);return['result'=>json_decode($old->result,true),'created_at'=>$old->created_at,'reason'=>json_decode($old->input,true)['reason'],'historical_receipt_only'=>true,'operation_executed'=>false];} + public function export(int $actor,int $id):array{$s=$this->scope($actor);$r=$this->row($this->rows($s)->where(['id'=>$id,'kind'=>'payroll','state'=>'approved'])->first());abort_unless($r,403);$d=$r['data'];abort_unless(isset($d['artifact_csv'],$d['artifact_sha256'])&&hash_equals($d['artifact_sha256'],hash('sha256',$d['artifact_csv'])),409,'Exportação conservada não confere.');return['bytes'=>$d['artifact_csv'],'sha256'=>$d['artifact_sha256']];} + public function listing(int $actor,array $input):array{ + $s=$this->scope($actor);$d=$this->exact($input,['kind'=>'required|in:worker,rules,payroll','record_key'=>'sometimes|uuid','period'=>'sometimes|date_format:Y-m','state'=>'sometimes|in:prepared,approved,refused','search'=>'sometimes|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']); + return DB::transaction(function()use($actor,$s,$d){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$q=$this->rows($s)->where('kind',$d['kind']);if(isset($d['record_key'])){$q->where('record_key',$d['record_key']);abort_unless((clone$q)->exists(),403);}else$q->whereNotExists(function($n){$n->selectRaw('1')->from('rm_versions as next');foreach(['tenant_id','company_id','unit_id','record_key']as$k)$n->whereColumn('next.'.$k,'rm_versions.'.$k);$n->whereColumn('next.version','>','rm_versions.version');}); + if(isset($d['period']))$q->whereRaw("data->'input'->>'period'=?",[$d['period']]);if(isset($d['state']))$q->where('state',$d['state']);if(isset($d['search'])){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->whereRaw("coalesce(data->'input'->>'code',data->'input'->>'label',data->'dependencies'->'worker'->'data'->'input'->>'code','') ILIKE ? ESCAPE '!'",[$needle]);} + $page=$q->orderByDesc('id')->paginate($d['per_page']??25,['*'],'page',$d['page']??1);$keys=$page->pluck('record_key')->unique()->all();$a=$this->rows($s)->whereIn('record_key',$keys)->where('state','approved')->whereNotExists(function($n){$n->selectRaw('1')->from('rm_versions as e');foreach(['tenant_id','company_id','unit_id','record_key']as$k)$n->whereColumn('e.'.$k,'rm_versions.'.$k);$n->where('e.state','approved')->whereColumn('e.version','>','rm_versions.version');})->get()->keyBy('record_key'); + $items=[];$totals=['gross_cents'=>'0','net_cents'=>'0','employer_cost_cents'=>'0','approved_active_on_page'=>0,'incomplete_on_page'=>0];foreach($page->items()as$r){$row=$this->row($r);$eff=$this->row($a->get($r->record_key));$row['effective']=$eff;$items[]=$row;if($d['kind']==='payroll'&&!isset($d['record_key'])&&$eff&&$eff['data']['status']==='active'){$c=$eff['data']['calculation'];if(!$c['complete'])$totals['incomplete_on_page']++;else{$totals['approved_active_on_page']++;foreach(['gross_cents','net_cents','employer_cost_cents']as$k)$totals[$k]=(string)\Brick\Math\BigInteger::of($totals[$k])->plus($c[$k]);}}}$page->setCollection(collect($items));return['policy'=>self::POLICY,'kind'=>$d['kind'],'records'=>$page->toArray(),'page_totals'=>$totals,'totals_basis'=>'effective_approved_active_payroll_on_this_page_only','capabilities'=>['prepare'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'human_resources.remunerations.prepare',$s['unit_id']),'review'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'human_resources.remunerations.review',$s['unit_id'])],'synthetic_only'=>true,'legal_payroll_validated'=>false];}); + } +} diff --git a/app/Modules/Identity/Services/IdentityWorkflow.php b/app/Modules/Identity/Services/IdentityWorkflow.php index d1fe61d..9fd6e52 100644 --- a/app/Modules/Identity/Services/IdentityWorkflow.php +++ b/app/Modules/Identity/Services/IdentityWorkflow.php @@ -46,8 +46,22 @@ final class IdentityWorkflow 'token' => $selected['token'], 'grants' => $grants, 'modules' => app(\App\Shared\Context\ModuleNavigation::class)->available($this->context), 'module_id' => $selected['module_id'] ?? null, + 'single_installation'=>app()->environment('budget-testing')&&config('sgf_single_installation.enabled')===true, 'geography_enabled'=>app()->environment('budget-testing')&&config('geography.enabled')===true, - 'stock'=>['period_report'=>app()->environment('budget-testing')&&config('receipt_qa.stock_read_enabled')===true&&config('receipt_qa.stock_period_enabled')===true], + 'analysis_qa_enabled'=>app()->environment('budget-testing')&&config('analysis_qa.enabled')===true, + 'bank_qa_enabled'=>app()->environment('budget-testing')&&config('bank_qa.enabled')===true, + 'fiscal_rules_qa_enabled'=>app()->environment('budget-testing')&&config('fiscal_rules_qa.enabled')===true, + 'internal_statements_enabled'=>app()->environment('budget-testing')&&config('internal_statements.enabled')===true, + 'hr_personnel_enabled'=>app()->environment('budget-testing')&&config('hr_personnel.enabled')===true, + 'remunerations_enabled'=>app()->environment('budget-testing')&&config('remunerations.enabled')===true, + 'fiscal_obligations_enabled'=>app()->environment('budget-testing')&&config('fiscal_obligations.enabled')===true, + 'fiscal_qa_enabled'=>app()->environment('budget-testing')&&config('fiscal_qa.enabled')===true, + 'cash_enabled'=>app()->environment('budget-testing')&&config('cash_qa.enabled')===true, + 'service_documents_enabled'=>app()->environment('budget-testing')&&config('service_documents.enabled')===true, + 'commercial_quotes_enabled'=>app()->environment('budget-testing')&&config('commercial_quotes.enabled')===true, + 'workshop_catalogue_enabled'=>app()->environment('budget-testing')&&config('workshop_catalogue.enabled')===true, + 'workshop_schedule_enabled'=>app()->environment('budget-testing')&&config('workshop_schedule.enabled')===true, + 'stock'=>['catalogue'=>app()->environment('budget-testing')&&config('stock_catalogue.enabled')===true,'inactive_report'=>app()->environment('budget-testing')&&config('receipt_qa.stock_read_enabled')===true&&config('receipt_qa.stock_inactive_enabled')===true,'completion'=>app()->environment('budget-testing')&&config('stock_completion.enabled')===true,'final'=>app()->environment('budget-testing')&&config('stock_operations.final_enabled')===true,'period_report'=>app()->environment('budget-testing')&&config('receipt_qa.stock_read_enabled')===true&&config('receipt_qa.stock_period_enabled')===true,'operations'=>app()->environment('budget-testing')&&config('receipt_qa.enabled')===true&&config('stock_operations.enabled')===true], 'purchasing' => ['internal_requisitions' => app()->environment('budget-testing') && config('internal_requisitions.qa_enabled') === true, 'receipts'=>app()->environment('budget-testing')&&config('receipt_qa.enabled')===true, @@ -57,7 +71,7 @@ final class IdentityWorkflow 'source_requests'=>app()->environment('budget-testing')&&config('receipt_qa.enabled')===true&&config('receipt_qa.requests_enabled')===true, 'suppliers'=>app()->environment('budget-testing')&&config('receipt_qa.enabled')===true&&config('receipt_qa.suppliers_enabled')===true, 'sourcing'=>app()->environment('budget-testing')&&config('receipt_qa.enabled')===true&&config('receipt_qa.sourcing_enabled')===true], - 'accounting' => ['stock_valuation_read' => app()->environment('budget-testing') + 'accounting' => ['single_book'=>app()->environment('budget-testing')&&config('sgf_single_installation.ledger_enabled')===true,'stock_valuation_read' => app()->environment('budget-testing') && config('stock_ledger.read_sources.'.$selected['tenant_id'].':'.$selected['company_id'].':'.($selected['unit_id']??'')) !== null]]; if ($result['context']['module_id'] && !in_array($result['context']['module_id'], array_column($result['context']['modules'], 'id'), true)) { $result['context']['module_id'] = null; diff --git a/app/Modules/Inventory/CompositionStockRead.php b/app/Modules/Inventory/CompositionStockRead.php index 6872e17..a5ddc5a 100644 --- a/app/Modules/Inventory/CompositionStockRead.php +++ b/app/Modules/Inventory/CompositionStockRead.php @@ -17,9 +17,10 @@ final class CompositionStockRead abort_unless(config('receipt_qa.stock_read_enabled') === true && config('receipt_qa.enabled') === true,404); abort_unless(app()->environment('budget-testing'),403); BudgetAdapter::guard(); - $d=$r->validate(['view'=>'sometimes|in:items,warehouses,balances,movements,period','search'=>'nullable|string|max:100', + $d=$r->validate(['view'=>'sometimes|in:items,warehouses,balances,movements,period,inactive','search'=>'nullable|string|max:100', 'warehouse_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000', 'per_page'=>'sometimes|integer|in:25,100','snapshot'=>'sometimes|string|max:8192', + 'as_of'=>'required_if:view,inactive|prohibited_unless:view,inactive|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31','days'=>'required_if:view,inactive|prohibited_unless:view,inactive|integer|min:1|max:3650', 'from'=>'required_if:view,period|prohibited_unless:view,period|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31', 'to'=>'required_if:view,period|prohibited_unless:view,period|date_format:Y-m-d|after_or_equal:from|before_or_equal:9998-12-31']); abort_if(array_diff(array_keys($r->query()),array_keys($d))!==[],422); @@ -28,7 +29,7 @@ final class CompositionStockRead if($filters['view']==='period'){ abort_unless(config('receipt_qa.stock_period_enabled')===true,404); $filters+=['from'=>$d['from'],'to'=>$d['to']]; - }else abort_if(array_key_exists('from',$d)||array_key_exists('to',$d),422); + }elseif($filters['view']==='inactive'){abort_unless(config('receipt_qa.stock_inactive_enabled')===true,404);$filters+=['as_of'=>$d['as_of'],'days'=>(int)$d['days']];}else abort_if(array_key_exists('from',$d)||array_key_exists('to',$d),422); $host=DB::connection();$c=$host->getConfig();$run=(string)getenv('SGF_ISOLATED_REGRESSION_RUN'); abort_unless(preg_match('/^budget_core_sgf_([a-f0-9]{12})$/D',$host->getDatabaseName(),$m) &&($c['username']??null)==='ir_composition_'.$m[1]&&preg_match('/^[a-f0-9]{12}$/D',$run) @@ -72,9 +73,9 @@ final class CompositionStockRead if($filters['warehouse_id']!==null)abort_unless(in_array($filters['warehouse_id'],$binding['warehouse_ids'],true),403); $binding['physical_source']=[$i->db,$i->role,$i->cluster]; $fingerprint=hash('sha256',json_encode([$scope,$unit,$user,$r->session()->get('company_context.token'),$binding,array_diff_key($filters,['page'=>true])],JSON_THROW_ON_ERROR)); - $result=$filters['view']==='period' + $result=$filters['view']==='inactive' ?(new StockInactiveReport($source))->read($binding,$fingerprint,$filters,$d['snapshot']??null) :($filters['view']==='period' ?(new StockPeriodReport($source))->read($binding,$fingerprint,$filters,$d['snapshot']??null) - :(new \Tempo\Inventory\InventoryReadSession($source))->read($binding,$fingerprint,$filters,$d['snapshot']??null); + :(new \Tempo\Inventory\InventoryReadSession($source))->read($binding,$fingerprint,$filters,$d['snapshot']??null)); $result['capabilities']['receipt_trace']=config('receipt_qa.stock_trace_enabled')===true &&$permissions->has($user,$scope['tenant_id'],$scope['company_id'],'purchasing_receipts.view',$unit); return response()->json($result)->header('Cache-Control','no-store'); diff --git a/app/Modules/Inventory/StockCatalogueController.php b/app/Modules/Inventory/StockCatalogueController.php new file mode 100644 index 0000000..22c2645 --- /dev/null +++ b/app/Modules/Inventory/StockCatalogueController.php @@ -0,0 +1,78 @@ +validate();} + private function q(array $p){return DB::table('sc_catalogue_versions')->where($p['scope']);} + private function latest(array $p){return $this->q($p)->whereNotExists(function($q){$q->selectRaw('1')->from('sc_catalogue_versions as newer')->whereColumn('newer.record_key','sc_catalogue_versions.record_key')->whereColumn('newer.tenant_id','sc_catalogue_versions.tenant_id')->whereColumn('newer.company_id','sc_catalogue_versions.company_id')->whereColumn('newer.unit_id','sc_catalogue_versions.unit_id')->whereColumn('newer.version','>','sc_catalogue_versions.version');});} + private function run(Request $r,ReceiptRuntime $rt,string $right,\Closure $fn):mixed + { + abort_unless(config('stock_catalogue.enabled')===true,404);CompositionRuntime::assert();$physical=CompositionRuntime::physical();$ctx=app(CompanyContext::class);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId()];abort_unless(ModuleRecords::active()==='stock',403); + $file='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-'.$physical['composition'].'/stock-catalogue-binding.json';$pin=config('stock_catalogue.binding_sha256');abort_unless(is_string($pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503);$p=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);abort_unless($p['composition']===$physical['composition']&&$p['scope']===$scope,403); + $host=(int)$r->user()->id;$generation=$this->hash($r->session()->get('company_context',[])); + try{return $rt->run($right,function($a,$b,$binding)use($p,$host,$generation,$right,$fn){ + $a=app(CurrentActor::class)->lock($a);abort_if(app(OrganizationAccess::class)->maintenanceOnly($a),403);abort_unless($a->tenant_id===$p['scope']['tenant_id']&&!empty($b['warehouse_ids']),403); + if($right!=='stock.view'){$prepare=$right==='stock.items.prepare';abort_unless($host===$p[$prepare?'preparer_host_id':'reviewer_host_id']&&$a->id===$p[$prepare?'preparer_native_id':'reviewer_native_id'],403);} + $meta=DB::table('sc_catalogue_meta')->where('policy',self::POLICY)->first();abort_unless($meta&&$meta->composition===$p['composition']&&hash_equals($meta->source_hash,(string)config('stock_catalogue.schema_sha256')),503,'Schema de catálogo não conferido.'); + return $fn($a,$p,$host,$generation,$this->hash([$binding,$p])); + });}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Catálogo ocupado. Recupere a mesma confirmação.'],503)->header('Retry-After','5')->header('Cache-Control','no-store');} + } + private function input(array $v):array + { + if(isset($v['reason'])&&is_string($v['reason']))$v['reason']=trim($v['reason']);$d=$this->exact($v,['action'=>'required|in:prepare,review','record_key'=>'required|uuid','version'=>'required|integer|min:0','data'=>'present|array','reason'=>'required|string|min:10|max:2000']);$d['record_key']=strtolower($d['record_key']);$d['version']=(int)$d['version']; + if($d['action']==='prepare'){$data=$d['data'];foreach(['code','name','unit']as$key)if(isset($data[$key])&&is_string($data[$key]))$data[$key]=trim($data[$key]);$d['data']=$this->exact($data,['code'=>['required','string','max:30','regex:/^[A-Z0-9][A-Z0-9_.-]{1,29}$/D'],'name'=>'required|string|min:3|max:150','unit'=>['required','string','max:12','regex:/^[A-Z][A-Z0-9]{0,11}$/D']]);} + else $d['data']=$this->exact($d['data'],['decision'=>'required|in:approved,refused']);return$d; + } + private function basis(array $p,string $key,bool $lock=false):array + { + $last=$this->q($p)->where('record_key',$key)->orderByDesc('version')->first();$approved=$this->q($p)->where(['record_key'=>$key,'state'=>'approved'])->orderByDesc('version')->first();$item=null;if($approved){$q=DB::table('stock_items')->where(['tenant_id'=>$p['scope']['tenant_id'],'id'=>$approved->stock_item_id]);if($lock)$q->lockForUpdate();$item=$q->first(['id','code','name','unit']);abort_unless($item,409,'Artigo de origem indisponível.');$saved=json_decode($approved->data,true);abort_unless($item->code===$saved['code']&&$item->unit===$saved['unit']&&$item->name===$saved['name'],409,'Catálogo alterado fora desta versão; exige conferência.');} + return ['version'=>(int)($last?->version??0),'last'=>$last?(array)$last:null,'approved'=>$approved?(array)$approved:null,'item'=>$item?(array)$item:null]; + } + private function execute(object $a,array $p,int $host,array $d,array $before):array + { + abort_unless($before['version']===$d['version'],409,'A versão mudou. Confira novamente.');$last=$before['last'];$approved=$before['approved'];$item=$before['item'];$data=$d['data'];$itemId=$item['id']??null;$willCreate=false;$willRename=false; + if($d['action']==='prepare'){ + abort_unless($host===$p['preparer_host_id'],403);$state='prepared';$prepared=$host;$preparedNative=$a->id; + if($approved){abort_unless($data['code']===$item['code']&&$data['unit']===$item['unit'],422,'Código e unidade são imutáveis.');abort_if($data['name']===$item['name'],422,'Indique uma alteração de designação.');} + else abort_if(DB::table('stock_items')->where(['tenant_id'=>$a->tenant_id,'code'=>$data['code']])->exists(),409,'Código já existente; não associar artigo anterior automaticamente.'); + }else{ + abort_unless($last&&$last['state']==='prepared'&&$host!==$last['prepared_host_id']&&$a->id!==$last['prepared_native_id'],403,'Revisão exige preparação alheia actual.');$state=$data['decision'];$data=json_decode($last['data'],true);$prepared=$last['prepared_host_id'];$preparedNative=$last['prepared_native_id']; + if($state==='approved'){ + if($item){abort_unless($data['code']===$item['code']&&$data['unit']===$item['unit'],409);$changed=DB::table('stock_items')->where(['id'=>$itemId,'tenant_id'=>$a->tenant_id,'code'=>$item['code'],'unit'=>$item['unit'],'name'=>$item['name']])->update(['name'=>$data['name'],'updated_at'=>now()->utc()]);abort_unless($changed===1,409);$willRename=true;} + else{abort_if(DB::table('stock_items')->where(['tenant_id'=>$a->tenant_id,'code'=>$data['code']])->exists(),409,'Código entretanto utilizado; confira novamente.');$itemId=DB::table('stock_items')->insertGetId(['tenant_id'=>$a->tenant_id,'created_by'=>$a->id,'code'=>$data['code'],'name'=>$data['name'],'unit'=>$data['unit'],'created_at'=>now()->utc(),'updated_at'=>now()->utc()]);$willCreate=true;} + } + } + $version=$before['version']+1;$id=DB::table('sc_catalogue_versions')->insertGetId($p['scope']+['record_key'=>$d['record_key'],'version'=>$version,'previous_id'=>$last['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_host_id'=>$prepared,'prepared_native_id'=>$preparedNative,'host_actor_id'=>$host,'actor_id'=>$a->id,'stock_item_id'=>$itemId,'data'=>json_encode($data,JSON_THROW_ON_ERROR),'reason'=>$d['reason'],'created_at'=>now()->utc()]); + $impact=['before'=>['version'=>$before['version'],'state'=>$last['state']??null,'item'=>$item],'after'=>['version'=>$version,'state'=>$state,'data'=>$data],'creates_article'=>$willCreate,'renames_owned_article'=>$willRename,'stock_movements'=>0,'journal_entries'=>0,'initial_balance'=>null]; + return ['id'=>$id,'record_key'=>$d['record_key'],'version'=>$version,'state'=>$state,'stock_item_id'=>$itemId,'impact'=>$impact]; + } + public function preview(Request $r,ReceiptRuntime $rt) + { + $d=$this->input($r->all());$key=strtolower((string)$r->header('Idempotency-Key'));$this->exact(['key'=>$key],['key'=>'required|uuid']);return$this->run($r,$rt,'stock.items.'.($d['action']==='prepare'?'prepare':'review'),function($a,$p,$host,$generation,$binding)use($d,$key){$before=$this->basis($p,$d['record_key'],true);$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction();try{$out=$this->execute($a,$p,$host,$d,$before);}finally{while($db->transactionLevel()>$level)$db->rollBack();}$cut=['actor'=>$host,'generation'=>$generation,'binding'=>$binding,'input'=>$d,'key'=>$key,'observed'=>time(),'basis'=>$this->hash($before),'impact'=>$out['impact']];return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>$out['impact']])->header('Cache-Control','no-store');}); + } + public function confirm(Request $r,ReceiptRuntime $rt) + { + $body=$this->exact($r->all(),['preview'=>'required|string|max:65536']);try{$cut=json_decode(Crypt::decryptString($body['preview']),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}abort_unless(is_array($cut)&&is_array($cut['input']??null),409);$d=$this->input($cut['input']);return$this->run($r,$rt,'stock.items.'.($d['action']==='prepare'?'prepare':'review'),function($a,$p,$host,$generation,$binding)use($cut,$d){abort_unless(($cut['actor']??null)===$host&&($cut['generation']??null)===$generation&&($cut['binding']??null)===$binding&&is_int($cut['observed']??null)&&$cut['observed']>=time()-600&&$cut['observed']<=time(),409,'Prévia/contexto alterados.');$scope=$p['scope']+['host_actor_id'=>$host,'command_key'=>$cut['key']];$hash=$this->hash($d);$old=DB::table('sc_catalogue_commands')->where($scope)->first();if($old){abort_unless(hash_equals($old->request_hash,$hash)&&hash_equals($old->binding_hash,$binding),409,'Chave utilizada com outros dados.');return response()->json(json_decode($old->result,true)+['replayed'=>true]);}$before=$this->basis($p,$d['record_key'],true);abort_unless(hash_equals($cut['basis'],$this->hash($before)),409,'Catálogo alterado desde a prévia.');$out=$this->execute($a,$p,$host,$d,$before);abort_unless($out['impact']===$cut['impact'],409,'Impacto alterado.');app(RequisitionWorkflow::class)->audit($a,'sgf.stock.catalogue.command',null,$before,['host_actor_id'=>$host,'record_key'=>$d['record_key'],'operation_key'=>$cut['key'],'reason'=>$d['reason'],'after'=>$out]);DB::table('sc_catalogue_commands')->insert($scope+['actor_id'=>$a->id,'request_hash'=>$hash,'binding_hash'=>$binding,'input'=>json_encode($d,JSON_THROW_ON_ERROR),'result'=>json_encode($out,JSON_THROW_ON_ERROR),'created_at'=>now()->utc()]);return response()->json($out+['replayed'=>false],201)->header('Cache-Control','no-store');}); + } + public function receipt(Request $r,ReceiptRuntime $rt,string $key) + { + abort_if($r->query()!==[],422);$this->exact(['key'=>$key],['key'=>'required|uuid']);return$this->run($r,$rt,'stock.view',function($a,$p,$host,$g,$binding)use($key){$old=DB::table('sc_catalogue_commands')->where($p['scope']+['host_actor_id'=>$host,'command_key'=>$key])->first();abort_unless($old,404);abort_unless(hash_equals($old->binding_hash,$binding),409);return response()->json(['result'=>json_decode($old->result,true),'reason'=>json_decode($old->input,true)['reason'],'created_at'=>$old->created_at,'operation_executed'=>false,'historical_receipt_only'=>true])->header('Cache-Control','no-store');}); + } + public function screen(Request $r,ReceiptRuntime $rt){abort_if($r->query()!==[],422);return$this->run($r,$rt,'stock.view',fn()=>response()->view('stock-catalogue-workspace',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store'));} + public function index(Request $r,ReceiptRuntime $rt) + { + $d=$this->exact($r->query(),['view'=>'sometimes|in:items,proposals,history','search'=>'sometimes|nullable|string|max:100','state'=>'sometimes|in:prepared,approved,refused','record_key'=>'required_if:view,history|prohibited_unless:view,history|uuid','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);$hostDb=DB::connection();return$this->run($r,$rt,'stock.view',function($a,$p,$host)use($d,$hostDb){$view=$d['view']??'proposals';$size=(int)($d['per_page']??25);$page=(int)($d['page']??1);if($view==='items'){$approved=$this->q($p)->where('state','approved')->whereNotExists(fn($q)=>$q->selectRaw('1')->from('sc_catalogue_versions as next')->whereColumn('next.record_key','sc_catalogue_versions.record_key')->whereColumn('next.version','>','sc_catalogue_versions.version')->whereColumn('next.tenant_id','sc_catalogue_versions.tenant_id')->whereColumn('next.company_id','sc_catalogue_versions.company_id')->whereColumn('next.unit_id','sc_catalogue_versions.unit_id')->where('next.state','approved'));$q=DB::table('stock_items as i')->where('i.tenant_id',$a->tenant_id)->leftJoinSub($approved,'v','v.stock_item_id','=','i.id')->select(['i.id','i.code','i.name','i.unit','v.record_key','v.version','v.state']);$code='i.code';$name='i.name';$order='i.id';} + else{$q=$view==='history'?$this->q($p)->where('record_key',$d['record_key']):$this->latest($p);if($view==='history')abort_unless((clone$q)->exists(),403,'Histórico fora deste contexto.');if(isset($d['state']))$q->where('state',$d['state']);$code="data->>'code'";$name="data->>'name'";$order='id';} + if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($v)=>$v->whereRaw("$code ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("$name ILIKE ? ESCAPE '!'",[$needle]));}$rows=$q->orderByDesc($order)->paginate($size,['*'],'page',$page);foreach($rows->items()as$row)if(isset($row->data))$row->data=json_decode($row->data,true);return response()->json(['view'=>$view,'records'=>$rows,'capabilities'=>['prepare'=>$host===$p['preparer_host_id']&&$hostDb->table('permission_grants')->where($p['scope']+['user_id'=>$host,'permission'=>'stock.items.prepare'])->exists(),'review'=>$host===$p['reviewer_host_id']&&$hostDb->table('permission_grants')->where($p['scope']+['user_id'=>$host,'permission'=>'stock.items.review'])->exists()],'notice'=>'Artigos existentes sem proveniência deste percurso são apenas consultados. Preparar não cria artigo ou saldo.'])->header('Cache-Control','no-store');}); + } +} diff --git a/app/Modules/Inventory/StockCompletionController.php b/app/Modules/Inventory/StockCompletionController.php new file mode 100644 index 0000000..9759031 --- /dev/null +++ b/app/Modules/Inventory/StockCompletionController.php @@ -0,0 +1,101 @@ +user()->id;$generation=$this->hash($r->session()->get('company_context',[])); + abort_unless($p['composition']===$physical['composition']&&$p['scope']===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],403); + try{return $rt->run($right,function($a,$b,$binding)use($fn,$p,$host,$generation){ + $a=app(CurrentActor::class)->lock($a);abort_if(app(OrganizationAccess::class)->maintenanceOnly($a),403); + $review=$host===$p['reviewer_host_id'];abort_unless(in_array($host,[$p['dispatcher_host_id'],$p['reviewer_host_id']],true)&&$a->id===($review?$p['reviewer_native_id']:$p['dispatcher_native_id'])&&in_array($p['warehouse_id'],$b['warehouse_ids'],true),403); + return $fn($a,$p,$host,$generation,$binding,$review); + });}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Operação ocupada. Recupere a mesma confirmação.'],503)->header('Retry-After','5');} + } + private function family(string $family):string{abort_unless(in_array($family,['policy','supplier_return'],true),422);return $family==='policy'?'stock.planning':'stock.disposition';} + private function query(array $p,string $family){return app(SupplyPlanning::class)->latest($family==='policy'?'policy':'stock_event')->where('tenant_id',1)->where('stock_item_id',$p['stock_item_id'])->where('warehouse_id',$p['warehouse_id']);} + private function basis(array $p):array + { + return ['policy'=>$this->hash($p),'balance'=>app(StockWorkflow::class)->balance($p['stock_item_id'],$p['warehouse_id']), + 'revision'=>(int)SupplyRecord::where('tenant_id',1)->where('stock_item_id',$p['stock_item_id'])->where('warehouse_id',$p['warehouse_id'])->max('id'), + 'item'=>StockItem::findOrFail($p['stock_item_id'])->only(['id','code','name','unit']), + 'warehouse'=>Warehouse::findOrFail($p['warehouse_id'])->only(['id','code','name'])]; + } + public function screen(Request $r,ReceiptRuntime $rt){return $this->run($r,$rt,'stock.planning.view',fn()=>response()->view('stock-completion-workspace',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store'));} + public function index(Request $r,ReceiptRuntime $rt) + { + $d=$r->validate(['family'=>'required|in:policy,supplier_return','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','state'=>'nullable|in:draft,returned,approved,suspended,executed','record_key'=>'sometimes|uuid']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422); + return $this->run($r,$rt,$this->family($d['family']).'.view',function($a,$p,$host,$g,$binding,$review)use($d){ + abort_unless($d['family']==='policy'?app(SupplyPlanning::class)->allows($a,'supply_read'):app(InventoryDispositions::class)->allows($a,'read'),403); + $q=isset($d['record_key'])?SupplyRecord::where('tenant_id',1)->where('kind',$d['family']==='policy'?'policy':'stock_event')->where('stock_item_id',$p['stock_item_id'])->where('warehouse_id',$p['warehouse_id'])->where('record_key',$d['record_key']):$this->query($p,$d['family']);if(!empty($d['state']))$q->where('state',$d['state']); + $page=$q->orderByDesc('id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1)); + $basis=$this->basis($p);return response()->json(['basis'=>$basis,'revision'=>$this->hash($basis),'reviewer'=>$review,'records'=>$page,'receipt_item_id'=>$p['receipt_item_id'],'capabilities'=>['prepare'=>!$review,'review'=>$review,'execute'=>!$review]])->header('Cache-Control','no-store'); + }); + } + private function right(array $d):string{return $d['action']==='execute'?'stock.operations.confirm':$this->family($d['family']).'.'.($d['action']==='save'?'prepare':'review');} + public function preview(Request $r,ReceiptRuntime $rt) + { + $d=$r->validate(['family'=>'required|in:policy,supplier_return','action'=>'required|in:save,approve,return,suspend,execute','operation_key'=>'required|uuid','record_key'=>'nullable|uuid','version'=>'required|integer|min:0','revision'=>'required|string|size:64','reason'=>'required|string|min:10|max:4000','data'=>'present|array']);abort_if(array_diff(array_keys($r->all()),array_keys($d)),422);$d['operation_key']=strtolower($d['operation_key']); + return $this->run($r,$rt,$this->right($d),function($a,$p,$host,$g,$binding,$review)use($d){ + abort_unless($d['revision']===$this->hash($this->basis($p)),409,'O estado mudou. Confira novamente.');$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction(); + try{$result=$this->execute($a,$p,$d,$review);}finally{while($db->transactionLevel()>$level)$db->rollBack();} + $cut=['input'=>$d,'host'=>$host,'native'=>$a->id,'generation'=>$g,'binding'=>$binding,'observed'=>time(),'impact'=>$result['impact']]; + return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>$result['impact']])->header('Cache-Control','no-store'); + }); + } + public function confirm(Request $r,ReceiptRuntime $rt) + { + $r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422); + try{$cut=json_decode(Crypt::decryptString($r->input('preview')),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');} + abort_unless(is_array($cut['input']??null),409); + return $this->run($r,$rt,$this->right($cut['input']),function($a,$p,$host,$g,$binding,$review)use($cut){ + abort_unless($cut['host']===$host&&$cut['native']===$a->id&&$cut['generation']===$g&&$cut['binding']===$binding&&$cut['observed']>=time()-600&&$cut['observed']<=time(),409,'Contexto ou prévia expirados.');$d=$cut['input'];$db=DB::connection(); + $old=$db->table('audit_logs')->where(['tenant_id'=>1,'user_id'=>$a->id,'action'=>'sgf.stock.completion.command'])->where('after->operation_key',$d['operation_key'])->first(); + if($old){$v=json_decode($old->after,true);abort_unless($v['request_hash']===$this->hash($d)&&$v['host_actor_id']===$host&&($v['policy']??null)===$this->hash($p),409);return response()->json($v['result']+['replayed'=>true]);} + $before=$this->basis($p);abort_unless($this->hash($before)===$d['revision'],409,'Estado alterado desde a conferência.');$result=$this->execute($a,$p,$d,$review);abort_unless($result['impact']===$cut['impact'],409,'Impacto alterado.'); + app(RequisitionWorkflow::class)->audit($a,'sgf.stock.completion.command',null,$before,['operation_key'=>$d['operation_key'],'request_hash'=>$this->hash($d),'host_actor_id'=>$host,'policy'=>$this->hash($p),'reason'=>$d['reason'],'result'=>$result,'after'=>$this->basis($p)]); + return response()->json($result+['replayed'=>false],201)->header('Cache-Control','no-store'); + }); + } + public function receipt(Request $r,ReceiptRuntime $rt,string $key) + { + abort_if($r->query()!==[],422);abort_unless(preg_match('/^[a-f0-9-]{36}$/D',$key),422); + return $this->run($r,$rt,'stock.planning.view',function($a,$p,$host)use($key){ + $row=DB::table('audit_logs')->where(['tenant_id'=>1,'user_id'=>$a->id,'action'=>'sgf.stock.completion.command'])->where('after->operation_key',$key)->first();abort_unless($row,404);$v=json_decode($row->after,true);abort_unless($v['host_actor_id']===$host&&$v['policy']===$this->hash($p),403); + return response()->json(['result'=>$v['result'],'reason'=>$v['reason'],'created_at'=>$row->created_at,'historical_receipt_only'=>true,'operation_executed'=>false])->header('Cache-Control','no-store'); + }); + } + private function execute(object $a,array $p,array $d,bool $review):array + { + $family=$d['family'];$action=$d['action'];abort_unless($family==='policy'?in_array($action,['save','approve','return','suspend'],true):in_array($action,['save','approve','return','execute'],true),422); + abort_unless($review===in_array($action,['approve','return','suspend'],true),403); + abort_if(SupplyRecord::where('operation_key',$d['operation_key'])->exists(),409,'Chave utilizada. Recupere a confirmação original.'); + if(!empty($d['record_key'])){$old=$this->query($p,$family)->where('record_key',$d['record_key'])->firstOrFail();abort_unless($old->version===$d['version'],409,'Versão alterada.');} + else abort_unless($d['version']===0&&$action==='save',409); + if($action==='save'){ + abort_unless(($d['data']['stock_item_id']??null)===$p['stock_item_id']&&($d['data']['warehouse_id']??null)===$p['warehouse_id'],403); + if($family==='supplier_return')abort_unless(($d['data']['kind']??null)==='supplier_return'&&($d['data']['receipt_item_id']??null)===$p['receipt_item_id']&&empty($d['data']['lot_id']),403); + else abort_unless(($d['data']['forecast_method']??null)==='manual'&&empty($d['data']['agreement_key'])&&($d['data']['origin']??null)==='local'&&($d['data']['purpose']??null)==='internal',422); + if(!empty($d['data']['predecessor_key']))abort_unless($this->query($p,'policy')->where('record_key',$d['data']['predecessor_key'])->where('version',$d['data']['predecessor_version']??0)->exists(),403); + } + // No automatic purchasing rule may act on this selected QA source. + abort_if(app(SupplyPlanning::class)->latest('auto_rule')->where('stock_item_id',$p['stock_item_id'])->where('warehouse_id',$p['warehouse_id'])->where('state','approved')->exists(),409,'Existe regra automática. É necessária conferência própria.'); + $envelope=array_intersect_key($d,array_flip(['operation_key','record_key','version','action','reason','data']));$before=app(StockWorkflow::class)->balance($p['stock_item_id'],$p['warehouse_id']); + $record=$family==='policy'?app(SupplyPlanning::class)->save($a,'policy',$envelope):app(InventoryDispositions::class)->save($a,$envelope); + $after=app(StockWorkflow::class)->balance($p['stock_item_id'],$p['warehouse_id']); + return ['record_id'=>$record->id,'record_key'=>$record->record_key,'version'=>$record->version,'impact'=>['family'=>$family,'action'=>$action,'state'=>$record->state,'before_milli'=>$before['quantity'],'after_milli'=>$after['quantity'],'physical_change_milli'=>$after['quantity']-$before['quantity'],'financial_credit_created'=>false,'financial_posting'=>false]]; + } +} diff --git a/app/Modules/Inventory/StockFinalController.php b/app/Modules/Inventory/StockFinalController.php new file mode 100644 index 0000000..ab1eb58 --- /dev/null +++ b/app/Modules/Inventory/StockFinalController.php @@ -0,0 +1,140 @@ +1,'company_id'=>2,'unit_id'=>2]&&isset($v['lot_item_id'],$v['transfer_item_id']),403);return $v; + } + private function run(Request $r,ReceiptRuntime $rt,string $right,\Closure $fn):mixed + { + $p=$this->policy();$hostActor=$r->user()->id;$generation=$this->hash($r->session()->get('company_context',[])); + try{return $rt->run($right,function($a,$b,$binding)use($fn,$p,$hostActor,$generation){ + $a=app(CurrentActor::class)->lock($a);abort_if(app(OrganizationAccess::class)->maintenanceOnly($a),403); + abort_unless(in_array($hostActor,[$p['dispatcher_host_id'],$p['reviewer_host_id']],true),403); + $review=$hostActor===$p['reviewer_host_id'];abort_unless($a->id===($review?$p['reviewer_native_id']:$p['dispatcher_native_id']),403); + foreach([$p['source_warehouse_id'],$p['destination_warehouse_id']] as $id)abort_unless(in_array($id,$b['warehouse_ids'],true),403); + return $fn($a,$p,$hostActor,$generation,$binding,$review); + });}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Operação ocupada. Recupere a mesma confirmação.'],503)->header('Retry-After','5');} + } + private function basis(array $p):array + { + $scope=$p['scope'];$db=DB::connection();$w=app(StockWorkflow::class); + return ['transfer_source'=>$w->balance($p['transfer_item_id'],$p['source_warehouse_id']),'transfer_destination'=>$w->balance($p['transfer_item_id'],$p['destination_warehouse_id']), + 'lot_balance'=>$w->balance($p['lot_item_id'],$p['source_warehouse_id']), + 'lot_context'=>app(InventoryLots::class)->context($p['lot_item_id'],$p['source_warehouse_id']), + 'lot_revision'=>(int)SupplyRecord::where('tenant_id',1)->where('stock_item_id',$p['lot_item_id'])->where('warehouse_id',$p['source_warehouse_id'])->max('id'), + 'transit_revision'=>(int)$db->table('sgf_stock_transit_events')->where($scope)->max('id'),'policy'=>$this->hash($p)]; + } + public function screen(Request $r,ReceiptRuntime $rt){return $this->run($r,$rt,'stock.view',fn()=>response()->view('stock-final-workspace',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store'));} + public function history(Request $r,ReceiptRuntime $rt) + { + $d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422); + return $this->run($r,$rt,'stock.view',function($a,$p)use($d){$q=DB::table('audit_logs')->where(['tenant_id'=>1,'action'=>'sgf.stock.final.command'])->whereIn('user_id',[$p['dispatcher_native_id'],$p['reviewer_native_id']])->whereIn('after->host_actor_id',[$p['dispatcher_host_id'],$p['reviewer_host_id']])->where('before->policy',$this->hash($p)); + $page=$q->orderByDesc('id')->paginate((int)($d['per_page']??25),['id','user_id','before','after','created_at'],'page',(int)($d['page']??1)); + $page->getCollection()->transform(function($row){$v=json_decode($row->after,true);return ['id'=>$row->id,'author_id'=>$row->user_id,'host_author_id'=>$v['host_actor_id'],'created_at'=>$row->created_at,'reason'=>$v['reason'],'operation_key'=>$v['operation_key'],'result'=>$v['result'],'before'=>json_decode($row->before,true),'after'=>$v['after']];});return response()->json($page)->header('Cache-Control','no-store');}); + } + public function index(Request $r,ReceiptRuntime $rt) + { + $d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','search'=>'nullable|string|max:100']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422); + return $this->run($r,$rt,'stock.view',function($a,$p,$host,$g,$binding,$review)use($d){ + abort_unless(app(InventoryLots::class)->allows($a,'inventory_lot_read'),403);$basis=$this->basis($p);$q=DB::table('sgf_stock_transit_events')->where($p['scope'])->where('action','dispatch'); + if(($d['search']??'')!=='')$q->whereRaw("transfer_key::text LIKE ? ESCAPE '!'",['%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%']); + $page=$q->orderByDesc('id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1));$ids=$page->pluck('transfer_key')->all(); + $settled=DB::table('sgf_stock_transit_events')->where($p['scope'])->whereIn('transfer_key',$ids)->where('action','<>','dispatch')->groupBy('transfer_key')->selectRaw('transfer_key,sum(quantity_milli) quantity')->get()->keyBy('transfer_key'); + $page->getCollection()->transform(function($row)use($settled){$row->remaining_milli=(int)$row->quantity_milli-(int)($settled->get($row->transfer_key)->quantity??0);return $row;}); + $records=SupplyRecord::where('tenant_id',1)->where('stock_item_id',$p['lot_item_id'])->where('warehouse_id',$p['source_warehouse_id'])->orderByDesc('id')->limit(100)->get(); + $counts=StockOperation::where('tenant_id',1)->where('stock_item_id',$p['lot_item_id'])->where('warehouse_id',$p['source_warehouse_id'])->where('kind','count')->whereNotIn('id',StockOperation::where('tenant_id',1)->whereNotNull('count_id')->select('count_id'))->orderByDesc('id')->limit(100)->get(['id','reference','quantity_milli']); + $inTransit=(int)DB::table('sgf_stock_transit_events')->where($p['scope'])->selectRaw("coalesce(sum(CASE WHEN action='dispatch' THEN quantity_milli ELSE -quantity_milli END),0) quantity")->value('quantity'); + return response()->json(['basis'=>$basis,'revision'=>$this->hash($basis),'reviewer'=>$review,'policy'=>$p,'items'=>StockItem::whereIn('id',[$p['lot_item_id'],$p['transfer_item_id']])->get(['id','code','name','unit']),'warehouses'=>Warehouse::whereIn('id',[$p['source_warehouse_id'],$p['destination_warehouse_id']])->get(['id','name']),'transfers'=>$page,'lot_records'=>$records,'counts'=>$counts,'lot_count_source'=>app(InventoryCounts::class)->source($p['lot_item_id'],$p['source_warehouse_id']),'in_transit_milli'=>$inTransit])->header('Cache-Control','no-store'); + }); + } + private function right(string $action):string{return match($action){'receive'=>'stock.transfers.receive','activate','release','adjustment'=>'stock.lots.review',default=>'stock.operations.prepare'};} + public function preview(Request $r,ReceiptRuntime $rt) + { + $d=$r->validate(['action'=>'required|in:dispatch,receive,return,prepare,activate,identify,hold,release,count,adjustment','operation_key'=>'required|uuid','revision'=>'required|string|size:64','reason'=>'required|string|min:10|max:4000','data'=>'present|array']);abort_if(array_diff(array_keys($r->all()),array_keys($d)),422);$d['operation_key']=strtolower($d['operation_key']); + return $this->run($r,$rt,$this->right($d['action']),function($a,$p,$host,$g,$binding,$review)use($d){ + abort_unless(hash_equals($this->hash($this->basis($p)),$d['revision']),409,'O estado mudou. Confira novamente.'); + $db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction();try{$result=$this->execute($a,$p,$host,$review,$d);}finally{while($db->transactionLevel()>$level)$db->rollBack();} + // Generated IDs are not a promise: preview inserts are rolled back. + $cut=['input'=>$d,'host'=>$host,'native'=>$a->id,'generation'=>$g,'binding'=>$binding,'observed'=>time(),'impact'=>$result['impact']]; + return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>$result['impact']])->header('Cache-Control','no-store'); + }); + } + public function confirm(Request $r,ReceiptRuntime $rt) + { + $r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422); + try{$cut=json_decode(Crypt::decryptString($r->input('preview')),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');} + abort_unless(in_array($cut['input']['action']??null,['dispatch','receive','return','prepare','activate','identify','hold','release','count','adjustment'],true),409); + return $this->run($r,$rt,$this->right($cut['input']['action']),function($a,$p,$host,$g,$binding,$review)use($cut){ + abort_unless($cut['host']===$host&&$cut['native']===$a->id&&$cut['generation']===$g&&$cut['binding']===$binding&&$cut['observed']>=time()-600&&$cut['observed']<=time(),409,'Contexto ou prévia expirados.');$d=$cut['input'];$db=DB::connection(); + $old=$db->table('audit_logs')->where(['tenant_id'=>1,'user_id'=>$a->id,'action'=>'sgf.stock.final.command'])->where('after->operation_key',$d['operation_key'])->first(); + if($old){$v=json_decode($old->after,true);abort_unless($v['request_hash']===$this->hash($d)&&$v['host_actor_id']===$host,409);return response()->json($v['result']+['replayed'=>true]);} + $before=$this->basis($p);abort_unless(hash_equals($this->hash($before),$d['revision']),409,'Estado alterado desde a conferência.'); + $result=$this->execute($a,$p,$host,$review,$d);abort_unless($result['impact']===$cut['impact'],409,'Impacto alterado.'); + app(RequisitionWorkflow::class)->audit($a,'sgf.stock.final.command',null,$before,['operation_key'=>$d['operation_key'],'request_hash'=>$this->hash($d),'host_actor_id'=>$host,'reason'=>$d['reason'],'result'=>$result,'after'=>$this->basis($p)]); + return response()->json($result+['replayed'=>false],201)->header('Cache-Control','no-store'); + }); + } + public function receipt(Request $r,ReceiptRuntime $rt,string $key) + { + abort_if($r->query()!==[],422);abort_unless(preg_match('/^[a-f0-9-]{36}$/D',$key),422); + return $this->run($r,$rt,'stock.view',function($a,$p,$host)use($key){$row=DB::table('audit_logs')->where(['tenant_id'=>1,'user_id'=>$a->id,'action'=>'sgf.stock.final.command'])->where('after->operation_key',$key)->first();abort_unless($row,404);$v=json_decode($row->after,true);abort_unless($v['host_actor_id']===$host,403);return response()->json(['command_key'=>$key,'created_at'=>$row->created_at,'reason'=>$v['reason'],'result'=>$v['result'],'operation_executed'=>false,'historical_receipt_only'=>true])->header('Cache-Control','no-store');}); + } + private function execute(object $a,array $p,int $host,bool $review,array $d):array + { + $action=$d['action'];$data=$d['data'];$w=app(StockWorkflow::class);$lots=app(InventoryLots::class); + abort_if(StockOperation::where('idempotency_key',$d['operation_key'])->exists()||SupplyRecord::where('operation_key',$d['operation_key'])->exists(),409,'Chave já utilizada. Consulte o recibo original; não altere o conteúdo.'); + if(in_array($action,['dispatch','receive','return'],true))return $this->transfer($a,$p,$host,$review,$d); + abort_unless($review===in_array($action,['activate','release','adjustment'],true),403); + if(in_array($action,['prepare','activate','identify','hold','release'],true)){ + $v=validator($data,['record_key'=>'nullable|uuid','version'=>'required|integer|min:0','data'=>'present|array'])->validate();abort_if(array_diff(array_keys($data),['record_key','version','data']),422); + if($action==='prepare'){abort_unless(($v['data']['stock_item_id']??null)===$p['lot_item_id']&&($v['data']['warehouse_id']??null)===$p['source_warehouse_id'],403);} + else{$old=SupplyRecord::where('tenant_id',1)->where('record_key',$v['record_key']??'')->orderByDesc('version')->firstOrFail();abort_unless($old->stock_item_id===$p['lot_item_id']&&$old->warehouse_id===$p['source_warehouse_id'],403);} + $record=$lots->save($a,['action'=>$action,'operation_key'=>$d['operation_key'],'record_key'=>$v['record_key']??null,'version'=>(int)$v['version'],'reason'=>$d['reason'],'data'=>$v['data']]); + return ['record_id'=>$record->id,'record_key'=>$record->record_key,'version'=>$record->version,'impact'=>['action'=>$action,'physical_change_milli'=>0,'state'=>$record->state,'stock_item_id'=>$p['lot_item_id'],'warehouse_id'=>$p['source_warehouse_id']]]; + } + abort_unless(in_array($action,['count','adjustment'],true),422); + $v=validator($data,['quantity'=>'required|string|regex:/^\d{1,9}(\.\d{1,3})?$/','ledger_version'=>'required|integer|min:0','lot_version'=>'nullable|string|size:64','lot_counts'=>'sometimes|array|max:100','count_id'=>'nullable|integer|min:1'])->validate();abort_if(array_diff(array_keys($data),array_keys($v)),422); + if($action==='adjustment'){$count=StockOperation::where('tenant_id',1)->findOrFail($v['count_id']??0);abort_unless($count->kind==='count'&&$count->stock_item_id===$p['lot_item_id']&&$count->warehouse_id===$p['source_warehouse_id'],403);} + $before=$w->balance($p['lot_item_id'],$p['source_warehouse_id']); + $op=$w->record($a,$v+['kind'=>$action,'idempotency_key'=>$d['operation_key'],'reference'=>'QA-LOTS-'.$d['operation_key'],'stock_item_id'=>$p['lot_item_id'],'warehouse_id'=>$p['source_warehouse_id'],'responsible_id'=>$a->id,'reason'=>$d['reason']]); + $after=$w->balance($p['lot_item_id'],$p['source_warehouse_id']); + return ['operation_id'=>$op->id,'impact'=>['action'=>$action,'physical_before_milli'=>$before['quantity'],'physical_after_milli'=>$after['quantity'],'physical_change_milli'=>$after['quantity']-$before['quantity'],'counted_milli'=>$op->quantity_milli,'financial_posting'=>false]]; + } + private function transfer(object $a,array $p,int $host,bool $review,array $d):array + { + $action=$d['action'];abort_unless($review===($action==='receive'),403);if(!$review)abort_unless($a->hasFunctions(['warehouse']),403); + $v=validator($d['data'],['quantity'=>'required|string|regex:/^\d{1,9}(\.\d{1,3})?$/','transfer_key'=>'required|uuid','stock_item_id'=>'required|integer','source_warehouse_id'=>'required|integer','destination_warehouse_id'=>'required|integer'])->validate();abort_if(array_diff(array_keys($d['data']),array_keys($v)),422); + abort_unless($v['stock_item_id']===$p['transfer_item_id']&&$v['source_warehouse_id']===$p['source_warehouse_id']&&$v['destination_warehouse_id']===$p['destination_warehouse_id'],403); + $db=DB::connection();$q=$db->table('sgf_stock_transit_events')->where($p['scope'])->where('transfer_key',$v['transfer_key']);$dispatch=(clone $q)->where('action','dispatch')->first();$qty=\Tempo\Purchasing\Embedded\Services\ReceiptWorkflow::milli($v['quantity']);abort_unless($qty>0,422); + $w=app(StockWorkflow::class);$warehouse=$action==='receive'?$p['destination_warehouse_id']:$p['source_warehouse_id'];$before=$w->balance($p['transfer_item_id'],$warehouse);$remaining=0; + if($action==='dispatch'){ + abort_if($dispatch!==null,409,'Expedição já registada.');abort_if(app(InventoryLots::class)->setup($p['transfer_item_id'],$warehouse)!==null,409,'Este protocolo exige artigo sem lotes.'); + $free=app(WarehouseReservations::class)->free($p['transfer_item_id'],$warehouse);abort_unless($before['quantity']>=$qty&&$free>=$qty,409,'Quantidade disponível insuficiente.');$remaining=$qty; + }else{ + abort_unless($dispatch&&$dispatch->host_actor_id!==$host||($dispatch&&$action==='return'),403); + $remaining=(int)$dispatch->quantity_milli-(int)(clone $q)->where('action','<>','dispatch')->sum('quantity_milli');abort_unless($qty<=$remaining,409,'A parcela excede o trânsito por receber.');$remaining-=$qty; + } + $sign=$action==='dispatch'?-1:1;$impact=['action'=>$action,'quantity_milli'=>$qty,'physical_before_milli'=>$before['quantity'],'physical_after_milli'=>$before['quantity']+$sign*$qty,'in_transit_after_milli'=>$remaining,'ownership_change_milli'=>0,'financial_posting'=>false]; + $snapshot=['sgf_internal_transit'=>['scope'=>$p['scope'],'transfer_key'=>$v['transfer_key'],'action'=>$action,'host_actor_id'=>$host,'source_warehouse_id'=>$p['source_warehouse_id'],'destination_warehouse_id'=>$p['destination_warehouse_id']],'development_only'=>true,'budget_checked'=>false,'impact'=>$impact]; + $op=StockOperation::create(['tenant_id'=>1,'created_by'=>$a->id,'responsible_id'=>$a->id,'stock_item_id'=>$p['transfer_item_id'],'warehouse_id'=>$warehouse,'destination_warehouse_id'=>$action==='dispatch'?$p['destination_warehouse_id']:null,'kind'=>'transfer','reference'=>'QA-TRANSIT-'.$d['operation_key'],'idempotency_key'=>$d['operation_key'],'payload_hash'=>$this->hash($d),'reason'=>$d['reason'],'quantity_milli'=>$qty,'theoretical_milli'=>$before['quantity'],'ledger_version'=>$before['version'],'snapshot'=>$snapshot,'recorded_at'=>now()]); + $movement=StockMovement::create(['tenant_id'=>1,'stock_operation_id'=>$op->id,'stock_item_id'=>$p['transfer_item_id'],'warehouse_id'=>$warehouse,'moved_by'=>$a->id,'movement_type'=>$action==='dispatch'?'transfer_out':'transfer_in','quantity_milli'=>$sign*$qty,'snapshot'=>$snapshot,'moved_at'=>now()]); + $id=$db->table('sgf_stock_transit_events')->insertGetId($p['scope']+['transfer_key'=>$v['transfer_key'],'command_key'=>$d['operation_key'],'request_hash'=>$this->hash($d),'action'=>$action,'actor_id'=>$a->id,'host_actor_id'=>$host,'stock_item_id'=>$p['transfer_item_id'],'source_warehouse_id'=>$p['source_warehouse_id'],'destination_warehouse_id'=>$p['destination_warehouse_id'],'quantity_milli'=>$qty,'movement_id'=>$movement->id,'reason'=>$d['reason'],'snapshot'=>json_encode($snapshot),'created_at'=>now()]); + return ['event_id'=>$id,'operation_id'=>$op->id,'movement_id'=>$movement->id,'transfer_key'=>$v['transfer_key'],'impact'=>$impact]; + } +} diff --git a/app/Modules/Inventory/StockInactiveReport.php b/app/Modules/Inventory/StockInactiveReport.php new file mode 100644 index 0000000..a30352e --- /dev/null +++ b/app/Modules/Inventory/StockInactiveReport.php @@ -0,0 +1,23 @@ +db->transaction(function()use($binding,$fingerprint,$f,$token){abort_unless($this->db->transactionLevel()===1,503);$this->db->statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY');$this->db->statement("SET LOCAL statement_timeout='3s'"); + $reader=new \Tempo\Inventory\InventoryReader($this->db,$binding['schema'],$binding['source_tenant_id'],$binding['warehouse_ids']);try{$revision=$reader->revision();}catch(\UnexpectedValueException){abort(503,'Fonte de stock incompatível.');}$observed=time(); + if($token!==null){try{$c=json_decode(Crypt::decryptString($token),true,32,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Referência de inactividade inválida. Actualize a consulta.');}abort_unless(($c['fingerprint']??null)===$fingerprint&&($c['revision']??null)===$revision&&is_int($c['observed']??null)&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'A origem ou o contexto mudou. Actualize o relatório.');$observed=$c['observed'];} + $asOf=new \DateTimeImmutable($f['as_of'],new \DateTimeZone('UTC'));$until=$asOf->modify('+1 day')->format('Y-m-d').' 00:00:00+00';$cut=$asOf->modify('-'.($f['days']-1).' days')->format('Y-m-d').' 00:00:00+00'; + $q=$this->db->table('public.stock_movements as m')->join('public.stock_items as i',fn($j)=>$j->on('i.id','=','m.stock_item_id')->on('i.tenant_id','=','m.tenant_id'))->join('public.warehouses as w',fn($j)=>$j->on('w.id','=','m.warehouse_id')->on('w.tenant_id','=','m.tenant_id'))->where('m.tenant_id',$binding['source_tenant_id'])->whereIn('m.warehouse_id',$binding['warehouse_ids'])->where('m.moved_at','<',$until); + if($f['warehouse_id']!==null)$q->where('m.warehouse_id',$f['warehouse_id']);if($f['search']!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$f['search']).'%';$q->where(fn($s)=>$s->whereRaw("i.code ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("i.name ILIKE ? ESCAPE '!'",[$needle]));} + $q->select(['m.stock_item_id','m.warehouse_id','i.code','i.name','i.unit','w.code as warehouse_code','w.name as warehouse_name'])->selectRaw('SUM(m.quantity_milli::numeric)::text physical_quantity_milli,MAX(m.moved_at) last_registered_at')->selectRaw("(?::date-(MAX(m.moved_at) AT TIME ZONE 'UTC')::date)::integer days_without_record",[$f['as_of']])->groupBy('m.stock_item_id','m.warehouse_id','i.code','i.name','i.unit','w.code','w.name')->havingRaw('MAX(m.moved_at) < ?',[$cut])->orderBy('m.stock_item_id')->orderBy('m.warehouse_id'); + $total=(int)$this->db->query()->fromSub((clone$q)->reorder(),'stock_inactive_pairs')->count();$rows=$q->limit($f['per_page'])->offset(($f['page']-1)*$f['per_page'])->get()->map(fn($r)=>(array)$r)->all(); + return ['status'=>'available','synthetic_only'=>true,'report'=>'known_pairs_inactivity','as_of'=>$f['as_of'],'minimum_days'=>$f['days'],'timezone'=>'UTC','rows'=>$rows,'pagination'=>['page'=>$f['page'],'per_page'=>$f['per_page'],'total'=>$total,'last_page'=>max(1,(int)ceil($total/$f['per_page']))],'observed_at'=>gmdate('c',$observed),'ledger_revision'=>$revision,'snapshot'=>Crypt::encryptString(json_encode(['fingerprint'=>$fingerprint,'revision'=>$revision,'observed'=>$observed],JSON_THROW_ON_ERROR)),'capabilities'=>['write'=>false,'valuation'=>false,'reserved'=>false,'blocked'=>false],'notice'=>'Último registo físico UTC em pares artigo/armazém com história conhecida até ao corte. Metadados actuais. Quantidade física não é disponível. Sem história não se atribui armazém nem se presume inactividade, saldo zero ou ausência de actividade real. Sem somar unidades; corte futuro não prevê movimentos por registar.']; + }); + } +} diff --git a/app/Modules/Inventory/StockOperationsController.php b/app/Modules/Inventory/StockOperationsController.php new file mode 100644 index 0000000..55376ff --- /dev/null +++ b/app/Modules/Inventory/StockOperationsController.php @@ -0,0 +1,150 @@ +run($right,$work); } + catch(\Illuminate\Database\QueryException $e){ + if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e; + return response()->json(['message'=>'Operação ocupada. Recupere a mesma confirmação; não crie outra chave.'],503)->header('Cache-Control','no-store')->header('Retry-After','5'); + } + } + private function guard(): void { abort_unless(config('stock_operations.enabled') === true,404); } + private function hash(array $value): string { return hash('sha256',json_encode($value,JSON_THROW_ON_ERROR)); } + private function generation(): string { return $this->hash(request()->session()->get('company_context',[])); } + + private function operator(object $actor): object + { + $actor=app(CurrentActor::class)->lock($actor,['warehouse']); + abort_if(app(OrganizationAccess::class)->maintenanceOnly($actor),403); + return $actor; + } + + private function entities(array $data,array $binding,object $actor): array + { + $warehouses=array_values(array_unique([$data['warehouse_id'],...($data['kind']==='transfer'?[$data['destination_warehouse_id']]:[])])); + foreach($warehouses as $id) abort_unless(in_array($id,$binding['warehouse_ids']??[],true),403,'Armazém fora do contexto autorizado.'); + $item=StockItem::where('tenant_id',$actor->tenant_id)->findOrFail($data['stock_item_id']); + $rows=Warehouse::where('tenant_id',$actor->tenant_id)->whereIn('id',$warehouses)->orderBy('id')->get(); + abort_unless($rows->count()===count($warehouses),403); + $responsible=User::where('tenant_id',$actor->tenant_id)->findOrFail($data['responsible_id']); + $basis=['item'=>$item->only(['id','code','name','unit']),'warehouses'=>$rows->map->only(['id','code','name'])->all(), + 'responsible'=>$responsible->only(['id','name']), 'balances'=>array_map(fn($id)=>['warehouse_id'=>$id]+app(StockWorkflow::class)->balance($item->id,$id),$warehouses)]; + if($data['kind']==='adjustment'){ + $count=StockOperation::where('tenant_id',$actor->tenant_id)->where('kind','count')->where('stock_item_id',$item->id)->where('warehouse_id',$data['warehouse_id'])->findOrFail($data['count_id']); + $basis['count']=$count->toArray();$basis['count_applied']=StockOperation::where('tenant_id',$actor->tenant_id)->where('count_id',$count->id)->exists(); + } + return $basis; + } + + private function execute(array $data,Request $http,object $actor): array + { + $input=Request::create('/embedded/stock-operation','POST',$data,[],[],['HTTP_ACCEPT'=>'application/json']); + $input->setUserResolver(fn($guard=null)=>$actor); + if($http->hasSession()) $input->setLaravelSession($http->session()); + $response=(new \Tempo\Purchasing\Embedded\Http\Controllers\StockOperationController)->store($input,app(StockWorkflow::class)); + $operation=StockOperation::findOrFail($response->getData(true)['id']); + $difference=match($operation->kind){'count'=>0,'adjustment'=>(int)$operation->quantity_milli-(int)$operation->theoretical_milli,default=>-(int)$operation->quantity_milli}; + return ['operation'=>$operation,'impact'=>[ + 'kind'=>$operation->kind,'reference'=>$operation->reference,'quantity_milli'=>$operation->quantity_milli, + 'stock_item_id'=>$operation->stock_item_id,'warehouse_id'=>$operation->warehouse_id, + 'destination_warehouse_id'=>$operation->destination_warehouse_id,'destination'=>$operation->destination, + 'theoretical_milli'=>$operation->theoretical_milli,'snapshot'=>$operation->snapshot, + 'change_milli'=>$difference,'quantity_after_milli'=>(int)$operation->theoretical_milli+$difference, + 'destination_quantity_after_milli'=>$operation->kind==='transfer'?app(StockWorkflow::class)->balance($operation->stock_item_id,$operation->destination_warehouse_id)['quantity']:null, + 'reason'=>$operation->reason,'financial_posting'=>false, + ]]; + } + + public function screen(Request $r,ReceiptRuntime $rt) + { + $this->guard();$access=$this->run($rt,'stock.view',fn($a)=>$this->operator($a)); + if($access instanceof \Symfony\Component\HttpFoundation\Response)return $access; + return response()->view('stock-operations-workspace',['context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()])->header('Cache-Control','no-store'); + } + + public function index(Request $r,ReceiptRuntime $rt) + { + $this->guard();$d=$r->validate(['view'=>'required|in:items,warehouses,people,equipment,counts,history','search'=>'nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','warehouse_id'=>'required_if:view,counts|prohibited_unless:view,counts|integer|min:1','stock_item_id'=>'required_if:view,counts|prohibited_unless:view,counts|integer|min:1']); + abort_if(array_diff(array_keys($r->query()),array_keys($d)),422); + return $this->run($rt,'stock.view',function($a,$b)use($d){ + $a=$this->operator($a);$kind=$d['view']; + if($kind==='counts')abort_unless(in_array((int)$d['warehouse_id'],$b['warehouse_ids']??[],true),403); + $q=match($kind){'items'=>StockItem::where('tenant_id',$a->tenant_id),'warehouses'=>Warehouse::where('tenant_id',$a->tenant_id)->whereIn('id',$b['warehouse_ids']??[]),'people'=>User::where('tenant_id',$a->tenant_id),'equipment'=>\Tempo\Purchasing\Embedded\Models\Equipment::where('tenant_id',$a->tenant_id),'counts'=>StockOperation::where('tenant_id',$a->tenant_id)->where('kind','count')->where('warehouse_id',$d['warehouse_id'])->where('stock_item_id',$d['stock_item_id'])->whereNotIn('id',StockOperation::where('tenant_id',$a->tenant_id)->whereNotNull('count_id')->select('count_id')),default=>StockOperation::where('tenant_id',$a->tenant_id)->whereIn('warehouse_id',$b['warehouse_ids']??[])->where(fn($q)=>$q->whereNull('destination_warehouse_id')->orWhereIn('destination_warehouse_id',$b['warehouse_ids']??[]))}; + if(($d['search']??'')!==''){ + $search='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%'; + $q->whereRaw((in_array($kind,['history','counts'],true)?'reference':($kind==='equipment'?'description':'name'))." ILIKE ? ESCAPE '!'",[$search]); + } + $fields=match($kind){'items'=>['id','code','name','unit'],'warehouses'=>['id','code','name'],'people'=>['id','name'],'equipment'=>['id','code','description as name'],'counts'=>['id','reference as name','quantity_milli','ledger_version'],default=>['id','kind','reference','stock_item_id','warehouse_id','destination_warehouse_id','quantity_milli','reason','recorded_at']}; + return response()->json($q->orderBy('id',$kind==='history'?'desc':'asc')->paginate((int)($d['per_page']??25),$fields,'page',(int)($d['page']??1)))->header('Cache-Control','no-store'); + }); + } + + public function basis(Request $r,ReceiptRuntime $rt) + { + $this->guard();$d=$this->data($r->query()); + return $this->run($rt,'stock.view',function($a,$b)use($d){$a=$this->operator($a);$basis=$this->entities($d,$b,$a);return response()->json(['basis'=>$basis,'revision'=>$this->hash($basis)])->header('Cache-Control','no-store');}); + } + + private function data(array $data): array + { + // Returns retain their own declaration and independent reception workflow. + $validated=Validator::make($data,[ + 'kind'=>'required|in:issue,transfer,count,adjustment','warehouse_id'=>'required|integer|min:1','stock_item_id'=>'required|integer|min:1', + 'responsible_id'=>'required|integer|min:1','destination_warehouse_id'=>'required_if:kind,transfer|prohibited_unless:kind,transfer|integer|min:1', + 'count_id'=>'required_if:kind,adjustment|prohibited_unless:kind,adjustment|integer|min:1', + ])->after(function($v)use($data){if(array_diff(array_keys($data),['kind','warehouse_id','stock_item_id','responsible_id','destination_warehouse_id','count_id']))$v->errors()->add('data','Campos não reconhecidos.');})->validate(); + foreach(['warehouse_id','stock_item_id','responsible_id','destination_warehouse_id','count_id'] as $key) if(isset($validated[$key])) $validated[$key]=(int)$validated[$key]; + return $validated; + } + + public function preview(Request $r,ReceiptRuntime $rt) + { + $this->guard();$d=$r->validate(['operation_key'=>'required|uuid','revision'=>'required|string|size:64','data'=>'required|array:kind,warehouse_id,stock_item_id,responsible_id,destination_warehouse_id,quantity,reference,reason,destination,equipment_id,count_id,ledger_version']); + abort_if(array_diff(array_keys($r->all()),array_keys($d)),422); + $d['operation_key']=strtolower($d['operation_key']);$generation=$this->generation(); + return $this->run($rt,'stock.operations.prepare',function($a,$b,$binding)use($d,$r,$generation){ + $a=$this->operator($a);$data=$d['data'];$keys=['kind','warehouse_id','stock_item_id','responsible_id','destination_warehouse_id','count_id']; + $basis=$this->entities($this->data(array_intersect_key($data,array_flip($keys))),$b,$a); + abort_unless(hash_equals($this->hash($basis),$d['revision']),409,'O saldo ou o contexto mudou. Confira novamente.'); + abort_if(StockOperation::where('idempotency_key',$d['operation_key'])->exists(),409,'Operação já registada. Consulte o histórico.'); + $db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction(); + try{$out=$this->execute($data+['idempotency_key'=>$d['operation_key']],$r,$a);}finally{while($db->transactionLevel()>$level)$db->rollBack();} + $cut=$d+['actor'=>$a->id,'binding'=>$binding,'generation'=>$generation,'observed'=>time(),'impact'=>$this->hash($out['impact'])]; + return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'basis'=>$basis,'impact'=>$out['impact']])->header('Cache-Control','no-store'); + }); + } + + public function confirm(Request $r,ReceiptRuntime $rt) + { + $this->guard();$d=$r->validate(['preview'=>'required|string|max:65536']);abort_if(array_diff(array_keys($r->all()),['preview']),422); + try{$cut=json_decode(Crypt::decryptString($d['preview']),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');} + $generation=$this->generation(); + return $this->run($rt,'stock.operations.confirm',function($a,$b,$binding,$origin)use($cut,$generation,$r){ + $a=$this->operator($a); + abort_unless(($cut['actor']??null)===$a->id&&($cut['binding']??null)===$binding&&($cut['generation']??null)===$generation&&is_int($cut['observed']??null)&&$cut['observed']>=time()-600&&$cut['observed']<=time(),409,'Prévia expirada ou contexto alterado.'); + $keys=['kind','warehouse_id','stock_item_id','responsible_id','destination_warehouse_id','count_id']; + $basis=$this->entities($this->data(array_intersect_key($cut['data'],array_flip($keys))),$b,$a); + $hash=$this->hash([$cut['operation_key'],$cut['data']]); + $old=AuditLog::where('tenant_id',$a->tenant_id)->where('user_id',$a->id)->where('action','sgf.stock.command')->where('after->operation_key',$cut['operation_key'])->first(); + if($old){abort_unless(($old->after['request_hash']??null)===$hash&&($old->after['binding_revision']??null)===$binding,409);return response()->json($old->after['result']+['replayed'=>true])->header('Cache-Control','no-store');} + abort_unless(hash_equals($this->hash($basis),$cut['revision']),409,'Movimentos posteriores à conferência. Actualize o saldo.'); + abort_if(StockOperation::where('idempotency_key',$cut['operation_key'])->exists(),409,'Chave utilizada fora desta conferência.'); + $out=$this->execute($cut['data']+['idempotency_key'=>$cut['operation_key']],$r,$a); + abort_unless(hash_equals($cut['impact'],$this->hash($out['impact'])),409,'Dependências alteradas. Confira novamente.'); + $result=['operation_id'=>$out['operation']->id,'kind'=>$out['operation']->kind,'reference'=>$out['operation']->reference]; + app(RequisitionWorkflow::class)->audit($a,'sgf.stock.command',null,$basis,$origin+['operation_key'=>$cut['operation_key'],'request_hash'=>$hash,'binding_revision'=>$binding,'reason'=>$out['operation']->reason,'result'=>$result,'impact'=>$out['impact']]); + return response()->json($result+['replayed'=>false],201)->header('Cache-Control','no-store'); + }); + } +} diff --git a/app/Modules/Purchasing/ComposedFinance.php b/app/Modules/Purchasing/ComposedFinance.php new file mode 100644 index 0000000..187884f --- /dev/null +++ b/app/Modules/Purchasing/ComposedFinance.php @@ -0,0 +1,32 @@ +['finance_prepare','finance_review','read_only'],'payment-prepare'=>['finance_prepare'],'allocation-prepare'=>['finance_prepare'],'reversal-prepare'=>['finance_prepare'],'payment-decide'=>['finance_review'],'allocation-decide'=>['finance_review'],'reversal-confirm'=>['finance_review']]; + public function run(string $action,\Closure $work):mixed + { + CompositionRuntime::assert();abort_unless(config('sgf_single_installation.finance_enabled')===true,404);abort_unless(isset(self::ACTIONS[$action]),404); + abort_unless(request()->session()->get('company_context.module_id')==='finance',409,'Seleccione Financeiro.'); + $ctx=app(CompanyContext::class);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId()];$host=DB::connection();$user=$ctx->userId(); + try{return $host->transaction(function()use($action,$work,$scope,$host,$user){ + $host->statement("SET LOCAL lock_timeout='5s'");$host->table('companies')->where('tenant_id',$scope['tenant_id'])->where('id',$scope['company_id'])->lockForUpdate()->firstOrFail(); + $profile=$host->table('sgf_presentation_profiles')->where($scope)->where('user_id',$user)->first();abort_unless($profile&&in_array($profile->profile_key,self::ACTIONS[$action],true),403); + return app(ReceiptRuntime::class)->run('supplier_payables.view',function($actor,$binding,$revision,$origin)use($action,$work,$scope,$host,$user,$profile){ + $r=DB::table('gl_sgf_composition')->where('tenant_id',$actor->tenant_id)->first();abort_unless($r&&$r->host_database===$host->getDatabaseName()&&(int)$r->company_id===$scope['company_id']&&(int)$r->unit_id===$scope['unit_id'],403); + $orders=PurchaseOrder::visibleTo($actor)->where(fn($q)=>$q->whereIn('id',$binding['order_ids']??[])->orWhereIn('requisition_id',RequisitionAccess::ids($binding))); + $unknown=DB::table('pa_sources as s')->join('pa_obligations as o','o.source_id','=','s.id')->join('invoices as i','i.id','=','s.local_source_id')->where(['s.tenant_id'=>$actor->tenant_id,'s.company_id'=>0,'s.unit_id'=>0])->whereNotIn('i.purchase_order_id',$orders->select('id'))->exists(); + abort_if($unknown,503,'Há origens financeiras sem unidade conferida; a partição tem de ser resolvida explicitamente.'); + $service=app(SupplierPayables::class);$service->readContext($actor);$engine=$service->engine($actor); + return $work($engine,$engine->settlements(),$actor,$origin+['sgf_host_user'=>$user,'sgf_host_scope'=>$scope,'profile'=>$profile->profile_key],$orders); + }); + });}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Livro ocupado. Recupere a mesma operação, conservando a chave e os dados.'],503)->header('Retry-After','5')->header('Cache-Control','no-store');} + } +} diff --git a/app/Modules/Purchasing/ComposedFinanceController.php b/app/Modules/Purchasing/ComposedFinanceController.php new file mode 100644 index 0000000..1840bdf --- /dev/null +++ b/app/Modules/Purchasing/ComposedFinanceController.php @@ -0,0 +1,37 @@ +validate(['document_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','payments_page'=>'sometimes|integer|min:1|max:100000','allocation_proposals_page'=>'sometimes|integer|min:1|max:100000','allocations_page'=>'sometimes|integer|min:1|max:100000','payment_events_page'=>'sometimes|integer|min:1|max:100000','payment_per_page'=>'sometimes|integer|in:25,100','payment_q'=>'sometimes|string|max:100','payment_state'=>'sometimes|in:all,proposed,confirmed,rejected']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422); + return $finance->run('view',function($engine,$settlements,$actor,$origin,$orders)use($r,$d){ + $q=Invoice::where('tenant_id',$actor->tenant_id)->whereIn('purchase_order_id',$orders->select('id'));if(isset($d['document_id']))$q->where('id',(int)$d['document_id']);$page=$q->orderBy('id')->paginate(25,['id','number'],'page',(int)($d['page']??1));if(isset($d['document_id']))abort_unless($page->total()===1,403,'Documento fora da origem financeira autorizada.'); + $base=$engine->positionsPage(array_column($page->items(),'id'));$money=$settlements->listingPage(array_diff_key($d,['page'=>true,'document_id'=>true])); + $numbers=DB::table('pa_sources as s')->join('invoices as i','i.id','=','s.local_source_id')->where('s.tenant_id',$actor->tenant_id)->whereIn('s.id',array_column($base['obligations'],'source_id'))->pluck('i.number','s.id'); + foreach($base['obligations'] as &$o)$o['document_number']=$numbers->get($o['source_id']);unset($o); + $data=['obligations'=>$base['obligations'],'money'=>$money,'profile'=>$origin['profile'],'pagination'=>['page'=>$page->currentPage(),'last_page'=>$page->lastPage()]]; + if($r->is('api/*'))return response()->json($data)->header('Cache-Control','no-store'); + return response()->view('finance.composed-payables',['data'=>$data,'context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()])->header('Cache-Control','no-store'); + }); + } + + public function command(string $action,Request $r,ComposedFinance $finance) + { + $key=strtolower((string)$r->header('Idempotency-Key'));abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$data=$r->all();abort_if(strlen(json_encode($data))>65536,422); + try{return $finance->run($action,function($engine,$settlements,$actor,$origin)use($action,$key,$data){ + $hash=hash('sha256',Payables::canonical([$action,$data]));$old=AuditLog::where('tenant_id',$actor->tenant_id)->where('action','sgf.finance.command')->where('after->operation_key',$key)->first(); + if($old)abort_unless(($old->after['sgf_context_key']??null)===$origin['sgf_context_key']&&($old->after['request_hash']??null)===$hash,409,'Chave pertence a outro contexto/conteúdo.'); + $result=$settlements->command($action,$data,$key); + if(!$old)app(RequisitionWorkflow::class)->audit($actor,'sgf.finance.command',null,null,$origin+['operation_key'=>$key,'request_hash'=>$hash,'command'=>$action,'result'=>$result,'reason'=>$data['reason']??'']); + return response()->json($result)->header('Cache-Control','no-store'); + });}catch(\InvalidArgumentException $e){abort(422,$e->getMessage());}catch(\DomainException $e){abort(409,$e->getMessage());} + } +} diff --git a/app/Modules/Purchasing/PayableAge.php b/app/Modules/Purchasing/PayableAge.php new file mode 100644 index 0000000..7268419 --- /dev/null +++ b/app/Modules/Purchasing/PayableAge.php @@ -0,0 +1,19 @@ +diff(new \DateTimeImmutable($reference,$zone))->format('%r%a'));$bucket=$days===0?'not_due':($days<=30?'days_1_30':($days<=60?'days_31_60':($days<=90?'days_61_90':'days_over_90')));} + } + return ['reviewed_age_days'=>$days,'reviewed_age_bucket'=>$bucket]; + } +} diff --git a/app/Modules/Purchasing/PayableDueController.php b/app/Modules/Purchasing/PayableDueController.php new file mode 100644 index 0000000..eb4c5cb --- /dev/null +++ b/app/Modules/Purchasing/PayableDueController.php @@ -0,0 +1,64 @@ +validate();} + private function run(Request $r,string $action,\Closure $fn):mixed + { + abort_unless(config('payable_due.enabled')===true,404);$physical=CompositionRuntime::physical();$host=DB::connection();$user=(int)$r->user()->id;$ctx=app(CompanyContext::class);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId()];$generation=$this->hash($r->session()->get('company_context',[])); + return app(ComposedFinance::class)->run('view',function($engine,$settlements,$actor,$origin,$orders)use($action,$fn,$host,$user,$scope,$generation,$physical){ + $meta=DB::table('pd_due_meta')->where('policy',self::POLICY)->first();abort_unless($meta&&$meta->composition===$physical['composition']&&hash_equals($meta->source_hash,(string)config('payable_due.schema_sha256')),503,'Schema de vencimentos não conferido.'); + if($action!=='view'){$expected=$action==='prepare'?13:14;$profile=$action==='prepare'?'finance_prepare':'finance_review';abort_unless($user===$expected&&$origin['profile']===$profile&&$host->table('permission_grants')->where($scope+['user_id'=>$user,'permission'=>'supplier_payables.due.'.$action])->lockForUpdate()->first(),403);} + $person=DB::table('pa_access')->where(['tenant_id'=>$actor->tenant_id,'company_id'=>0,'unit_id'=>0,'user_id'=>$actor->id,'action'=>'view'])->lockForUpdate()->value('person');abort_unless(is_string($person)&&$person!=='',403); + return $fn($engine,$actor,$origin,$orders,$scope,$user,$generation,$this->hash([$physical,$origin['sgf_context_key'],$scope])); + }); + } + private function input(array $v):array + { + if(isset($v['reason'])&&is_string($v['reason']))$v['reason']=trim($v['reason']);$d=$this->exact($v,['action'=>'required|in:prepare,review','obligation_id'=>'required|integer|min:1','version'=>'required|integer|min:0','data'=>'present|array','reason'=>'required|string|min:10|max:2000']);$d['obligation_id']=(int)$d['obligation_id'];$d['version']=(int)$d['version']; + if($d['action']==='prepare'){$data=$d['data'];if(isset($data['evidence'])&&is_string($data['evidence']))$data['evidence']=trim($data['evidence']);$d['data']=$this->exact($data,['due_on'=>'present|nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2100-12-31','evidence'=>'required|string|min:10|max:1000']);} + else $d['data']=$this->exact($d['data'],['decision'=>'required|in:approved,refused']);return$d; + } + private function basis($engine,$actor,$orders,array $scope,int $id):array + { + $source=DB::table('pa_obligations as o')->join('pa_sources as s','s.id','=','o.source_id')->join('invoices as i','i.id','=','s.local_source_id')->where(['o.id'=>$id,'o.tenant_id'=>$actor->tenant_id,'o.company_id'=>0,'o.unit_id'=>0,'s.tenant_id'=>$actor->tenant_id,'s.company_id'=>0,'s.unit_id'=>0,'i.tenant_id'=>$actor->tenant_id])->whereIn('i.purchase_order_id',$orders->select('id'))->first(['i.id']);abort_unless($source,403,'Obrigação fora da origem autorizada.'); + $identities=DB::table('pa_sources')->where(['tenant_id'=>$actor->tenant_id,'company_id'=>0,'unit_id'=>0,'local_source_id'=>$source->id])->count();abort_unless($identities===1,409,'Identidade financeira ambígua.'); + $book=$engine->positionsPage([(int)$source->id]);$position=collect($book['obligations'])->firstWhere('id',$id);$document=collect($book['documents'])->firstWhere('local_id',(int)$source->id);abort_unless($position&&$document&&!$position['source_conflict']&&$position['balance_minor']!==null,409,'Origem ou saldo exige nova conferência.'); + $q=DB::table('pd_due_versions')->where($scope+['obligation_id'=>$id]);$last=(clone$q)->orderByDesc('version')->first();$approved=(clone$q)->where('state','approved')->orderByDesc('version')->first(); + return ['current_person'=>DB::table('pa_access')->where(['tenant_id'=>$actor->tenant_id,'company_id'=>0,'unit_id'=>0,'user_id'=>$actor->id,'action'=>'view'])->value('person'),'version'=>(int)($last?->version??0),'last'=>$last?(array)$last:null,'approved'=>$approved?(array)$approved:null,'position'=>$position,'document'=>$document,'source_basis'=>$this->hash([$position,$document])]; + } + private function execute($actor,array $scope,int $host,array $d,array $before):array + { + abort_unless($before['version']===$d['version'],409,'A versão mudou; confira novamente.');$last=$before['last'];$person=DB::table('pa_access')->where(['tenant_id'=>$actor->tenant_id,'company_id'=>0,'unit_id'=>0,'user_id'=>$actor->id,'action'=>'view'])->value('person'); + if($d['action']==='prepare'){$state='prepared';$data=$d['data']+['_prepared_person'=>$person];$prepared=$host;$native=$actor->id;} + else{abort_unless($last&&$last['state']==='prepared'&&(int)$last['prepared_host_id']!==$host&&(int)$last['prepared_native_id']!==$actor->id,403,'Revisão exige preparação alheia actual.');abort_unless(hash_equals($last['source_basis'],$before['source_basis']),409,'Origem ou saldo alterado desde a preparação; prepare novamente.');$state=$d['data']['decision'];$data=json_decode($last['data'],true);$preparedPerson=DB::table('pa_access')->where(['tenant_id'=>$actor->tenant_id,'company_id'=>0,'unit_id'=>0,'user_id'=>$last['prepared_native_id'],'action'=>'view'])->lockForUpdate()->value('person');abort_unless(isset($data['_prepared_person'])&&$preparedPerson===$data['_prepared_person'],409,'Identidade do preparador alterada; prepare novamente.');abort_unless($person!==$preparedPerson,403,'A mesma pessoa não pode rever a preparação.');$prepared=$last['prepared_host_id'];$native=$last['prepared_native_id'];} + $version=$before['version']+1;$id=DB::table('pd_due_versions')->insertGetId($scope+['obligation_id'=>$d['obligation_id'],'version'=>$version,'previous_id'=>$last['id']??null,'action'=>$d['action'],'state'=>$state,'source_basis'=>$before['source_basis'],'prepared_host_id'=>$prepared,'prepared_native_id'=>$native,'host_actor_id'=>$host,'actor_id'=>$actor->id,'data'=>json_encode($data,JSON_THROW_ON_ERROR),'reason'=>$d['reason'],'created_at'=>now()->utc()]); + $prior=$before['approved']?json_decode($before['approved']['data'],true):null;$impact=['before'=>['version'=>$before['version'],'state'=>$last['state']??null,'reviewed_terms'=>$prior],'after'=>['version'=>$version,'state'=>$state,'proposal'=>$data,'reviewed_terms'=>$state==='approved'?$data:$prior],'source_basis'=>$before['source_basis'],'principal_minor'=>(string)$before['position']['principal_minor'],'balance_minor'=>(string)$before['position']['balance_minor'],'currency'=>$before['position']['currency'],'original_due_on'=>$before['position']['due_on'],'payment_created'=>false,'journal_entries'=>0,'original_obligation_changed'=>false]; + return ['id'=>$id,'obligation_id'=>$d['obligation_id'],'version'=>$version,'state'=>$state,'impact'=>$impact]; + } + public function preview(Request $r) + { + $d=$this->input($r->all());$key=strtolower((string)$r->header('Idempotency-Key'));$this->exact(['key'=>$key],['key'=>'required|uuid']);return$this->run($r,$d['action'],function($engine,$a,$origin,$orders,$scope,$host,$generation,$binding)use($d,$key){$before=$this->basis($engine,$a,$orders,$scope,$d['obligation_id']);$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction();try{$out=$this->execute($a,$scope,$host,$d,$before);}finally{while($db->transactionLevel()>$level)$db->rollBack();}$cut=['actor'=>$host,'generation'=>$generation,'binding'=>$binding,'input'=>$d,'key'=>$key,'observed'=>time(),'basis'=>$this->hash($before),'impact'=>$out['impact']];return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>$out['impact']])->header('Cache-Control','no-store');}); + } + public function confirm(Request $r) + { + $v=$this->exact($r->all(),['preview'=>'required|string|max:65536']);try{$cut=json_decode(Crypt::decryptString($v['preview']),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}abort_unless(is_array($cut)&&is_array($cut['input']??null),409);$d=$this->input($cut['input']);return$this->run($r,$d['action'],function($engine,$a,$origin,$orders,$scope,$host,$generation,$binding)use($cut,$d){abort_unless(($cut['actor']??null)===$host&&($cut['generation']??null)===$generation&&($cut['binding']??null)===$binding&&is_int($cut['observed']??null)&&$cut['observed']>=time()-600&&$cut['observed']<=time(),409,'Prévia/contexto alterados.');$before=$this->basis($engine,$a,$orders,$scope,$d['obligation_id']);$command=$scope+['host_actor_id'=>$host,'command_key'=>$cut['key']];$old=DB::table('pd_due_commands')->where($command)->first();if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d))&&hash_equals($old->binding_hash,$binding),409,'Chave utilizada com outros dados.');return response()->json(json_decode($old->result,true)+['replayed'=>true])->header('Cache-Control','no-store');}abort_unless(hash_equals($cut['basis'],$this->hash($before)),409,'Estado alterado desde a prévia. Confira novamente.');$out=$this->execute($a,$scope,$host,$d,$before);abort_unless($out['impact']===$cut['impact'],409,'Impacto alterado.');app(RequisitionWorkflow::class)->audit($a,'sgf.finance.due.command',null,$before,['host_actor_id'=>$host,'operation_key'=>$cut['key'],'reason'=>$d['reason'],'after'=>$out]);DB::table('pd_due_commands')->insert($command+['actor_id'=>$a->id,'request_hash'=>$this->hash($d),'binding_hash'=>$binding,'input'=>json_encode($d,JSON_THROW_ON_ERROR),'result'=>json_encode($out,JSON_THROW_ON_ERROR),'created_at'=>now()->utc()]);return response()->json($out+['replayed'=>false],201)->header('Cache-Control','no-store');}); + } + public function receipt(Request $r,string $key) + { + abort_if($r->query()!==[],422);$this->exact(['key'=>$key],['key'=>'required|uuid']);return$this->run($r,'view',function($engine,$a,$origin,$orders,$scope,$host,$generation,$binding)use($key){$old=DB::table('pd_due_commands')->where($scope+['host_actor_id'=>$host,'command_key'=>$key])->first();abort_unless($old,404);abort_unless(hash_equals($old->binding_hash,$binding),409);$d=json_decode($old->input,true);$this->basis($engine,$a,$orders,$scope,$d['obligation_id']);return response()->json(['result'=>json_decode($old->result,true),'reason'=>$d['reason'],'created_at'=>$old->created_at,'historical_receipt_only'=>true,'operation_executed'=>false])->header('Cache-Control','no-store');}); + } + public function index(Request $r) + { + $d=$this->exact($r->query(),['obligation_id'=>'required|integer|min:1','reference_on'=>'required|date_format:Y-m-d','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);$hostDb=DB::connection();return$this->run($r,'view',function($engine,$a,$origin,$orders,$scope,$host)use($d,$hostDb){$before=$this->basis($engine,$a,$orders,$scope,(int)$d['obligation_id']);$approved=$before['approved'];$terms=$approved?json_decode($approved['data'],true):null;$date=$terms['due_on']??null;$days=$date===null?null:max(0,(int)(new \DateTimeImmutable($date,new \DateTimeZone('UTC')))->diff(new \DateTimeImmutable($d['reference_on'],new \DateTimeZone('UTC')))->format('%r%a'));$q=DB::table('pd_due_versions')->where($scope+['obligation_id'=>(int)$d['obligation_id']]);$history=$q->orderByDesc('version')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1));foreach($history->items()as$row)$row->data=json_decode($row->data,true);$cap=fn($action,$expected)=>$host===$expected&&$hostDb->table('permission_grants')->where($scope+['user_id'=>$host,'permission'=>'supplier_payables.due.'.$action])->exists();return response()->json(['basis'=>$before,'reviewed_terms'=>$terms,'reference_on'=>$d['reference_on'],'days_overdue'=>$days,'due_status'=>$date===null?'unknown':($date<$d['reference_on']?'overdue':'not_overdue'),'history'=>$history,'capabilities'=>['prepare'=>$cap('prepare',13),'review'=>$cap('review',14)],'notice'=>'Condição administrativa QA revista, separada do vencimento original. Saldo actual; referência apenas classifica atraso, sem juros ou pagamento.'])->header('Cache-Control','no-store');}); + } +} diff --git a/app/Modules/Purchasing/PayablePositionReadController.php b/app/Modules/Purchasing/PayablePositionReadController.php new file mode 100644 index 0000000..0584fa0 --- /dev/null +++ b/app/Modules/Purchasing/PayablePositionReadController.php @@ -0,0 +1,33 @@ +where('policy',PayableDueController::POLICY)->first();abort_unless($meta&&DB::connection()->getDatabaseName()==='compras_receipts_test_'.$meta->composition&&hash_equals($meta->source_hash,(string)config('payable_due.schema_sha256')),503,'Condições administrativas não conferidas.'); + $q=DB::table('pd_due_versions')->where($scope)->whereIn('obligation_id',$ids)->where('state','approved')->whereNotExists(function($next){$next->selectRaw('1')->from('pd_due_versions as newer')->where('newer.state','approved');foreach(['tenant_id','company_id','unit_id','obligation_id']as$field)$next->whereColumn('newer.'.$field,'pd_due_versions.'.$field);$next->whereColumn('newer.version','>','pd_due_versions.version');}); + $out=[];foreach($q->get(['obligation_id','version','data','created_at'])as$row){$d=json_decode($row->data,true);$out[$row->obligation_id]=['version'=>(int)$row->version,'due_on'=>$d['due_on'],'evidence'=>$d['evidence'],'reviewed_at'=>$row->created_at];}return$out; + } + public function index(Request $r,ComposedFinance $finance) + { + $d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','search'=>'sometimes|nullable|string|max:100','document_state'=>'sometimes|in:pending,validated','reference_on'=>'sometimes|date_format:Y-m-d']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422); + try{return $finance->run('view',function($engine,$settlements,$actor,$origin,$orders)use($d){ + $q=Invoice::query()->where('invoices.tenant_id',$actor->tenant_id)->whereIn('invoices.purchase_order_id',$orders->select('id')) + ->join('purchase_orders as po',fn($j)=>$j->on('po.id','=','invoices.purchase_order_id')->on('po.tenant_id','=','invoices.tenant_id')) + ->leftJoin('suppliers as supplier',fn($j)=>$j->on('supplier.id','=','po.supplier_id')->on('supplier.tenant_id','=','po.tenant_id')); + if(isset($d['document_state']))$q->where('invoices.status',$d['document_state']); + if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($x)=>$x->whereRaw("invoices.number ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("supplier.name ILIKE ? ESCAPE '!'",[$needle]));} + $p=$q->orderByDesc('invoices.id')->paginate((int)($d['per_page']??25),['invoices.id'],'page',(int)($d['page']??1));$ids=$p->pluck('id')->map(fn($id)=>(int)$id)->all();$book=$engine->positionsPage($ids);$positions=collect($book['obligations'])->keyBy('source_id');$sources=DB::table('pa_sources')->where('tenant_id',$actor->tenant_id)->whereIn('local_source_id',$ids)->where(['company_id'=>0,'unit_id'=>0])->get(['id','local_source_id'])->groupBy('local_source_id');$documents=collect($book['documents'])->keyBy('local_id');$rows=[]; + foreach($ids as$id){$source=$documents->get($id);abort_unless($source,409,'Documento de origem alterado durante a consulta.');$identities=$sources->get($id,collect());abort_if($identities->count()>1,409,'Há mais de uma identidade financeira para este documento; exige conferência.');$position=$positions->get($identities->first()?->id);$money=[];foreach(['principal_minor','balance_minor','confirmed_payments_minor','allocated_payments_minor']as$field)$money[$field]=$position&&$position[$field]!==null?(string)$position[$field]:null;$rows[]=['document_id'=>$id,'reference'=>$source['number'],'supplier'=>$source['supplier_name'],'supplier_id'=>$source['supplier_id'],'document_state'=>$source['status'],'currency'=>$position['currency']??$source['currency'],'document_currency'=>$source['currency'],'obligation_id'=>$position['id']??null,'recognition_state'=>$position?'recognised':'not_recognised','payment_state'=>$position['payment_state']??'not_recognised','source_conflict'=>$position['source_conflict']??false,'source_revision'=>$source['revision'],'due_on'=>$position['due_on']??null,'due_status'=>$position['due_status']??'not_recognised','days_overdue'=>$position['days_overdue']??null,'pending'=>$position['documentary_pending']??$source['missing']]+$money;} + $terms=$this->reviewedTerms($origin['sgf_host_scope'],array_values(array_filter(array_column($rows,'obligation_id'))));foreach($rows as&$row){$row['reviewed_terms']=$terms[$row['obligation_id']]??null;$row+=PayableAge::classify($row,$d['reference_on']??null);}unset($row); + return response()->json(['positions'=>$rows,'aging_reference_on'=>$d['reference_on']??null,'aging_basis'=>'reviewed_administrative_terms_current_balance','pagination'=>['page'=>$p->currentPage(),'last_page'=>$p->lastPage(),'per_page'=>$p->perPage(),'total'=>$p->total()],'observed_at'=>now()->utc()->toIso8601String(),'balance_basis'=>'original_current_payable_engine','totals_basis'=>'visible_page_only','synthetic_only'=>true,'notice'=>'Documentos sem obrigação reconhecida e saldos indeterminados não são apresentados como dívida zero. Cada página é uma leitura actual; não é um relatório histórico.'])->header('Cache-Control','no-store, private'); + });}catch(\InvalidArgumentException$e){abort(422,$e->getMessage());}catch(\DomainException$e){abort(403,$e->getMessage());} + } +} diff --git a/app/Modules/Purchasing/ReceiptRuntime.php b/app/Modules/Purchasing/ReceiptRuntime.php index e40768d..f32feca 100644 --- a/app/Modules/Purchasing/ReceiptRuntime.php +++ b/app/Modules/Purchasing/ReceiptRuntime.php @@ -21,6 +21,16 @@ final class ReceiptRuntime public function run(string $right,\Closure $work):mixed { abort_unless(config('receipt_qa.enabled')===true,404); + if(str_starts_with($right,'general_ledger.'))abort_unless(config('sgf_single_installation.enabled')===true&&config('sgf_single_installation.ledger_enabled')===true,404); + if(str_starts_with($right,'supplier_payables.'))abort_unless(config('sgf_single_installation.enabled')===true&&config('sgf_single_installation.finance_enabled')===true,404); + if(str_starts_with($right,'stock.')){ + abort_unless(config('stock_operations.enabled')===true,404); + $allowed=['stock.view','stock.operations.prepare','stock.operations.confirm']; + if(config('stock_operations.final_enabled')===true)$allowed=[...$allowed,'stock.transfers.receive','stock.lots.review']; + if(config('stock_catalogue.enabled')===true)$allowed=[...$allowed,'stock.items.prepare','stock.items.review']; + if(config('stock_completion.enabled')===true)$allowed=[...$allowed,'stock.planning.view','stock.planning.prepare','stock.planning.review','stock.disposition.view','stock.disposition.prepare','stock.disposition.review']; + abort_unless(in_array($right,$allowed,true),403); + } if(str_starts_with($right,'purchasing_obligations.'))abort_unless(config('receipt_qa.obligations_enabled')===true,404); if(str_starts_with($right,'purchasing_invoices.'))abort_unless(config('receipt_qa.invoices_enabled')===true,404); if(str_starts_with($right,'purchasing_requests.'))abort_unless(config('receipt_qa.requests_enabled')===true,404); @@ -41,7 +51,23 @@ final class ReceiptRuntime $p=json_decode(file_get_contents($profile),true,64,JSON_THROW_ON_ERROR); $ctx=app(CompanyContext::class);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId()];$user=$ctx->userId(); abort_unless($scope['unit_id']&&request()->user()->id===$user,403); - $key=implode(':',array_values($scope)).':'.$user;$binding=$p['bindings'][$key]??null;abort_unless($binding,403,'Origem de recepção não autorizada neste contexto.'); + $key=implode(':',array_values($scope)).':'.$user;$binding=$p['bindings'][$key]??null; + if(config('sgf_single_installation.enabled')===true&&config('sgf_single_installation.bindings_sha256')!==null){ + $file=$expected.'/single-bindings.json';$pin=config('sgf_single_installation.bindings_sha256'); + abort_unless(is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503); + $additional=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR); + abort_unless(($additional['composition']??null)===$m[1],403); + if(isset($additional['bindings'][$key]))$binding=$additional['bindings'][$key]; + } + abort_unless($binding,403,'Origem de recepção não autorizada neste contexto.'); + // A Stock-specific map is explicit and pinned; it cannot change purchasing bindings. + if(str_starts_with($right,'stock.')&&config('stock_operations.profile_sha256')!==null){ + $mapFile=$expected.'/stock-operations-binding.json';$pin=config('stock_operations.profile_sha256'); + abort_unless(is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin)&&is_file($mapFile)&&!is_link($mapFile)&&hash_equals($pin,hash_file('sha256',$mapFile)),503); + $map=json_decode(file_get_contents($mapFile),true,32,JSON_THROW_ON_ERROR);$warehouses=$map['warehouse_bindings'][$key]??null; + abort_unless(($map['composition']??null)===$m[1]&&is_array($warehouses)&&count($warehouses)>0&&count($warehouses)<=1000&&!array_filter($warehouses,fn($id)=>!is_int($id)||$id<1),403); + $binding['warehouse_ids']=$warehouses;$binding['stock_profile_sha256']=$pin; + } if(config('receipt_qa.obligations_enabled')===true)abort_unless(($binding['payable_instance']??null)==='qa-stock-ledger-'.$m[1]&&config('common-payables.source_instance')===$binding['payable_instance']&&config('common-payables.history_enabled')===false&&config('common-payables.history_decisions_enabled')===false,403); $p['context_key']=$key; $bundle=realpath($expected.'/bundle');$manifest=$bundle.'/manifest.json'; @@ -69,11 +95,15 @@ final class ReceiptRuntime $i=$native->selectOne("SELECT current_user role,current_database() db,current_setting('cluster_name') cluster,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user"); abort_unless(!$i->elevated&&$i->role===$connection['username']&&$i->db===$connection['database']&&$i->cluster==='sgf_modular_regression_'.$run,403); return $host->transaction(function()use($host,$scope,$user,$right,$binding,$work,$p,$native){ + if(str_starts_with($right,'stock.')){$host->statement("SET LOCAL lock_timeout='5s'");$host->statement("SET LOCAL statement_timeout='15s'");} // Hold existing authority until the native transaction ends; no host business writes. abort_unless($host->table('users')->where('id',$user)->lockForUpdate()->first(),403); abort_unless($host->table('company_memberships')->where(['user_id'=>$user,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id']])->lockForUpdate()->first(),403); $view=str_starts_with($right,'purchasing_invoices.')?'purchasing_invoices.view':(str_starts_with($right,'purchasing_requests.')?'purchasing_requests.view':(str_starts_with($right,'purchasing_suppliers.')?'purchasing_suppliers.view':(str_starts_with($right,'purchasing_sourcing.')?'purchasing_sourcing.view':(str_starts_with($right,'purchasing_orders.')?'purchasing_orders.view':'purchasing_receipts.view')))); if(str_starts_with($right,'purchasing_obligations.'))$view='purchasing_obligations.view'; + if(str_starts_with($right,'stock.'))$view='stock.view'; + if(str_starts_with($right,'general_ledger.'))$view='general_ledger.view'; + if(str_starts_with($right,'supplier_payables.'))$view='supplier_payables.view'; foreach(['company.view'=>null,'unit.view'=>$scope['unit_id'],$view=>$scope['unit_id'],$right=>$scope['unit_id']]as$permission=>$unit) abort_unless($host->table('permission_grants')->where(['user_id'=>$user,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'unit_id'=>$unit,'permission'=>$permission])->lockForUpdate()->first(),403); $default=DB::getDefaultConnection();$guard=Auth::getDefaultDriver();$environment=app()->environment(); @@ -81,7 +111,8 @@ final class ReceiptRuntime DB::setDefaultConnection('receipt_native');Schema::clearResolvedInstance('db.schema');Auth::shouldUse('receipt_native');app()->instance('env','testing'); try{ $actor=\Tempo\Purchasing\Embedded\Models\User::where('tenant_id',$binding['tenant_id'])->findOrFail($binding['actor_id']);Auth::guard('receipt_native')->setUser($actor); - return $native->transaction(function()use($native,$actor,$binding,$work,$p){ + return $native->transaction(function()use($native,$actor,$binding,$work,$p,$right){ + if(str_starts_with($right,'stock.')){$native->statement("SET LOCAL lock_timeout='5s'");$native->statement("SET LOCAL statement_timeout='15s'");} $native->table('tenants')->where('id',$actor->tenant_id)->lockForUpdate()->firstOrFail(); $version=[$p['context_key'],$binding,$p['manifest_sha256']];$origin=['sgf_context_key'=>$p['context_key'],'workflow_manifest'=>$p['manifest_sha256']]; if(isset($p['internal_manifest_sha256'])){$version[]=$p['internal_manifest_sha256'];$origin['internal_manifest']=$p['internal_manifest_sha256'];} diff --git a/app/Modules/Purchasing/SgcInvoiceIngress.php b/app/Modules/Purchasing/SgcInvoiceIngress.php new file mode 100644 index 0000000..2bcbbff --- /dev/null +++ b/app/Modules/Purchasing/SgcInvoiceIngress.php @@ -0,0 +1,54 @@ +validated(); $id = (int)$body['order_id']; + $key = strtolower($request->header('X-Idempotency-Key')); + $controller = app(InvoiceController::class); $runtime = app(ReceiptRuntime::class); + $host = DB::connection(); + abort_unless($host->getDriverName() === 'pgsql', 503); + return $host->transaction(function () use ($request, $body, $id, $key, $controller, $runtime, $host) { + // Serialize the complete retry check + native commit, including different orders using one key. + $host->statement("SET LOCAL lock_timeout = '5s'"); + $host->select('SELECT pg_advisory_xact_lock(hashtextextended(?, 0))', ['sgc.invoice:'.$key]); + $controller->show($id, $runtime); // exact origin, current visibility and native policies + if ($body['phase'] === 'confirm') { + try { $cut = json_decode(Crypt::decryptString($body['preview']), true, 128, JSON_THROW_ON_ERROR); } + catch (\Throwable) { abort(409, 'Prévia inválida.'); } + abort_unless(($cut['action'] ?? null) === 'register' && ($cut['order_id'] ?? null) === $id + && ($cut['operation_key'] ?? null) === $key, 409, 'Prévia de outra operação.'); + $data = $cut['data']; $reason = $cut['reason']; + } else { $cut = null; $data = $body['data']; $reason = $body['reason']; } + $hash = hash('sha256', Payables::canonical([[$id, 'register', $data, $reason]])); + $generation = hash('sha256', Payables::canonical([$request->session()->get('company_context')])); + $old = $runtime->run('purchasing_invoices.prepare', function ($actor, $binding, $fingerprint) use ($key, $id, $hash, $cut, $generation) { + $actor = app(\Tempo\Purchasing\Embedded\Services\CurrentActor::class)->lock($actor, ['finance']); + abort_if(app(\Tempo\Purchasing\Embedded\Services\OrganizationAccess::class)->maintenanceOnly($actor), 403); + if ($cut) abort_unless(($cut['actor'] ?? null) === $actor->id && ($cut['binding'] ?? null) === $fingerprint + && ($cut['generation'] ?? null) === $generation, 409, 'Contexto da prévia alterado.'); + $receipt = \Tempo\Purchasing\Embedded\Models\AuditLog::where('user_id', $actor->id) + ->where('action', 'sgf.invoice.command')->where('after->operation_key', $key)->first(); + if (!$receipt) return null; + abort_unless(($receipt->after['result']['order_id'] ?? null) === $id && ($receipt->after['request_hash'] ?? null) === $hash + && ($receipt->after['binding_revision'] ?? null) === $fingerprint, 409, 'Chave usada com outro conteúdo ou origem.'); + return $receipt->after['result'] + ['replayed'=>true]; + }); + if ($old) return response()->json($old); + $input = $body['phase'] === 'confirm' ? ['preview'=>$body['preview']] : + ['operation_key'=>$key, 'revision'=>$body['revision'], 'action'=>'register', 'reason'=>$reason, 'data'=>$data]; + $native = Request::create('/api/purchasing-invoices/'.$id, 'POST', $input, [], [], ['HTTP_ACCEPT'=>'application/json']); + $native->setLaravelSession($request->session()); $native->setUserResolver($request->getUserResolver()); + return $body['phase'] === 'confirm' ? $controller->confirm($id, $native, $runtime) : $controller->preview($id, $native, $runtime); + }); + } +} diff --git a/app/Modules/Services/BillingRegister.php b/app/Modules/Services/BillingRegister.php new file mode 100644 index 0000000..9a79d2f --- /dev/null +++ b/app/Modules/Services/BillingRegister.php @@ -0,0 +1,41 @@ +context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&ModuleRecords::active()==='commercial',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']); + $rules=['kind'=>'sometimes|in:invoice,invoice_receipt,receipt,credit_note,quote,draft','search'=>'sometimes|nullable|string|max:100','from'=>'sometimes|date_format:Y-m-d','to'=>'sometimes|date_format:Y-m-d','currency'=>'sometimes|regex:/^[A-Z]{3}$/','series'=>'sometimes|string|max:100','author'=>'sometimes|integer|min:1','state'=>'sometimes|in:issued,cancelled,draft,confirmed_internal,superseded,approved,rejected,refused,customer_accepted,customer_declined,converted','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();abort_if(isset($d['from'],$d['to'])&&$d['from']>$d['to'],422,'Intervalo de emissão inválido.'); + $has=fn($right)=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],$right,$s['unit_id']);$rights=['invoice'=>$has('service_invoices.view'),'additional'=>$has('service_documents.view'),'quote'=>$has('commercial_quotes.view'),'draft'=>$has('billing_drafts.view'),'finance'=>$has('service_receivables.view')];abort_unless($rights['invoice']||$rights['additional']||$rights['quote']||$rights['draft'],403); + return DB::transaction(function()use($actor,$s,$d,$rights){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$families=[]; + $invoiceQuery=fn()=>ModuleRecords::query('si_invoices')->where($s); + if($rights['invoice']){$q=$invoiceQuery();if($rights['additional'])$q->whereNotExists(fn($v)=>$v->selectRaw('1')->from('sd_outputs as fr')->whereColumn('fr.invoice_id','si_invoices.id')->where(['fr.tenant_id'=>$s['tenant_id'],'fr.company_id'=>$s['company_id'],'fr.unit_id'=>$s['unit_id'],'fr.kind'=>'invoice_receipt'])->whereIn('fr.invoice_id',app(ServiceDocuments::class)->sourceInvoices($actor)->select('id'))); + $families[]=$q->selectRaw("'invoice'::text kind,id source_id,series_code||'/'||number::text reference,snapshot->'document'->'client'->>'name' client,snapshot->'document'->'client'->'data'->>'nuit' nuit,(issued_at AT TIME ZONE 'Africa/Maputo')::date document_on,snapshot->'calculation'->>'total_document_proposed' amount,NULL::text base,'MZN'::text currency,'issued'::text document_state,actor_id,id invoice_id,NULL::text source_key,1::integer version,issued_at sort_at");} + if($rights['additional']){ + $q=DB::table('sd_outputs as o')->join('si_invoices as i','i.id','=','o.invoice_id')->where(['o.tenant_id'=>$s['tenant_id'],'o.company_id'=>$s['company_id'],'o.unit_id'=>$s['unit_id']])->whereIn('i.id',app(ServiceDocuments::class)->sourceInvoices($actor)->select('id')); + $families[]=$q->selectRaw("o.kind,o.id source_id,'TEST-SD-'||upper(o.kind)||'/'||o.number::text reference,i.snapshot->'document'->'client'->>'name' client,i.snapshot->'document'->'client'->'data'->>'nuit' nuit,(o.created_at AT TIME ZONE 'Africa/Maputo')::date document_on,o.snapshot->>'amount' amount,NULL::text base,'MZN'::text currency,CASE WHEN EXISTS(SELECT 1 FROM sd_cancellations c WHERE c.output_id=o.id AND c.state='cancelled') THEN 'cancelled' ELSE 'issued' END document_state,o.actor_id,o.invoice_id,o.document_key::text source_key,3::integer version,o.created_at sort_at"); + } + if($rights['draft']){ + $draftState="CASE WHEN EXISTS(SELECT 1 FROM b_internal_confirmations b WHERE b.billing_draft_id=a_billing_drafts.id AND b.tenant_id=a_billing_drafts.tenant_id AND b.company_id=a_billing_drafts.company_id AND b.unit_id=a_billing_drafts.unit_id) THEN 'confirmed_internal' ELSE 'draft' END"; + if(DB::getSchemaBuilder()->hasTable('wc_applications'))$draftState="CASE WHEN EXISTS(SELECT 1 FROM wc_applications w WHERE w.old_billing_id=a_billing_drafts.id AND w.kind='replace') THEN 'superseded' WHEN EXISTS(SELECT 1 FROM wc_applications w WHERE w.old_billing_id=a_billing_drafts.id) THEN 'cancelled' ELSE ".$draftState.' END'; + $families[]=ModuleRecords::query('a_billing_drafts')->where($s)->selectRaw("'draft'::text kind,id source_id,'Interno #'||id::text reference,data->>'client_name' client,NULL::text nuit,NULL::date document_on,NULL::text amount,NULL::text base,data->>'currency' currency,".$draftState." document_state,(SELECT h.actor_id FROM a_history h WHERE h.area='billing_drafts' AND h.record_id=a_billing_drafts.id AND h.version=a_billing_drafts.version AND h.tenant_id=a_billing_drafts.tenant_id AND h.company_id=a_billing_drafts.company_id AND h.unit_id=a_billing_drafts.unit_id) actor_id,NULL::bigint invoice_id,NULL::text source_key,version,updated_at sort_at"); + } + if($rights['quote']){ + $q=DB::table('cq_versions as q')->where(['q.tenant_id'=>$s['tenant_id'],'q.company_id'=>$s['company_id'],'q.unit_id'=>$s['unit_id']])->whereIn('q.client_id',ModuleRecords::query('a_clients')->select('id'))->whereNotExists(fn($v)=>$v->selectRaw('1')->from('cq_versions as n')->whereColumn('n.quote_key','q.quote_key')->whereColumn('n.tenant_id','q.tenant_id')->whereColumn('n.company_id','q.company_id')->whereColumn('n.unit_id','q.unit_id')->whereColumn('n.version','>','q.version')); + $families[]=$q->selectRaw("'quote'::text kind,q.id source_id,'ORC-QA-'||upper(substr(q.quote_key::text,1,8)) reference,q.dependencies->'client'->>'name' client,NULL::text nuit,NULL::date document_on,NULL::text amount,q.data->>'base' base,q.data->>'currency' currency,q.state document_state,q.actor_id,NULL::bigint invoice_id,q.quote_key::text source_key,q.version,q.created_at sort_at"); + } + $union=array_shift($families);foreach($families as $q)$union->unionAll($q);$q=DB::query()->fromSub($union,'document_register');if(isset($d['kind']))$q->where('kind',$d['kind']);if(isset($d['state']))$q->where('document_state',$d['state']);if(isset($d['from']))$q->where('document_on','>=',$d['from']);if(isset($d['to']))$q->where('document_on','<=',$d['to']); + if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($v)=>$v->whereRaw("reference ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("client ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("nuit ILIKE ? ESCAPE '!'",[$needle]));} + if(isset($d['currency']))$q->where('currency',$d['currency']);if(isset($d['author']))$q->where('actor_id',$d['author']);if(isset($d['series']))$q->whereRaw("CASE WHEN kind IN('invoice','invoice_receipt','receipt','credit_note') THEN split_part(reference,'/',1) ELSE NULL END = ?",[$d['series']]); + $p=$q->orderByDesc('sort_at')->orderBy('kind')->orderByDesc('source_id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1));$ids=$p->pluck('invoice_id')->filter()->unique()->values()->all();$positions=collect();$draftIds=collect($p->items())->where('kind','draft')->pluck('source_id')->all();$bases=[]; + if($draftIds!==[])foreach(ModuleRecords::query('a_billing_drafts')->where($s)->whereIn('id',$draftIds)->get(['id','data']) as $draft)$bases[$draft->id]=DraftValues::base(json_decode($draft->data,true,32,JSON_THROW_ON_ERROR)); + if($rights['finance']&&$ids!==[]){$positions=DB::table('sr_records as register_ob')->leftJoin('sr_effects as e','e.obligation_id','=','register_ob.id')->where(['register_ob.tenant_id'=>$s['tenant_id'],'register_ob.company_id'=>$s['company_id'],'register_ob.unit_id'=>$s['unit_id'],'register_ob.kind'=>'obligation'])->whereIn('register_ob.invoice_id',$ids)->groupBy('register_ob.id','register_ob.invoice_id','register_ob.amount')->selectRaw("register_ob.invoice_id,CASE WHEN count(*) FILTER(WHERE e.kind='open')>0 THEN (register_ob.amount-coalesce(sum(CASE WHEN e.kind IN('allocate','reduction') THEN e.amount WHEN e.kind IN('reverse_allocation','reverse_reduction') THEN -e.amount ELSE 0 END),0))::text ELSE NULL END balance")->get()->keyBy('invoice_id');} + foreach($p->items() as $row){if($row->kind==='draft')$row->base=$bases[$row->source_id]??null;$row->series=in_array($row->kind,['invoice','invoice_receipt','receipt','credit_note'],true)?explode('/',$row->reference,2)[0]:null;$row->origin_invoice_balance=$rights['finance']?$positions->get($row->invoice_id)?->balance:null;$row->balance_available=$rights['finance']&&$row->origin_invoice_balance!==null;$row->pdf_url=match($row->kind){'invoice'=>'/api/service-invoices/'.$row->source_id.'/pdf','invoice_receipt','receipt','credit_note'=>'/api/service-documents/'.$row->source_id.'/pdf',default=>null};$row->print_url=$row->kind==='quote'?'/commercial-quotes/'.$row->source_key.'/print?version='.$row->version:null;} + return ['documents'=>$p,'financial_access'=>$rights['finance'],'notice'=>ServiceInvoicing::NOTICE,'has_mixed_document_types'=>true,'totals_not_summed'=>true]; + }); + } +} diff --git a/app/Modules/Services/CommercialQuotes.php b/app/Modules/Services/CommercialQuotes.php new file mode 100644 index 0000000..0deeba5 --- /dev/null +++ b/app/Modules/Services/CommercialQuotes.php @@ -0,0 +1,55 @@ +context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&ModuleRecords::active()==='commercial',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(['view',$action] as $right)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'commercial_quotes.'.$right,$s['unit_id']);return $s;} + private function canonical(mixed $v):mixed{if(is_object($v))$v=(array)$v;if(!is_array($v))return $v;if(!array_is_list($v))ksort($v);return array_map(fn($x)=>$this->canonical($x),$v);} + private function json(mixed $v):string{return json_encode($this->canonical($v),JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR);} + private function hash(mixed $v):string{return hash('sha256',$this->json($v));} + private function current(array $s){return DB::table('cq_versions as q')->where(['q.tenant_id'=>$s['tenant_id'],'q.company_id'=>$s['company_id'],'q.unit_id'=>$s['unit_id']])->whereNotExists(fn($x)=>$x->selectRaw('1')->from('cq_versions as n')->whereColumn('n.previous_id','q.id'));} + private function right(string $action):string{return match($action){'prepare','customer'=>'prepare','review'=>'review','convert'=>'convert',default=>abort(422)};} + private function data(array $input):array + { + foreach(['reason','representative','evidence'] as $k)if(isset($input[$k])&&is_string($input[$k]))$input[$k]=trim($input[$k]);$rules=['action'=>'required|in:prepare,review,customer,convert','quote_key'=>'required|uuid','version'=>'required|integer|min:0','reason'=>'required|string|min:10|max:2000','data'=>'present|array'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();$d['version']=(int)$d['version'];$d['quote_key']=strtolower($d['quote_key']);return $d; + } + private function basis(int $actor,array $s,array $d):array + { + $old=$this->current($s)->where('quote_key',$d['quote_key'])->first();abort_unless(($old?(int)$old->version:0)===$d['version'],409,'Orçamento alterado. Actualize a conferência.');abort_if($old&&$old->state==='converted',409,'Orçamento já convertido; o original permanece preservado.');$fields=$d['data']; + if($d['action']==='prepare'){ + $rules=['client_id'=>'required|integer|min:1','currency'=>'required|in:MZN','valid_until'=>'required|date_format:Y-m-d','source_order_id'=>'nullable|integer|min:1','description'=>'required|string|min:3|max:1000','lines'=>'required|array|min:1|max:100','lines.*.service_id'=>'required|integer|min:1','lines.*.quantity'=>'required|string','lines.*.unit_price'=>'required|string','lines.*.description'=>'nullable|string|max:500'];abort_if(array_diff(array_keys($fields),['client_id','currency','valid_until','source_order_id','description','lines']),422);$v=Validator::make($fields,$rules)->validate();$v['client_id']=(int)$v['client_id'];$v['source_order_id']=isset($v['source_order_id'])?(int)$v['source_order_id']:null;abort_if($v['valid_until']toDateString(),422,'Validade anterior à data actual.');$v['lines']=DraftValues::lines($v['lines'],$s);foreach($v['lines'] as $line)abort_if($line['quantity']===null||$line['unit_price']===null,422,'Quantidade e preço explícitos obrigatórios.');$v['base']=ServiceReceivables::money(DraftValues::base($v));$v['tax_status']='pending_separate_review';$state='draft'; + }else{abort_unless($old,403,'Orçamento fora do âmbito.');$v=json_decode($old->data,true);$state=$old->state;} + $client=ModuleRecords::query('a_clients')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'id'=>$v['client_id']])->first();abort_unless($client,403,'Cliente sem associação explícita ao Comercial.'); + $services=ModuleRecords::query('a_services')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->whereIn('id',array_column($v['lines'],'service_id'))->orderBy('id')->get(['id','version','name'])->all();abort_unless(count($services)===count(array_unique(array_column($v['lines'],'service_id'))),403,'Serviço fora do módulo autorizado.'); + $origin=null;if($v['source_order_id']!==null){$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'workshop_billing.view',$s['unit_id']);$o=DB::table('wo_orders')->where($s)->where('id',$v['source_order_id'])->first();abort_unless($o&&$o->client_id===$v['client_id'],403,'Ordem de origem incompatível.');abort_if(DB::table('wb_generations')->where('order_id',$o->id)->exists()||DB::table('cq_conversions')->where('source_order_id',$o->id)->exists(),409,'Ordem já convertida por outro percurso.');$origin=['order'=>['id'=>$o->id,'reference'=>$o->reference,'client_id'=>$o->client_id,'equipment_id'=>$o->equipment_id],'revision'=>(int)DB::selectOne('SELECT sgf_wo_revision(?) v',[$o->id])->v];} + $dependencies=['client'=>['id'=>$client->id,'version'=>$client->version,'name'=>$client->name],'services'=>$services,'origin'=>$origin];if($d['action']!=='prepare')abort_unless($this->hash(json_decode($old->dependencies,true))===$this->hash($dependencies),409,'Cliente, serviço ou ordem mudou desde a preparação. Prepare nova versão.'); + if($d['action']==='review'){abort_unless($state==='draft',409,'Revisão exige a versão preparada actual.');abort_if($old->prepared_by===$actor,403,'Revisão distinta da preparação.');$v['review']=Validator::make($fields,['decision'=>'required|in:approved,rejected'])->validate();abort_if(array_diff(array_keys($fields),['decision']),422);$v['review']+=['actor_id'=>$actor,'reason'=>$d['reason']];$state=$fields['decision']==='approved'?'approved':'rejected';} + if($d['action']==='customer'){abort_unless($state==='approved',409,'Conferência comercial aprovada necessária.');abort_if($v['valid_until']toDateString(),409,'Orçamento caducado.');$rules=['decision'=>'required|in:accepted,declined','representative'=>'required|string|min:3|max:150','capacity'=>'required|string|min:3|max:150','evidence'=>'required|string|min:10|max:2000','decided_on'=>'required|date_format:Y-m-d|before_or_equal:today'];abort_if(array_diff(array_keys($fields),array_keys($rules)),422);$v['customer']=Validator::make($fields,$rules)->validate();$v['customer']['recorded_by']=$actor;$state=$fields['decision']==='accepted'?'customer_accepted':'customer_declined';} + if($d['action']==='convert'){abort_unless($fields===[]&&$state==='customer_accepted',409,'Aceitação documentada necessária; conversão não é automática.');abort_if($v['valid_until']toDateString(),409,'Orçamento caducado.');abort_if(DB::table('cq_conversions')->where($s)->where('quote_key',$d['quote_key'])->exists(),409);} + $grants=DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->orderBy('permission')->orderBy('unit_id')->get(['permission','unit_id'])->all(); + return ['previous'=>$old?(array)$old:null,'data'=>$v,'state'=>$state,'prepared_by'=>$d['action']==='prepare'?$actor:$old->prepared_by,'dependencies'=>['client'=>['id'=>$client->id,'version'=>$client->version,'name'=>$client->name],'services'=>$services,'origin'=>$origin],'authority'=>$grants]; + } + private function locked(int $actor,string $action,\Closure $fn):mixed{$s=$this->scope($actor,$this->right($action));return DB::transaction(function()use($actor,$action,$s,$fn){DB::statement("SET LOCAL lock_timeout='5s'");DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();$this->scope($actor,$this->right($action));DB::table('permission_grants')->where(['user_id'=>$actor,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->lockForUpdate()->get();return $fn($s);});} + private function execute(int $actor,array $s,array $d,array $b):array + { + $state=$d['action']==='convert'?'converted':$b['state'];$drafts=null; + if($d['action']==='convert'){$writer=app(ServiceWorkflow::class);$data=$b['data'];$source=$writer->save('service_drafts',null,$actor,['name'=>'Orçamento '.substr($d['quote_key'],0,8),'client_id'=>$data['client_id'],'currency'=>$data['currency'],'description'=>$data['description'],'lines'=>$data['lines']],(string)\Illuminate\Support\Str::uuid());$billing=$writer->save('billing_drafts',null,$actor,['name'=>'Orçamento '.substr($d['quote_key'],0,8),'source_id'=>$source['id']],(string)\Illuminate\Support\Str::uuid());$drafts=['service_draft_id'=>$source['id'],'billing_draft_id'=>$billing['id']];} + $id=DB::table('cq_versions')->insertGetId($s+['quote_key'=>$d['quote_key'],'version'=>$d['version']+1,'previous_id'=>$b['previous']['id']??null,'action'=>$d['action'],'state'=>$state,'client_id'=>$b['data']['client_id'],'source_order_id'=>$b['data']['source_order_id'],'prepared_by'=>$b['prepared_by'],'actor_id'=>$actor,'data'=>$this->json($b['data']),'dependencies'=>$this->json($b['dependencies']),'reason'=>$d['reason'],'created_at'=>now()]); + if($drafts)DB::table('cq_conversions')->insert($s+['quote_key'=>$d['quote_key'],'quote_version_id'=>$id,'source_order_id'=>$b['data']['source_order_id'],'actor_id'=>$actor,'created_at'=>now()]+$drafts); + $impact=['action'=>$d['action'],'reference'=>'ORC-QA-'.strtoupper(substr($d['quote_key'],0,8)),'version'=>$d['version']+1,'state'=>$state,'client'=>$b['dependencies']['client']['name'],'base'=>$b['data']['base'],'currency'=>'MZN','valid_until'=>$b['data']['valid_until'],'tax_status'=>'pending_separate_review','lines'=>$b['data']['lines'],'source_reference'=>$b['dependencies']['origin']['order']['reference']??null,'creates_draft'=>$drafts!==null,'invoice_issued'=>false,'stock_effect'=>false,'financial_effect'=>false];return ['id'=>$id,'quote_key'=>$d['quote_key'],'version'=>$d['version']+1,'state'=>$state,'drafts'=>$drafts,'impact'=>$impact]; + } + public function preview(int $actor,array $input,string $key):array{$d=$this->data($input);Validator::make(['key'=>$key],['key'=>'required|uuid'])->validate();return $this->locked($actor,$d['action'],function($s)use($actor,$d,$key){abort_if(DB::table('cq_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$key])->exists(),409,'Consulte o recibo da chave já usada.');$b=$this->basis($actor,$s,$d);$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction();try{$out=$this->execute($actor,$s,$d,$b);}finally{while($db->transactionLevel()>$level)$db->rollBack();}$cut=['actor'=>$actor,'scope'=>$s,'data'=>$d,'key'=>$key,'revision'=>$this->hash($b),'impact'=>$out['impact'],'generation'=>$this->hash(request()->session()->get('company_context',[])),'observed'=>time()];return ['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];});} + public function confirm(int $actor,string $token):array{try{$c=json_decode(Crypt::decryptString($token),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$this->data($c['data']??[]);return $this->locked($actor,$d['action'],function($s)use($actor,$c,$d){abort_unless(($c['actor']??null)===$actor&&$this->hash($c['scope']??null)===$this->hash($s)&&($c['generation']??null)===$this->hash(request()->session()->get('company_context',[]))&&($c['observed']??0)>=time()-600&&$c['observed']<=time(),409,'Prévia/contexto expirados ou alterados.');$old=DB::table('cq_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d)),409);$result=json_decode($old->result,true);$source=DB::table('cq_versions')->where($s)->where('id',$result['id'])->first();abort_unless($source&&ModuleRecords::query('a_clients')->where(['id'=>$source->client_id,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->exists(),403,'Origem já não autorizada no módulo.');if($source->source_order_id!==null)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'workshop_billing.view',$s['unit_id']);return $result+['replayed'=>true];}$b=$this->basis($actor,$s,$d);abort_unless(hash_equals($this->hash($b),$c['revision']),409,'Cliente, serviços, origem, autoridade ou versão mudaram. Confira novamente.');$out=$this->execute($actor,$s,$d,$b);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409);DB::table('cq_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('commercial.quote.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'version'=>$out['version'],'reason'=>$d['reason']]);return $out+['replayed'=>false];});} + public function listing(int $actor,array $input):array{$s=$this->scope($actor);$rules=['state'=>'sometimes|in:draft,approved,rejected,customer_accepted,customer_declined,converted','search'=>'sometimes|nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();$q=$this->current($s)->join('a_clients as c','c.id','=','q.client_id')->whereIn('q.client_id',ModuleRecords::query('a_clients')->select('id'));if(isset($d['state']))$q->where('q.state',$d['state']);if(($d['search']??'')!=='')$q->whereRaw("c.name ILIKE ? ESCAPE '!'",['%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%']);$p=$q->orderByDesc('q.id')->paginate((int)($d['per_page']??25),['q.*','c.name as client_name'],'page',(int)($d['page']??1));foreach($p->items() as $row){$row->data=json_decode($row->data,true);$row->dependencies=json_decode($row->dependencies,true);$row->expired=$row->data['valid_until']toDateString();$row->conversion=DB::table('cq_conversions')->where($s)->where('quote_key',$row->quote_key)->first();}return ['quotes'=>$p,'notice'=>'Orçamento não é factura. Impostos exigem conferência separada.'];} + public function history(int $actor,string $key,int $page=1):array{$s=$this->scope($actor);Validator::make(['key'=>$key,'page'=>$page],['key'=>'required|uuid','page'=>'required|integer|min:1|max:100000'])->validate();$q=DB::table('cq_versions')->where($s)->where('quote_key',$key)->whereIn('client_id',ModuleRecords::query('a_clients')->select('id'));abort_unless((clone$q)->exists(),403);return ['events'=>$q->orderByDesc('version')->paginate(25,['*'],'page',$page)];} + public function options(int $actor,array $input):array + { + $s=$this->scope($actor);$rules=['view'=>'required|in:clients,services,orders','search'=>'sometimes|nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate(); + if($d['view']==='orders'){$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'workshop_billing.view',$s['unit_id']);$q=DB::table('wo_orders')->where($s)->whereIn('client_id',ModuleRecords::query('a_clients')->select('id'))->whereNotIn('id',DB::table('wb_generations')->select('order_id'))->whereNotIn('id',DB::table('cq_conversions')->whereNotNull('source_order_id')->select('source_order_id'));$fields=['id','reference as name','client_id'];$column='reference';} + else{$q=ModuleRecords::query('a_'.$d['view'])->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']]);$fields=['id','name','version'];$column='name';} + if(($d['search']??'')!=='')$q->whereRaw($column." ILIKE ? ESCAPE '!'",['%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%']);return ['records'=>$q->orderBy('id')->paginate((int)($d['per_page']??25),$fields,'page',(int)($d['page']??1))]; + } + public function document(int $actor,string $key,int $version):object{$s=$this->scope($actor);Validator::make(['key'=>$key,'version'=>$version],['key'=>'required|uuid','version'=>'required|integer|min:1'])->validate();$row=DB::table('cq_versions')->where($s)->where(['quote_key'=>$key,'version'=>$version])->whereIn('client_id',ModuleRecords::query('a_clients')->select('id'))->first();abort_unless($row,403);$row->data=json_decode($row->data,true);$row->dependencies=json_decode($row->dependencies,true);return $row;} +} diff --git a/app/Modules/Services/DocumentPeriod.php b/app/Modules/Services/DocumentPeriod.php new file mode 100644 index 0000000..0395e08 --- /dev/null +++ b/app/Modules/Services/DocumentPeriod.php @@ -0,0 +1,12 @@ +$c]);$period=SourceBook::table('gl_periods')->where($scope)->where('period_key','QA-2026-INTEGRADO')->orderByDesc('version')->first();abort_unless($period&&$period->status==='open',409,'A anulação exige período QA aberto; não há reabertura.');return (array)$period; + } +} diff --git a/app/Modules/Services/FiscalAssessment.php b/app/Modules/Services/FiscalAssessment.php new file mode 100644 index 0000000..386fd3f --- /dev/null +++ b/app/Modules/Services/FiscalAssessment.php @@ -0,0 +1,99 @@ +json($a));} + private function prefix(string $kind):string{return $kind==='assessment'?'fiscal_assessments.':'fiscal_forms.';} + public function scope(int $actor,string $kind='assessment',string $action='view'):array + { + abort_unless(config('fiscal_qa.enabled')===true,404);CompositionRuntime::assert(); + $s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&ModuleRecords::active()==='accounting',403); + $this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(array_unique(['view',$action])as$r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],$this->prefix($kind).$r,$s['unit_id']);abort_unless($this->binding()['scope']===$s,403);return $s; + } + public function binding():array + { + $p=CompositionRuntime::physical();$path='C:/laragon/data/sgf-tempo/run/modular-regression-'.getenv('SGF_ISOLATED_REGRESSION_RUN').'/private/receipt-composition-'.$p['composition'].'/fiscal-binding.json';$pin=config('fiscal_qa.binding_sha256');abort_unless(is_string($pin)&&is_file($path)&&!is_link($path)&&hash_equals($pin,hash_file('sha256',$path)),503,'Origem fiscal QA não conferida.');$b=json_decode(file_get_contents($path),true,32,JSON_THROW_ON_ERROR);abort_unless($b['composition']===$p['composition']&&$b['contract']===self::CONTRACT&&$b['scope']===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],403);return $b; + } + private function rows(array $s,array $b,string $kind){return DB::table('fx_versions')->where($s)->where(['process_key'=>$b['process_key'],'kind'=>$kind]);} + private function latest(array $s,array $b,string $kind):?array{$r=$this->rows($s,$b,$kind)->orderByDesc('version')->first();return $r?$this->row($r):null;} + private function row(object $r):array{$r=(array)$r;foreach(['data','dependencies']as$k)$r[$k]=json_decode($r[$k],true,64,JSON_THROW_ON_ERROR);return $r;} + public function source(array $s,array $b):array + { + $doc=DB::table('sd_outputs')->where($s)->where(['id'=>$b['source_output_id'],'invoice_id'=>$b['invoice_id'],'kind'=>'invoice_receipt'])->first(); + $invoice=DB::table('si_invoices')->where($s)->where('id',$b['invoice_id'])->first();abort_unless($doc&&$invoice,409,'Origem QA emitida não encontrada.'); + $draft=DB::table('a_billing_drafts')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'id'=>$invoice->draft_id]);ModuleRecords::constrain($draft,'a_billing_drafts','billing_drafts','commercial');abort_unless($draft->exists(),403,'Origem sem associação Comercial actual.'); + $latest=DB::table('sd_versions')->where($s)->where('document_key',$doc->document_key)->orderByDesc('version')->first();$pending=DB::table('sd_cancellations')->where($s)->where('output_id',$doc->id)->exists(); + $allocation=DB::table('sr_effects')->where(['id'=>$doc->effect_id,'receipt_id'=>$b['receipt_id'],'kind'=>'allocate'])->first();$receipt=DB::table('sr_records')->where($s)->where(['id'=>$b['receipt_id'],'invoice_id'=>$invoice->id,'kind'=>'receipt'])->first();$confirmation=$receipt?DB::table('sr_effects')->where(['record_id'=>$receipt->id,'kind'=>'receipt_confirm'])->first():null; + abort_unless($latest&&$latest->state==='issued'&&$latest->id===$doc->version_id&&!$pending&&$allocation&&$receipt&&$confirmation&&$receipt->actor_id!==$confirmation->actor_id&&!DB::table('sr_effects')->where('target_effect_id',$allocation->id)->exists()&&!DB::table('sr_effects')->where(['receipt_id'=>$receipt->id,'kind'=>'reverse_receipt'])->exists(),409,'Documento/recebimento alterado, revertido ou em correcção. Exige nova conferência.'); + foreach([$doc,$invoice]as$file){$bytes=base64_decode($file->pdf_base64,true);abort_unless(is_string($bytes)&&hash_equals($file->pdf_sha256,hash('sha256',$bytes)),409,'Documento congelado incompatível.');} + $snap=json_decode($invoice->snapshot,true,64,JSON_THROW_ON_ERROR);$cal=$snap['calculation']??[]; + abort_unless(($snap['simulation_only']??false)===true&&($cal['policy']??null)===ServiceCalculation::POLICY&&($cal['currency']??null)==='MZN'&&($snap['assessment_period']??null)===$b['period']&&ServiceReceivables::money($cal['base_exact']??'0')==='862.07'&&($cal['vat_document_proposed']??null)==='137.93'&&($cal['total_document_proposed']??null)==='1000.00'&&ServiceReceivables::money($allocation->amount)==='1000.00',409,'Montantes, política ou período não correspondem ao caso QA autorizado.'); + $summary=['output_id'=>$doc->id,'kind'=>'invoice_receipt','document_reference'=>'FR-QA-'.$doc->number,'technical_invoice_id'=>$invoice->id,'document_sha256'=>$doc->pdf_sha256,'currency'=>'MZN','base'=>'862.07','vat'=>'137.93','total'=>'1000.00','vat_exact'=>$cal['vat_exact'],'representation_difference'=>$cal['discarded_difference'],'policy'=>$cal['policy'],'profile_identifier'=>$snap['profile_identifier'],'profile_version'=>$snap['profile_version'],'exigibility_date'=>$snap['exigibility_date'],'period'=>$snap['assessment_period'],'synthetic_only'=>true]; + $state=['invoice_snapshot'=>$snap,'document_snapshot'=>json_decode($doc->snapshot,true),'latest'=>(array)$latest,'allocation'=>(array)$allocation,'receipt'=>(array)$receipt,'confirmation'=>(array)$confirmation];return ['summary'=>$summary,'hash'=>$this->hash($state)]; + } + private function basis(array $s,array $b,string $kind):array + { + $source=$this->source($s,$b);$period=DocumentPeriod::open($s);$assessment=$this->latest($s,$b,'assessment');$old=$kind==='assessment'?$assessment:$this->latest($s,$b,'form'); + return ['source'=>$source,'period'=>$period,'binding'=>$this->hash($b),'old'=>$old,'assessment'=>$assessment]; + } + private function dependencies(array $basis,string $kind):array{return ['source'=>$basis['source']['hash'],'period'=>$this->hash($basis['period']),'binding'=>$basis['binding'],'assessment_id'=>$kind==='form'?($basis['assessment']['id']??null):null];} + private function lock(int $actor,array $s):void + { + DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->lockForUpdate()->firstOrFail();DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->orderBy('id')->lockForUpdate()->get(); + } + private function exact(array $data,array $rules):array{abort_if(array_diff(array_keys($data),array_keys($rules)),422,'Campo fora do âmbito.');return Validator::make($data,$rules)->validate();} + public function preview(int $actor,array $input,string $key):array + { + abort_if(strlen(json_encode($input))>16384,422);$d=$this->exact($input,['kind'=>'required|in:assessment,form','action'=>'required|in:prepare,review','version'=>'required|integer|min:0','data'=>'present|array','reason'=>'required|string|min:10|max:4000']);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$s=$this->scope($actor,$d['kind'],$d['action']);$b=$this->binding(); + return DB::transaction(function()use($actor,$s,$d,$b,$key){$this->lock($actor,$s);$this->scope($actor,$d['kind'],$d['action']);$basis=$this->basis($s,$b,$d['kind']);$level=DB::transactionLevel();DB::beginTransaction();try{$out=$this->execute($actor,$s,$b,$d,$basis);}finally{while(DB::transactionLevel()>$level)DB::rollBack();}$cut=['input'=>$d,'key'=>$key,'actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'observed'=>time(),'basis'=>$this->hash($basis),'impact'=>$out['impact']];return ['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];}); + } + public function confirm(int $actor,string $preview):array + { + try{$c=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$c['input'];$s=$this->scope($actor,$d['kind'],$d['action']);$b=$this->binding(); + return DB::transaction(function()use($actor,$s,$d,$c,$b){$this->lock($actor,$s);$this->scope($actor,$d['kind'],$d['action']);abort_unless($c['actor']===$actor&&$c['scope']==$s&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Contexto ou prévia expirados.');$old=DB::table('fx_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409);return json_decode($old->result,true)+['replayed'=>true,'historical_receipt_only'=>true];}$basis=$this->basis($s,$b,$d['kind']);abort_unless($this->hash($basis)===$c['basis'],409,'Origem, versão ou dependências mudaram. Confira novamente.');$out=$this->execute($actor,$s,$b,$d,$basis);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409,'Impacto alterado.');DB::table('fx_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('fiscal_qa.'.$d['kind'].'.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'version'=>$out['version'],'before_revision'=>$d['version'],'reason'=>$d['reason'],'review_hash'=>$basis['source']['hash']]);return $out+['replayed'=>false];}); + } + private function validAssessment(array $basis):bool{$a=$basis['assessment'];return $a&&$a['state']==='reviewed'&&$this->hash($a['dependencies'])===$this->hash($this->dependencies($basis,'assessment'));} + private function execute(int $actor,array $s,array $b,array $d,array $basis):array + { + $kind=$d['kind'];$old=$basis['old'];abort_unless(($old['version']??0)===$d['version'],409,'Versão alterada.');$dependencies=$this->dependencies($basis,$kind);$prepared=$actor; + if($kind==='form')abort_unless($this->validAssessment($basis),409,'O livro exige revisão actual antes de preparar/rever o mapa.'); + if($d['action']==='prepare'){ + $data=$this->exact($d['data'],['source_output_id'=>'required|integer|min:1','period'=>'required|date_format:Y-m','classification'=>'required|in:qa_output_only','evidence'=>'required|string|min:10|max:2000']);abort_unless($data['source_output_id']===$b['source_output_id']&&$data['period']===$b['period'],403,'Escolha individual da origem e período QA autorizados.');$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_fiscal.view'); + $data+=['source'=>$basis['source']['summary'],'deductible_vat'=>null,'tax_payable'=>null,'complete_company_assessment'=>false,'pending'=>['Aquisições/deduções e outras origens não classificadas; não são zero.','Mapa interno QA — não é declaração oficial nem apuramento integral da empresa.']]; + if($kind==='form'){$data['assessment_id']=$basis['assessment']['id'];$data['artifact_csv']=$this->csv($data);$data['artifact_sha256']=hash('sha256',$data['artifact_csv']);}$state='prepared'; + }else{ + $review=$this->exact($d['data'],['decision'=>'required|in:reviewed,refused']);abort_unless($old&&$old['state']==='prepared'&&$old['prepared_by']!==$actor,403,'Preparação actual e revisor distinto obrigatórios.');$prepared=$old['prepared_by'];DB::table('users')->where('id',$prepared)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$prepared])->lockForUpdate()->first(),403);$grant=DB::table('permission_grants')->where($s+['user_id'=>$prepared,'permission'=>$this->prefix($kind).'prepare'])->lockForUpdate()->first();abort_unless($grant,403,'Autor da preparação já não autorizado.');abort_unless($this->hash($old['dependencies'])===$this->hash($dependencies),409,'A origem/revisão mudou desde a preparação.');$data=$old['data'];$state=$review['decision']; + } + $version=($old['version']??0)+1;$id=DB::table('fx_versions')->insertGetId($s+['process_key'=>$b['process_key'],'kind'=>$kind,'version'=>$version,'previous_id'=>$old['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_by'=>$prepared,'actor_id'=>$actor,'data'=>$this->json($data),'dependencies'=>$this->json($dependencies),'reason'=>$d['reason'],'created_at'=>now()]); + return ['id'=>$id,'kind'=>$kind,'version'=>$version,'state'=>$state,'impact'=>['before'=>['version'=>$old['version']??0,'state'=>$old['state']??null],'after'=>['version'=>$version,'state'=>$state],'source'=>$basis['source']['summary'],'deductible_vat'=>null,'tax_payable'=>null,'company_assessment_complete'=>false,'new_journals'=>0,'official_submission'=>false,'artifact_sha256'=>$data['artifact_sha256']??null]]; + } + private function csv(array $data):string + { + $s=$data['source'];$rows=[['MAPA INTERNO IVA QA — SEM VALIDADE FISCAL'],['Ambito','Apenas a origem seleccionada; apuramento da empresa incompleto'],['Periodo','Documento','Base MZN','IVA documental MZN','Total MZN','Regra','Versao perfil','SHA documento'],[$s['period'],$s['document_reference'],$s['base'],$s['vat'],$s['total'],$s['policy'],(string)$s['profile_version'],$s['document_sha256']],['IVA dedutivel','INDETERMINADO'],['Imposto a pagar','INDETERMINADO'],['Origem do livro revisto',(string)$data['assessment_id']]];$fp=fopen('php://temp','w+');foreach($rows as$row)fputcsv($fp,$row,';', '"','',"\r\n");rewind($fp);return stream_get_contents($fp); + } + public function listing(int $actor,array $input):array + { + $s=$this->scope($actor);$d=$this->exact($input,['page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','kind'=>'sometimes|in:assessment,form']);if(($d['kind']??null)==='form')$this->scope($actor,'form');$b=$this->binding(); + return DB::transaction(function()use($actor,$s,$d,$b){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$blocked=null;$basis=null;try{$basis=$this->basis($s,$b,'assessment');}catch(\Symfony\Component\HttpKernel\Exception\HttpException $e){if($e->getStatusCode()!==409)throw $e;$blocked=$e->getMessage();}$a=$this->latest($s,$b,'assessment');$canForm=$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_forms.view',$s['unit_id']);$form=$canForm?$this->latest($s,$b,'form'):null;$q=DB::table('fx_versions')->where($s)->where('process_key',$b['process_key']);if(!$canForm)$q->where('kind','assessment');if(isset($d['kind']))$q->where('kind',$d['kind']);$page=$q->orderByDesc('id')->paginate($d['per_page']??25,['*'],'page',$d['page']??1);$rows=$page->getCollection()->map(fn($r)=>$this->row($r))->all();$refs=DB::table('fx_commands')->where($s)->where('actor_id',$actor)->whereIn(DB::raw("(result->>'id')::bigint"),array_column($rows,'id'))->get();foreach($rows as&$row){$row['command_reference']=null;foreach($refs as$ref)if(json_decode($ref->result,true)['id']===$row['id'])$row['command_reference']=$ref->command_key;unset($row['data']['artifact_csv']);}$page->setCollection(collect($rows)); + $aCurrent=$basis&&$this->validAssessment($basis);$formCurrent=$basis&&$aCurrent&&$form&&$form['state']==='reviewed'&&$this->hash($form['dependencies'])===$this->hash($this->dependencies($basis,'form'));if($form)unset($form['data']['artifact_csv']);return ['synthetic_only'=>true,'contract'=>self::CONTRACT,'blocked'=>$blocked,'source'=>$basis['source']['summary']??null,'period'=>$b['period'],'assessment'=>$a,'form'=>$form,'assessment_current_review'=>$aCurrent,'form_current_review'=>$formCurrent,'events'=>$page->toArray(),'capabilities'=>['assessment_prepare'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_assessments.prepare',$s['unit_id']),'assessment_review'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_assessments.review',$s['unit_id']),'form_prepare'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_forms.prepare',$s['unit_id']),'form_review'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_forms.review',$s['unit_id'])],'official_submission'=>false,'complete_company_assessment'=>false];}); + } + public function receipt(int $actor,string $key):array + { + $s=$this->scope($actor);abort_unless(preg_match('/^[a-f0-9-]{36}$/D',$key),422);$r=DB::table('fx_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$key])->first();abort_unless($r,404);$result=json_decode($r->result,true);$this->scope($actor,$result['kind']);return ['result'=>$result,'command_key'=>$key,'created_at'=>$r->created_at,'historical_receipt_only'=>true,'operation_executed'=>false]; + } + public function export(int $actor,int $id):array + { + $s=$this->scope($actor,'form');$b=$this->binding();return DB::transaction(function()use($actor,$s,$b,$id){$this->lock($actor,$s);$basis=$this->basis($s,$b,'form');$f=$basis['old'];abort_unless($f&&$f['id']===$id&&$f['state']==='reviewed'&&$this->validAssessment($basis)&&$this->hash($f['dependencies'])===$this->hash($this->dependencies($basis,'form')),409,'Mapa ainda não revisto ou desactualizado.');$csv=$f['data']['artifact_csv'];abort_unless(hash_equals($f['data']['artifact_sha256'],hash('sha256',$csv)),409);Audit::record('fiscal_qa.form.downloaded',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'version'=>$f['version'],'review_hash'=>$f['data']['artifact_sha256']]);return ['bytes'=>$csv,'sha256'=>$f['data']['artifact_sha256'],'version'=>$f['version']];}); + } +} diff --git a/app/Modules/Services/FiscalCoverage.php b/app/Modules/Services/FiscalCoverage.php new file mode 100644 index 0000000..7f23d18 --- /dev/null +++ b/app/Modules/Services/FiscalCoverage.php @@ -0,0 +1,32 @@ +'SGF-FISCAL-COVERAGE-v2', + 'automatic_knowledge_import'=>false, + 'legal_validation'=>false, + 'notice'=>'Conhecimento em documentos ou conversas não passa automaticamente para a aplicação. Cada regra exige fonte, versão, âmbito, provas e revisão antes de utilização.', + 'taxes'=>[ + ['id'=>'iva','label'=>'IVA','state'=>'partial','available'=>'Cálculo delimitado de serviço, MZN, uma linha e imposto acrescido; perfis versionados, revisão distinta e decisão temporal. Documentos QA e diário integrado; livro/mapa interno da origem seleccionada revistos distintamente. Núcleo comum com versões/vigência e cálculo interno QA de1–25linhas, preço acrescido/incluído, exactos e diferenças preservados.','pending'=>'Emissão documental por múltiplas linhas/tratamentos, livros gerais de apuramento/dedução, regularizações e modelos oficiais.','legal_use'=>false], + ['id'=>'irps','label'=>'IRPS','state'=>'pending','available'=>null,'pending'=>'Motor operacional de rendimentos/retenções, remunerações, apuramento e modelos ligados às operações do SGF.','legal_use'=>false], + ['id'=>'irpc','label'=>'IRPC','state'=>'pending','available'=>null,'pending'=>'Ponte do resultado contabilístico para ajustamentos/base/colecta, retenções e modelos no SGF.','legal_use'=>false], + ['id'=>'ispc','label'=>'ISPC','state'=>'pending','available'=>null,'pending'=>'Enquadramento, cálculo, obrigações e modelos operacionais no SGF.','legal_use'=>false], + ['id'=>'ice','label'=>'ICE','state'=>'pending','available'=>null,'pending'=>'Classificação dos bens, parâmetros com vigência, liquidação e obrigações operacionais no SGF.','legal_use'=>false], + ['id'=>'inss','label'=>'INSS','state'=>'pending','available'=>null,'pending'=>'Cadastro/processamento de remunerações, contribuições e modelos no SGF.','legal_use'=>false], + ['id'=>'other','label'=>'Outros impostos e obrigações','state'=>'pending','available'=>null,'pending'=>'Identificação dos tributos aplicáveis, calendário legal, multas/juros e modelos; sem taxas ou prazos presumidos.','legal_use'=>false], + ], + 'management'=>[ + ['id'=>'finance','label'=>'Gestão financeira','state'=>'partial','available'=>'Percursos QA de orçamento de Compras, obrigações/recebíveis, afectações/reversões e Caixa com fecho; conferência individual de extracto artificial contra duas linhas existentes.','pending'=>'Operação geral real, bancos e reconciliação bancária geral.'], + ['id'=>'accounting','label'=>'Contabilidade','state'=>'partial','available'=>'Diário, plano, razão, balancete e conferência/fecho QA com origens distintas.','pending'=>'Demonstrações financeiras legais, encerramento anual real e integração dos apuramentos fiscais.'], + ['id'=>'analysis','label'=>'Análise financeira','state'=>'partial','available'=>'Custos directos/industriais, orçamento versus realizado, diário/saldos e dois critérios/indicadores QA revistos com fontes explícitas e relatórios.','pending'=>'Rácios de liquidez/rentabilidade/endividamento e tendências conciliadas, com plano de contas e critérios aprovados.'], + ], + 'rule_lifecycle'=>['Fonte identificada e versão preservada','Aplicabilidade e vigência explícitas','Cálculo exacto com pressupostos/pendências','Provas e revisão por pessoa distinta','Activação específica e histórico sem reescrever operações anteriores'], + 'official_submission_enabled'=>false, + ]; + } +} diff --git a/app/Modules/Services/FiscalDocumentMath.php b/app/Modules/Services/FiscalDocumentMath.php new file mode 100644 index 0000000..def946a --- /dev/null +++ b/app/Modules/Services/FiscalDocumentMath.php @@ -0,0 +1,18 @@ +isLessThanOrEqualTo(0)||BigRational::of($rate)->isGreaterThanOrEqualTo(1))throw new \InvalidArgumentException('Modalidade/taxa artificial inválida.');$baseSum=BigRational::of(0);$taxSum=BigRational::of(0);$totalSum=BigRational::of(0);$exactBaseSum=BigRational::of(0);$exactTaxSum=BigRational::of(0);$rows=[];$pending=[];$r=BigRational::of($rate); + foreach($lines as$i=>$line){$amount=BigRational::of($line['quantity'])->multipliedBy($line['unit_price']);$base=$mode==='net_added'?$amount:$amount->dividedBy(BigRational::of(1)->plus($r));$tax=$mode==='net_added'?$base->multipliedBy($r):$amount->minus($base);$taxDoc=$tax->toScale(2,RoundingMode::Down);$taxDocR=BigRational::of((string)$taxDoc);$baseDoc=null;$total=null; + try{if($mode==='net_added'){$baseDoc=$base->toScale(2,RoundingMode::Unnecessary);$total=$base->plus($taxDocR)->toScale(2,RoundingMode::Unnecessary);}else{$gross=$amount->toScale(2,RoundingMode::Unnecessary);$baseDoc=BigRational::of((string)$gross)->minus($taxDocR)->toScale(2);$total=$gross;}}catch(\Brick\Math\Exception\RoundingNecessaryException){$pending[]='Linha '.($i+1).': '.($mode==='net_added'?'base':'bruto').' não representável em duas casas; não truncado.';} + $rows[]=['line'=>($i+1),'reference'=>$line['reference'],'quantity'=>$line['quantity'],'unit_price'=>$line['unit_price'],'amount_exact_fraction'=>(string)$amount->simplified(),'base_exact_fraction'=>(string)$base->simplified(),'tax_exact_fraction'=>(string)$tax->simplified(),'base_document'=>$baseDoc===null?null:(string)$baseDoc,'tax_document'=>(string)$taxDoc,'total_document'=>$total===null?null:(string)$total,'base_difference_fraction'=>$baseDoc===null?null:(string)BigRational::of((string)$baseDoc)->minus($base)->simplified(),'tax_difference_fraction'=>(string)$tax->minus($taxDocR)->simplified()]; + $exactBaseSum=$exactBaseSum->plus($base);$exactTaxSum=$exactTaxSum->plus($tax);if($baseDoc!==null){$baseSum=$baseSum->plus(BigRational::of((string)$baseDoc));$taxSum=$taxSum->plus($taxDocR);$totalSum=$totalSum->plus(BigRational::of((string)$total));} + } + return ['state'=>$pending?'pending_representation':'determined_qa','policy'=>self::POLICY,'price_mode'=>$mode,'rate'=>$rate,'lines'=>$rows,'base_exact_fraction'=>(string)$exactBaseSum->simplified(),'tax_exact_fraction'=>(string)$exactTaxSum->simplified(),'base_document'=>$pending?null:(string)$baseSum->toScale(2),'tax_document'=>$pending?null:(string)$taxSum->toScale(2),'total'=>$pending?null:(string)$totalSum->toScale(2),'base_difference_fraction'=>$pending?null:(string)$baseSum->minus($exactBaseSum)->simplified(),'tax_difference_fraction'=>$pending?null:(string)$exactTaxSum->minus($taxSum)->simplified(),'aggregation'=>'sum_document_lines','pending'=>$pending,'synthetic_only'=>true,'legal_use'=>false,'financial_effects'=>0]; + } +} diff --git a/app/Modules/Services/FiscalObligations.php b/app/Modules/Services/FiscalObligations.php new file mode 100644 index 0000000..31037d1 --- /dev/null +++ b/app/Modules/Services/FiscalObligations.php @@ -0,0 +1,88 @@ +json($a));} + private function exact(array $a,array $rules):array{abort_if(array_diff(array_keys($a),array_keys($rules)),422);return Validator::make($a,$rules)->validate();} + public function scope(int $actor,string $action='view'):array + { + abort_unless(config('fiscal_obligations.enabled')===true,404);CompositionRuntime::assert();$p=CompositionRuntime::physical();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&ModuleRecords::active()==='accounting',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']); + foreach(array_unique(['view',$action])as$right)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'fiscal_obligations.'.$right,$s['unit_id']); + if($action!=='view')abort_unless($actor===($action==='prepare'?13:14),403);$meta=DB::table('fo_meta')->where('policy',self::POLICY)->first();abort_unless($meta&&$meta->composition===$p['composition']&&hash_equals($meta->source_hash,(string)config('fiscal_obligations.schema_sha256')),503,'Schema de obrigações não conferido.');return$s; + } + private function identity(int $actor,array $s):string + { + $u=DB::table('users')->where('id',$actor)->first(['id','email','name']);abort_unless($u&&DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->exists(),403);return$this->hash((array)$u); + } + private function responsible(int $actor,array $s):void + { + abort_unless(in_array($actor,[13,14,15,16,17],true),403,'Responsável fora dos perfis QA aprovados.');$this->identity($actor,$s);$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'fiscal_obligations.view',$s['unit_id']);$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'fiscal_forms.view',$s['unit_id']); + } + private function lock(int $actor,array $s):void + { + DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->lockForUpdate()->first();DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->orderBy('id')->lockForUpdate()->get(); + } + private function source(int $actor,array $s):array + { + $state=$this->forms->listing($actor,['kind'=>'form','per_page'=>25]);abort_unless($state['form_current_review']===true&&$state['form'],409,'Mapa interno revisto deixou de ser actual.');$form=$state['form'];$row=DB::table('fx_versions')->where($s+['id'=>$form['id'],'kind'=>'form','state'=>'reviewed'])->first();abort_unless($row,409);$data=json_decode($row->data,true,64,JSON_THROW_ON_ERROR);$bytes=$data['artifact_csv']??null;$sha=$data['artifact_sha256']??null;abort_unless(is_string($bytes)&&is_string($sha)&&hash_equals($sha,hash('sha256',$bytes)),409,'Exportação interna alterada.'); + return ['form_id'=>$row->id,'form_version'=>$row->version,'period'=>$state['period'],'export_sha256'=>$sha,'document_reference'=>$state['source']['document_reference'],'document_sha256'=>$state['source']['document_sha256'],'dependencies'=>json_decode($row->dependencies,true),'source_basis'=>$this->hash(['row'=>(array)$row,'source'=>$state['source'],'binding'=>$this->forms->binding()]),'synthetic_only'=>true,'tax_payable'=>null,'official_submission'=>false]; + } + private function rows(array $s){return DB::table('fo_versions')->where($s);} + private function row(?object $r):?array{if(!$r)return null;$a=(array)$r;foreach(['data','impact']as$key)$a[$key]=json_decode($a[$key],true,64,JSON_THROW_ON_ERROR);return$a;} + private function latest(array $s,string $key,bool $approved=false):?array{$q=$this->rows($s)->where('record_key',$key);if($approved)$q->where('state','approved');return$this->row($q->orderByDesc('version')->first());} + private function basis(int $actor,array $s,string $key):array{return ['source'=>$this->source($actor,$s),'last'=>$this->latest($s,$key),'effective'=>$this->latest($s,$key,true),'actor_identity'=>$this->identity($actor,$s)];} + private function input(array $v):array + { + if(isset($v['reason'])&&is_string($v['reason']))$v['reason']=trim($v['reason']);$d=$this->exact($v,['action'=>'required|in:prepare,review','record_key'=>'required|uuid','version'=>'required|integer|min:0','reason'=>'required|string|min:10|max:2000','data'=>'present|array']);$d['record_key']=strtolower($d['record_key']);$d['version']=(int)$d['version']; + if($d['action']==='prepare'){$d['data']=$this->exact($d['data'],['operation'=>'required|in:establish,amend,cancel,delivery','form_id'=>'required|integer|min:1','period'=>'required|date_format:Y-m','responsible_id'=>'required|integer|min:1','due_on'=>'present|nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2100-12-31','evidence'=>'required|string|min:10|max:2000','export_sha256'=>'present|nullable|regex:/^[a-f0-9]{64}$/D']);foreach(['form_id','responsible_id']as$field)$d['data'][$field]=(int)$d['data'][$field];}else$d['data']=$this->exact($d['data'],['decision'=>'required|in:approved,refused']);return$d; + } + private function duplicate(array $s,string $key,int $form):bool + { + // Effective approved record, or a first unresolved preparation; no universe in PHP. + $q=$this->rows($s)->where('record_key','<>',$key)->whereRaw("(data->>'form_id')::bigint=?",[$form]); + return$q->where(function($q){$q->where(function($q){$q->where('state','approved')->whereRaw("data->>'administrative_status'<>'cancelled'")->whereNotExists(function($n){$n->selectRaw('1')->from('fo_versions as newer')->whereColumn('newer.record_key','fo_versions.record_key')->whereColumn('newer.tenant_id','fo_versions.tenant_id')->whereColumn('newer.company_id','fo_versions.company_id')->whereColumn('newer.unit_id','fo_versions.unit_id')->where('newer.state','approved')->whereColumn('newer.version','>','fo_versions.version');});})->orWhere(function($q){$q->where('state','prepared')->whereNotExists(function($n){$n->selectRaw('1')->from('fo_versions as anyapproved')->whereColumn('anyapproved.record_key','fo_versions.record_key')->whereColumn('anyapproved.tenant_id','fo_versions.tenant_id')->whereColumn('anyapproved.company_id','fo_versions.company_id')->whereColumn('anyapproved.unit_id','fo_versions.unit_id')->where('anyapproved.state','approved');})->whereNotExists(function($n){$n->selectRaw('1')->from('fo_versions as next')->whereColumn('next.record_key','fo_versions.record_key')->whereColumn('next.tenant_id','fo_versions.tenant_id')->whereColumn('next.company_id','fo_versions.company_id')->whereColumn('next.unit_id','fo_versions.unit_id')->whereColumn('next.version','>','fo_versions.version');});});})->exists(); + } + private function execute(int $actor,array $s,array $d,array $b):array + { + $last=$b['last'];$effective=$b['effective'];abort_unless(($last['version']??0)===$d['version'],409,'Versão alterada; confira novamente.');$source=$b['source']; + if($d['action']==='prepare'){$data=$d['data'];$prepared=$actor;$identity=$this->identity($actor,$s);} + else{abort_unless($last&&$last['state']==='prepared'&&$last['prepared_by']!==$actor,403,'Preparação alheia actual obrigatória.');$prepared=$last['prepared_by'];$identity=$last['preparer_identity'];abort_unless(hash_equals($identity,$this->identity($prepared,$s)),409,'Identidade do preparador alterada.');$this->permissions->require($prepared,$s['tenant_id'],$s['company_id'],'fiscal_obligations.prepare',$s['unit_id']);abort_unless(hash_equals($last['source_basis'],$source['source_basis']),409,'Origem mudou desde a preparação.');$data=$last['data'];} + abort_unless($data['form_id']===$source['form_id']&&$data['period']===$source['period'],409,'Escolha o mapa/período revistos actuais.');$this->responsible($data['responsible_id'],$s);$op=$data['operation'];$old=$effective['data']??null; + if($op==='establish'){abort_unless(!$effective,409,'Já existe condição aprovada neste registo.');abort_if($this->duplicate($s,$d['record_key'],$data['form_id']),409,'Já existe obrigação ou preparação deste mapa. Não duplicar.');} + else{abort_unless($old&&$old['administrative_status']!=='cancelled'&&$old['form_id']===$data['form_id']&&$old['period']===$data['period'],409,'Operação exige obrigação activa da mesma origem.');if(in_array($op,['cancel','delivery'],true))foreach(['due_on','responsible_id']as$field)abort_unless($data[$field]===$old[$field],409,'Esta operação conserva as condições aprovadas.');if($op==='delivery')abort_unless($old['administrative_status']!=='delivery_simulated',409,'Entrega simulada já registada.');} + if($op==='delivery')abort_unless(is_string($data['export_sha256'])&&hash_equals($source['export_sha256'],$data['export_sha256']),409,'SHA da exportação interna não corresponde.');else abort_unless($data['export_sha256']===null,422,'SHA de entrega só na declaração simulada.'); + $data['administrative_status']=$op==='cancel'?'cancelled':($op==='delivery'?'delivery_simulated':($old['administrative_status']??'active'));$data['delivery_evidence']=$op==='delivery'?['export_sha256'=>$data['export_sha256'],'reference'=>$data['evidence'],'simulated_only'=>true]:($old['delivery_evidence']??null);$data['source']=$source; + $state=$d['action']==='prepare'?'prepared':$d['data']['decision'];$version=($last['version']??0)+1;$impact=['before'=>['version'=>$last['version']??0,'state'=>$last['state']??null,'effective'=>$old],'after'=>['version'=>$version,'state'=>$state,'proposal'=>$data,'effective'=>$state==='approved'?$data:$old],'tax_payable'=>null,'new_debt'=>false,'payments'=>0,'journals'=>0,'official_submission'=>false]; + $id=DB::table('fo_versions')->insertGetId($s+['record_key'=>$d['record_key'],'version'=>$version,'previous_id'=>$last['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_by'=>$prepared,'actor_id'=>$actor,'preparer_identity'=>$identity,'data'=>$this->json($data),'source_basis'=>$source['source_basis'],'impact'=>$this->json($impact),'reason'=>$d['reason'],'created_at'=>now()->utc()]);return['id'=>$id,'record_key'=>$d['record_key'],'version'=>$version,'state'=>$state,'impact'=>$impact]; + } + public function preview(int $actor,array $input,string $key):array + { + $d=$this->input($input);$this->exact(['key'=>$key],['key'=>'required|uuid']);$s=$this->scope($actor,$d['action']);return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($actor,$s);$this->scope($actor,$d['action']);$b=$this->basis($actor,$s,$d['record_key']);$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction();try{$out=$this->execute($actor,$s,$d,$b);}finally{while($db->transactionLevel()>$level)$db->rollBack();}$cut=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>$key,'basis'=>$this->hash($b),'impact'=>$out['impact'],'observed'=>time()];return['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];}); + } + public function confirm(int $actor,string $preview):array + { + try{$c=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}abort_unless(is_array($c)&&is_array($c['input']??null),409);$d=$this->input($c['input']);$s=$this->scope($actor,$d['action']);return DB::transaction(function()use($actor,$s,$d,$c){$this->lock($actor,$s);$this->scope($actor,$d['action']);abort_unless($c['actor']===$actor&&$this->hash($c['scope'])===$this->hash($s)&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Contexto/prévia alterados.');$b=$this->basis($actor,$s,$d['record_key']);$q=$s+['actor_id'=>$actor,'command_key'=>$c['key']];$old=DB::table('fo_commands')->where($q)->first();if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d)),409,'Chave usada com outros dados.');return json_decode($old->result,true)+['replayed'=>true];}abort_unless(hash_equals($c['basis'],$this->hash($b)),409,'Origem, pessoa ou versão mudaram. Confira novamente.');$out=$this->execute($actor,$s,$d,$b);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409,'Impacto alterado.');DB::table('fo_commands')->insert($q+['request_hash'=>$this->hash($d),'result'=>$this->json($out),'input'=>$this->json($d),'created_at'=>now()->utc()]);Audit::record('fiscal_obligations.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'record_key'=>$d['record_key'],'version'=>$out['version'],'reason'=>$d['reason'],'impact'=>$out['impact']]);return$out+['replayed'=>false];}); + } + public function receipt(int $actor,string $key):array + { + $this->exact(['key'=>$key],['key'=>'required|uuid']);$s=$this->scope($actor);$this->forms->scope($actor,'form');$old=DB::table('fo_commands')->where($s+['actor_id'=>$actor,'command_key'=>$key])->first();abort_unless($old,404);return['result'=>json_decode($old->result,true),'created_at'=>$old->created_at,'reason'=>json_decode($old->input,true)['reason'],'historical_receipt_only'=>true,'operation_executed'=>false]; + } + public function listing(int $actor,array $input):array + { + $s=$this->scope($actor);$this->forms->scope($actor,'form');$d=$this->exact($input,['reference_on'=>'required|date_format:Y-m-d','period'=>'sometimes|date_format:Y-m','status'=>'sometimes|in:active,delivery_simulated,cancelled,unapproved','record_key'=>'sometimes|uuid','due_from'=>'required_with:due_to|date_format:Y-m-d','due_to'=>'required_with:due_from|date_format:Y-m-d|after_or_equal:due_from','due_state'=>'sometimes|in:unknown,overdue,not_overdue','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']); + return DB::transaction(function()use($actor,$s,$d){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$source=null;$blocked=null;try{$source=$this->source($actor,$s);}catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){if($e->getStatusCode()!==409)throw$e;$blocked=$e->getMessage();}$q=$this->rows($s);if(isset($d['record_key'])){$q->where('record_key',$d['record_key']);abort_unless((clone$q)->exists(),403,'Histórico fora do âmbito.');}else$q->whereNotExists(function($n){$n->selectRaw('1')->from('fo_versions as next');foreach(['tenant_id','company_id','unit_id','record_key']as$key)$n->whereColumn('next.'.$key,'fo_versions.'.$key);$n->whereColumn('next.version','>','fo_versions.version');}); + $effective="(SELECT e.data->>'administrative_status' FROM fo_versions e WHERE e.tenant_id=fo_versions.tenant_id AND e.company_id=fo_versions.company_id AND e.unit_id=fo_versions.unit_id AND e.record_key=fo_versions.record_key AND e.state='approved' ORDER BY e.version DESC LIMIT 1)";$effectiveDue="(SELECT e.data->>'due_on' FROM fo_versions e WHERE e.tenant_id=fo_versions.tenant_id AND e.company_id=fo_versions.company_id AND e.unit_id=fo_versions.unit_id AND e.record_key=fo_versions.record_key AND e.state='approved' ORDER BY e.version DESC LIMIT 1)";if(isset($d['due_from']))$q->whereRaw("($effectiveDue)>=? AND ($effectiveDue)<=?",[$d['due_from'],$d['due_to']]);if(isset($d['due_state'])){if($d['due_state']==='unknown')$q->whereRaw("($effectiveDue) IS NULL");else$q->whereRaw("($effectiveDue)".($d['due_state']==='overdue'?'<':'>=').'?',[$d['reference_on']]);}if(isset($d['status']))$q->whereRaw("coalesce($effective,'unapproved')=?",[$d['status']]);if(isset($d['period']))$q->whereRaw("data->>'period'=?",[$d['period']]);$page=$q->orderByDesc('id')->paginate($d['per_page']??25,['*'],'page',$d['page']??1);$keys=$page->pluck('record_key')->unique()->all();$approved=$this->rows($s)->whereIn('record_key',$keys)->where('state','approved')->whereNotExists(function($n){$n->selectRaw('1')->from('fo_versions as later');foreach(['tenant_id','company_id','unit_id','record_key']as$key)$n->whereColumn('later.'.$key,'fo_versions.'.$key);$n->where('later.state','approved')->whereColumn('later.version','>','fo_versions.version');})->get()->keyBy('record_key'); + $items=[];foreach($page->items()as$r){$row=$this->row($r);$a=$approved->get($r->record_key);$terms=$a?json_decode($a->data,true):null;$due=$terms['due_on']??null;$days=$due===null?null:max(0,(int)(new \DateTimeImmutable($due))->diff(new \DateTimeImmutable($d['reference_on']))->format('%r%a'));$row['effective']=$terms;$row['days_overdue']=$days;$row['due_status']=$due===null?'unknown':($due<$d['reference_on']?'overdue':'not_overdue');$row['current_source_matches']=$source!==null&&hash_equals($r->source_basis,$source['source_basis']);$items[]=$row;}$page->setCollection(collect($items));$can=fn($right,$expected)=>$actor===$expected&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_obligations.'.$right,$s['unit_id']);return['policy'=>self::POLICY,'synthetic_only'=>true,'reference_on'=>$d['reference_on'],'source'=>$source,'blocked'=>$blocked,'records'=>$page->toArray(),'capabilities'=>['prepare'=>$can('prepare',13),'review'=>$can('review',14)],'official_submission'=>false,'tax_payable'=>null,'notice'=>'Prazo administrativo artificial; entrega apenas simulada, sem transmissão à AT ou pagamento.'];}); + } +} diff --git a/app/Modules/Services/FiscalRuleExpression.php b/app/Modules/Services/FiscalRuleExpression.php new file mode 100644 index 0000000..d3ae7c6 --- /dev/null +++ b/app/Modules/Services/FiscalRuleExpression.php @@ -0,0 +1,205 @@ + RoundingMode::Unnecessary, + 'UP' => RoundingMode::Up, + 'DOWN' => RoundingMode::Down, + 'CEILING' => RoundingMode::Ceiling, + 'FLOOR' => RoundingMode::Floor, + 'HALF_UP' => RoundingMode::HalfUp, + 'HALF_DOWN' => RoundingMode::HalfDown, + 'HALF_CEILING' => RoundingMode::HalfCeiling, + 'HALF_FLOOR' => RoundingMode::HalfFloor, + 'HALF_EVEN' => RoundingMode::HalfEven, + ]; + + /** @param array $definition */ + public function validate(array $definition): void + { + $this->topologicalOrder($definition); + } + + /** + * Evaluate a validated expression. Missing values are reported as indeterminate; + * callers must supply versioned parameters and provenance explicitly. + * + * @param array $definition + * @param array $inputs + * @param array $parameters + * @return array{status:string,result:?string,missing:list,steps:list>} + */ + public function evaluate(array $definition, array $inputs, array $parameters): array + { + $order = $this->topologicalOrder($definition); + $nodes = $definition['nodes']; + $values = []; + $missing = []; + $steps = []; + + foreach ($order as $id) { + $node = $nodes[$id]; + $op = $node['op']; + if ($op === 'input' || $op === 'parameter') { + $key = $node['key']; + $source = $op === 'input' ? $inputs : $parameters; + if (! array_key_exists($key, $source)) { + $missing[] = $op.':'.$key; + $steps[] = ['id' => $id, 'op' => $op, 'key' => $key, 'value' => null]; + + continue; + } + $value = $this->number($source[$key]); + } elseif ($op === 'literal') { + $value = $this->number($node['value']); + } else { + $args = array_values(array_map(static fn (string $ref): ?BigRational => $values[$ref] ?? null, $node['args'] ?? [])); + if (in_array(null, $args, true)) { + $values[$id] = null; + $steps[] = ['id' => $id, 'op' => $op, 'args' => $node['args'] ?? [], 'value' => null]; + + continue; + } + $value = $this->operate($op, $args, $node); + } + $values[$id] = $value; + $steps[] = ['id' => $id, 'op' => $op, 'value' => (string) $value]; + } + + $result = $values[$definition['result']] ?? null; + + return [ + 'status' => $missing === [] && $result !== null ? 'determinate' : 'indeterminate', + 'result' => $result === null ? null : (string) $result, + 'missing' => array_values(array_unique($missing)), + 'steps' => $steps, + ]; + } + + /** + * @param array $definition + * @return list + */ + private function topologicalOrder(array $definition): array + { + if (($definition['language'] ?? null) !== self::LANGUAGE || ! is_array($definition['nodes'] ?? null) || ! is_string($definition['result'] ?? null)) { + throw new InvalidArgumentException('O formato da expressão não é suportado.'); + } + $nodes = $definition['nodes']; + if ($nodes === [] || count($nodes) > 100) { + throw new InvalidArgumentException('A expressão deve conter entre 1 e 100 elementos.'); + } + + foreach ($nodes as $id => $node) { + if (! is_string($id) || ! preg_match('/^[a-z][a-z0-9_]{0,47}$/', $id) || ! is_array($node) || ! is_string($node['op'] ?? null)) { + throw new InvalidArgumentException('Reveja o elemento indicado na expressão.'); + } + $op = $node['op']; + $allowedFields = match ($op) { + 'input', 'parameter' => ['op', 'key'], + 'literal' => ['op', 'value'], + 'add', 'subtract', 'multiply', 'divide', 'min', 'max' => ['op', 'args'], + 'round' => ['op', 'args', 'scale', 'mode'], + default => throw new InvalidArgumentException('Esta operação não é suportada na expressão.'), + }; + if (array_diff(array_keys($node), $allowedFields) !== [] || array_diff($allowedFields, array_keys($node)) !== []) { + throw new InvalidArgumentException('Há campos em falta ou não suportados num elemento da expressão.'); + } + if (in_array($op, ['input', 'parameter'], true) && (! is_string($node['key']) || ! preg_match('/^[a-z][a-z0-9_.-]{0,79}$/', $node['key']))) { + throw new InvalidArgumentException('Reveja o identificador da entrada ou do parâmetro.'); + } + if ($op === 'literal') { + $this->number($node['value']); + } + if (isset($node['args'])) { + if (! is_array($node['args'])) { + throw new InvalidArgumentException('Reveja os argumentos da expressão.'); + } + $count = count($node['args']); + $validCount = $op === 'round' ? $count === 1 : ($op === 'subtract' || $op === 'divide' ? $count === 2 : $count >= 2 && $count <= 20); + if (! $validCount || count(array_filter($node['args'], 'is_string')) !== $count) { + throw new InvalidArgumentException('Reveja os argumentos da expressão.'); + } + } + if ($op === 'round' && (! is_int($node['scale']) || $node['scale'] < 0 || $node['scale'] > 12 || ! isset(self::ROUNDING[$node['mode']]))) { + throw new InvalidArgumentException('Reveja a configuração de arredondamento.'); + } + } + + if (! array_key_exists($definition['result'], $nodes)) { + throw new InvalidArgumentException('O resultado da expressão refere um elemento inexistente.'); + } + $state = []; + $order = []; + $visit = function (string $id) use (&$visit, &$state, &$order, $nodes): void { + if (($state[$id] ?? 0) === 1) { + throw new InvalidArgumentException('A expressão contém uma dependência circular.'); + } + if (($state[$id] ?? 0) === 2) { + return; + } + $state[$id] = 1; + foreach ($nodes[$id]['args'] ?? [] as $dependency) { + if (! is_string($dependency) || ! array_key_exists($dependency, $nodes)) { + throw new InvalidArgumentException('A expressão refere um elemento inexistente.'); + } + $visit($dependency); + } + $state[$id] = 2; + $order[] = $id; + }; + foreach (array_keys($nodes) as $id) { + $visit($id); + } + + return $order; + } + + /** + * @param list $args + * @param array $node + */ + private function operate(string $op, array $args, array $node): BigRational + { + if ($op === 'round') { + return BigRational::of((string) $args[0]->toScale($node['scale'], self::ROUNDING[$node['mode']])); + } + if ($op === 'add') { + return array_reduce(array_slice($args, 1), static fn (BigRational $carry, BigRational $value): BigRational => $carry->plus($value), $args[0]); + } + if ($op === 'subtract') { + return $args[0]->minus($args[1]); + } + if ($op === 'multiply') { + return array_reduce(array_slice($args, 1), static fn (BigRational $carry, BigRational $value): BigRational => $carry->multipliedBy($value), $args[0]); + } + if ($op === 'divide') { + return $args[0]->dividedBy($args[1]); + } + if ($op === 'min' || $op === 'max') { + return array_reduce(array_slice($args, 1), static fn (BigRational $carry, BigRational $value): BigRational => ($op === 'min' ? $carry->compareTo($value) <= 0 : $carry->compareTo($value) >= 0) ? $carry : $value, $args[0]); + } + throw new InvalidArgumentException('Esta operação não é suportada na expressão.'); + } + + private function number(mixed $value): BigRational + { + if ((! is_string($value) && ! is_int($value)) || ! preg_match('/^-?\d{1,24}(?:\.\d{1,8})?$/D', (string) $value)) { + throw new InvalidArgumentException('Introduza valores numéricos decimais ou inteiros.'); + } + + return BigRational::of((string) $value); + } +} diff --git a/app/Modules/Services/FiscalRules.php b/app/Modules/Services/FiscalRules.php new file mode 100644 index 0000000..be5c84a --- /dev/null +++ b/app/Modules/Services/FiscalRules.php @@ -0,0 +1,114 @@ +json($a));} + public function scope(int $actor,string $action='view'):array + { + abort_unless(config('fiscal_rules_qa.enabled')===true,404);CompositionRuntime::assert();$s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2]&&ModuleRecords::active()==='accounting',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(array_unique(['view',$action])as$r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'fiscal_rules.'.$r,$s['unit_id']);return $s; + } + public function fixture():array + { + $file=base_path('resources/data/fiscal-rule-qa.json');$pin=config('fiscal_rules_qa.source_sha256');abort_unless(is_string($pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503,'Fonte artificial do contrato não conferida.');$f=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);abort_unless($f['contract']===self::CONTRACT&&$f['not_tax_law']===true,503);$f['sha256']=$pin;return $f; + } + private function events(array $s){return DB::table('fr_events')->where($s);} + private function row(object $r):array{$a=(array)$r;foreach(['definition','input','result','dependencies']as$k)if(isset($a[$k]))$a[$k]=json_decode($a[$k],true,64,JSON_THROW_ON_ERROR);return $a;} + private function currentVersions(array $s) + { + return DB::table('fr_events as f')->where(array_combine(array_map(fn($k)=>'f.'.$k,array_keys($s)),array_values($s)))->whereNotExists(function($q){$q->selectRaw('1')->from('fr_events as newer');foreach(['tenant_id','company_id','unit_id','rule_key','rule_version']as$k)$q->whereColumn('newer.'.$k,'f.'.$k);$q->whereColumn('newer.revision','>','f.revision');}); + } + private function exact(array $data,array $rules):array{abort_if(array_diff(array_keys($data),array_keys($rules)),422,'Campo não suportado neste contrato.');return Validator::make($data,$rules)->validate();} + public function definition(array $d):array + { + $d=$this->exact($d,['label'=>'required|string|min:3|max:160','family'=>'required|in:qa,iva,irps,irpc,ispc,ice,inss,other','operation'=>'required|in:percentage_add,metadata_only','conditions'=>'required|string|min:10|max:2000','currency'=>'nullable|in:MZN','unit'=>'required|in:money,none','precision'=>'required|integer|in:2','rounding'=>'required|in:DOWN','priority'=>'required|integer|min:0|max:100','source_reference'=>'required|string|min:3|max:200','source_sha256'=>'nullable|string|regex:/^[a-f0-9]{64}$/D','source_status'=>'required|in:pending,qa_checked','valid_from'=>'nullable|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31','valid_until'=>'nullable|date_format:Y-m-d|after_or_equal:valid_from|before_or_equal:9998-12-31','parameters'=>'present|array','expression'=>'present|array']); + $this->exact($d['parameters'],['rate'=>'nullable|string|regex:/^0\.[0-9]{1,8}$/D']); + if($d['expression']){try{(new FiscalRuleExpression)->validate($d['expression']);}catch(\InvalidArgumentException){abort(422,'Expressão declarativa inválida; não é executado código.');}abort_if(array_diff(array_keys($d['expression']),['language','nodes','result']),422);} + if($d['source_status']==='qa_checked')abort_unless($this->verified($d),422,'Somente os casos artificiais exactos deste contrato podem ser conferidos para execução QA. Fontes legais não são activadas.');return $d; + } + private function verified(array $d):bool + { + $f=$this->fixture();if(($d['source_sha256']??null)!==$f['sha256']||($d['family']??null)!=='qa'||($d['source_status']??null)!=='qa_checked')return false; + foreach($f['cases']as$case){$expected=$case['definition'];$actual=$d;unset($actual['source_sha256'],$actual['label']);unset($expected['label']);if($this->hash($actual)===$this->hash($expected))return true;}return false; + } + private function basis(array $s,array $d):array + { + $q=$this->events($s);$last=isset($d['rule_key'])?(clone$q)->where('rule_key',$d['rule_key'])->orderByDesc('revision')->first():null;$target=isset($d['target_id'])?(clone$q)->where('id',$d['target_id'])->first():null;$selected=isset($d['data']['rule_event_id'])?(clone$q)->where('id',$d['data']['rule_event_id'])->first():null; + return ['source_sha256'=>$this->fixture()['sha256'],'count'=>(int)(clone$q)->count(),'last_id'=>(int)(clone$q)->max('id'),'last'=>$last?$this->row($last):null,'target'=>$target?$this->row($target):null,'selected'=>$selected?$this->row($selected):null]; + } + private function right(string $action):string{return in_array($action,['review','withdraw'],true)?'review':'prepare';} + private function lock(int $actor,array $s):void + { + DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where(['tenant_id'=>$s['tenant_id'],'id'=>$s['company_id']])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->lockForUpdate()->first(),403);DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->orderBy('id')->lockForUpdate()->get(); + } + public function preview(int $actor,array $input,string $key):array + { + abort_if(strlen(json_encode($input))>32768,422);$d=$this->exact($input,['action'=>'required|in:prepare,review,withdraw,calculate,calculate_document','rule_key'=>'nullable|required_unless:action,calculate,calculate_document|string|regex:/^[A-Z0-9_-]{3,80}$/D','revision'=>'required|integer|min:0','target_id'=>'nullable|required_if:action,review,withdraw|integer|min:1','data'=>'present|array','reason'=>'required|string|min:10|max:4000']);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);if($d['action']==='calculate_document')abort_unless(config('fiscal_rules_qa.document_enabled')===true,404);$d['revision']=(int)$d['revision'];if(isset($d['target_id']))$d['target_id']=(int)$d['target_id'];$s=$this->scope($actor,$this->right($d['action'])); + return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($actor,$s);$this->scope($actor,$this->right($d['action']));$b=$this->basis($s,$d);$level=DB::transactionLevel();DB::beginTransaction();try{$out=$this->execute($actor,$s,$d,$b);}finally{while(DB::transactionLevel()>$level)DB::rollBack();}$cut=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>$key,'observed'=>time(),'basis'=>$this->hash($b),'impact'=>$out['impact']];return ['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];}); + } + public function confirm(int $actor,string $preview):array + { + try{$c=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$c['input'];$s=$this->scope($actor,$this->right($d['action']));return DB::transaction(function()use($actor,$s,$d,$c){$this->lock($actor,$s);$this->scope($actor,$this->right($d['action']));abort_unless($c['actor']===$actor&&$c['scope']==$s&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Contexto ou prévia expirados.');$old=DB::table('fr_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409,'A mesma chave não aceita outra entrada.');return json_decode($old->result,true)+['replayed'=>true,'historical_receipt_only'=>true];}$b=$this->basis($s,$d);abort_unless($this->hash($b)===$c['basis'],409,'Regras, fonte ou versão mudaram; exige nova conferência.');$out=$this->execute($actor,$s,$d,$b);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409);DB::table('fr_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('fiscal_rule.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'version'=>$out['revision'],'before_revision'=>$d['revision'],'reason'=>$d['reason'],'review_hash'=>$b['source_sha256']]);return $out+['replayed'=>false];}); + } + private function execute(int $actor,array $s,array $d,array $basis):array + { + if(in_array($d['action'],['calculate','calculate_document'],true)){ + abort_if(!empty($d['rule_key'])||isset($d['target_id'])||$d['revision']!==0,422);$in=$d['action']==='calculate_document'?$this->documentInput($d['data']):$this->exact($d['data'],['rule_event_id'=>'nullable|integer|min:1','base'=>'required|string|regex:/^\d{1,16}(\.\d{1,8})?$/D','currency'=>'required|string|regex:/^[A-Z]{3}$/D','date'=>'required|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31']);$selected=$basis['selected'];abort_if(isset($in['rule_event_id'])&&!$selected,403,'Regra fora do contexto.');$result=$d['action']==='calculate_document'?$this->evaluateDocument($s,$in,$selected):$this->evaluate($s,$in,$selected);$deps=['source_sha256'=>$basis['source_sha256'],'rules_count'=>$basis['count'],'rules_last_id'=>$basis['last_id'],'selected_id'=>$selected['id']??null,'selected_fingerprint'=>$selected?$this->hash($selected):null];$id=DB::table('fr_calculations')->insertGetId($s+['actor_id'=>$actor,'rule_event_id'=>$selected['id']??null,'input'=>$this->json($in),'result'=>$this->json($result),'dependencies'=>$this->json($deps),'reason'=>$d['reason'],'created_at'=>now()]);return ['id'=>$id,'type'=>'calculation','revision'=>0,'state'=>$result['state'],'impact'=>['action'=>'calculate','result'=>$result,'new_journals'=>0,'legal_activation'=>false]]; + } + $last=$basis['last'];abort_unless(($last['revision']??0)===$d['revision'],409,'Revisão do catálogo alterada.');$prepared=$actor;$target=null; + if($d['action']==='prepare'){abort_if(isset($d['target_id']),422);$definition=$this->definition($d['data']);$version=(int)$this->events($s)->where('rule_key',$d['rule_key'])->max('rule_version')+1;$state='prepared';} + else{ + $decision=$d['action']==='review'?$this->exact($d['data'],['decision'=>'required|in:reviewed,refused']):$this->exact($d['data'],[]);$target=$basis['target'];abort_unless($target&&$target['rule_key']===$d['rule_key'],403,'Escolha a versão exacta desta regra.');$current=$this->events($s)->where(['rule_key'=>$target['rule_key'],'rule_version'=>$target['rule_version']])->orderByDesc('revision')->value('id');abort_unless($current===$target['id'],409,'A versão escolhida já mudou.');abort_unless($target['prepared_by']!==$actor,403,'Preparação e revisão por pessoas distintas.');abort_unless($target['state']===($d['action']==='withdraw'?'reviewed':'prepared'),409,'Estado incompatível com a decisão.');$prepared=$target['prepared_by'];DB::table('users')->where('id',$prepared)->lockForUpdate()->firstOrFail();abort_unless(DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$prepared])->lockForUpdate()->first(),403);abort_unless(DB::table('permission_grants')->where($s+['user_id'=>$prepared,'permission'=>'fiscal_rules.prepare'])->lockForUpdate()->first(),403,'Autor da preparação sem autoridade actual.');$definition=$this->definition($target['definition']);$version=$target['rule_version'];$state=$d['action']==='withdraw'?'withdrawn':$decision['decision']; + } + $revision=$d['revision']+1;$id=DB::table('fr_events')->insertGetId($s+['rule_key'=>$d['rule_key'],'revision'=>$revision,'rule_version'=>$version,'previous_id'=>$last['id']??null,'target_id'=>$target['id']??null,'action'=>$d['action'],'state'=>$state,'prepared_by'=>$prepared,'actor_id'=>$actor,'definition'=>$this->json($definition),'reason'=>$d['reason'],'created_at'=>now()]);return ['id'=>$id,'type'=>'rule','revision'=>$revision,'rule_version'=>$version,'state'=>$state,'impact'=>['action'=>$d['action'],'before'=>['revision'=>$d['revision'],'state'=>$last['state']??null],'after'=>['revision'=>$revision,'rule_version'=>$version,'state'=>$state],'source_checked'=>$this->verified($definition),'definition'=>$definition,'new_journals'=>0,'legal_activation'=>false]]; + } + private function evaluate(array $s,array $in,?array $selected):array + { + $pending=[];$def=$selected['definition']??null; + if(!$selected)$pending[]='Escolha explícita de regra em falta.'; + else{$current=$this->events($s)->where(['rule_key'=>$selected['rule_key'],'rule_version'=>$selected['rule_version']])->orderByDesc('revision')->first();if(!$current||$current->id!==$selected['id']||$current->state!=='reviewed')$pending[]='Regra exacta não revista, revogada ou alterada.';if(!$this->verified($def))$pending[]='Fonte/aplicabilidade não conferida para execução neste ensaio.';if(($def['currency']??null)!==$in['currency'])$pending[]='Moeda incompatível.';if(empty($def['valid_from'])||$in['date']<$def['valid_from']||(!empty($def['valid_until'])&&$in['date']>$def['valid_until']))$pending[]='Data fora de vigência ou vigência desconhecida.'; + $eligible=$this->currentVersions($s)->where('f.state','reviewed')->where('f.definition->family',$def['family'])->where('f.definition->operation',$def['operation'])->where('f.definition->conditions',$def['conditions'])->where('f.definition->currency',$in['currency'])->where('f.definition->source_status','qa_checked')->where('f.definition->valid_from','<=',$in['date'])->where(fn($q)=>$q->whereNull('f.definition->valid_until')->orWhere('f.definition->valid_until','>=',$in['date']));$max=(clone$eligible)->max(DB::raw("(f.definition->>'priority')::int"));if($max!==null){if((int)$max>(int)$def['priority'])$pending[]='Existe prioridade superior; escolha a regra exacta aplicável.';if((clone$eligible)->whereRaw("(f.definition->>'priority')::int=?",[$max])->count()>1)$pending[]='Conflito de regras com a mesma prioridade e âmbito.';}} + if($pending)return ['state'=>'pending','pending'=>$pending,'base'=>$in['base'],'tax_exact'=>null,'tax_document'=>null,'total'=>null,'synthetic_only'=>true,'legal_use'=>false]; + $result=(new FiscalRuleExpression)->evaluate($def['expression'],['base'=>$in['base']],$def['parameters']);if($result['status']!=='determinate')return ['state'=>'pending','pending'=>$result['missing'],'tax_exact'=>null,'tax_document'=>null,'total'=>null,'synthetic_only'=>true,'legal_use'=>false]; + $tax=BigRational::of($result['result'])->toBigDecimal();$document=$tax->toScale(2,RoundingMode::Down);$base=BigDecimal::of($in['base']);$representable=$base->stripTrailingZeros()->getScale()<=2; + return ['state'=>$representable?'determined_qa':'pending_representation','base'=>(string)$base->stripTrailingZeros(),'tax_exact'=>(string)$tax->stripTrailingZeros(),'tax_document'=>(string)$document,'difference'=>(string)$tax->minus($document)->stripTrailingZeros(),'total'=>$representable?(string)$base->plus($document)->toScale(2):null,'currency'=>$in['currency'],'rule'=>['id'=>$selected['id'],'key'=>$selected['rule_key'],'version'=>$selected['rule_version'],'revision'=>$selected['revision'],'source_reference'=>$def['source_reference'],'source_sha256'=>$def['source_sha256']],'steps'=>$result['steps'],'pending'=>$representable?[]:['Base não representável em duas casas; não truncada.'],'synthetic_only'=>true,'legal_use'=>false]; + } + private function documentInput(array $d):array + { + $in=$this->exact($d,['rule_event_id'=>'nullable|integer|min:1','currency'=>'required|string|regex:/^[A-Z]{3}$/D','date'=>'required|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31','price_mode'=>'required|in:net_added,gross_included','lines'=>'required|array|min:1|max:25']); + foreach($in['lines']as$i=>$line){abort_unless(is_array($line),422);$in['lines'][$i]=$this->exact($line,['reference'=>'required|string|max:80','quantity'=>'required|string|regex:/^\d{1,16}(\.\d{1,8})?$/D','unit_price'=>'required|string|regex:/^\d{1,16}(\.\d{1,8})?$/D']);abort_unless(BigDecimal::of($line['quantity'])->isGreaterThan(0),422,'Quantidade positiva obrigatória.');}abort_unless(array_is_list($in['lines'])&&count(array_unique(array_column($in['lines'],'reference')))===count($in['lines']),422,'Linhas sequenciais e referências distintas obrigatórias.');return $in; + } + private function evaluateDocument(array $s,array $in,?array $selected):array + { + $gate=$this->evaluate($s,['base'=>'0','currency'=>$in['currency'],'date'=>$in['date']],$selected);if($gate['state']!=='determined_qa'){unset($gate['base']);return $gate+['policy'=>FiscalDocumentMath::POLICY,'price_mode'=>$in['price_mode'],'base_document'=>null,'lines'=>[]];} + return FiscalDocumentMath::calculate($in['lines'],$in['price_mode'],$selected['definition']['parameters']['rate'])+['currency'=>$in['currency'],'rule'=>$gate['rule']]; + } + public function documentReport(int $actor,int $id):array + { + $detail=$this->detail($actor,$id,'calculation');$r=$detail['result'];abort_unless(isset($r['price_mode'],$r['lines']),422,'Este registo não é cálculo por linhas.');$fp=fopen('php://temp','w+');$rule=$r['rule']??[];$rows=[['MAPA INTERNO POR LINHAS QA — SEM VALIDADE FISCAL'],['Politica',$r['policy']],['Modo',$r['price_mode']],['Data',$detail['input']['date']],['Moeda',$detail['input']['currency']],['Regra escolhida',(string)($detail['input']['rule_event_id']??'INDETERMINADA')],['Versao regra',(string)($rule['version']??'INDETERMINADA')],['Fonte',$rule['source_reference']??'INDETERMINADA'],['SHA fonte',$rule['source_sha256']??'INDETERMINADO'],['Estado historico',$r['state']],['Estado actual',$detail['current_evaluation']['state']],['Linha','Referencia','Base exacta fraccao','Imposto exacto fraccao','Base documental','Imposto documental','Total','Diferenca base fraccao','Diferenca imposto fraccao']];foreach($r['lines']as$line){$ref="'".$line['reference'];$rows[]=[(string)$line['line'],$ref,$line['base_exact_fraction'],$line['tax_exact_fraction'],$line['base_document']??'INDETERMINADO',$line['tax_document'],$line['total_document']??'INDETERMINADO',$line['base_difference_fraction']??'INDETERMINADO',$line['tax_difference_fraction']];}$rows[]=['Totais documentais','','','',$r['base_document']??'INDETERMINADO',$r['tax_document']??'INDETERMINADO',$r['total']??'INDETERMINADO'];foreach($rows as$row)fputcsv($fp,$row,';','"','',"\r\n");rewind($fp);$bytes=stream_get_contents($fp);$s=$this->scope($actor);Audit::record('fiscal_rule.document_report',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'version'=>0,'review_hash'=>hash('sha256',$bytes)]);return ['bytes'=>$bytes,'sha256'=>hash('sha256',$bytes)]; + } + public function listing(int $actor,array $input):array + { + $s=$this->scope($actor);$d=$this->exact($input,['view'=>'sometimes|in:rules,history,calculations','rule_key'=>'sometimes|string|regex:/^[A-Z0-9_-]{3,80}$/D','family'=>'sometimes|in:qa,iva,irps,irpc,ispc,ice,inss,other','state'=>'sometimes|in:prepared,reviewed,refused,withdrawn','search'=>'nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100']);$view=$d['view']??'rules';abort_if($view==='history'&&!isset($d['rule_key']),422);abort_if($view==='calculations'&&array_intersect(array_keys($d),['rule_key','family','state','search']),422); + return DB::transaction(function()use($actor,$s,$d,$view){if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');$q=$view==='calculations'?DB::table('fr_calculations')->where($s):($view==='history'?$this->events($s):$this->currentVersions($s));$alias=$view==='rules'?'f.':'';if(isset($d['rule_key']))$q->where($alias.'rule_key',$d['rule_key']);foreach(['family'=>'definition->family','state'=>'state']as$filter=>$column)if(isset($d[$filter]))$q->where($alias.$column,$d[$filter]);if(!empty($d['search'])){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($w)=>$w->whereRaw($alias."rule_key ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw($alias."definition->>'label' ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw($alias."definition->>'source_reference' ILIKE ? ESCAPE '!'",[$needle]));}$page=$q->orderByDesc($alias.'id')->paginate($d['per_page']??25,['*'],'page',$d['page']??1);$rows=$page->getCollection()->map(fn($r)=>$this->row($r));$ids=$rows->pluck('id')->all();$refs=DB::table('fr_commands')->where($s)->where('actor_id',$actor)->where('result->type',$view==='calculations'?'calculation':'rule')->whereIn(DB::raw("(result->>'id')::bigint"),$ids)->get();$page->setCollection($rows->map(function($r)use($refs){$r['command_reference']=null;foreach($refs as$ref)if(json_decode($ref->result,true)['id']===$r['id'])$r['command_reference']=$ref->command_key;return $r;}));return ['document_enabled'=>config('fiscal_rules_qa.document_enabled')===true,'stream_revision'=>isset($d['rule_key'])?(int)$this->events($s)->where('rule_key',$d['rule_key'])->max('revision'):null,'contract'=>self::CONTRACT,'view'=>$view,'records'=>$page->toArray(),'fixtures'=>$this->fixture(),'capabilities'=>['prepare'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_rules.prepare',$s['unit_id']),'review'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'fiscal_rules.review',$s['unit_id'])],'synthetic_only'=>true,'legal_activation'=>false];}); + } + public function detail(int $actor,int $id,string $type='rule'):array + { + $s=$this->scope($actor);$row=DB::table($type==='calculation'?'fr_calculations':'fr_events')->where($s)->where('id',$id)->first();abort_unless($row,404);$a=$this->row($row);if($type==='rule'){$a['current_revision']=(int)$this->events($s)->where('rule_key',$row->rule_key)->max('revision');$a['current_version_event_id']=(int)$this->events($s)->where(['rule_key'=>$row->rule_key,'rule_version'=>$row->rule_version])->orderByDesc('revision')->value('id');}else{$selected=isset($a['input']['rule_event_id'])?$this->events($s)->where('id',$a['input']['rule_event_id'])->first():null;$a['current_evaluation']=isset($a['input']['lines'])?$this->evaluateDocument($s,$a['input'],$selected?$this->row($selected):null):$this->evaluate($s,$a['input'],$selected?$this->row($selected):null);$a['historical_snapshot_unchanged']=true;}return $a; + } + public function receipt(int $actor,string $key):array + { + $s=$this->scope($actor);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$r=DB::table('fr_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$key])->first();abort_unless($r,404);return ['result'=>json_decode($r->result,true),'created_at'=>$r->created_at,'command_key'=>$key,'historical_receipt_only'=>true,'operation_executed'=>false]; + } +} diff --git a/app/Modules/Services/Http/CommercialQuotesController.php b/app/Modules/Services/Http/CommercialQuotesController.php new file mode 100644 index 0000000..81af3cb --- /dev/null +++ b/app/Modules/Services/Http/CommercialQuotesController.php @@ -0,0 +1,13 @@ +json($fn())->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Orçamento ocupado. Recupere a mesma decisão.'],503)->header('Retry-After','5');}} + public function index(Request $r,CommercialQuotes $s){return $this->response(fn()=>$s->listing($r->user()->id,$r->query()));} + public function options(Request $r,CommercialQuotes $s){return $this->response(fn()=>$s->options($r->user()->id,$r->query()));} + public function history(Request $r,CommercialQuotes $s,string $key){$d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),['page']),422);return $this->response(fn()=>$s->history($r->user()->id,$key,(int)($d['page']??1)));} + public function preview(Request $r,CommercialQuotes $s){return $this->response(fn()=>$s->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,CommercialQuotes $s){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return $this->response(fn()=>$s->confirm($r->user()->id,$d['preview']));} + public function print(Request $r,CommercialQuotes $s,string $key){$d=$r->validate(['version'=>'required|integer|min:1']);abort_if(array_diff(array_keys($r->query()),['version']),422);return response()->view('commercial.quote-print',['quote'=>$s->document($r->user()->id,$key,(int)$d['version'])])->header('Cache-Control','no-store, private');} +} diff --git a/app/Modules/Services/Http/FiscalAssessmentController.php b/app/Modules/Services/Http/FiscalAssessmentController.php new file mode 100644 index 0000000..c9b8be9 --- /dev/null +++ b/app/Modules/Services/Http/FiscalAssessmentController.php @@ -0,0 +1,13 @@ +json($fn())->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Livro fiscal ocupado. Recupere a mesma confirmação; se a versão mudou, confira novamente.'],503)->header('Retry-After','5');}} + public function screen(Request $r,FiscalAssessment $f){abort_if($r->query()!==[],422);$f->scope($r->user()->id);return response()->view('fiscal.workspace',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store');} + public function index(Request $r,FiscalAssessment $f){return $this->response(fn()=>$f->listing($r->user()->id,$r->query()));} + public function preview(Request $r,FiscalAssessment $f){return $this->response(fn()=>$f->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,FiscalAssessment $f){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return $this->response(fn()=>$f->confirm($r->user()->id,$d['preview']));} + public function receipt(Request $r,FiscalAssessment $f,string $key){abort_if($r->query()!==[],422);return $this->response(fn()=>$f->receipt($r->user()->id,strtolower($key)));} + public function export(Request $r,FiscalAssessment $f,int $id){abort_if($r->query()!==[],422);$r=$f->export($r->user()->id,$id);return response($r['bytes'])->header('Content-Type','text/csv; charset=UTF-8')->header('Content-Disposition','attachment; filename="mapa-interno-IVA-QA-v'.$r['version'].'.csv"')->header('X-Content-SHA256',$r['sha256'])->header('Cache-Control','no-store, private');} +} diff --git a/app/Modules/Services/Http/FiscalObligationsController.php b/app/Modules/Services/Http/FiscalObligationsController.php new file mode 100644 index 0000000..392ebff --- /dev/null +++ b/app/Modules/Services/Http/FiscalObligationsController.php @@ -0,0 +1,11 @@ +json($fn())->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException$e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw$e;return response()->json(['message'=>'Controlo fiscal ocupado. Recupere a mesma confirmação.'],503)->header('Retry-After','5')->header('Cache-Control','no-store');}} + public function index(Request $r,FiscalObligations $f){return$this->response(fn()=>$f->listing($r->user()->id,$r->query()));} + public function preview(Request $r,FiscalObligations $f){abort_if(strlen(json_encode($r->all()))>16384,422);return$this->response(fn()=>$f->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,FiscalObligations $f){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return$this->response(fn()=>$f->confirm($r->user()->id,$d['preview']));} + public function receipt(Request $r,FiscalObligations $f,string $key){abort_if($r->query()!==[],422);return$this->response(fn()=>$f->receipt($r->user()->id,strtolower($key)));} +} diff --git a/app/Modules/Services/Http/FiscalRulesController.php b/app/Modules/Services/Http/FiscalRulesController.php new file mode 100644 index 0000000..eb1b6b0 --- /dev/null +++ b/app/Modules/Services/Http/FiscalRulesController.php @@ -0,0 +1,14 @@ +json($out)->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Catálogo fiscal ocupado. Recupere o mesmo pedido; uma versão alterada exige nova conferência.'],503)->header('Retry-After','5');}} + public function index(Request $r,FiscalRules $f){return $this->response(fn()=>$f->listing($r->user()->id,$r->query()));} + public function detail(Request $r,FiscalRules $f,int $id){$d=$r->validate(['type'=>'sometimes|in:rule,calculation']);abort_if(array_diff(array_keys($r->query()),['type']),422);return $this->response(fn()=>$f->detail($r->user()->id,$id,$d['type']??'rule'));} + public function preview(Request $r,FiscalRules $f){return $this->response(fn()=>$f->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,FiscalRules $f){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return $this->response(fn()=>$f->confirm($r->user()->id,$d['preview']));} + public function receipt(Request $r,FiscalRules $f,string $key){abort_if($r->query()!==[],422);return $this->response(fn()=>$f->receipt($r->user()->id,strtolower($key)));} + public function screen(Request $r,FiscalRules $f){abort_if($r->query()!==[],422);$f->scope($r->user()->id);return response()->view('fiscal.rules',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store');} + public function documentReport(Request $r,FiscalRules $f,int $id){abort_if($r->query()!==[],422);return $this->response(function()use($r,$f,$id){$d=$f->documentReport($r->user()->id,$id);return response($d['bytes'])->header('Content-Type','text/csv; charset=UTF-8')->header('Content-Disposition','attachment; filename="mapa-linhas-QA-'.$id.'.csv"')->header('X-Content-SHA256',$d['sha256'])->header('Cache-Control','no-store, private');});} +} diff --git a/app/Modules/Services/Http/ReceivablePositionReadController.php b/app/Modules/Services/Http/ReceivablePositionReadController.php new file mode 100644 index 0000000..8b9a2ed --- /dev/null +++ b/app/Modules/Services/Http/ReceivablePositionReadController.php @@ -0,0 +1,8 @@ +json($reader->listing($request->user()->id,$request->query()))->header('Cache-Control','private, no-store');} +} diff --git a/app/Modules/Services/Http/ServiceDocumentsController.php b/app/Modules/Services/Http/ServiceDocumentsController.php new file mode 100644 index 0000000..91453c0 --- /dev/null +++ b/app/Modules/Services/Http/ServiceDocumentsController.php @@ -0,0 +1,14 @@ +json($fn())->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Documento ocupado. Recupere a mesma confirmação.'],503)->header('Retry-After','5');}} + public function index(Request $r,ServiceDocuments $s){return $this->response(fn()=>$s->listing($r->user()->id,$r->query()));} + public function register(Request $r,\App\Modules\Services\BillingRegister $s){return $this->response(fn()=>$s->listing($r->user()->id,$r->query()));} + public function screen(Request $r,ServiceDocuments $s){$d=$r->validate(['kind'=>'sometimes|in:receipt,credit_note,invoice_receipt']);abort_if(array_diff(array_keys($r->query()),['kind']),422);$s->listing($r->user()->id,['per_page'=>25]);return response()->view('commercial.documents',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token'),'kind'=>$d['kind']??'receipt'])->header('Cache-Control','no-store');} + public function options(Request $r,ServiceDocuments $s){return $this->response(fn()=>$s->options($r->user()->id,$r->query()));} + public function preview(Request $r,ServiceDocuments $s){return $this->response(fn()=>$s->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,ServiceDocuments $s){$d=$r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return $this->response(fn()=>$s->confirm($r->user()->id,$d['preview']));} + public function pdf(Request $r,ServiceDocuments $s,int $id){abort_if($r->query()!==[],422);$row=$s->pdf($r->user()->id,$id);return response(base64_decode($row->pdf_base64,true))->header('Content-Type','application/pdf')->header('Content-Disposition','inline; filename="TEST-SD-'.$row->id.'.pdf"')->header('Cache-Control','no-store, private');} +} diff --git a/app/Modules/Services/ReceivablePositionRead.php b/app/Modules/Services/ReceivablePositionRead.php new file mode 100644 index 0000000..6255af9 --- /dev/null +++ b/app/Modules/Services/ReceivablePositionRead.php @@ -0,0 +1,32 @@ +context->identifiers()+['unit_id'=>$this->context->unitId()]; + abort_unless($s['unit_id']&&$actor===$this->context->userId()&&ModuleRecords::active()==='finance',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_receivables.view',$s['unit_id']); + $rules=['reference_on'=>'required|date_format:Y-m-d','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','search'=>'sometimes|nullable|string|max:100','state'=>'sometimes|in:proposed,refused,open,partial,settled','due_state'=>'sometimes|in:not_open,unknown,overdue,not_due,settled'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate(); + return DB::transaction(function()use($s,$d){ + if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY'); + $scope=fn($alias)=>array_combine(array_map(fn($k)=>$alias.'.'.$k,array_keys($s)),array_values($s)); + $effects=DB::table('sr_effects as e')->join('sr_records as scoped','scoped.id','=','e.obligation_id')->where($scope('scoped'))->where('scoped.kind','obligation')->groupBy('e.obligation_id')->selectRaw("e.obligation_id,count(*) revision,bool_or(e.kind='open') opened,bool_or(e.kind='refused' AND e.record_id=e.obligation_id) refused,coalesce(sum(CASE WHEN e.kind='reduction' THEN e.amount WHEN e.kind='reverse_reduction' THEN -e.amount ELSE 0 END),0) reduced,coalesce(sum(CASE WHEN e.kind='allocate' THEN e.amount WHEN e.kind='reverse_allocation' THEN -e.amount ELSE 0 END),0) allocated"); + $base=DB::table('sr_records as o')->leftJoinSub($effects,'f','f.obligation_id','=','o.id')->join('si_invoices as invoice',function($j){$j->on('invoice.id','=','o.invoice_id');foreach(['tenant_id','company_id','unit_id']as$key)$j->on('invoice.'.$key,'=','o.'.$key);})->where($scope('o'))->where('o.kind','obligation')->select(['o.id','o.invoice_id','o.client_id','o.currency','o.created_at'])->selectRaw("invoice.series_code||'/'||invoice.number::text reference,invoice.snapshot->'document'->'client'->>'name' client,o.amount::text original,coalesce(f.revision,0) revision,coalesce(f.opened,false) opened,coalesce(f.refused,false) refused,coalesce(f.reduced,0)::text reduced,coalesce(f.allocated,0)::text allocated,(o.amount-coalesce(f.reduced,0)-coalesce(f.allocated,0))::text balance,CASE WHEN EXISTS(SELECT 1 FROM sr_effects due WHERE due.obligation_id=o.id AND due.kind='due') THEN (SELECT due.data->>'due_date' FROM sr_effects due WHERE due.obligation_id=o.id AND due.kind='due' ORDER BY due.id DESC LIMIT 1) ELSE o.data->>'due_date' END due_date"); + $positions=DB::query()->fromSub($base,'p')->select('p.*')->selectRaw("CASE WHEN NOT opened THEN CASE WHEN refused THEN 'refused' ELSE 'proposed' END WHEN balance::numeric=0 THEN 'settled' WHEN allocated::numeric>0 THEN 'partial' ELSE 'open' END state,CASE WHEN NOT opened THEN 'not_open' WHEN balance::numeric=0 THEN 'settled' WHEN due_date IS NULL THEN 'unknown' WHEN ?::date>due_date::date THEN 'overdue' ELSE 'not_due' END due_state,CASE WHEN opened AND balance::numeric>0 AND due_date IS NOT NULL THEN greatest(0,?::date-due_date::date) ELSE NULL END overdue_days",[$d['reference_on'],$d['reference_on']]); + $q=DB::query()->fromSub($positions,'positions');foreach(['state','due_state']as$key)if(isset($d[$key]))$q->where($key,$d[$key]);if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(fn($x)=>$x->whereRaw("reference ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("client ILIKE ? ESCAPE '!'",[$needle]));} + $totals=(clone$q)->selectRaw("count(*) FILTER(WHERE opened) recognized_positions,count(*) FILTER(WHERE NOT opened) unrecognized_proposals,count(*) FILTER(WHERE due_state='unknown') unknown_due_positions,coalesce(sum(balance::numeric) FILTER(WHERE opened),0)::text recognized_balance,count(*) FILTER(WHERE opened AND balance::numeric<0) invalid_negative_positions")->first();abort_if((int)$totals->invalid_negative_positions>0,409,'Há saldo incompatível com o livro; exige conferência da origem.');$totals->recognized_balance=(string)BigDecimal::of($totals->recognized_balance)->toScale(2);unset($totals->invalid_negative_positions); + $p=$q->orderByDesc('id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1));foreach($p->items()as$row){foreach(['original','reduced','allocated','balance']as$key)$row->$key=(string)BigDecimal::of($row->$key)->toScale(2);if(!$row->opened)$row->balance=null;$row->revision=(int)$row->revision;} + $records=DB::table('sr_records')->where($s)->selectRaw('count(*) records,coalesce(max(id),0) last_record_id')->first();$marker=DB::table('sr_effects as e')->join('sr_records as scoped','scoped.id','=','e.obligation_id')->where($scope('scoped'))->where('scoped.kind','obligation')->selectRaw('count(*) effects,coalesce(max(e.id),0) last_effect_id')->first(); + return ['positions'=>$p,'totals'=>$totals,'basis'=>['scope'=>$s,'records'=>(int)$records->records,'last_record_id'=>(int)$records->last_record_id,'effects'=>(int)$marker->effects,'last_effect_id'=>(int)$marker->last_effect_id],'reference_on'=>$d['reference_on'],'observed_at'=>now()->utc()->toIso8601String(),'currency'=>'MZN','read_only'=>true,'balance_basis'=>'current_confirmed_effects_not_historical_cut','notice'=>'Saldos actuais. A data de referência classifica apenas o vencimento; não reconstitui saldos históricos. Propostas não reconhecidas não entram no saldo total.']; + }); + } +} diff --git a/app/Modules/Services/ServiceCalculation.php b/app/Modules/Services/ServiceCalculation.php index 654b6dd..70b98be 100644 --- a/app/Modules/Services/ServiceCalculation.php +++ b/app/Modules/Services/ServiceCalculation.php @@ -16,7 +16,10 @@ final class ServiceCalculation public function __construct(private ServiceWorkflow $workflow, private DocumentPreparation $preparation, private CompanyContext $context) {} - public static function isolated(): bool { return app()->environment(['testing','browser-testing']); } + public static function isolated(): bool { + if(app()->environment('budget-testing')&&config('sgf_single_installation.enabled')===true){\App\Shared\Installation\CompositionRuntime::assert();return true;} + return app()->environment(['testing','browser-testing']); + } public static function evaluateReal(array $data, array $prepared, array $profile): array { diff --git a/app/Modules/Services/ServiceDocuments.php b/app/Modules/Services/ServiceDocuments.php new file mode 100644 index 0000000..74f388e --- /dev/null +++ b/app/Modules/Services/ServiceDocuments.php @@ -0,0 +1,143 @@ +json($v));} + private function scope(int $actor,string $right='view'):array + { + abort_unless(config('service_documents.enabled')===true,404);CompositionRuntime::assert();ServiceInvoicing::guard(); + $s=$this->context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']&&ModuleRecords::active()==='commercial',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']); + foreach(['view',$right] as $r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.'.$r,$s['unit_id']);return $s; + } + private function inputs():array + { + $p=CompositionRuntime::physical();$file='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-'.$p['composition'].'/single-document-inputs.json';$pin=config('service_documents.inputs_sha256'); + abort_unless(is_string($pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503);return json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR); + } + private function current(array $s,string $key):?object{return DB::table('sd_versions')->where($s)->where('document_key',$key)->orderByDesc('version')->first();} + private function invoice(array $s,int $id):object + { + $allowed=$this->inputs();$i=ModuleRecords::query('si_invoices')->where($s)->where('id',$id)->first();abort_unless($i&&in_array($i->draft_id,array_column($allowed,'draft_id'),true),403,'Documento fora das novas origens QA autorizadas.');return $i; + } + private function right(array $d):string{return match($d['action']){'review','cancel_review'=>'review','issue'=>'issue',default=>'prepare'};} + private function basis(int $actor,array $s,array $d):array + { + $old=$this->current($s,$d['document_key']);abort_unless(($old?->version??0)===$d['version'],409,'Versão alterada.'); + if($old&&$d['action']!=='prepare'){$data=json_decode($old->data,true);$kind=$old->kind;}else{$data=$d['data'];$kind=$d['kind'];} + if(str_starts_with($d['action'],'cancel_')){ + abort_unless($old&&$old->state==='issued'&&$old->kind===$d['kind']&&$d['data']===[],409);$out=DB::table('sd_outputs')->where($s)->where('document_key',$old->document_key)->firstOrFail();$this->invoice($s,$out->invoice_id);$effect=DB::table('sr_effects')->find($out->effect_id);$last=DB::table('sd_cancellations')->where($s)->where('output_id',$out->id)->orderByDesc('version')->first();$period=DocumentPeriod::open($s);return ['old'=>(array)$old,'data'=>$data,'kind'=>$kind,'origin'=>['output'=>(array)$out,'effect'=>(array)$effect,'last'=>$last?(array)$last:null,'revision'=>DB::table('sr_effects')->where('obligation_id',$effect->obligation_id)->count(),'position'=>app(ServiceReceivables::class)->position($effect->obligation_id,$actor),'period'=>$period]]; + } + abort_unless(!$old||$kind===$d['kind'],409);$origin=[]; + if($kind==='invoice_receipt'){ + abort_unless(config('service_documents.invoice_receipt_enabled')===true,403,'Facturação-recibo pendente dos acessos de consulta documental apresentados.'); + $allowed=$this->inputs()['invoice_receipt'];abort_unless(($data['draft_id']??null)===$allowed['draft_id']&&($data['series_id']??null)===$allowed['series_id']&&($data['rule_id']??null)===$allowed['rule_id'],403); + // Readiness uses the proposed document authority; final issue retains the old SI right. + $review=app(ServiceInvoicing::class)->reviewDocumentIntent($data['draft_id'],$data['series_id'],$data['rule_id'],$actor); + abort_unless($review['ready'],422,'Origem documental ainda não pronta.');$origin=['invoice_review_hash'=>$review['review_hash'],'invoice_review'=>$review['payload']]; + }else{ + $invoice=$this->invoice($s,(int)($data['invoice_id']??0));$origin=['invoice_id'=>$invoice->id,'invoice_sha256'=>hash('sha256',$invoice->snapshot)]; + $ob=DB::table('sr_records')->where($s)->where(['invoice_id'=>$invoice->id,'kind'=>'obligation'])->first();abort_unless($ob,409,'Reconhecimento explícito da obrigação necessário.'); + $origin['obligation_id']=$ob->id;$origin['revision']=DB::table('sr_effects')->where('obligation_id',$ob->id)->count(); + if($kind==='receipt'){ + $effect=DB::table('sr_effects')->where(['obligation_id'=>$ob->id,'id'=>$data['allocation_id']??0,'kind'=>'allocate'])->first();abort_unless($effect,403,'Escolha a afectação efectiva da nova origem.'); + abort_if(DB::table('sr_effects')->where('target_effect_id',$effect->id)->exists(),409,'Afectação revertida; o recibo não pode ser emitido.');$origin['allocation']=(array)$effect; + abort_if(DB::table('sd_outputs')->where($s)->where(['kind'=>'receipt','effect_id'=>$effect->id])->where('document_key','<>',$d['document_key'])->exists(),409,'A afectação já tem recibo. Consulte o documento original.'); + } + } + return ['old'=>$old?(array)$old:null,'data'=>$data,'kind'=>$kind,'origin'=>$origin]; + } + private function lock(int $actor,array $s):void + { + DB::statement("SET LOCAL lock_timeout='5s'");DB::statement("SET LOCAL statement_timeout='20s'");DB::table('companies')->where('tenant_id',$s['tenant_id'])->where('id',$s['company_id'])->lockForUpdate()->firstOrFail(); + DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail(); + abort_unless(DB::table('company_memberships')->where(['user_id'=>$actor,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->lockForUpdate()->first(),403); + DB::table('permission_grants')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$actor])->where(fn($q)=>$q->whereNull('unit_id')->orWhere('unit_id',$s['unit_id']))->orderBy('id')->lockForUpdate()->get(); + } + public function preview(int $actor,array $input,string $key):array + { + $rules=['action'=>'required|in:prepare,review,issue,cancel_prepare,cancel_review','kind'=>'required|in:receipt,credit_note,invoice_receipt','document_key'=>'required|uuid','version'=>'required|integer|min:0','reason'=>'required|string|min:10|max:4000','data'=>'present|array'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();abort_unless(preg_match('/^[a-f0-9-]{36}$/D',$key),422);$s=$this->scope($actor,$this->right($d)); + return DB::transaction(function()use($actor,$s,$d,$key){$this->lock($actor,$s);$this->scope($actor,$this->right($d));$b=$this->basis($actor,$s,$d);$level=DB::transactionLevel();DB::beginTransaction();try{$out=$this->execute($actor,$s,$d,$b,$key);}finally{while(DB::transactionLevel()>$level)DB::rollBack();} + $cut=['actor'=>$actor,'scope'=>$s,'generation'=>request()->session()->get('company_context.token'),'input'=>$d,'key'=>$key,'basis'=>$this->hash($b),'impact'=>$out['impact'],'observed'=>time()];return ['preview'=>Crypt::encryptString($this->json($cut)),'impact'=>$out['impact']];}); + } + public function confirm(int $actor,string $preview):array + { + try{$c=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$d=$c['input'];$s=$this->scope($actor,$this->right($d)); + return DB::transaction(function()use($actor,$s,$c,$d){$this->lock($actor,$s);$this->scope($actor,$this->right($d));abort_unless($c['actor']===$actor&&$c['scope']==$s&&$c['generation']===request()->session()->get('company_context.token')&&$c['observed']>=time()-600&&$c['observed']<=time(),409,'Contexto ou prévia expirados.'); + $old=DB::table('sd_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$c['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409);return json_decode($old->result,true)+['replayed'=>true];} + $b=$this->basis($actor,$s,$d);abort_unless($this->hash($b)===$c['basis'],409,'A origem ou dependências mudaram. Confira novamente.');$out=$this->execute($actor,$s,$d,$b,$c['key']);abort_unless($this->hash($out['impact'])===$this->hash($c['impact']),409,'Impacto alterado.'); + DB::table('sd_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$c['key'],'request_hash'=>$this->hash($d),'result'=>$this->json($out),'created_at'=>now()]);Audit::record('service_document.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$out['id'],'reason'=>$d['reason'],'before'=>$b['old'],'after'=>$out]);return $out+['replayed'=>false];}); + } + private function execute(int $actor,array $s,array $d,array $b,string $key):array + { + $old=$b['old'];$kind=$b['kind'];$data=$b['data'];$action=$d['action'];$prepared=$old['prepared_by']??$actor;$effect=null;$output=null; + if(str_starts_with($action,'cancel_'))return $this->cancel($actor,$s,$d,$b,$key); + if($action==='prepare'){ + abort_if($old&&$kind==='credit_note',409,'A preparação financeira é imutável; conserve-a e prepare outro processo após a recusa.'); + abort_unless(!$old||in_array($old['state'],['draft','refused'],true),409);abort_unless(!$old||$prepared===$actor,403); + $rules=match($kind){'receipt'=>['invoice_id'=>'required|integer','allocation_id'=>'required|integer'], 'credit_note'=>['invoice_id'=>'required|integer','amount'=>'required|string','explanation'=>'required|string|min:10|max:1000'], 'invoice_receipt'=>['draft_id'=>'required|integer','series_id'=>'required|integer','rule_id'=>'required|integer','payment_date'=>'required|date_format:Y-m-d|before_or_equal:today','evidence'=>'required|string|min:10|max:1000']}; + abort_if(array_diff(array_keys($d['data']),array_keys($rules)),422);$data=Validator::make($d['data'],$rules)->validate(); + if($kind==='credit_note'){ + $data['amount']=ServiceReceivables::money($data['amount']);abort_unless($data['amount']==='100.00',422,'Este recorte QA limita o crédito a100MZN.'); + $record=app(ServiceReceivables::class)->propose($actor,['kind'=>'correction','invoice_id'=>$data['invoice_id'],'obligation_id'=>$b['origin']['obligation_id'],'currency'=>'MZN','amount'=>$data['amount'],'source'=>'SIM-DOCUMENTOS','external_id'=>'SD-'.$d['document_key'],'revision'=>$b['origin']['revision'],'action'=>'reduction','document_ref'=>'TEST-REDUCTION-'.$d['document_key'],'line'=>'1','reason'=>$data['explanation']],$key); + $data['correction_id']=$record['id']; + } + if($kind==='invoice_receipt')foreach(['prepare','allocate'] as $r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_receivables.'.$r,$s['unit_id']); + $state='draft'; + }elseif($action==='review'){ + abort_unless($old&&$old['state']==='draft'&&$prepared!==$actor,403);$review=Validator::make($d['data'],['decision'=>'required|in:approved,refused'])->validate();abort_if(array_diff(array_keys($d['data']),['decision']),422);$state=$review['decision']==='approved'?'approved':'refused'; + }else{ + abort_unless($old&&$old['state']==='approved'&&$d['data']===[],409);$state='issued'; + if($kind==='credit_note'){ + $this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.credit_apply',$s['unit_id']); + $decided=app(ServiceReceivables::class)->decide($data['correction_id'],$actor,['version'=>1,'revision'=>$b['origin']['revision'],'decision'=>'approved','reason'=>$d['reason']],$key);$effect=$decided['id'];$invoice=$data['invoice_id'];$amount=$data['amount']; + }elseif($kind==='receipt'){$effect=$data['allocation_id'];$invoice=$data['invoice_id'];$amount=$b['origin']['allocation']['amount'];} + else{ + $review=app(ServiceInvoicing::class)->review($data['draft_id'],$data['series_id'],$data['rule_id'],$actor);$issued=app(ServiceInvoicing::class)->issue($data['draft_id'],$actor,['series_id'=>$data['series_id'],'rule_id'=>$data['rule_id'],'review_hash'=>$review['review_hash']],$key);$invoice=$issued['id'];$amount='1000.00'; + $aggregate=app(ServiceReceivables::class)->completeDocument($old['id'],$invoice,$actor,$key);$effect=$aggregate['allocation_id']; + } + } + $id=DB::table('sd_versions')->insertGetId($s+['document_key'=>$d['document_key'],'version'=>$d['version']+1,'previous_id'=>$old['id']??null,'kind'=>$kind,'action'=>$action,'state'=>$state,'prepared_by'=>$prepared,'actor_id'=>$actor,'data'=>$this->json($data),'dependencies'=>$this->json($b['origin']),'reason'=>$d['reason'],'created_at'=>now()]); + if($state==='issued'){ + $number=(int)DB::table('sd_outputs')->where($s)->where('kind',$kind)->max('number')+1;$inv=$this->invoice($s,$invoice);$origin=json_decode($inv->snapshot,true);$label=match($kind){'receipt'=>'Recibo','credit_note'=>'Nota de credito',default=>'Factura-recibo'}; + $lines=[['title',$label.' de TESTE'],['text','TEST-SD-'.strtoupper($kind).'/'.$number],['text',ServiceInvoicing::NOTICE],['text','Origem: '.$inv->series_code.'/'.$inv->number],['text','Cliente: '.($origin['document']['client']['name']??'Cliente QA')],['text','Montante: '.$amount.' MZN'],['text','Autor da preparacao: '.$prepared],['text','Autor da emissao: '.$actor],['text','Fundamento: '.$d['reason']],['text','Sem dinheiro real. Sem efeito automatico em stock.']]; + if($kind==='credit_note')$lines[]=['text','Reducao nominal de teste; sem reparticao fiscal certificada.'];if($kind==='invoice_receipt')$lines[]=['text','Base862,07 + imposto documental137,93 =1000,00 MZN.']; + $snapshot=['document_lines'=>$lines,'kind'=>$kind,'invoice_id'=>$invoice,'effect_id'=>$effect,'amount'=>$amount,'simulation_only'=>true,'notice'=>ServiceInvoicing::NOTICE,'source_sha256'=>hash('sha256',$inv->snapshot),'prepared_by'=>$prepared,'issued_by'=>$actor,'issued_at'=>now()->toIso8601String()];$pdf=app(InvoicePdf::class)->render($snapshot); + $output=DB::table('sd_outputs')->insertGetId($s+['document_key'=>$d['document_key'],'version_id'=>$id,'kind'=>$kind,'number'=>$number,'invoice_id'=>$invoice,'effect_id'=>$effect,'snapshot'=>$this->json($snapshot),'pdf_base64'=>base64_encode($pdf),'pdf_sha256'=>hash('sha256',$pdf),'actor_id'=>$actor,'created_at'=>now()]); + } + $impact=['kind'=>$kind,'action'=>$action,'state'=>$state,'amount'=>$kind==='credit_note'?($data['amount']??null):($kind==='receipt'?($b['origin']['allocation']['amount']??null):'1000.00'),'physical_change'=>false,'financial_reduction'=>$kind==='credit_note'&&$action==='issue','atomic_invoice_receipt'=>$kind==='invoice_receipt'&&$action==='issue']; + return ['id'=>$id,'document_key'=>$d['document_key'],'version'=>$d['version']+1,'state'=>$state,'output_id'=>$output,'effect_id'=>$effect,'impact'=>$impact]; + } + public function listing(int $actor,array $input):array + { + $s=$this->scope($actor);$d=Validator::make($input,['kind'=>'sometimes|in:receipt,credit_note,invoice_receipt','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','document_key'=>'sometimes|uuid'])->validate();abort_if(array_diff(array_keys($input),array_keys($d)),422); + $q=DB::table('sd_versions as v')->where(['v.tenant_id'=>$s['tenant_id'],'v.company_id'=>$s['company_id'],'v.unit_id'=>$s['unit_id']]);if(isset($d['document_key']))$q->where('v.document_key',$d['document_key']);else $q->whereNotExists(fn($x)=>$x->selectRaw('1')->from('sd_versions as n')->whereColumn('n.document_key','v.document_key')->whereColumn('n.tenant_id','v.tenant_id')->whereColumn('n.company_id','v.company_id')->whereColumn('n.unit_id','v.unit_id')->whereColumn('n.version','>','v.version')); + if(isset($d['kind']))$q->where('v.kind',$d['kind']);$p=$q->orderByDesc('v.id')->paginate((int)($d['per_page']??25),['v.*'],'page',(int)($d['page']??1));$outputs=DB::table('sd_outputs')->where($s)->whereIn('document_key',$p->pluck('document_key')->all())->get(['id','document_key','number','pdf_sha256'])->keyBy('document_key');foreach($p->items() as $row){$row->data=json_decode($row->data,true);$row->output=$outputs->get($row->document_key);}if(DB::getSchemaBuilder()->hasTable('sd_cancellations')){$events=DB::table('sd_cancellations')->where($s)->whereIn('output_id',$outputs->pluck('id')->all())->orderBy('version')->get()->groupBy('output_id');foreach($p->items() as $row)if($row->output)$row->output->cancellations=($events->get($row->output->id)??collect())->all();}$caps=[];foreach(['prepare','review','issue','credit_apply'] as $right)$caps[$right]=$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_documents.'.$right,$s['unit_id']);if(($d['kind']??null)==='invoice_receipt')foreach(['prepare','allocate'] as $right)$caps['prepare']=$caps['prepare']&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_receivables.'.$right,$s['unit_id']);if(($d['kind']??null)==='credit_note')$caps['prepare']=$caps['prepare']&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_receivables.correction_request',$s['unit_id']);$caps['cancel_prepare']=$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_documents.prepare',$s['unit_id'])&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_receivables.correction_request',$s['unit_id']);$caps['cancel_review']=$caps['review']&&$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'service_receivables.correction_approve',$s['unit_id']);return ['documents'=>$p,'notice'=>ServiceInvoicing::NOTICE,'inputs'=>$this->inputs(),'capabilities'=>$caps,'actor_id'=>$actor]; + } + public function pdf(int $actor,int $id):object{$s=$this->scope($actor);$row=DB::table('sd_outputs')->where($s)->where('id',$id)->first();abort_unless($row,403);$this->invoice($s,$row->invoice_id);abort_unless(hash_equals($row->pdf_sha256,hash('sha256',base64_decode($row->pdf_base64,true))),409,'Integridade documental incompatível.');return $row;} + private function cancel(int $actor,array $s,array $d,array $b,string $key):array + { + $o=$b['origin'];$last=$o['last'];abort_if(($last['state']??null)==='cancelled',409,'Documento já anulado tecnicamente.');$target=$o['effect'];abort_if(DB::table('sr_effects')->where('target_effect_id',$target['id'])->exists(),409,'O efeito já foi revertido; exige reconciliação própria.');$sr=app(ServiceReceivables::class); + if($d['action']==='cancel_prepare'){ + $record=$sr->propose($actor,['kind'=>'correction','invoice_id'=>$o['output']['invoice_id'],'obligation_id'=>$target['obligation_id'],'currency'=>'MZN','source'=>'SIM-SD-ANULACAO','external_id'=>'SD-CANCEL-'.$key,'revision'=>$o['revision'],'action'=>$b['kind']==='credit_note'?'reverse_reduction':'reverse_allocation','target_effect_id'=>$target['id'],'reason'=>$d['reason']],$key);$correction=$record['id'];$effect=null;$state='prepared'; + }else{ + abort_unless($last&&$last['state']==='prepared'&&$last['actor_id']!==$actor,403,'Revisão distinta da preparação da anulação necessária.');$correction=$last['correction_id'];$r=DB::table('sr_records')->find($correction);abort_unless((json_decode($r->data,true)['revision']??null)===$o['revision'],409,'O recebível mudou. O preparador deve renovar a conferência da anulação.'); + if($b['kind']==='credit_note')$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.credit_apply',$s['unit_id']);$result=$sr->decide($correction,$actor,['version'=>1,'revision'=>$o['revision'],'decision'=>'approved','reason'=>$d['reason']],$key);$effect=$result['id'];$state='cancelled'; + } + $id=DB::table('sd_cancellations')->insertGetId($s+['cancellation_key'=>$d['document_key'],'version'=>($last['version']??0)+1,'output_id'=>$o['output']['id'],'state'=>$state,'actor_id'=>$actor,'correction_id'=>$correction,'effect_id'=>$effect,'snapshot'=>$this->json(['target_effect_id'=>$target['id'],'output_sha256'=>$o['output']['pdf_sha256'],'revision'=>$o['revision'],'period'=>$o['period'],'invoice_preserved'=>true,'money_refunded'=>false]),'reason'=>$d['reason'],'created_at'=>now()]); + return ['id'=>$id,'document_key'=>$d['document_key'],'version'=>$d['version'],'cancellation_version'=>($last['version']??0)+1,'state'=>$state,'effect_id'=>$effect,'impact'=>['kind'=>$b['kind'],'action'=>$d['action'],'state'=>$state,'amount'=>$target['amount'],'balance_increase_on_confirmation'=>$target['amount'],'balance_before'=>$o['position']['balance'],'balance_after_confirmation'=>(string)\Brick\Math\BigDecimal::of($o['position']['balance'])->plus($target['amount'])->toScale(2),'invoice_preserved'=>true,'physical_change'=>false,'money_refunded'=>false,'financial_reduction'=>false,'atomic_invoice_receipt'=>false]]; + } + public function options(int $actor,array $input):array + { + $s=$this->scope($actor);$d=Validator::make($input,['view'=>'required|in:invoices,allocations','invoice_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'])->validate();abort_if(array_diff(array_keys($input),array_keys($d)),422); + $invoices=ModuleRecords::query('si_invoices')->where($s)->whereIn('draft_id',array_column($this->inputs(),'draft_id')); + if($d['view']==='invoices')$q=$invoices->select('id','series_code','number','draft_id');else{ + $invoice=$this->invoice($s,$d['invoice_id']??0);$q=DB::table('sr_effects as e')->join('sr_records as r','r.id','=','e.record_id')->where(['r.tenant_id'=>$s['tenant_id'],'r.company_id'=>$s['company_id'],'r.unit_id'=>$s['unit_id'],'r.invoice_id'=>$invoice->id,'e.kind'=>'allocate'])->whereNotExists(fn($v)=>$v->selectRaw('1')->from('sr_effects as rev')->whereColumn('rev.target_effect_id','e.id'))->select('e.id','e.amount','r.invoice_id','e.created_at'); + }return ['records'=>$q->orderBy('id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1))]; + } + public function sourceInvoices(int $actor){$s=$this->scope($actor);return ModuleRecords::query('si_invoices')->where($s)->whereIn('draft_id',array_column($this->inputs(),'draft_id'));} +} diff --git a/app/Modules/Services/ServiceFiscal.php b/app/Modules/Services/ServiceFiscal.php index b971ecf..9957492 100644 --- a/app/Modules/Services/ServiceFiscal.php +++ b/app/Modules/Services/ServiceFiscal.php @@ -55,9 +55,9 @@ final class ServiceFiscal public function listing(int $actor): array { $s=$this->scope($actor); - if (!\Illuminate\Support\Facades\Schema::hasTable('sf_proposals')) return ['migration_pending'=>true,'proposals'=>[]]; + if (!\Illuminate\Support\Facades\Schema::hasTable('sf_proposals')) return ['migration_pending'=>true,'proposals'=>[],'coverage'=>FiscalCoverage::describe()]; $rows=DB::table('sf_proposals')->where($s)->orderByDesc('id')->get()->map(function($r) { return (array)$r+['definition'=>$this->data($r),'decision'=>$this->decision($r->id),'active'=>$r->kind==='profile' && $this->activated($r)]; })->all(); - return ['proposals'=>$rows,'migration_pending'=>false,'notice'=>'RASCUNHO — SEM VALIDADE FISCAL; aprovação não activa o perfil.']; + return ['coverage'=>FiscalCoverage::describe(),'proposals'=>$rows,'migration_pending'=>false,'notice'=>'RASCUNHO — SEM VALIDADE FISCAL; aprovação não activa o perfil.']; } public function propose(int $actor,array $input): array { diff --git a/app/Modules/Services/ServiceInvoicing.php b/app/Modules/Services/ServiceInvoicing.php index a162c91..0b5be81 100644 --- a/app/Modules/Services/ServiceInvoicing.php +++ b/app/Modules/Services/ServiceInvoicing.php @@ -107,6 +107,12 @@ final class ServiceInvoicing { return $this->command($actor,'service_invoices.issue',fn($s)=>$this->reviewInside($draft,$series,$rule,$actor,$s)); } + /** Readiness for a staged document; issuance still requires the original issue right. */ + public function reviewDocumentIntent(int $draft,int $series,int $rule,int $actor):array + { + abort_unless(config('service_documents.enabled')===true,404); + return $this->command($actor,'service_documents.view',fn($s)=>$this->reviewInside($draft,$series,$rule,$actor,$s)); + } private function reviewInside(int $draft,int $series,int $rule,int $actor,array $s): array { $view=$this->preview->captureDraftForIssuance($draft,$actor); $date=now(self::ZONE)->format('Y-m-d'); $blocks=[]; diff --git a/app/Modules/Services/ServiceReceivables.php b/app/Modules/Services/ServiceReceivables.php index a400c76..13a8ebc 100644 --- a/app/Modules/Services/ServiceReceivables.php +++ b/app/Modules/Services/ServiceReceivables.php @@ -85,7 +85,7 @@ final class ServiceReceivables public function propose(int $actor,array $input,string $key): array { self::guard(); - $d=$this->validate($input,['kind'=>'required|in:obligation,receipt,observation,correction','invoice_id'=>'required|integer|min:1','obligation_id'=>'nullable|integer|min:1','receipt_id'=>'nullable|integer|min:1','currency'=>'required|in:MZN','amount'=>'nullable|string','source'=>'required|string|min:3|max:80','external_id'=>'required|string|min:3|max:100','date'=>'nullable|date_format:Y-m-d','evidence'=>'nullable|string|min:3|max:1000','due_date'=>'nullable|date_format:Y-m-d','reason'=>'nullable|string|min:3|max:1000','action'=>'nullable|in:reverse_allocation,reverse_reconcile,reverse_receipt,reduction,due','target_effect_id'=>'nullable|integer|min:1','document_ref'=>'nullable|string|max:100','line'=>'nullable|in:1','revision'=>'required|integer|min:0']); + $d=$this->validate($input,['kind'=>'required|in:obligation,receipt,observation,correction','invoice_id'=>'required|integer|min:1','obligation_id'=>'nullable|integer|min:1','receipt_id'=>'nullable|integer|min:1','currency'=>'required|in:MZN','amount'=>'nullable|string','source'=>'required|string|min:3|max:80','external_id'=>'required|string|min:3|max:100','date'=>'nullable|date_format:Y-m-d','evidence'=>'nullable|string|min:3|max:1000','due_date'=>'nullable|date_format:Y-m-d','reason'=>'nullable|string|min:3|max:1000','action'=>'nullable|in:reverse_allocation,reverse_reconcile,reverse_receipt,reduction,reverse_reduction,due','target_effect_id'=>'nullable|integer|min:1','document_ref'=>'nullable|string|max:100','line'=>'nullable|in:1','revision'=>'required|integer|min:0']); $kind=$d['kind']; $right=$kind==='correction'?'correction_request':'prepare'; if(isset($d['amount'])) $d['amount']=self::money($d['amount'],$kind!=='observation'); @@ -121,10 +121,10 @@ final class ServiceReceivables abort_unless(isset($d['amount'],$d['document_ref'],$d['line']) && str_starts_with($d['document_ref'],'TEST-REDUCTION-'),422,'Documento redutor sintético e linha necessários.'); $amount=$d['amount']; } - if(in_array($d['action'],['reverse_allocation','reverse_reconcile'])) { + if(in_array($d['action'],['reverse_allocation','reverse_reconcile','reverse_reduction'])) { $effect=DB::table('sr_effects')->where('obligation_id',$ob->id)->where('id',$d['target_effect_id']??0)->first(); - abort_unless($effect && $effect->kind===($d['action']==='reverse_allocation'?'allocate':'reconcile'),422,'Alvo de reversão incompatível.'); - $receipt=$this->row($effect->receipt_id,$s,'receipt'); $amount=$effect->amount; + abort_unless($effect && $effect->kind===(match($d['action']){'reverse_allocation'=>'allocate','reverse_reduction'=>'reduction',default=>'reconcile'}),422,'Alvo de reversão incompatível.'); + if($d['action']==='reverse_reduction'){abort_unless(config('service_documents.enabled')===true&&DB::table('sd_outputs')->where($s)->where(['kind'=>'credit_note','effect_id'=>$effect->id])->exists(),403);DocumentPeriod::open($s);}else $receipt=$this->row($effect->receipt_id,$s,'receipt'); $amount=$effect->amount; } if($d['action']==='reverse_receipt') { abort_unless($receipt,422); $amount=$receipt->amount; } } @@ -153,7 +153,7 @@ final class ServiceReceivables if($r->kind==='correction') { abort_unless(($rd['revision']??-1)===$d['revision'],409,'Proposta ligada a revisão anterior; prepare nova proposta.'); $kind=$rd['action']; $amount=$r->amount; $receipt=$r->receipt_id; $target=$rd['target_effect_id']??null; - if($kind==='reduction' || $kind==='due') $receipt=null; + if($kind==='reduction' || $kind==='due' || $kind==='reverse_reduction') $receipt=null;if($kind==='reverse_reduction'){DocumentPeriod::open($s);$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.credit_apply',$s['unit_id']);} } } elseif($r->kind==='observation') abort(422,'A conferência regista correspondência ou divergência; não é aprovação de recibo.'); return $this->effect($id,$ob,$receipt,$kind,$amount,$target,$d['revision'],$actor,$d+['due_date'=>$rd['due_date']??null]); @@ -188,6 +188,36 @@ final class ServiceReceivables return $this->effect($id,$r->obligation_id,$id,'allocate',$d['amount'],null,$d['revision'],$actor,[]); }); } + /** + * One sealed QA invoice-receipt, using the original record/effect writers. + * prepared_by is the actual prior declarant; actor_id on effects is the real executor. + * No guard/context impersonation and no general delegation of prepare/allocate rights. + */ + public function completeDocument(int $versionId,int $invoiceId,int $actor,string $key):array + { + self::guard();abort_unless(config('service_documents.enabled')===true&&config('service_documents.invoice_receipt_enabled')===true&&DB::transactionLevel()>0,403); + $s=$this->scope($actor,'confirm');foreach(['view','issue'] as $r)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'service_documents.'.$r,$s['unit_id']); + $v=DB::table('sd_versions')->where($s)->where(['id'=>$versionId,'kind'=>'invoice_receipt','state'=>'approved'])->first();abort_unless($v&&$v->prepared_by!==$actor,403,'Intenção financeira distinta e revista necessária.'); + $latest=DB::table('sd_versions')->where($s)->where('document_key',$v->document_key)->max('version');abort_unless($latest===$v->version,409); + $intent=json_decode($v->data,true,32,JSON_THROW_ON_ERROR);$preparer=(int)$v->prepared_by; + abort_unless(DB::table('users')->where('id',$preparer)->lockForUpdate()->exists()&&DB::table('company_memberships')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'user_id'=>$preparer])->lockForUpdate()->exists(),403); + DB::table('permission_grants')->where($s)->where('user_id',$preparer)->orderBy('id')->lockForUpdate()->get(); + foreach(['service_documents.prepare','service_receivables.view','service_receivables.prepare','service_receivables.allocate'] as $right)$this->permissions->require($preparer,$s['tenant_id'],$s['company_id'],$right,$s['unit_id']); + $inv=\App\Shared\Context\ModuleRecords::query('si_invoices')->where($s)->where(['id'=>$invoiceId,'draft_id'=>$intent['draft_id'],'series_id'=>$intent['series_id'],'rule_id'=>$intent['rule_id'],'actor_id'=>$actor])->first();abort_unless($inv,403,'A emissão deve pertencer à origem selada e ao executor actual.'); + $snap=json_decode($inv->snapshot,true,64,JSON_THROW_ON_ERROR);abort_unless(($snap['simulation_only']??null)===true&&($snap['notice']??null)===self::NOTICE&&($snap['calculation']['total_document_proposed']??null)==='1000.00'&&($snap['calculation']['currency']??null)==='MZN',422); + abort_if(DB::table('sr_records')->where('invoice_id',$invoiceId)->exists()||DB::table('sd_outputs')->where('invoice_id',$invoiceId)->exists(),409,'Origem já utilizada.'); + $history=DB::table('a_history')->where($s)->where(['area'=>'billing_drafts','record_id'=>$inv->draft_id,'version'=>$snap['document']['draft_version']])->value('snapshot');abort_unless($history,409);$client=(int)json_decode($history,true)['client_id']; + $source='SIM-FACTURA-RECIBO';$external='SD-'.$v->document_key;$reason='Conclusão atómica da intenção financeira QA revista: '.$v->document_key; + $ob=$this->record($s,$preparer,'obligation',$inv,$client,null,null,'1000.00',$source,$external.'-OB',['due_date'=>null,'document_intent_id'=>$v->id]);$obId=$ob['id']; + $this->effect($obId,$obId,null,'open','1000.00',null,0,$actor,['reason'=>$reason,'document_intent_id'=>$v->id]); + $rc=$this->record($s,$preparer,'receipt',$inv,$client,$obId,null,'1000.00',$source,$external.'-RC',['date'=>$intent['payment_date'],'evidence'=>$intent['evidence'],'document_intent_id'=>$v->id]); + $this->effect($rc['id'],$obId,null,'receipt_confirm','1000.00',null,1,$actor,['reason'=>$reason,'document_intent_id'=>$v->id]); + $allocation=$this->effect($rc['id'],$obId,$rc['id'],'allocate','1000.00',null,2,$actor,['reason'=>$reason,'document_intent_id'=>$v->id,'aggregate_authority'=>'service_documents.issue']); + $result=['id'=>$obId,'obligation_id'=>$obId,'receipt_id'=>$rc['id'],'allocation_id'=>$allocation['id'],'event'=>'invoice_receipt.simulation','balance'=>'0.00']; + $command=DB::table('sr_commands')->insertGetId($s+['actor_id'=>$actor,'action'=>'document_issue','command_key'=>$key,'request_hash'=>hash('sha256',$this->json([$v->id,$invoiceId,$intent])),'result'=>$this->json($result),'created_at'=>now()]); + $event=DB::table('sr_events')->insertGetId(['command_id'=>$command,'type'=>'invoice_receipt.simulation','schema_version'=>1,'payload'=>$this->json($s+['result'=>$result,'prepared_by'=>$preparer,'executed_by'=>$actor,'document_intent_id'=>$v->id]),'created_at'=>now()]);DB::table('sr_outbox')->insert(['event_id'=>$event,'state'=>'captured_simulation_only']); + Audit::record('service_receivables.document_issue',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['command_id'=>$command,'prepared_by'=>$preparer,'document_intent_id'=>$v->id,'simulation_only'=>true]);return $result; + } private function state(int $ob,?string $cut=null): array { $r=DB::table('sr_records')->find($ob); $rd=$this->data($r); @@ -204,7 +234,7 @@ final class ServiceReceivables $x=BigDecimal::of($e->amount)->multipliedBy($e->kind==='allocate'?1:-1); $allocated=$allocated->plus($x); $rc=&$receipts[$e->receipt_id]; $rc['allocated']=(string)BigDecimal::of($rc['allocated'])->plus($x)->toScale(2); $rc['available']=(string)BigDecimal::of($rc['available'])->minus($x)->toScale(2); unset($rc); } - if($e->kind==='reduction') $reduced=$reduced->plus($e->amount); + if($e->kind==='reduction') $reduced=$reduced->plus($e->amount);if($e->kind==='reverse_reduction') $reduced=$reduced->minus($e->amount); if($e->kind==='reconcile') $receipts[$e->receipt_id]['reconciled']=true; if($e->kind==='reverse_reconcile') $receipts[$e->receipt_id]['reconciled']=false; if($e->kind==='reverse_receipt') { $receipts[$e->receipt_id]['state']='reversed'; $receipts[$e->receipt_id]['available']='0.00'; } diff --git a/app/Modules/Services/ServiceWorkflow.php b/app/Modules/Services/ServiceWorkflow.php index d72d727..a56e2cf 100644 --- a/app/Modules/Services/ServiceWorkflow.php +++ b/app/Modules/Services/ServiceWorkflow.php @@ -89,6 +89,7 @@ final class ServiceWorkflow $old = $id ? $this->query($area, $scope)->where('id', $id)->first() : null; if ($id) { abort_unless($old, 403, 'Registo fora do âmbito autorizado.'); + if(in_array($area,['service_drafts','billing_drafts'],true)&&\Illuminate\Support\Facades\Schema::hasTable('cq_conversions')){$column=$area==='service_drafts'?'service_draft_id':'billing_draft_id';abort_if(DB::table('cq_conversions')->where($column,$id)->exists(),409,'Rascunho de orçamento convertido: preserve a proposta aceite; alterações exigem correcção própria.');} if (in_array($area, ['service_drafts','billing_drafts'], true) && \Illuminate\Support\Facades\Schema::hasTable('wb_generations')) { $column = $area === 'service_drafts' ? 'service_draft_id' : 'billing_draft_id'; abort_if(\App\Modules\Workshop\WorkshopCommercialState::linked($area, $id),409,'Rascunho ligado a oficina: correcção comercial própria necessária; a proposta revista não pode ser alterada por edição directa.'); diff --git a/app/Modules/Workshop/ComposedWorkshopStockBook.php b/app/Modules/Workshop/ComposedWorkshopStockBook.php new file mode 100644 index 0000000..c0a5eba --- /dev/null +++ b/app/Modules/Workshop/ComposedWorkshopStockBook.php @@ -0,0 +1,41 @@ +table($name);} + public static function hasTable(string $name):bool{return self::connection()->getSchemaBuilder()->hasTable($name);} + public static function actor(int $hostActor):int{return self::$nativeActor??$hostActor;} + public function run(\Closure $work):mixed + { + CompositionRuntime::assert();abort_unless(config('workshop_stock.composed_enabled')===true,404); + $ctx=app(CompanyContext::class);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId()];$host=DB::connection();$user=\App\Models\User::findOrFail($ctx->userId());$physical=CompositionRuntime::physical($host); + abort_unless(request()->session()->get('company_context.module_id')==='workshop',409,'Seleccione Oficinas.'); + $file='C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-'.$physical['composition'].'/workshop-stock-binding.json';$pin=config('workshop_stock.composed_sha256'); + abort_unless(is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503,'Ligação física não conferida.'); + $p=json_decode(file_get_contents($file),true,64,JSON_THROW_ON_ERROR);abort_unless(($p['scope']??null)===$scope&&($p['composition']??null)===$physical['composition']&&($p['source']['schema']??null)==='public',403); + foreach(['resources','eligible_order_ids','eligible_deliveries'] as $key)abort_unless(is_array($p['source'][$key]??null)&&count($p['source'][$key])<=1000,503); + return $host->transaction(function()use($scope,$host,$user,$p,$work){ + $host->statement("SET LOCAL lock_timeout='5s'");$host->table('companies')->where(['tenant_id'=>$scope['tenant_id'],'id'=>$scope['company_id']])->lockForUpdate()->firstOrFail(); + return app(ReceiptRuntime::class)->run('stock.view',function($actor,$binding)use($scope,$host,$user,$p,$work){ + $native=DB::connection();$marker=$native->table('gl_sgf_composition')->where('tenant_id',$scope['tenant_id'])->first();abort_unless($marker&&$marker->host_database===$host->getDatabaseName()&&(int)$marker->company_id===$scope['company_id']&&(int)$marker->unit_id===$scope['unit_id'],403); + foreach($p['source']['resources'] as $resource)abort_if(array_diff($resource['warehouse_ids'],$binding['warehouse_ids']??[]),403); + // Physical authors are native warehouse identities, never coincident host IDs. + if(request()->isMethod('POST')&&str_ends_with(request()->path(),'/confirm'))abort_unless($actor->hasFunctions(['warehouse']),403); + $default=DB::getDefaultConnection();$guard=Auth::getDefaultDriver();$environment=app()->environment();$old=config('workshop_stock.synthetic_sources'); + self::$native=$native;self::$nativeActor=(int)$actor->id;DB::setDefaultConnection($host->getName());Schema::clearResolvedInstance('db.schema');Auth::shouldUse('web');Auth::guard('web')->setUser($user);app()->instance('env','budget-testing');config(['workshop_stock.synthetic_sources.'.implode(':',$scope)=>$p['source']]); + try{return $work();}finally{self::$native=null;self::$nativeActor=null;config(['workshop_stock.synthetic_sources'=>$old]);DB::setDefaultConnection($default);Schema::clearResolvedInstance('db.schema');Auth::shouldUse($guard);Auth::guard($guard)->setUser($actor);app()->instance('env',$environment);} + }); + }); + } +} diff --git a/app/Modules/Workshop/Http/WorkshopEquipmentController.php b/app/Modules/Workshop/Http/WorkshopEquipmentController.php new file mode 100644 index 0000000..1fe22e8 --- /dev/null +++ b/app/Modules/Workshop/Http/WorkshopEquipmentController.php @@ -0,0 +1,9 @@ +json($w->listing($r->user()->id,$r->query()))->header('Cache-Control','no-store, private');} + public function preview(Request $r,WorkshopEquipment $w){return response()->json($w->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))))->header('Cache-Control','no-store');} + public function confirm(Request $r,WorkshopEquipment $w){$d=$r->validate(['preview'=>'required|string|max:65536']);abort_if(array_diff(array_keys($r->all()),['preview']),422);try{return response()->json($w->confirm($r->user()->id,$d['preview']))->header('Cache-Control','no-store');}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Registo ocupado. Recupere a mesma confirmação.'],503)->header('Retry-After','5');}} +} diff --git a/app/Modules/Workshop/Http/WorkshopOrderReadController.php b/app/Modules/Workshop/Http/WorkshopOrderReadController.php new file mode 100644 index 0000000..433e9db --- /dev/null +++ b/app/Modules/Workshop/Http/WorkshopOrderReadController.php @@ -0,0 +1,8 @@ +json($reader->listing($request->user()->id,$request->query()))->header('Cache-Control','private, no-store');} +} diff --git a/app/Modules/Workshop/Http/WorkshopScheduleController.php b/app/Modules/Workshop/Http/WorkshopScheduleController.php new file mode 100644 index 0000000..a0d78e5 --- /dev/null +++ b/app/Modules/Workshop/Http/WorkshopScheduleController.php @@ -0,0 +1,13 @@ +json($fn())->header('Cache-Control','no-store, private');}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Agenda ocupada. Recupere a mesma confirmação.'],503)->header('Retry-After','5');}} + public function index(Request $r,WorkshopSchedule $w){return $this->response(fn()=>$w->listing($r->user()->id,$r->query()));} + public function options(Request $r,WorkshopSchedule $w){return $this->response(fn()=>$w->options($r->user()->id,$r->query()));} + public function preview(Request $r,WorkshopSchedule $w){return $this->response(fn()=>$w->preview($r->user()->id,$r->all(),strtolower((string)$r->header('Idempotency-Key'))));} + public function confirm(Request $r,WorkshopSchedule $w){$d=$r->validate(['preview'=>'required|string|max:65536']);abort_if(array_diff(array_keys($r->all()),['preview']),422);return $this->response(fn()=>$w->confirm($r->user()->id,$d['preview']));} + public function history(Request $r,WorkshopSchedule $w,string $key){$d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000']);abort_if(array_diff(array_keys($r->query()),['page']),422);return $this->response(fn()=>$w->history($r->user()->id,$key,(int)($d['page']??1)));} +} diff --git a/app/Modules/Workshop/Http/WorkshopStockController.php b/app/Modules/Workshop/Http/WorkshopStockController.php index b9b67f4..80d6c9c 100644 --- a/app/Modules/Workshop/Http/WorkshopStockController.php +++ b/app/Modules/Workshop/Http/WorkshopStockController.php @@ -7,7 +7,7 @@ final class WorkshopStockController { private function response(callable $read) { - try{return response()->json($read())->header('Cache-Control','no-store');} + try{return config('workshop_stock.composed_enabled')===true?app(\App\Modules\Workshop\ComposedWorkshopStockBook::class)->run(fn()=>response()->json($read())->header('Cache-Control','no-store')):response()->json($read())->header('Cache-Control','no-store');} catch(\Illuminate\Database\QueryException $e){ $code=$e->errorInfo[0]??null; if($code==='55P03')return response()->json(['code'=>'workshop_stock_busy','message'=>'Consumo ocupado por outra operação. Aguarde e repita manualmente; o estado será revalidado.'],423)->header('Cache-Control','no-store'); @@ -15,7 +15,7 @@ final class WorkshopStockController throw $e; } } - public function index(Request $r,WorkshopStock $stock){$d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000','proposal_page'=>'sometimes|integer|min:1|max:100000','status'=>'sometimes|in:all,pending,confirmed,withdrawn,rejected']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);return $this->response(fn()=>\Illuminate\Support\Facades\DB::transaction(function()use($stock,$d){\Illuminate\Support\Facades\DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');return $stock->listing((int)($d['page']??1),(int)($d['proposal_page']??1),$d['status']??'all');}));} + public function index(Request $r,WorkshopStock $stock){$d=$r->validate(['page'=>'sometimes|integer|min:1|max:100000','proposal_page'=>'sometimes|integer|min:1|max:100000','status'=>'sometimes|in:all,pending,confirmed,withdrawn,rejected']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);return $this->response(fn()=>\Illuminate\Support\Facades\DB::transaction(function()use($stock,$d){if(!\App\Modules\Workshop\ComposedWorkshopStockBook::active())\Illuminate\Support\Facades\DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');return $stock->listing((int)($d['page']??1),(int)($d['proposal_page']??1),$d['status']??'all');}));} public function withdraw(Request $r,int $id,WorkshopStock $stock){return $this->close($r,$id,$stock,'withdrawn');} public function reject(Request $r,int $id,WorkshopStock $stock){return $this->close($r,$id,$stock,'rejected');} private function close(Request $r,int $id,WorkshopStock $stock,string $state){$d=$r->validate(['state_version'=>'required|integer|min:1|max:100000','source_hash'=>'required|string|regex:/^[a-f0-9]{64}$/D','reason'=>'required|string|min:10|max:1000']);abort_if(array_diff(array_keys($r->all()),array_keys($d)),422);return $this->response(fn()=>$stock->close($id,$state,(int)$d['state_version'],$d['source_hash'],$d['reason']));} @@ -26,3 +26,4 @@ final class WorkshopStockController public function prepareReturn(Request $r,WorkshopStock $stock){return $this->delivery($r,$stock,false,true);} private function delivery(Request $r,WorkshopStock $stock,bool $partial,bool $returning=false){$d=$r->validate(['entry_id'=>'required|integer|min:1','version'=>'required|integer|min:1','warehouse_id'=>'required|integer|min:1','movement_id'=>'required|integer|min:1','reason'=>'required|string|min:10|max:1000','evidence'=>'required|string|min:3|max:500']);abort_if(array_diff(array_keys($r->all()),array_keys($d)),422);$key=(string)$r->header('Idempotency-Key');abort_unless(\Illuminate\Support\Str::isUuid($key),422,'Chave de repetição obrigatória.');return $this->response(fn()=>$stock->prepareDelivery($d,$key,$partial,$returning));} } + diff --git a/app/Modules/Workshop/WorkshopBilling.php b/app/Modules/Workshop/WorkshopBilling.php index 276b784..2009e79 100644 --- a/app/Modules/Workshop/WorkshopBilling.php +++ b/app/Modules/Workshop/WorkshopBilling.php @@ -26,6 +26,12 @@ final class WorkshopBilling { foreach(['clients.view','services.view'] as $right)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],$right);return $s; } private function order(int $id,array $s): object{return DB::table('wo_orders')->where($s)->where('id',$id)->first()??abort(403,'Ordem fora do contexto.');} + private function commercial(\Closure $work):mixed{ + if(config('sgf_single_installation.enabled')!==true)return $work(); + \App\Shared\Installation\CompositionRuntime::assert();$modules=app(\App\Shared\Context\ModuleNavigation::class)->available($this->context);abort_unless(in_array('commercial',array_column($modules,'id'),true),403,'Comercial não autorizado neste contexto.'); + $request=request();$old=$request->attributes->get('sgf_module');$request->attributes->set('sgf_module','commercial');try{return $work();}finally{if($old===null)$request->attributes->remove('sgf_module');else $request->attributes->set('sgf_module',$old);} + } + private function catalogue(string $table){return $this->commercial(fn()=>\App\Shared\Context\ModuleRecords::query($table));} private function source(int $id,int $version,int $order): object { $e=DB::table('wo_entries')->where(['id'=>$id,'version'=>$version,'order_id'=>$order])->first()??abort(409,'Origem/versão incompatível.');$e->data=json_decode($e->data,true); abort_unless(!$e->outside_authority&&!DB::table('wo_entries')->where('previous_id',$id)->exists()&&DB::table('wo_decisions')->where(['entry_id'=>$id,'entry_version'=>$version,'decision'=>'verified'])->exists(),409,'Origem não verificada ou desactualizada.'); @@ -35,13 +41,13 @@ final class WorkshopBilling { } private function compose(int $order,array $input,array $s,bool $allowClaim=false): array { $o=$this->order($order,$s);abort_unless($input['client_id']===$o->client_id&&$input['currency']==='MZN',409,'Cliente ou moeda incompatível.'); - $client=\App\Shared\Context\ModuleRecords::query('a_clients')->where(['id'=>$o->client_id,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->first()??abort(409); + $client=$this->catalogue('a_clients')->where(['id'=>$o->client_id,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->first()??abort(409); abort_unless(count($input['lines'])>0,422);$seen=[];$lines=[]; foreach($input['lines'] as $line){ $d=$this->exact($line,['entry_id'=>'required|integer|min:1','entry_version'=>'required|integer|min:1','service_id'=>'required|integer|min:1','service_version'=>'required|integer|min:1','price'=>'required|string','price_source'=>'required|string|min:3|max:120']); $e=$this->source($d['entry_id'],$d['entry_version'],$order);abort_if(isset($seen[$e->origin]),409,'Origem repetida.');$seen[$e->origin]=true; abort_if(!$allowClaim&&DB::table('wb_claims')->where(['order_id'=>$order,'origin'=>$e->origin])->exists(),409,'Origem já utilizada comercialmente.'); - $service=\App\Shared\Context\ModuleRecords::query('a_services')->where(['id'=>$d['service_id'],'version'=>$d['service_version'],'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->first()??abort(409,'Serviço/versão incompatível.'); + $service=$this->catalogue('a_services')->where(['id'=>$d['service_id'],'version'=>$d['service_version'],'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->first()??abort(409,'Serviço/versão incompatível.'); $price=DraftValues::decimal($d['price'],'price');abort_if($price===null,422,'Preço desconhecido não permite gerar.'); $quantity=$e->kind==='time'?Exact::text(Exact::hours($e->minutes)):$e->quantity;$quantity=DraftValues::decimal($quantity,'quantity',true); $lines[]=['entry_id'=>$e->id,'entry_version'=>$e->version,'origin'=>$e->origin,'task_id'=>$e->task_id,'source_kind'=>$e->kind,'source_quantity'=>$e->kind==='time'?(string)$e->minutes:$e->quantity,'unit'=>$e->kind==='time'?'H':$e->data['unit'],'service_id'=>$service->id,'service_version'=>$service->version,'quantity'=>$quantity,'unit_price'=>$price,'price_source'=>$d['price_source'],'description'=>"Oficina #{$order} · origem #{$e->id} v{$e->version} · ".($e->kind==='time'?'Trabalho efectivo':$e->data['resource'])]; @@ -57,7 +63,7 @@ final class WorkshopBilling { } public function prepare(int $order,int $actor,array $in,string $key): array { self::guard();$d=$this->exact($in,['version'=>'required|integer|min:1','previous_id'=>'present|nullable|integer|min:1','client_id'=>'required|integer|min:1','currency'=>'required|in:MZN','lines'=>'required|array|min:1|max:50','evidence'=>'required|string|min:3|max:1000']); - return $this->command($order,$actor,'prepare',$d,$key,function($s)use($order,$actor,$d){abort_if(DB::table('wb_generations')->where('order_id',$order)->exists(),409,'Ordem já convertida; exige correcção própria.');$last=DB::table('wb_proposals')->where('order_id',$order)->orderByDesc('version')->first();abort_unless($d['version']===($last?->version??0)+1&&$d['previous_id']===$last?->id,409,'Rever versão anterior.');$snapshot=$this->compose($order,$d,$s);$id=DB::table('wb_proposals')->insertGetId(['order_id'=>$order,'version'=>$d['version'],'previous_id'=>$last?->id,'snapshot'=>$this->json($snapshot),'actor_id'=>$actor,'created_at'=>now()]);return ['id'=>$id,'version'=>$d['version']];}); + return $this->command($order,$actor,'prepare',$d,$key,function($s)use($order,$actor,$d){abort_if(\Illuminate\Support\Facades\Schema::hasTable('cq_conversions')&&DB::table('cq_conversions')->where('source_order_id',$order)->exists(),409,'Ordem convertida por orçamento; não duplicar a facturação.');abort_if(DB::table('wb_generations')->where('order_id',$order)->exists(),409,'Ordem já convertida; exige correcção própria.');$last=DB::table('wb_proposals')->where('order_id',$order)->orderByDesc('version')->first();abort_unless($d['version']===($last?->version??0)+1&&$d['previous_id']===$last?->id,409,'Rever versão anterior.');$snapshot=$this->compose($order,$d,$s);$id=DB::table('wb_proposals')->insertGetId(['order_id'=>$order,'version'=>$d['version'],'previous_id'=>$last?->id,'snapshot'=>$this->json($snapshot),'actor_id'=>$actor,'created_at'=>now()]);return ['id'=>$id,'version'=>$d['version']];}); } private function proposal(int $order,array $d): object {$p=DB::table('wb_proposals')->where(['id'=>$d['id'],'order_id'=>$order,'version'=>$d['version']])->first()??abort(409,'Proposta/versão incompatível.');abort_if(DB::table('wb_proposals')->where('previous_id',$p->id)->exists(),409,'Proposta desactualizada.');return $p;} private function recheck(object $p,array $s,bool $allowClaim=false): array { @@ -69,11 +75,10 @@ final class WorkshopBilling { } public function generate(int $order,int $actor,array $in,string $key): array { self::guard();$d=$this->exact($in,['id'=>'required|integer|min:1','version'=>'required|integer|min:1']); - return $this->command($order,$actor,'generate',$d,$key,function($s)use($order,$actor,$d,$key){$p=$this->proposal($order,$d);abort_unless(DB::table('wb_decisions')->where(['proposal_id'=>$p->id,'version'=>$p->version,'decision'=>'approved'])->exists(),409,'Revisão exacta necessária.');abort_if(DB::table('wb_generations')->where('order_id',$order)->exists(),409,'Ordem já convertida.');$snap=$this->recheck($p,$s); + return $this->command($order,$actor,'generate',$d,$key,function($s)use($order,$actor,$d,$key){$p=$this->proposal($order,$d);abort_unless(DB::table('wb_decisions')->where(['proposal_id'=>$p->id,'version'=>$p->version,'decision'=>'approved'])->exists(),409,'Revisão exacta necessária.');abort_if(\Illuminate\Support\Facades\Schema::hasTable('cq_conversions')&&DB::table('cq_conversions')->where('source_order_id',$order)->exists(),409,'Ordem convertida por orçamento; não duplicar a facturação.');abort_if(DB::table('wb_generations')->where('order_id',$order)->exists(),409,'Ordem já convertida.');$snap=$this->recheck($p,$s); $this->workflow->scope('service_drafts',$actor,'create');$this->workflow->scope('billing_drafts',$actor,'create'); $lines=array_map(fn($l)=>array_intersect_key($l,array_flip(['service_id','quantity','unit_price','description'])),$snap['lines']);$name='Oficina #'.$order.' · proposta #'.$p->id.' v'.$p->version; - $service=$this->workflow->save('service_drafts',null,$actor,['client_id'=>$snap['client_id'],'name'=>$name,'currency'=>'MZN','lines'=>$lines,'description'=>'Origem rastreável de oficina; sem validade fiscal.'],substr(hash('sha256','wb-service-'.$key),0,40)); - $billing=$this->workflow->save('billing_drafts',null,$actor,['source_id'=>$service['id'],'name'=>$name],substr(hash('sha256','wb-billing-'.$key),0,40)); + [$service,$billing]=$this->commercial(function()use($actor,$snap,$name,$lines,$key){$service=$this->workflow->save('service_drafts',null,$actor,['client_id'=>$snap['client_id'],'name'=>$name,'currency'=>'MZN','lines'=>$lines,'description'=>'Origem rastreável de oficina; sem validade fiscal.'],substr(hash('sha256','wb-service-'.$key),0,40));$billing=$this->workflow->save('billing_drafts',null,$actor,['source_id'=>$service['id'],'name'=>$name],substr(hash('sha256','wb-billing-'.$key),0,40));return [$service,$billing];}); $id=DB::table('wb_generations')->insertGetId(['order_id'=>$order,'proposal_id'=>$p->id,'service_draft_id'=>$service['id'],'billing_draft_id'=>$billing['id'],'actor_id'=>$actor,'created_at'=>now()]);foreach($snap['lines'] as $l)DB::table('wb_claims')->insert(['generation_id'=>$id,'order_id'=>$order,'origin'=>$l['origin'],'entry_id'=>$l['entry_id'],'entry_version'=>$l['entry_version']]);return ['id'=>$id,'version'=>$p->version,'service_draft_id'=>$service['id'],'billing_draft_id'=>$billing['id'],'base_exact'=>$snap['base_exact']];}); } private function generationState(int $order): ?object { @@ -86,6 +91,7 @@ final class WorkshopBilling { $s=$this->scope($actor,'view');if(!ServiceCalculation::isolated())return ['blocked'=>true,'orders'=>[]];self::guard(); return DB::transaction(function()use($s){DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();$orders=[];foreach(DB::table('wo_orders')->where($s)->get() as $o){$entries=[];foreach(DB::table('wo_entries')->where('order_id',$o->id)->whereIn('kind',['time','material'])->get() as $e){try{$entries[]=$this->source($e->id,$e->version,$o->id);}catch(\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface){}} $proposals=DB::table('wb_proposals')->where('order_id',$o->id)->orderBy('version')->get()->all();foreach($proposals as $p){$p->snapshot=json_decode($p->snapshot,true);$p->decision=DB::table('wb_decisions')->where('proposal_id',$p->id)->first();$p->current=!DB::table('wb_proposals')->where('previous_id',$p->id)->exists();if($p->current){try{$this->recheck($p,$s,true);}catch(\Throwable){$p->current=false;}}} - $orders[]=(array)$o+['entries'=>$entries,'proposals'=>$proposals,'generation'=>$this->generationState($o->id)];}return ['blocked'=>false,'orders'=>$orders,'services'=>\App\Shared\Context\ModuleRecords::query('a_services')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->get()->all()];}); + $orders[]=(array)$o+['entries'=>$entries,'proposals'=>$proposals,'generation'=>$this->generationState($o->id)];}return ['blocked'=>false,'orders'=>$orders,'services'=>$this->catalogue('a_services')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->get()->all()];}); } } + diff --git a/app/Modules/Workshop/WorkshopEquipment.php b/app/Modules/Workshop/WorkshopEquipment.php new file mode 100644 index 0000000..6dfee7c --- /dev/null +++ b/app/Modules/Workshop/WorkshopEquipment.php @@ -0,0 +1,20 @@ +context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($s['unit_id']&&$actor===$this->context->userId()&&ModuleRecords::active()==='workshop',403);$this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(['view',...($write?['open']:[])] as $right)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'workshop.'.$right,$s['unit_id']);return $s;} + private function manager(int $actor,array $s):bool{foreach(['open','assign','authorize','verify'] as $right)if($this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'workshop.'.$right,$s['unit_id']))return true;return false;} + private function query(int $actor,array $s){$q=DB::table('wo_equipment as e')->where(['e.tenant_id'=>$s['tenant_id'],'e.company_id'=>$s['company_id']])->whereIn('e.client_id',ModuleRecords::query('a_clients')->select('id'));if(!$this->manager($actor,$s))$q->whereIn('e.id',DB::table('wo_orders as o')->where(['o.tenant_id'=>$s['tenant_id'],'o.company_id'=>$s['company_id'],'o.unit_id'=>$s['unit_id']])->whereRaw("(SELECT a.technician_id FROM wo_entries a WHERE a.order_id=o.id AND a.kind='assignment' ORDER BY a.id DESC LIMIT 1)=?",[$actor])->select('o.equipment_id'));return $q;} + private function hash(mixed $v):string{return hash('sha256',json_encode($v,JSON_THROW_ON_ERROR));} + public function listing(int $actor,array $input):array{$s=$this->scope($actor);$rules=['view'=>'sometimes|in:equipment,clients','search'=>'sometimes|nullable|string|max:100','client_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();$clients=($d['view']??'equipment')==='clients';if($clients){$this->scope($actor,true);$q=ModuleRecords::query('a_clients')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']]);$fields=['id','name','version'];}else{$q=$this->query($actor,$s)->join('a_clients as c','c.id','=','e.client_id');if(isset($d['client_id']))$q->where('e.client_id',$d['client_id']);$fields=['e.*','c.name as client_name'];}if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';if($clients)$q->whereRaw("name ILIKE ? ESCAPE '!'",[$needle]);else$q->where(fn($x)=>$x->whereRaw("e.identifier ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("e.description ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("e.serial ILIKE ? ESCAPE '!'",[$needle]));}$p=$q->orderBy($clients?'id':'e.id','desc')->paginate((int)($d['per_page']??25),$fields,'page',(int)($d['page']??1));return ['records'=>$p,'can_create'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'workshop.open',$s['unit_id']),'immutable_identification'=>true];} + public function visible(int $actor,int $id):array + { + $s=$this->scope($actor);$row=$this->query($actor,$s)->where('e.id',$id)->join('a_clients as client',fn($j)=>$j->on('client.id','=','e.client_id')->on('client.tenant_id','=','e.tenant_id')->on('client.company_id','=','e.company_id'))->first(['e.id','e.client_id','e.identifier','e.description','e.serial','client.name as client_name']);abort_unless($row,403,'Equipamento fora do catálogo autorizado.');return(array)$row; + } + private function data(array $input):array{foreach(['identifier','description','serial','reason'] as $key)if(isset($input[$key])&&is_string($input[$key]))$input[$key]=trim($input[$key]);$rules=['client_id'=>'required|integer|min:1','identifier'=>'required|string|min:2|max:100','description'=>'required|string|min:3|max:300','serial'=>'nullable|string|max:100','reason'=>'required|string|min:10|max:2000'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();return ['client_id'=>(int)$d['client_id'],'identifier'=>trim($d['identifier']),'description'=>trim($d['description']),'serial'=>isset($d['serial'])&&trim($d['serial'])!==''?trim($d['serial']):null,'reason'=>trim($d['reason'])];} + private function client(array $s,array $d):object{return ModuleRecords::query('a_clients')->where(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'id'=>$d['client_id']])->first()??abort(403,'Cliente sem associação explícita ao módulo e à empresa.');} + public function preview(int $actor,array $input,string $key):array{$s=$this->scope($actor,true);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422);$d=$this->data($input);$c=$this->client($s,$d);abort_if(DB::table('wo_equipment')->where(['company_id'=>$s['company_id'],'identifier'=>$d['identifier']])->exists(),409,'Identificação existente. Reutilize o equipamento.');$scope=['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'unit_id'=>$s['unit_id']];$cut=['actor'=>$actor,'scope'=>$scope,'key'=>$key,'data'=>$d,'client_version'=>(int)$c->version,'generation'=>$this->hash(request()->session()->get('company_context',[])),'observed'=>time()];return ['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>['client'=>$c->name,'identifier'=>$d['identifier'],'description'=>$d['description'],'serial'=>$d['serial'],'creates_order'=>false,'financial_effect'=>false]];} + public function confirm(int $actor,string $token):array{try{$cut=json_decode(Crypt::decryptString($token),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$s=$this->scope($actor,true);return DB::transaction(function()use($actor,$cut,$s){DB::statement("SET LOCAL lock_timeout='5s'");DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();$this->scope($actor,true);DB::table('permission_grants')->where($s+['user_id'=>$actor])->lockForUpdate()->get();abort_unless(($cut['actor']??null)===$actor&&($cut['scope']??null)===$s&&($cut['generation']??null)===$this->hash(request()->session()->get('company_context',[]))&&($cut['observed']??0)>=time()-600&&$cut['observed']<=time(),409,'Contexto/prévia alterados.');$d=$this->data($cut['data']);$c=$this->client($s,$d);$old=DB::table('wo_equipment_commands')->where($s)->where(['actor_id'=>$actor,'command_key'=>$cut['key']])->first();if($old){abort_unless($old->request_hash===$this->hash($d),409);return ['id'=>$old->equipment_id,'replayed'=>true];}abort_unless((int)$c->version===$cut['client_version'],409,'Cliente alterado. Confira novamente.');abort_if(DB::table('wo_equipment')->where(['company_id'=>$s['company_id'],'identifier'=>$d['identifier']])->exists(),409,'Identificação já registada.');$id=DB::table('wo_equipment')->insertGetId(['tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id'],'client_id'=>$d['client_id'],'identifier'=>$d['identifier'],'description'=>$d['description'],'serial'=>$d['serial'],'actor_id'=>$actor,'created_at'=>now()]);DB::table('wo_equipment_commands')->insert($s+['actor_id'=>$actor,'command_key'=>$cut['key'],'request_hash'=>$this->hash($d),'equipment_id'=>$id,'reason'=>$d['reason'],'created_at'=>now()]);Audit::record('workshop.equipment.registered',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['record_id'=>$id,'reason'=>$d['reason']]);return ['id'=>$id,'replayed'=>false];});} +} diff --git a/app/Modules/Workshop/WorkshopOrderRead.php b/app/Modules/Workshop/WorkshopOrderRead.php new file mode 100644 index 0000000..e536aae --- /dev/null +++ b/app/Modules/Workshop/WorkshopOrderRead.php @@ -0,0 +1,45 @@ +context->identifiers()+['unit_id'=>$this->context->unitId()]; + abort_unless($s['unit_id']&&$actor===$this->context->userId()&&ModuleRecords::active()==='workshop',403); + $this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']); + $this->permissions->require($actor,$s['tenant_id'],$s['company_id'],'workshop.view',$s['unit_id']); + $rules=['equipment_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','search'=>'sometimes|nullable|string|max:100','state'=>'sometimes|in:open,in_review,exception_review,records_verified','from'=>'required_with:to|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2100-12-31','to'=>'required_with:from|date_format:Y-m-d|after_or_equal:from|before_or_equal:2100-12-31']; + abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate(); + $manager=false;foreach(['open','assign','authorize','verify']as$right)$manager=$manager||$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'workshop.'.$right,$s['unit_id']); + return DB::transaction(function()use($s,$actor,$manager,$d){ + if(DB::transactionLevel()===1)DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY'); + $equipment=isset($d['equipment_id'])?app(WorkshopEquipment::class)->visible($actor,(int)$d['equipment_id']):null; + $current="NOT EXISTS(SELECT 1 FROM wo_entries newer WHERE newer.previous_id=e.id)"; + $points="SELECT 1 FROM wo_entries e LEFT JOIN wo_decisions decision ON decision.entry_id=e.id WHERE e.order_id=o.id AND e.kind IN ('task','time','material') AND $current"; + $state="CASE WHEN NOT EXISTS($points) THEN 'open' WHEN EXISTS($points AND e.outside_authority=true) THEN 'exception_review' WHEN EXISTS($points AND (decision.id IS NULL OR decision.decision<>'verified')) THEN 'in_review' ELSE 'records_verified' END"; + $assignment="(SELECT a.technician_id FROM wo_entries a WHERE a.order_id=o.id AND a.kind='assignment' ORDER BY a.id DESC LIMIT 1)"; + $q=DB::table('wo_orders as o')->where(array_combine(array_map(fn($k)=>'o.'.$k,array_keys($s)),array_values($s))) + ->leftJoin('wo_equipment as equipment',fn($j)=>$j->on('equipment.id','=','o.equipment_id')->on('equipment.company_id','=','o.company_id')->on('equipment.tenant_id','=','o.tenant_id')) + ->leftJoin('a_clients as client',fn($j)=>$j->on('client.id','=','o.client_id')->on('client.company_id','=','o.company_id')->on('client.tenant_id','=','o.tenant_id')) + ->leftJoin('users as technician',fn($j)=>$j->whereRaw('technician.id='.$assignment)); + $period=null;if(isset($d['from'])){$start=\Carbon\CarbonImmutable::createFromFormat('!Y-m-d',$d['from'],WorkshopWorkflow::ZONE);$end=\Carbon\CarbonImmutable::createFromFormat('!Y-m-d',$d['to'],WorkshopWorkflow::ZONE)->addDay();$q->where('o.created_at','>=',$start->utc()->format('Y-m-d H:i:sP'))->where('o.created_at','<',$end->utc()->format('Y-m-d H:i:sP'));$period=['from'=>$d['from'],'to'=>$d['to'],'zone'=>WorkshopWorkflow::ZONE,'basis'=>'order_created_at_current_records','start_utc'=>$start->utc()->toIso8601String(),'end_utc_exclusive'=>$end->utc()->toIso8601String()];} + if($equipment)$q->where('o.equipment_id',$equipment['id']); + if(!$manager)$q->whereRaw($assignment.'=?',[$actor]); + if(isset($d['state']))$q->whereRaw("($state)=?",[$d['state']]); + if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$q->where(function($x)use($needle){foreach(['o.reference','equipment.identifier','equipment.description','equipment.serial','client.name','technician.name']as$i=>$column)$x->{$i?'orWhereRaw':'whereRaw'}("$column ILIKE ? ESCAPE '!'",[$needle]);});} + $p=$q->orderByDesc('o.id')->paginate((int)($d['per_page']??25),['o.id','o.reference','o.created_at','o.client_id','o.equipment_id','client.name as client','equipment.identifier as equipment','equipment.description','equipment.serial','technician.name as technician',DB::raw("$assignment as technician_id"),DB::raw("$state as state")],'page',(int)($d['page']??1)); + $stats=DB::table('wo_entries as e')->leftJoin('wo_decisions as decision','decision.entry_id','=','e.id')->whereIn('e.order_id',$p->pluck('id')->all())->whereRaw($current)->groupBy('e.order_id')->selectRaw("e.order_id,count(*) FILTER(WHERE e.kind IN ('task','time','material')) points,count(*) FILTER(WHERE e.kind IN ('task','time','material') AND (decision.id IS NULL OR decision.decision<>'verified')) points_not_verified,coalesce(sum(e.minutes) FILTER(WHERE e.kind='time'),0) minutes_reported,coalesce(sum(e.minutes) FILTER(WHERE e.kind='time' AND decision.decision='verified'),0) minutes_verified")->get()->keyBy('order_id'); + foreach($p->items()as$row){$statsRow=$stats->get($row->id);foreach(['points','points_not_verified','minutes_reported','minutes_verified']as$key)$row->$key=(int)($statsRow?->$key??0);} + return ['equipment'=>$equipment,'orders'=>$p,'observed_at'=>now()->utc()->toIso8601String(),'states_are'=>'current_resource_records_not_delivery_or_payment','read_only'=>true,'opening_period'=>$period,'notice'=>'Estado dos apontamentos actuais; não equivale a conclusão, entrega, facturação ou pagamento.']; + }); + } +} diff --git a/app/Modules/Workshop/WorkshopSchedule.php b/app/Modules/Workshop/WorkshopSchedule.php new file mode 100644 index 0000000..6f713ab --- /dev/null +++ b/app/Modules/Workshop/WorkshopSchedule.php @@ -0,0 +1,88 @@ +context->identifiers()+['unit_id'=>$this->context->unitId()];abort_unless($actor===$this->context->userId()&&$s['unit_id']!==null&&ModuleRecords::active()==='workshop',403); + $this->context->selectFor($actor,$s['tenant_id'],$s['company_id'],$s['unit_id']);foreach(['company.view'=>null,'unit.view'=>$s['unit_id'],'workshop.view'=>$s['unit_id']]+($write?['workshop.assign'=>$s['unit_id']]:[]) as $right=>$unit)$this->permissions->require($actor,$s['tenant_id'],$s['company_id'],$right,$unit);return $s; + } + private function manager(int $actor,array $s):bool{foreach(['open','assign','authorize','verify'] as $right)if($this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'workshop.'.$right,$s['unit_id']))return true;return false;} + private function orders(int $actor,array $s) + { + $q=DB::table('wo_orders as o')->where(['o.tenant_id'=>$s['tenant_id'],'o.company_id'=>$s['company_id'],'o.unit_id'=>$s['unit_id']])->whereIn('o.client_id',ModuleRecords::query('a_clients')->select('id')); + if(!$this->manager($actor,$s))$q->whereRaw("(SELECT e.technician_id FROM wo_entries e WHERE e.order_id=o.id AND e.kind='assignment' ORDER BY e.id DESC LIMIT 1)=?",[$actor]);return $q; + } + private function current(array $s){return DB::table('wo_schedule_events as b')->where(['b.tenant_id'=>$s['tenant_id'],'b.company_id'=>$s['company_id'],'b.unit_id'=>$s['unit_id']])->whereNotExists(fn($q)=>$q->selectRaw('1')->from('wo_schedule_events as n')->whereColumn('n.previous_id','b.id'));} + private function json(mixed $v):string{if(is_array($v)&&!array_is_list($v)){ksort($v);foreach($v as &$x)if(is_array($x))$x=json_decode($this->json($x),true);}return json_encode($v,JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR);} + private function hash(mixed $v):string{return hash('sha256',$this->json($v));} + private function time(string $value):CarbonImmutable + { + abort_unless(preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})$/D',$value),422,'Indique data/hora com zona explícita.');try{$d=new \DateTimeImmutable($value);$errors=\DateTimeImmutable::getLastErrors();abort_if($errors&&($errors['warning_count']||$errors['error_count']),422);return CarbonImmutable::instance($d)->utc();}catch(\Exception){abort(422,'Data/hora inválida.');} + } + private function normalize(array $input):array + { + if(isset($input['reason'])&&is_string($input['reason']))$input['reason']=trim($input['reason']);$rules=['action'=>'required|in:book,reschedule,cancel','booking_key'=>'required|uuid','version'=>'required|integer|min:0','order_id'=>'required|integer|min:1','technician_id'=>'required|integer|min:1','starts_at'=>'required|string|max:35','ends_at'=>'required|string|max:35','reason'=>'required|string|min:10|max:2000'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();foreach(['version','order_id','technician_id'] as $key)$d[$key]=(int)$d[$key];$d['booking_key']=strtolower($d['booking_key']);$start=$this->time($d['starts_at']);$end=$this->time($d['ends_at']);abort_unless($end>$start&&$end<=$start->addDays(7),422,'Fim deve ser posterior e o intervalo não pode exceder sete dias.');$d['starts_at']=$start->toIso8601String();$d['ends_at']=$end->toIso8601String();return $d; + } + private function basis(int $actor,array $s,array $d):array + { + $order=$this->orders($actor,$s)->where('o.id',$d['order_id'])->first();abort_unless($order,403,'Ordem fora do âmbito autorizado.'); + abort_unless(ModuleRecords::query('a_clients')->where(['id'=>$order->client_id,'tenant_id'=>$s['tenant_id'],'company_id'=>$s['company_id']])->exists(),403,'Cliente sem associação explícita a Oficinas.'); + $assignment=DB::table('wo_entries')->where(['order_id'=>$order->id,'kind'=>'assignment'])->orderByDesc('id')->first(); + if($d['action']!=='cancel'){abort_unless($assignment&&(int)$assignment->technician_id===$d['technician_id'],409,'Atribua primeiro o técnico na ordem; agendar não altera essa decisão.');$this->permissions->require($d['technician_id'],$s['tenant_id'],$s['company_id'],'workshop.record',$s['unit_id']);} + $old=$this->current($s)->where('booking_key',$d['booking_key'])->first(); + abort_unless(($old?(int)$old->version:0)===$d['version'],409,'Agendamento alterado. Confira novamente.'); + abort_unless($d['action']==='book'?!$old:($old&&$old->state==='scheduled'&&$old->order_id===$order->id),409,'Agendamento incompatível ou cancelado.'); + if($d['action']==='cancel')abort_unless($d['starts_at']===CarbonImmutable::parse($old->starts_at)->utc()->toIso8601String()&&$d['ends_at']===CarbonImmutable::parse($old->ends_at)->utc()->toIso8601String()&&$d['technician_id']===$old->technician_id,409,'Cancelar conserva o intervalo e técnico originais.'); + $conflicts=$d['action']==='cancel'?[]:$this->current($s)->where('booking_key','<>',$d['booking_key'])->where('state','scheduled')->where(fn($q)=>$q->where('technician_id',$d['technician_id'])->orWhere('equipment_id',$order->equipment_id))->where('starts_at','<',$d['ends_at'])->where('ends_at','>',$d['starts_at'])->orderBy('id')->limit(10)->get(['id','booking_key','technician_id','equipment_id','starts_at','ends_at'])->all(); + $revision=(int)DB::selectOne('SELECT sgf_wo_revision(?) v',[$order->id])->v; + return ['order'=>(array)$order,'assignment'=>$assignment?->id,'order_revision'=>$revision,'previous'=>$old?(array)$old:null,'conflicts'=>$conflicts]; + } + private function locked(int $actor,\Closure $fn):mixed + { + $s=$this->scope($actor,true);return DB::transaction(function()use($actor,$s,$fn){DB::statement("SET LOCAL lock_timeout='5s'");DB::table('companies')->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();DB::table('users')->where('id',$actor)->lockForUpdate()->firstOrFail();$this->scope($actor,true);DB::table('permission_grants')->where($s+['user_id'=>$actor])->lockForUpdate()->get();return $fn($s);}); + } + private function impact(array $d,array $basis):array{return ['action'=>$d['action'],'order_reference'=>$basis['order']['reference'],'technician_id'=>$d['technician_id'],'equipment_id'=>$basis['order']['equipment_id'],'starts_at'=>$d['starts_at'],'ends_at'=>$d['ends_at'],'state'=>$d['action']==='cancel'?'cancelled':'scheduled','conflicts'=>$basis['conflicts'],'stock_effect'=>false,'financial_effect'=>false,'external_messages'=>false];} + public function preview(int $actor,array $input,string $key):array + { + abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key),422,'Use uma chave UUID.');$d=$this->normalize($input); + return $this->locked($actor,function($s)use($actor,$d,$key){abort_if(DB::table('wo_schedule_events')->where($s)->where(['actor_id'=>$actor,'command_key'=>$key])->exists(),409,'Consulte o recibo da chave já usada.');$basis=$this->basis($actor,$s,$d);abort_if($basis['conflicts']!==[],409,'Técnico ou equipamento ocupado neste intervalo.');$impact=$this->impact($d,$basis);$cut=['actor'=>$actor,'scope'=>$s,'input'=>$d,'key'=>$key,'revision'=>$this->hash($basis),'generation'=>$this->hash(request()->session()->get('company_context',[])),'observed'=>time(),'impact'=>$impact];return ['preview'=>Crypt::encryptString($this->json($cut)),'revision'=>$cut['revision'],'impact'=>$impact];}); + } + public function confirm(int $actor,string $preview):array + { + try{$cut=json_decode(Crypt::decryptString($preview),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');} + return $this->locked($actor,function($s)use($actor,$cut){abort_unless(($cut['actor']??null)===$actor&&$this->hash($cut['scope']??null)===$this->hash($s)&&($cut['generation']??null)===$this->hash(request()->session()->get('company_context',[]))&&($cut['observed']??0)>=time()-600&&$cut['observed']<=time(),409,'Contexto/prévia alterados ou expirados.');$d=$this->normalize($cut['input']);$hash=$this->hash($d);$old=DB::table('wo_schedule_events')->where($s)->where(['actor_id'=>$actor,'command_key'=>$cut['key']])->first(); + if($old){abort_unless(hash_equals($old->request_hash,$hash)&&$this->orders($actor,$s)->where('o.id',$old->order_id)->exists(),409);return $this->result($old)+['replayed'=>true];} + $basis=$this->basis($actor,$s,$d);abort_unless(hash_equals($this->hash($basis),$cut['revision'])&&$basis['conflicts']===[],409,'Disponibilidade, ordem ou versão mudou. Faça nova conferência.');abort_unless($this->hash($this->impact($d,$basis))===$this->hash($cut['impact']),409); + $id=DB::table('wo_schedule_events')->insertGetId($s+['booking_key'=>$d['booking_key'],'version'=>$d['version']+1,'previous_id'=>$basis['previous']['id']??null,'order_id'=>$d['order_id'],'equipment_id'=>$basis['order']['equipment_id'],'technician_id'=>$d['technician_id'],'starts_at'=>$d['starts_at'],'ends_at'=>$d['ends_at'],'state'=>$d['action']==='cancel'?'cancelled':'scheduled','reason'=>$d['reason'],'actor_id'=>$actor,'command_key'=>$cut['key'],'request_hash'=>$hash,'source_revision'=>$cut['revision'],'snapshot'=>$this->json(['before'=>$basis,'after'=>$cut['impact']]),'created_at'=>now()]);Audit::record('workshop.schedule.'.$d['action'],$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['booking_key'=>$d['booking_key'],'record_id'=>$id,'version'=>$d['version']+1,'reason'=>$d['reason']]);return $this->result(DB::table('wo_schedule_events')->where('id',$id)->first())+['replayed'=>false]; + }); + } + private function result(object $r):array{return ['id'=>$r->id,'booking_key'=>$r->booking_key,'version'=>$r->version,'state'=>$r->state,'order_id'=>$r->order_id,'starts_at'=>$r->starts_at,'ends_at'=>$r->ends_at];} + public function listing(int $actor,array $input):array + { + $s=$this->scope($actor);$rules=['from'=>'required|date_format:Y-m-d','to'=>'required|date_format:Y-m-d|after_or_equal:from','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','state'=>'sometimes|in:scheduled,cancelled','technician_id'=>'sometimes|integer|min:1','search'=>'sometimes|nullable|string|max:100','order_id'=>'sometimes|integer|min:1'];abort_if(array_diff(array_keys($input),array_keys($rules)),422);$d=Validator::make($input,$rules)->validate();$from=CarbonImmutable::parse($d['from'],WorkshopWorkflow::ZONE)->startOfDay();$to=CarbonImmutable::parse($d['to'],WorkshopWorkflow::ZONE)->addDay()->startOfDay();abort_if($from->diffInDays($to)>93,422,'Escolha no máximo93dias.'); + $query=$this->current($s)->whereIn('b.order_id',$this->orders($actor,$s)->select('o.id'))->join('wo_orders as o','o.id','=','b.order_id')->join('users as u','u.id','=','b.technician_id')->join('wo_equipment as e','e.id','=','b.equipment_id')->where('b.starts_at','<',$to->utc()->toIso8601String())->where('b.ends_at','>',$from->utc()->toIso8601String());foreach(['state','technician_id','order_id'] as $key)if(isset($d[$key]))$query->where('b.'.$key,$d[$key]);if(($d['search']??'')!==''){$needle='%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%';$query->where(fn($q)=>$q->whereRaw("o.reference ILIKE ? ESCAPE '!'",[$needle])->orWhereRaw("e.identifier ILIKE ? ESCAPE '!'",[$needle]));} + $page=$query->orderBy('b.starts_at')->orderBy('b.id')->paginate((int)($d['per_page']??25),['b.*','o.reference','e.identifier as equipment','u.name as technician'],'page',(int)($d['page']??1)); + $now=CarbonImmutable::now('UTC');foreach($page->items() as $row)$row->reminder=$row->state!=='scheduled'?'cancelled':(CarbonImmutable::parse($row->ends_at)<$now?'elapsed':(CarbonImmutable::parse($row->starts_at)<=$now?'in_progress':(CarbonImmutable::parse($row->starts_at)<=$now->addDay()?'within24h':'planned'))); + return ['bookings'=>$page,'can_manage'=>$this->permissions->has($actor,$s['tenant_id'],$s['company_id'],'workshop.assign',$s['unit_id']),'zone'=>WorkshopWorkflow::ZONE,'reminders'=>'local_only_no_messages','notice'=>'Agendar não autoriza trabalho nem altera a ordem.']; + } + public function options(int $actor,array $input):array + { + $s=$this->scope($actor);$d=Validator::make($input,['search'=>'sometimes|nullable|string|max:100','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100'])->validate();abort_if(array_diff(array_keys($input),array_keys($d)),422);$q=$this->orders($actor,$s)->join('wo_equipment as e','e.id','=','o.equipment_id');if(($d['search']??'')!=='')$q->whereRaw("o.reference ILIKE ? ESCAPE '!'",['%'.str_replace(['!','%','_'],['!!','!%','!_'],$d['search']).'%']); + $p=$q->orderByDesc('o.id')->paginate((int)($d['per_page']??25),['o.id','o.reference','e.identifier as equipment'],'page',(int)($d['page']??1));$ids=$p->pluck('id')->all();$assigned=DB::table('wo_entries as a')->join('users as u','u.id','=','a.technician_id')->whereIn('a.order_id',$ids)->where('a.kind','assignment')->whereNotExists(fn($x)=>$x->selectRaw('1')->from('wo_entries as n')->whereColumn('n.order_id','a.order_id')->where('n.kind','assignment')->whereColumn('n.id','>','a.id'))->get(['a.order_id','a.technician_id','u.name'])->keyBy('order_id');foreach($p->items() as $row)$row->assignment=$assigned->get($row->id);return ['orders'=>$p]; + } + public function history(int $actor,string $key,int $page=1):array + { + $s=$this->scope($actor);abort_unless(preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/D',$key)&&$page>0&&$page<=100000,422);$q=DB::table('wo_schedule_events')->where($s)->where('booking_key',$key)->whereIn('order_id',$this->orders($actor,$s)->select('o.id'));abort_unless((clone$q)->exists(),403);return ['events'=>$q->orderByDesc('version')->paginate(25,['*'],'page',$page)]; + } +} diff --git a/app/Modules/Workshop/WorkshopStock.php b/app/Modules/Workshop/WorkshopStock.php index e8be0dc..e1d3c3d 100644 --- a/app/Modules/Workshop/WorkshopStock.php +++ b/app/Modules/Workshop/WorkshopStock.php @@ -23,29 +23,29 @@ final class WorkshopStock private function binding(array $s): array { $b=config('workshop_stock.synthetic_sources.'.implode(':',$s)); - abort_unless($b&&preg_match('/^sgf_stock_read_[a-f0-9]{12}$/D',$b['schema']??'')&&!isset($b['connection']) + abort_unless($b&&(preg_match('/^sgf_stock_read_[a-f0-9]{12}$/D',$b['schema']??'')||(ComposedWorkshopStockBook::active()&&($b['schema']??null)==='public'))&&!isset($b['connection']) &&($b['lots']??true)===false&&($b['reservations']??true)===false&&($b['spare_parts']??true)===false,503,'Fonte local de consumo ainda não ligada ou fora do recorte sem lotes/reservas/peças.');return $b; } private function lock(array $s): void {DB::table('companies')->where('tenant_id',$s['tenant_id'])->where('id',$s['company_id'])->lockForUpdate()->firstOrFail();} private function allocationTable(array $b): ?string { $table=$b['schema'].'.ws_allocations'; - if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=DB::getSchemaBuilder()->hasTable($table); + if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=ComposedWorkshopStockBook::hasTable($table); return $this->allocationTables[$table]?$table:null; } private function returnTable(array $b): ?string { - $table=$b['schema'].'.ws_returns';if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=DB::getSchemaBuilder()->hasTable($table);return $this->allocationTables[$table]?$table:null; + $table=$b['schema'].'.ws_returns';if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=ComposedWorkshopStockBook::hasTable($table);return $this->allocationTables[$table]?$table:null; } private function closureTable(array $b): ?string { $table=$b['schema'].'.ws_closures'; - if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=DB::getSchemaBuilder()->hasTable($table); + if(!array_key_exists($table,$this->allocationTables))$this->allocationTables[$table]=ComposedWorkshopStockBook::hasTable($table); return $this->allocationTables[$table]?$table:null; } private function closure(array $s,array $b,int $id): ?object { - return ($table=$this->closureTable($b))?DB::table($table)->where($s)->where('proposal_id',$id)->first():null; + return ($table=$this->closureTable($b))?ComposedWorkshopStockBook::table($table)->where($s)->where('proposal_id',$id)->first():null; } private function proposalResult(array $s,array $b,object $p): array { @@ -61,7 +61,7 @@ final class WorkshopStock $right=$action==='withdrawn'?'prepare':'confirm';$s=$this->scope($right);$b=$this->binding($s);$actor=$this->context->userId(); return DB::transaction(function()use($id,$action,$version,$sourceHash,$reason,$right,$s,$b,$actor){ $this->lock($s);$this->scope($right);$table=$this->closureTable($b);abort_unless($table,503,'Encerramento de propostas ainda não instalado nesta fonte.'); - $p=DB::table($b['schema'].'.ws_proposals')->where($s)->where('id',$id)->lockForUpdate()->first();abort_unless($p,403); + $p=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->where($s)->where('id',$id)->lockForUpdate()->first();abort_unless($p,403); abort_unless($action==='withdrawn'?(int)$p->preparer_id===$actor:(int)$p->preparer_id!==$actor,403,'Retirada exige o preparador; recusa exige outra identidade autorizada.'); abort_unless($version===1&&hash_equals($p->source_hash,$sourceHash),409,'A proposta apresentada mudou. Consulte novamente o histórico.'); if($old=$this->closure($s,$b,$id)){ @@ -70,18 +70,18 @@ final class WorkshopStock } abort_if($this->proposalClaim($b,$id),409,'Tratamento já confirmado; exige correcção física própria.'); // Closing an obsolete preview deliberately does not execute or revalidate its physical effect. - $decision=DB::table($table)->insertGetId($s+['proposal_id'=>$id,'actor_id'=>$actor,'source_hash'=>$sourceHash,'reason'=>$reason,'before_state'=>'pending','state'=>$action,'state_version'=>2,'created_at'=>now()]); + $decision=ComposedWorkshopStockBook::table($table)->insertGetId($s+['proposal_id'=>$id,'actor_id'=>$actor,'source_hash'=>$sourceHash,'reason'=>$reason,'before_state'=>'pending','state'=>$action,'state_version'=>2,'created_at'=>now()]); Audit::record('workshop_stock.'.$action,$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['area'=>'workshop_stock_proposal','record_id'=>$id,'decision_id'=>$decision,'version'=>2,'review_hash'=>$sourceHash,'reason'=>$reason]); return ['id'=>$id,'state'=>$action,'state_version'=>2,'decision_id'=>$decision,'replayed'=>false,'physical_effect_milli'=>'0']; }); } private function originClaimed(array $b,string $origin): bool { - return DB::table($b['schema'].'.ws_claims')->where('origin_key',$origin)->exists()||(($t=$this->allocationTable($b))&&DB::table($t)->where('origin_key',$origin)->exists())||(($t=$this->returnTable($b))&&DB::table($t)->where('origin_key',$origin)->exists()); + return ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->where('origin_key',$origin)->exists()||(($t=$this->allocationTable($b))&&ComposedWorkshopStockBook::table($t)->where('origin_key',$origin)->exists())||(($t=$this->returnTable($b))&&ComposedWorkshopStockBook::table($t)->where('origin_key',$origin)->exists()); } private function proposalClaim(array $b,int $id): ?object { - return DB::table($b['schema'].'.ws_claims')->where('proposal_id',$id)->first()??(($t=$this->allocationTable($b))?DB::table($t)->where('proposal_id',$id)->first():null)??(($t=$this->returnTable($b))?DB::table($t)->where('proposal_id',$id)->first():null); + return ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->where('proposal_id',$id)->first()??(($t=$this->allocationTable($b))?ComposedWorkshopStockBook::table($t)->where('proposal_id',$id)->first():null)??(($t=$this->returnTable($b))?ComposedWorkshopStockBook::table($t)->where('proposal_id',$id)->first():null); } private function source(array $s,array $b,int $id,int $version,int $warehouse,bool $lock=false,bool $existingDelivery=false,string $event='consumed'): array { @@ -92,20 +92,20 @@ final class WorkshopStock abort_unless((int)$e->version===$version&&!$e->outside_authority&&!DB::table('wo_entries')->where('previous_id',$id)->exists(),409,'Origem antiga ou fora da autorização. Faça nova conferência.'); $decision=DB::table('wo_decisions')->where(['entry_id'=>$id,'entry_version'=>$version,'decision'=>'verified'])->first();abort_unless($decision&&(int)$decision->actor_id!==(int)$e->actor_id,409,'Consumo sem verificação técnica independente.'); $map=$b['resources'][$d['resource'].'|'.$d['unit']]??null;abort_unless($map&&in_array($warehouse,$map['warehouse_ids'],true),403,'Sem correspondência explícita de recurso/unidade/armazém.'); - $item=DB::table($b['schema'].'.stock_items')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$map['stock_item_id']])->first(); + $item=ComposedWorkshopStockBook::table($b['schema'].'.stock_items')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$map['stock_item_id']])->first(); abort_unless($item&&$item->unit===$map['stock_unit']&&strtoupper(trim($item->unit))===$d['unit'],409,'Unidade de stock incompatível; sem conversão automática.'); - abort_unless(DB::table($b['schema'].'.warehouses')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$warehouse])->exists(),403); + abort_unless(ComposedWorkshopStockBook::table($b['schema'].'.warehouses')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$warehouse])->exists(),403); try{$quantity=BigDecimal::of((string)$e->quantity)->multipliedBy(1000)->toScale(0)->toInt();}catch(\Brick\Math\Exception\MathException){abort(422,'Quantidade não representável exactamente em milésimos.');}abort_unless($quantity>0,422); $origin=hash('sha256',json_encode([$s,$e->order_id,$e->origin],JSON_THROW_ON_ERROR)); // Do not deduct consumption again after an unlinked physical delivery/issue. - $unknownQuery=DB::table($b['schema'].'.stock_movements AS m')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.stock_item_id'=>$item->id,'m.warehouse_id'=>$warehouse])->where('m.quantity_milli','<',0) + $unknownQuery=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements AS m')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.stock_item_id'=>$item->id,'m.warehouse_id'=>$warehouse])->where('m.quantity_milli','<',0) ->whereNotExists(fn($x)=>$x->selectRaw('1')->from($b['schema'].'.ws_claims AS c')->whereColumn('c.movement_id','m.id')); $accounted='0';if($t=$this->allocationTable($b))$accounted.=' + (SELECT coalesce(sum(a.quantity_milli),0) FROM '.$t.' AS a WHERE a.movement_id=m.id)'; if($t=$this->returnTable($b))$accounted.=' + (SELECT coalesce(sum(r.quantity_milli),0) FROM '.$t.' AS r WHERE r.parent_movement_id=m.id)'; $unknownQuery->whereRaw('('.$accounted.') < -m.quantity_milli'); $unknown=$unknownQuery->exists(); abort_if($unknown&&!$existingDelivery,409,'Existe saída física sem correspondência reconciliada. Não se volta a abater a entrega como consumo.'); - $book=DB::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'stock_item_id'=>$item->id,'warehouse_id'=>$warehouse])->selectRaw('coalesce(sum(quantity_milli),0)::text AS quantity,coalesce(max(id),0)::text AS revision')->first(); + $book=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'stock_item_id'=>$item->id,'warehouse_id'=>$warehouse])->selectRaw('coalesce(sum(quantity_milli),0)::text AS quantity,coalesce(max(id),0)::text AS revision')->first(); abort_unless($existingDelivery||BigDecimal::of($book->quantity)->isGreaterThanOrEqualTo($quantity),409,'Stock físico insuficiente.'); return ['entry'=>(array)$e,'decision'=>(array)$decision,'binding'=>$b,'origin_key'=>$origin,'stock_item_id'=>(int)$item->id,'warehouse_id'=>$warehouse,'quantity_milli'=>$quantity,'unit'=>$item->unit,'book'=>(array)$book]; } @@ -118,37 +118,37 @@ final class WorkshopStock private function delivery(array $v,array $b,int $movementId,bool $lock=false,bool $partial=false,bool $returning=false): array { abort_unless(($b['eligible_deliveries'][$movementId]['order_id']??null)===(int)$v['entry']['order_id'],403,'Entrega sem elegibilidade explícita para esta ordem.'); - $q=DB::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$movementId,'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']]);if($lock)$q->lockForUpdate(); + $q=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$movementId,'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']]);if($lock)$q->lockForUpdate(); $m=$q->first();abort_unless($m,403); - $q=DB::table($b['schema'].'.stock_operations')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$m->stock_operation_id,'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']]);if($lock)$q->lockForUpdate(); + $q=ComposedWorkshopStockBook::table($b['schema'].'.stock_operations')->where(['tenant_id'=>$b['source_tenant_id'],'id'=>$m->stock_operation_id,'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']]);if($lock)$q->lockForUpdate(); $o=$q->first();abort_unless($o&&$o->kind==='issue'&&$m->movement_type==='issue'&&$m->equipment_id===null&&$m->spare_part_id===null,409,'Movimento incompatível com entrega simples.'); $snapshot=json_decode($m->snapshot,true,512,JSON_THROW_ON_ERROR); abort_unless(($snapshot['inventory_operation_hash']??null)===$this->hash((array)$o),409,'Entrega sem fotografia íntegra do escritor comum; exige conferência histórica própria.'); - abort_if(DB::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']])->whereNotIn('movement_type',['receipt_in','issue','workshop_return_in'])->exists(),409,'Livro com estornos, transferências ou ajustes fora deste ensaio simples.'); + abort_if(ComposedWorkshopStockBook::table($b['schema'].'.stock_movements')->where(['tenant_id'=>$b['source_tenant_id'],'stock_item_id'=>$v['stock_item_id'],'warehouse_id'=>$v['warehouse_id']])->whereNotIn('movement_type',['receipt_in','issue','workshop_return_in'])->exists(),409,'Livro com estornos, transferências ou ajustes fora deste ensaio simples.'); $returns=$this->returnTable($b); - $q=DB::table($b['schema'].'.stock_movements AS m')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.stock_item_id'=>$v['stock_item_id'],'m.warehouse_id'=>$v['warehouse_id'],'m.movement_type'=>'workshop_return_in']); + $q=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements AS m')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.stock_item_id'=>$v['stock_item_id'],'m.warehouse_id'=>$v['warehouse_id'],'m.movement_type'=>'workshop_return_in']); if($returns)$q->whereNotExists(fn($x)=>$x->selectRaw('1')->from($returns.' AS r')->whereColumn('r.movement_id','m.id')); abort_if($q->exists(),409,'Entrada de devolução sem decisão rastreável.'); $total=BigDecimal::of((string)$m->quantity_milli)->negated(); abort_unless($total->isPositive()&&$total->isEqualTo((string)$o->quantity_milli),409,'Quantidade da entrega incompatível com a operação.'); - $allocations=$this->allocationTable($b);$used=$allocations?(string)DB::table($allocations)->where('movement_id',$movementId)->sum('quantity_milli'):'0'; - $returned=$returns?(string)DB::table($returns)->where('parent_movement_id',$movementId)->sum('quantity_milli'):'0';$remaining=$total->minus($used)->minus($returned); + $allocations=$this->allocationTable($b);$used=$allocations?(string)ComposedWorkshopStockBook::table($allocations)->where('movement_id',$movementId)->sum('quantity_milli'):'0'; + $returned=$returns?(string)ComposedWorkshopStockBook::table($returns)->where('parent_movement_id',$movementId)->sum('quantity_milli'):'0';$remaining=$total->minus($used)->minus($returned); if($partial||$returning){abort_unless($returning?(($b['stock_returns']??false)&&$returns):(($b['partial_delivery_links']??false)&&$allocations),503,'Tratamento de repartição/devolução não configurado neste ensaio.');abort_unless($remaining->isGreaterThanOrEqualTo($v['quantity_milli']),409,'Quantidade não associada insuficiente. Faça nova conferência.');} else abort_unless($total->isEqualTo($v['quantity_milli'])&&BigDecimal::of($used)->isZero()&&BigDecimal::of($returned)->isZero(),409,'Entrega e consumo devem ter a mesma quantidade integral; use conferência de repartição quando aplicável.'); abort_unless(\Carbon\CarbonImmutable::parse($m->moved_at)->lessThanOrEqualTo(\Carbon\CarbonImmutable::parse($v['entry']['starts_at'])),409,'A entrega deve preceder o consumo observado.'); - abort_if(DB::table($b['schema'].'.ws_claims')->where('movement_id',$movementId)->exists(),409,'Entrega já ligada a outra decisão.'); + abort_if(ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->where('movement_id',$movementId)->exists(),409,'Entrega já ligada a outra decisão.'); $v['delivery']=['movement'=>(array)$m,'operation'=>(array)$o,'partial'=>$partial,'returning'=>$returning,'allocated_milli'=>$used,'returned_milli'=>$returned,'remaining_milli'=>(string)$remaining];return $v; } public function prepareDelivery(array $d,string $key,bool $partial=false,bool $returning=false): array { $s=$this->scope('prepare');$b=$this->binding($s);$actor=$this->context->userId();$request=['mode'=>$returning?'return_existing_delivery':($partial?'allocate_existing_delivery':'link_existing_delivery')]+$d; return DB::transaction(function()use($s,$b,$actor,$d,$key,$request,$partial,$returning){$this->lock($s);$this->scope('prepare'); - $old=DB::table($b['schema'].'.ws_proposals')->where($s)->where(['preparer_id'=>$actor,'command_key'=>$key])->first(); + $old=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->where($s)->where(['preparer_id'=>$actor,'command_key'=>$key])->first(); if($old){abort_unless(hash_equals($old->request_hash,$this->hash($request)),409,'Chave repetida com conteúdo diferente.');return $this->proposalResult($s,$b,$old);} $v=$this->source($s,$b,$d['entry_id'],$d['version'],$d['warehouse_id'],true,true,$returning?'returned':'consumed'); abort_if($this->originClaimed($b,$v['origin_key']),409,'Origem já ligada; exige correcção própria.'); $v=$this->delivery($v,$b,$d['movement_id'],true,$partial,$returning); - $id=DB::table($b['schema'].'.ws_proposals')->insertGetId($s+['entry_id'=>$d['entry_id'],'entry_version'=>$d['version'],'preparer_id'=>$actor,'reason'=>$d['reason'],'evidence'=>$d['evidence'],'command_key'=>$key,'request_hash'=>$this->hash($request),'source_hash'=>$this->hash($v),'snapshot'=>json_encode($v,JSON_THROW_ON_ERROR),'created_at'=>now()]); + $id=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->insertGetId($s+['entry_id'=>$d['entry_id'],'entry_version'=>$d['version'],'preparer_id'=>$actor,'reason'=>$d['reason'],'evidence'=>$d['evidence'],'command_key'=>$key,'request_hash'=>$this->hash($request),'source_hash'=>$this->hash($v),'snapshot'=>json_encode($v,JSON_THROW_ON_ERROR),'created_at'=>now()]); Audit::record($returning?'workshop_stock.return_prepared':'workshop_stock.delivery_prepared',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['proposal_id'=>$id,'entry_id'=>$d['entry_id'],'version'=>$d['version'],'movement_id'=>$d['movement_id'],'physical_effect'=>$returning?(string)$v['quantity_milli']:'0']); return ['id'=>$id,'impact'=>$this->impact($v),'reason'=>$d['reason'],'evidence'=>$d['evidence'],'source_hash'=>$this->hash($v),'state'=>'pending','state_version'=>1,'replayed'=>false];}); } @@ -160,7 +160,7 @@ final class WorkshopStock ->where(['e.kind'=>'material','e.outside_authority'=>false,'d.decision'=>'verified'])->whereRaw("e.data::jsonb->>'owner'='company'")->whereRaw(($b['stock_returns']??false)?"e.data::jsonb->>'event' IN ('consumed','returned')":"e.data::jsonb->>'event'='consumed'") ->whereNotExists(fn($x)=>$x->selectRaw('1')->from('wo_entries AS n')->whereColumn('n.previous_id','e.id')) ->select(['e.id','e.order_id','e.origin','e.version','e.quantity','e.data'])->orderBy('e.id')->paginate(25,['*'],'page',$page); - $proposalQuery=DB::table($b['schema'].'.ws_proposals AS p')->where($s); + $proposalQuery=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals AS p')->where($s); $closureTable=$this->closureTable($b); if(in_array($status,['withdrawn','rejected'],true)){ if($closureTable)$proposalQuery->whereExists(fn($x)=>$x->selectRaw('1')->from($closureTable.' AS z')->whereColumn('z.proposal_id','p.id')->where('z.state',$status)); @@ -172,14 +172,29 @@ final class WorkshopStock }); if($status==='pending'&&$closureTable)$proposalQuery->whereNotExists(fn($x)=>$x->selectRaw('1')->from($closureTable.' AS z')->whereColumn('z.proposal_id','p.id')); $proposalPager=$proposalQuery->orderByDesc('p.id')->paginate(25,['p.id','p.entry_id','p.entry_version','p.preparer_id','p.reason','p.evidence','p.snapshot','p.source_hash','p.created_at'],'proposal_page',$proposalPage);$proposals=$proposalPager->getCollection(); - $keys=array_map(fn($e)=>$this->hash([$s,$e->order_id,$e->origin]),$q->items());$linked=DB::table($b['schema'].'.ws_claims')->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key'); - if($t=$this->allocationTable($b))$linked=$linked->merge(DB::table($t)->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key')); - if($t=$this->returnTable($b))$linked=$linked->merge(DB::table($t)->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key')); - foreach($q->items() as $e)$e->movement_id=$linked->get($this->hash([$s,$e->order_id,$e->origin]))?->movement_id; - $claims=DB::table($b['schema'].'.ws_claims')->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id'])->keyBy('proposal_id'); - if($t=$this->allocationTable($b))foreach(DB::table($t)->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id']) as $a)$claims->put($a->proposal_id,$a); - if($t=$this->returnTable($b))foreach(DB::table($t)->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id']) as $a)$claims->put($a->proposal_id,$a); - $closed=$closureTable?DB::table($closureTable)->where($s)->whereIn('proposal_id',$proposals->pluck('id'))->get()->keyBy('proposal_id'):collect(); + $keys=array_map(fn($e)=>$this->hash([$s,$e->order_id,$e->origin]),$q->items());$linked=ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key'); + if($t=$this->allocationTable($b))$linked=$linked->merge(ComposedWorkshopStockBook::table($t)->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key')); + if($t=$this->returnTable($b))$linked=$linked->merge(ComposedWorkshopStockBook::table($t)->whereIn('origin_key',$keys)->get(['origin_key','movement_id'])->keyBy('origin_key')); + $physicalWarehouses=collect();$physicalDeliveries=collect(); + if(ComposedWorkshopStockBook::active()){ + $warehouses=[];$articles=[];foreach($q->items() as $entry){$data=json_decode($entry->data,true);$map=$b['resources'][$data['resource'].'|'.$data['unit']]??null;if($map){$warehouses=[...$warehouses,...$map['warehouse_ids']];$articles[]=$map['stock_item_id'];}} + $orders=array_column($q->items(),'order_id');$deliveries=[];foreach($b['eligible_deliveries'] as $id=>$known)if(in_array($known['order_id'],$orders,true))$deliveries[]=(int)$id; + $physicalWarehouses=ComposedWorkshopStockBook::table($b['schema'].'.warehouses')->where('tenant_id',$b['source_tenant_id'])->whereIn('id',array_unique($warehouses))->orderBy('id')->get(['id','code','name']); + $physicalDeliveries=ComposedWorkshopStockBook::table($b['schema'].'.stock_movements as m')->join($b['schema'].'.stock_operations as o','o.id','=','m.stock_operation_id')->where(['m.tenant_id'=>$b['source_tenant_id'],'m.movement_type'=>'issue'])->whereIn('m.stock_item_id',array_unique($articles))->whereIn('m.id',$deliveries)->whereIn('m.warehouse_id',array_unique($warehouses))->orderBy('m.id')->get(['m.id','m.stock_item_id','o.reference','m.warehouse_id','m.quantity_milli']); + } + foreach($q->items() as $e){ + $e->movement_id=$linked->get($this->hash([$s,$e->order_id,$e->origin]))?->movement_id; + if(ComposedWorkshopStockBook::active()){ + $data=json_decode($e->data,true);$map=$b['resources'][$data['resource'].'|'.$data['unit']]??null; + $e->warehouses=$physicalWarehouses->whereIn('id',$map['warehouse_ids']??[])->values()->all(); + $deliveries=[];foreach(($b['eligible_deliveries']??[]) as $id=>$known)if($known['order_id']===$e->order_id)$deliveries[]=(int)$id; + $e->deliveries=$physicalDeliveries->where('stock_item_id',$map['stock_item_id']??0)->whereIn('id',$deliveries)->whereIn('warehouse_id',$map['warehouse_ids']??[])->values()->all(); + } + } + $claims=ComposedWorkshopStockBook::table($b['schema'].'.ws_claims')->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id'])->keyBy('proposal_id'); + if($t=$this->allocationTable($b))foreach(ComposedWorkshopStockBook::table($t)->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id']) as $a)$claims->put($a->proposal_id,$a); + if($t=$this->returnTable($b))foreach(ComposedWorkshopStockBook::table($t)->whereIn('proposal_id',$proposals->pluck('id'))->get(['proposal_id','movement_id']) as $a)$claims->put($a->proposal_id,$a); + $closed=$closureTable?ComposedWorkshopStockBook::table($closureTable)->where($s)->whereIn('proposal_id',$proposals->pluck('id'))->get()->keyBy('proposal_id'):collect(); foreach($proposals as $p){$p->impact=$this->impact(json_decode($p->snapshot,true,512,JSON_THROW_ON_ERROR));unset($p->snapshot);$p->movement_id=$claims->get($p->id)?->movement_id;$p->closure=$closed->get($p->id);$p->state=$p->closure?->state??($p->movement_id?'confirmed':'pending');$p->state_version=$p->state==='pending'?1:2; $p->can_withdraw=$closureTable&&$p->state==='pending'&&(int)$p->preparer_id===$this->context->userId();$p->can_review=$p->state==='pending'&&(int)$p->preparer_id!==$this->context->userId();} return ['sources'=>$q->items(),'pagination'=>['page'=>$q->currentPage(),'last_page'=>$q->lastPage(),'total'=>$q->total()], @@ -189,11 +204,11 @@ final class WorkshopStock { $s=$this->scope('prepare');$b=$this->binding($s);$actor=$this->context->userId(); return DB::transaction(function()use($s,$b,$actor,$d,$key){$this->lock($s);$this->scope('prepare'); - $old=DB::table($b['schema'].'.ws_proposals')->where($s)->where(['preparer_id'=>$actor,'command_key'=>$key])->first(); + $old=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->where($s)->where(['preparer_id'=>$actor,'command_key'=>$key])->first(); if($old){abort_unless(hash_equals($old->request_hash,$this->hash($d)),409,'Chave repetida com conteúdo diferente.');return $this->proposalResult($s,$b,$old);} $v=$this->source($s,$b,$d['entry_id'],$d['version'],$d['warehouse_id'],true); abort_if($this->originClaimed($b,$v['origin_key']),409,'Origem já consumida; correcções exigem compensação própria.'); - $id=DB::table($b['schema'].'.ws_proposals')->insertGetId($s+['entry_id'=>$d['entry_id'],'entry_version'=>$d['version'],'preparer_id'=>$actor,'reason'=>$d['reason'],'evidence'=>$d['evidence'], + $id=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->insertGetId($s+['entry_id'=>$d['entry_id'],'entry_version'=>$d['version'],'preparer_id'=>$actor,'reason'=>$d['reason'],'evidence'=>$d['evidence'], 'command_key'=>$key,'request_hash'=>$this->hash($d),'source_hash'=>$this->hash($v),'snapshot'=>json_encode($v,JSON_THROW_ON_ERROR),'created_at'=>now()]); Audit::record('workshop_stock.prepared',$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['proposal_id'=>$id,'entry_id'=>$d['entry_id'],'version'=>$d['version']]); return ['id'=>$id,'impact'=>$this->impact($v),'reason'=>$d['reason'],'evidence'=>$d['evidence'],'source_hash'=>$this->hash($v),'state'=>'pending','state_version'=>1,'replayed'=>false];}); @@ -202,7 +217,7 @@ final class WorkshopStock { $s=$this->scope('confirm');$b=$this->binding($s);$actor=$this->context->userId(); return DB::transaction(function()use($s,$b,$actor,$id,$reason,$receivedInWarehouse){$this->lock($s);$this->scope('confirm'); - $p=DB::table($b['schema'].'.ws_proposals')->where($s)->where('id',$id)->lockForUpdate()->first();abort_unless($p,403);abort_if((int)$p->preparer_id===$actor,403,'Confirmação deve ser independente da preparação.'); + $p=ComposedWorkshopStockBook::table($b['schema'].'.ws_proposals')->where($s)->where('id',$id)->lockForUpdate()->first();abort_unless($p,403);abort_if((int)$p->preparer_id===$actor,403,'Confirmação deve ser independente da preparação.'); abort_if($this->closure($s,$b,$id),409,'Proposta retirada ou recusada. Faça nova conferência; este original permanece encerrado.'); $old=$this->proposalClaim($b,$id);if($old)return ['ok'=>true,'replayed'=>true,'movement_id'=>(int)$old->movement_id,'claim_id'=>(int)$old->id]; $frozen=json_decode($p->snapshot,true,512,JSON_THROW_ON_ERROR);$link=isset($frozen['delivery']);$returning=$link&&($frozen['delivery']['returning']??false);$v=$this->source($s,$b,(int)$p->entry_id,(int)$p->entry_version,$frozen['warehouse_id'],true,$link,$returning?'returned':'consumed'); @@ -213,17 +228,19 @@ final class WorkshopStock abort_unless(hash_equals($p->source_hash,$this->hash($v)),409,'Origem, mapa ou livro mudou. Faça nova conferência.'); if($link&&!$returning){$operation=(int)$v['delivery']['operation']['id'];$out=['movement_id'=>(int)$v['delivery']['movement']['id']];} else { - $operation=DB::table($b['schema'].'.stock_operations')->insertGetId(['tenant_id'=>$b['source_tenant_id'],'created_by'=>$actor,'responsible_id'=>$actor,'warehouse_id'=>$v['warehouse_id'],'stock_item_id'=>$v['stock_item_id'], + $operation=ComposedWorkshopStockBook::table($b['schema'].'.stock_operations')->insertGetId(['tenant_id'=>$b['source_tenant_id'],'created_by'=>ComposedWorkshopStockBook::actor($actor),'responsible_id'=>ComposedWorkshopStockBook::actor($actor),'warehouse_id'=>$v['warehouse_id'],'stock_item_id'=>$v['stock_item_id'], 'kind'=>$returning?'workshop_return':'issue','reference'=>'SGF-WO-'.$v['origin_key'],'idempotency_key'=>(string)Str::uuid(),'payload_hash'=>$p->source_hash,'destination'=>'workshop-origin:'.$v['origin_key'],'reason'=>$reason, 'quantity_milli'=>$v['quantity_milli'],'theoretical_milli'=>$v['book']['quantity'],'ledger_version'=>$v['book']['revision'],'recorded_at'=>now(), 'snapshot'=>json_encode(($returning?['delivery_movement_id'=>(int)$v['delivery']['movement']['id'],'delivery_movement_hash'=>$this->hash($v['delivery']['movement']),'received_in_warehouse'=>$receivedInWarehouse]:[])+['workshop_origin'=>$v['entry'],'verification'=>$v['decision'],'proposal_id'=>$id,'preparer_id'=>$p->preparer_id,'scope'=>$s,'evidence'=>$p->evidence],JSON_THROW_ON_ERROR)]); $writer=$returning?\Tempo\Inventory\DeliveryReturnWriter::class:\Tempo\Inventory\IssueMovementWriter::class; - $w=new $writer(DB::connection(),$b['schema'],$b['source_tenant_id'],$actor,function(){$this->scope('confirm');});$out=$w->write($operation,$w->preview($operation)); + $w=new $writer(ComposedWorkshopStockBook::connection(),$b['schema'],$b['source_tenant_id'],ComposedWorkshopStockBook::actor($actor),function(){$this->scope('confirm');});$out=$w->write($operation,$w->preview($operation)); } $after=$this->impact($v)['physical_after']; - $claim=DB::table($returning?$this->returnTable($b):($partial?$this->allocationTable($b):$b['schema'].'.ws_claims'))->insertGetId($s+($returning?['quantity_milli'=>$v['quantity_milli'],'parent_movement_id'=>(int)$v['delivery']['movement']['id']]:($partial?['quantity_milli'=>$v['quantity_milli']]:[]))+['origin_key'=>$v['origin_key'],'proposal_id'=>$id,'entry_id'=>$p->entry_id,'entry_version'=>$p->entry_version,'operation_id'=>$operation,'movement_id'=>$out['movement_id'], + $claim=ComposedWorkshopStockBook::table($returning?$this->returnTable($b):($partial?$this->allocationTable($b):$b['schema'].'.ws_claims'))->insertGetId($s+($returning?['quantity_milli'=>$v['quantity_milli'],'parent_movement_id'=>(int)$v['delivery']['movement']['id']]:($partial?['quantity_milli'=>$v['quantity_milli']]:[]))+['origin_key'=>$v['origin_key'],'proposal_id'=>$id,'entry_id'=>$p->entry_id,'entry_version'=>$p->entry_version,'operation_id'=>$operation,'movement_id'=>$out['movement_id'], 'preparer_id'=>$p->preparer_id,'confirmer_id'=>$actor,'reason'=>$reason,'before_milli'=>$v['book']['quantity'],'after_milli'=>$after,'source_hash'=>$p->source_hash,'created_at'=>now()]); Audit::record($returning?'workshop_stock.return_confirmed':($partial?'workshop_stock.delivery_allocated':($link?'workshop_stock.delivery_linked':'workshop_stock.confirmed')),$actor,$s['tenant_id'],$s['company_id'],$s['unit_id'],details:['claim_id'=>$claim,'proposal_id'=>$id,'entry_id'=>$p->entry_id,'version'=>$p->entry_version,'movement_id'=>$out['movement_id'],'linked_quantity'=>$link?(string)$v['quantity_milli']:null,'remaining_after'=>($partial||$returning)?$this->impact($v)['unallocated_after']:null,'before'=>$v['book']['quantity'],'after'=>$after,'reason'=>$reason]); return ['ok'=>true,'replayed'=>false,'movement_id'=>$out['movement_id'],'claim_id'=>$claim];}); } } + + diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 135d506..fe20f9a 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -38,6 +38,7 @@ class AppServiceProvider extends ServiceProvider Event::listen(CommandStarting::class, function (CommandStarting $event): void { if (in_array($event->command, ['migrate:fresh', 'migrate:refresh', 'migrate:reset', 'migrate:rollback', 'db:wipe'], true)) { + if($this->app->environment('budget-testing'))throw new \RuntimeException('SGF_COMPOSITION_DESTRUCTIVE_COMMAND_REFUSED'); $connection = $event->input->hasOption('database') ? $event->input->getOption('database') : null; TestingDatabaseGuard::assertSafe($connection); } diff --git a/app/Requests/Sgc/StoreInvoiceRequest.php b/app/Requests/Sgc/StoreInvoiceRequest.php new file mode 100644 index 0000000..e08807e --- /dev/null +++ b/app/Requests/Sgc/StoreInvoiceRequest.php @@ -0,0 +1,43 @@ + 'required|in:INVOICE_CREATED', + 'metadata.timestamp' => 'required|date', + 'sgf_doc_number' => 'required|string|max:40', + 'sgf_invoice_id' => 'required|integer', + 'oc_reference' => 'required|string|max:50', + 'supplier.sgf_supplier_id' => 'required|string|max:40', + 'supplier.nuit' => 'required|string|max:20', + 'supplier.name' => 'required|string|max:200', + 'amounts.subtotal' => 'required|numeric|min:0', + 'amounts.iva_amount' => 'required|numeric|min:0', + 'amounts.total' => 'required|numeric|min:0', + 'amounts.currency' => 'required|string|size:3', + 'amounts.exchange_rate' => 'required|numeric|min:0', + 'dates.invoice_date' => 'required|date', + 'dates.due_date' => 'required|date|after_or_equal:dates.invoice_date', + ]; + } + + public function messages(): array + { + return [ + 'sgf_doc_number.required' => 'Número do documento SGF é obrigatório', + 'oc_reference.required' => 'Referência da OC é obrigatória', + 'amounts.subtotal.min' => 'Valor subtotal deve ser positivo', + ]; + } +} diff --git a/app/Requests/Sgc/StorePaymentRequest.php b/app/Requests/Sgc/StorePaymentRequest.php new file mode 100644 index 0000000..cc9e273 --- /dev/null +++ b/app/Requests/Sgc/StorePaymentRequest.php @@ -0,0 +1,28 @@ + 'required|in:PAYMENT_EXECUTED', + 'metadata.timestamp' => 'required|date', + 'sgf_doc_number' => 'required|string|max:40', + 'payment_id' => 'required|string|max:50', + 'payment_date' => 'required|date', + 'payment_method' => 'required|string|max:50', + 'amount_paid' => 'required|numeric|min:0', + 'currency' => 'required|string|size:3', + 'bank_reference' => 'required|string|max:100', + ]; + } +} diff --git a/app/Services/Sgc/BudgetCommitmentService.php b/app/Services/Sgc/BudgetCommitmentService.php new file mode 100644 index 0000000..3fb7efb --- /dev/null +++ b/app/Services/Sgc/BudgetCommitmentService.php @@ -0,0 +1,90 @@ + true, + 'saldo_atual' => 500000.00, + 'valor_solicitado' => $valor, + 'saldo_apos_compromisso' => 500000.00 - $valor, + ]; + } + + /** + * Cabimenta (compromete) orçamento no SGF + */ + public function cabimentar(OrdemCompra $oc): array + { + $verificacao = $this->verificarSaldo( + $oc->centroCusto->sgf_centro_custo_id, + $oc->categoria, + $oc->valor_comprometido + ); + + if (!$verificacao['disponivel']) { + throw new \Exception('Saldo orçamental insuficiente'); + } + + // Em produção, isto chamaria uma API do SGF para criar o compromisso + // Simulamos o sucesso + $compromissoId = 'COMP-' . strtoupper(substr(md5($oc->oc_id), 0, 8)); + + DB::transaction(function () use ($oc, $compromissoId) { + $oc->update([ + 'cabimentado' => true, + 'sgf_compromisso_id' => $compromissoId, + ]); + }); + + Log::info('Cabimentação realizada', [ + 'oc_id' => $oc->oc_id, + 'compromisso_id' => $compromissoId, + 'valor' => $oc->valor_comprometido, + ]); + + return [ + 'sucesso' => true, + 'compromisso_id' => $compromissoId, + 'saldo_apos_compromisso' => $verificacao['saldo_apos_compromisso'], + ]; + } + + /** + * Cancela cabimentação (quando OC é cancelada) + */ + public function cancelarCabimentacao(OrdemCompra $oc): bool + { + if (!$oc->cabimentado) { + return true; + } + + // Em produção, chamaria API do SGF para cancelar o compromisso + DB::transaction(function () use ($oc) { + $oc->update([ + 'cabimentado' => false, + 'sgf_compromisso_id' => null, + ]); + }); + + Log::info('Cabimentação cancelada', [ + 'oc_id' => $oc->oc_id, + 'compromisso_id' => $oc->sgf_compromisso_id, + ]); + + return true; + } +} diff --git a/app/Services/Sgc/CalendarDimension.php b/app/Services/Sgc/CalendarDimension.php new file mode 100644 index 0000000..6aa238b --- /dev/null +++ b/app/Services/Sgc/CalendarDimension.php @@ -0,0 +1,29 @@ + 100 || $startYear + $years > 10000) throw new InvalidArgumentException('Invalid calendar interval.'); + $months = [1=>'Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro']; + $weekdays = [1=>'Segunda','Terça','Quarta','Quinta','Sexta','Sábado','Domingo']; + $date = new DateTimeImmutable(sprintf('%04d-01-01', $startYear), new DateTimeZone('Africa/Maputo')); + while ((int)$date->format('Y') < $startYear + $years) { + $month = (int)$date->format('n'); $weekday = (int)$date->format('N'); + yield ['data_id'=>(int)$date->format('Ymd'), 'data_completa'=>$date->format('Y-m-d'), + 'dia_semana'=>$weekday, 'nome_dia_semana'=>$weekdays[$weekday], 'dia_mes'=>(int)$date->format('j'), + 'mes'=>$month, 'nome_mes'=>$months[$month], 'trimestre'=>intdiv($month-1,3)+1, + 'ano'=>(int)$date->format('Y'), 'ano_mes'=>$date->format('Y-m'), + 'eh_dia_util'=>$weekday<=5, 'eh_feriado'=>false, 'semestre'=>$month<=6?1:2]; + $date = $date->modify('+1 day'); + } + } +} diff --git a/app/Services/Sgc/IntegrationService.php b/app/Services/Sgc/IntegrationService.php new file mode 100644 index 0000000..d014456 --- /dev/null +++ b/app/Services/Sgc/IntegrationService.php @@ -0,0 +1,224 @@ +matchService = $matchService; + $this->budgetService = $budgetService; + } + + /** + * Recebe fatura do SGC e processa no SGF + */ + public function receberFatura(array $payload, string $idempotencyKey): array + { + // Verificar idempotência + $logExistente = DB::table('sgc_integration_log') + ->where('idempotency_key', $idempotencyKey) + ->first(); + + if ($logExistente) { + return [ + 'status' => 'DUPLICATE', + 'original_receipt_id' => $logExistente->id, + 'message' => 'Idempotency key já processada', + ]; + } + + try { + $resultado = DB::transaction(function () use ($payload) { + // Buscar ou criar fornecedor + $fornecedor = $this->sincronizarFornecedor($payload['supplier']); + + // Buscar OC + $oc = OrdemCompra::where('oc_id', $payload['oc_reference'])->first(); + if (!$oc) { + throw new \Exception('OC não encontrada: ' . $payload['oc_reference']); + } + + // Verificar se fornecedor bate + if ($oc->fornecedor_id !== $fornecedor->id) { + throw new \Exception('Fornecedor não corresponde à OC'); + } + + // Criar fatura + $fatura = Fatura::create([ + 'fatura_id' => $payload['sgf_invoice_id'], + 'ordem_compra_id' => $oc->id, + 'fornecedor_id' => $fornecedor->id, + 'data_fatura' => $payload['dates']['invoice_date'], + 'data_vencimento' => $payload['dates']['due_date'], + 'valor_fatura' => $payload['amounts']['subtotal'], + 'iva_valor' => $payload['amounts']['iva_amount'], + 'moeda' => $payload['amounts']['currency'], + 'taxa_cambio' => $payload['amounts']['exchange_rate'], + 'status_3_way_match' => 'Pendente', + 'status_pagamento' => 'Aguardando', + 'sgf_doc_number' => $payload['sgf_doc_number'], + ]); + + // Executar 3-Way Match + $matchResult = $this->matchService->executarMatch($fatura); + + return [ + 'status' => 'ACCEPTED', + 'fatura_id' => $fatura->id, + 'sgf_doc_number' => $fatura->sgf_doc_number, + 'match_result' => $matchResult, + ]; + }); + + // Log de sucesso + DB::table('sgc_integration_log')->insert([ + 'idempotency_key' => $idempotencyKey, + 'endpoint' => '/api/v1/integration/sgc/invoices', + 'event_type' => 'INVOICE_CREATED', + 'payload' => json_encode($payload), + 'status' => 'ACCEPTED', + 'response' => json_encode($resultado), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return $resultado; + + } catch (\Exception $e) { + // Log de erro + DB::table('sgc_integration_log')->insert([ + 'idempotency_key' => $idempotencyKey, + 'endpoint' => '/api/v1/integration/sgc/invoices', + 'event_type' => 'INVOICE_CREATED', + 'payload' => json_encode($payload), + 'status' => 'REJECTED', + 'error_code' => 'PROCESSING_ERROR', + 'error_message' => $e->getMessage(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + Log::error('Erro ao processar fatura', [ + 'idempotency_key' => $idempotencyKey, + 'error' => $e->getMessage(), + ]); + + return [ + 'status' => 'REJECTED', + 'error_code' => 'PROCESSING_ERROR', + 'message' => $e->getMessage(), + ]; + } + } + + /** + * Recebe confirmação de pagamento do SGC + */ + public function receberPagamento(array $payload, string $idempotencyKey): array + { + // Verificar idempotência + $logExistente = DB::table('sgc_integration_log') + ->where('idempotency_key', $idempotencyKey) + ->first(); + + if ($logExistente) { + return [ + 'status' => 'DUPLICATE', + 'original_receipt_id' => $logExistente->id, + ]; + } + + try { + $resultado = DB::transaction(function () use ($payload) { + // Buscar fatura + $fatura = Fatura::where('sgf_doc_number', $payload['sgf_doc_number'])->first(); + if (!$fatura) { + throw new \Exception('Fatura não encontrada: ' . $payload['sgf_doc_number']); + } + + // Atualizar fatura com pagamento + $fatura->update([ + 'data_pagamento' => $payload['payment_date'], + 'valor_pago' => $payload['amount_paid'], + 'status_pagamento' => 'Pago', + 'numero_comprovativo' => $payload['bank_reference'], + ]); + + // Atualizar OC se totalmente paga + $oc = $fatura->ordemCompra; + $totalPago = $oc->faturas()->where('status_pagamento', 'Pago')->sum('valor_pago'); + + if ($totalPago >= $oc->valor_comprometido) { + $oc->update(['status' => 'Totalmente Paga']); + } + + return [ + 'status' => 'ACCEPTED', + 'fatura_id' => $fatura->id, + 'payment_id' => $payload['payment_id'], + ]; + }); + + // Log de sucesso + DB::table('sgc_integration_log')->insert([ + 'idempotency_key' => $idempotencyKey, + 'endpoint' => '/api/v1/integration/sgc/payments', + 'event_type' => 'PAYMENT_EXECUTED', + 'payload' => json_encode($payload), + 'status' => 'ACCEPTED', + 'response' => json_encode($resultado), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return $resultado; + + } catch (\Exception $e) { + DB::table('sgc_integration_log')->insert([ + 'idempotency_key' => $idempotencyKey, + 'endpoint' => '/api/v1/integration/sgc/payments', + 'event_type' => 'PAYMENT_EXECUTED', + 'payload' => json_encode($payload), + 'status' => 'REJECTED', + 'error_code' => 'PROCESSING_ERROR', + 'error_message' => $e->getMessage(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return [ + 'status' => 'REJECTED', + 'error_code' => 'PROCESSING_ERROR', + 'message' => $e->getMessage(), + ]; + } + } + + /** + * Sincroniza fornecedor do SGC para o SGF + */ + private function sincronizarFornecedor(array $supplierData): Fornecedor + { + return Fornecedor::updateOrCreate( + ['sgf_fornecedor_id' => $supplierData['sgf_supplier_id']], + [ + 'nuit' => $supplierData['nuit'], + 'nome_fornecedor' => $supplierData['name'], + 'categoria_fornecedor' => $supplierData['category'] ?? 'Geral', + ] + ); + } +} diff --git a/app/Services/Sgc/ThreeWayMatchService.php b/app/Services/Sgc/ThreeWayMatchService.php new file mode 100644 index 0000000..17433e7 --- /dev/null +++ b/app/Services/Sgc/ThreeWayMatchService.php @@ -0,0 +1,97 @@ +ordemCompra; + $recebimentos = $oc->recebimentos; + + // Calcular total recebido + $valorTotalRecebido = $recebimentos->sum('valor_recebido'); + $valorFatura = $fatura->valor_fatura; + $valorOC = $oc->valor_comprometido; + + // Verificar divergências + $divergenciaOC = abs($valorFatura - $valorOC); + $divergenciaRecebimento = abs($valorFatura - $valorTotalRecebido); + + // Aplicar tolerâncias + $toleranciaOC = max($valorOC * self::TOLERANCIA_PERCENTUAL, self::TOLERANCIA_VALOR); + $toleranciaRecebimento = max($valorTotalRecebido * self::TOLERANCIA_PERCENTUAL, self::TOLERANCIA_VALOR); + + $matchOC = $divergenciaOC <= $toleranciaOC; + $matchRecebimento = $divergenciaRecebimento <= $toleranciaRecebimento; + + // Determinar status + $status = 'Conforme'; + $motivo = null; + + if (!$matchOC && !$matchRecebimento) { + $status = 'Com Excecao'; + $motivo = "Divergência com OC ({$divergenciaOC} MT) e Recebimento ({$divergenciaRecebimento} MT)"; + } elseif (!$matchOC) { + $status = 'Com Excecao'; + $motivo = "Divergência com OC: {$divergenciaOC} MT"; + } elseif (!$matchRecebimento) { + $status = 'Com Excecao'; + $motivo = "Divergência com Recebimento: {$divergenciaRecebimento} MT"; + } + + // Atualizar fatura + DB::transaction(function () use ($fatura, $status, $motivo) { + $fatura->update([ + 'status_3_way_match' => $status, + 'motivo_excecao' => $motivo, + ]); + }); + + Log::info('3-Way Match executado', [ + 'fatura_id' => $fatura->fatura_id, + 'status' => $status, + 'valor_fatura' => $valorFatura, + 'valor_oc' => $valorOC, + 'valor_recebido' => $valorTotalRecebido, + ]); + + return [ + 'status' => $status, + 'motivo' => $motivo, + 'valor_fatura' => $valorFatura, + 'valor_oc' => $valorOC, + 'valor_recebido' => $valorTotalRecebido, + 'divergencia_oc' => $divergenciaOC, + 'divergencia_recebimento' => $divergenciaRecebimento, + ]; + } + + /** + * Processa todas as faturas pendentes de match + */ + public function processarPendentes(): int + { + $faturasPendentes = Fatura::where('status_3_way_match', 'Pendente')->get(); + $processadas = 0; + + foreach ($faturasPendentes as $fatura) { + $this->executarMatch($fatura); + $processadas++; + } + + return $processadas; + } +} diff --git a/app/Shared/Context/ModuleNavigation.php b/app/Shared/Context/ModuleNavigation.php index 99ecd67..8f555e6 100644 --- a/app/Shared/Context/ModuleNavigation.php +++ b/app/Shared/Context/ModuleNavigation.php @@ -6,13 +6,14 @@ use Illuminate\Support\Facades\DB; final class ModuleNavigation { - public const LABELS = ['commercial'=>'Comercial', 'purchasing'=>'Compras', 'stock'=>'Stock', 'workshop'=>'Oficinas', 'finance'=>'Financeiro', 'accounting'=>'Contabilidade']; + public const LABELS = ['commercial'=>'Comercial', 'purchasing'=>'Compras', 'stock'=>'Stock', 'workshop'=>'Oficinas', 'finance'=>'Financeiro', 'accounting'=>'Contabilidade', 'human_resources'=>'Recursos Humanos']; private const RIGHTS = [ - 'commercial'=>['clients.', 'services.', 'service_drafts.', 'billing_drafts.', 'issuer_documents.', 'client_documents.', 'draft_documents.', 'service_invoices.'], + 'human_resources'=>['human_resources.remunerations.','human_resources.contracts.','human_resources.attendance.','human_resources.leave.'], + 'commercial'=>['commercial_quotes.','service_documents.','clients.', 'services.', 'service_drafts.', 'billing_drafts.', 'issuer_documents.', 'client_documents.', 'draft_documents.', 'service_invoices.'], 'purchasing'=>['purchasing_budget.', 'purchasing_requisitions.', 'purchasing_receipts.', 'purchasing_orders.', 'purchasing_sourcing.', 'purchasing_suppliers.', 'purchasing_requests.', 'purchasing_invoices.', 'purchasing_obligations.', 'supplier_payables.'], 'stock'=>['stock.'], 'workshop'=>['workshop.', 'workshop_billing.', 'workshop_corrections.'], - 'finance'=>['direct_costs.', 'industrial_costs.', 'industrial_budgets.', 'industrial_budget_comparisons.', 'service_receivables.'], - 'accounting'=>['catalogue_accounting.', 'service_fiscal.', 'general_ledger.'], + 'finance'=>['financial_analysis.','bank_reconciliation.','cash.', 'direct_costs.', 'industrial_costs.', 'industrial_budgets.', 'industrial_budget_comparisons.', 'service_receivables.'], + 'accounting'=>['fiscal_obligations.','fiscal_rules.','fiscal_assessments.','fiscal_forms.','catalogue_accounting.', 'service_fiscal.', 'general_ledger.'], ]; public function available(CompanyContext $context): array @@ -50,6 +51,14 @@ final class ModuleNavigation // Company, unit and access configuration are explicitly general. public static function owner(string $path): ?string { + if(preg_match('~^(stock/catalogue|api/stock-catalogue)(/|$)~',$path))return 'stock'; + if(preg_match('~^(finance/cash|api/cash)(/|$)~',$path))return 'finance'; + if(preg_match('~^api/(remunerations|hr-personnel)(/|$)~',$path))return 'human_resources'; + if(preg_match('~^api/fiscal-obligations(/|$)~',$path))return 'accounting'; + if(preg_match('~^api/finance/payable-positions(/|$)~',$path))return 'finance'; + if(preg_match('~^api/finance/payable-due(/|$)~',$path))return 'finance'; + if(preg_match('~^(finance/payables|api/finance/payables)(/|$)~',$path))return 'finance'; + if (preg_match('~^api/v1/integration/sgc/invoices(?:/orders/[1-9][0-9]*)?$~D', $path)) return 'purchasing'; if (preg_match('~^(geography|geography/segments|api/geography/entities|api/geography/segments)/client(/|$)~', $path)) return 'commercial'; if (preg_match('~^(geography|geography/segments|api/geography/entities|api/geography/segments)/supplier(/|$)~', $path)) return 'purchasing'; if (preg_match('~^(purchasing/dossiers|api/purchasing-dossiers)(/|$)~', $path)) return 'purchasing'; @@ -63,10 +72,17 @@ final class ModuleNavigation if (preg_match('~^(purchasing/receipts|api/purchasing-receipts)(/|$)~', $path)) return 'purchasing'; if (preg_match('~^api/stock-valuation-read(/|$)~', $path)) return 'accounting'; if (preg_match('~^(purchasing/internal-requisitions|api/internal-requisitions)(/|$)~', $path)) return 'purchasing'; + if (preg_match('~^(commercial/documents|api/service-documents)(/|$)~', $path)) return 'commercial'; + if(preg_match('~^(finance/indicators|api/financial-indicators)(/|$)~',$path))return 'finance'; + if(preg_match('~^(finance/bank|api/bank-reconciliation)(/|$)~',$path))return 'finance'; + if(preg_match('~^(fiscal/rules|api/fiscal-rules)(/|$)~',$path))return 'accounting'; + if(preg_match('~^(fiscal/iva|api/fiscal-assessments)(/|$)~',$path))return 'accounting'; if (preg_match('~^(general-ledger|api/general-ledger)(/|$)~', $path)) return 'accounting'; + if(preg_match('~^(api/commercial-quotes|commercial-quotes)(/|$)~',$path))return 'commercial'; if (preg_match('~^api/stock-read(/|$)~', $path)) return 'stock'; + if (preg_match('~^(stock/(operations|final|completion)|api/(stock-operations|stock-final|stock-completion))(/|$)~', $path)) return 'stock'; if (preg_match('~^(supplier-payables|supplier-payments|api/(supplier-payables|supplier-payments|purchasing-budget))(/|$)~', $path)) return 'purchasing'; - if (preg_match('~^api/(workshop|workshop-stock|workshop-billing|workshop-corrections)(/|$)~', $path)) return 'workshop'; + if (preg_match('~^api/(workshop|workshop-schedule|workshop-equipment|workshop-stock|workshop-billing|workshop-corrections)(/|$)~', $path)) return 'workshop'; if (preg_match('~^api/(direct-costs|industrial-costs|industrial-budgets|service-receivables)(/|$)~', $path)) return 'finance'; if (preg_match('~^api/(catalogue-accounting|service-fiscal)(/|$)~', $path)) return 'accounting'; if (preg_match('~^api/(a|b1|service-invoices|service-calculations|document-preview|document-preparation)(/|$)~', $path)) return 'commercial'; diff --git a/app/Shared/Installation/CompositionRuntime.php b/app/Shared/Installation/CompositionRuntime.php new file mode 100644 index 0000000..d7bd7df --- /dev/null +++ b/app/Shared/Installation/CompositionRuntime.php @@ -0,0 +1,29 @@ +getConfig();$run=(string)getenv('SGF_ISOLATED_REGRESSION_RUN'); + abort_unless($db->getDriverName()==='pgsql'&&preg_match('/^budget_core_sgf_([a-f0-9]{12})$/D',$db->getDatabaseName(),$m) + &&($c['username']??null)==='ir_composition_'.$m[1]&&($c['host']??null)==='127.0.0.1'&&(int)($c['port']??0)===55439 + &&preg_match('/^[a-f0-9]{12}$/D',$run)&&empty($c['url'])&&empty($c['read'])&&empty($c['write'])&&($c['search_path']??'public')==='public',403); + $actual=$db->selectOne("SELECT current_database() db,current_user role,inet_server_port() port,current_setting('cluster_name') cluster,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user"); + abort_unless($actual->db===$db->getDatabaseName()&&$actual->role===$c['username']&&(int)$actual->port===55439&&$actual->cluster==='sgf_modular_regression_'.$run&&!$actual->elevated,403); + return ['database'=>$actual->db,'role'=>$actual->role,'cluster'=>$actual->cluster,'composition'=>$m[1]]; + } + + public static function assert(?string $connection=null):void + { + abort_unless(app()->environment('budget-testing')&&config('sgf_single_installation.enabled')===true,403); + $db=DB::connection($connection);self::physical($db);$pin=config('sgf_single_installation.schema_sha256'); + abort_unless(is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin)&&$db->getSchemaBuilder()->hasTable('sgf_composition_schema'),503,'Composição SGF não instalada ou não conferida.'); + abort_unless(hash_equals($pin,(string)$db->table('sgf_composition_schema')->where('id',1)->value('source_hash')),503,'A composição SGF mudou; exige nova conferência.'); + } +} diff --git a/app/Shared/Installation/CompositionSchema.php b/app/Shared/Installation/CompositionSchema.php new file mode 100644 index 0000000..91bcd98 --- /dev/null +++ b/app/Shared/Installation/CompositionSchema.php @@ -0,0 +1,102 @@ +runningInConsole()&&app()->environment('budget-testing'),403); + return CompositionRuntime::physical(); + } + + /** Callback may prove installed structures inside the same transaction. + * Default is a full rollback. No role, grant, account, association or document is created. + */ + public function run(bool $commit=false,?\Closure $verify=null):array + { + $identity=self::identity();$host=DB::connection(); + abort_unless($host->transactionLevel()===0,409,'Não compor dentro de uma transacção alheia.'); + $constraint=$host->selectOne("SELECT pg_get_constraintdef(oid) value FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->value??null; + if(!is_string($constraint)||!preg_match('/^CHECK \((.*)\)$/s',$constraint,$m))throw new \LogicException('COMPOSITION_PERMISSION_CHECK_MISSING'); + $connection=new CompositionSchemaConnection($host->getPdo(),$host->getDatabaseName(),'',$host->getConfig()); + $connection->originalPermissionCheck=$m[1]; + $name='sgf_composition_schema';$original=DB::getDefaultConnection(); + config(['database.connections.'.$name=>$host->getConfig()]);DB::extend($name,fn()=>$connection);DB::setDefaultConnection($name);Schema::clearResolvedInstance('db.schema');app()->forgetInstance('db.schema'); + $result=[];$connection->beginTransaction(); + try{ + $connection->statement("SET LOCAL lock_timeout='5s'");$connection->statement("SET LOCAL statement_timeout='30s'"); + $connection->selectOne("SELECT pg_advisory_xact_lock(hashtext('sgf-single-installation-schema'))"); + if($connection->getSchemaBuilder()->hasTable('sgf_composition_schema')){ + $stored=$connection->table('sgf_composition_schema')->where('id',1)->first(); + if(!$stored||$stored->source_hash!==$this->fingerprint())throw new \LogicException('COMPOSITION_SCHEMA_VERSION_CHANGED'); + $result=['identity'=>$identity,'replayed'=>true,'source_hash'=>$stored->source_hash,'reused_tables'=>[],'migrations'=>[]]; + }else{ + $before=array_column($connection->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename'); + foreach(['b_internal_confirmations','doc_records','wo_orders','sr_records','ca_proposals'] as $table)if(in_array($table,$before,true))throw new \LogicException('COMPOSITION_PARTIAL_SCHEMA_REQUIRES_REVIEW: '.$table); + if(!$connection->selectOne("SELECT to_regprocedure('public.sgf_append_only_audit()') value")->value)$connection->unprepared("CREATE FUNCTION sgf_append_only_audit() RETURNS trigger LANGUAGE plpgsql AS \$\$ BEGIN RAISE EXCEPTION 'SGF_APPEND_ONLY_AUDIT'; END; \$\$;"); + $migrations=[]; + $auditTables=['a_history','a_events','a_commands','a_outbox','a_pending'];$present=array_intersect($auditTables,$before); + if(count($present)!==0&&count($present)!==count($auditTables))throw new \LogicException('COMPOSITION_PARTIAL_A_HISTORY'); + foreach(self::MIGRATIONS as $index=>$file){ + if($index===0&&count($present)===count($auditTables)){ + foreach(['a_clients','a_services','a_service_drafts','a_billing_drafts'] as $table){ + if(!in_array($table,$before,true))throw new \LogicException('COMPOSITION_PARTIAL_A_RECORDS'); + $connection->getSchemaBuilder()->create($table,fn()=>null); + } + $guards=$connection->selectOne("SELECT count(*) n FROM pg_trigger WHERE NOT tgisinternal AND tgenabled='O' AND tgname IN ('a_clients_guard','a_services_guard','a_service_drafts_guard','a_billing_drafts_guard','a_history_scope','a_history_append_only','a_events_append_only','a_commands_append_only','a_outbox_append_only','a_pending_append_only')")->n; + if((int)$guards!==10)throw new \LogicException('COMPOSITION_A_GUARDS_INCOMPLETE'); + continue; + } + $migration=require database_path('migrations/'.$file);$migration->up();$migrations[]=$file; + } + $connection->unprepared('CREATE TABLE sgf_composition_schema(id smallint PRIMARY KEY CHECK(id=1),source_hash char(64) NOT NULL,installed_at timestamptz NOT NULL DEFAULT now())'); + $connection->table('sgf_composition_schema')->insert(['id'=>1,'source_hash'=>$this->fingerprint()]); + $connection->unprepared('CREATE TRIGGER composition_schema_append_only BEFORE UPDATE OR DELETE ON sgf_composition_schema FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()'); + // Rebind ONLY the newly created QA guards to this exact database, + // role and cluster. The source SQL and all other installations retain + // their original guard; no general local/development bypass is added. + foreach(['wo','sr','dc','ic'] as $prefix){ + $connection->unprepared("CREATE OR REPLACE FUNCTION sgf_{$prefix}_environment() RETURNS void LANGUAGE plpgsql AS \$\$ BEGIN IF current_database() IS DISTINCT FROM '".$identity['database']."' OR current_user IS DISTINCT FROM '".$identity['role']."' OR inet_server_port() IS DISTINCT FROM 55439 OR current_setting('cluster_name') IS DISTINCT FROM '".$identity['cluster']."' THEN RAISE EXCEPTION 'SGF_COMPOSITION_OWNED_QA_ONLY'; END IF; END; \$\$;"); + } + $after=array_column($connection->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename'); + $result=['identity'=>$identity,'replayed'=>false,'source_hash'=>$this->fingerprint(),'reused_tables'=>$connection->getSchemaBuilder()->reused,'new_tables'=>array_values(array_diff($after,$before)),'migrations'=>$migrations]; + } + if($verify)$result['verification']=$verify($connection); + if($commit)$connection->commit();else $connection->rollBack(); + $result['committed']=$commit;return $result; + }finally{ + while($connection->transactionLevel()>0)$connection->rollBack(); + DB::setDefaultConnection($original);Schema::clearResolvedInstance('db.schema');app()->forgetInstance('db.schema');DB::purge($name); + } + } + + public function fingerprint():string + { + $files=[];foreach(self::MIGRATIONS as $file)$files['database/migrations/'.$file]=hash_file('sha256',database_path('migrations/'.$file)); + foreach(glob(database_path('sql/*.sql')) as $file)$files['database/sql/'.basename($file)]=hash_file('sha256',$file); + foreach(['CompositionSchema.php','CompositionSchemaConnection.php','CompositionSchemaBuilder.php','CompositionRuntime.php'] as $file)$files[$file]=hash_file('sha256',__DIR__.'/'.$file); + return hash('sha256',json_encode($files,JSON_THROW_ON_ERROR)); + } +} diff --git a/app/Shared/Installation/CompositionSchemaBuilder.php b/app/Shared/Installation/CompositionSchemaBuilder.php new file mode 100644 index 0000000..79470db --- /dev/null +++ b/app/Shared/Installation/CompositionSchemaBuilder.php @@ -0,0 +1,30 @@ +hasTable($table)){ + $expected=['id','tenant_id','company_id','unit_id','name','version','data','created_at','updated_at']; + if(in_array($table,['a_service_drafts','a_billing_drafts'],true))$expected=[...$expected,'client_id','state']; + if($table==='a_billing_drafts')$expected[]='source_id'; + $columns=$this->getColumns($table);$names=array_column($columns,'name');sort($expected);sort($names); + if($names!==$expected)throw new \LogicException('COMPOSITION_EXISTING_TABLE_INCOMPATIBLE: '.$table); + foreach($columns as $column){ + $type=$column['type_name'];$name=$column['name']; + $valid=match($name){'data'=>$type==='jsonb','name','state'=>$type==='varchar','created_at','updated_at'=>$type==='timestamptz','version'=>in_array($type,['int4','integer'],true),default=>in_array($type,['int8','bigint'],true)}; + if(!$valid)throw new \LogicException('COMPOSITION_EXISTING_COLUMN_INCOMPATIBLE: '.$table.'.'.$name); + } + $this->reused[]=$table; + return; + } + parent::create($table,$callback); + } +} diff --git a/app/Shared/Installation/CompositionSchemaConnection.php b/app/Shared/Installation/CompositionSchemaConnection.php new file mode 100644 index 0000000..37af516 --- /dev/null +++ b/app/Shared/Installation/CompositionSchemaConnection.php @@ -0,0 +1,28 @@ +useDefaultSchemaGrammar(); + return $this->compositionBuilder??=new CompositionSchemaBuilder($this); + } + + public function statement($query,$bindings=[]) + { + // Older migrations replaced the permission whitelist. Preserve its original + // branches as well as the migration's additions, without granting any right. + if(preg_match('/^ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK \((.*)\)$/s',$query,$m)){ + $query='ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$this->originalPermissionCheck.') OR ('.$m[1].'))'; + } + return parent::statement($query,$bindings); + } +} diff --git a/app/Shared/Installation/HumanResourcesProfiles.php b/app/Shared/Installation/HumanResourcesProfiles.php new file mode 100644 index 0000000..ce7b6a5 --- /dev/null +++ b/app/Shared/Installation/HumanResourcesProfiles.php @@ -0,0 +1,25 @@ +1,'company_id'=>2,'unit_id'=>2]&&($p['authorization']['decision']??null)==='Autorizar os perfis QA próprios de RH e as concessões apresentadas',403,'Perfis próprios de RH por autorizar.');return$p;} + public static function fingerprint(array $p):string{return hash('sha256',json_encode(array_intersect_key($p,array_flip(['version','scope','module','profiles'])),JSON_THROW_ON_ERROR));} + public static function registered():array{ + abort_unless(config('human_resources.enabled')===true,404);CompositionRuntime::assert();$p=self::policy();$pin=self::fingerprint($p);abort_unless(hash_equals($pin,(string)config('human_resources.profiles_sha256')),503,'Matriz de RH não conferida.');$rows=DB::table('hr_qa_profiles')->orderBy('profile_key')->get();$expected=collect($p['profiles'])->keyBy('id');abort_unless($rows->count()===3&&$rows->every(fn($r)=>isset($expected[$r->profile_key])&&hash_equals($pin,$r->source_hash)&&$r->tenant_id===1&&$r->company_id===2&&$r->unit_id===2),503,'Registo de perfis de RH alterado.');return$rows->all(); + } + public static function assertActor(int $actor,array $s,string $action):void{$rows=self::registered();$row=collect($rows)->firstWhere('user_id',$actor);abort_unless($row&&['tenant_id'=>$row->tenant_id,'company_id'=>$row->company_id,'unit_id'=>$row->unit_id]===$s,403,'Perfil próprio de RH obrigatório.');if($action!=='view')abort_unless($row->profile_key===($action==='prepare'?'hr_prepare':'hr_review'),403);} + public function install():array{ + CompositionRuntime::assert();$p=self::policy();$pin=self::fingerprint($p);$expected=[];foreach(['prepare','review','read']as$role){$rights=[['permission'=>'company.view','scope'=>'company'],['permission'=>'unit.view','scope'=>'unit'],['permission'=>'human_resources.remunerations.view','scope'=>'unit']];if($role!=='read')$rights[]=['permission'=>'human_resources.remunerations.'.$role,'scope'=>'unit'];$expected['hr_'.$role]=$rights;}abort_unless(count($p['profiles'])===3&&collect($p['profiles'])->every(fn($r)=>isset($expected[$r['id']])&&$r['rights']===$expected[$r['id']]),403,'Não ampliar a matriz aprovada.'); + return DB::transaction(function()use($p,$pin){DB::selectOne("SELECT pg_advisory_xact_lock(hashtext('sgf-approved-human-resources-profiles'))");DB::table('companies')->where(['tenant_id'=>1,'id'=>2])->lockForUpdate()->firstOrFail(); + if(!DB::getSchemaBuilder()->hasTable('hr_qa_profiles')){DB::unprepared("CREATE TABLE hr_qa_profiles(profile_key text PRIMARY KEY CHECK(profile_key IN('hr_prepare','hr_review','hr_read')),user_id bigint UNIQUE NOT NULL REFERENCES users(id),tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,label text NOT NULL,source_hash char(64) NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id));CREATE TRIGGER hr_qa_profiles_immutable BEFORE UPDATE OR DELETE ON hr_qa_profiles FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit();REVOKE ALL ON TABLE hr_qa_profiles FROM PUBLIC;");} + $old=DB::table('hr_qa_profiles')->get();if($old->isNotEmpty()){abort_unless($old->count()===3&&$old->every(fn($r)=>hash_equals($pin,$r->source_hash)),409,'Preservar perfis RH diferentes.');return['profiles'=>$old->all(),'replayed'=>true,'source_hash'=>$pin,'new_grants'=>0];} + $check=DB::selectOne("SELECT pg_get_constraintdef(oid) d FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->d;abort_unless(preg_match('/^CHECK \((.*)\)$/s',$check,$m),503);if(!str_contains($check,'human_resources.remunerations.view')){DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');DB::statement('ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$m[1].") OR (unit_id IS NOT NULL AND permission IN('human_resources.remunerations.view','human_resources.remunerations.prepare','human_resources.remunerations.review')))");} + foreach($p['profiles']as$profile){$email='sgf-'.$profile['id'].'-'.CompositionRuntime::physical()['composition'].'@example.test';abort_if(User::where('email',$email)->exists(),409,'Identidade existente sem registo: conferir, sem adoptar.');$user=User::create(['name'=>$profile['label'],'email'=>$email,'password'=>bin2hex(random_bytes(32))]);DB::table('company_memberships')->insert(['user_id'=>$user->id,'tenant_id'=>1,'company_id'=>2]);foreach($profile['rights']as$r){$g=['user_id'=>$user->id,'tenant_id'=>1,'company_id'=>2,'unit_id'=>$r['scope']==='unit'?2:null,'permission'=>$r['permission']];DB::table('permission_grants')->insert($g);Audit::record('access.granted_on_creation',null,1,2,$g['unit_id'],$user->id,['permission'=>$r['permission'],'reason'=>'Matriz de RH exclusivamente QA autorizada11/11.']);}DB::table('hr_qa_profiles')->insert(['profile_key'=>$profile['id'],'user_id'=>$user->id]+$p['scope']+['label'=>$profile['label'],'source_hash'=>$pin]);Audit::record('access.synthetic_hr_profile_created',null,1,2,2,$user->id,['reason'=>'Perfil autónomo QA de RH expressamente autorizado.']);} + return['profiles'=>DB::table('hr_qa_profiles')->orderBy('profile_key')->get()->all(),'replayed'=>false,'source_hash'=>$pin,'new_grants'=>11]; + }); + } +} diff --git a/app/Shared/Installation/PresentationEntry.php b/app/Shared/Installation/PresentationEntry.php new file mode 100644 index 0000000..bef0d83 --- /dev/null +++ b/app/Shared/Installation/PresentationEntry.php @@ -0,0 +1,75 @@ +['sgf:read_only','/?start=service_invoice_setup','Comercial','Clientes, orçamentos, documentos e PDF; passagem explícita para recebíveis'], + 'purchasing'=>['purchasing:actor','/purchasing/workspace','Compras','Fornecedores, requisição, propostas, ordem, recepção, factura e obrigação'], + 'stock'=>['purchasing:validator','/stock/final','Stock','Artigos, armazéns, existências, trânsito, lotes, contagem e histórico'], + 'workshop'=>['sgf:workshop_manage','/?start=workshop','Oficinas','Equipamento, cliente, agenda, ordem, autorização e apontamentos verificados'], + 'finance'=>['sgf:read_only','/?start=service_receivables','Financeiro','Recebíveis, obrigações, Caixa, conferência de extracto e indicadores'], + 'accounting'=>['sgf:read_only','/general-ledger','Contabilidade','Origens, diário integrado, plano, balancete, razão e mapas fiscais internos'], + 'human_resources'=>['hr:hr_read','/?start=remunerations','Recursos Humanos','Cadastro fictício, regras artificiais, processamento interno e revisão distinta; acesso próprio'], + ]; + private function guard():void + { + abort_unless(in_array(request()->ip(),['127.0.0.1','::1'],true),404); + CompositionRuntime::assert(); + abort_unless(is_string(config('sgf_single_installation.profiles_sha256'))&&preg_match('/^[a-f0-9]{64}$/D',config('sgf_single_installation.profiles_sha256')),503); + } + + private function profiles():array + { + $pin=config('sgf_single_installation.profiles_sha256');$out=[];$context=app(CompanyContext::class); + $rows=DB::table('sgf_presentation_profiles')->orderBy('profile_key')->get(); + abort_unless($rows->count()===10&&$rows->every(fn($r)=>hash_equals($r->source_hash,$pin)),503,'Perfis QA não conferidos.'); + try{ + foreach($rows as $row){ + $context->selectFor((int)$row->user_id,(int)$row->tenant_id,(int)$row->company_id,(int)$row->unit_id); + $out['sgf:'.$row->profile_key]=['user_id'=>(int)$row->user_id,'label'=>$row->label,'scope'=>['tenant_id'=>(int)$row->tenant_id,'company_id'=>(int)$row->company_id,'unit_id'=>(int)$row->unit_id],'modules'=>app(ModuleNavigation::class)->available($context)]; + } + if(config('human_resources.enabled')===true){foreach(HumanResourcesProfiles::registered() as$row){ + $context->selectFor($row->user_id,$row->tenant_id,$row->company_id,$row->unit_id); + $out['hr:'.$row->profile_key]=['user_id'=>$row->user_id,'label'=>$row->label,'scope'=>['tenant_id'=>$row->tenant_id,'company_id'=>$row->company_id,'unit_id'=>$row->unit_id],'modules'=>app(ModuleNavigation::class)->available($context)]; + }} + $scope=config('purchasing_workspace.scope'); + foreach(config('purchasing_workspace.profiles',[]) as $key=>$profile){ + $context->selectFor($profile['user_id'],$scope['tenant_id'],$scope['company_id'],$scope['unit_id']); + $out['purchasing:'.$key]=['user_id'=>$profile['user_id'],'label'=>$profile['label'].' — Compras','scope'=>$scope,'modules'=>app(ModuleNavigation::class)->available($context)]; + } + }finally{$context->clear();} + return $out; + } + + public function screen(Request $r) + { + $this->guard();abort_if($r->query()!==[],422); + $profiles=$this->profiles();$tour=[];foreach(self::TOUR as $module=>[$key,$url,$label,$description])if(isset($profiles[$key])&&in_array($module,array_column($profiles[$key]['modules'],'id'),true))$tour[$module]=['profile'=>$key,'function'=>$profiles[$key]['label'],'label'=>$label,'description'=>$description]; + $primary=['commercial'=>['sgf:commercial_prepare','sgf:commercial_review'],'purchasing'=>array_map(fn($id)=>'purchasing:'.$id,['requester','approver','actor','issuer','validator','reviewer','auditor']),'stock'=>['purchasing:validator','sgf:workshop_verify','sgf:read_only'],'workshop'=>['sgf:workshop_manage','sgf:workshop_record','sgf:workshop_verify','sgf:finance_prepare','sgf:finance_review','purchasing:validator'],'finance'=>['sgf:finance_prepare','sgf:finance_review'],'accounting'=>['sgf:accounting_prepare','sgf:accounting_review','sgf:finance_prepare','sgf:finance_review']]; + if(config('human_resources.enabled')===true)$primary['human_resources']=['hr:hr_prepare','hr:hr_review','hr:hr_read']; + $moduleProfiles=[];foreach($primary as $module=>$keys)foreach($keys as $key)if(isset($profiles[$key])&&in_array($module,array_column($profiles[$key]['modules'],'id'),true))$moduleProfiles[$module][$key]=$profiles[$key]; + return response()->view('sgf-presentation-entry',['profiles'=>$profiles,'tour'=>$tour,'moduleProfiles'=>$moduleProfiles])->header('Cache-Control','no-store')->header('X-SGF-Installation',CompositionRuntime::physical()['composition']); + } + + public function enter(Request $r) + { + $this->guard();$d=$r->validate(['profile'=>'required|string|max:100','module_id'=>'required|string|max:30','landing'=>'sometimes|in:'.implode(',',array_keys(self::TOUR))]); + abort_if(array_diff(array_keys($r->all()),['_token','profile','module_id','landing']),422); + $target='/';if(isset($d['landing'])){$route=self::TOUR[$d['landing']];abort_unless($d['profile']===$route[0]&&$d['module_id']===$d['landing'],403);$target=$route[1];} + $profile=$this->profiles()[$d['profile']]??null; + abort_unless($profile&&in_array($d['module_id'],array_column($profile['modules'],'id'),true),403,'Função ou módulo não autorizado.'); + $user=User::findOrFail($profile['user_id']);auth()->login($user);$r->session()->regenerate(); + $r->session()->put('auth_version',$user->auth_version); + $r->session()->put('company_context',$profile['scope']+['module_id'=>$d['module_id'],'token'=>bin2hex(random_bytes(24))]); + \App\Modules\Identity\Services\Audit::record('context.synthetic_profile_selected',$user->id,$profile['scope']['tenant_id'],$profile['scope']['company_id'],$profile['scope']['unit_id'],details:['module_id'=>$d['module_id'],'reason'=>'Escolha explícita da função QA aprovada, na instalação única.']); + return redirect($target); + } +} diff --git a/app/Shared/Installation/SyntheticProfiles.php b/app/Shared/Installation/SyntheticProfiles.php new file mode 100644 index 0000000..0b5fb3f --- /dev/null +++ b/app/Shared/Installation/SyntheticProfiles.php @@ -0,0 +1,84 @@ +1,'company_id'=>2,'unit_id'=>2],403); + return $p; + } + + public static function fingerprint(array $p):string + { + return hash('sha256',json_encode(array_intersect_key($p,array_flip(['version','scope','profiles','existing_stock_profile','new_right_names','financial_source_policy'])),JSON_THROW_ON_ERROR)); + } + + public function install():array + { + CompositionSchema::identity();$p=self::policy();$pin=self::fingerprint($p);$scope=$p['scope']; + $known=[]; + foreach([base_path('app/Modules/Services/DocumentPreview.php'),base_path('app/Modules/Purchasing/ReceiptRuntime.php'),...glob(database_path('migrations/*.php'))] as $file){preg_match_all("/'([a-z_]+\\.[a-z_.]+)'/",file_get_contents($file),$matches);$known=[...$known,...$matches[1]];} + foreach(\App\Modules\Services\ServicePermissions::AREAS as $area)foreach(\App\Modules\Services\ServicePermissions::ACTIONS as $action)$known[]=$area.'.'.$action; + foreach(['view','prepare','validate','post','correct_prepare','correct_validate','correct_post','period_prepare','period_validate','period_close'] as $action)$known[]='general_ledger.'.$action; + foreach(['view','prepare','confirm'] as $action)$known[]='workshop_stock.'.$action; + $known=[...$known,'stock.view','stock.operations.prepare','stock.operations.confirm']; + foreach($p['profiles'] as $profile)foreach($profile['rights'] as $right)abort_unless(in_array($right['permission'],$known,true),422,'Direito fora do catálogo existente/aprovado: '.$right['permission']); + foreach($p['existing_stock_profile']['rights'] as $right)abort_unless(in_array($right,$known,true),422); + return DB::transaction(function()use($p,$pin,$scope){ + DB::selectOne("SELECT pg_advisory_xact_lock(hashtext('sgf-approved-presentation-profiles'))"); + DB::table('companies')->where('id',$scope['company_id'])->where('tenant_id',$scope['tenant_id'])->lockForUpdate()->firstOrFail(); + abort_unless(DB::table('units')->where(['tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'id'=>$scope['unit_id']])->exists(),403); + if(!DB::getSchemaBuilder()->hasTable('sgf_presentation_profiles')){ + DB::unprepared('CREATE TABLE sgf_presentation_profiles(profile_key varchar(80) PRIMARY KEY,user_id bigint NOT NULL UNIQUE REFERENCES users(id),tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,label text NOT NULL,source_hash char(64) NOT NULL,created_at timestamptz NOT NULL DEFAULT now())'); + DB::statement('REVOKE ALL ON TABLE sgf_presentation_profiles FROM PUBLIC'); + DB::statement('CREATE TRIGGER sgf_presentation_profiles_append_only BEFORE UPDATE OR DELETE ON sgf_presentation_profiles FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()'); + } + $existing=DB::table('sgf_presentation_profiles')->get(); + if($existing->isNotEmpty()){ + abort_unless($existing->count()===count($p['profiles'])&&$existing->every(fn($v)=>hash_equals($v->source_hash,$pin)),409,'A matriz de perfis mudou; não ampliar acessos por inferência.'); + return ['profiles'=>$existing->keyBy('profile_key')->map(fn($r)=>['user_id'=>(int)$r->user_id,'label'=>$r->label])->all(),'replayed'=>true,'source_hash'=>$pin]; + } + $company=[];$unit=[]; + foreach($p['profiles'] as $profile)foreach($profile['rights'] as $r){ + abort_unless(preg_match('/^[a-z][a-z_.]+$/D',$r['permission'])&&in_array($r['scope'],['company','unit'],true),422); + if($r['scope']==='company')$company[]=$r['permission'];else $unit[]=$r['permission']; + } + $unit=[...$unit,...$p['existing_stock_profile']['rights']]; + $old=DB::selectOne("SELECT pg_get_constraintdef(oid) value FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->value; + if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new \LogicException('Permission schema changed'); + $literal=fn($values)=>implode(',',array_map(fn($v)=>"'".$v."'",array_unique($values))); + DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check'); + DB::statement('ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$m[1].') OR (unit_id IS NULL AND permission IN ('.$literal($company).')) OR (unit_id IS NOT NULL AND permission IN ('.$literal($unit).')))'); + $profiles=[]; + foreach($p['profiles'] as $profile){ + abort_if(User::where('email','sgf-'.$profile['id'].'-'.CompositionRuntime::physical()['composition'].'@example.test')->exists(),409,'Conta existente não registada nesta matriz.'); + $user=User::create(['name'=>$profile['label'],'email'=>'sgf-'.$profile['id'].'-'.CompositionRuntime::physical()['composition'].'@example.test','password'=>bin2hex(random_bytes(32))]); + DB::table('company_memberships')->insert(['user_id'=>$user->id,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id']]); + foreach($profile['rights'] as $right)$this->grant($user->id,$scope,$right['permission'],$right['scope']==='unit'); + DB::table('sgf_presentation_profiles')->insert(['profile_key'=>$profile['id'],'user_id'=>$user->id]+$scope+['label'=>$profile['label'],'source_hash'=>$pin]); + Audit::record('access.synthetic_profile_created',null,$scope['tenant_id'],$scope['company_id'],$scope['unit_id'],$user->id,['operator'=>'Autorização expressa do proprietário em10/11/2026','reason'=>'Perfil exclusivamente sintético da matriz aprovada.']); + $profiles[$profile['id']]=['user_id'=>$user->id,'label'=>$profile['label']]; + } + $stock=(int)$p['existing_stock_profile']['user_id']; + abort_unless(DB::table('company_memberships')->where(['user_id'=>$stock,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id']])->exists(),403); + foreach($p['existing_stock_profile']['rights'] as $right)$this->grant($stock,$scope,$right,true); + return ['profiles'=>$profiles,'replayed'=>false,'source_hash'=>$pin]; + }); + } + + private function grant(int $user,array $scope,string $right,bool $unit):void + { + $values=['tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'unit_id'=>$unit?$scope['unit_id']:null,'user_id'=>$user,'permission'=>$right]; + if(DB::table('permission_grants')->where($values)->exists())return; + DB::table('permission_grants')->insert($values); + Audit::record('access.granted_on_creation',null,$scope['tenant_id'],$scope['company_id'],$values['unit_id'],$user,['permission'=>$right,'operator'=>'Autorização10/11/2026','reason'=>'Apenas perfil QA e âmbito da matriz aprovada; sem acesso real.']); + } +} diff --git a/app/Shared/Testing/TestingDatabaseGuard.php b/app/Shared/Testing/TestingDatabaseGuard.php index 777dc14..51fec26 100644 --- a/app/Shared/Testing/TestingDatabaseGuard.php +++ b/app/Shared/Testing/TestingDatabaseGuard.php @@ -9,6 +9,7 @@ final class TestingDatabaseGuard { public static function assertSafe(?string $connection = null): void { + if(app()->environment('budget-testing')&&config('sgf_single_installation.enabled')===true){\App\Shared\Installation\CompositionRuntime::assert($connection);return;} $name = $connection ?: config('database.default'); $config = config("database.connections.{$name}", []); if(app()->environment(['testing','browser-testing'])&&(config('sales_ledger.synthetic_enabled')===true||config('cost_ledger.synthetic_enabled')===true||config('industrial_ledger.synthetic_enabled')===true) diff --git a/bootstrap/app.php b/bootstrap/app.php index 1085719..050d441 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -8,6 +8,7 @@ use Illuminate\Http\Request; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/sgc.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/config/analysis_qa.php b/config/analysis_qa.php new file mode 100644 index 0000000..8c4387d --- /dev/null +++ b/config/analysis_qa.php @@ -0,0 +1,2 @@ +false]; diff --git a/config/bank_qa.php b/config/bank_qa.php new file mode 100644 index 0000000..265ebd4 --- /dev/null +++ b/config/bank_qa.php @@ -0,0 +1,2 @@ +false,'binding_sha256'=>null]; diff --git a/config/cash_qa.php b/config/cash_qa.php new file mode 100644 index 0000000..265ebd4 --- /dev/null +++ b/config/cash_qa.php @@ -0,0 +1,2 @@ +false,'binding_sha256'=>null]; diff --git a/config/commercial_quotes.php b/config/commercial_quotes.php new file mode 100644 index 0000000..8c4387d --- /dev/null +++ b/config/commercial_quotes.php @@ -0,0 +1,2 @@ +false]; diff --git a/config/fiscal_obligations.php b/config/fiscal_obligations.php new file mode 100644 index 0000000..356b93a --- /dev/null +++ b/config/fiscal_obligations.php @@ -0,0 +1,2 @@ +false,'schema_sha256'=>null]; diff --git a/config/fiscal_qa.php b/config/fiscal_qa.php new file mode 100644 index 0000000..265ebd4 --- /dev/null +++ b/config/fiscal_qa.php @@ -0,0 +1,2 @@ +false,'binding_sha256'=>null]; diff --git a/config/fiscal_rules_qa.php b/config/fiscal_rules_qa.php new file mode 100644 index 0000000..5a8858a --- /dev/null +++ b/config/fiscal_rules_qa.php @@ -0,0 +1,2 @@ +false,'enabled'=>false,'source_sha256'=>null]; diff --git a/config/hr_personnel.php b/config/hr_personnel.php new file mode 100644 index 0000000..ed14828 --- /dev/null +++ b/config/hr_personnel.php @@ -0,0 +1,2 @@ +false,'schema_sha256'=>null,'access_sha256'=>null]; diff --git a/config/human_resources.php b/config/human_resources.php new file mode 100644 index 0000000..48fe86e --- /dev/null +++ b/config/human_resources.php @@ -0,0 +1,2 @@ +false,'profiles_sha256'=>null]; diff --git a/config/internal_statements.php b/config/internal_statements.php new file mode 100644 index 0000000..356b93a --- /dev/null +++ b/config/internal_statements.php @@ -0,0 +1,2 @@ +false,'schema_sha256'=>null]; diff --git a/config/payable_due.php b/config/payable_due.php new file mode 100644 index 0000000..356b93a --- /dev/null +++ b/config/payable_due.php @@ -0,0 +1,2 @@ +false,'schema_sha256'=>null]; diff --git a/config/receipt_qa.php b/config/receipt_qa.php index cf4d579..68d9dc2 100644 --- a/config/receipt_qa.php +++ b/config/receipt_qa.php @@ -1,2 +1,2 @@ env('SGF_STOCK_PERIOD_QA_ENABLED',false),'stock_trace_enabled'=>env('SGF_STOCK_RECEIPT_TRACE_QA_ENABLED',false),'stock_read_enabled'=>env('SGF_COMPOSITION_STOCK_READ_QA_ENABLED',false),'obligation_evidence_enabled'=>env('SGF_OBLIGATION_EVIDENCE_QA_ENABLED',false),'invoice_progress_enabled'=>env('SGF_INVOICE_PROGRESS_QA_ENABLED',false),'request_history_enabled'=>env('SGF_REQUEST_HISTORY_QA_ENABLED',false),'continuity_enabled'=>env('SGF_PURCHASING_CONTINUITY_QA_ENABLED',false),'focus_enabled'=>env('SGF_PURCHASING_FOCUS_QA_ENABLED',false),'receipt_documents_enabled'=>env('SGF_RECEIPT_DOCUMENTS_QA_ENABLED',false),'enabled'=>env('SGF_RECEIPTS_QA_ENABLED',false),'orders_enabled'=>env('SGF_ORDERS_QA_ENABLED',false),'sourcing_enabled'=>env('SGF_SOURCING_QA_ENABLED',false),'supplier_documents_enabled'=>env('SGF_SUPPLIER_DOCUMENTS_QA_ENABLED',false),'suppliers_enabled'=>env('SGF_SUPPLIERS_QA_ENABLED',false),'dossiers_enabled'=>env('SGF_DOSSIERS_QA_ENABLED',false),'requests_enabled'=>env('SGF_REQUESTS_QA_ENABLED',false),'obligations_enabled'=>env('SGF_OBLIGATIONS_QA_ENABLED',false),'invoices_enabled'=>env('SGF_INVOICES_QA_ENABLED',false),'profile'=>env('SGF_RECEIPTS_QA_PROFILE','')]; +return ['stock_inactive_enabled'=>env('SGF_STOCK_INACTIVE_QA_ENABLED',false),'stock_period_enabled'=>env('SGF_STOCK_PERIOD_QA_ENABLED',false),'stock_trace_enabled'=>env('SGF_STOCK_RECEIPT_TRACE_QA_ENABLED',false),'stock_read_enabled'=>env('SGF_COMPOSITION_STOCK_READ_QA_ENABLED',false),'obligation_evidence_enabled'=>env('SGF_OBLIGATION_EVIDENCE_QA_ENABLED',false),'invoice_progress_enabled'=>env('SGF_INVOICE_PROGRESS_QA_ENABLED',false),'request_history_enabled'=>env('SGF_REQUEST_HISTORY_QA_ENABLED',false),'continuity_enabled'=>env('SGF_PURCHASING_CONTINUITY_QA_ENABLED',false),'focus_enabled'=>env('SGF_PURCHASING_FOCUS_QA_ENABLED',false),'receipt_documents_enabled'=>env('SGF_RECEIPT_DOCUMENTS_QA_ENABLED',false),'enabled'=>env('SGF_RECEIPTS_QA_ENABLED',false),'orders_enabled'=>env('SGF_ORDERS_QA_ENABLED',false),'sourcing_enabled'=>env('SGF_SOURCING_QA_ENABLED',false),'supplier_documents_enabled'=>env('SGF_SUPPLIER_DOCUMENTS_QA_ENABLED',false),'suppliers_enabled'=>env('SGF_SUPPLIERS_QA_ENABLED',false),'dossiers_enabled'=>env('SGF_DOSSIERS_QA_ENABLED',false),'requests_enabled'=>env('SGF_REQUESTS_QA_ENABLED',false),'obligations_enabled'=>env('SGF_OBLIGATIONS_QA_ENABLED',false),'invoices_enabled'=>env('SGF_INVOICES_QA_ENABLED',false),'profile'=>env('SGF_RECEIPTS_QA_PROFILE','')]; diff --git a/config/remunerations.php b/config/remunerations.php new file mode 100644 index 0000000..356b93a --- /dev/null +++ b/config/remunerations.php @@ -0,0 +1,2 @@ +false,'schema_sha256'=>null]; diff --git a/config/service_documents.php b/config/service_documents.php new file mode 100644 index 0000000..45742e9 --- /dev/null +++ b/config/service_documents.php @@ -0,0 +1,2 @@ +false,'invoice_receipt_enabled'=>false,'inputs_sha256'=>null]; diff --git a/config/sgc_invoice.php b/config/sgc_invoice.php new file mode 100644 index 0000000..06770f3 --- /dev/null +++ b/config/sgc_invoice.php @@ -0,0 +1,9 @@ + false, + // Provision separately; no credentials, principals or grants are shipped. + // client => [enabled, token_sha256, user_id, auth_version, tenant_id, + // company_id, unit_id, context_version] + 'principals' => [], +]; diff --git a/config/sgf_remaining_functions.php b/config/sgf_remaining_functions.php new file mode 100644 index 0000000..df4ef12 --- /dev/null +++ b/config/sgf_remaining_functions.php @@ -0,0 +1,3 @@ +false,'policy'=>'SGF-REMAINING-COMMERCIAL-STOCK-QA-v1','access_proposal'=>'docs/integration/sgf-remaining-functions-access-proposal.json']; diff --git a/config/sgf_single_installation.php b/config/sgf_single_installation.php new file mode 100644 index 0000000..ad38649 --- /dev/null +++ b/config/sgf_single_installation.php @@ -0,0 +1,4 @@ +false,'schema_sha256'=>null,'profiles_sha256'=>null,'bindings_sha256'=>null,'material_perimeter_sha256'=>null,'ledger_enabled'=>false,'finance_enabled'=>false]; diff --git a/config/stock_catalogue.php b/config/stock_catalogue.php new file mode 100644 index 0000000..eed6501 --- /dev/null +++ b/config/stock_catalogue.php @@ -0,0 +1,2 @@ +false,'binding_sha256'=>null,'schema_sha256'=>null]; diff --git a/config/stock_completion.php b/config/stock_completion.php new file mode 100644 index 0000000..265ebd4 --- /dev/null +++ b/config/stock_completion.php @@ -0,0 +1,2 @@ +false,'binding_sha256'=>null]; diff --git a/config/stock_operations.php b/config/stock_operations.php new file mode 100644 index 0000000..ac791b9 --- /dev/null +++ b/config/stock_operations.php @@ -0,0 +1,4 @@ + false, 'profile_sha256' => null, 'final_enabled'=>false, 'final_sha256'=>null]; diff --git a/config/workshop_catalogue.php b/config/workshop_catalogue.php new file mode 100644 index 0000000..8c4387d --- /dev/null +++ b/config/workshop_catalogue.php @@ -0,0 +1,2 @@ +false]; diff --git a/config/workshop_schedule.php b/config/workshop_schedule.php new file mode 100644 index 0000000..8c4387d --- /dev/null +++ b/config/workshop_schedule.php @@ -0,0 +1,2 @@ +false]; diff --git a/config/workshop_stock.php b/config/workshop_stock.php index 0aa4cd1..19ad416 100644 --- a/config/workshop_stock.php +++ b/config/workshop_stock.php @@ -1,2 +1,2 @@ []]; +return ['synthetic_sources'=>[],'composed_enabled'=>false,'composed_sha256'=>null]; diff --git a/database/migrations/2026_11_09_000001_create_sgc_integration_tables.php b/database/migrations/2026_11_09_000001_create_sgc_integration_tables.php new file mode 100644 index 0000000..4bcfb02 --- /dev/null +++ b/database/migrations/2026_11_09_000001_create_sgc_integration_tables.php @@ -0,0 +1,184 @@ +id(); + $table->string('sgc_entidade_tipo', 30); // Fornecedor, CentroCusto, Categoria + $table->string('sgc_entidade_id', 60); + $table->string('sgf_codigo_mestre', 60); + $table->string('sgf_descricao', 200)->nullable(); + $table->timestamp('data_sincronizacao')->useCurrent(); + $table->string('status', 20)->default('Ativo'); + + $table->unique(['sgc_entidade_tipo', 'sgc_entidade_id']); + $table->index('sgc_entidade_tipo'); + }); + + // Fornecedores (espelhado do SGC) + Schema::create('sgc_fornecedores', function (Blueprint $table) { + $table->id(); + $table->string('sgf_fornecedor_id', 40)->unique(); + $table->string('nuit', 20); + $table->string('nome_fornecedor', 200); + $table->string('nome_comercial', 200)->nullable(); + $table->string('categoria_fornecedor', 80); + $table->string('subcategoria', 80)->nullable(); + $table->string('pais', 3)->default('MOZ'); + $table->string('moeda_preferencial', 3)->default('MZN'); + $table->string('status_risco', 20)->default('Ativo'); + $table->boolean('tem_contrato_mestre')->default(false); + $table->date('data_cadastro')->useCurrent(); + $table->date('data_ultima_revisao')->nullable(); + $table->timestamps(); + + $table->index('categoria_fornecedor'); + $table->index('nuit'); + }); + + // Centros de Custo + Schema::create('sgc_centros_custo', function (Blueprint $table) { + $table->id(); + $table->string('sgf_centro_custo_id', 40)->unique(); + $table->string('codigo', 20)->unique(); + $table->string('nome_departamento', 150); + $table->string('director_responsavel', 150)->nullable(); + $table->string('regiao', 80)->nullable(); + $table->string('empresa', 100)->default('Tempo'); + $table->string('status', 20)->default('Ativo'); + $table->timestamps(); + }); + + // Ordens de Compra + Schema::create('sgc_ordens_compra', function (Blueprint $table) { + $table->id(); + $table->string('oc_id', 50)->unique(); + $table->string('requisicao_id', 50)->nullable(); + $table->date('data_oc'); + $table->foreignId('fornecedor_id')->constrained('sgc_fornecedores'); + $table->foreignId('centro_custo_id')->constrained('sgc_centros_custo'); + $table->string('categoria', 80); + $table->decimal('valor_comprometido', 18, 2); + $table->string('moeda', 3)->default('MZN'); + $table->decimal('taxa_cambio', 12, 6)->default(1.0); + $table->decimal('valor_comprometido_mzn', 18, 2) + ->storedAs('valor_comprometido * taxa_cambio'); + $table->integer('prazo_pagamento_dias')->default(30); + $table->date('data_entrega_prevista')->nullable(); + $table->string('status', 20)->default('Emitida'); + $table->boolean('cabimentado')->default(false); + $table->string('sgf_compromisso_id', 50)->nullable(); + $table->timestamps(); + + $table->index('fornecedor_id'); + $table->index('data_oc'); + $table->index('status'); + }); + + // Faturas + Schema::create('sgc_faturas', function (Blueprint $table) { + $table->id(); + $table->string('fatura_id', 50)->unique(); + $table->foreignId('ordem_compra_id')->constrained('sgc_ordens_compra'); + $table->foreignId('fornecedor_id')->constrained('sgc_fornecedores'); + $table->date('data_fatura'); + $table->date('data_vencimento')->nullable(); + $table->date('data_pagamento')->nullable(); + $table->decimal('valor_fatura', 18, 2); + $table->decimal('valor_pago', 18, 2)->nullable(); + $table->decimal('desconto_obtido', 18, 2)->default(0); + $table->decimal('iva_valor', 18, 2)->default(0); + $table->string('moeda', 3)->default('MZN'); + $table->decimal('taxa_cambio', 12, 6)->default(1.0); + $table->decimal('valor_fatura_mzn', 18, 2) + ->storedAs('valor_fatura * taxa_cambio'); + $table->string('status_3_way_match', 20)->default('Pendente'); + $table->string('status_pagamento', 20)->default('Aguardando'); + $table->string('numero_comprovativo', 40)->nullable(); + $table->text('motivo_excecao')->nullable(); + $table->string('sgf_doc_number', 40)->unique(); + $table->timestamp('sgf_sync_timestamp')->useCurrent(); + $table->timestamps(); + + $table->index('ordem_compra_id'); + $table->index('fornecedor_id'); + $table->index('data_fatura'); + $table->index('data_pagamento'); + $table->index('status_pagamento'); + }); + + // Recebimentos (Goods Receipt) + Schema::create('sgc_recebimentos', function (Blueprint $table) { + $table->id(); + $table->string('recebimento_id', 50)->unique(); + $table->foreignId('ordem_compra_id')->constrained('sgc_ordens_compra'); + $table->date('data_recebimento'); + $table->decimal('quantidade_recebida', 18, 4); + $table->decimal('valor_recebido', 18, 2); + $table->string('moeda', 3)->default('MZN'); + $table->boolean('divergencia_valor')->default(false); + $table->boolean('divergencia_qtd')->default(false); + $table->text('observacoes')->nullable(); + $table->timestamps(); + + $table->index('ordem_compra_id'); + $table->index('data_recebimento'); + }); + + // Log de integração (idempotência e auditoria) + Schema::create('sgc_integration_log', function (Blueprint $table) { + $table->id(); + $table->uuid('idempotency_key')->unique(); + $table->string('endpoint', 100); + $table->string('event_type', 50); + $table->json('payload'); + $table->string('status', 20); // ACCEPTED, REJECTED, DUPLICATE + $table->json('response')->nullable(); + $table->string('error_code', 40)->nullable(); + $table->text('error_message')->nullable(); + $table->integer('attempts')->default(1); + $table->timestamps(); + + $table->index('event_type'); + $table->index('status'); + $table->index('created_at'); + }); + + // Dead Letter Queue + Schema::create('sgc_integration_dead_letter', function (Blueprint $table) { + $table->id(); + $table->uuid('idempotency_key'); + $table->string('endpoint', 100); + $table->json('payload'); + $table->string('error_code', 40); + $table->text('error_message')->nullable(); + $table->smallInteger('attempts')->default(0); + $table->timestamp('next_retry_at')->nullable(); + $table->timestamp('resolved_at')->nullable(); + $table->string('resolution', 40)->nullable(); + $table->timestamps(); + + $table->index('error_code'); + $table->index('next_retry_at'); + }); + } + + public function down(): void + { + Schema::dropIfExists('sgc_integration_dead_letter'); + Schema::dropIfExists('sgc_integration_log'); + Schema::dropIfExists('sgc_recebimentos'); + Schema::dropIfExists('sgc_faturas'); + Schema::dropIfExists('sgc_ordens_compra'); + Schema::dropIfExists('sgc_centros_custo'); + Schema::dropIfExists('sgc_fornecedores'); + Schema::dropIfExists('sgc_mapping'); + } +}; diff --git a/database/migrations/2026_11_09_000002_create_sgc_analytics_tables.php b/database/migrations/2026_11_09_000002_create_sgc_analytics_tables.php new file mode 100644 index 0000000..74ff2b1 --- /dev/null +++ b/database/migrations/2026_11_09_000002_create_sgc_analytics_tables.php @@ -0,0 +1,212 @@ +integer('data_id')->primary(); // YYYYMMDD + $table->date('data_completa')->unique(); + $table->smallInteger('dia_semana'); + $table->string('nome_dia_semana', 12); + $table->smallInteger('dia_mes'); + $table->smallInteger('mes'); + $table->string('nome_mes', 12); + $table->smallInteger('trimestre'); + $table->smallInteger('ano'); + $table->string('ano_mes', 7); // '2026-11' + $table->boolean('eh_dia_util')->default(true); + $table->boolean('eh_feriado')->default(false); + $table->smallInteger('semestre'); + + $table->index(['ano', 'mes']); + $table->index(['ano', 'trimestre']); + }); + + // Dimensão Fornecedor + Schema::create('analytics_dim_fornecedor', function (Blueprint $table) { + $table->id(); + $table->string('sgf_fornecedor_id', 40)->unique(); + $table->string('nuit', 20); + $table->string('nome_fornecedor', 200); + $table->string('nome_comercial', 200)->nullable(); + $table->string('categoria_fornecedor', 80); + $table->string('subcategoria', 80)->nullable(); + $table->string('pais', 3)->default('MOZ'); + $table->string('moeda_preferencial', 3)->default('MZN'); + $table->string('status_risco', 20)->default('Ativo'); + $table->boolean('tem_contrato_mestre')->default(false); + $table->date('data_cadastro'); + $table->date('data_ultima_revisao')->nullable(); + $table->timestamps(); + + $table->index('categoria_fornecedor'); + $table->index('nuit'); + }); + + // Dimensão Organização + Schema::create('analytics_dim_organizacao', function (Blueprint $table) { + $table->id(); + $table->string('sgf_centro_custo_id', 40)->unique(); + $table->string('codigo', 20)->unique(); + $table->string('nome_departamento', 150); + $table->string('director_responsavel', 150)->nullable(); + $table->string('regiao', 80)->nullable(); + $table->string('empresa', 100)->default('Tempo'); + $table->string('status', 20)->default('Ativo'); + $table->timestamps(); + }); + + // Dimensão Categoria de Despesa + Schema::create('analytics_dim_categoria_despesa', function (Blueprint $table) { + $table->id(); + $table->string('codigo', 20)->unique(); + $table->string('nivel_1', 80); // ex: 'Despesas Operacionais' + $table->string('nivel_2', 80); // ex: 'Tecnologia de Informação' + $table->string('nivel_3', 80)->nullable(); // ex: 'Licenças de Software' + $table->string('tipo', 20); // Bem, Servico, Misto + $table->boolean('eh_capex')->default(false); + $table->timestamps(); + + $table->index(['nivel_1', 'nivel_2']); + }); + + // Dimensão Usuário + Schema::create('analytics_dim_usuario', function (Blueprint $table) { + $table->id(); + $table->string('codigo_colaborador', 20)->unique(); + $table->string('nome_completo', 150); + $table->string('email', 150)->nullable(); + $table->string('cargo', 80)->nullable(); + $table->foreignId('departamento_id') + ->constrained('analytics_dim_organizacao'); + $table->date('data_admissao')->nullable(); + $table->string('status', 20)->default('Ativo'); + $table->timestamps(); + + $table->index('departamento_id'); + }); + + // Facto Ordem de Compra + Schema::create('analytics_fact_ordem_compra', function (Blueprint $table) { + $table->bigInteger('oc_id')->primary(); + $table->bigInteger('requisicao_id')->nullable(); + $table->integer('data_oc_id'); + $table->foreignId('fornecedor_id') + ->constrained('analytics_dim_fornecedor'); + $table->foreignId('departamento_id') + ->constrained('analytics_dim_organizacao'); + $table->foreignId('categoria_id') + ->constrained('analytics_dim_categoria_despesa'); + $table->decimal('valor_comprometido', 18, 2); + $table->string('moeda', 3)->default('MZN'); + $table->decimal('taxa_cambio', 12, 6)->default(1.0); + $table->decimal('valor_comprometido_mzn', 18, 2) + ->storedAs('valor_comprometido * taxa_cambio'); + $table->smallInteger('prazo_pagamento_dias')->default(30); + $table->date('data_entrega_prevista')->nullable(); + $table->string('status', 20); + $table->timestamps(); + + $table->index('fornecedor_id'); + $table->index('data_oc_id'); + $table->index('status'); + }); + + // Facto Fatura e Pagamento + Schema::create('analytics_fact_fatura_pagamento', function (Blueprint $table) { + $table->bigInteger('fatura_id')->primary(); + $table->bigInteger('oc_id'); + $table->foreignId('fornecedor_id') + ->constrained('analytics_dim_fornecedor'); + $table->integer('data_fatura_id'); + $table->integer('data_vencimento_id')->nullable(); + $table->integer('data_pagamento_id')->nullable(); + $table->decimal('valor_fatura', 18, 2); + $table->decimal('valor_pago', 18, 2)->nullable(); + $table->decimal('desconto_obtido', 18, 2)->default(0); + $table->decimal('iva_valor', 18, 2)->default(0); + $table->string('moeda', 3)->default('MZN'); + $table->decimal('taxa_cambio', 12, 6)->default(1.0); + $table->decimal('valor_fatura_mzn', 18, 2) + ->storedAs('valor_fatura * taxa_cambio'); + $table->string('status_3_way_match', 20); + $table->string('status_pagamento', 20); + $table->string('numero_comprovativo', 40)->nullable(); + $table->text('motivo_excecao')->nullable(); + $table->string('sgf_doc_number', 40)->unique(); + $table->timestamp('sgf_sync_timestamp')->useCurrent(); + $table->timestamps(); + + $table->index('oc_id'); + $table->index('fornecedor_id'); + $table->index('data_fatura_id'); + $table->index('data_pagamento_id'); + $table->index('status_pagamento'); + }); + + // View Materializada (agregado mensal) + DB::statement(' + CREATE MATERIALIZED VIEW analytics_mv_spend_mensal AS + SELECT + t.ano, + t.mes, + t.ano_mes, + fp.fornecedor_id, + df.nome_fornecedor, + df.categoria_fornecedor, + org.id AS departamento_id, + org.nome_departamento, + dc.nivel_1, + dc.nivel_2, + COUNT(*) AS total_faturas, + SUM(fp.valor_fatura_mzn) AS valor_total_mzn, + SUM(fp.desconto_obtido * fp.taxa_cambio) AS desconto_total_mzn, + AVG(fp.valor_fatura_mzn) AS ticket_medio_mzn, + COUNT(*) FILTER (WHERE fp.status_3_way_match = \'Conforme\') AS faturas_conformes, + ROUND( + COUNT(*) FILTER (WHERE fp.status_3_way_match = \'Conforme\')::NUMERIC / + NULLIF(COUNT(*), 0) * 100, 2 + ) AS taxa_sucesso_3way_match + FROM analytics_fact_fatura_pagamento fp + JOIN analytics_dim_tempo t ON fp.data_fatura_id = t.data_id + JOIN analytics_dim_fornecedor df ON fp.fornecedor_id = df.id + JOIN analytics_dim_organizacao org ON org.id = ( + SELECT oc.departamento_id + FROM analytics_fact_ordem_compra oc + WHERE oc.oc_id = fp.oc_id + ) + JOIN analytics_dim_categoria_despesa dc ON dc.id = ( + SELECT oc.categoria_id + FROM analytics_fact_ordem_compra oc + WHERE oc.oc_id = fp.oc_id + ) + GROUP BY t.ano, t.mes, t.ano_mes, fp.fornecedor_id, df.nome_fornecedor, + df.categoria_fornecedor, org.id, org.nome_departamento, + dc.nivel_1, dc.nivel_2 + '); + + DB::statement(' + CREATE UNIQUE INDEX idx_mv_spend_mensal_pk + ON analytics_mv_spend_mensal (ano, mes, fornecedor_id, departamento_id, nivel_1, nivel_2) + '); + } + + public function down(): void + { + DB::statement('DROP MATERIALIZED VIEW IF EXISTS analytics_mv_spend_mensal'); + Schema::dropIfExists('analytics_fact_fatura_pagamento'); + Schema::dropIfExists('analytics_fact_ordem_compra'); + Schema::dropIfExists('analytics_dim_usuario'); + Schema::dropIfExists('analytics_dim_categoria_despesa'); + Schema::dropIfExists('analytics_dim_organizacao'); + Schema::dropIfExists('analytics_dim_fornecedor'); + Schema::dropIfExists('analytics_dim_tempo'); + } +}; diff --git a/database/sql/bank_reconciliation.sql b/database/sql/bank_reconciliation.sql new file mode 100644 index 0000000..6e30425 --- /dev/null +++ b/database/sql/bank_reconciliation.sql @@ -0,0 +1,31 @@ +CREATE TABLE br_statements(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,statement_key uuid NOT NULL,account_ref text NOT NULL,currency text NOT NULL CHECK(currency='MZN'),period text NOT NULL,csv_sha256 char(64) NOT NULL,csv_bytes text NOT NULL,opening_minor bigint NOT NULL,closing_minor bigint NOT NULL,entries jsonb NOT NULL,source_hash char(64) NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(id,tenant_id,company_id,unit_id),UNIQUE(tenant_id,company_id,unit_id,statement_key),UNIQUE(tenant_id,company_id,unit_id,account_ref,csv_sha256),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)); +CREATE TABLE br_events(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,statement_id bigint NOT NULL,version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES br_events(id),proposal_id bigint UNIQUE REFERENCES br_events(id),action text NOT NULL CHECK(action IN('import','review_statement','prepare_match','review_match','finalize')),state text NOT NULL CHECK(state IN('imported','statement_reviewed','statement_refused','match_prepared','match_reviewed','match_refused','finalized')),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(statement_id,version),FOREIGN KEY(statement_id,tenant_id,company_id,unit_id) REFERENCES br_statements(id,tenant_id,company_id,unit_id)); +CREATE UNIQUE INDEX br_once_ledger_line ON br_events(tenant_id,company_id,unit_id,((data->>'ledger_line_id')::bigint)) WHERE state='match_reviewed'; +CREATE UNIQUE INDEX br_once_statement_row ON br_events(statement_id,(data->>'row_reference')) WHERE state='match_reviewed'; +CREATE TABLE br_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)); +CREATE FUNCTION sgf_br_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE permission_name text;lastrow br_events;p br_events;header br_statements; +BEGIN + PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE tenant_id=NEW.tenant_id AND id=NEW.company_id FOR UPDATE; + IF TG_TABLE_NAME='br_statements' THEN permission_name:='bank_reconciliation.import';ELSE permission_name:='bank_reconciliation.'||CASE NEW.action WHEN 'import' THEN 'import' WHEN 'prepare_match' THEN 'prepare' ELSE 'review' END;END IF; + IF NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=permission_name) THEN RAISE EXCEPTION 'SGF_BR_ACCESS';END IF; + IF TG_TABLE_NAME='br_statements' THEN RETURN NEW;END IF; + SELECT * INTO header FROM br_statements WHERE id=NEW.statement_id AND (tenant_id,company_id,unit_id)=(NEW.tenant_id,NEW.company_id,NEW.unit_id);IF header.id IS NULL THEN RAISE EXCEPTION 'SGF_BR_SCOPE';END IF; + SELECT * INTO lastrow FROM br_events WHERE statement_id=NEW.statement_id ORDER BY version DESC LIMIT 1;IF NEW.version<>coalesce(lastrow.version,0)+1 OR NEW.previous_id IS DISTINCT FROM lastrow.id OR EXISTS(SELECT 1 FROM br_events WHERE statement_id=NEW.statement_id AND state='finalized') THEN RAISE EXCEPTION 'SGF_BR_STALE_OR_FINAL';END IF; + IF NEW.action='import' THEN IF lastrow.id IS NOT NULL OR NEW.state<>'imported' OR NEW.actor_id<>header.actor_id THEN RAISE EXCEPTION 'SGF_BR_IMPORT';END IF; + ELSIF NEW.action='review_statement' THEN IF NEW.actor_id=header.actor_id OR NEW.prepared_by<>header.actor_id OR NEW.state NOT IN('statement_reviewed','statement_refused') OR EXISTS(SELECT 1 FROM br_events WHERE statement_id=NEW.statement_id AND action='review_statement') THEN RAISE EXCEPTION 'SGF_BR_REVIEW';END IF; + ELSE IF NOT EXISTS(SELECT 1 FROM br_events WHERE statement_id=NEW.statement_id AND state='statement_reviewed') THEN RAISE EXCEPTION 'SGF_BR_NOT_REVIEWED';END IF; + IF NEW.action='prepare_match' THEN IF NEW.actor_id<>NEW.prepared_by OR NEW.state<>'match_prepared' THEN RAISE EXCEPTION 'SGF_BR_PREPARE';END IF; + ELSIF NEW.action='review_match' THEN SELECT * INTO p FROM br_events WHERE id=NEW.proposal_id AND statement_id=NEW.statement_id AND state='match_prepared';IF p.id IS NULL OR NEW.actor_id=p.actor_id OR NEW.prepared_by<>p.prepared_by OR NEW.data IS DISTINCT FROM p.data OR NEW.state NOT IN('match_reviewed','match_refused') THEN RAISE EXCEPTION 'SGF_BR_MATCH_REVIEW';END IF; + ELSIF NEW.action='finalize' THEN IF NEW.actor_id=header.actor_id OR NEW.state<>'finalized' OR (SELECT count(*) FROM br_events WHERE statement_id=NEW.statement_id AND state='match_reviewed')<>jsonb_array_length(header.entries) OR EXISTS(SELECT 1 FROM br_events proposed WHERE proposed.statement_id=NEW.statement_id AND proposed.state='match_prepared' AND NOT EXISTS(SELECT 1 FROM br_events r WHERE r.proposal_id=proposed.id)) THEN RAISE EXCEPTION 'SGF_BR_INCOMPLETE';END IF;END IF;END IF; + RETURN NEW; +END; $$; +CREATE TRIGGER br_statement_guard BEFORE INSERT ON br_statements FOR EACH ROW EXECUTE FUNCTION sgf_br_guard(); +CREATE TRIGGER br_event_guard BEFORE INSERT ON br_events FOR EACH ROW EXECUTE FUNCTION sgf_br_guard(); +CREATE TRIGGER br_statements_immutable BEFORE UPDATE OR DELETE ON br_statements FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER br_events_immutable BEFORE UPDATE OR DELETE ON br_events FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER br_commands_immutable BEFORE UPDATE OR DELETE ON br_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER br_commands_test BEFORE INSERT ON br_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only(); +REVOKE ALL ON br_statements,br_events,br_commands FROM PUBLIC; +REVOKE ALL ON SEQUENCE br_statements_id_seq,br_events_id_seq,br_commands_id_seq FROM PUBLIC; +REVOKE ALL ON FUNCTION sgf_br_guard() FROM PUBLIC; diff --git a/database/sql/cash.sql b/database/sql/cash.sql new file mode 100644 index 0000000..08cf3b6 --- /dev/null +++ b/database/sql/cash.sql @@ -0,0 +1,24 @@ +CREATE TABLE cash_events(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,session_key uuid NOT NULL,version bigint NOT NULL CHECK(version>0),action text NOT NULL CHECK(action IN('prepare','review','close')),kind text NOT NULL CHECK(kind IN('open','receipt','expense','count')),state text NOT NULL CHECK(state IN('proposed','approved','refused','closed')),proposal_id bigint REFERENCES cash_events(id),source_receipt_id bigint REFERENCES sr_records(id),actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,dependencies jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,session_key,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)); +CREATE UNIQUE INDEX cash_one_review ON cash_events(proposal_id) WHERE action='review'; +CREATE UNIQUE INDEX cash_one_receipt ON cash_events(source_receipt_id) WHERE action='review' AND kind='receipt' AND state='approved'; +CREATE UNIQUE INDEX cash_one_close ON cash_events(tenant_id,company_id,unit_id,session_key) WHERE action='close'; +CREATE UNIQUE INDEX cash_one_open ON cash_events(tenant_id,company_id,unit_id,session_key) WHERE action='review' AND kind='open' AND state='approved'; +CREATE TABLE cash_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)); +CREATE FUNCTION sgf_cash_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE latest bigint;p cash_events;permission_name text; +BEGIN + PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE id=NEW.company_id AND tenant_id=NEW.tenant_id FOR UPDATE; + permission_name:='cash.'||CASE NEW.action WHEN 'prepare' THEN 'prepare' WHEN 'review' THEN 'review' ELSE 'close' END; + IF NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=permission_name) THEN RAISE EXCEPTION 'SGF_CASH_ACCESS'; END IF; + SELECT coalesce(max(version),0) INTO latest FROM cash_events WHERE (tenant_id,company_id,unit_id,session_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.session_key); + IF NEW.version<>latest+1 OR EXISTS(SELECT 1 FROM cash_events WHERE (tenant_id,company_id,unit_id,session_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.session_key) AND state='closed') THEN RAISE EXCEPTION 'SGF_CASH_STALE_OR_CLOSED'; END IF; + IF NEW.action='review' THEN SELECT * INTO STRICT p FROM cash_events WHERE id=NEW.proposal_id AND action='prepare';IF ROW(p.tenant_id,p.company_id,p.unit_id,p.session_key,p.kind) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.session_key,NEW.kind) OR p.actor_id=NEW.actor_id OR NEW.data IS DISTINCT FROM p.data OR NEW.source_receipt_id IS DISTINCT FROM p.source_receipt_id THEN RAISE EXCEPTION 'SGF_CASH_SELF_OR_SOURCE'; END IF; END IF; + RETURN NEW; +END; $$; +CREATE TRIGGER cash_guard BEFORE INSERT ON cash_events FOR EACH ROW EXECUTE FUNCTION sgf_cash_guard(); +CREATE TRIGGER cash_events_immutable BEFORE UPDATE OR DELETE ON cash_events FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER cash_commands_immutable BEFORE UPDATE OR DELETE ON cash_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER cash_commands_test BEFORE INSERT ON cash_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only(); +REVOKE ALL ON cash_events,cash_commands FROM PUBLIC; +REVOKE ALL ON SEQUENCE cash_events_id_seq,cash_commands_id_seq FROM PUBLIC; +REVOKE ALL ON FUNCTION sgf_cash_guard() FROM PUBLIC; diff --git a/database/sql/commercial_quotes.sql b/database/sql/commercial_quotes.sql new file mode 100644 index 0000000..e99c71e --- /dev/null +++ b/database/sql/commercial_quotes.sql @@ -0,0 +1,26 @@ +CREATE TABLE cq_versions( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + quote_key uuid NOT NULL,version int NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES cq_versions(id), + action text NOT NULL CHECK(action IN('prepare','review','customer','convert')),state text NOT NULL CHECK(state IN('draft','approved','rejected','customer_accepted','customer_declined','converted')), + client_id bigint NOT NULL REFERENCES a_clients(id),source_order_id bigint REFERENCES wo_orders(id),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id), + data jsonb NOT NULL,dependencies jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(tenant_id,company_id,unit_id,quote_key,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id) +); +CREATE TABLE cq_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)); +CREATE TABLE cq_conversions(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,quote_key uuid NOT NULL,quote_version_id bigint NOT NULL UNIQUE REFERENCES cq_versions(id),service_draft_id bigint NOT NULL UNIQUE REFERENCES a_service_drafts(id),billing_draft_id bigint NOT NULL UNIQUE REFERENCES a_billing_drafts(id),source_order_id bigint UNIQUE REFERENCES wo_orders(id),actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,quote_key)); +CREATE FUNCTION sgf_cq_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE p cq_versions; right_name text; +BEGIN + PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE; + right_name:=CASE NEW.action WHEN 'review' THEN 'commercial_quotes.review' WHEN 'convert' THEN 'commercial_quotes.convert' ELSE 'commercial_quotes.prepare' END; + IF NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=right_name) + OR NOT EXISTS(SELECT 1 FROM a_clients WHERE id=NEW.client_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) + THEN RAISE EXCEPTION 'SGF_CQ_AUTHORITY_OR_SCOPE'; END IF; + SELECT * INTO p FROM cq_versions WHERE (tenant_id,company_id,unit_id,quote_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.quote_key) ORDER BY version DESC LIMIT 1; + IF (p.id IS NULL AND (NEW.version<>1 OR NEW.previous_id IS NOT NULL OR NEW.action<>'prepare')) OR (p.id IS NOT NULL AND (NEW.previous_id IS DISTINCT FROM p.id OR NEW.version<>p.version+1 OR p.state='converted')) THEN RAISE EXCEPTION 'SGF_CQ_STALE_OR_CONVERTED'; END IF; + IF NEW.action='review' AND (p.state<>'draft' OR NEW.actor_id=NEW.prepared_by) THEN RAISE EXCEPTION 'SGF_CQ_SELF_OR_STALE_REVIEW'; END IF; + IF NEW.action='customer' AND p.state<>'approved' THEN RAISE EXCEPTION 'SGF_CQ_CUSTOMER_BEFORE_REVIEW'; END IF; + IF NEW.action='convert' AND (p.state<>'customer_accepted' OR (NEW.data->>'valid_until')::date<(now() AT TIME ZONE 'Africa/Maputo')::date) THEN RAISE EXCEPTION 'SGF_CQ_NOT_CONVERTIBLE'; END IF; + RETURN NEW; +END; $$; +CREATE TRIGGER sgf_cq_guard BEFORE INSERT ON cq_versions FOR EACH ROW EXECUTE FUNCTION sgf_cq_guard(); diff --git a/database/sql/financial_indicators.sql b/database/sql/financial_indicators.sql new file mode 100644 index 0000000..9ddff0b --- /dev/null +++ b/database/sql/financial_indicators.sql @@ -0,0 +1,25 @@ +CREATE TABLE an_criteria(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,metric text NOT NULL CHECK(metric IN('cash_coverage_single_payable','cash_expense_share')),version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES an_criteria(id),action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','reviewed','refused')),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),definition jsonb NOT NULL,source_hash char(64) NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,metric,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)); +CREATE TABLE an_results(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),criterion_id bigint REFERENCES an_criteria(id),input jsonb NOT NULL,result jsonb NOT NULL,source_snapshot jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)); +CREATE TABLE an_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)); +CREATE FUNCTION sgf_an_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE p an_criteria;permission_name text; +BEGIN + PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE tenant_id=NEW.tenant_id AND id=NEW.company_id FOR UPDATE; + IF TG_TABLE_NAME='an_results' THEN permission_name:='financial_analysis.prepare';ELSE permission_name:='financial_analysis.'||NEW.action;END IF; + IF NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=permission_name) THEN RAISE EXCEPTION 'SGF_AN_ACCESS';END IF; + IF TG_TABLE_NAME='an_results' THEN RETURN NEW;END IF; + SELECT * INTO p FROM an_criteria WHERE (tenant_id,company_id,unit_id,metric)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.metric) ORDER BY version DESC LIMIT 1; + IF NEW.version<>coalesce(p.version,0)+1 OR NEW.previous_id IS DISTINCT FROM p.id THEN RAISE EXCEPTION 'SGF_AN_STALE';END IF; + IF NEW.action='prepare' THEN IF NEW.state<>'prepared' OR NEW.prepared_by<>NEW.actor_id THEN RAISE EXCEPTION 'SGF_AN_PREPARE';END IF; + ELSE IF p.id IS NULL OR p.state<>'prepared' OR NEW.prepared_by<>p.prepared_by OR NEW.actor_id=p.prepared_by OR NEW.definition IS DISTINCT FROM p.definition OR NEW.source_hash<>p.source_hash OR NEW.state NOT IN('reviewed','refused') THEN RAISE EXCEPTION 'SGF_AN_REVIEW';END IF;END IF; + RETURN NEW; +END; $$; +CREATE TRIGGER an_criterion_guard BEFORE INSERT ON an_criteria FOR EACH ROW EXECUTE FUNCTION sgf_an_guard(); +CREATE TRIGGER an_result_guard BEFORE INSERT ON an_results FOR EACH ROW EXECUTE FUNCTION sgf_an_guard(); +CREATE TRIGGER an_criteria_immutable BEFORE UPDATE OR DELETE ON an_criteria FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER an_results_immutable BEFORE UPDATE OR DELETE ON an_results FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER an_commands_immutable BEFORE UPDATE OR DELETE ON an_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER an_commands_test BEFORE INSERT ON an_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only(); +REVOKE ALL ON an_criteria,an_results,an_commands FROM PUBLIC; +REVOKE ALL ON SEQUENCE an_criteria_id_seq,an_results_id_seq,an_commands_id_seq FROM PUBLIC; +REVOKE ALL ON FUNCTION sgf_an_guard() FROM PUBLIC; diff --git a/database/sql/fiscal_assessment.sql b/database/sql/fiscal_assessment.sql new file mode 100644 index 0000000..844ece8 --- /dev/null +++ b/database/sql/fiscal_assessment.sql @@ -0,0 +1,25 @@ +CREATE TABLE fx_versions(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,process_key uuid NOT NULL,kind text NOT NULL CHECK(kind IN('assessment','form')),version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES fx_versions(id),action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','reviewed','refused')),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,dependencies jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,process_key,kind,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)); +CREATE TABLE fx_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)); +CREATE FUNCTION sgf_fx_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE p fx_versions;latest bigint;prefix text; +BEGIN + PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE tenant_id=NEW.tenant_id AND id=NEW.company_id FOR UPDATE; + prefix:=CASE NEW.kind WHEN 'assessment' THEN 'fiscal_assessments.' ELSE 'fiscal_forms.' END; + IF NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=prefix||NEW.action) THEN RAISE EXCEPTION 'SGF_FX_ACCESS'; END IF; + IF NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) THEN RAISE EXCEPTION 'SGF_FX_MEMBER'; END IF; + SELECT coalesce(max(version),0) INTO latest FROM fx_versions WHERE (tenant_id,company_id,unit_id,process_key,kind)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.process_key,NEW.kind); + IF NEW.version<>latest+1 THEN RAISE EXCEPTION 'SGF_FX_STALE'; END IF; + IF latest=0 THEN IF NEW.previous_id IS NOT NULL OR NEW.action<>'prepare' THEN RAISE EXCEPTION 'SGF_FX_FIRST'; END IF; + ELSE SELECT * INTO p FROM fx_versions WHERE (tenant_id,company_id,unit_id,process_key,kind,version)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.process_key,NEW.kind,latest); + IF NEW.previous_id IS DISTINCT FROM p.id THEN RAISE EXCEPTION 'SGF_FX_PARENT'; END IF; END IF; + IF NEW.action='review' THEN IF p.state<>'prepared' OR NEW.prepared_by<>p.prepared_by OR NEW.actor_id=p.prepared_by OR NEW.state NOT IN('reviewed','refused') OR NEW.data IS DISTINCT FROM p.data OR NEW.dependencies IS DISTINCT FROM p.dependencies THEN RAISE EXCEPTION 'SGF_FX_REVIEW'; END IF; + ELSE IF NEW.prepared_by<>NEW.actor_id OR NEW.state<>'prepared' THEN RAISE EXCEPTION 'SGF_FX_PREPARE'; END IF; END IF; + RETURN NEW; +END; $$; +CREATE TRIGGER fx_guard BEFORE INSERT ON fx_versions FOR EACH ROW EXECUTE FUNCTION sgf_fx_guard(); +CREATE TRIGGER fx_versions_immutable BEFORE UPDATE OR DELETE ON fx_versions FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER fx_commands_immutable BEFORE UPDATE OR DELETE ON fx_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER fx_commands_test BEFORE INSERT ON fx_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only(); +REVOKE ALL ON fx_versions,fx_commands FROM PUBLIC; +REVOKE ALL ON SEQUENCE fx_versions_id_seq,fx_commands_id_seq FROM PUBLIC; +REVOKE ALL ON FUNCTION sgf_fx_guard() FROM PUBLIC; diff --git a/database/sql/fiscal_obligations.sql b/database/sql/fiscal_obligations.sql new file mode 100644 index 0000000..84ff052 --- /dev/null +++ b/database/sql/fiscal_obligations.sql @@ -0,0 +1,25 @@ +CREATE TABLE fo_meta(policy text PRIMARY KEY,composition text NOT NULL,source_hash char(64) NOT NULL); +CREATE TABLE fo_versions( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + record_key uuid NOT NULL,version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES fo_versions(id), + action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','approved','refused')), + prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id), + preparer_identity char(64) NOT NULL,data jsonb NOT NULL,source_basis char(64) NOT NULL, + impact jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,record_key,version), + FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id), + CHECK(jsonb_typeof(data)='object'),CHECK(jsonb_typeof(impact)='object'), + CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))), + CHECK(action<>'review' OR prepared_by<>actor_id) +); +CREATE TABLE fo_commands( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL, + result jsonb NOT NULL,input jsonb NOT NULL,created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key) +); +CREATE FUNCTION fo_append_only() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION 'SGF_FISCAL_OBLIGATIONS_APPEND_ONLY'; END; $$; +CREATE TRIGGER fo_meta_immutable BEFORE UPDATE OR DELETE ON fo_meta FOR EACH ROW EXECUTE FUNCTION fo_append_only(); +CREATE TRIGGER fo_versions_immutable BEFORE UPDATE OR DELETE ON fo_versions FOR EACH ROW EXECUTE FUNCTION fo_append_only(); +CREATE TRIGGER fo_commands_immutable BEFORE UPDATE OR DELETE ON fo_commands FOR EACH ROW EXECUTE FUNCTION fo_append_only(); diff --git a/database/sql/fiscal_rules.sql b/database/sql/fiscal_rules.sql new file mode 100644 index 0000000..867e55d --- /dev/null +++ b/database/sql/fiscal_rules.sql @@ -0,0 +1,27 @@ +CREATE TABLE fr_events(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,rule_key text NOT NULL CHECK(rule_key ~ '^[A-Z0-9_-]{3,80}$'),revision bigint NOT NULL CHECK(revision>0),rule_version integer NOT NULL CHECK(rule_version>0),previous_id bigint UNIQUE REFERENCES fr_events(id),target_id bigint REFERENCES fr_events(id),action text NOT NULL CHECK(action IN('prepare','review','withdraw')),state text NOT NULL CHECK(state IN('prepared','reviewed','refused','withdrawn')),prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),definition jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,rule_key,revision),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)); +CREATE TABLE fr_calculations(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),rule_event_id bigint REFERENCES fr_events(id),input jsonb NOT NULL,result jsonb NOT NULL,dependencies jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id)); +CREATE TABLE fr_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)); +CREATE FUNCTION sgf_fr_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE lastrow fr_events;t fr_events;v bigint;required_right text; +BEGIN + PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE tenant_id=NEW.tenant_id AND id=NEW.company_id FOR UPDATE; + IF TG_TABLE_NAME='fr_calculations' THEN required_right:='fiscal_rules.prepare'; ELSE required_right:='fiscal_rules.'||CASE NEW.action WHEN 'prepare' THEN 'prepare' ELSE 'review' END; END IF; + IF NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id) OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=required_right) THEN RAISE EXCEPTION 'SGF_FR_ACCESS'; END IF; + IF TG_TABLE_NAME='fr_calculations' THEN RETURN NEW; END IF; + SELECT * INTO lastrow FROM fr_events WHERE (tenant_id,company_id,unit_id,rule_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.rule_key) ORDER BY revision DESC LIMIT 1; + IF NEW.revision<>coalesce(lastrow.revision,0)+1 OR NEW.previous_id IS DISTINCT FROM lastrow.id THEN RAISE EXCEPTION 'SGF_FR_STALE'; END IF; + IF NEW.action='prepare' THEN SELECT coalesce(max(rule_version),0)+1 INTO v FROM fr_events WHERE (tenant_id,company_id,unit_id,rule_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.rule_key); IF NEW.rule_version<>v OR NEW.state<>'prepared' OR NEW.prepared_by<>NEW.actor_id OR NEW.target_id IS NOT NULL THEN RAISE EXCEPTION 'SGF_FR_PREPARE'; END IF; + ELSE SELECT * INTO t FROM fr_events WHERE id=NEW.target_id AND (tenant_id,company_id,unit_id,rule_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.rule_key); + IF t.id IS NULL OR EXISTS(SELECT 1 FROM fr_events WHERE (tenant_id,company_id,unit_id,rule_key,rule_version)=(t.tenant_id,t.company_id,t.unit_id,t.rule_key,t.rule_version) AND revision>t.revision) OR NEW.rule_version<>t.rule_version OR NEW.prepared_by<>t.prepared_by OR NEW.actor_id=t.prepared_by OR NEW.definition IS DISTINCT FROM t.definition THEN RAISE EXCEPTION 'SGF_FR_REVIEW'; END IF; + IF (NEW.action='review' AND (t.state<>'prepared' OR NEW.state NOT IN('reviewed','refused'))) OR (NEW.action='withdraw' AND (t.state<>'reviewed' OR NEW.state<>'withdrawn')) THEN RAISE EXCEPTION 'SGF_FR_STATE'; END IF; END IF; + RETURN NEW; +END; $$; +CREATE TRIGGER fr_event_guard BEFORE INSERT ON fr_events FOR EACH ROW EXECUTE FUNCTION sgf_fr_guard(); +CREATE TRIGGER fr_calculation_guard BEFORE INSERT ON fr_calculations FOR EACH ROW EXECUTE FUNCTION sgf_fr_guard(); +CREATE TRIGGER fr_events_immutable BEFORE UPDATE OR DELETE ON fr_events FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER fr_calculations_immutable BEFORE UPDATE OR DELETE ON fr_calculations FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER fr_commands_immutable BEFORE UPDATE OR DELETE ON fr_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER fr_commands_test BEFORE INSERT ON fr_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only(); +REVOKE ALL ON fr_events,fr_calculations,fr_commands FROM PUBLIC; +REVOKE ALL ON SEQUENCE fr_events_id_seq,fr_calculations_id_seq,fr_commands_id_seq FROM PUBLIC; +REVOKE ALL ON FUNCTION sgf_fr_guard() FROM PUBLIC; diff --git a/database/sql/hr_personnel.sql b/database/sql/hr_personnel.sql new file mode 100644 index 0000000..2d06c01 --- /dev/null +++ b/database/sql/hr_personnel.sql @@ -0,0 +1,28 @@ +CREATE TABLE hp_meta(policy text PRIMARY KEY,composition text NOT NULL,source_hash char(64) NOT NULL); +CREATE TABLE hp_versions( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + kind text NOT NULL CHECK(kind IN('contract','day','entitlement','leave','report')),record_key uuid NOT NULL,version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES hp_versions(id), + action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','approved','refused')), + prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id), + preparer_identity char(64) NOT NULL,data jsonb NOT NULL,source_basis char(64) NOT NULL, + impact jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,record_key,version), + FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id), + CHECK(jsonb_typeof(data)='object'),CHECK(jsonb_typeof(impact)='object'), + CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))), + CHECK(action<>'review' OR prepared_by<>actor_id) +); +CREATE TABLE hp_commands( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL, + result jsonb NOT NULL,input jsonb NOT NULL,created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key) +); +CREATE FUNCTION hp_append_only() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION 'SGF_HR_PERSONNEL_APPEND_ONLY'; END; $$; +CREATE TRIGGER hp_meta_immutable BEFORE UPDATE OR DELETE ON hp_meta FOR EACH ROW EXECUTE FUNCTION hp_append_only(); +CREATE TRIGGER hp_versions_immutable BEFORE UPDATE OR DELETE ON hp_versions FOR EACH ROW EXECUTE FUNCTION hp_append_only(); +CREATE TRIGGER hp_commands_immutable BEFORE UPDATE OR DELETE ON hp_commands FOR EACH ROW EXECUTE FUNCTION hp_append_only(); + +CREATE INDEX hp_versions_scope_latest ON hp_versions(tenant_id,company_id,unit_id,record_key,version DESC); +CREATE INDEX hp_versions_kind ON hp_versions(tenant_id,company_id,unit_id,kind,id DESC); diff --git a/database/sql/internal_statements.sql b/database/sql/internal_statements.sql new file mode 100644 index 0000000..5d3adc2 --- /dev/null +++ b/database/sql/internal_statements.sql @@ -0,0 +1,39 @@ +CREATE TABLE fs_meta(policy text PRIMARY KEY,composition text NOT NULL,source_hash char(64) NOT NULL); +CREATE TABLE fs_versions( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + kind text NOT NULL CHECK(kind IN('mapping','result')),version bigint NOT NULL CHECK(version>0), + previous_id bigint UNIQUE REFERENCES fs_versions(id),action text NOT NULL CHECK(action IN('prepare','review')), + state text NOT NULL CHECK(state IN('prepared','approved','refused')),prepared_host_id bigint NOT NULL, + prepared_native_id bigint NOT NULL REFERENCES users(id),prepared_person text NOT NULL,preparer_identity char(64) NOT NULL, + host_actor_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL, + source_basis char(64) NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,kind,version),CHECK(jsonb_typeof(data)='object'), + CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))), + CHECK(action<>'review' OR (prepared_host_id<>host_actor_id AND prepared_native_id<>actor_id)) +); +CREATE TABLE fs_results( + tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,snapshot_key uuid NOT NULL, + data jsonb NOT NULL,csv_sha256 char(64) NOT NULL,source_basis char(64) NOT NULL,created_at timestamptz NOT NULL, + PRIMARY KEY(tenant_id,company_id,unit_id,snapshot_key) +); +CREATE TABLE fs_result_lines( + tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,snapshot_key uuid NOT NULL, + account_id bigint NOT NULL REFERENCES gl_accounts(id),code text NOT NULL,label text NOT NULL,currency text NOT NULL, + category text NOT NULL CHECK(category IN('asset','liability','income','expense','unclassified')), + opening_net_minor numeric(40,0) NOT NULL,debit_minor numeric(40,0) NOT NULL,credit_minor numeric(40,0) NOT NULL,closing_net_minor numeric(40,0) NOT NULL, + PRIMARY KEY(tenant_id,company_id,unit_id,snapshot_key,account_id), + FOREIGN KEY(tenant_id,company_id,unit_id,snapshot_key) REFERENCES fs_results(tenant_id,company_id,unit_id,snapshot_key) DEFERRABLE INITIALLY DEFERRED +); +CREATE TABLE fs_commands( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + host_actor_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL, + request_hash char(64) NOT NULL,binding_hash char(64) NOT NULL,input jsonb NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,host_actor_id,command_key) +); +CREATE FUNCTION fs_append_only() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION 'SGF_INTERNAL_STATEMENTS_APPEND_ONLY'; END; $$; +CREATE TRIGGER fs_meta_immutable BEFORE UPDATE OR DELETE ON fs_meta FOR EACH ROW EXECUTE FUNCTION fs_append_only(); +CREATE TRIGGER fs_versions_immutable BEFORE UPDATE OR DELETE ON fs_versions FOR EACH ROW EXECUTE FUNCTION fs_append_only(); +CREATE TRIGGER fs_results_immutable BEFORE UPDATE OR DELETE ON fs_results FOR EACH ROW EXECUTE FUNCTION fs_append_only(); +CREATE TRIGGER fs_lines_immutable BEFORE UPDATE OR DELETE ON fs_result_lines FOR EACH ROW EXECUTE FUNCTION fs_append_only(); +CREATE TRIGGER fs_commands_immutable BEFORE UPDATE OR DELETE ON fs_commands FOR EACH ROW EXECUTE FUNCTION fs_append_only(); diff --git a/database/sql/payable_due.sql b/database/sql/payable_due.sql new file mode 100644 index 0000000..bc9ca65 --- /dev/null +++ b/database/sql/payable_due.sql @@ -0,0 +1,25 @@ +CREATE TABLE pd_due_meta(policy text PRIMARY KEY, composition text NOT NULL, source_hash char(64) NOT NULL); +CREATE TABLE pd_due_versions( + id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL, + obligation_id bigint NOT NULL REFERENCES pa_obligations(id), version bigint NOT NULL CHECK(version>0), + previous_id bigint UNIQUE REFERENCES pd_due_versions(id), action text NOT NULL CHECK(action IN('prepare','review')), + state text NOT NULL CHECK(state IN('prepared','approved','refused')), source_basis char(64) NOT NULL, + prepared_host_id bigint NOT NULL, prepared_native_id bigint NOT NULL REFERENCES users(id), + host_actor_id bigint NOT NULL, actor_id bigint NOT NULL REFERENCES users(id), data jsonb NOT NULL, + reason text NOT NULL CHECK(length(reason)>=10), created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,obligation_id,version), + CHECK(jsonb_typeof(data)='object'), + CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))), + CHECK(action<>'review' OR (host_actor_id<>prepared_host_id AND actor_id<>prepared_native_id)) +); +CREATE TABLE pd_due_commands( + id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL, + host_actor_id bigint NOT NULL, actor_id bigint NOT NULL REFERENCES users(id), command_key uuid NOT NULL, + request_hash char(64) NOT NULL, binding_hash char(64) NOT NULL, input jsonb NOT NULL, result jsonb NOT NULL, + created_at timestamptz NOT NULL, UNIQUE(tenant_id,company_id,unit_id,host_actor_id,command_key) +); +CREATE FUNCTION pd_due_append_only() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION 'SGF_PAYABLE_DUE_APPEND_ONLY'; END; $$; +CREATE TRIGGER pd_due_meta_immutable BEFORE UPDATE OR DELETE ON pd_due_meta FOR EACH ROW EXECUTE FUNCTION pd_due_append_only(); +CREATE TRIGGER pd_due_versions_immutable BEFORE UPDATE OR DELETE ON pd_due_versions FOR EACH ROW EXECUTE FUNCTION pd_due_append_only(); +CREATE TRIGGER pd_due_commands_immutable BEFORE UPDATE OR DELETE ON pd_due_commands FOR EACH ROW EXECUTE FUNCTION pd_due_append_only(); diff --git a/database/sql/remunerations.sql b/database/sql/remunerations.sql new file mode 100644 index 0000000..97974c7 --- /dev/null +++ b/database/sql/remunerations.sql @@ -0,0 +1,28 @@ +CREATE TABLE rm_meta(policy text PRIMARY KEY,composition text NOT NULL,source_hash char(64) NOT NULL); +CREATE TABLE rm_versions( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + kind text NOT NULL CHECK(kind IN('worker','rules','payroll')),record_key uuid NOT NULL,version bigint NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES rm_versions(id), + action text NOT NULL CHECK(action IN('prepare','review')),state text NOT NULL CHECK(state IN('prepared','approved','refused')), + prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id), + preparer_identity char(64) NOT NULL,data jsonb NOT NULL,source_basis char(64) NOT NULL, + impact jsonb NOT NULL,reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,record_key,version), + FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id), + CHECK(jsonb_typeof(data)='object'),CHECK(jsonb_typeof(impact)='object'), + CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))), + CHECK(action<>'review' OR prepared_by<>actor_id) +); +CREATE TABLE rm_commands( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL, + result jsonb NOT NULL,input jsonb NOT NULL,created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key) +); +CREATE FUNCTION rm_append_only() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION 'SGF_REMUNERATIONS_APPEND_ONLY'; END; $$; +CREATE TRIGGER rm_meta_immutable BEFORE UPDATE OR DELETE ON rm_meta FOR EACH ROW EXECUTE FUNCTION rm_append_only(); +CREATE TRIGGER rm_versions_immutable BEFORE UPDATE OR DELETE ON rm_versions FOR EACH ROW EXECUTE FUNCTION rm_append_only(); +CREATE TRIGGER rm_commands_immutable BEFORE UPDATE OR DELETE ON rm_commands FOR EACH ROW EXECUTE FUNCTION rm_append_only(); + +CREATE INDEX rm_versions_scope_latest ON rm_versions(tenant_id,company_id,unit_id,record_key,version DESC); +CREATE INDEX rm_versions_kind ON rm_versions(tenant_id,company_id,unit_id,kind,id DESC); diff --git a/database/sql/service_documents.sql b/database/sql/service_documents.sql new file mode 100644 index 0000000..20623bb --- /dev/null +++ b/database/sql/service_documents.sql @@ -0,0 +1,35 @@ +CREATE TABLE sd_versions( + id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL, + document_key uuid NOT NULL,version integer NOT NULL CHECK(version>0),previous_id bigint UNIQUE REFERENCES sd_versions(id), + kind text NOT NULL CHECK(kind IN('receipt','credit_note','invoice_receipt')), + action text NOT NULL CHECK(action IN('prepare','review','issue')),state text NOT NULL CHECK(state IN('draft','approved','refused','issued')), + prepared_by bigint NOT NULL REFERENCES users(id),actor_id bigint NOT NULL REFERENCES users(id),data jsonb NOT NULL,dependencies jsonb NOT NULL, + reason text NOT NULL CHECK(length(reason)>=10),created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(tenant_id,company_id,unit_id,document_key,version),FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id) +); +CREATE TABLE sd_outputs(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,document_key uuid NOT NULL UNIQUE,version_id bigint NOT NULL UNIQUE REFERENCES sd_versions(id),kind text NOT NULL,number bigint NOT NULL,invoice_id bigint NOT NULL REFERENCES si_invoices(id),effect_id bigint REFERENCES sr_effects(id),snapshot jsonb NOT NULL,pdf_base64 text NOT NULL,pdf_sha256 char(64) NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,kind,number)); +CREATE UNIQUE INDEX sd_receipt_allocation_once ON sd_outputs(effect_id) WHERE kind='receipt'; +CREATE UNIQUE INDEX sd_credit_effect_once ON sd_outputs(effect_id) WHERE kind='credit_note'; +CREATE UNIQUE INDEX sd_invoice_receipt_once ON sd_outputs(invoice_id) WHERE kind='invoice_receipt'; +CREATE TABLE sd_commands(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,actor_id bigint NOT NULL REFERENCES users(id),command_key uuid NOT NULL,request_hash char(64) NOT NULL,result jsonb NOT NULL,created_at timestamptz NOT NULL DEFAULT now(),UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key)); +CREATE FUNCTION sgf_sd_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE p sd_versions; permission_name text; +BEGIN + PERFORM sgf_wo_environment();PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE; + permission_name:='service_documents.'||CASE NEW.action WHEN 'review' THEN 'review' WHEN 'issue' THEN 'issue' ELSE 'prepare' END; + IF NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission=permission_name) THEN RAISE EXCEPTION 'SGF_SD_ACCESS'; END IF; + SELECT * INTO p FROM sd_versions WHERE (tenant_id,company_id,unit_id,document_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.document_key) ORDER BY version DESC LIMIT 1; + IF (p.id IS NULL AND (NEW.version<>1 OR NEW.action<>'prepare' OR NEW.previous_id IS NOT NULL OR NEW.prepared_by<>NEW.actor_id)) OR(p.id IS NOT NULL AND(NEW.previous_id IS DISTINCT FROM p.id OR NEW.version<>p.version+1 OR NEW.kind<>p.kind OR NEW.prepared_by<>p.prepared_by OR p.state='issued')) THEN RAISE EXCEPTION 'SGF_SD_VERSION'; END IF; + IF NEW.action='review' AND(p.state<>'draft' OR NEW.actor_id=NEW.prepared_by) THEN RAISE EXCEPTION 'SGF_SD_SELF_REVIEW'; END IF; + IF NEW.action='issue' AND p.state<>'approved' THEN RAISE EXCEPTION 'SGF_SD_UNREVIEWED'; END IF; + RETURN NEW; +END; $$; +CREATE TRIGGER sd_guard BEFORE INSERT ON sd_versions FOR EACH ROW EXECUTE FUNCTION sgf_sd_guard(); +CREATE TRIGGER sd_versions_immutable BEFORE UPDATE OR DELETE ON sd_versions FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER sd_outputs_immutable BEFORE UPDATE OR DELETE ON sd_outputs FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +CREATE TRIGGER sd_commands_immutable BEFORE UPDATE OR DELETE ON sd_commands FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit(); +REVOKE ALL ON sd_versions,sd_outputs,sd_commands FROM PUBLIC; +REVOKE ALL ON FUNCTION sgf_sd_guard() FROM PUBLIC; + +CREATE TRIGGER sd_outputs_test BEFORE INSERT ON sd_outputs FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only(); +CREATE TRIGGER sd_commands_test BEFORE INSERT ON sd_commands FOR EACH ROW EXECUTE FUNCTION sgf_wo_test_only(); diff --git a/database/sql/stock_catalogue.sql b/database/sql/stock_catalogue.sql new file mode 100644 index 0000000..a5ab366 --- /dev/null +++ b/database/sql/stock_catalogue.sql @@ -0,0 +1,22 @@ +CREATE TABLE sc_catalogue_meta(policy text PRIMARY KEY, composition text NOT NULL, source_hash char(64) NOT NULL); +CREATE TABLE sc_catalogue_versions( + id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL, + record_key uuid NOT NULL, version bigint NOT NULL CHECK(version>0), previous_id bigint UNIQUE REFERENCES sc_catalogue_versions(id), + action text NOT NULL CHECK(action IN('prepare','review')), state text NOT NULL CHECK(state IN('prepared','approved','refused')), + prepared_host_id bigint NOT NULL, prepared_native_id bigint NOT NULL REFERENCES users(id), host_actor_id bigint NOT NULL, actor_id bigint NOT NULL REFERENCES users(id), + stock_item_id bigint REFERENCES stock_items(id), data jsonb NOT NULL CHECK(jsonb_typeof(data)='object'), + reason text NOT NULL CHECK(length(reason)>=10), created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,record_key,version), CHECK((action='prepare' AND state='prepared') OR (action='review' AND state IN('approved','refused'))), + CHECK(action<>'review' OR (host_actor_id<>prepared_host_id AND actor_id<>prepared_native_id)) +); +CREATE TABLE sc_catalogue_commands( + id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL, + host_actor_id bigint NOT NULL, actor_id bigint NOT NULL REFERENCES users(id), command_key uuid NOT NULL, + request_hash char(64) NOT NULL, binding_hash char(64) NOT NULL, input jsonb NOT NULL, result jsonb NOT NULL, created_at timestamptz NOT NULL, + UNIQUE(tenant_id,company_id,unit_id,host_actor_id,command_key) +); +CREATE FUNCTION sc_catalogue_append_only() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION 'SGF_CATALOGUE_APPEND_ONLY'; END; $$; +CREATE TRIGGER sc_catalogue_versions_immutable BEFORE UPDATE OR DELETE ON sc_catalogue_versions FOR EACH ROW EXECUTE FUNCTION sc_catalogue_append_only(); +CREATE TRIGGER sc_catalogue_commands_immutable BEFORE UPDATE OR DELETE ON sc_catalogue_commands FOR EACH ROW EXECUTE FUNCTION sc_catalogue_append_only(); +CREATE TRIGGER sc_catalogue_meta_immutable BEFORE UPDATE OR DELETE ON sc_catalogue_meta FOR EACH ROW EXECUTE FUNCTION sc_catalogue_append_only(); diff --git a/database/sql/workshop_schedule.sql b/database/sql/workshop_schedule.sql new file mode 100644 index 0000000..30b1f07 --- /dev/null +++ b/database/sql/workshop_schedule.sql @@ -0,0 +1,32 @@ +CREATE TABLE wo_schedule_events ( + id bigserial PRIMARY KEY, tenant_id bigint NOT NULL, company_id bigint NOT NULL, unit_id bigint NOT NULL, + booking_key uuid NOT NULL, version integer NOT NULL CHECK(version>0), previous_id bigint UNIQUE REFERENCES wo_schedule_events(id), + order_id bigint NOT NULL REFERENCES wo_orders(id), equipment_id bigint NOT NULL REFERENCES wo_equipment(id), technician_id bigint NOT NULL REFERENCES users(id), + starts_at timestamptz NOT NULL, ends_at timestamptz NOT NULL, state text NOT NULL CHECK(state IN('scheduled','cancelled')), + reason text NOT NULL CHECK(length(reason)>=10), actor_id bigint NOT NULL REFERENCES users(id), command_key uuid NOT NULL, + request_hash char(64) NOT NULL, source_revision char(64) NOT NULL, snapshot jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), + CHECK(ends_at>starts_at AND ends_at<=starts_at+interval '7 days'), + UNIQUE(tenant_id,company_id,unit_id,booking_key,version), UNIQUE(tenant_id,company_id,unit_id,actor_id,command_key), + FOREIGN KEY(tenant_id,company_id,unit_id) REFERENCES units(tenant_id,company_id,id) +); +CREATE FUNCTION sgf_schedule_guard() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE o wo_orders; p wo_schedule_events; +BEGIN + PERFORM sgf_wo_environment(); + IF TG_OP<>'INSERT' THEN RAISE EXCEPTION 'SGF_SCHEDULE_APPEND_ONLY'; END IF; + PERFORM 1 FROM companies WHERE id=NEW.company_id FOR UPDATE; + SELECT * INTO STRICT o FROM wo_orders WHERE id=NEW.order_id; + IF ROW(o.tenant_id,o.company_id,o.unit_id,o.equipment_id) IS DISTINCT FROM ROW(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.equipment_id) + OR NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.actor_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission='workshop.assign') + OR (NEW.state='scheduled' AND (NOT EXISTS(SELECT 1 FROM permission_grants WHERE user_id=NEW.technician_id AND tenant_id=NEW.tenant_id AND company_id=NEW.company_id AND unit_id=NEW.unit_id AND permission='workshop.record') OR NEW.technician_id IS DISTINCT FROM (SELECT technician_id FROM wo_entries WHERE order_id=NEW.order_id AND kind='assignment' ORDER BY id DESC LIMIT 1))) + THEN RAISE EXCEPTION 'SGF_SCHEDULE_SCOPE_OR_AUTHORITY'; END IF; + SELECT * INTO p FROM wo_schedule_events WHERE (tenant_id,company_id,unit_id,booking_key)=(NEW.tenant_id,NEW.company_id,NEW.unit_id,NEW.booking_key) ORDER BY version DESC LIMIT 1; + IF (p.id IS NULL AND (NEW.version<>1 OR NEW.previous_id IS NOT NULL OR NEW.state<>'scheduled')) OR + (p.id IS NOT NULL AND (NEW.previous_id IS DISTINCT FROM p.id OR NEW.version<>p.version+1 OR NEW.order_id<>p.order_id OR p.state='cancelled')) THEN RAISE EXCEPTION 'SGF_SCHEDULE_STALE_VERSION'; END IF; + IF NEW.state='scheduled' AND EXISTS(SELECT 1 FROM wo_schedule_events e WHERE e.tenant_id=NEW.tenant_id AND e.company_id=NEW.company_id AND e.unit_id=NEW.unit_id AND e.booking_key<>NEW.booking_key AND e.state='scheduled' + AND NOT EXISTS(SELECT 1 FROM wo_schedule_events n WHERE n.previous_id=e.id) + AND (e.technician_id=NEW.technician_id OR e.equipment_id=NEW.equipment_id) + AND e.starts_atNEW.starts_at) THEN RAISE EXCEPTION 'SGF_SCHEDULE_RESOURCE_CONFLICT' USING ERRCODE='23514'; END IF; + RETURN NEW; +END; $$; +CREATE TRIGGER sgf_schedule_guard BEFORE INSERT OR UPDATE OR DELETE ON wo_schedule_events FOR EACH ROW EXECUTE FUNCTION sgf_schedule_guard(); diff --git a/docs/SGC_8085_INTEGRATION_PREPARATION.md b/docs/SGC_8085_INTEGRATION_PREPARATION.md new file mode 100644 index 0000000..339b7eb --- /dev/null +++ b/docs/SGC_8085_INTEGRATION_PREPARATION.md @@ -0,0 +1,73 @@ +# Preparação SGC → facturas SGF8085 — 10/11/2026 + +**Continuação posterior autorizada:** [ecrã e persistência auditada agora implementados](SGC_INVOICE_WORKSPACE_DELIVERY.md),20 casos únicos/142 asserções e14 verificações PostgreSQL entre aplicações. Novo pacote inactivo `3bb99e866f52965dbb7d`; o anterior mantém-se preservado. As menções abaixo a ecrã/persistência por implementar descrevem a preparação **anterior**. Instalação/configuração/activação continuam pendentes, sem alterar8085. + +**Preparação técnica entregue; ligação ainda não instalada/activada em8085.** Não foi criada outra demonstração, base, role, porta, conta ou concessão. O percurso de Compras já concluído em8085 mantém código, dados e configuração anteriores. + +## O que existe e o que falta + +| Peça | Estado desta entrega | Antes de utilizar | +| --- | --- | --- | +| Endpoint SGF | Implementado e78 provas válidas reutilizadas; permanece desligado | Instalar o pacote preparado na mesma8085, sem trocar motor/livro; conferir código ainda desligado | +| Cliente de transporte SGC | Implementado, com prévia, aprovação exacta e recuperação da mesma intenção | Compor o ficheiro verificado pelo hash no SGC e configurar uma ligação aprovada | +| Gateway nativo SGC | `SgfInvoiceGateway`, sem listeners/rotas/jobs; configuração desligada | Ligar a uma acção autenticada do percurso de envio, com autoridade financeira actual | +| Origem documental | Contrato apenas para **rascunho ainda não registado no SGC**, autoridade única SGF | Implementar o ecrã/armazenamento durável desse rascunho/intenção/decisão/recibo; ainda não entregue como fluxo utilizável do SGC | +| Mapeamentos | Modelo preparado, nenhuma selecção ou associação criada | Escolher/versionar/conferir tenant de origem e empresa/unidade de destino, ordem e cada linha; sem inferência por nomes/NUIT/IDs coincidentes | +| Principal e segredo | Nenhum criado; modelos com camposNULL e `enabled=false` | Aprovar identidade QA, direitos existentes, versões de autenticação/contexto e provisionar bearer privado por via protegida | +| Ensaio SGC→8085 | Contrato provado por fakes e kernel real no livro de regressão existente | Após autorização, ensaio de rede nas duas aplicações e nessa mesma8085, com novo documento sintético e confirmação individual | + +Instalar código desligado, configurar identidades/segredos e activar tráfego são passos distintos. Nenhum foi executado por consequência deste pedido de **preparação sem activar**. Não basta mudar uma flag para declarar o percurso de envio do SGC concluído. + +## Fronteira de autoridade + +O SGC está no projecto `C:/laragon/www/compras`, produto autónomo sobre o mesmo núcleo. Os livros do SGC e de8085 não são automaticamente o mesmo livro. Uma factura já registada/validada/paga no SGC não deve ser reenviada como se fosse nova em8085: isso pode duplicar documentos, consumo e responsabilidade. Este cliente recusa estados de origem diferentes de `unregistered_draft`; não intercepta `InvoiceWorkflow` nem exporta `Invoice` existentes. + +O próximo ecrã de envio terá de capturar/conservar o documento recebido **antes** de o registar financeiramente no SGC. O cliente não cria essa fonte nem comprova que uma declaração de rascunho corresponde a um estado real: o adaptador do futuro ecrã deverá reler a fonte autorizada e impedir o fluxo local concorrente. Se a decisão for partilhar o mesmo livro/instância em vez de transportar rascunhos, deve usar referência à factura existente, não uma segunda chamada de criação. Essa mudança de topologia e a migração de documentos históricos não estão presumidas. + +O endpoint confirma só uma factura **pendente** em8085. Revisão distinta, consumo, obrigação, pagamento e contabilidade continuam nos mecanismos existentes. `received_pending` no cliente significa recepção documental pelo SGF, não validação financeira ou conclusão da compra. + +## Cliente preparado + +Ficheiro portátil: `packages/sgc-invoice-client/src/InvoiceClient.php`, sem dependência de Laravel, base, ficheiros ou relógio. O transporte é injectado. Destino permitido neste recorte: exclusivamente `http://127.0.0.1:8085`, sem redirects nem URLs fornecidos pelo payload. Uma utilização remota/TLS requer recorte próprio. + +`intent(mapping, draft, key, reason)` exige mapeamento explícito/versionado e UUID escolhido uma única vez; não faz rede nem escolhe entidades. Converte só as referências de linha aprovadas para IDs de destino, recusa âmbito diferente, duplicação/mapeamento em falta, floats e campos inesperados. A intenção inclui hashes do rascunho, mapeamento e ligação, sem bearer. + +`preview(intent, currentSourceVersion, currentMappingHash)` confere a mesma origem/mapeamento/ligação, lê a ordem exacta e pede a prévia. Não confirma. Pode recuperar um recibo já confirmado pelo mesmo UUID. + +`confirm(state, approvedHash, currentSourceVersion, currentMappingHash)` só aceita a decisão sobre esse impacto exacto e versão actual. Conserva UUID, cut e conteúdo quando a resposta é incerta. Timeout,503, redirects ou resposta de sucesso incoerente não são tratados como sucesso confirmado. Não há retry automático nem nova chave automática;401/403 bloqueiam,409 exige conferência. Uma recuperação explícita devolve o mesmo recibo pendente. As mensagens de transporte não são expostas e bearer não entra em estados serializáveis/debug. + +O SGC ganhou `app/Services/SgfInvoiceGateway.php` e `config/sgf_invoice.php`. O gateway só cria o cliente com configuração aprovada/activa e pacote com hash verificado; relê a pessoa/empresa nativa, função financeira e exclusão de manutenção, tanto ao criar o cliente como imediatamente antes de cada operação de rede. Alteração da configuração impede usar o objecto antigo. A configuração entregue é `enabled=false`, pacoteNULL, hashNULL, ligações vazias; nenhum processo existente o invoca. + +## Persistência e interface ainda necessárias + +A intenção é um **envelope de transporte**, não uma outbox persistida: não foi criada migração/tabela nem usado ficheiro/cache como armazenamento informal. O chamador do SGC ainda precisa de persistência auditada e ecrã de conferência. Contrato preparado para essa implementação: + +1. Guardar fonte autorizada, versão, mapeamento/hash, UUID e intenção **antes do primeiro envio**. Uma fonte com envio pendente/incerto não pode gerar outra intenção/chave sem resolução explícita. +2. Conservar prévia, impacto e `approval_hash`; apresentar ao utilizador e registar a decisão individual, pessoa, data e fundamento. +3. Guardar o estado de confirmação em curso antes da rede. Se houver interrupção depois do commit remoto, recuperar pelo mesmo conteúdo/UUID/cut. +4. Guardar recibo `invoice_id/order_id/pending`, referência de origem e transições anteriores/posteriores. Não confiar apenas no retorno do browser nem iniciar validação financeira no SGC. +5. Antes de nova prévia/confirmação, reler pessoa/permissões, fonte, versão e mapeamento. Uma alteração invalida a conferência. Rotação de contexto também exige nova conferência; uma operação com resultado incerto não pode ser abandonada e refeita com outra chave. + +Nenhuma nova tabela/direito foi concedida ou instalada. A futura persistência exige um desenho de migração/retencão e prova própria; a instalação em base operacional deverá ter autorização específica. Este relatório não declara essas capacidades implementadas. + +## Pacote de8085 preparado e verificável + +Identificador: **`d09aac9d09da98e61df5`**. Local privado: `C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/prepared-sgc-invoice/d09aac9d09da98e61df5`. + +Contém oito ficheiros do endpoint para8085, três ficheiros do cliente/gateway/configuração, modelos não preenchidos e manifesto de hashes. Guarda as duas versões anteriores de bootstrap/navegação; não contém `.env`, binding, estado da instalação, contas ou credenciais. Bootstrap/navegação diferem somente pelas rotas SGC; o writer/controlador/runtime existentes não são substituídos. + +```powershell +& 'C:/laragon/tools/sgf-tempo/node-v24.22.1-win-arm64/node.exe' scripts/prepare-sgc-8085.mjs check +``` + +O comando verifica o código actual de8085, os78 resultados aceites e os hashes da preparação, sem instalar/activar/iniciar serviços/migrar/limpar cache. `prepare` cria apenas essa área de revisão fora da aplicação. Não existe opção de aplicar neste comando. Qualquer alteração posterior do destino ou do código exige nova conferência; não copiar o pacote às cegas sobre trabalho alheio. O script de actualização geral anterior não inclui estes oito ficheiros. + +Depois de autorizado instalar código desligado: conferir hashes/pré-condições, backup privado dos ficheiros a substituir, aplicar apenas a allowlist e verificar `/compras` e a recusa404 do endpoint desligado. Reversão de código não desfaz dados; depois de um futuro envio confirmado, conservar o recibo e usar recuperação, nunca reset como rollback. + +## Evidência e limitações + +16 testes Unit/46 asserções novos, sem base/rede: mapeamento, âmbito, documento já registado, fonte/mapeamento/contexto alterados, decisão individual, cabeçalhos, recuperação, estados de falha e gateway desligado. Seis verificações adicionais ligam o cliente ao **kernel real** no livro de regressão `da77bb730751`: consulta exacta/prévia real e recuperação de recibo existente, sem nova factura/evento persistido. As sequências de tentativa da prévia podem avançar nesse livro de regressão; não são revertidas artificialmente. + +Os78 ensaios anteriores do endpoint e63 do percurso de8085 foram reutilizados por hash, não repetidos nem somados como cobertura nova do transporte.16 Unit+seis verificações de contrato não equivalem a22 testes de rede. Não foi executado tráfego para8085 nem comando/migração no piloto SGC. Os seis registos e a cache bloqueada não foram alterados;8085 manteve código e estado. Sem activação de candidatas, publicação, aceitação ou checkpoint. Visual, carga sustentada, TLS operacional, envio completo pela interface do SGC e implantação permanecem com provas próprias. + +[Prova da preparação](proofs/sgc-8085-integration-preparation-20261110.json). O pacote identifica ficheiros e hashes concretos para revisão; não representa autorização de aplicar/activar nem prova de um fluxo de utilizador ainda não implementado. diff --git a/docs/SGC_INVOICE_ENDPOINT.md b/docs/SGC_INVOICE_ENDPOINT.md new file mode 100644 index 0000000..cf95f9c --- /dev/null +++ b/docs/SGC_INVOICE_ENDPOINT.md @@ -0,0 +1,60 @@ +# Endpoint de facturas SGC — implementação local de10/11/2026 + +Continuação autorizada: [workspace persistente SGC entregue](SGC_INVOICE_WORKSPACE_DELIVERY.md), com intenção, decisão, recibo e recuperação auditados. Reutiliza este endpoint/motor sem novas alterações ao seu writer; ambas as capacidades permanecem desligadas no destino. + +Implementação concluída para transporte sintético até **factura pendente**, reutilizando o controlador/workflow/writer fixados de Compras. Não utiliza `IntegrationService` legado, tabelas espelho `sgc_faturas` ou saldo orçamental fictício. Não valida a própria factura, consome orçamento, reconhece obrigações, paga ou contabiliza. O percurso e a instalação8085 permanecem intactos. + +## Rotas e contrato + +`bootstrap/app.php` regista especificamente `routes/sgc.php`, preservando `routes/api.php` legado sem montagem. Só existem: + +- `GET /api/v1/integration/sgc/invoices/orders/{order}`: ordem exacta autorizada, linhas, versão actual e progresso da recepção. Não escolhe uma ordem/linha nem devolve a lista de outras facturas. +- `POST /api/v1/integration/sgc/invoices`: fases `preview` e `confirm` no mesmo endereço. A confirmação cria apenas a factura pendente; a revisão distinta continua no percurso existente. + +Ambas exigem `Authorization: Bearer …`, `X-SGC-Client`, `X-SGC-Context` e `Accept: application/json`. O POST exige `Content-Type: application/json` e `X-Idempotency-Key` UUID. HTTP só é admitido em loopback; fora dele é exigido TLS. O adaptador nativo conserva o bloqueio `budget-testing` e a validação física do cluster/base/role/manifests QA. Não há capacidade operacional/produtiva presumida. + +Na prévia enviar `phase`, `order_id`, `revision`, `reason` e `data` com número/data/totais/linhas. Valores monetários são strings decimais, não floats. Quantidades, unidade, preço, imposto, total, linhas aceites e origem são conferidos pelo mesmo motor original. Campos desconhecidos, IDs de autoridade no JSON/query, datas inválidas, linhas repetidas/estrangeiras e corpos acima128KiB são recusados. Até50 linhas. Fundamento de10 a2000 caracteres. + +A resposta200 apresenta `preview` cifrada e `impact`, sem gravar facturas. O integrador deve apresentar/conferir o impacto antes da confirmação individual, sem confirmação automática. Para confirmar enviar somente: + +```json +{"phase":"confirm","order_id":123,"preview":""} +``` + +Usar a mesma chave UUID da prévia. Uma confirmação válida devolve201 com `invoice_id`, `order_id`, `status: pending`, `replayed: false`. Alteração da versão/origem/contexto exige nova conferência, não uma nova tentativa automática de escrever. + +`tests/payloads/invoice_test.json` é um **modelo**: os placeholders de ordem/linha/revisão têm de ser substituídos depois de escolha explícita e GET autenticado.100MZN/1UN/imposto0 são apenas o exemplo QA; não são preenchidos automaticamente a partir de uma entidade real. No PowerShell, gerar a chave com `[guid]::NewGuid().ToString()`; não pressupor `uuidgen`. + +## Autenticação e isolamento + +`config/sgc_invoice.php` conserva `enabled=false` e `principals=[]`. Nenhuma credencial, associação, concessão ou activação foi instalada. Uma futura configuração exige aprovação própria; o nome do cliente não concede direitos. + +Cada principal configurado separadamente contém `enabled`, SHA256 de um bearer aleatório de pelo menos32 caracteres, `user_id`, `auth_version`, `tenant_id`, `company_id`, `unit_id` e `context_version` de64 hexadecimais. O token nunca é aceite por query/cookie, guardado em log ou distribuído no código. `X-SGC-Context` deve corresponder à versão configurada; rodar essa versão invalida as prévias antigas. A versão de autenticação deve coincidir com a pessoa actual. + +Empresa/unidade/pessoa provêm exclusivamente dessa configuração no servidor. Filiação, empresa/unidade, módulo disponível e direitos existentes são revalidados. Reutilizam-se `company.view`, `unit.view`, `purchasing_invoices.view/prepare` e as funções/autoridades nativas. Nenhum nome de direito novo e nenhuma concessão real. O contexto usa sessão efémera em memória, é limpo ao terminar e não é persistido em cookies/sessões/cache. As respostas de sucesso são `no-store, private`. + +## Idempotência, transacção e recuperação + +O adaptador serializa cada UUID com advisory lock transaccional PostgreSQL no host, abrangendo a consulta de recuperação e o commit nativo. A chave é normalizada para minúsculas. O recibo autoritativo é o evento append-only **já existente** `sgf.invoice.command`, gravado na mesma transacção da factura e das linhas no livro nativo. Não há recibo externo ou espelho de dívida no host. + +Antes de uma recuperação, revalidam-se identidade, contexto, direitos, binding actual e visibilidade exacta da ordem. A mesma chave com fundamento/dados/outra ordem gera409; a mesma intenção confirmada devolve200 e o mesmo `invoice_id`, sem nova factura/auditoria. A recuperação pode ser pedida com a confirmação original ou repetindo a prévia original. A versão de leitura antiga não é uma autorização para outra escrita. + +Uma prévia não aplicada expira em600s. Um recibo **já confirmado** pode ser recuperado depois desse prazo, com a autoridade actual e sem repetir comandos. Rodar contexto/binding ou revogar acesso impede a recuperação pela prévia antiga. Uma falha depois de criar a factura e antes do recibo reverte tudo; a mesma operação pode ser retomada. Se o commit tiver resultado incerto, repetir **a mesma chave e conteúdo**, sem mudar de número ou inventar nova chave. + +Bloqueio da chave acima5s devolve503 `RETRY_SAME_KEY`, com `Retry-After: 1`. Recusas de versão/conteúdo:409; validação:422; credencial:401; autoridade:403; configuração desligada:404. Deadlock/serialização também devolvem503 recuperável, sem tratar como sucesso. A duração máxima de todos os possíveis bloqueios nativos/carga sustentada não foi validada. + +## Provas sintéticas e preservação + +78 verificações:42 do percurso/validações/atomicidade/concorrência,20 de revogação/isolamento/recuperação de bloqueio,10 da consulta autenticada/compatibilidade de repetição e seis da autoridade nativa na recuperação. Foram executadas pelo kernel HTTP Laravel com PostgreSQL real e processos PHP independentes, **sem criar servidor/porta/base/role/demonstração**. Não são testes TCP no portal, revisão visual ou implantação. + +Livro de regressão existente `receipt-composition-da77bb730751`, separado de8085. O cenário original tinha a recepção integralmente facturada; a primeira prévia foi correctamente recusada. Foi então acrescentada uma **fixture SQL de origem**, marcada `fixture_only`: cópias de requisição/linha/proposta/adjudicação/ordem/recepção aceite e evento de admissão de teste. Essa preparação não prova uma nova execução desses workflows nem um movimento físico. Facturas/linhas/recibos de comando, por sua vez, passaram pelo motor original: quatro novas facturas pendentes, nenhum consumo/obrigação/pagamento/lançamento novo. Registos originais e provas aceites preservados. + +Concorrência da mesma chave:201/200, um `invoice_id`. Falha injectada no evento imediatamente após o writer:500, sem factura parcial; nova confirmação da mesma intenção:201. Bloqueio da chave mantido por outra ligação:503 após o limite; depois da libertação:200 com a factura original. Conteúdo diferente, versão desactualizada, prévia adulterada, outra ordem/tenant/empresa/unidade, permissões revogadas, token/contexto rodados e módulo indisponível foram recusados. + +Comparação de todas as linhas e sequências do host/livro de8085 antes/depois: idênticas. Todas as linhas/sequências/permissões/orçamento do host de regressão também idênticas. Nenhuma ligação de escrita a55432 ou à base da demonstração8039; seis associações protegidas e cache bloqueada não foram alteradas/limpas. Dados de autenticação, cut cifrado, logs e resultados detalhados permanecem privados fora do repositório. Sem checkpoint, publicação ou activação. + +Depois das primeiras72 provas, a conferência do operador no caminho de recuperação foi reforçada com o mesmo `CurrentActor::lock` e a recusa de operador limitado a manutenção. As seis verificações adicionais cobrem essa alteração sem repetir as escritas: função financeira revogada dentro de transacção QA/rollback e manutenção injectada no harness recusam a recuperação; autoridade válida recupera o mesmo recibo. A inclusão da consulta autenticada foi coberta por dez verificações próprias; provas anteriores não foram sobrescritas nem suites alheias repetidas. + +`node scripts/sgc-invoice-check.mjs` apenas consulta o resultado válido, sem repetir ensaios. `--new` executa outra prova no mesmo livro de regressão existente e acrescenta novas fixtures/documentos QA; usar apenas perante alteração/falha relevante e com o cluster próprio disponível. Não modifica8085 nem instala o endpoint lá. + +[Prova consolidada](proofs/sgc-invoice-endpoint-20261110.json). Mantêm-se pendentes activação aprovada/configuração de clientes, utilização real, transporte TLS operacional, política de rotação/rate limiting em implantação, carga sustentada e capacidade de implantação. O serviço legado e os endpoints de pagamentos/analytics continuam fora deste recorte e sem montagem. diff --git a/docs/SGC_INVOICE_WORKSPACE_DELIVERY.md b/docs/SGC_INVOICE_WORKSPACE_DELIVERY.md new file mode 100644 index 0000000..3e947a8 --- /dev/null +++ b/docs/SGC_INVOICE_WORKSPACE_DELIVERY.md @@ -0,0 +1,68 @@ +# SGC → SGF: rascunho, conferência, decisão e recibo persistentes + +10/11/2026. **Bloco implementado e testado**, incluindo ecrã, persistência auditada, exclusão de duplicação local e integração com o endpoint SGF. Não instalado/activado no SGC operacional ou8085. Não se declara concluído o SGF inteiro, implantação ou uso real por consequência desta entrega. + +## Percurso utilizável no código entregue + +Entrada SGC preparada: `/facturas/envio-sgf`, ligada à página nativa de facturas quando a capacidade estiver autorizada/activa. Rotas usam autenticação, verificação existente, empresa configurada e contexto de trabalho nativos. Não foi aberta sessão de demonstração ou criado novo servidor. + +1. Finanças escolhe explicitamente ligação e mapeamento aprovados da sua empresa; não se selecciona primeiro fornecedor/ordem/linha nem se deduzem IDs por nomes. +2. Guarda referência de origem, número/data/documento/linhas e fundamento. Só rascunhos ainda não registados no SGC: a associação ao fornecedor da origem vem do mapeamento autorizado, não do pedido do browser. +3. O rascunho reserva a identidade empresa/fornecedor/número. Edição antes de qualquer confirmação cria nova versão auditada e invalida prévia/intenção anteriores; nunca apaga o histórico. Uma confirmação iniciada torna o conteúdo imutável, mesmo que se perca a resposta. +4. A prévia guarda intenção, UUID e reserva de execução antes da rede. Obtém contexto exacto da ordem no SGF e apresenta o impacto recebido, sem criar factura persistente. Não há confirmação automática. +5. A decisão individual exige fundamento, versão actual, mesmo operador/contexto da conferência e o hash desse impacto. A decisão/intenção/cut são conservados **antes** da chamada de confirmação. +6. O recibo verificado fica persistido como `received_pending`, com IDs de ordem/factura e repetição observada. O ecrã oferece continuidade pela entrada existente de8085, com escolha explícita de função e os IDs do recibo, sem bearer/cut na URL. Revisão financeira/consumo/obrigação continuam no SGF e exigem as decisões distintas existentes. + +O ecrã tem lista SQL paginada25 e filtro de estado por empresa, formulário com linhas explicitamente mapeadas, versão/fundamento, prévia legível, confirmação individual, recuperação e histórico SQL paginado25. A evidência é apresentada por campos, escapada contra HTML injectado. A adição de linhas não reutiliza nomes dos campos existentes. Campos por guardar e confirmação em curso são protegidos ao sair; comandos no servidor continuam autoritativos entre abas/processos. + +## Persistência e isolamento + +Migração preparada no SGC: `2026_11_10_000001_create_sgf_invoice_dispatches.php`, **não aplicada ao piloto**. Acrescenta apenas: + +- `sgf_invoice_dispatches`: fonte, documento/mapeamento congelado, versão de fonte e de controlo, UUID, estado cifrado, hash de aprovação, lease, recibo e autor inicial. Unicidade por empresa/referência e empresa/fornecedor/número. +- `sgf_invoice_dispatch_events`: autor, data UTC, acção, fundamento e estado anterior/posterior, incluindo versões, documento, mapeamento, intenção/impacto por hash e recibo. Eventos append-only e rascunhos sem DELETE, por triggers PostgreSQL/SQLite. Não há tabela de dívida/consumo/stock/contabilidade nova. + +O cut/intenção/impacto de transporte fica cifrado com a chave da aplicação na tabela; segredos de ligação nunca entram nos rascunhos/eventos/HTML. As consultas usam o tenant da pessoa autenticada. A pessoa, função financeira, exclusão de manutenção, fornecedor autorizado, mapeamento e versão são revalidados nos comandos; autoridade e ligação são ainda revalidadas pelo gateway antes de cada chamada. + +O writer **original** `InvoiceWorkflow::register` ganhou apenas a guarda da reserva SGF dentro do bloqueio empresarial já existente, antes de criar uma factura local. A criação do rascunho toma o mesmo bloqueio e recusa uma factura local preexistente. Assim, as duas direcções não podem registar a mesma identidade documental em simultâneo. A reserva mantém efeito mesmo se o workspace/transporte forem depois desligados. Se a nova estrutura ainda não estiver instalada, o writer nativo anterior conserva o seu percurso; activar o workspace sem estrutura devolve503 e não migra por inferência. + +Não há novo nome de direito ou concessão. Usam-se função/âmbito/contexto nativos e os direitos SGF já existentes. Os mapeamentos/principais operacionais continuam vazios/não escolhidos; `workspace_enabled=false` e `enabled=false` na configuração entregue. + +O manifesto apresenta os privilégios SQL mínimos **para revisão antes de qualquer concessão**, com role/base aindaNULL e aprovaçãofalse: SELECT/INSERT/UPDATE no rascunho, SELECT/INSERT nos eventos e USAGE/SELECT nas sequências. Não propõe UPDATE/DELETE da auditoria ou grantsPUBLIC. São necessidades de instalação a conferir contra privilégios já existentes; nenhumGRANT foi aplicado a contas reais. + +## Concorrência e falhas + +Cada transição toma empresa/pessoa/rascunho na mesma ordem e compara `version`. A transacção conserva intenção ou decisão e um lease120s, termina **antes da rede**, e regista a observação numa segunda transacção. Não se mantêm locks empresariais durante HTTP. A mesma fonte não ganha outra chave para contornar uma operação pendente/incerta. + +Uma execução simultânea/versão antiga é recusada409. Se houver interrupção antes de observar a resposta, o estado `sending` com intenção/cut/decisão já existe; terminado o lease, a recuperação explícita usa a mesma chave e conteúdo. Uma resposta tardia de lease substituído não sobrescreve o estado actual. Uma falha de persistência depois do commit remoto conserva o estado de envio/decisão, permitindo recuperar o recibo sem outra factura. + +Não há retries automáticos ou novos UUIDs gerados para confirmação/recuperação.401/403 bloqueiam,409 exige conferência, timeout/503/resultado incerto conservam a intenção. Mudanças de contexto da ligação são recusadas **antes** de gravar uma decisão sobre a prévia antiga. Uma nova prévia explicitamente pedida usa a autoridade actual; depois de iniciado um envio mantém a chave/conteúdo. Alterar o mapeamento de uma fonte já enviada/incerta não é autorizado para contornar o resultado: exige reconciliação da autoridade original, sem forçar outro documento. + +## Provas reutilizáveis + +**20 casos únicos/142 asserções** em SQLite exclusivamente em memória, seleccionados dos XMLs JUnit (17 da consolidação, um do formulário completo, um da estrutura ausente e um da fotografia de operador/contexto na auditoria). A repetição dirigida de uma guarda anterior não é somada novamente. Incluem ecrã real Blade/HTTP, formulário→prévia→decisão→recibo, CSRF, acesso estrangeiro/função indevida/revogada, versões/hash/operador/contexto/mapeamento, paginação e histórico além25, HTML injectado, cifragem, imutabilidade, reserva local, falha de armazenamento depois da confirmação remota e recuperação. Não houve `migrate:fresh` ou suites alheias. + +**14 verificações PostgreSQL e integração entre dois processos de aplicação** nos livros de regressão **já existentes**: origem SGC `b4093c6f6707`, receptor SGF `da77bb730751`. Só nesses ensaios se activaram flags em memória. Foram criadas as duas tabelas de teste no livro de origem, sem nova base/role/porta/demo. Duas fontes sintéticas produziram exactamente duas facturas pendentes no receptor: + +- Primeiro caso: commit real do writer SGF, resposta perdida na origem, estado incerto persistido e recuperação do mesmo recibo com a mesma chave. +- Segundo caso: duas decisões concorrentes em processos PHP; uma envia/conserva recibo, outra recusa409 por versão/em curso; uma única decisão persistida e uma factura no receptor. +- Writer nativo SGC recusou o documento reservado antes de qualquer registo local. PostgreSQL recusou reescrita de evento. Todos os dados/sequências **anteriores** da origem mantiveram-se; orçamento, movimentos e pagamentos do receptor não mudaram.8085 manteve todas as linhas/sequências dos dois livros. + +A primeira prévia deste harness falhou porque variáveis de cache herdadas do processo SGC apontavam o processo receptor para metadados de providers do SGC. O harness receptor passou a isolar esses valores antes de arrancar; retomou-se o rascunho/UUID já conservados, sem repetir confirmação aceite nem apagar a tentativa auditada. Logs de falha ficam privados. + +As aplicações usaram os kernels Laravel e o writer nativo real em processos independentes, com a ponte interceptada para processos CLI: **não é tráfego TCP para8085**, nem activação de8085. As78 provas anteriores do endpoint e as16 do cliente portátil foram reutilizadas, sem repetir suites de negócio. A pequena guarda de estrutura instalada acrescentada no final foi verificada por ensaio dirigido, sem repetir os commits integrados. + +Typescript sem erros; ficheiro TSX alterado compilado para artefacto privado, sem alterar `public/build`. Os Blade foram compilados/renderizados nos testes, e os três scripts inline renderizados passaram na verificação sintáctica. Isso não é revisão visual, prova de interação de browser ou build completo do produto/implantação. + +## Preparação concreta e próximos passos autorizáveis + +Novo pacote inactivo: **`3bb99e866f52965dbb7d`**, fora da aplicação8085, no mesmo directório privado de preparação. Contém oito ficheiros SGF e15 do cliente/SGC, incluindo migração, ecrãs, serviço e guardas. Conserva o pacote anterior `d09aac9d09da98e61df5` como prova histórica. Ficheiros SGC existentes (rotas, writer, controlador e TSX) são assinalados para merge dirigido: não sobrescrever alterações alheias. Os modelos de principal/mapeamento não foram preenchidos/aprovados. + +`node scripts/prepare-sgc-8085.mjs check` verifica a preparação e8085 sem instalar, activar, migrar, iniciar servidor ou limpar cache. Antes do uso operacional deste recorte faltam **passos de instalação/configuração**, não os ecrãs/persistência agora entregues: + +1. Autorizar e preparar backup/restauro para instalação exacta da nova migração SGC e código desligado; não executar todas as migrations pendentes. +2. Aprovar ligação, mapeamentos e identidade/segredo/contexto privados com direitos existentes. Não criar ou conceder acessos por inferência. +3. Instalar o código do endpoint desligado na mesma8085, compor o cliente e preparar o build do link de navegação SGC por via privada/revista. Os ecrãs Blade têm rota própria e não dependem do build do link. +4. Só após autorização de activação, ensaiar tráfego entre as aplicações nessa mesma8085 com fonte nova sintética e decisões individuais. Não reenviar facturas históricas, activar Compras/candidatas ou migrar responsabilidades financeiras. + +Os seis registos protegidos e a cache não foram alterados/limpos. Nenhum comando/migração no SGC operacional, concessão real, instalação/activação em8085, publicação, aceitação ou checkpoint. Visual, TLS/carga sustentada, recuperação de desastre e capacidade de implantação mantêm provas próprias. [Prova final consolidada](proofs/sgc-invoice-workspace-final-20261110.json); o resumo inicial de19 casos/138 asserções fica preservado. diff --git a/docs/SGC_PREPARATION_20261110.md b/docs/SGC_PREPARATION_20261110.md new file mode 100644 index 0000000..1f86bd7 --- /dev/null +++ b/docs/SGC_PREPARATION_20261110.md @@ -0,0 +1,37 @@ +# Preparação SGC — 10/11/2026 + +Continuação posterior do mesmo dia: [endpoint de facturas implementado e provado](SGC_INVOICE_ENDPOINT.md),78 verificações, motor nativo e autenticação/contexto/idempotência. A rota está agora registada especificamente, mas permanece desligada por omissão. Os bloqueios descritos abaixo registam o estado **anterior** desta preparação; as classes/serviços legados não são usados como writer alternativo. + +Pedido: aplicar migrations, implementar/popular dimensão temporal de dez anos, limpar configuração/rotas e experimentar o POST de facturas. Não constitui activação do conector ou autorização de pagamentos/concessões novas. + +## Executado + +- PostgreSQL existente em127.0.0.1:55432 retomado, sem criar cluster/base/porta. Destino confirmado pela configuração: `sgf_tempo_dev`. A instalação8085 não foi copiada, actualizada ou substituída nesta preparação. +- Backup privado anterior à aplicação: `C:/laragon/data/sgf-tempo/backups/sgc-preparation-20261110/before.dump`. Não se executou restauro; não se declara recuperação ensaiada. +- Migrations explícitas `2026_11_09_000001_create_sgc_integration_tables` e `2026_11_09_000002_create_sgc_analytics_tables` aplicadas por proprietário de manutenção existente, lotes18/19. Não se executou migrate global: `2026_10_28_130000_create_workshop_billing_corrections` continua pendente e não pertence ao pedido SGC. +- Corrigidas três falhas da migration analítica: importação de `DB`, referência de departamento para `analytics_dim_organizacao.id` e alias reservado PostgreSQL `do` substituído por `org`. A primeira tentativa analítica falhou pelo alias e reverteu transaccionalmente; a segunda concluiu. A primeira migration não foi repetida nem revertida. +- O comando `PopulateDimTempo` já existia como esqueleto vazio com nome `app:populate-dim-tempo`; foi implementado como `sgc:populate-dim-tempo`. Não se executou `make:command` sobre esse ficheiro existente. +- População explícita de01/01/2026 a31/12/2035:3652 dias. Segunda execução:0 inserções. Usa gerador, lotes250 e transacção única; `insertOrIgnore` preserva datas e classificações existentes, sem truncate/update. +- `eh_dia_util` representa segunda a sexta; os feriados oficiais não foram fornecidos/validados e não são inferidos. `eh_feriado=false` é o valor preparatório da migration, não uma certificação de calendário laboral. +- `config:clear` e `route:clear` executados com PHP8.4.27 do projecto. Não se executaram `cache:clear`, `optimize:clear` ou remoções da cache de dados protegida. + +## Verificação + +Quatro testes unitários/17 asserções: intervalo, unicidade, ano bissexto, campos de calendário, simulação sem ligação à base e recusa de argumentos inválidos. Apenas este ficheiro de testes foi executado. O harness inicial de dois testes não fornecia o container mínimo exigido pelo comando Laravel; corrigido sem ligar a bases ou executar suites alheias. + +Conferência SQL de3652 linhas, limites2026–2035 e existência da vista materializada. Fotografia anterior/posterior de todas as86 tabelas públicas preexistentes, excepto a tabela de controlo `migrations`: contagens e MD5 de todas as linhas ordenadas idênticos. Inclui dados e concessões/cache anteriores, não apenas as suas contagens. Fotografias ficam privadas; não foram impressos dados/credenciais. As tabelas novas de facturas/log SGC permanecem vazias. Nenhuma concessão explícita nova; SELECT/INSERT da conta de aplicação sobre o calendário estão disponíveis pelas permissões existentes. + +```powershell +& 'C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe' artisan sgc:populate-dim-tempo --start-year=2026 --years=10 +& 'C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe' artisan sgc:populate-dim-tempo --start-year=2026 --years=10 --dry-run +``` + +## POST: resultado e bloqueios + +O POST de diagnóstico para `http://sgf-tempo.test/api/v1/integration/sgc/invoices`, com JSON vazio e Accept JSON, devolveu **404**. Não é o teste positivo pedido, nem uma factura aceite. O ficheiro `tests/payloads/invoice_test.json` não existe. A chave de idempotência deve ser gerada com `[guid]::NewGuid().ToString()` no PowerShell quando houver um contrato/payload válido; `uuidgen` não é pressuposto neste Windows. + +`artisan route:list --path=api/v1/integration/sgc` confirma ausência da rota registada: `bootstrap/app.php` não inclui `routes/api.php`. Esse ficheiro novo também oferece pagamentos/analytics e não declara autenticação/isolamento. Não se registou todo o ficheiro para contornar o404. + +Classes referenciadas e ausentes: `StoreInvoiceRequest`, `StorePaymentRequest`, modelos `Fornecedor` e `Recebimento`. A integração proposta sincroniza fornecedor automaticamente, não demonstra isolamento por empresa/unidade e grava o recibo de idempotência depois do commit de negócio. O serviço orçamental contém uma resposta de saldo simulado fixa. Não equivale ao percurso nativo já validado em8085 e não deve ser activado como alternativa por consequência das migrations. + +Próxima entrega necessária para um teste positivo: contrato/payload sintético, autenticação e contexto explícitos, reutilização do motor existente em vez de espelho autónomo, idempotência atómica com comparação do payload e classes/validações completas. Pagamentos não são necessários para testar facturas. Sem publicação, activação de candidatas, alteração dos seis registos, cache de dados ou provas anteriores. Não se declara capacidade de implantação. diff --git a/docs/SGF_8085_NEXT_AUTHORIZATION.md b/docs/SGF_8085_NEXT_AUTHORIZATION.md new file mode 100644 index 0000000..7e3a75d --- /dev/null +++ b/docs/SGF_8085_NEXT_AUTHORIZATION.md @@ -0,0 +1,36 @@ +# Decisão para concluir os seis módulos na8085 + +Autorização expressa de10/11/2026: **«autorizo»** para o recorte abaixo. Aplicação em curso; não presumir etapas concluídas. Não constitui conclusão ou aceitação do SGF. + +## Trabalho já efectuado + +A8085 recebeu70 tabelas aditivas de documentos, classificação, recebíveis, Oficinas, custos e ligações. As quatro tabelas iniciais de Comercial foram reutilizadas. Dados anteriores, sequências, permissões e associações foram comparados e preservados; existe dump privado anterior e cópia dos ficheiros substituídos. Não houve migrate global nem limpeza da cache. + +As guardas de ensaio foram ligadas à identidade física exacta desta composição. Resets/destruição ficam recusados mesmo quando a composição for activada. Não se criaram contas ou concessões. As funções/tabelas/sequências novas não concedem acesso a PUBLIC no destino. + +Stock tem código instalado, ainda desligado: saída, transferência imediata, contagem, aplicação da diferença e selector de equipamento, pelos writers originais. Prévia revertida, versão, impacto antes/depois, fundamento, recibo auditado e recuperação. Contagem por lotes e devolução conservam seus percursos próprios; não se substituem por ajuste genérico. + +O servidor existente estava parado e foi iniciado novamente na mesma8085. Compras continua em `/compras`; recepção e obrigação100MZN foram consultadas por HTTP sem repetir comandos de negócio. Nenhuma base/porta/demonstração nova ou dependência SGC. + +## Autorização solicitada, num único recorte + +1. **Perfis exclusivamente fictícios:** criar dez contas de aplicação para preparação/revisão Comercial, gestão/técnico/conferência Oficinas, preparação/revisão Financeiro, preparação/revisão Contabilidade e consulta. A [matriz exacta](integration/sgf-8085-access-proposal.json) discrimina direitos e âmbitos. Não criar roles PostgreSQL, contas reais ou `access.manage`. +2. **Âmbito único:** tenant1, empresa2, unidade2, já existentes. Preservar os sete perfis de Compras. Os perfis não recebem acesso a outra unidade nem autoridade para decidir os seis registos protegidos. +3. **Armazém:** acrescentar ao perfil sintético existente os direitos discriminados na matriz. Apenas dois nomes novos: `stock.operations.prepare` e `stock.operations.confirm`, apresentados antes de qualquer concessão. Preparar um segundo armazém vazio e um artigo de ensaio explicitamente ligados; não usar o armazém estrangeiro como destino. +4. **Preparação do percurso:** criar cliente/serviço de teste, emissor/série/perfis fictícios, equipamento/tarefa e parâmetros de custo. Usar os workflows e revisões existentes com pessoas distintas. Não converter, associar ou alterar os seis registos pendentes. +5. **Continuação da obrigação100MZN existente:** autorizar a liquidação/afectação sintética20MZN, sua reversão e contabilização individual revista. Reutilizar a obrigação original, sem outra dívida. Esta origem estava expressamente excluída dos efeitos financeiros da entrega anterior de Compras. +6. **Reunir os cenários já aprovados:** Comercial1000MZN/afectações400+600; custo directo275→295; industrial1800→1820; stock2UN/100MZN, saída1UN/50 e devolução0,5UN/25. Registos novos do ensaio integrado na mesma instalação, preservando anteriores. Cada efeito conserva decisão, autoridade, versão e idempotência; nada é propagado automaticamente. +7. **Um diário comum:** instalar no livro financeiro local já usado por Compras, com âmbito SGF explícito empresa/unidade e origens qualificadas de Comercial/Oficinas. Não copiar obrigações/saldos nem criar autoridade concorrente. Esse livro nativo é parte local do SGF, sem serviço remoto SGC. Comprovar o protocolo de origem/versão/bloqueio entre bases antes de disponibilizar a continuidade. +8. **Contas/período QA e activação limitada:** preparação/revisão artificiais, conferência e fecho sintético, sem reabertura. Activar apenas os percursos SGF do recorte quando a integração e as recusas de acesso afectadas passarem, conservando confirmações individuais. + +Este recorte permite continuar até à entrega dos seis módulos sem pedir autorização por cada campo ou teste. Não presume funcionalidades por implementar aceites nem capacidade de implantação. + +## Exclusões + +Sem dinheiro/banco/emissão fiscal, contas ou acessos reais, direitos públicos, publicação, candidata de Compras, nova demonstração/base/porta ou activação SGC. Sem decidir os seis registos, limpar a cache bloqueada, reabrir períodos ou apagar provas aceites. Revisão visual e implantação exigem evidência própria. + +## Motivo da decisão + +A regra de acessos do projecto exige apresentar direitos novos antes de os conceder. A entrega de Compras na8085 excluiu pagamentos/Contabilidade desta origem. A instrução de continuar permitiu instalar e preparar código; não a usei para conceder acessos ou executar os efeitos financeiros excluídos. A matriz e os cenários acima tornam a decisão concreta. + +Após aprovação, continuar implementação e integração até ao percurso demonstrável dos seis módulos. **O SGF ainda não está pronto para apresentação integral.** diff --git a/docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md b/docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md new file mode 100644 index 0000000..2cdd828 --- /dev/null +++ b/docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md @@ -0,0 +1,32 @@ +# Contabilidade — submenus funcionais e relatórios por período + +10/11/2026, instalação8085 existente. O bloco foi desenvolvido no código e conferido antes de actualizar a instalação de apresentação. Sem demonstração/base/porta nova, SGC, direitos adicionais ou efeitos financeiros. + +## Percurso utilizável + +Os submenus de Contabilidade deixaram de devolver o mesmo painel: Painel explica a ligação entre origem, decisão e relatório; Trabalho diário abre o diário original; Conferência apresenta fontes fiscais/bancárias apenas quando autorizadas; Fecho encaminha para a conferência do período sem o fechar; Configuração reúne contas, classificação e regras já existentes. A navegação aceite e a transição explícita entre módulos mantêm-se. + +Em **Contabilidade → Análise**, o utilizador escolhe datas e consulta o balancete. Não há intervalo anual fixo ou datas escolhidas automaticamente. Cada conta mostra abertura líquida, débitos, créditos e fecho; clicar **Abrir razão desta conta** conserva o intervalo e escolhe explicitamente a conta, sem redigitar identificadores. O razão mostra lançamento/linha, data, fundamento, proposta, revisão e saldo progressivo. + +As páginas25/100 e os totais vêm do leitor original, sem recálculo no navegador. Totais referem-se ao filtro completo; saldo progressivo inclui páginas anteriores. Montantes são representados por inteiros exactos, recusando valores JavaScript inseguros. O marcador append-only do livro é comparado entre páginas: se mudar, exige-se actualizar a primeira página. Isto detecta mudança entre leituras, não cria um snapshot persistente do universo. + +CSV exporta **apenas a página consultada**, com datas, moeda e referência do livro; texto livre é protegido contra fórmulas em folhas de cálculo. Não é arquivo certificado ou relatório completo exportado. A opção de impressão abre **nova leitura** no relatório original, com os mesmos filtros; não afirma que a base ficou congelada desde a consulta anterior. Filtros não disparam pedido de guardar alterações de negócio. + +## Correcção HTTP dirigida + +A conferência identificou que o adaptador composto deixava escapar excepções de domínio/argumentos: conta inexistente e razão sem conta podiam produzir500. O adaptador passa a devolver403 para conta indisponível e422 para argumentos inválidos, alinhado com o controlador original. Contexto, permissões, engine de relatórios, SQL, writers e schema nativos permanecem iguais. + +## Evidência e preservação + +- Cinco verificações do adaptador na regressão existente: conta indisponível403, conta obrigatória422, identificador inválido422, tentativa de alterar empresa422 e consulta válida200. Sem operações de negócio. +-19 verificações HTTP8085 do bloco integrado: balancete/razão efectivos, conta QA-CAIXA com fecho950MZN, equilíbrio, proposta/revisão/fundamento, paginação25/100, representação/exportação, resposta atrasada de outro intervalo, marcador alterado simulado sem escrita, entradas indevidas e módulo errado. +-14 verificações de renderização e autoridade: linhas efectivas, distinção dos cinco submenus, datas vazias, escolha de conta sem digitar ID, links condicionados e escapagem. Não são revisão visual de navegador. +-32 conjuntos de dados e binding comparados antes/depois: intactos. Diário27, Caixa950, cinco cálculos e restantes exemplos preservados. Zero concessões/lançamentos/pagamentos ou limpeza da cache; sem comandos dirigidos aos seis registos protegidos. + +[Índice com SHA das fontes/provas](proofs/sgf-accounting-reports-20261110.json). As17 verificações positivas antes da instalação foram substituídas pela conferência final19; não são somadas outra vez. Provas anteriores do engine e dos workflows foram reutilizadas, sem repetir suites alheias. Alterações fora do método HTTP de relatórios foram comparadas antes de copiar o controlador; backups e dados privados fora do repositório. + +## Limites que permanecem + +O balancete e o razão são do exercício QA, com contas artificiais. Não foram implementados balanço legal, demonstração de resultados/fluxos, classificação normativa automática, recorrências, depreciações, câmbio, reabertura, motores fiscais gerais ou modelos oficiais. As fontes bancárias/fiscais continuam nos seus recortes aprovados. O leitor composto ainda conserva o bloqueio administrativo original; não se declara que esta interface removeu esse gargalo ou validou carga sustentada. + +Revisão visual do conjunto, carga sustentada e capacidade de implantação mantêm provas próprias pendentes. Sem publicação, candidata de Compras, aceitação/checkpoint ou declaração de produto completo. A8085 é a integração de apresentação; o trabalho de código e as verificações não exigem alterações sucessivas dos seus exemplos financeiros. diff --git a/docs/SGF_BANK_RECONCILIATION_DELIVERY.md b/docs/SGF_BANK_RECONCILIATION_DELIVERY.md new file mode 100644 index 0000000..daa1ca8 --- /dev/null +++ b/docs/SGF_BANK_RECONCILIATION_DELIVERY.md @@ -0,0 +1,51 @@ +# Conferência de extracto QA — entrega na8085 + +10/11/2026, `SGF-BANK-RECONCILIATION-QA-v1`, conforme [âmbito autorizado](SGF_BANK_RECONCILIATION_SCOPE.md). Mesma instalação, entrada **Financeiro → Conferência de extracto QA → Abrir extracto, conferência e histórico**, ou `/finance/bank` após escolha da função autorizada. + +## Resultado conservado + +Um extracto **artificial interno** `QA-BANCO-LOCAL`, MZN,2026-11: abertura0, débito20, crédito20 e fecho0. O saldo intermédio−20 é explícito no exercício; não significa autorização de descoberto, caixa disponível ou saldo bancário real. A conta de liquidação não foi classificada como banco. + +Importação13 → revisão14 do extracto → proposta13 da linha `QA-OUT-20` para a linha contabilística4/diário2 → revisão14 → proposta13 da linha `QA-IN-20` para a linha5/diário3 → revisão14 → conferência final14, diferença0. **Um cabeçalho, duas linhas e sete eventos** conservados na8085; relatório CSV interno com assinatura verificável. + +O leitor nativo conferiu as duas linhas **efectivamente existentes** de `QA-LIQUIDACAO`,20/reversão20, por SELECT no livro físico ligado à mesma composição/empresa/unidade. Confere IDs fixados, conta, data, moeda, sinal/valor e fotografia do diário; não toma correspondência por igualdade de valor nem oferece toda a contabilidade. Reutiliza a ligação nativa existente, sem novo PGrole; o adaptador apenas executa leituras, não é uma concessão de escrita nem uma nova role SELECT-only. + +Não foram criados pagamentos/estornos/comissões/ajustes ou lançamentos para preparar ou fechar o exercício. Caixa950 fechada,27 lançamentos, período contabilístico, IVA com quatro versões e regras com quatro eventos/dois cálculos intactos. Não existe SGC/portal/banco/API de instituição em execução neste percurso. + +## Controlos e histórico + +CSV UTF-8 limitado8192 bytes e ao caso artificial aprovado, com validação de cabeçalho, conta/período/moeda, ordem/data/referência, sinais, montantes, abertura/fecho e saldos intermédios. Não é importador bancário geral. Ficheiro não substitui fotografia anterior; mesmo ficheiro/chave recupera a origem sem duplicação. Com outra chave e o mesmo ficheiro, devolve o extracto existente sem criar eventos ou linhas, inclusive depois da conferência final; isso não reabre o processo. + +Extracto importado não cria correspondências. Revisão distinta do extracto precede propostas individuais; revisão de cada proposta exige alvo exacto e preparador ainda autorizado. Proposta por rever não permite outra para a mesma linha. Uma linha contabilística ou de extracto já conferida não pode ser usada novamente. Finalização exige as duas correspondências revistas e nenhuma proposta pendente. Não há reabertura de conferência finalizada, Caixa ou período. + +Prévia cifrada600s em savepoint revertido, impacto antes da confirmação, contexto/autor/direitos/versão/origem revalidados sob lock da empresa. `br_statements` fixa CSV/SHA e linhas; `br_events` fixa predecessor, proposta exacta, autor/preparador/data/fundamento/estados e correspondências; `br_commands` fixa chave/hash/resultado. Triggers exigem acesso/revisão distinta/versão, protegem história append-only e impedem duplicação de correspondências. Guarda física QA e PUBLIC revogado. + +Recibo próprio recuperável noutra sessão por GET, sem executar de novo. Fonte alterada bloqueia a conferência/relatório, sem preencher saldos zero. Interface exige escolha explícita das duas linhas e da decisão, protege campos durante operação incerta e conserva histórico/consulta depois da finalização. Upload/leitura de ficheiro limitado ao CSV QA; botão do exemplo só preenche após escolha do utilizador. Não lê ficheiros por caminhos recebidos do cliente. + +Linhas de extracto são **limitadas a duas por este contrato**, com filtro de estado. Histórico é SQL25/100 e foi percorrido além100 eventos. Não se declara importação/paginação de extractos de volume arbitrário. O relatório é derivado das fotografias imutáveis da conferência, com checksum no download e auditoria; não se declarou arquivo independente do binário/renderer para retenção legal de longo prazo. + +## Acessos e provas + +`bank_reconciliation.view/import/prepare/review`: **oito concessões por cada uma das duas bases QA existentes**,16 no conjunto, exactamente [como aprovado](integration/sgf-bank-reconciliation-access-proposal.json). Importação/preparação13, revisão14 e consultas13–17. Não herdados de Caixa/recebíveis. Sem novo utilizador/PGrole/direito PUBLIC/acesso real. Rootflag desligada; somente a8085 disponibilizada com vínculo privado por hash. + +|Prova|Resultado| +|---|---| +|37 regressão, rollback integral|Importação/prévia, sinais/moeda/valor, revisão distinta, fase de extracto, versões antigas, linha/pedido repetidos, correspondência manual, finalização/relatório, origem alterada, falha após importação com rollback/recuperação e dados anteriores intactos.| +|Concorrência na regressão|Segunda revisão503/Retry-After5 durante o lock da empresa; libertação→409 por versão antiga. Uma revisão válida, recuperação da primeira chave sem duplicação.| +|11 SQL/fronteiras, rollback|126 eventos de metadados (63 propostas/recusas) além dos dois eventos de concorrência;128 percorridos em25 e100/28. Filtros, actor/contexto/unidade/módulo, histórico imutável e writer indevido. Fixtures não são correspondências confirmadas.| +|28 HTTP na8085|Leitor nativo actual, duas linhas reais do QA, sete confirmações/repetições, dois pares conferidos individualmente, finalização/relatório/checksum, sinais/IDs/reutilização/actor/módulo/tenant, recibo noutra sessão e ecrã. Progresso guardado para não repetir operações válidas.| +|15 DOM→HTTP|Consulta do estado final, duas correspondências visíveis, formulário fechado, filtros/histórico100/detalhe, recibo e integração com Caixa950/IVA4/regras2cálculos. **Zero POSTs de negócio**. Não é navegador ou aceitação visual.| +|Três HTTP do relatório|Resposta CSV/checksum preservados através do mesmo tratamento HTTP limitado para ocupação. Não foi repetido o percurso de sete decisões.| +|Preservação SELECT-only|20 conjuntos originais completos e binding idênticos; apenas oito concessões aprovadas adicionadas. Sem append nativo ou alteração dos exemplos anteriores.| + +Total94 verificações dirigidas, concorrência separada. [Índice verificável](proofs/sgf-bank-reconciliation-20261110.json). + +**Distinção de origem das provas:** a regressão existente não contém os dois lançamentos20/20. Os testes de componente/persistência/concorrência injectaram um fornecedor de origem artificial **apenas no harness CLI**, como autorizado, com `source_kind=regression_harness_fixture`. Não foram criados lançamentos/pagamentos para preparar testes. O leitor instalado não tem fallback ou origem de regressão; a execução HTTP8085 comprovou `source_kind=native_qa_ledger` e as duas linhas efectivas. Não apresentar a prova com origem simulada como integração nativa total da regressão. + +Uma ambiguidade de nome no trigger de finalização foi detectada/corrigida só na regressão antes da instalação; a execução de negócio foi revertida e a prova final passou com a guarda corrigida. Não foram somadas tentativas falhadas como provas válidas. + +## Limitações restantes + +Amostra artificial de duas linhas/um período/uma conta de exercício. Não é integração bancária, reconciliação geral, pagamento, livro bancário real ou confirmação da natureza de uma conta. Recusa do extracto mantém o processo bloqueado; protocolo geral de correcção/substituição/reenvio de extractos não faz parte desta entrega. Proposta de correspondência recusada pode ser preparada novamente, sem apagar a recusa. + +Sem novos motores legais, remunerações, modelos oficiais, rácios financeiros amplos ou capacidade de implantação. Visual continua pendente, via bloqueada não repetida; carga sustentada, retenção/órfãos, arranque completo, conformidade e implantação exigem evidência própria. Seis registos/cache/trabalho alheio preservados; sem novas bases/portas/demos, SGC, publicação/candidata, aceitação/checkpoint ou dados/acessos reais. Dumps e dados privados fora do repositório. diff --git a/docs/SGF_BANK_RECONCILIATION_SCOPE.md b/docs/SGF_BANK_RECONCILIATION_SCOPE.md new file mode 100644 index 0000000..7438f28 --- /dev/null +++ b/docs/SGF_BANK_RECONCILIATION_SCOPE.md @@ -0,0 +1,36 @@ +# Proposta agrupada — conferência de extracto bancário QA + +`SGF-BANK-RECONCILIATION-QA-v1`,10/11/2026. **Autorizado expressamente e entregue**; ver `SGF_BANK_RECONCILIATION_DELIVERY.md`. O texto abaixo conserva o âmbito aprovado. Fecha a lacuna de conferência de extracto no módulo Financeiro, mantendo o percurso de Caixa e os livros existentes. Não é autorização para operar bancos ou dinheiro real. + +## Percurso completo proposto + +Na mesma8085/regressão, importar o CSV artificial `QA-EXTRACTO-20261110`: entidade fictícia `QA-BANCO-LOCAL`, MZN, abertura0, débito20 e crédito20 em10/11/2026, fecho0. O saldo intermédio−20 é parte deste exercício documental; não representa autorização de descoberto, caixa disponível ou crédito real. + +O documento é um **extracto artificial interno**, sem conexão de instituição ou transmissão externa. Não se usa o recebimento1000 da Caixa, nem se reclassifica a Caixa fechada950. A referência de comparação são duas linhas contabilísticas de liquidação já existentes: crédito20 do diário2 e débito20 do diário3, conta artificial `QA-LIQUIDACAO` (linhas4/5 na8085). Corresponderam ao ciclo fornecedor20/reversão20 já provado. A conta não é declarada bancária globalmente; a comparação fica identificada e limitada a este exercício. + +Importação13 → revisão distinta14 do extracto/abertura/fecho → escolha explícita, individual e fundamentada da linha do extracto e da linha contabilística → prévia do impacto → confirmação/revisão14 da correspondência exacta → conferência final com zero linhas por conferir e diferença0 → histórico e relatório interno recuperáveis. Não criar pagamento, estorno, comissão, ajuste ou lançamento para fazer coincidir valores. Referências de valor igual não são associadas automaticamente. + +Importar o mesmo ficheiro/chave recupera a mesma origem, sem duplicar linhas. Ficheiro alterado não substitui fotografia confirmada. Uma linha contabilística ou de extracto já conciliada não pode ser usada de novo; divergência ou origem ausente permanece pendência. Dados fora da instituição/conta/período/empresa/unidade QA fixados são recusados. Datas, sinal, moeda, totais e sequência são conferidos sem inferir saldo de conta real a partir desta amostra. + +Confirmação revalida autor/direitos/contexto, versão do extracto/correspondência, fonte contabilística e dependências. Histórico append-only com preparador/revisor/data/fundamento/estado anterior e posterior; recuperação de resposta incerta pelo pedido original. Sem reabertura de Caixa, período contabilístico ou conciliação finalizada. Importação não confirma correspondências nem altera o diário. + +## Novos direitos a decidir + +Quatro nomes e **oito concessões por cada base QA existente**,16 no conjunto, todos tenant1/empresa2/unidade2 e perfis existentes: + +|Direito|Destinatários| +|---|---| +|`bank_reconciliation.view`|Financeiro13/14; Contabilidade15/16; consulta17| +|`bank_reconciliation.import`|Financeiro preparação13| +|`bank_reconciliation.prepare`|Financeiro preparação13| +|`bank_reconciliation.review`|Financeiro revisão14| + +[Matriz integral](integration/sgf-bank-reconciliation-access-proposal.json). Consulta abrange apenas o extracto artificial e as duas fontes fixadas do exercício. Não confere acesso geral a dados bancários/documentos de outros módulos. `service_receivables.reconcile` já existente refere-se ao percurso de recebimento, não concede importação/conferência de extractos. Sem novo utilizador, PGrole, direito PUBLIC, conta bancária real ou concessão real. + +## Provas e instalação depois da autorização + +A base de regressão existente não contém as duas linhas de liquidação20/20 da8085 (confirmado por consulta só de leitura). Nesse livro, a persistência/concorrência/guardas serão testadas com um fornecedor de origem artificial injectado apenas no harness, sem criar pagamentos ou lançamentos para preparar testes. O leitor nativo de produção não terá fallback e continuará limitado ao seu livro físico; o percurso HTTP na8085 provará a ligação às duas linhas efectivamente existentes. O relatório distinguirá explicitamente a origem simulada nos testes de componente e a origem contabilística real do QA na instalação. + +Primeiro regressão existente: leitura/importação por hash, filtros/páginas25/100 com metadados de teste, contexto/empresa/unidade/módulo, revisão própria, abertura/fecho/sinais/moeda, correspondências manuais, versões antigas, concorrência, reutilização indevida da mesma linha, repetição e falha/recuperação atómica. Depois disponibilizar o percurso completo limitado na mesma8085, conservando a evidência e exemplos anteriores. Não repetir aquisições, liquidações ou lançamentos que originaram as duas linhas. + +Este bloco inteiro inclui os campos/testes do contrato, sem pedidos por cada detalhe. Preservar27 lançamentos, Caixa950, quatro versões IVA, quatro eventos de regra/dois cálculos, seis registos/cache/trabalho alheio. Sem novas bases/portas/demos, SGC/portal em execução, bancos/APIs/credenciais/dados reais, pagamentos/efeitos financeiros novos, mudança da classificação do plano, índices no destino, reabertura, publicação/candidata/checkpoint/aceitação ou capacidade de implantação. Visual/carga continuam separados. Outros motores fiscais, modelos oficiais e operação bancária geral permanecem fora deste recorte. diff --git a/docs/SGF_BILLING_REGISTER_DELIVERY.md b/docs/SGF_BILLING_REGISTER_DELIVERY.md new file mode 100644 index 0000000..0d1283e --- /dev/null +++ b/docs/SGF_BILLING_REGISTER_DELIVERY.md @@ -0,0 +1,18 @@ +# Registo unificado de documentos —10/11/2026 + +Implementado na8085 em **Comercial → Todos os documentos** e **Facturas**, reutilizando os direitos de consulta já concedidos. Sem novos direitos, tabelas, índices, escritores ou operações financeiras. + +A lista anterior carregava facturas e respectivos detalhes, além de rascunhos, antes de filtrar no cliente; os novos documentos não apareciam nesse conjunto. O registo actual combina na SQL facturas, facturas-recibo, recibos, créditos, orçamentos e rascunhos autorizados. Filtros por tipo, referência/cliente/NUIT literal, moeda, série exacta, autor, estado e emissão, ordenação estável e páginas25/100 são aplicados no servidor. Mantém os estados internos/substituídos/cancelados dos rascunhos. Bases dos rascunhos são calculadas pelo mecanismo canónico apenas nas linhas da página; não são totais fiscais. + +Uma factura-recibo aparece uma vez, com a factura técnica acessível no seu percurso, sem duplicação na lista comum. Uma família não autorizada não entra nas linhas ou contagens e não oculta outra família autorizada. Metadados estrangeiros não suprimem uma factura deste contexto. O saldo é uma projecção conjunta, limitada às facturas da página, só com acesso financeiro; não se usa zero quando falta autoridade ou reconhecimento. Recibo/crédito mostram o saldo da **factura de origem**, não um saldo próprio. Não se somam facturas, recebimentos, créditos, bases ou moedas diferentes. + +PDFs e impressão de orçamento continuam nos leitores originais com revalidação de acesso; não se regeneram documentos. A abertura do rascunho conserva o destino existente. O carregamento do novo componente é diferido; build privado aprovado, sem alterar o limiar de tamanho. + +## Provas e preservação + +- 33 verificações dirigidas na regressão existente:127 fixtures de metadados percorridas em seis páginas25 e duas páginas100, pesquisa literal de `%_!`, filtros anteriores, base canónica, datas, direitos, ocultação financeira, módulo/contexto e fixture estrangeira que não pode ocultar uma factura autorizada. Fixtures e alterações transitórias de direitos integralmente revertidas; documentos, rascunhos e efeitos originais intactos. Prova privada `receipt-composition-b4093c6f6707/single-integration/20261110-174502-e0b30bb6/billing-register-result.json`. +- Duas consultas de dados do registo por página de orçamentos observadas no ensaio, excluindo validações de contexto/direitos. Páginas com facturas/rascunhos acrescentam projecções limitadas dos respectivos saldos/bases; não se declara o mesmo número para todas as famílias nem capacidade de carga sustentada. +- 16 controlos HTTP na8085: seis famílias, contagens, factura-recibo sem duplicação, crédito100/saldo500, factura-recibo/saldo0, bases sem emissão presumida, pesquisa literal, contexto/página indevidos recusados e diário25 equilibrado. `receipt-composition-cc50a276c10c/billing-register-http-20261110.json`. +- Instalação local com cópia anterior dos quatro ficheiros e manifesto/build privados; assets anteriores conservados. Sem limpeza da cache, novas demonstrações, publicação, direitos públicos ou alterações dos seis registos. Não se repetiram comandos dos percursos financeiros/Compras. + +Os estados do documento continuam distintos do pagamento, da custódia de Caixa e da validade fiscal. Navegador/revisão visual não foram declarados concluídos; carga sustentada e implantação continuam a exigir evidência própria. A próxima lacuna operacional identificada, Caixa, tem âmbito/direitos apresentados em `SGF_CASH_NEXT_SCOPE.md`; ainda sem implementação ou concessão. diff --git a/docs/SGF_CASH_DELIVERY.md b/docs/SGF_CASH_DELIVERY.md new file mode 100644 index 0000000..4a51238 --- /dev/null +++ b/docs/SGF_CASH_DELIVERY.md @@ -0,0 +1,46 @@ +# Caixa QA integrada na8085 —10/11/2026 + +Bloco `SGF-CASH-QA-v1` expressamente autorizado pelo utilizador e entregue na mesma instalação. **Financeiro → Recebimentos e Caixa → Caixa — abertura, despesas e fecho QA**, ou `/finance/cash` após escolha de função. Oficinas → Financeiro tem continuação explícita para o módulo Financeiro. Mantém navegação horizontal/dropdowns e guardas de contexto; sem novas demonstrações, bases, portas ou dependência do SGC. + +Os quatro direitos aprovados foram concedidos exactamente aos perfis existentes: `cash.view`13/14/15/16/17; `cash.prepare`13; `cash.review`14; `cash.close`14. Oito concessões em cada uma das duas bases QA existentes, empresa2/unidade2. Sem novas pessoas/perfis, roles PostgreSQL, funções nativas ou acessos reais/PUBLIC. Configuração desligada por omissão fora da instalação seleccionada. + +## Percurso concluído e conservado + +|Passo|Preparação|Conferência|Saldo QA| +|---|---|---|---:| +|Abertura explícita0|Financeiro13|Financeiro14|0MZN| +|Custódia fictícia1000, recebimento existente escolhido e meio Numerário declarado|Financeiro13|Financeiro14|1000MZN| +|Despesa fictícia50 com beneficiário/descrição/comprovativo|Financeiro13|Financeiro14|950MZN| +|Contagem950 e fecho|Contagem13|Revisão e fecho14|950MZN, fechado| + +Nove eventos append-only na8085. O recebimento é o da factura-recibo nova, efectivamente confirmado/afectado anteriormente; Caixa não emite outro documento, não recebe de novo, não altera a dívida nem presume numerário a partir da factura. A declaração de custódia é própria, explícita e fundamentada. Nenhum dinheiro real foi movimentado. + +No mesmo diário, Contabilidade15 prepara e aplica, Contabilidade16 revê individualmente: reclassificação1000 D QA-CAIXA/C QA-LIQUIDACAO-CLIENTE e despesa50 D QA-DESPESA-CAIXA/C QA-CAIXA. A origem da liquidez tem de estar contabilizada antes da reclassificação. A conta de liquidez QA não representa uma conta bancária real. Contagem e fecho não lançam automaticamente. A instalação termina com **27 lançamentos equilibrados**, incluindo apenas esses dois novos factos. + +Fecho de Caixa recusa movimentos por valorizar, contagem divergente/antiga, decisões pendentes, autoridade revogada ou origem revertida. Divergência fica preservada; revisão pode recusar para permitir nova contagem, sem ajuste automático. O livro fechado não reabre. Uma reversão posterior da origem é bloqueio de conferência, sem retirar numerário ou devolver dinheiro por arrasto. + +## Consulta, prévia e recuperação + +Extracto por datas/tipo e páginas25/100 na SQL; abertura/entradas/despesas/fecho do intervalo em leitura consistente. O filtro por tipo limita o histórico, não altera o resumo global da Caixa. Contagem não é movimento. Antes da abertura e quando a origem está bloqueada, o ecrã identifica o estado em vez de fabricar um saldo real. + +Todas as decisões exigem fundamento, prévia revertida e confirmação com nova validação de identidade/contexto/permissões/versão/origem. A transacção mantém autoridade e bloqueio de empresa; escritor, recibo e auditoria são atómicos no host. Datas de classificação/despesa/contagem não podem anteceder as suas origens. Só valores exactos do recorte aprovado são admitidos. + +Uma referência por confirmação recupera o recibo histórico, mesmo noutra sessão da mesma pessoa/contexto autorizado, sem executar de novo. Referências próprias ficam no histórico; a consulta não concede poderes de escrita. Formulários/prévia e resultado incerto ficam protegidos; não há limpeza de cache ou armazenamento de credenciais no cliente. + +## Provas + +- 23 verificações dirigidas, todas revertidas na regressão existente: abertura, revisão própria/acesso indevido recusados, origem estrangeira, saldo, versão antiga, revogação, falha depois do writer e recuperação, duplicação, divergência, origem revertida, fecho sem diário recusado, duas valorizações e conservação dos documentos/efeitos financeiros. `receipt-composition-b4093c6f6707/single-integration/20261110-175251-07723b42/cash-result.json`. +- Concorrência entre dois processos apenas na regressão: segunda confirmação503 durante o bloqueio da empresa, depois409 por versão antiga; uma classificação e recuperação da primeira chave. `cash-concurrency/result.json`. O ensaio conserva uma abertura/classificação1000 nessa regressão, sem a apresentar como o livro concluído da8085; não repetir a preparação nem a prova positiva inicial por rotina. +- Sete verificações adicionais de leitura/fronteiras, após a prova concorrente: SQL25/100, intervalo posterior com abertura1000, tipo sem alterar saldo, data incompatível e empresa alheia recusadas, ciclo incompleto sem fecho. `single-integration/20261110-175845-a7f3eb7b/cash-read-boundaries.json` na regressão. +- 21 controlos HTTP na8085: nove decisões com recuperação, declaração1000 sem outra factura/recibo, despesa50, contagem950, fecho prematuro recusado, extracto, consulta sem escrita403, recuperação noutra sessão e diário27 equilibrado. As duas páginas do diário contêm25+2 registos únicos; tamanho da página não é o total. `cash-http-20261110.json`. +- Oito verificações de período na8085: **38 factos/zero pendentes**, fecho/repetição e recusas em rollback, sem reabertura; período de apresentação mantém-se aberto. `single-integration/20261110-180324-fe0a46fd/period-cash-result.json`. +- 14 verificações DOM→HTTP de consulta/recuperação: Caixa fechada950, nove eventos, comandos bloqueados, filtros por tipo/data, saldo transportado e recibo recuperado, **zero POSTs de confirmação**. `cash-dom-http-20261110.json`. Não é revisão visual de navegador. +- Comparação SELECT-only com backup privado anterior a Caixa:25 lançamentos, clientes, facturas, movimentos físicos, registos/efeitos de recebíveis e binding original byte a byte intactos. `single-integration/20261110-180329-e03178dc/cash-preservation.json`. Backup/dumps e dados privados fora do repositório; nenhuma aceitação/checkpoint Git presumida. + +Build privado e verificações de sintaxe aprovados; assets anteriores retidos. Provas válidas dos restantes módulos reutilizadas, sem repetir comandos de Compras, documentos ou associações. Os seis registos protegidos e a cache bloqueada não foram alvo. + +## Limites + +Um livro/uma origem fictícia1000, despesa50, MZN e período QA seleccionados, com duas contas artificiais novas. Não é operação geral de Caixa de uma empresa. Sem bancos/POS/câmbio/transferências externas, fiscalidade certificada, salários, dinheiro/acessos reais, ajuste automático ou reabertura. Sem publicação, candidata de Compras, SGC, novas demonstrações, aceitação ou capacidade de implantação presumida. + +Revisão visual continua separada e a via de navegador bloqueada não foi repetida. Carga sustentada, armazenamento/retensão, conformidade real e implantação exigem evidência própria. diff --git a/docs/SGF_CASH_NEXT_SCOPE.md b/docs/SGF_CASH_NEXT_SCOPE.md new file mode 100644 index 0000000..24847a5 --- /dev/null +++ b/docs/SGF_CASH_NEXT_SCOPE.md @@ -0,0 +1,39 @@ +# Próxima lacuna operacional: Caixa QA + +Estado10/11/2026: **autorizado, implementado e verificado na8085 — ver SGF_CASH_DELIVERY.md. O diagnóstico seguinte conserva o contexto anterior à aprovação**. A directiva executiva autoriza continuar funcionalidades; a regra de apresentar novos direitos antes de conceder acessos continua aplicável. A matriz anterior dos documentos não inclui poderes de Caixa. Não usar `service_receivables.view`, a emissão de recibos ou a aprovação de uma factura como autorização para despesas ou fecho de Caixa. + +O menu de Oficinas → Financeiro declara correctamente Caixa, despesas, abertura e fecho ainda ausentes. A pesquisa em `app`, `packages` e serviços nativos de Compras não encontrou motor de Caixa reutilizável. A consulta SELECT-only nas duas bases QA existentes encontrou **zero concessões** dos quatro direitos propostos. Não foram criados schema, flag, motor, sessão ou movimento de Caixa. + +## Recorte funcional agrupado + +No Financeiro da **mesma8085**, e regressão existente, um livro de Caixa QA em MZN, com as seguintes funções completas, sem mais demonstrações: + +1. Abertura deliberada com saldo inicial explícito0, preparação e revisão distintas, responsáveis e contexto identificados. +2. Conferência individual de **um recebimento sintético1000 já efectivamente confirmado** na nova factura-recibo. Escolha e declaração explícitas do meio Caixa com comprovativo; o documento anterior não prova por si entrega em numerário. Fonte exclusiva e recuperação por chave. Não emitir outra factura, outro recibo, dívida ou entrada financeira duplicada. Os documentos e a origem anteriores mantêm-se imutáveis. +3. Uma despesa fictícia nova50, com beneficiário, descrição, data e comprovativo QA, preparação e revisão distintas, limite pelo saldo disponível. Sem pagamento real, retenção/imposto presumido ou obrigação a fornecedor criada automaticamente. +4. Contagem950 fundamentada, revisão distinta e fecho só com versão/contexto/origens actuais, movimentos confirmados e valorizados. Divergência não gera ajuste automático. Uma alteração/reversão da origem impede declarar a caixa reconciliada sem nova conferência; não desencadeia reembolso. +5. Histórico e extracto por período, filtros e páginas25/100, indicação de preparador/revisor e estados anteriores/posteriores. Consulta genérica não adquire poderes de movimentação. Não há reabertura de Caixa fechada neste recorte. +6. Duas origens explícitas no mesmo diário: reclassificação1000 D QA-CAIXA/C QA-LIQUIDACAO-CLIENTE, e despesa50 D QA-DESPESA-CAIXA/C QA-CAIXA. A classificação não reconhece dinheiro pela segunda vez. Contabilidade prepara/revê/aplica pelos direitos e writer existentes; fecho de Caixa e conferência do período não ignoram origens ou lançamentos pendentes. Contas/regras apenas nas fixtures QA existentes, sem implantação empresarial. + +O exemplo terminado conserva950 na Caixa QA. Os25 lançamentos anteriores e todos os exemplos aceites ficam intactos; só serão acrescentados os dois lançamentos escolhidos e conferidos. Não inferir que QA-LIQUIDACAO-CLIENTE é uma conta bancária real. + +## Direitos exactos a autorizar + +Quatro nomes novos, **oito concessões por base QA existente**, apenas empresa2/unidade2: + +|Direito|Perfis QA existentes|Âmbito| +|---|---|---| +|`cash.view`|Financeiro preparação13/revisão14; Contabilidade preparação15/revisão16; consulta17|Consulta e rastreabilidade| +|`cash.prepare`|Financeiro preparação13|Abertura, classificação, despesa e contagem propostas| +|`cash.review`|Financeiro revisão14|Revisão distinta das propostas| +|`cash.close`|Financeiro revisão14|Fecho após contagem preparada por outra pessoa e conferências válidas| + +[Matriz exacta](integration/sgf-cash-access-proposal.json). Não são novos utilizadores/perfis, funções nativas, roles PostgreSQL ou direitos reais/PUBLIC. Não se alteram os direitos de administração ou emissão fiscal. As consultas documentais/financeiras já aprovadas são reutilizadas; não há nova concessão implícita de escrita noutro módulo. + +## Provas e limites da eventual autorização + +Após aprovação: implementar o bloco inteiro, provar primeiro na regressão existente e disponibilizar limitadamente na8085 depois dos testes afectados, sem pedir autorização por campo/teste do mesmo recorte. Ensaiar acesso indevido, isolamento por empresa/unidade/módulo, revisão própria, versão antiga, limites de saldo, fonte já classificada/revertida, duas confirmações concorrentes, repetição sem duplicação e falha/recuperação. Confirmar fecho recusado com divergências/origens pendentes e preservação dos25 lançamentos anteriores. Não repetir suites alheias válidas. + +Sem dinheiro, operações, contas ou acessos reais; sem banco, POS, AT, emissão fiscal, salários, reabertura ou ajuste automático. Sem novas bases/portas/demonstrações, SGC, publicação, candidata de Compras, checkpoint ou aceitação presumidos. Os seis registos e a cache bloqueada continuam fora dos alvos. Visual, carga sustentada, conformidade real e implantação permanecem separados. + +Provas privadas do diagnóstico: regressão `single-integration/20261110-174122-692e202d/cash-rights-readonly.json`;8085 `single-integration/20261110-174122-55d06675/cash-rights-readonly.json`. Ambos SELECT-only, zero concessões/operações. diff --git a/docs/SGF_CLIENT_ASSET_DELIVERY.md b/docs/SGF_CLIENT_ASSET_DELIVERY.md new file mode 100644 index 0000000..c4fd020 --- /dev/null +++ b/docs/SGF_CLIENT_ASSET_DELIVERY.md @@ -0,0 +1,9 @@ +# Entrega dos módulos JavaScript na8085 + +10/11/2026. Durante o cadastro de artigos, o checksum HTTP detectou que o router local devolvia o HTML da aplicação, não o módulo pedido, para vários endereços novos `.mjs`. Os scripts existiam em disco e as APIs respondiam; faltava a inclusão no encaminhamento de ficheiros públicos. Ecrãs podiam ficar sem carregar dados ou sem executar acções do cliente. Provas de API/DOM anteriores não demonstravam essa entrega. + +Correcção em `scripts/receipt-composition-http.php` e `scripts/sgf-selected-assets.php`: somente a composição `cc50a276c10c`, modo de instalação única, pode servir dez nomes públicos exactos adicionais. Exige ficheiro real dentro do directório público, sem symlink. As regras originais de assets e o corpo do router foram preservados. Fora da composição seleccionada, a nova função não muda a decisão de encaminhamento. `.mjs` desconhecido na8085 devolve404, sem HTML da aplicação nem leitura de ficheiro privado. + +Conferidos com MIME JavaScript e SHA dos bytes instalados: entrada, Stock final/conclusão/catálogo, documentos, Caixa, IVA, regras, extractos e indicadores. Inclui `sgf-stock-final.mjs`, dependência dos controlos de confirmação. Treze verificações HTTP e17 de limites/comparação do router, sem operação de negócio. Provas privadas: `assets-http-20261110.json` e `asset-routing-boundaries-20261110.json`, indexadas em [cadastro de artigos](proofs/sgf-stock-catalogue-20261110.json). + +Não foi usado o navegador bloqueado, reiniciado o serviço, criada demonstração ou limpa cache. O router activo lê o código actualizado por pedido. Não se afirma que estes checks substituem a conferência visual, testam todos os handlers no navegador, validam carga ou conferem capacidade de implantação. Os percursos e efeitos financeiros anteriormente comprovados não foram repetidos. O arranque e empacotamento para outro servidor continuam a exigir evidência própria. diff --git a/docs/SGF_CURRENT_DELIVERY_20261110.md b/docs/SGF_CURRENT_DELIVERY_20261110.md new file mode 100644 index 0000000..8996522 --- /dev/null +++ b/docs/SGF_CURRENT_DELIVERY_20261110.md @@ -0,0 +1,86 @@ +# SGF — estado consolidado da instalação8085 + +Estado actual: [matriz v25](integration/sgf-solution-contract.json), [última entrega de RH autónomo](SGF_HUMAN_RESOURCES_DELIVERY.md) e [conferência estrutural sem escrita](proofs/sgf-solution-structure-v25-final-20261111.json). As secções seguintes conservam a evolução histórica; propostas marcadas por autorizar são substituídas pelas respectivas entregas posteriores. + +Actualização11/11/2026. Código funcional, interfaces e ligações na instalação existente, sem dependência do SGC, novas demonstrações, publicação ou checkpoint. Esta actualização não constitui aceitação do utilizador nem validação de implantação. + +RH autónomo: [entregue com perfis próprios e percurso interno QA](SGF_HUMAN_RESOURCES_DELIVERY.md). São agora sete módulos na8085. Não integra o menu Financeiro nem conclui todas as funções laborais. + +## Incrementos concluídos nesta continuação + +|Incremento|Resultado|Provas dirigidas| +|---|---|---| +|Registo Comercial|Seis famílias autorizadas, filtros e SQL25/100; factura-recibo sem duplicar factura técnica; valores canónicos e saldos da página|33 regressão/16 HTTP; [relatório](SGF_BILLING_REGISTER_DELIVERY.md)| +|Caixa autorizado|Abertura0, custódia explícita do recebimento existente1000, despesa50, contagem/fecho950; preparação/revisão distintas; histórico/recuperação|23 regressão, concorrência503→409 sem duplicação, sete fronteiras,21 HTTP,14 DOM→HTTP de consulta, oito período; [relatório](SGF_CASH_DELIVERY.md)| +|Stock sem movimento|Corte UTC/dias, pares com história conhecida, pesquisa literal/armazém e páginas25/100; Existências abre a consulta já ligada|23 regressão,15 HTTP, sete cliente e comparação física; [relatório](SGF_STOCK_INACTIVITY_DELIVERY.md)| + +O diário tem27 lançamentos equilibrados: os25 anteriores a Caixa permanecem idênticos; as duas origens novas são1000 de reclassificação da custódia e50 de despesa sintética, sem criar outro recebimento/recebível. Caixa QA fechada950. Conferência contabilística38 factos/zero pendentes; fecho testado em rollback, período da instalação conservado aberto. + +As provas válidas dos percursos Compras, Comercial, Stock, Oficina, Financeiro e Contabilidade foram reutilizadas. As provas financeiras antigas mantêm os seus números históricos, não são reescritas ou contadas outra vez. Nenhuma emissão/recepção/pagamento/associação antiga foi repetida para esta consolidação. + +## Matriz e limites da conclusão + +[Matriz v6](integration/sgf-solution-contract.json): seis módulos, todas as estruturas host/nativas mínimas discriminadas presentes e fontes/provas localizadas. [Conferência estrutural SELECT-only](proofs/sgf-solution-structure-v6-20261110.json) não executa workflows nem substitui as provas funcionais citadas. A ferramenta passou a considerar a declaração real de `sd_cancellations` no seu script QA existente; não criou tabela nem relaxou um requisito. + +Este recorte funcional QA não é a operação geral real de uma empresa. A falta de aceitação visual ou de prova operacional não deve ser disfarçada com outro módulo, demonstração ou repetição dos mesmos percursos. A matriz mantém `client_presentable=false` enquanto os critérios de apresentação/aceitação não estiverem comprovados. + +Pendências concretas: + +- Conferência visual dos restantes ecrãs e das adições recentes, sem repetir a via de navegador bloqueada. A confirmação anterior do utilizador refere-se à abertura/legibilidade do Stock então apresentado. +- Carga sustentada, consumo/quotas/retenção de anexos, reconciliação geral de órfãos e capacidade de implantação; paginação/concorrência QA não bastam. +- Arranque completo do computador/cluster partilhado ainda sem prova própria. Arranque/reinício8085 já ensaiados não foram repetidos desnecessariamente. +- Conformidade fiscal/contabilística real, bancos/dinheiro real e concessões reais estão fora destas autorizações sintéticas. Não são declarados implementados ou autorizados por esta entrega. + +[Guião actualizado](SGF_GUIAO_APRESENTACAO.md), entrada `http://127.0.0.1:8085/sgf`. Preservados os seis registos, cache bloqueada, exemplos anteriores e alterações alheias. Backups, dados e credenciais privados fora do repositório; sem publicação, candidata de Compras ou aceitação/checkpoint presumidos. + +## Conferência posterior do requisito fiscal/financeiro amplo + +A matriz QA anterior não representa todo o produto: continuam por implementar livros/apuramentos fiscais gerais, IRPS/INSS, IRPC, ISPC/ICE, modelos oficiais, calendário legal, reconciliação bancária e rácios/tendências conciliados. [Conferência e código de cobertura entregues](SGF_FISCAL_COVERAGE_DELIVERY.md); matriz v7 mantém estas lacunas separadas dos seis percursos QA e não declara produto completo. Livro/mapa interno IVA QA está concretamente proposto, com seis direitos/14 concessões por base QA, ainda por autorizar; não foi aplicado. + +## Livro e mapa interno IVA QA autorizado e entregue + +[Entrega funcional](SGF_IVA_ASSESSMENT_DELIVERY.md): mesma8085 `/fiscal/iva`, FR1000/base862,07/IVA137,93 já existente → livro preparado/revisto → mapa interno preparado/revisto e CSV congelado. Quatro versões fiscais novas,27 lançamentos e dados anteriores intactos. Seis direitos/14 concessões por base QA expressamente aprovados/aplicados. Dedução/imposto a pagar indeterminados; não é apuramento integral ou modelo oficial. Esta actualização substitui apenas a pendência anterior deste bloco; os restantes motores fiscais/modelos/reconciliação/rácios continuam pendentes na matriz v8. + +Próximo bloco agrupado preparado: `SGF_FISCAL_RULES_COMMON_SCOPE.md`, três direitos próprios/sete concessões por base QA existente (fiscal_rules.view/prepare/review), ainda por autorizar e sem aplicação. Separa manutenção transversal de regras dos direitos já concedidos para serviço/livro/mapa. Não pressupõe novos motores fiscais completos nem altera documentos existentes. + +## Núcleo comum de regras QA entregue + +[Entrega funcional](SGF_FISCAL_RULES_COMMON_DELIVERY.md), `/fiscal/rules`: catálogo pesquisável/paginado, fontes/SHA, revisão distinta, versões/vigência/conflitos/retirada, cálculos160/1160 e170/1170 guardados.102 verificações dirigidas mais concorrência; apenas sete concessões aprovadas por QA.17 conjuntos originais e binding intactos; diário27/Caixa950/quatro versões IVA conservados. Esta actualização substitui a pendência anterior desse núcleo. São casos artificiais, não legislação ou motores legais completos; restante backlog mantém-se na matriz v9. + +Proposta seguinte, ainda por autorizar: `SGF_BANK_RECONCILIATION_SCOPE.md`, importação/revisão/conferência manual de extracto artificial0−20+20=0 contra duas linhas contabilísticas já existentes, sem novos efeitos financeiros. Quatro direitos/oito concessões por base QA apresentados e não aplicados. Não se confunde com reconciliação de recebimento já entregue nem atribui natureza bancária real à conta de liquidação. + +## Conferência de extracto QA entregue + +[Entrega funcional](SGF_BANK_RECONCILIATION_DELIVERY.md), `/finance/bank`: CSV artificial0−20+20=0, revisão distinta, duas correspondências individuais com linhas contabilísticas existentes4/5, sete eventos e relatório interno.94 verificações mais concorrência; HTTP8085 prova fonte nativa, origem artificial apenas no harness da regressão.20 conjuntos originais intactos, sem novos lançamentos/pagamentos; diário27/Caixa950/IVA4/regras4+cálculos2 conservados. Oito concessões por base QA aprovadas/aplicadas. Substitui a pendência anterior deste bloco; reconciliação bancária geral/real continua separada na matriz v10. + +Próximo bloco agrupado preparado: `SGF_FINANCIAL_INDICATORS_SCOPE.md`, critérios/indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta e retrato conciliado. Três direitos/sete concessões por QA apresentados, ainda não aplicados. Não é rácio geral de liquidez/margem, critério legal ou recomendação de pagamento. + +## Critérios e indicadores QA entregues + +[Entrega funcional](SGF_FINANCIAL_INDICATORS_DELIVERY.md), `/finance/indicators`: critérios13/revisão14, quatro eventos e dois resultados com fontes efectivas950/100=9,50vezes e50/1000=5,00%. Exactos19/2 e1/20, histórico/retratos/relatório, sem tendência zero inventada ou critério real.98 verificações mais concorrência;23 conjuntos anteriores intactos e sete concessões aprovadas por QA. Diário27/Caixa950/IVA4/regras4+cálculos2/bancos7 conservados. Substitui a pendência anterior deste bloco; rácios gerais/critérios reais/séries continuam na matriz v11. + +Decisão seguinte preparada, sem concessões novas: `SGF_MULTI_LINE_INCLUDED_SCOPE.md`, modalidade QA1–25linhas/preço incluído, com política explícita da base/imposto documentais e diferenças preservadas. Ainda não implementada no endpoint nem aplicada a documentos; não inferir a política do caso de base simples. + +## Cálculo QA por linhas e preço incluído entregue + +[Entrega funcional](SGF_MULTI_LINE_INCLUDED_DELIVERY.md), `/fiscal/rules`: modalidade/regra explícitas,1–25linhas, exactos e ambas as diferenças preservados, mapa interno/checksum/recuperação. Três cálculos conservados:400+600 sem imposto e464+696 incluídos produzem1000+160=1160;100 incluído produz86,21+13,79, diferenças9/2900.80 verificações mais concorrência mesma chave503→200 replay sem duplicação.26 conjuntos originais/binding intactos, zero concessões novas, dois cálculos anteriores conservados (agora cinco). Diário27/Caixa950/IVA4/regras4/bancos7/indicadores4+2 intactos. Substitui a pendência anterior desta política/modalidade; não emite documentos nem conclui fiscalidade real, revisão visual, carga ou implantação. [Matriz actual v12](integration/sgf-solution-contract.json) mantém produto amplo incompleto. + +## Início operacional e ordens paginadas entregues + +[Entrega funcional](SGF_OPERATIONAL_INFORMATION_DELIVERY.md): entrada8085 informa o âmbito dos seis módulos; Início mostra percurso, evidências, funções autorizadas e consulta das fontes existentes. Oficinas tem lista SQL25/100 por cliente/equipamento/responsável/estado e minutos apontados/verificados, sem adquirir o mutex da empresa. Regressão manteve o bloqueio de escrita e a leitura terminou0,093s antes da libertação.32 conjuntos originais intactos e zero novas operações financeiras, concessões ou ordens na8085. Detalhe antigo da ordem, fiscalidade geral, visual/carga e capacidade de implantação mantêm limites expressos; matriz v13 não declara produto completo ou aceitação. + +## Contabilidade: utilização dos relatórios por período + +[Entrega funcional](SGF_ACCOUNTING_REPORTS_DELIVERY.md): submenus distintos, Análise com datas explícitas/balancete/razão por conta/SQL25/100, CSV da página e mudança de marcador conferida. Corrigido500 de conta indisponível para403 e argumentos inválidos para422. Cinco regressão/19 HTTP/14 UI;32 conjuntos originais intactos. A8085 recebeu o bloco após código e provas, sem alterar exemplos financeiros; não condiciona cada passo de desenvolvimento. Engine e mutex existentes preservados; visual/carga/fiscalidade ampla continuam pendentes na matriz v14. + +## Posições de clientes e vencimentos + +[Entrega](SGF_RECEIVABLE_POSITIONS_DELIVERY.md): SQL25/100, pesquisa/estados/vencimentos, saldo actual confirmado, totais sem propostas e referência apenas para vencimento; CSV da página, selecção explícita no detalhe.30 regressão/16 UI/15 HTTP. Zero operações financeiras novas na8085; dados e binding preservados. Próxima lacuna discriminada: [cadastro/revisão de artigos](SGF_STOCK_CATALOGUE_SCOPE.md), dois direitos ausentes/duas concessões por QA existente, ainda não autorizados/aplicados. Sem inferir esse poder de stock.view ou confirmação de movimentos/lotes. + +## Cadastro de artigos e entrega correcta dos scripts + +[Cadastro entregue](SGF_STOCK_CATALOGUE_DELIVERY.md) após aprovação: preparação2/revisão12, um artigo sem saldo inicial na8085, código/unidade imutáveis, histórico/recibos e correcção de designação só dos novos.85 verificações, mais concorrência e dois checksums;38 conjuntos/três bindings intactos, quatro concessões exactas nas duas QA. Substitui a pendência anterior de direitos/cadastro deste recorte. + +A conferência detectou scripts `.mjs` a receber HTML200. [Correcção transversal](SGF_CLIENT_ASSET_DELIVERY.md), apenas8085: dez módulos exactos e dependência comum servidos como JavaScript, MIME/SHA conferidos; desconhecido404.17 fronteiras e13 HTTP. Preserva corpo original do router/outras composições, sem reinício ou cache. As provas antigas de API/DOM não demonstravam essa entrega e não são apresentadas como validação visual. Restantes lacunas/visual/carga/implantação permanecem na matriz v22. + +Continuação11/11: [contratos, assiduidade e férias — âmbito integrado preparado](SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md). Nove direitos novos,15 concessões por livro existente aos mesmos perfis RH; por autorizar/não aplicados. Apenas cálculo puro de tempos/saldo preparado e22 verificações; funções novas ainda não entregues na8085. diff --git a/docs/SGF_DOCUMENT_READ_ACCESS.md b/docs/SGF_DOCUMENT_READ_ACCESS.md new file mode 100644 index 0000000..70e0dce --- /dev/null +++ b/docs/SGF_DOCUMENT_READ_ACCESS.md @@ -0,0 +1,16 @@ +# Consulta necessária para terminar a factura-recibo QA + +10/11/2026. **Decisão recebida e aplicada: «Autorizar as10 concessões de consulta QA». Bloqueio ultrapassado; estado final em SGF_SERVICE_DOCUMENTS_DELIVERY.md. O diagnóstico seguinte é histórico.** A autorização do bloco anterior permitiu15 novos direitos, as funções nativas discriminadas e `service_invoices.issue` para o revisor Financeiro. Foi aplicada exactamente essa lista nas duas bases QA existentes. Não incluía as consultas documentais exigidas pelo motor original; os acessos actuais recusam a captura da origem com403. + +A verificação SELECT-only na8085 encontrou **10 concessões em falta, seis nomes já existentes**, nos perfis Financeiro preparação13 e revisão14. A [matriz exacta](integration/sgf-document-read-access-proposal.json) apresenta destinatários e âmbito. Consulta de emitente, cliente e enquadramento fiscal na empresa QA2; preparação documental na unidade QA2 para ambos. Consulta de rascunhos de serviço/facturação na mesma unidade apenas para revisão14. Não se concedem edição de perfis fiscais, cálculo, aprovação fiscal, alteração de séries ou acessos reais. + +A implementação candidata preserva as guardas de consulta e emissão existentes. Não usa identidade de outro utilizador nem transforma `service_documents.view` em direito fiscal. A finalização preparada consome a intenção financeira anterior, conserva o seu autor como declarante e o executor real nos efeitos; esta parte **ainda não foi validada**, fica desligada e não foi copiada para8085. Aprovar os acessos permitirá concluir os testes desse ramo antes de o disponibilizar. + +## Trabalho concreto já realizado + +- Duas origens novas preparadas exclusivamente na regressão existente: base862,07 + imposto documental de teste137,93 =1000. Séries QA novas, aprovação distinta, zero facturas emitidas pela preparação e zero alterações de direitos. As origens anteriores estão preservadas. +- Motor candidato versionado/auditado de Recibo, Nota de crédito e factura-recibo; PDF guardado com SHA, confirmação individual, versão/contexto/origem e idempotência. Configuração desligada por omissão. Ainda sem ecrã/instalação documental na8085. +- 16 verificações em rollback passaram para o recorte já executável: Recibo vinculado à afectação efectiva400, revisão distinta, uma saída/PDF por chave, integridade do PDF, crédito100 pelo writer original deixando saldo500 sobre a nova factura1000, recusa403 da factura-recibo sem os direitos, ausência de efeitos parciais e preservação integral das facturas/direitos anteriores. Prova privada `receipt-composition-b4093c6f6707/single-integration/20261110-163526-13905e7f/service-documents-result.json`. +- O teste positivo da factura-recibo, concorrência documental, reversões técnicas, ligação do crédito ao diário, UI e HTTP dos documentos **não estão concluídos**. A recusa correcta não é prova positiva. Não se declara o bloco ou a aplicação terminados. + +Stock/políticas/devolução ao fornecedor e orçamentos Comercial/Oficina estão implementados na8085. O diário actual tem20 lançamentos equilibrados; os18 anteriores foram comparados e estão intactos. Mantêm-se os seis registos protegidos, a cache, revisão visual, carga sustentada e capacidade de implantação como âmbitos separados. Sem SGC, novas demonstrações, publicação ou activação de candidata de Compras. diff --git a/docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md b/docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md new file mode 100644 index 0000000..ca9d297 --- /dev/null +++ b/docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md @@ -0,0 +1,51 @@ +# Critérios e indicadores financeiros QA — entrega na8085 + +10/11/2026, `SGF-FINANCIAL-INDICATORS-QA-v1`, conforme [âmbito autorizado](SGF_FINANCIAL_INDICATORS_SCOPE.md). Mesma8085, entrada **Financeiro → Critérios e indicadores QA → Abrir critérios, indicadores e histórico**, ou `/finance/indicators` com função autorizada. Sem SGC, nova demonstração ou efeito financeiro. + +## Percurso conservado e resultados + +Financeiro13 preparou cada um dos dois critérios, com fontes/direcção/âmbito/evidência explícitos. Financeiro14 reviu a versão exacta. Preparador13 escolheu cada critério revisto e confirmou o cálculo para o retrato10/11/2026. **Quatro eventos de critério e dois resultados** conservados, com fotografias/fontes/histórico e relatório interno CSV verificável. + +|Indicador limitado do exercício|Fontes efectivas|Resultado| +|---|---|---| +|Cobertura QA de uma obrigação por Caixa|Caixa fechada/revista950 MZN, conciliada com saldo devedor QA-CAIXA; saldo **credor** QA-FORNECEDOR100 MZN no mesmo diário|19/2 =9,50 vezes| +|Despesa/entradas da Caixa QA|Despesa revista50 MZN e custódia/entradas1000 MZN do mesmo livro fechado|1/20 =5,00%| + +Não são rácio geral de liquidez corrente, margem de resultados, solvabilidade, critério legal da empresa ou recomendação de pagamento. Direcção credora do denominador explícita, sem valor absoluto ou classificação pelo nome. Não se aplica a outras contas/empresas. Série anterior e tendência não disponíveis ficam indeterminadas; não se apresentam como variação zero. + +## Fontes, precisão e manutenção + +O leitor real reutiliza a consulta/autoridade de Caixa, o vínculo físico e as leituras nativas do diário. Confere Caixa fechada e respectivas origens, período QA aberto e igualdade do saldo de Caixa com a conta QA-CAIXA. Lê apenas as duas contas/saldos seleccionados, com âmbito empresa/unidade e corte10/11; não concede consulta global nova à contabilidade. Sem novo PGrole ou comando nativo de escrita. + +Valores são strings de unidades menores, com aritmética racional exacta e representação explícita de duas casas (vezes ou percentagem), preservando diferença racional. Moeda/dimensão/data compatíveis. Fonte ausente/não conciliada, critério não revisto/versão antiga, fonte alterada, numerador/denominador desconhecidos, denominador zero/não credor ou sinal incompatível produzem pendência; montante desconhecido não vira0/1 ou infinito. + +Nova versão ou mudança de fonte exige nova conferência para cálculo actual. A fotografia antiga não é reescrita; consulta mostra resultado histórico e avaliação actual separados. Relatório deriva do resultado/fonte preservados e identifica o estado actual, sem substituir o passado. CSV com valores em MZN, fracção exacta, representação, versão e limites; checksum no download. Não se declarou arquivo binário/renderer para retenção legal. + +## Autoridade e recuperação + +`financial_analysis.view/prepare/review`: **sete concessões por cada base QA existente**,14 no conjunto, [exactamente como aprovado](integration/sgf-financial-indicators-access-proposal.json). Preparação de critério/cálculo13, revisão14, consultas13–17. Reutiliza direitos de Caixa já existentes; não atribui poderes por herança. Sem novo utilizador/PGrole/direito PUBLIC/acesso real. Rootflag desligada; somente8085 seleccionada disponibilizada. + +`an_criteria` conserva versão, predecessor, definição, fonte/SHA, preparador/revisor/data/fundamento/estado. `an_results` conserva entrada, fonte completa delimitada, critério, exacto/representação/diferença/resultado. `an_commands` conserva chave própria/hash/resultado/impacto anterior e posterior. Triggers exigem direitos, pertença, revisão distinta, versão actual e história append-only, com guarda física QA e PUBLIC revogado. + +Prévia cifrada600s em savepoint revertido; confirmação revalida contexto/autor/direitos do executor e preparador, versões, fontes e impacto sob o bloqueio de empresa comum. Repetição recupera resultado histórico, sem executar; mesma chave com outra entrada recusada. Recibo próprio recuperável noutra sessão por GET. Não se escolhe o primeiro critério ou um substituto para contornar falta de fonte. Interface protege campos/consulta durante operação e conserva formulário não submetido ao paginar. + +## Provas e preservação + +|Conjunto|Resultado| +|---|---| +|41 regressão, rollback integral|Critérios/fontes/direcção, valores injectados, revisão distinta, critérios não revistos/antigos, exacto19/2 e1/20, relatório/checksum, zero/ausência/sinal/moeda/dimensão/data, nova fonte sem alterar passado, autor revogado, falha/reversão atómica/recuperação, acessos e dados antigos intactos.| +|Concorrência na regressão|Segunda revisão503/Retry-After5 durante lock da empresa; libertação→409 por versão antiga. Uma revisão e primeira chave recuperada sem duplicação. Dois eventos conservados só na regressão.| +|12 SQL/fronteiras, rollback|127 versões de metadados em25 e100/27, pesquisa literal, histórico por indicador, contexto/expiração/módulo/unidade, histórico imutável e writer indevido. Não são127 critérios operacionais aprovados ou novos saldos.| +|29 HTTP na8085|Fonte composta efectiva950/100/50/1000, seis confirmações/repetições, revisão distinta, resultados exactos/retratos, relatório/checksum, período anterior ausente, critérios/filtros/histórico, actor/módulo/tenant/IDs/recibo noutra sessão e ecrã. Progresso guardado.| +|16 DOM→HTTP|Escolha explícita, fontes visíveis, prévia real revertida,5%/1/20, tendência indeterminada, bloqueio/cancelamento, formulário preservado ao paginar, histórico/relatório/recibo e integração com conferência bancária ainda finalizada. **Zero confirmações de negócio**, resultados mantidos em dois. Não é navegador.| +|Preservação SELECT-only|23 conjuntos originais completos e binding idênticos, incluindo diário27, Caixa950, IVA4, regras4/cálculos2, bancos7, documentos, recebíveis, fiscalidade, clientes, Stock e períodos. Apenas sete concessões aprovadas adicionadas.| + +Total98 verificações dirigidas, concorrência discriminada à parte. [Índice verificável](proofs/sgf-financial-indicators-20261110.json). Provas dos restantes módulos reutilizadas, sem repetir movimentos/emissões/pagamentos/lançamentos que originaram os valores. + +**Origem das provas:** na regressão, fornecedor artificial injectado só no harness CLI, identificado como `analysis_harness_fixture`, sem criar Caixa ou lançamentos. Não existe fallback no leitor instalado. O percurso HTTP8085 comprovou `composed_qa_sources`, Caixa e diário efectivos e retratos coincidentes. Não apresentar a simulação como prova de integração nativa integral da regressão. + +## Limitações + +Dois critérios de apresentação QA, duas projecções e uma data, com duas representações. Não conclui rácios gerais de liquidez/rentabilidade/endividamento, séries/tendências, demonstrações legais, critérios reais, previsões ou aconselhamento financeiro. Não importa valores de conversas nem atribui critérios automaticamente. + +Sem novos pagamentos, movimentos, impostos, documentos, lançamentos, direitos/operações reais, reabertura, publicação/candidata/checkpoint/aceitação ou capacidade de implantação. Seis registos/cache/trabalho alheio intactos; backups/credenciais/dados privados fora do repositório. Visual bloqueada não foi repetida; carga sustentada, retenção/órfãos, arranque completo, conformidade e implantação mantêm evidências próprias pendentes. diff --git a/docs/SGF_FINANCIAL_INDICATORS_SCOPE.md b/docs/SGF_FINANCIAL_INDICATORS_SCOPE.md new file mode 100644 index 0000000..ce6228f --- /dev/null +++ b/docs/SGF_FINANCIAL_INDICATORS_SCOPE.md @@ -0,0 +1,38 @@ +# Proposta agrupada — critérios e indicadores financeiros QA + +`SGF-FINANCIAL-INDICATORS-QA-v1`,10/11/2026. **Autorizado expressamente e entregue**; ver `SGF_FINANCIAL_INDICATORS_DELIVERY.md`. O texto abaixo conserva o âmbito aprovado. É uma funcionalidade de Financeiro na mesma8085, não um sétimo módulo ou uma nova demonstração. Fecha o primeiro percurso de critérios revistos → fontes conciliadas → indicador explicável → histórico/relatório interno. + +## Dois indicadores com dados já existentes + +1. **Cobertura QA de uma obrigação por Caixa**: numerador950 MZN da Caixa QA fechada/revista; denominador100 MZN do saldo **credor** da conta artificial `QA-FORNECEDOR` (ID2 na8085), lido no mesmo diário. Resultado exacto19/2 =9,5 vezes. A direcção credora é parte explícita do critério; não se aplica valor absoluto ou se classifica automaticamente a conta. Não é rácio geral de liquidez corrente, solvabilidade ou recomendação de pagamento. +2. **Despesa/entradas da Caixa QA**:50/1000 =1/20, ou5%. Usa exclusivamente os dois movimentos de custódia/despesa já preparados/revistos e o livro fechado, não a totalidade de proveitos/despesas da empresa ou uma margem contabilística. Não cria movimentos, facturas ou efeitos fiscais. + +Fontes observadas em10/11/2026: saldo devedor QA-CAIXA950, saldo credor QA-FORNECEDOR100; Caixa950/entradas1000/despesa50 já comprovadas. A classificação e a fórmula são hipóteses expressas de apresentação QA, não critérios contabilísticos/financeiros legais aprovados para a empresa real. + +## Percurso completo proposto + +Financeiro13 escolhe explicitamente o indicador/fontes/direcção/âmbito do critério e prepara uma versão com fundamento. Financeiro14 confere a mesma versão; aprovação não atribui critérios a outras contas ou empresas. Preparador13 escolhe a versão revista e solicita cálculo pelo retrato actual das fontes fixadas. Prévia mostra numerador/denominador/unidades, fórmula, exacto/representação/diferença, limites e impacto; confirmação grava fotografia/histórico/recibo recuperável. Consultas e relatório interno apenas dos dois indicadores, sem concluir demonstrações financeiras gerais. + +Valores em strings decimais/racionais; moeda e dimensões compatíveis. Fonte ausente/não conciliada, revisão antiga, data/âmbito incompatíveis ou denominador zero ficam pendentes, sem substituir por0/1 ou mostrar infinito. Comparativo anterior/tendência não demonstrado permanece indisponível, não variação zero. Sem cruzar moedas ou assumir valorização geral/adequação de conta pela sua designação. + +Nova versão/critério/fonte não reescreve cálculo anterior: mostra retrato e actualidade separados. Direitos/contexto/versão/dependências revalidados na confirmação; preparação/revisão distintas, histórico append-only, repetição sem duplicação e recuperação após falha. Não se escolhe automaticamente o primeiro critério ou outro indicador para contornar falta de fonte. + +## Direitos novos a decidir + +Três nomes e **sete concessões por cada base QA existente**,14 no conjunto. Tenant1/empresa2/unidade2; somente perfis existentes: + +|Direito|Destinatários| +|---|---| +|`financial_analysis.view`|Financeiro13/14, Contabilidade15/16 e consulta17| +|`financial_analysis.prepare`|Financeiro preparação13 — critério/cálculo QA| +|`financial_analysis.review`|Financeiro revisão14 — revisão exacta de critério| + +[Matriz integral](integration/sgf-financial-indicators-access-proposal.json). Consulta inclui as projecções limitadas da Caixa e do saldo credor QA seleccionados, não toda a contabilidade ou dados de outra empresa. Reutiliza os controlos/leitores originais e direitos de Caixa já existentes. Os acessos actuais de consulta não autorizam configurar/publicar critérios de análise; não se herdam estes poderes de regras fiscais, bancos ou contabilidade geral. Sem novo utilizador, PGrole, direito PUBLIC ou acesso real. + +## Provas, instalação e exclusões + +Primeiro regressão existente: source provider artificial injectado apenas no harness quando faltam fontes completas (sem criar caixa/pagamentos/lançamentos para preparar testes). Distinguir essa simulação da execução HTTP8085 que comprovará fontes efectivas. Ensaiar moeda/dimensão, sinal explícito, zero/ausência, critérios não revistos, isolamento/actor/módulo/versões, concorrência, repetição, mudança de fonte e falha/recuperação; pesquisa/filtros/páginas25/100 de critérios/histórico. Depois completar o percurso na mesma8085. Não repetir os workflows válidos que originaram os valores. + +A autorização abrange o bloco inteiro, sem aprovação por campo/fórmula/teste deste contrato. Preservar27 lançamentos, Caixa950, IVA4, regras4+cálculos2, extracto/correspondências bancárias sete eventos, seis registos/cache/trabalho alheio. Sem novas bases/portas/demos, SGC/portal, novas transacções/efeitos financeiros, pagamento recomendado ou execução bancária, critérios reais/legais, dados/acessos reais, aplicação retroactiva, reabertura, publicação/candidata, checkpoint/aceitação ou capacidade de implantação. + +Rácios gerais de liquidez/rentabilidade/endividamento, demonstrações legais, séries históricas e critérios reais continuam requisitos próprios, não são declarados completos por estes dois indicadores QA. Visual/carga permanecem separados. diff --git a/docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md b/docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md new file mode 100644 index 0000000..ca80222 --- /dev/null +++ b/docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md @@ -0,0 +1,58 @@ +# Demonstrações internas parciais — entrega8085 + +11/11/2026 · `SGF-INTERNAL-STATEMENTS-QA-v1`. Bloco autorizado concluído na mesma8085, **Contabilidade → Demonstrações internas parciais QA**. Código funcional, interface, correspondências revistas, resultado conservado, histórico, recibos e exportação. Sem SGC, outra demonstração, contas ou lançamentos novos. + +## O que está disponível + +Preparação15/revisão16 de correspondências explícitas entre contas e rubricas internas: activo, passivo, rendimento, gasto ou por classificar. Primeira preparação sem conta/rubrica escolhidas automaticamente; correcção parte da versão revista, com escolhas visíveis, fundamento e nova revisão. O servidor conserva conta/código/designação/moeda e a impressão da identidade observada. Não interpreta prefixos, designações ou sinais como rubrica. Contas sem correspondência ficam pendentes, não desaparecem nem recebem classificação por arrasto. + +Preparação/revisão distintas do resultado interno, com datas e versão de correspondências escolhidas explicitamente. O leitor original `trialBalance` fornece abertura, débitos/créditos do período e fecho; `accountLedger` continua a ser o destino para conferir cada conta/datas. Motor, writer, fórmulas, contas, linhas e períodos originais não foram alterados. O balanço parcial usa fechos; os resultados parciais usam movimento do período. Valores invertidos conservam sinais e aviso, sem valor absoluto para forçar natureza. + +O resultado conserva as linhas, grupos, pressupostos, versão das correspondências, marcador do diário, fonte e SHA. A captura processa páginas25/100 e acumula apenas grupos; não materializa o universo de contas no PHP. As linhas são escritas por página na mesma transacção nativa. O CSV é construído por lotes100 num fluxo com limite de memória1MiB e eventual armazenamento temporário do PHP, encerrado no fim; não usa nem limpa a cache bloqueada. O navegador materializa o ficheiro pedido para a descarga — não se declara capacidade para exportações de volume arbitrário. + +Histórico e resultado paginados25/100. Totais do resultado abrangem todas as páginas conservadas, com a tabela paginada identificada. A revisão de correspondências posterior ou uma alteração da fonte tornam visível que o resultado anterior é histórico; não o recalculam. A exportação de uma revisão antiga continua idêntica. A ligação ao razão transmite conta e datas exactas; o destino é uma nova leitura original, não um arquivo do razão daquela observação. + +## Exemplo conservado, sem preencher lacunas + +Foram preparadas15 e revistas16 as19 escolhas expressamente aprovadas no âmbito, incluindo nove contas por classificar. Depois foi preparado/revisto um resultado interno sobre10/11/2026 e os27 lançamentos já existentes. Ficaram **quatro versões, quatro recibos, uma conservação de resultado e19 linhas**, sem repetir origens financeiras. + +|Rubrica classificada QA|Valor observado MZN| +|---|---:| +|Activos conhecidos|2155,00| +|Passivo de fornecedor conhecido|100,00| +|Rendimentos do período classificados|2586,21| +|Gastos do período classificados|2200,00| +|Subtotal das rubricas de resultados classificadas|386,21| + +**386,21 não é o resultado da empresa.** Nove contas continuam pendentes; o resultado completo e o capital próprio são indeterminados e a equação patrimonial não é completada artificialmente. Rubrica sem conta observada é distinta de saldo zero observado. Os ecrãs separam balanço parcial, resultados parciais e contas por classificar. Não são demonstrações legais, comparativos anuais ou fluxos de caixa. + +## Autoridade, transacções e descarga + +Exactamente dois direitos aprovados: `general_ledger.statements.prepare` para15 e `general_ledger.statements.review` para16, tenant1/empresa2/unidade2. Duas concessões por livro QA existente, quatro no total. Consulta reutiliza `general_ledger.view`. Sem nova pessoa/perfil, role PostgreSQL, concessão SQL/PUBLIC ou acesso real. Configuração principal desligada por omissão;8085 habilitada no seu estado privado existente. + +Prévia por savepoint revertido; confirmação revalida contexto físico, versão, pessoa nativa, identidade host actual sob bloqueio, direitos, preparador distinto e ainda autorizado, conta/correspondências e fonte. Alteração exige nova conferência. Versão, conservação, linhas, auditoria e recibo são atómicos na transacção nativa. Sequências podem avançar numa prévia revertida. Resultado incerto na interface conserva corpo/chave e impede descarte/troca de contexto; repetição recupera o recibo. Consulta histórica de recibo próprio não executa outra decisão nem presume actualidade financeira. + +Na retoma foi corrigido o botão de descarga: a ligação directa à API não enviava `X-SGF-Context` e recebia409. Agora a leitura autenticada envia a geração actual em cabeçalho, recusa resposta de contexto antigo, tipo de ficheiro inesperado, SHA de revisão diferente ou bytes corrompidos. Só depois cria o ficheiro. O token não entra no URL; não se alterou nem excepcionou `RequireModuleContext`. + +O mesmo problema foi corrigido no atalho do mapa IVA em Obrigações fiscais. Reutiliza o mesmo leitor de descarga verificada, com destino fixo correspondente à origem. Não se recriou o mapa nem se repetiram decisões; a descarga do mapa mantém a auditoria de consulta original. CSV/SHAs conservados não foram modificados para corrigir a interface. + +## Provas e seus limites + +-38 verificações de integração na regressão existente: intenção/revisão distinta, fonte de correspondências, versões antigas, acessos/contexto, resultado vazio sem saldos inventados, CSV/SHA, história, exportação antiga conservada e dados anteriores intactos. O livro de regressão tem19 contas mas **zero lançamentos persistentes**; não se criaram lançamentos para preencher esse ensaio. Paridade de valores não vazios foi comprovada na8085 com o motor real. +-17 verificações de fronteiras: sinais invertidos, montantes exactos/moedas separadas, rubrica ausente, fingerprint alterado, identidade/pessoa/direito alterados, falha no recibo e recuperação. A captura25/100 foi exercida com um **double de leitura de128 linhas**, geradas por página, sem contas/lançamentos inseridos. Isso comprova o adaptador de paginação, não SQL, carga ou capacidade de implantação. As provas válidas anteriores do balancete/razão SQL foram reutilizadas. +-Quatro verificações de falha parcial: valores de leitura dos19 códigos QA e um double de relatório na regressão, falha forçada na segunda linha, rollback da primeira, recuperação da mesma chave com19 linhas e FKs válidas, rollback integral. Nenhuma conta/lançamento criado; não é um segundo livro financeiro ou execução do writer original. +-23 verificações de interface/contrato: escolhas explícitas, campos vazios, duas secções parciais/pendências, nenhuma conta/rubrica automática, protecção do formulário, resposta incerta, recuperação idêntica, razão com conta/datas e botão de descarga que só guarda após SHA. API/DOM simulados; não é revisão visual do navegador. As passagens anteriores18/19 foram substituídas, não somadas. +-46 HTTP8085: quatro comandos administrativos efectivos,19 escolhas/nove pendências, resultado versus todas as19 linhas do balancete real,27 lançamentos originais, valores da tabela acima, histórico/recibos, checksum/exportação,25/100 e recusas. Progresso/chaves privados permitem recuperar sem reexecutar os quatro comandos. Esta prova válida anterior à pausa foi reutilizada. +-11 de descarga: exportação efectiva com contexto/SHA, guarda409 sem cabeçalho preservada, contexto antigo antes/depois da resposta, HTML, SHA/bytes corrompidos, acesso e identidade inválidos recusados. Cinco de compatibilidade da descarga IVA: origem exacta, SHA/bytes originais, limite sintético, geração exigida e SHA da prova UI histórica aceite preservado. As22 verificações UI de Obrigações fiscais foram reexecutadas pela alteração de hooks/atalho; a evidência anterior é byte a byte igual e não é somada como nova suite. +-13 finais de entrada/entrega: autenticação401, CSRF419, consulta sem escrita, navegação por configuração/direito, MIME/SHA dos ficheiros principal, ambos os ecrãs e módulo partilhado de descarga; fontes finais instaladas iguais às conferidas. Sem comandos de negócio. +-Concorrência apenas na regressão: a primeira revisão mantém `FOR UPDATE` da empresa; `pg_blocking_pids` identifica o bloqueador, com espera `Lock/transactionid`. Segunda confirmação503 após5,13s; libertada a primeira, a segunda recusa409 e a primeira repete200 com o mesmo recibo. Uma preparação/uma revisão de correspondências conservadas nessa fixture. Não houve nova medição de carga sustentada ou eliminação do mutex existente. + +[Índice das provas e hashes](proofs/sgf-statements-20261111.json). Ensaios anteriores só foram repetidos quando as alterações de identidade/interface/descarga afectaram a fronteira respectiva; não se repetiram suites de Compras, Stock, Caixa, facturação, vencimentos ou apuramentos. + +## Preservação e pendências + +65 verificações de preservação/ACL:49 conjuntos anteriores iguais; nas outras duas tabelas, todas as linhas antigas iguais, apenas duas concessões aprovadas e quatro eventos nativos de auditoria acrescentados. Três bindings intactos,19 contas e27 lançamentos, Caixa950, IVA4, cálculos5, obrigações/condições/recibos, artigos e movimentos conservados. As contas também foram comparadas com a leitura anterior de código/designação/moeda. Uma conservação de resultado,19 linhas e quatro recibos; PUBLIC sem direitos nas novas tabelas/sequências/função. + +Entradas normais de perfis e descargas do mapa original têm a auditoria de consulta própria; não se declara a auditoria global host byte a byte igual. Nenhum comando dirigido aos seis registos ou à cache bloqueada. Backups, dados, CSV, chaves e credenciais permanecem privados, fora do repositório; ficheiros/assets anteriores conservados, sem limpeza de cache ou reinício. + +Sem publicação, candidata de Compras, novas demonstrações/portas/bases, SGC, contas/dinheiro/acessos reais, lançamento, fecho/reabertura, checkpoint ou aceitação presumidos. As correspondências de configuração estão limitadas a100 escolhas por versão; linhas de fonte/captura/exportação usam páginas limitadas. Os leitores compostos conservam guardas e bloqueios anteriores; sem promessa de desempenho global. Revisão visual, carga sustentada e capacidade de implantação continuam pendentes. Classificação legal das contas, capital, demonstrações completas e fiscalidade geral permanecem fora deste bloco; matriz v22 mantém produto amplo incompleto. diff --git a/docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md b/docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md new file mode 100644 index 0000000..d12e583 --- /dev/null +++ b/docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md @@ -0,0 +1,58 @@ +# Decisão agrupada — balanço e resultados internos QA + +10/11/2026 · `SGF-INTERNAL-STATEMENTS-QA-v1`. **Autorizado expressamente11/11 e entregue; ver [relatório](SGF_FINANCIAL_STATEMENTS_DELIVERY.md).** O texto seguinte conserva o âmbito da decisão. Há balancete/razão efectivos, mas não serviço de correspondências revistas e demonstrações internas. A conferência SELECT-only confirmou os dois poderes abaixo ausentes nas duas bases existentes. Preparar/publicar lançamentos não confere automaticamente poder de aprovar rubricas de apresentação. + +## Resultado funcional do bloco inteiro + +Na mesma8085, Contabilidade → Demonstrações internas: escolher datas explícitas e versão de correspondências; consultar/editar individualmente a correspondência de conta para rubrica, preparar e rever por pessoas distintas, conservar motivo/versão/antes/depois. Não inferir rubrica pelo prefixo da conta, descrição, saldo ou referencial suposto. Conta nova/alterada não recebe classificação automática. Os relatórios distinguem classificação pendente, rubrica sem conta e valor zero observado. + +Produzir balanço **interno/parcial QA** com saldos finais das rubricas classificadas e quadro de pendências; resultados **internos/parciais QA** com movimento do período, receitas/gastos classificados e pendências. Preservar sinais e saldos invertidos; não usar valor absoluto para forçar natureza. Capital próprio não é pressuposto zero. Havendo rubricas por classificar, resultado completo/equação patrimonial completa permanecem indeterminados, mesmo que o balancete esteja equilibrado. + +Reutilizar `trialBalance`/`accountLedger` originais e as suas fórmulas, autorizações, datas e marcador de27 lançamentos; não criar segundo motor de débitos/créditos nem alterar o writer. Processar páginas25/100, sem acumular o universo em memória; grupos exactos por moeda e pendências, com ligação à conta/razão/diário. Os resultados de uma página nunca são declarados totais do universo. Alteração do marcador, da conta ou das correspondências durante preparação exige nova conferência. + +Preparar/rever também o resultado congelado com linhas, pressupostos, pendências, fontes/versões e SHA; consultar/exportar posteriormente sem recalcular ou reescrever a revisão anterior. Uma nova consulta de saldos é actual e identifica se a fonte mudou desde o resultado conservado. CSV/exportação e histórico recuperáveis, prévia reversível, confirmação individual, recibo/chave, isolamento e revalidação de perfil/permissões/pessoa/contexto/versão/dependências. Correcção cria sucessora, não modifica história; repetição/falha/concorrência não duplicam versão ou resultado. Não aprovar a própria preparação. + +## Correspondências artificiais propostas, para escolha/revisão explícitas + +Fonte lida sem escrita: balancete original de10/11/2026,19 contas com movimento/saldo,27 lançamentos. A tabela é **proposta QA**, ainda não aplicada e sem afirmação de classificação contabilística real. Não atribuir estas rubricas a contas reais ou contas futuras. + +|Conta artificial observada|Rubrica QA proposta| +|---|---| +|QA-CAIXA|Activo interno — custódia QA| +|QA-CLIENTES|Activo interno — recebíveis QA| +|QA-STOCK|Activo interno — stock QA| +|QA-FORNECEDOR|Passivo interno — fornecedor QA| +|QA-VENDAS|Rendimento interno QA| +|QA-CONSUMO-STOCK|Gasto interno QA| +|QA-CUSTO-DIRECTO|Gasto interno QA| +|QA-CUSTO-INDUSTRIAL|Gasto interno QA| +|QA-DESPESA-CAIXA|Gasto interno QA| +|QA-DIFERENCA-INVENTARIO|Gasto interno QA| +|QA-COMPENSACAO-ENSAIO|Por classificar — contrapartida de ensaio| +|QA-COMPENSACAO-INDUSTRIAL|Por classificar — contrapartida de ensaio| +|QA-COMPENSACAO-STOCK|Por classificar — contrapartida de ensaio| +|QA-CONTRAPARTIDA|Por classificar| +|QA-DEVOLUCAO-FORNECEDOR|Por classificar| +|QA-IMPOSTO-TESTE|Por classificar; não equivale a imposto apurado/a pagar| +|QA-LIQUIDACAO|Por classificar; não presumir banco real| +|QA-LIQUIDACAO-CLIENTE|Por classificar; não presumir banco real| +|QA-REDUCAO-CLIENTE|Por classificar| + +No recorte observado, as rubricas propostas dão activos conhecidos2155MZN, fornecedor100, rendimentos2586,21 e gastos classificados2200; o subtotal das rubricas de resultados é386,21. **Não é resultado da empresa**: nove contas continuam explicitamente por classificar, incluindo contrapartidas e redução de cliente. Valores são da fonte QA observada e devem ser novamente lidos/conferidos na execução. O bloco deve mostrar essas lacunas, não preencher capital ou “equilibrar” a apresentação. + +## Dois direitos e duas concessões por base + +Tenant1/empresa2/unidade2, apenas8085 e regressão existentes: + +|Direito SGF novo|Único perfil QA|Poder| +|---|---|---| +|`general_ledger.statements.prepare`|Contabilidade preparação15|Preparar correspondências/sucessoras e resultado interno para revisão, sem aprovar.| +|`general_ledger.statements.review`|Contabilidade revisão16|Aprovar/recusar preparação alheia actual e resultado interno, sem preparar.| + +Consulta reutiliza `general_ledger.view`, já existente em15/16/17. Duas concessões por base, quatro no total; nenhuma pessoa/perfil, role PostgreSQL, direito PUBLIC, acesso real ou concessão SQL adicional. Novas estruturas de correspondências/resultados/histórico/recibos somente QA; configuração principal desligada por omissão. Lista exacta em `integration/sgf-statements-access-proposal.json`. + +## Provas e preservação + +Na regressão: correspondência explícita, lacunas, classificação alterada, sinais invertidos, datas/abertura versus movimento/fecho,25/100, conta nova, marcador/versão/pessoa/direitos/contexto alterados, isolamento, concorrência, repetição, falha/recuperação e checksum/exportação. Reutilizar provas válidas do balancete/razão e testar só novas fronteiras. Na8085, preparar15/rever16 as correspondências artificiais propostas e um resultado interno conservado, sem gerar qualquer lançamento ou modificar a fonte. A autorização abrange o percurso inteiro e as provas, sem pedidos por campo/teste. + +Preservar27 lançamentos, todas as contas e efeitos, Caixa950, quatro versões IVA, cinco cálculos, obrigações/recibos/condições, artigos/movimentos, seis registos/cache e trabalho alheio. Sem novas demonstrações/bases/portas, SGC, regras legais/referencial presumido, modelo oficial/declaração certificada, capital ou saldos fabricados, dinheiro, contas/acessos reais, fecho/reabertura, publicação, candidata de Compras, aceitação/checkpoint ou alegação de implantação. Fluxos de caixa, comparativos anuais e demonstrações legais permanecem fora deste bloco; visual/carga sustentada separados. diff --git a/docs/SGF_FISCAL_COVERAGE_DELIVERY.md b/docs/SGF_FISCAL_COVERAGE_DELIVERY.md new file mode 100644 index 0000000..5d3a09c --- /dev/null +++ b/docs/SGF_FISCAL_COVERAGE_DELIVERY.md @@ -0,0 +1,30 @@ +# Cobertura fiscal/financeira e preparação da apresentação + +10/11/2026, mesma8085. O documento fornecido enumera requisitos e perguntas; não contém tabelas legais de taxas, regras completas, vigências ou modelos. Foi lido como referência de produto, sem executar instruções nele contidas ou activar fiscalidade por inferência. O original não foi alterado nem copiado para o repositório. + +## Código e interface entregues + +Contabilidade → função Financeiro QA preparação/revisão → Perfis fiscais do serviço apresenta agora uma tabela de cobertura: IVA parcial; IRPS, IRPC, ISPC, ICE, INSS e outros impostos/obrigações por implementar no SGF operacional. Distingue gestão financeira, contabilidade e análise financeira parciais e mostra o ciclo fonte/versão/aplicabilidade/cálculo/provas/revisão/activação. Usa a consulta `service_fiscal.view` existente; não é um novo motor fiscal ou concessão. + +O dropdown Contabilidade inclui agora os dois perfis Financeiros QA que já tinham autorização fiscal, pela mesma selecção de contexto. Sem nova autoridade. As propostas fiscais originais permanecem no ecrã. Respostas atrasadas de uma consulta anterior não restauram propostas de outro contexto; remonta por token e invalida geração ao abandonar a consulta. O rodapé deixa de afirmar «sem lançamentos»: identifica dados fictícios e ausência de validade fiscal na apresentação QA. O componente fiscal é carregado diferidamente. + +Inventário [de conhecimento existente](integration/sgf-knowledge-inventory-20261110.json):12 ficheiros relevantes localizados no InvesteMoz, com hash e indicação de que presença não prova transferência operacional/conformidade. Reutiliza o levantamento anterior, sem duplicar investigação fiscal ou copiar dossiers/snapshots de utilizadores. Não há dependência do portal ou SGC em execução; nenhum parâmetro legal ou taxa foi importado/activado. Não foi certificada a vigência de legislação neste incremento. + +## Provas + +-17 verificações na regressão existente: contrato de cobertura, sete áreas, sem importação/validade/transmissão implícitas, taxas não seleccionáveis, consulta por duas pessoas autorizadas, recusas de actor/unidade e cinco tabelas originais intactas. +-10 HTTP reais: consulta autorizada, resposta privada sem cache, propostas anteriores, módulo errado, actor de Compras e anónimo recusados. Escolhas de contexto normais auditadas; zero comandos fiscais/de negócio. +-Sete de renderização estática: famílias/estados, aviso de transferência, sem campos de taxa/activação, texto escapado e ausência de direitos implícitos. +-Três do ciclo real do componente com hooks controlados: contexto novo prevalece, propostas antigas descartadas e erro do contexto abandonado suprimido. Não é navegador ou aceitação visual. +-Três HTTP da entrada: seis dropdowns conservados e os dois perfis Financeiros já autorizados acessíveis no dropdown Contabilidade. +-Comparação na8085 de11 tabelas: propostas/decisões/activações/resultados fiscais, facturas, recebíveis/efeitos, Caixa, concessões, movimentos de Stock e diário idênticos. Diário27. Build privado aprovado, anteriores assets/backups retidos. + +[Índice de provas](proofs/sgf-fiscal-coverage-20261110.json). Provas detalhadas e backups privados fora do repositório. Testes anteriores reutilizados, não repetidos. Sem novos direitos, schema, bases, demonstrações, índices, activação fiscal, publicação, SGC, checkpoint ou aceitação presumida. Seis registos e cache não tocados. + +## Pendências corrigidas na matriz + +O fecho anterior refere-se ao recorte QA dos seis módulos. **Não concluiu o requisito amplo de fiscalidade, modelos, remunerações, reconciliação bancária ou análise financeira** de `SERVICE_INVOICE_CALCULATION_SPEC.md`, FIS-SGF-13…19. A matriz passa a mostrar estas lacunas de produto separadamente da revisão visual e da implantação. O painel de cobertura documenta a distinção; não resolve os motores que faltam. + +Próximo bloco concreto preparado: [livro e mapa interno de IVA QA](SGF_IVA_ASSESSMENT_NEXT_SCOPE.md), seis direitos/14 concessões por base QA existente, por autorizar e não aplicados. Sem novos motores fiscais ou modelos oficiais presumidos. + +Revisão visual continua pendente. Uma única tentativa de obter a aba integrada já aberta por URL exacto terminou em timeout30s, com reset do conector. Não se controlou a página, não se repetiu a via Windows bloqueada e não se contornou a falha. Carga sustentada, retenção/órfãos, arranque completo, conformidade e implantação mantêm provas próprias pendentes. diff --git a/docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md b/docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md new file mode 100644 index 0000000..f6b574f --- /dev/null +++ b/docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md @@ -0,0 +1,48 @@ +# Obrigações e prazos administrativos QA — entrega8085 + +10/11/2026 · `SGF-FISCAL-OBLIGATIONS-QA-v1`. Bloco autorizado concluído na mesma8085, **Contabilidade → Obrigações e prazos administrativos QA**. Configuração do código principal desligada por omissão; somente as bases QA existentes têm estruturas/acessos aprovados, e a8085 tem o percurso disponibilizado. Sem SGC ou nova demonstração. + +## Percurso utilizável + +Escolher uma referência temporal explícita para consultar obrigações, com período da origem, estado administrativo, situação do prazo, intervalo de vencimento e páginas SQL25/100. A última condição aprovada é distinguida da preparação actual. Uma preparação ou recusa não altera condições aprovadas. Prazo desconhecido conserva atraso indeterminado; no próprio dia há0 dias e no seguinte1 dia. A situação temporal é independente do registo de entrega/cancelamento e não reconstitui estado histórico. + +Preparação13 e revisão14 individuais, com fundamento e prévia anterior/posterior. O responsável é escolhido explicitamente entre os perfis elegíveis13–17; a interface não escolhe por omissão. Novas obrigações escolhem explicitamente o mapa interno revisto apresentado. A mesma origem não admite outra obrigação activa ou primeira preparação pendente. Correcção, declaração de entrega simulada e cancelamento conservam o registo e exigem preparação/revisão distintas. Cancelamento administrativo não retira uma entrega a uma autoridade; não há tal transmissão neste percurso. + +Na entrega simulada, indicar referência textual e SHA da exportação **interna já existente**. O serviço compara SHA/bytes e estado/versionamento do mapa com o consumidor original. Não existe upload binário, recibo oficial, chamada à AT, envio de mensagem ou alegação de submissão. A obrigação não cria apuramento, imposto a pagar, dívida, pagamento ou lançamento. O campo de imposto a pagar mantém-se indeterminado, nunca zero presumido. + +Histórico individual com versões, operação, autor/data, fundamento e impacto anterior/posterior. A tabela identifica a condição aprovada **actual**; escolher uma versão mostra também o impacto registado nessa versão. CSV contém apenas a página aplicada, filtros/referência, versões, fonte e natureza artificial, com texto protegido contra fórmulas. Recibo próprio é recuperável numa sessão renovada sem executar outra decisão. O recibo histórico não é uma confirmação de validade actual da origem. + +Formulários/preparações em curso impedem trocar consulta ou registo sem descarte explícito. Revisão começa sem decisão escolhida e fundamentos não são copiados. Resposta incerta conserva corpo/chave e oferece recuperação da mesma confirmação. Escolher «Nova obrigação» depois de consultar um registo cria identidade nova, sem reutilizar silenciosamente a anterior; o servidor continua a impedir duplicação de origem. + +## Fonte, revalidação e exemplo conservado + +O consumidor original `FiscalAssessment` continua a conferir documento emitido, recebimento, cancelamentos, período, revisão do livro/mapa e dependências. O novo adaptador verifica ainda os bytes/SHA congelados do mapa. Não altera o serviço original nem o seu writer. + +Confirmação revalida direitos, contexto físico/empresa/unidade, pessoa/autoria, versão, prazo/condição observados, responsável elegível e origem. Mudança de nome/email da identidade observada invalida a prévia; revisão volta a conferir identidade e direito do preparador. Só os perfis QA aprovados13/14 escrevem. Versão, recibo e auditoria de identidade são transaccionais; o histórico append-only próprio guarda o impacto integral. A auditoria comum conserva a referência da versão, autor e fundamento pelo seu allowlist existente, sem o alargar. + +Na8085 ficou **uma obrigação, quatro versões e quatro recibos**: obrigação preparada13/revista14, prazo artificial15/12/2026 e responsável13; declaração de entrega APENAS simulada preparada13/revista14, com referência/SHA da exportação interna. Mapa e período efectivos foram lidos do livro, sem hardcode de ID. O mapa não foi recriado, nem se repetiram factura, recibo financeiro, livro IVA ou lançamento. + +O prazo15/12 é dado artificial de ensaio, **não prazo legal**. O mapa continua interno/parcial, com base862,07/IVA documental137,93/total1000MZN e imposto a pagar indeterminado. São mantidas as quatro versões IVA anteriores e a sua exportação. + +## Acessos e provas + +Aplicados exactamente os três direitos aprovados: `fiscal_obligations.view` a13–17, `prepare` a13 e `review` a14. Sete concessões por base QA existente,14 no total. Nenhuma pessoa/perfil/role PostgreSQL, concessão SQL ou acesso real novo. As tabelas/sequências/função novas negam privilégios PUBLIC. O submenu exige a configuração específica e o novo direito de consulta; os outros módulos mantêm as entradas aceites. + +-41 verificações do percurso na regressão: preparação/revisão distinta, recusa/correcção/desconhecido, entrega/SHA, duplicação de origem, cancelamento, recibos, versão/contexto/direito/origem alterados e falha injectada no recibo com rollback/recuperação. Fixtures administrativas revertidas e sete conjuntos originais comparados. +-19 de fronteiras/relatório:29 linhas SQL adicionais de relatório,25+5/100, estados/período/prazo/referência, vazio, datas inválidas, unidade estrangeira, módulo/flag, identidade e mapa alterados, história imutável. São fixtures de leitura, não obrigações criadas pelo workflow; revertidas. +-22 de interface/contrato/CSV, executando handlers reais com API simulada: referência/responsável explícitos, impacto, identidade nova, protecção de edição, resposta incerta e recuperação idêntica. Não são revisão visual de navegador. +-29 HTTP8085: os quatro comandos administrativos efectivos, exportação original conferida por bytes/SHA, sequência de versões, repetição, recibo em sessão nova, filtros/25/100/recusas e JS principal/lazy com MIME/SHA. Progresso privado conserva chaves/resultados para não repetir os comandos. +-Sete adicionais de entrada/guardas: autenticação401, CSRF419, consultas15/16 sem escrita e navegação por configuração/direito. +-Concorrência em dois processos apenas na regressão: primeira revisão segura a linha da empresa; `pg_blocking_pids` identifica o bloqueador e espera `Lock/transactionid`. Segunda confirmação503 após5,64s; após libertação recusa409 por versão antiga. Primeira repetição200 recupera o mesmo recibo, sem revisão duplicada. Duas operações adicionais cancelam administrativamente essa fixture, conservando quatro versões auditadas na regressão e permitindo futuros ensaios sem duplicar origem activa. Não ocorreu na8085. + +São118 verificações dirigidas distintas (41+19+22+29+7), mais concorrência e61 verificações de preservação/ACL. [Índice das provas](proofs/sgf-fiscal-obligations-20261110.json). Suites alheias/provas válidas foram reutilizadas. + +Na regressão existia livro IVA revisto, mas não mapa revisto persistente. Foram necessários **dois comandos do workflow original** para preparar/rever essa fonte em falta, somente nesse livro de regressão; não se repetiu o livro ou a origem financeira. Essa preparação necessária tem prova própria e não é contada como suite nova. A fonte8085 já existia e permaneceu intacta. + +## Preservação e limites + +Backup privado anterior;47 conjuntos actuais iguais. Nas permissões, todas as linhas anteriores iguais e apenas sete concessões aprovadas acrescentadas. Três bindings intactos; diário27, Caixa950, cinco cálculos, quatro versões IVA, condições de fornecedores, catálogo e movimentos preservados. Quatro eventos de auditoria correspondem aos quatro comandos administrativos novos. Entrada normal nos perfis e uma descarga auditada da exportação existente conservam os registos de consulta próprios; não se declara a tabela global de auditoria byte a byte igual. + +Nenhum comando dirigido aos seis registos ou à cache bloqueada. Dados, credenciais, recibos/chaves e dumps permanecem privados, fora do repositório. Sem novas bases/portas/demonstrações, SGC, fiscalidade real, pagamento, publicação, candidata de Compras, reabertura, aceitação ou checkpoint. + +O âmbito é controlo administrativo de uma origem QA, não calendário legal, aplicabilidade fiscal geral, apuramento, modelo oficial, guia/juros, arquivo binário legal ou submissão electrónica. Se o mapa original deixar de estar actual/revisto, novas decisões são bloqueadas e o histórico continua identificado com a divergência. O bloqueio global da empresa e as guardas de exportação do consumidor original permanecem; não se declara optimização de carga ou agendamento distribuído. Revisão visual, carga sustentada e capacidade de implantação continuam pendentes, assim como os motores fiscais gerais. Matriz v20 mantém o produto amplo incompleto. diff --git a/docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md b/docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md new file mode 100644 index 0000000..b0ba1cb --- /dev/null +++ b/docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md @@ -0,0 +1,33 @@ +# Decisão agrupada — controlo de obrigações fiscais QA + +10/11/2026 · `SGF-FISCAL-OBLIGATIONS-QA-v1`. **Autorizado expressamente e entregue; ver [relatório](SGF_FISCAL_OBLIGATIONS_DELIVERY.md).** O texto seguinte conserva o âmbito da decisão. A matriz identifica calendário/controlo de obrigações entre as lacunas. O mapa interno IVA e os seus direitos existem; não existe o percurso para atribuir/rever um prazo e conservar a declaração/comprovativo de cumprimento. Conferência SELECT-only nas duas bases existentes confirmou os novos poderes abaixo ausentes. Esta proposta implementa um controlo administrativo ligado à evidência existente; não calcula prazos legais ou tributos. + +## Percurso funcional completo + +Na mesma8085, Contabilidade → Obrigações e prazos: pesquisar por período/estado/data, páginas SQL25/100, escolher explicitamente o mapa interno IVA **já revisto**, preparar obrigação administrativa individual com responsável, período da origem, prazo e evidência/fundamento. Outra pessoa confere anterior/proposto, prazo e origem actual antes de aprovar/recusar. Não gerar obrigações em massa nem presumir aplicabilidade por regime/empresa. + +Separar estado administrativo e situação temporal: preparação, obrigação revista, declaração de entrega simulada preparada e revista; pendente/no prazo/atrasada segundo referência explícita; prazo desconhecido mantém atraso indeterminado. Preparação/recusa não substitui a última versão aprovada. Correcção de prazo/evidência e cancelamento administrativo exigem preparação/revisão distintas e conservam história. Uma entrega declarada nunca equivale a transmissão aceite pela AT ou pagamento. + +Registar uma referência textual e SHA do ficheiro **interno já exportável** como evidência da declaração QA. Conferir o mapa/exportação nativos, versão e hash antes de confirmar; não criar arquivo binário genérico, upload, credenciais AT, mensagem, notificação externa ou submissão. Se faltar evidência de envio/aceitação, mostrar explicitamente a lacuna. Não inventar recibo oficial. Relatório e CSV da página identificam origem/versão/período/autores e a natureza artificial; recibo histórico próprio pode ser recuperado sem executar. + +Reutilizar os mecanismos administrativos de prévia reversível, confirmação individual, operação/chave, autor/pessoa distinta, contexto físico, empresa/unidade, direitos e auditoria transaccional. Confirmação revalida origem/mapa revisto, hash, período, responsável elegível, versão, pessoa e prazo observado; mudança exige nova conferência. Concorrência, falha e repetição não duplicam obrigações/entregas. Nenhuma alteração de cálculo, apuramento, dívida, pagamento, diário ou mapa original. + +## Três direitos e sete concessões por base QA existente + +Tenant1/empresa2/unidade2, apenas8085 e regressão existentes: + +|Direito novo|Perfis QA|Poder| +|---|---|---| +|`fiscal_obligations.view`|13,14,15,16,17|Consultar obrigações/evidências autorizadas, relatório e histórico.| +|`fiscal_obligations.prepare`|13|Preparar obrigação, correcção/cancelamento ou declaração de entrega simulada, sem aprovar.| +|`fiscal_obligations.review`|14|Aprovar/recusar preparação alheia actual, sem preparar.| + +**Sete concessões por base,14 no total**, sem nova pessoa/perfil, role PostgreSQL, grant PUBLIC, acesso real ou concessão SQL adicional. Direitos de consultar/rever mapas não concedem gestão de prazos/declarações de cumprimento. Lista exacta em `integration/sgf-fiscal-obligations-access-proposal.json`. Novas estruturas administrativas append-only apenas nestas bases QA; configuração do código principal desligada por omissão. + +## Exemplo e provas do bloco inteiro + +Reutilizar o mapa interno IVA já revisto na8085, com o seu período efectivo e versão, sem o recriar. Preparação13/revisão14 de uma obrigação explicitamente **artificial**, prazo15/12/2026 escolhido como dado de ensaio, **sem fundamento legal ou afirmação de vencimento oficial**. Consulta15/12 sem atraso e16/12 com1dia. Preparação/revisão da declaração simulada usando a referência/SHA da exportação interna; sem chamada a autoridade ou dinheiro. Conservar esse exemplo administrativo e todos os exemplos anteriores. + +Regressão: desconhecido, prazo corrigido, recusa, cancelamento e entrega simulada, origem/versão/hash/pessoa/contexto alterados, escolha de responsável sem autoridade, acesso entre empresas/unidades,25/100, concorrência, repetição e rollback/recuperação. Fixtures revertidas quando possível; prova HTTP da origem efectiva8085 e do percurso autorizado. Não repetir suites fiscais/financeiras válidas ou outras operações já aceites. + +Esta decisão cobre implementação e provas de todo o percurso, sem pedido por campo/teste. Não conclui calendário legal, motores IRPS/INSS/IRPC/ISPC/ICE, modelo oficial, juros, guia ou submissão. Preservar27 lançamentos, Caixa950, quatro versões IVA, cinco cálculos, obrigações/condições, artigos, seis registos/cache e trabalho alheio. Sem novas demonstrações/bases/portas, SGC, operações/acessos reais, publicação, candidata de Compras, reabertura, aceitação/checkpoint ou alegação de implantação. Visual/carga sustentada separados. diff --git a/docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md b/docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md new file mode 100644 index 0000000..6aedf6f --- /dev/null +++ b/docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md @@ -0,0 +1,52 @@ +# Núcleo comum de regras fiscais versionadas QA + +Entregue10/11/2026, `SGF-FISCAL-RULES-COMMON-QA-v1`, após a [autorização expressa](SGF_FISCAL_RULES_COMMON_SCOPE.md). Mesma8085, entrada **Contabilidade → Regras fiscais versionadas QA → Abrir catálogo, histórico e cálculo QA**, ou `/fiscal/rules` depois de escolher uma função autorizada. Sem SGC ou nova demonstração. + +## Percurso funcional conservado + +Financeiro13 preparou a regra artificial versão1; Financeiro14 reviu a versão exacta. Cálculo explicitamente escolhido para30/11/2026: base1000, parâmetro0,16, imposto160,00 e total1160,00. Nova preparação/revisão distintas da sucessora versão2; cálculo escolhido para01/12/2026: base1000, parâmetro artificial0,17, imposto170,00 e total1170,00. **Quatro eventos de regra e dois cálculos novos** conservados na8085. Não se trata de uma mudança legal de IVA. + +O catálogo tem pesquisa literal de chave/nome/fonte, filtros família/estado, páginas SQL25/100, versões temporais e histórico por chave. Fontes, SHA, condições, moeda/unidade, precisão, prioridade, vigência e parâmetros são preservados em cada versão. A interface exige escolha explícita da definição e da regra; não adopta o primeiro registo. Referências IVA/IRPS/IRPC/ISPC/ICE/INSS/outros podem ser inventariadas como pendentes, mas não se transformam em motores legais implementados. + +Revisão não equivale a activação fiscal real. Só os dois DAGs/definições artificiais exactos e fixados por SHA deste contrato podem executar no novo endpoint. Fonte desconhecida/não conferida, moeda diferente, vigência ausente/fora do intervalo, regra não escolhida/não revista/retirada ou conflito de prioridade produzem pendência, com montantes indeterminados. Não se escolhe automaticamente outra regra ou parâmetro zero. + +Revisão temporal pertence à versão lógica; a sucessora futura não elimina a regra anterior válida no seu intervalo. Retirada por revisor distinto cria evento imutável para a versão escolhida, sem apagar cálculo antigo. A consulta do cálculo histórico conserva a fotografia e mostra também a avaliação actual, distinguindo passado e aplicabilidade presente. Nada é atribuído por arrasto a contribuintes, serviços, documentos, livro/mapa IVA ou seis registos antigos. + +## Reutilização e precisão + +Reutilizado o avaliador declarativo `SimulationTaxRuleExpression.php` já existente no InvesteMoz, numa cópia local controlada: [proveniência e adaptações](integration/sgf-fiscal-expression-port.json). Namespace/classe próprios e limites decimais24 inteiros/8 casas na linguagem; a entrada pública de base limita16 inteiros/8 casas. Sem carregar o portal em execução, dossiers de utilizadores, conversas ou regras legais automaticamente. A comparação com a classe de origem ocorreu apenas no teste local de paridade, com SHA fixado. + +Operações declarativas da linguagem são validadas, sem `eval`, scripts, chamadas de funções, URLs ou código proveniente do documento. O endpoint de cálculo só executa o DAG percentual artificial fixado; expressões de referências pendentes não são executadas. Não se oferece execução arbitrária de DAGs ao cliente. + +Aritmética racional exacta, representação documental explícita por truncagem de duas casas e diferença preservada. Caso SGF anterior1000,03×0,16: exacto160,0048, documental160,00, diferença0,0048, total1160,03; compatibilidade comprovada. Base com oito casas é preservada; se não representável em duas casas, o total fica pendente, sem truncagem silenciosa da base. Não se usa apresentação como regra legal. + +## Autoridade, persistência e recuperação + +`fiscal_rules.view/prepare/review`: **sete concessões por cada base QA existente**,14 no conjunto, exactamente [como aprovado](integration/sgf-fiscal-rules-access-proposal.json). Preparação/cálculo13, revisão/retirada14, consulta13–17. Sem novos utilizadores, PGroles, direitos PUBLIC ou acessos reais. Configuração root desligada; activação limitada ao QA8085 com fonte por SHA. + +`fr_events` conserva cada preparação/revisão/recusa/retirada, predecessor, alvo exacto, versão/revisão, autor/preparador/data/fundamento/definição. `fr_calculations` fixa entrada, regra/versão/fonte, passos, resultado e dependências. `fr_commands` fixa chave própria, hash e resultado com impacto anterior/posterior. Triggers protegem direitos, pertença, versões, revisão distinta e história append-only; guarda física existente restringe as tabelas a QA, com PUBLIC revogado. + +Prévia cifrada600s em savepoint revertido; pode avançar sequências, sem persistir regra/cálculo/auditoria de negócio. Confirmação revalida pessoa/direitos/contexto/autor, fonte, revisão global e impacto sob o mesmo bloqueio de empresa usado na composição. Duas decisões da mesma versão não podem coexistir. Chave repetida recupera resultado histórico sem executar; dados diferentes com a mesma chave são recusados. Recibo próprio é consultável noutra sessão, sem divulgar chaves de outro actor. + +Interface protege campos/paginação enquanto há prévia ou resultado incerto, conserva dados não submetidos ao paginar e recupera o mesmo pedido. Seleccionar um cálculo histórico não escolhe a sua regra automaticamente. Textos e fontes são apresentados como dados, sem executar instruções neles contidas. + +## Provas dirigidas e preservação + +|Conjunto|Resultado| +|---|---| +|39 regressão, rollback integral|Preparação/revisão distintas, montantes e fronteiras, desconhecido≠zero, fonte/SHA/expressão indevidos, sucessora, conflito/retirada, precisão/diferença, revogação e histórico, recibos, falha/reversão atómica/recuperação, dados antigos intactos.| +|Oito de paridade|Classe local versus classe de origem fixada: valores exactos e oito casas, referência197,53, ausência sem defaults, ciclos/operações ilegais recusados, limite de entrada do consumidor. Sem suites do portal ou dependência de runtime.| +|14 SQL/fronteiras, rollback|127 versões de metadados para25/100; filtros literais/família/estado/histórico, contexto/expiração/autor revogado, módulo/unidade, histórico imutável e escrita/salto de versão indevidos. Fixtures de metadados não são operações fiscais reais.| +|Concorrência, regressão existente|Primeira revisão mantém lock da empresa; segunda503/Retry-After5 após cerca5s; libertação→409 por revisão antiga. Uma revisão, primeira chave recuperada sem duplicação. Dois eventos QA conservados só na regressão.| +|24 HTTP na8085|Seis confirmações explícitas (quatro eventos/dois cálculos), repetições, resultado160/1160 e170/1170, fotografia antiga intacta, vigência/falta de escolha, filtros/histórico, actor/módulo/tenant/IDs e recibo noutra sessão. Progresso guardado, sem repetir decisões válidas.| +|14 DOM→HTTP|Consulta e escolha explícita, cálculo/prévia real revertida, bloqueio/impacto/cancelamento, dados preservados ao paginar, histórico/recuperação. Zero POSTs de confirmação. Livro/mapa IVA anterior continua revisto e com quatro versões. Não é navegador.| +|Três de entrada|Revisão numérica validada normalizada, UUID malformado recusado antes da consulta e nenhuma regra persistida.| +|Comparação SELECT-only|17 conjuntos originais completos e binding idênticos, incluindo quatro versões/comandos IVA, documentos, recebíveis, Caixa, clientes, fiscalidade, Stock,27 lançamentos e períodos. Apenas sete concessões aprovadas adicionadas.| + +[Índice de provas](proofs/sgf-fiscal-rules-20261110.json). Total102 verificações dirigidas, concorrência discriminada à parte. Provas válidas dos outros módulos e do IVA reaproveitadas, não repetidas. Backups/credenciais/dados privados fora do repositório; assets e alterações alheias conservados. Sem novos efeitos financeiros, índices no destino, novas bases/portas/demos, SGC, publicação/candidata, aceitação/checkpoint ou capacidade de implantação presumidos. + +## Limites + +Este núcleo governa versões e executa dois casos aritméticos artificiais. Não conclui IVA operacional geral, IRPS/IRPC/ISPC/ICE/INSS, remunerações, modelos oficiais ou calendário legal; não confirma fontes legislativas actuais. O documento de requisitos não contém por si essas regras. Os restantes requisitos amplos continuam visíveis na matriz, sem serem escondidos pelo recorte QA. + +Diário27, Caixa fechada950, livro/mapa IVA com quatro versões, seis registos e cache intactos. Visual continua pendente; a via bloqueada não foi repetida. Carga sustentada, retenção/órfãos, arranque completo, conformidade e implantação exigem prova própria. diff --git a/docs/SGF_FISCAL_RULES_COMMON_SCOPE.md b/docs/SGF_FISCAL_RULES_COMMON_SCOPE.md new file mode 100644 index 0000000..439d9ba --- /dev/null +++ b/docs/SGF_FISCAL_RULES_COMMON_SCOPE.md @@ -0,0 +1,33 @@ +# Próximo bloco agrupado — regras fiscais comuns versionadas QA + +`SGF-FISCAL-RULES-COMMON-QA-v1`,10/11/2026. **Autorizado expressamente e entregue**; ver `SGF_FISCAL_RULES_COMMON_DELIVERY.md`. O texto abaixo conserva o âmbito aprovado. É o bloco transversal FIS-SGF-01/12/19 do levantamento existente, não uma nova demonstração ou uma declaração de implementação de todos os impostos. + +## Resultado funcional proposto + +Na mesma8085/regressão: catálogo pesquisável/paginado de fontes, regras e parâmetros por versão; preparação13 e revisão14 distintas; selecção explícita da regra exacta num cálculo QA explicável; fotografia de entrada/resultado/fontes/versão e recibo recuperável. Leitores autorizados consultam histórico e resultado. Nenhum parâmetro é carregado de conversas automaticamente. Sem ligação ao portal/SGC em execução. + +Uma regra identifica família/âmbito/condições, fonte e SHA, precisão, moeda/unidade, início/fim de vigência, prioridade/conflitos e política de representação. Informação não verificada fica pendente e não entra em cálculo determinado. O avaliador aceita apenas operações aritméticas declaradas e validadas; não executa código, scripts, URLs ou expressões arbitrárias de documentos. Reutilizar contratos/casos compatíveis do inventário InvesteMoz, com paridade exacta e diferenças de precisão explícitas; não repetir investigação legal já disponível. + +Caso integral de aceitação do núcleo, explicitamente **artificial**: regra QA versão1, base1000, taxa de ensaio16%, válida até30/11/2026 → exacto/documental160 e total1160. Sucessora QA versão2, taxa artificial17%, válida desde01/12/2026 →170 e1170. Não é afirmação de alteração da taxa legal de IVA. Conferir fronteiras30/11 e01/12, revisão/recusa, ausência/conflito de regra, moeda/precisão e campos indevidos. Não atribuir a taxa artificial a contribuintes, serviços ou documentos existentes. + +Os resultados são ensaios internos do contrato, sem emissão, declaração, retenção, cobrança ou lançamento. Regras revogadas/sucessoras não reescrevem cálculo ou PDF antigo. O livro/mapa IVA entregue conserva as suas quatro versões e origem congelada; a ligação futura de uma regra exige selecção/revisão próprias, nunca adopção automática. Nenhum dos seis registos protegidos recebe associação. + +O catálogo pode inventariar IVA/IRPS/IRPC/ISPC/ICE/INSS e outros identificados com documentação, mas **este bloco não presume motores operacionais completos dessas famílias**, remunerações, calendários legais ou modelos oficiais. Serve o mecanismo comum de manutenção e execução de regras expressamente verificadas; ausência de fonte/parâmetro/aplicabilidade produz pendência. Novos efeitos de negócio fora dos aprovados continuam sujeitos ao seu âmbito. + +## Autoridade proposta + +Três nomes novos, **sete concessões por cada base QA existente**,14 no conjunto. Todos tenant1/empresa2/unidade2, só nos cinco perfis existentes: + +|Direito|Destinatários| +|---|---| +|`fiscal_rules.view`|Financeiro13/14, Contabilidade15/16 e consulta17| +|`fiscal_rules.prepare`|Financeiro preparação13| +|`fiscal_rules.review`|Financeiro revisão14| + +[Matriz integral](integration/sgf-fiscal-rules-access-proposal.json). Direitos dos perfis fiscais de um serviço não são poderes genéricos para manter regras transversais. Direitos do livro/mapa continuam idênticos; não são concedidos por arrasto. Sem novo utilizador, PGrole, direito PUBLIC ou acesso real. + +## Entrega e fronteiras + +Autorizar o bloco inteiro permite implementar/provar/instalar limitadamente na8085, sem novos pedidos por campo, fronteira de data ou teste do mesmo contrato. Primeiro regressão existente com acesso/isolamento, revisão própria, versões, concorrência, repetição e falha/recuperação; depois percurso completo na instalação escolhida. Guardar snapshots antes/depois e reaproveitar as provas válidas dos módulos. + +Preservar27 lançamentos, Caixa fechada950, quatro versões livro/mapa IVA, documentos, seis registos/cache/trabalho alheio. Sem novas bases/portas/demos, motores legais presumidos, aplicação retroactiva, dados pessoais laborais, salários/operações/acessos reais, novos lançamentos/efeitos financeiros, activação legal, reabertura, publicação/candidata, checkpoint/aceitação ou capacidade de implantação. Visual e carga permanecem separados. diff --git a/docs/SGF_GUIAO_APRESENTACAO.md b/docs/SGF_GUIAO_APRESENTACAO.md new file mode 100644 index 0000000..7e6d0d2 --- /dev/null +++ b/docs/SGF_GUIAO_APRESENTACAO.md @@ -0,0 +1,74 @@ +# Guião de apresentação local do SGF + +Entrada: **http://127.0.0.1:8085/sgf**. A aplicação é o SGF; não precisa do SGC ou de outra demonstração. Os seis módulos estão alinhados horizontalmente: abra o dropdown do módulo e escolha **Consultar** para chegar directamente ao exemplo, ou escolha a função de trabalho indicada. Os nomes QA identificam dados de teste. A lista anterior dos17 perfis continua em «Todos os perfis e módulos autorizados». + +Dentro da aplicação, cada módulo tem o seu dropdown de funções. Escolher uma função de outro módulo muda explicitamente o contexto; continua a exigir a mesma autoridade e protege formulários não gravados/confirmações pendentes. Início e opções gerais estão separados dos módulos. + +Este guião consulta os exemplos concluídos; não repita confirmações para os apresentar. Uma nova decisão exige prévia, fundamento e a função responsável. A revisão não pode ser feita pela própria pessoa que preparou. + +| Etapa | Como abrir | O que mostrar | +|---|---|---| +|1. Comercial |Função Comercial — preparação; módulo Comercial; documentos emitidos |Documento de teste1000MZN, origem e PDF. Não é documento fiscal válido. | +|2. Compras |Funções anteriores de Compras; módulo Compras; percurso de Compras |REQ-2026-000003 → OC-2026-000001 → recepção → FACT-COMPRA-COMPLETA-20261104 → obrigação100MZN. Consultar documentos, revisões e histórico. | +|3. Stock |Armazém — Compras; módulo Stock; Existências e «Abrir trânsito, lotes e inventário» |Relatórios → Produtos sem movimento: escolher corte UTC e dias, consultar apenas pares com história conhecida, páginas25/100. No artigo de transferência: origem0,5UN, destino1UN, trânsito0, propriedade1,5UN. No novo artigo de lotes:1,5+2=3,5UN. Histórico de expedição, recepções0,4/0,6, preparação/activação/libertação, contagem e ajuste por outra pessoa. | +|4. Oficinas |Gestão/técnico/verificador conforme consulta; módulo Oficinas |Ordem, autorização, tarefa e apontamentos verificados. Em peças/Stock: consumo ligado à entrega já abatida e recepção da devolução não usada. Em Oficina→rascunho: geração500MZN; botão de continuação escolhe explicitamente Comercial e abre o mesmo rascunho. Não emite outra factura automaticamente. | +|5. Financeiro |Financeiro — preparação/revisão; módulo Financeiro |Recebível1000 com400/600 e reversão400, saldo400. Obrigação do fornecedor100→80→100; comprovativos e histórico. Custo directo275→295 e industrial1800→1820, residual300 separado. Em Caixa (`/finance/cash`): abertura0, classificação explícita do recebimento1000 já existente, despesa50, contagem e fecho950, histórico e recuperação de referência. Caixa QA fechada; não repetir operações. | +|6. Contabilidade |Contabilidade — preparação/revisão; módulo Contabilidade; diário integrado |27 lançamentos equilibrados no mesmo diário QA, duas correcções por pares, origens individuais, Stock145MZN e diferença de inventário10MZN. Plano pesquisável, balancete e razão por conta. Transferência interna é mudança de localização, sem despesa. Período aberto; o fecho QA já foi provado com rollback. | + +Para confirmar as autorizações, use também a função de consulta: uma leitura não oferece os comandos de decisão. Erros de contexto/versão exigem reabrir e conferir, nunca escolher outra origem automaticamente. Se uma resposta se perder durante uma confirmação, mantenha a página e recupere o mesmo pedido. + +Arranque da instalação existente: `scripts/abrir-sgf.ps1`. Reutiliza a porta8085 e o cluster existente; não cria base/demo, não faz migrations nem limpa cache. As provas de reinício não incluem um reinício completo do computador. + +A revisão visual automática foi interrompida porque a ferramenta não conseguiu verificar com segurança o endereço do navegador. O utilizador confirmou a abertura/legibilidade do novo Stock. Conferir ainda a legibilidade e continuidade dos outros cinco módulos antes da aceitação integral para apresentação. [Resultados e limitações](SGF_SIX_MODULE_CLOSURE_20261110.md). + +Actualização10/11: Comercial → Todos os documentos reúne seis famílias com filtros/páginas; Caixa e relatório de inactividade foram acrescentados na mesma8085. Ver `SGF_CASH_DELIVERY.md`, `SGF_BILLING_REGISTER_DELIVERY.md` e `SGF_STOCK_INACTIVITY_DELIVERY.md`. Estas entregas não substituem a revisão visual ou a aceitação explícita. + +Cobertura fiscal/financeira: no dropdown Contabilidade escolha Financeiro QA preparação/revisão e abra Perfis fiscais do serviço. O painel distingue IVA parcial e motores/modelos/rácios ainda pendentes. As duas funções já possuíam os direitos de consulta; não há concessões por escolher a entrada. O conhecimento documental não é importado automaticamente. Ver SGF_FISCAL_COVERAGE_DELIVERY.md. + +Livro/mapa interno IVA QA: Contabilidade → função Financeiro preparação/revisão → Livro e mapa interno de IVA QA, ou `/fiscal/iva`. As quatro decisões já estão conservadas; consulte histórico e descarregue o CSV revisto sem repetir operações. Base862,07/IVA137,93/total1000; dedução e imposto a pagar indeterminados. Consulta QA17 também pode ler o mapa. Não apresentar como modelo oficial ou IVA completo da empresa. + +Regras fiscais QA: Contabilidade → Regras fiscais versionadas QA, ou `/fiscal/rules`. Duas versões revistas e cinco cálculos estão conservados (dois simples e três por linhas): escolha uma versão e consulte histórico/resultados sem repetir preparações. As taxas16%/17% são artificiais neste contrato; não afirmar mudança legal. Retirada não apaga a fotografia de cálculo anterior. Referências fiscais não conferidas ficam pendentes. + +Conferência de extracto QA: Financeiro → Conferência de extracto QA ou `/finance/bank`. O extracto já está finalizado: abertura0, débito20/crédito20, fecho0; duas correspondências escolhidas/revistas individualmente com diários2/3, histórico e relatório. Não repetir importação/decisões. Não o apresentar como banco real ou integração bancária geral; não alterou Caixa950 nem diário27. + +Indicadores QA: Financeiro → Critérios e indicadores QA ou `/finance/indicators`. Há dois critérios revistos e dois resultados conservados: Caixa950/saldo credor QA-FORNECEDOR100=9,50vezes; despesa50/entradas1000=5,00%. Consulte versão/fontes/fracção/relatório sem repetir decisões. Não é rácio geral de liquidez ou margem, critério real ou recomendação de pagamento; série/tendência anteriores ficam indeterminadas. + +Cálculo por linhas/preço incluído: no mesmo ecrã Regras fiscais QA, consulte os três resultados novos e descarregue o mapa interno.400+600 sem imposto e464+696 incluídos: base1000/imposto160/total1160;100 incluído:86,21+13,79, exactos2500/29 e400/29, diferenças9/2900. Não repetir confirmações para apresentar. Uma nova simulação exige escolha explícita de regra/modalidade/data/moeda e fundamento; não emite factura nem altera dívida ou diário. + +Início operacional: actualize a página8085 e escolha «Abrir início de…» na entrada ou «Início» após entrar. Mostre o percurso/evidências e escolha uma fonte para ver os registos existentes; não repetir confirmações. Na Oficina, Painel/Equipa/Relatórios permitem pesquisar cliente, equipamento ou responsável, filtrar o estado dos apontamentos e paginar25/100. Minutos apontados/verificados e registos sem verificação favorável mantêm significados próprios; abrir a ordem continua no workflow original. Ver SGF_OPERATIONAL_INFORMATION_DELIVERY.md. + +Contabilidade → Análise: escolha explicitamente datas (o exercício tem movimentos em10/11/2026), consulte o balancete e abra o razão pela linha da conta. Há páginas25/100, totais do filtro completo, saldo progressivo e CSV apenas da página. Não há geração de lançamentos. Uma impressão faz nova leitura; não representa um relatório certificado. Ver SGF_ACCOUNTING_REPORTS_DELIVERY.md. + +Financeiro → Posições de clientes e vencimentos: indique a referência para classificar vencimentos e consulte documento/cliente, estado, saldo reconhecido e revisão. Saldos são actuais; não são um relatório histórico na data escolhida. Pesquise, filtre e abra a posição exacta para comprovativos sem repetir operações. CSV apenas da página. Vencimento desconhecido continua indeterminado. Ver SGF_RECEIVABLE_POSITIONS_DELIVERY.md. + +Stock → Artigos e revisão do catálogo: entre com Armazém — Compras para preparar e com a função de conferência técnica QA para rever. QA-CATALOGO-INTEGRADO/UN já está aprovado, sem saldo inicial; consulte ficha e duas versões sem repetir decisões. Artigos anteriores só para consulta. O histórico de correcção da designação foi provado na regressão. Ver SGF_STOCK_CATALOGUE_DELIVERY.md. + +Os scripts de vários ecrãs devolviam HTML no servidor local; a entrega foi corrigida e conferida por MIME/SHA na8085. Actualize a página antes de conferir as funções. Isto não substitui a revisão visual ainda pendente; ver SGF_CLIENT_ASSET_DELIVERY.md. + +## Posições de fornecedores —10/11 + +Na8085, função Financeiro com consulta autorizada → Posições de fornecedores: pesquisar fornecedor/documento, filtrar estado documental e navegar25/100. O saldo vem do motor original; o resumo/CSV abrange só a página e separa moedas. A obrigação reconhecida abre o documento exacto. Vencimento sem evidência permanece desconhecido. Não confirmar pagamentos para apresentar esta consulta. Ver `SGF_PAYABLE_POSITIONS_DELIVERY.md`. + +## Condições de vencimento de fornecedor —10/11 + +Na mesma8085, escolher Financeiro → Posições de fornecedores. A factura100MZN mostra a data original desconhecida e, separadamente, a condição administrativa QA30/11/2026. Abrir «Condições de vencimento e histórico», indicar explicitamente30/11/2026 e consultar:0 dias;01/12/2026:1 dia. Histórico conserva preparação13/aprovação14 e a referência de evidência artificial. O saldo permanece100MZN. Para a apresentação, consultar o exemplo existente; não repetir preparação/aprovação ou pagamentos. Perfis de consulta não recebem botões de preparação/revisão. O recibo próprio pode ser consultado pela chave conservada privadamente. Ver `SGF_PAYABLE_DUE_DELIVERY.md`. + +## Oficina — relatório por abertura e CSV + +Na8085, função Oficina gestão ou técnico já autorizado → Oficinas → Relatórios. Escolher ambas as datas de abertura e pesquisar; pode combinar referência/cliente/equipamento/responsável e estado,25/100. Exportar apenas a página consultada. Identificação/apontamentos são actuais, o intervalo é da abertura das ordens no dia local; não apresentar como fecho diário, entrega ou produção histórica. A apresentação usa ordens existentes, sem criar novas. Ver `SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md`. + +## Obrigações e prazos administrativos QA —10/11 + +Na8085, entrar numa função Contabilidade de consulta já autorizada (por exemplo QA17) e escolher «Obrigações e prazos administrativos QA». Indicar15/12/2026 e Consultar: uma obrigação ligada ao mapa IVA interno existente, responsável13 e entrega APENAS simulada, revista14. O prazo é artificial, não legal. A referência16/12/2026 mostra1dia decorrido, independentemente da entrega administrativa. Escolher o registo e abrir o histórico: quatro versões com autores/fundamentos/impactos anteriores e posteriores. O CSV exporta apenas a página. Não repetir preparação, revisão ou descarga para provar o que já está guardado; o SHA conferido permanece na evidência. Não apresentar como modelo oficial, submissão AT ou imposto pago. Ver `SGF_FISCAL_OBLIGATIONS_DELIVERY.md`. + +## Antiguidade de fornecedores —10/11 + +Na8085, função Financeiro com consulta de fornecedores → «Mapa de antiguidade — fornecedores». Indicar15/12/2026 e pesquisar: o saldo original100MZN é classificado em1–30dias, com15dias desde a condição administrativa QA30/11. O prazo e o atraso originais continuam desconhecidos e separados. Totais e CSV só desta página; moedas e vencimento desconhecido separados. Abrir a condição/obrigação exacta permite consultar o histórico sem repetir decisões ou pagamentos. Não apresentar como dívida histórica, prazo legal ou recomendação. Ver `SGF_PAYABLE_AGING_DELIVERY.md`. + +## Balanço e resultados internos parciais —11/11 + +Na8085, entrar na função Contabilidade de consulta QA17 e escolher «Demonstrações internas parciais QA». No histórico, abrir o Resultado interno v2 revisto já existente. Balanço: activos classificados2155 e fornecedor100MZN; resultados: rendimentos2586,21/gastos2200/subtotal386,21. Mostrar as nove contas por classificar e o capital/resultado completo indeterminados: não é resultado da empresa nem modelo legal. As19 linhas vêm dos27 lançamentos anteriores; a ligação ao razão conserva conta/datas. O botão de CSV envia o contexto e confere o SHA antes de guardar, sem recalcular o resultado. Não repetir preparação/revisão, não criar lançamentos para apresentar. Dados, chaves e CSV de prova ficam privados. Ver `SGF_FINANCIAL_STATEMENTS_DELIVERY.md`. + +Oficinas → Equipamentos → **Consultar ordens deste equipamento**: escolher um registo existente, consultar identificação/ordens actuais, pesquisar/paginar e abrir a ordem exacta. **Voltar ao catálogo de equipamentos** conserva a pesquisa anterior. Equipamento sem ordens mostra zero; não é necessário criar operações para apresentar. [Detalhes](SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md). + +**7. Recursos Humanos (autónomo):** na entrada `/sgf`, abra o dropdown Recursos Humanos e escolha RH consulta, preparação ou revisão. Dentro, abra Cadastro e remunerações internas QA: cadastro QA-COLABORADOR-01, regras artificiais3/4/8 e processamento2026-11 revistos, bruto10000/líquido8900/custo10400, mapa conservado e histórico. Consultar não repete seis decisões. Os perfis Financeiros não têm acesso a RH. Ainda não há contratos, assiduidade/férias ou cálculo salarial legal. [Relatório](SGF_HUMAN_RESOURCES_DELIVERY.md). diff --git a/docs/SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md b/docs/SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md new file mode 100644 index 0000000..d6b4b1f --- /dev/null +++ b/docs/SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md @@ -0,0 +1,31 @@ +# Recursos Humanos — autonomia e perfis QA + +Directiva expressa11/11/2026: Recursos Humanos é um módulo autónomo; permissões e perfis são diferentes dos Financeiros. A estrutura e o recorte funcional de remunerações estão autorizados. **Perfis e concessões autorizados expressamente11/11: Autorizar os perfis QA próprios de RH e as concessões apresentadas.** + +## Separação proposta + +Sétimo módulo `human_resources`, **Recursos Humanos**, no mesmo menu horizontal/dropdown e na mesma8085. Funções: cadastro fictício, regras artificiais e processamento interno de remunerações. Usa o controlo comum de sessão/contexto/empresa/unidade e auditoria, com namespace e perfis próprios. Não copia os poderes dos perfis Financeiros, não fornece cadastros/rascunhos comerciais partilhados e não concede leitura de salários a Contabilidade por inferência. + +O percurso funcional e o caso10000→8900/custo10400 permanecem os do [âmbito autorizado](SGF_REMUNERATIONS_SCOPE.md): preparação e revisão distintas, prévia/versionamento/dependências, correcção/cancelamento, mapa conservado/SHA/exportação/histórico/recibos. Percentagens3/4/8 são artificiais, não cálculo legal validado. Sem dívida, pagamento, salário real ou lançamento novo. A ligação futura a Financeiro/Contabilidade deverá usar contrato e direitos explícitos; ainda não executa esses efeitos. + +As sete concessões a Financeiros13–17 por livro, aplicadas nesta continuação antes da clarificação, já foram **retiradas integralmente**, com auditoria, apenas deste bloco. Nenhum dado de cadastro ou processamento ficou persistido nesses perfis. A prova inicial de40 verificações foi anterior a esta separação e não constitui aceitação da nova fronteira de RH. As fontes antigas permanecem intactas. Os nomes antigos `remunerations.*` não serão reutilizados para conceder acesso. + +## Matriz concreta de acessos + +Criar três identidades exclusivamente sintéticas **novas em cada um dos dois livros já existentes**, sem contas reais, passwords públicas ou roles PostgreSQL. Identificadores físicos serão registados no acto, sem assumir que18/19/20 estão livres. Preservar a matriz e o pin dos dez perfis anteriores: usar registo separado de perfis de RH, append-only e verificado por SHA. + +|Perfil novo|Poderes de RH|Acessos comuns indispensáveis| +|---|---|---| +|`hr_prepare` — RH preparação|`human_resources.remunerations.view`, `human_resources.remunerations.prepare`|`company.view` apenas empresa2; `unit.view` apenas unidade2| +|`hr_review` — RH revisão|`human_resources.remunerations.view`, `human_resources.remunerations.review`|`company.view` apenas empresa2; `unit.view` apenas unidade2| +|`hr_read` — RH consulta|`human_resources.remunerations.view`|`company.view` apenas empresa2; `unit.view` apenas unidade2| + +Tenant1/empresa2/unidade2. **Três novos nomes de direito, três perfis e11 concessões por livro; seis perfis/22 concessões no total.** Dessas22,12 são acessos comuns de contexto e10 são de RH. Não conceder qualquer outro direito ou estender os perfis reais/anteriores. `company.view` permite apenas a consulta geral da empresa exigida pelo selector comum, sem gestão de empresa/unidades/acessos. + +A decisão de separar RH não equivale à aprovação desta nova lista de concessões. A regra local exige apresentar novos poderes antes da concessão. A aprovação desta matriz permite criar somente estes perfis QA, disponibilizar o recorte na mesma8085 e executar as provas positivas, negativas, concorrência/recuperação e preservação previstas, sem autorização por campo/rubrica. + +## Funcionalidades de RH ainda fora deste recorte + +Contratos e alterações contratuais, assiduidade/turnos/férias/ausências, benefícios/regimes reais, cálculo legal IRPS/INSS, modelos oficiais e ligação salarial a dívida/pagamento/Contabilidade permanecem tarefas explícitas de produto. O cadastro mínimo fictício e o processamento interno não serão apresentados como RH completo. Não ampliar a recolha de dados pessoais ou poderes de perfil sem correspondência de âmbito. + +Preservar os27 lançamentos,19 contas, Caixa950, provas válidas, seis registos, cache bloqueada e trabalho alheio. Sem nova demonstração/base/porta, SGC, publicação, candidata de Compras, aceitação/checkpoint presumidos. Visual, carga sustentada e capacidade de implantação separados. diff --git a/docs/SGF_HUMAN_RESOURCES_DELIVERY.md b/docs/SGF_HUMAN_RESOURCES_DELIVERY.md new file mode 100644 index 0000000..d4151a6 --- /dev/null +++ b/docs/SGF_HUMAN_RESOURCES_DELIVERY.md @@ -0,0 +1,34 @@ +# Recursos Humanos autónomo — cadastro e remunerações internas QA + +11/11/2026. Disponível na **mesma8085**, entrada `/sgf` → **Recursos Humanos** → consultar/preparar/rever. Dentro do módulo: **Cadastro e remunerações internas QA**, com cadastro fictício, regras artificiais e processamento. O módulo tem menu, proprietário HTTP e direitos próprios; não é uma função do Financeiro. Não depende do SGC nem de outra demonstração. + +## Percurso utilizável + +Cadastro pseudónimo individual → preparação/revisão distinta → regras com vigência/fontes QA revistas → escolha explícita do trabalhador/regras/período/bruto → prévia explicável → confirmação fundamentada → revisão alheia → mapa conservado/SHA, histórico e recibos. Listas com filtros SQL antes de páginas25/100, histórico paginado, escolha sem primeiro registo automático, fonte revalidada, formulário/decisão em curso protegidos e recuperação pela mesma chave/corpo. Correcção e cancelamento exigem novas versões e revisão; não substituem nem apagam resultados anteriores. + +O exemplo efectivo é **QA-COLABORADOR-01**, período2026-11: bruto10000MZN, desconto trabalhador300, retenção artificial800, encargo patronal400, líquido interno8900 e custo interno10400. Percentagens3/4/8 são parâmetros **artificiais**, sem validação legal de IRPS/INSS. O cálculo conserva valores exactos e diferenças racionais por rubrica; valores ausentes não viram zero e impedem aprovação completa. + +Há seis versões e seis recibos técnicos na8085: cadastro preparado/revisto, regras preparadas/revistas e processamento preparado/revisto. O mapa individual revisto é imutável, exportável mediante autenticação/contexto e SHA; o resumo da lista é apenas dos aprovados activos da página, sem soma global ou afirmação histórica. Não é recibo salarial legal nem prova de pagamento. + +## Perfis e isolamento + +Autorização expressa: **Autorizar os perfis QA próprios de RH e as concessões apresentadas**. Criados `hr_prepare`, `hr_review`, `hr_read`, em registo próprio append-only/SHA. São três identidades e11 concessões por livro QA existente, seis identidades/22 concessões no total: `company.view`/`unit.view` mínimos e `human_resources.remunerations.view/prepare/review` discriminados. Não se concedeu direito PostgreSQL/PUBLIC nem acesso real. Os dez perfis anteriores e os seus pins foram conservados. + +Os perfis RH só descobrem RH e não consultam Financeiro/Contabilidade/Comercial. Os Financeiros não descobrem nem consultam RH. Cada confirmação revalida contexto/pessoa/direitos, preparador, versão/dependências e impacto, com transacção única para versão/recibo/auditoria. Prévia não grava versão. A sessão/CSRF/contexto são os mecanismos comuns existentes; descarregar usa o contexto em cabeçalho, nunca no URL. + +As sete concessões antigas deste bloco aos Financeiros por livro, anteriores à directiva de autonomia, tinham sido retiradas e permanecem ausentes. O instalador antigo está bloqueado; não restaurar esses poderes. A nova matriz é a única autorização actual. Não repetir as seis decisões8085: progresso/chaves e resultado conservados em privado. + +## Provas dirigidas + +-40 verificações do percurso com os perfis RH efectivos na regressão: cadastro/regras, cálculo, revisão distinta, repetição, versão obsoleta, correcção/recusa/cancelamento, exportação antiga imutável, parâmetros ausentes e recusas. Casos/versões/recibos revertidos. Não contar a passagem anterior à separação como prova nova. +-23 fronteiras finais: fonte alterada entre prévia/confirmação e entre preparação/revisão, segunda pessoa/período, vigência/rates/atributo desconhecido, direito do preparador revogado, falha injectada após versão antes de recibo com rollback integral/recuperação única, filtros/25+2/100/ordenação, unidade estrangeira antes da contagem e histórico recusado.27 linhas SQL de paginação e uma estrangeira são fixtures de leitura, não workflows; tudo revertido. +-Concorrência real apenas na regressão: primeira revisão conserva `FOR UPDATE` da empresa; `pg_blocking_pids` identifica a segunda sessão à espera de `transactionid`. Segunda confirmação devolve503 após5,20s; após libertação409 por versão alterada; repetição da primeira200, um par preparação/revisão e dois recibos de processamento. Seis versões/recibos de suporte ficaram só na regressão, sem efeito financeiro. Não é medição de carga sustentada. +-16 verificações de handlers de interface com API simulada: escolha explícita, formulário, prévia,503/recuperação idêntica, fontes sem escolha automática, menu autónomo e resposta de outro tipo recusada. Prova reutilizada; não é revisão visual. +-43 HTTP finais sobre a8085: identidades e direitos exactos, módulo disponível, seis resultados aceites reutilizados, valores/página/histórico, exportação/SHA, recusas de preparação/revisão/recibo/âmbito, separação bidireccional, autenticação401/CSRF419, dropdown de entrada e JS principal/lazy/partilhado MIME/SHA. Na primeira passagem foram feitas as seis decisões; a continuação corrigiu um URL inexistente do teste de recusa contabilística e reutilizou os resultados, sem repetir comandos de negócio. +-71 verificações de preservação/ACL:55 conjuntos originais idênticos e todas as linhas antigas de permissões idênticas, só11 grants aprovados acrescentados, três bindings iguais,27 lançamentos/19 contas, resultados anteriores, seis registos/cache intactos. Sem nova base/porta/demo, cache clear, reinício ou alterações de direitos públicos. + +São132 verificações dirigidas (incluindo dez leituras finais de entrada/registos) mais concorrência e71 de preservação; não somar passagens substituídas, provas pré-autorização ou a suite antiga anterior à separação. [Índice com hashes](proofs/sgf-human-resources-20261111.json). Copiados apenas os ficheiros deste bloco e assets privados compilados, com anteriores conservados. Conferência final de38 ficheiros instalados e pin de perfis inalterado após actualizar apenas o estado documental da matriz. As sete entradas e um cadastro/conjunto de regras/processamento efectivos foram confirmados sem repetir decisões. Root flags desligadas por omissão; só os dois livros QA autorizados recebem perfis e apenas a instalação8085 existente disponibiliza o percurso. + +## Pendências explícitas + +Este recorte não conclui Recursos Humanos completo: contratos/alterações contratuais, assiduidade/turnos/férias/ausências, benefícios/regimes reais, cálculo legal IRPS/INSS/modelos oficiais e ligação salarial a dívida/pagamento/Contabilidade continuam por implementar. Não foram criados salário real, dívida, pagamento, lançamento ou submissão. Visual, carga sustentada, armazenamento e capacidade de implantação exigem provas próprias. Sem publicação, candidata de Compras, aceitação ou checkpoint presumidos. diff --git a/docs/SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md b/docs/SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md new file mode 100644 index 0000000..77d0267 --- /dev/null +++ b/docs/SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md @@ -0,0 +1,61 @@ +# RH — contratos, assiduidade e férias integrados + +11/11/2026. Pedido expresso para concluir estas três áreas no módulo autónomo RH, na mesma8085. **Bloco e30 concessões QA autorizados expressamente11/11: Autorizar o bloco e as30 concessões QA apresentadas. Implementação em curso.** A conferência SELECT-only dos dois livros existentes confirma nove nomes de direito sem nenhuma concessão. + +## Entrega única proposta + +Completar **Contratos → planeamento/assiduidade → férias → conferência do período**, ligado ao cadastro RH já revisto. Reutilizar identidades, sessão/contexto, revisão distinta, prévia/confirmar/recuperar e auditoria existentes. Não criar outro módulo, utilizador, demonstração, base, porta ou dependência do SGC. Os perfis Financeiros não recebem acesso. + +### Contratos + +Escolha explícita do trabalhador fictício revisto, referência individual, modalidade interna QA (prazo explícito ou fim desconhecido/sem termo indicado), início/fim, bruto mensal MZN e evidência documental textual. Preparar, rever/recusar, corrigir, terminar e cancelar por versões e decisões distintas, com fundamento, impacto anterior/posterior, autores/datas e exportação interna conservada. Não é contrato juridicamente validado nem assinatura electrónica. Código de trabalhador e identidade do contrato são estáveis; não reutilizar a referência para outra pessoa. + +Impedir sobreposição de contratos efectivos incompatíveis da mesma pessoa, datas invertidas e remuneração sem origem explícita. Preparação não activa contrato nem altera o cadastro ou processamento. Correcção/terminação não reescreve assiduidade, férias ou documentos revistos; mostrar dependências/impactos e recusar alterações incompatíveis até conferência individual. Sem exclusões, indemnizações, antiguidade legal ou salário proporcional presumidos. + +### Planeamento e assiduidade + +Data local e zona Africa/Johannesburg explícitas; intervalos previstos e declarados separados, até oito intervalos por conjunto/dia, pausas representadas por intervalos separados, minutos inteiros e limites UTC verificáveis. Registos nocturnos são divididos explicitamente por dia; não dividir nem criar linhas automaticamente. Bloquear duplicação/sobreposição e intervalos inválidos. Planeamento não comprova trabalho; declaração não revista não é assiduidade confirmada. + +Preparação e revisão distintas, correcção/cancelamento, histórico e recibos. Plano ausente, declaração ausente ou conferência de férias em falta mantêm valores desconhecidos; declaração explícita vazia distingue-se de informação inexistente. Mostrar minutos previstos, declarados, cobertos por férias e por conferir, sem qualificar automaticamente uma falta como injustificada. Tempo fora do plano não é trabalho extraordinário autorizado. Sem relógio de ponto, biometria, localização ou dados reais. + +### Férias + +Condição/saldo inicial QA por pessoa e intervalo de vigência, explicitamente preparados/revistos; nenhum direito anual legal ou saldo zero é inferido. Usar minutos, com equivalência a jornadas apenas quando o plano individual a suporta. Não pressupor fins-de-semana, feriados, dias úteis, acumulação ou transição anual sem fonte/regra expressas. + +Pedido individual de intervalos previstos → preparação/revisão → reserva → declaração de gozo/revisão → consumo; correcção/cancelamento com revisão libera ou ajusta a contribuição correspondente, preservando a versão anterior. Sem gozo automático por passagem da data. Cada pedido contribui uma única vez pelo estado efectivo: reservado, gozado ou cancelado. Revalidar saldo, contrato, plano, intervalos e conflitos na confirmação. Impedir saldo negativo, férias fora do plano/contrato e sobreposição com trabalho declarado ou outro pedido efectivo. Repetição não reserva/consome outra vez; concorrência sobre o mesmo saldo exige revalidação antes de decidir. + +### Integração e conferência + +Ficha do trabalhador abre exactamente os contratos, dias e pedidos escolhidos; ligações e histórico respeitam empresa/unidade/perfil. Listas com pesquisa/filtros SQL, ordenação e páginas25/100; não materializar o universo da empresa em memória. Saldo é calculado sobre estados efectivos, não sobre uma página nem pela soma de todas as versões históricas. + +Conferência de período por pessoa identifica fontes/versões, cobertura de dias conhecidos, situações desconhecidas e conflitos; resultado revisto/exportação com SHA conservados e recuperáveis. O poder de preparar/rever esta conferência é `human_resources.attendance.prepare/review`, exigindo também consultas de contratos/férias/remunerações. O relatório não qualifica mês completo sem evidência suficiente para todos os dias aplicáveis. + +Novas preparações de remuneração poderão escolher explicitamente contrato revisto, captando versão/vigência/bruto e revalidando-os na confirmação. Não escolher contrato automaticamente, propagar alterações ou recalcular resultados anteriores. As seis versões/recibos e o mapa salarial actual da8085 permanecem intactos; a ligação para uma preparação salarial nova é testada na regressão e não cria outro processamento8085. O relatório identifica o processamento anterior como sem ligação contratual histórica comprovada; não inventa essa relação. + +## Exemplo efectivo na8085 + +Usar somente **QA-COLABORADOR-01**, já revisto. Contrato interno QA-CONTRATO-01, início01/11/2026/fim31/12/2026, bruto mensal10000MZN, sem alterar o cálculo salarial existente. Plano explícito nos dias09 e10/11,08–12 e13–17 (480min/dia). Dia09: declaração480min e revisão distinta. Dia10: condição QA960min, pedido480min, reserva e gozo declarado/revisto; disponível final480min, reservado0, gozado480min. A referência do ensaio é11/11/2026. + +O saldo960min é um **orçamento fictício declarado**, não um direito legal a férias. Só dois dias estão cobertos pelo exemplo; os restantes não se tornam faltas nem tornam o mês completo. Correcção, recusa, terminação/cancelamento, concorrência e falha parcial são demonstradas na regressão existente, preservando o percurso positivo8085. + +## Novos direitos apresentados antes da concessão + +|Área|Consulta dos três perfis RH existentes|Preparação — hr_prepare|Revisão — hr_review| +|---|---|---|---| +|Contratos|`human_resources.contracts.view`|`human_resources.contracts.prepare`|`human_resources.contracts.review`| +|Assiduidade/conferência|`human_resources.attendance.view`|`human_resources.attendance.prepare`|`human_resources.attendance.review`| +|Férias|`human_resources.leave.view`|`human_resources.leave.prepare`|`human_resources.leave.review`| + +`hr_read` conserva apenas consultas. **Nove nomes novos e15 concessões por livro QA existente;30 concessões no total**, exclusivamente tenant1/empresa2/unidade2. Reutilizar os três perfis existentes em cada livro: zero utilizadores/perfis/roles novos, zero concessões a Financeiro, contas reais ou PUBLIC. `company.view`, `unit.view` e consultas de cadastro/remunerações já existem; não os conceder outra vez. + +Aplicar a extensão por decisão de acesso própria e auditada; não alterar o pin/registo append-only da matriz inicial dos perfis RH nem o dos dez perfis anteriores. [Matriz exacta](integration/sgf-human-resources-personnel-access-proposal.json). Os direitos de remunerações não concedem estas operações. A regra local exige apresentar poderes novos antes de conceder; a decisão proposta cobre todo o bloco, sem autorizações por campo, estado, rubrica ou teste. + +## Validação e preservação + +Confirmar pessoa, sessão/CSRF, módulo/empresa/unidade, direitos actuais do autor/preparador/revisor, versão, fontes, vigências, conflitos e saldo dentro da transacção. Versionar prévia/impacto, registar autor/data/fundamento e estados anterior/posterior, persistir versão/efeitos administrativos/recibo/auditoria atomicamente. Commit incerto conserva corpo/chave e permite recuperar o mesmo resultado, sem nova execução; acesso continua a ser revalidado. + +Provas novas apenas das áreas/fronteiras alteradas: duas pessoas/contratos, pesquisas/filtros25/100, permissões e isolamento entre unidades/módulos, sobreposição, valores desconhecidos, fontes/versões alteradas, saldo insuficiente, correcção/cancelamento/libertação, concorrência com bloqueador identificado e resultado após libertação, repetição sem duplicação, falha parcial/recuperação e CSV/SHA. Fixtures de volume/casos negativos revertidas. Reutilizar132 provas de RH/remunerações e restantes provas válidas; não repetir salários, lançamentos, Caixa ou percursos alheios. + +Já preparado em código local, sem persistência/instalação: `PersonnelTime`, cálculo de intervalos/estado desconhecido/conflitos/saldo efectivo, com22 verificações puras. Não constitui entrega das interfaces, writers, acessos ou percurso integrado; estes ficam por concluir após a decisão de acessos. Não executar migrations globais ou alterações no destino real; tabelas técnicas novas apenas nos dois livros QA existentes, com guardas e flags próprias desligadas por omissão. + +Preservar seis registos protegidos, cache bloqueada,27lançamentos/19contas, Caixa950, seis versões/recibos de remuneração8085 e demais dados/provas/trabalho alheio. Sem salário/colaborador/contrato real, descontos de faltas, processamento/dívida/pagamento/GL novos na8085, cálculo legal IRPS/INSS, modelos oficiais, submissão, novas demonstrações/SGC, publicação, candidata de Compras, aceitação/checkpoint ou implantação presumidos. Visual, carga sustentada e capacidade de implantação permanecem separados. diff --git a/docs/SGF_HUMAN_RESOURCES_PREPARATION.md b/docs/SGF_HUMAN_RESOURCES_PREPARATION.md new file mode 100644 index 0000000..1c59d78 --- /dev/null +++ b/docs/SGF_HUMAN_RESOURCES_PREPARATION.md @@ -0,0 +1,30 @@ +# Recursos Humanos autónomo — preparação local (histórico) + +Substituída pela [entrega funcional](SGF_HUMAN_RESOURCES_DELIVERY.md), após autorização expressa dos perfis próprios11/11. As pendências de acesso/instalação descritas abaixo são históricas. + +11/11/2026. Directiva do utilizador incorporada: **Recursos Humanos é um módulo autónomo**, com identidade, menu e perfis diferentes dos Financeiros. Ainda não está disponibilizado na8085. O código local foi preparado e compilado em privado; as permissões próprias aguardam a [decisão concreta](SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md). + +## Preparado + +- Serviço em `app/Modules/HumanResources`, controlador e rotas com proprietário `human_resources`; gates próprios, namespace `human_resources.remunerations.*` e registo de perfis separado por SHA. Sem IDs13/14 fixos no motor nem ampliação da matriz aceite dos dez perfis antigos. +- Ecrã RH próprio: cadastro fictício, regras versionadas, processamento, filtros/SQL25/100, escolha explícita de fontes/registo, prévia e fundamento, confirmação/recuperação, histórico, detalhe e descarga autenticada com SHA. O módulo não oferece os cadastros/rascunhos comerciais partilhados nem herda poderes Financeiros. +- Preparação/revisão distintas por registo/versão, validação de pessoa/contexto/dependências, idempotência e persistência transaccional das versões/recibos/auditoria. Valores racionais, truncagem por rubrica e diferenças exactas; parâmetros ausentes não viram zero. Resultado individual revisto/exportação é conservado; resumo da lista refere só aprovados activos da página. +- Exemplo artificial10000 bruto,300+800 descontos,400 encargo patronal,8900 líquido interno e10400 custo. Não representa cálculo legal de IRPS/INSS nem salário pago. Nenhuma dívida, pagamento ou lançamento é criado. + +## Acessos e estado efectivo + +A aprovação inicial autorizava sete concessões por livro aos Financeiros13–17. Foram aplicadas nos dois livros, mas a directiva posterior determinou perfis distintos. Essas14 concessões novas foram retiradas com auditoria; nenhum cadastro/regra/processamento tinha sido conservado na8085. Não se alteraram os acessos anteriores. + +O novo instalador de perfis falha sem a decisão explícita da nova matriz. A proposta é de **três perfis e11 concessões por livro existente**, seis perfis/22 concessões no total:12 comuns de contexto e10 específicas de RH. Nenhuma dessas concessões foi aplicada; nenhuma identidade RH foi criada. Não se reutilizam os nomes antigos `remunerations.*`. A extensão inicial da restrição de nomes admissíveis permanece sem grants; não constitui acesso. Não foram concedidos direitos PostgreSQL/PUBLIC. + +Apenas as três tabelas técnicas `rm_meta/rm_versions/rm_commands` foram preparadas nos livros QA já existentes, sob a primeira autorização. As versões/recibos estão vazios. O bundle, rotas e serviço novos **não foram copiados para a8085**; não houve activação, cache clear ou reinício. + +## Evidências e trabalho por concluir + +[Índice das provas](proofs/sgf-human-resources-preparation-20261111.json):16 verificações de handlers reais com API simulada (incluindo503 e repetição do mesmo corpo/chave),11 verificações de cálculo/ausência de poderes em cada livro e65 de preservação.56 conjuntos originais e três bindings idênticos,27 lançamentos e todos os dados anteriores preservados. Seis registos e cache não foram alvo de comandos. Compilação privada e sintaxe PHP aprovadas; renderização simulada não é revisão visual. + +As40 verificações funcionais iniciais foram anteriores à separação de RH, com casos/versões/recibos revertidos na regressão. Conservam-se como prova do núcleo inicial; **não são contadas como validação da nova fronteira de perfis ou do percurso HTTP RH**. + +Após autorização dos perfis: criar apenas as identidades/grants discriminadas; testar o percurso completo com a identidade RH efectiva, dependências/vigências/cadastro suspenso, isolamento, filtros/paginação, concorrência observada/pós-libertação e recuperação de falha parcial; disponibilizar limitadamente e confirmar o exemplo efectivo na mesma8085, preservando os anteriores. Estes testes positivos/concorrência/HTTP RH ainda estão pendentes; o código preparado não é declarado entrega funcional concluída. + +Contratos/alterações contratuais, assiduidade/turnos/férias/ausências, benefícios/regimes reais, cálculo legal IRPS/INSS/modelos oficiais e ligação salarial a dívida/pagamento/Contabilidade continuam lacunas de produto. Este bloco não conclui Recursos Humanos completo. Visual, carga sustentada e capacidade de implantação permanecem separados. Sem novas demonstrações, bases/portas, SGC, publicação, candidata de Compras, aceitação ou checkpoint. diff --git a/docs/SGF_IVA_ASSESSMENT_DELIVERY.md b/docs/SGF_IVA_ASSESSMENT_DELIVERY.md new file mode 100644 index 0000000..532e8b7 --- /dev/null +++ b/docs/SGF_IVA_ASSESSMENT_DELIVERY.md @@ -0,0 +1,46 @@ +# Livro e mapa interno de IVA QA — entrega na8085 + +10/11/2026. `SGF-IVA-ASSESSMENT-FORM-QA-v1`, [âmbito aprovado](SGF_IVA_ASSESSMENT_NEXT_SCOPE.md). Implementação funcional no mesmo SGF, sem SGC ou nova demonstração. Entrada: **Contabilidade → Livro e mapa interno de IVA QA → Abrir livro, mapa e histórico**, ou `/fiscal/iva` após escolha da função autorizada. + +## Percurso conservado + +O preparador Financeiro13 escolheu explicitamente a factura-recibo fictícia já emitida, base862,07/IVA documental137,93/total1000,00 MZN, período2026-11. O revisor14 conferiu a mesma versão do livro. O preparador13 gerou o mapa interno da revisão exacta; o revisor14 confirmou-o. **Quatro versões fiscais novas** conservadas na8085: livro preparado/revisto e mapa preparado/revisto. A classificação é de saída QA, não determina o regime legal da empresa. + +Exportação CSV congelada e verificável por SHA256, com referência FR, base/IVA/total, regra/perfil/período/SHA do documento e ID do livro revisto. **IVA dedutível e imposto a pagar indeterminados**, nunca zero por falta de aquisições/deduções. Não é apuramento integral da empresa, Modelo A oficial ou transmissão às autoridades. A factura técnica subjacente não entra como segunda origem; nenhuma outra factura/crédito foi classificada automaticamente. + +O mesmo documento/PDF, recebível, recebimento, afectação e diário foram usados por leitura, sem recriar ou recalcular o facto emitido. Não há lançamento fiscal novo. Diário27, Caixa fechada950 e período contabilístico QA aberto preservados. + +## Histórico, conferência e recuperação + +`fx_versions` conserva estados, predecessor, preparador/autor, data, fundamento, fotografia fiscal e dependências; `fx_commands` conserva pedido único, hash e resultado com estado anterior/posterior. Trigger impede escrita sem direito, salto de versão, predecessor diferente, revisão própria e alteração/remoção da história. SQL limitado às bases QA por guarda física existente; PUBLIC revogado. + +Prévia em savepoint revertido mostra impacto, valores/pendências e ausência de lançamento/transmissão. Confirmação individual revalida contexto, direitos do executor e preparador, origem congelada/SHA/associação Comercial, versão documental, correcções/reversões, período aberto e revisão do livro. A prévia cifrada expira600s. O bloqueio da empresa é o mesmo usado pela composição integrada; não se cria writer financeiro ou transacção distribuída com efeitos no livro nativo. + +Repreparação produz sucessor; mapa anterior passa a desactualizado e a exportação exige nova conferência. Recibo histórico por chave própria pode ser recuperado noutra sessão sem executar novamente a decisão. Repetição da confirmação já concluída devolve resultado histórico identificado; nova execução com dados diferentes sob a mesma chave é recusada. Recuperar recibo não torna uma versão antiga actual. + +Filtros livro/mapa e páginas25/100 no servidor; não se carrega todo o histórico. Fonte autorizada escolhida na interface, sem primeiro documento preseleccionado. Paginar conserva evidência/fundamento não submetidos; prévia/confirmação protege campos e histórico. Detalhe mostra a versão escolhida. Sem direitos de escrita, o formulário fica desactivado. Não se presume disponibilidade se a origem estiver bloqueada. + +## Direitos aplicados + +Seis nomes/14 concessões por cada uma das **duas bases QA existentes**,28 no total, exactamente [como aprovado](integration/sgf-iva-assessment-access-proposal.json). Preparação13, revisão14, consultas13–17; sem novo utilizador/PGrole/acesso real. Direitos separados dos perfis de serviço e da emissão documental. Configuração root desligada; disponibilização apenas na8085 com binding privado por hash. + +## Provas dirigidas + +|Prova|Resultado| +|---|---| +|38 regressão, rollback integral|Origem FR exacta, prévia sem persistência, desconhecido≠zero, actor/origem/período/campos indevidos, versão antiga, revisão distinta, recuperação, mapa não revisto/desactualizado, revogação do preparador, contexto/expiração, falha após evento com rollback e recuperação, origem incompatível e domínios antigos intactos.| +|Concorrência na regressão|Primeira revisão mantém lock da empresa; segunda recebe503/Retry-After5 após cerca5s. Após libertação:409 para versão antiga, uma revisão válida e primeira chave recuperada sem duplicação.| +|10 fronteiras/SQL/paginação, rollback|128 versões de metadados para percorrer25/100, filtros, tenant override, páginas inválidas, histórico imutável, writer sem direito, salto de versão e preparador indevido. Fixtures não representam novas facturas/declarações/lançamentos.| +|23 HTTP na8085|Quatro confirmações explícitas e respectivas repetições sem duplicação, revisão distinta, estados/bloqueios, CSV/checksum/valores, recibo noutra sessão, isolamento e ecrã acessível. Progresso guardado para não repetir decisões válidas.| +|14 DOM→HTTP|Consulta, fonte inicialmente vazia, preservação do formulário ao paginar, prévia real revertida, impacto/bloqueio de controlos, cancelamento e recuperação/histórico. **Zero POSTs de confirmação**, quatro versões da instalação intactas. Não é navegador.| +|Preservação SELECT-only|15 tabelas originais completas comparadas, incluindo documentos/PDF, fiscalidade, recebíveis, Caixa, clientes, Stock, diário e períodos. Linhas de concessões anteriores idênticas; apenas14 adições aprovadas. Binding original intacto.| + +[Índice verificável](proofs/sgf-iva-assessment-20261110.json). Provas detalhadas/dumps privados fora do repositório; backups anteriores retidos. A tentativa inicial do harness de concorrência usou uma requisição GET num controlador POST e falhou na validação, não em carga/concorrência. Os dois eventos QA então preparados/revistos foram conservados na regressão, sem apagar evidências; a prova corrigida mantém mais dois, totalizando quatro eventos fiscais só nessa regressão. Apenas a execução corrigida conta como prova de concorrência. Não ocorreu na8085. + +Não foram repetidos percursos de Compras, emissão, recebimentos, Caixa ou contabilidade. Seis registos/cache/trabalho alheio intactos; sem novas bases/portas/demos, índices no destino, SGC, operações/direitos reais, publicação, candidata, aceitação/checkpoint ou capacidade de implantação. + +## Limitações restantes + +Uma origem FR e período QA, apenas saídas e mapa interno. IVA operacional geral, deduções/regularizações, múltiplos tratamentos, restantes impostos/remunerações, modelos oficiais, reconciliação bancária e análise financeira ampla **continuam pendentes**. O inventário de conhecimento não equivale a motores implementados ou vigência legal verificada. Não se reabre período, não se redistribui imposto de créditos nominais nem se altera documento emitido. + +Revisão visual permanece bloqueada e não foi repetida. Carga sustentada, retenção/órfãos, arranque completo e implantação exigem evidência própria. As provas actuais não validam uso real, representação legal ou dimensão geral de empresa. diff --git a/docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md b/docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md new file mode 100644 index 0000000..8d1969b --- /dev/null +++ b/docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md @@ -0,0 +1,34 @@ +# Proposta de autorização — livro e mapa interno de IVA QA + +Estado: **autorizado expressamente e entregue**; ver `SGF_IVA_ASSESSMENT_DELIVERY.md`. A proposta abaixo conserva o âmbito aprovado. `SGF-IVA-ASSESSMENT-FORM-QA-v1`,10/11/2026. Esta proposta resulta da comparação do documento «Incorporacao de impostos e analise ao SGF.docx» com o código e o acervo já inventariado na secção10/11 de `SERVICE_INVOICE_CALCULATION_SPEC.md`. + +## Entrega completa proposta + +Na mesma8085 e regressão existente: escolher explicitamente a factura-recibo QA1000 já emitida no bloco documental (base862,07/imposto documental137,93), fixar documento/perfil/regra/versão/período e preparar uma classificação fiscal de ensaio. Outra pessoa confere a mesma versão. O livro de saídas e um **mapa interno de IVA QA** mostram o valor e a origem, com histórico, decisão e representação recuperáveis. Não se recalcula nem altera o documento emitido, recebível, afectação, Caixa ou diário. + +O resultado é expressamente relativo à origem QA seleccionada: base862,07 e imposto137,93. Não é apuramento completo da empresa ou montante legal a pagar. Dedução, aquisições ou outras origens não classificadas permanecem pendentes/indeterminadas; ausência de informação não se transforma em dedução zero. Sem cópia da factura técnica subjacente à factura-recibo, nem classificação automática das restantes facturas/créditos. Crédito nominal sem decomposição fiscal comprovada continua pendência, sem repartição proporcional presumida. + +Estados separados: preparação → revisão distinta do livro → preparação do mapa a partir da versão revista → revisão distinta do mapa → consulta/exportação interna identificada como ensaio. Sem transmissão às autoridades, comprovativo de entrega oficial inventado ou formulário rotulado como modelo oficial validado. Reutilizar funções/fontes existentes quando compatíveis, preservando a política monetária e sem depender do portal/SGC em execução. + +Prévia apresenta origens, tratamento, montantes, pendências e impacto. Confirmação revalida pessoa/direitos/empresa/unidade/contexto, documento e decisão originais, versões, hash e estado do período QA. Correcção gera sucessor, não altera história. Um mapa antigo fica identificado como desactualizado quando a origem ou a revisão muda. Pedidos com a mesma chave recuperam o mesmo resultado, sem duas classificações/mapas. + +## Direitos e destinatários exactos + +Seis nomes novos, **14 concessões por livro QA existente**,28 no conjunto8085/regressão, todos no tenant1/empresa2/unidade2: + +|Direito|Perfis QA existentes| +|---|---| +|`fiscal_assessments.view`|Financeiro preparação13/revisão14; Contabilidade preparação15/revisão16; consulta17| +|`fiscal_assessments.prepare`|Financeiro preparação13| +|`fiscal_assessments.review`|Financeiro revisão14| +|`fiscal_forms.view`|Financeiro preparação13/revisão14; Contabilidade preparação15/revisão16; consulta17| +|`fiscal_forms.prepare`|Financeiro preparação13| +|`fiscal_forms.review`|Financeiro revisão14| + +[Lista integral](integration/sgf-iva-assessment-access-proposal.json). Consulta do livro/mapa abrange a fotografia fiscal da origem QA seleccionada, não concede descarga ou consulta geral dos documentos de outros módulos. Reutiliza as consultas documentais já aprovadas dos preparador/revisor. Sem alterar a política dos perfis existentes, nomes de utilizadores, roles PostgreSQL ou direitos reais/PUBLIC. Estes seis direitos estão ausentes e não estão incluídos nas concessões anteriores de perfis fiscais ou Caixa; por isso precisam de decisão antes da aplicação. + +## Instalação e provas após autorização + +Persistência append-only e recibo de comando no host QA existente, sem duplicar documentos/livro contabilístico. Sem novas bases, portas ou demonstrações. Provar primeiro na regressão existente: selecção explícita, origem dupla factura/FR, autor/revisor, acessos indevidos e empresa/unidade, exactidão, desconhecido≠zero, referência desactualizada, concorrência, repetição e recuperação após falha. Ensaiar geração e consulta do mapa interno a partir da revisão exacta e invalidação após mudança. Só depois disponibilizar limitadamente na8085, conservando os27 lançamentos, Caixa fechada950, exemplos, seis registos e cache. Reutilizar as restantes provas válidas. + +Esta decisão autoriza o bloco inteiro, não pedidos de aprovação por campo/teste. Não abrange novos motores IRPS/IRPC/ISPC/ICE/INSS, cálculo de remunerações, preço IVA incluído ou múltiplos tratamentos, aquisição/dedução/regularizações reais, modelos oficiais, novos lançamentos, reabertura, dinheiro/direitos reais, publicação, candidata de Compras, aceitação/checkpoint ou capacidade de implantação. Essas lacunas permanecem explicitadas na matriz global, não são declaradas resolvidas pelo mapa QA. diff --git a/docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md b/docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md new file mode 100644 index 0000000..1edc76f --- /dev/null +++ b/docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md @@ -0,0 +1,34 @@ +# Cálculo QA por linhas e preço incluído — entrega local + +10/11/2026 · `SGF-MULTI-LINE-INCLUDED-QA-v1`. Política expressamente aprovada pelo utilizador. Código, ecrã e persistência auditada integrados na instalação8085 existente, em **Contabilidade → Regras fiscais versionadas QA** (`/fiscal/rules`). Sem direito, concessão, esquema ou demonstração novos. + +## Funcionalidade entregue + +Preparador13 escolhe explicitamente uma versão revista, data, moeda e modalidade de preço, informa1–25 linhas de referência/quantidade/preço e fundamento, confere impacto por linha/agregado e confirma um retrato recuperável por chave. A taxa vem da definição QA conferida no servidor; o cliente não a fornece. Ausência, conflito, vigência, moeda ou fonte incompatíveis deixam o cálculo pendente. A confirmação revalida contexto, autoridade e dependências actuais. Consulta17 lê resultado conservado, avaliação actual e mapa CSV com checksum; referências são escapadas contra fórmulas de folhas de cálculo. Não se escolhe automaticamente a regra ao abrir o histórico. + +A política `SGF-QA-LINE-TRUNCATE2-INCLUDED-RESIDUAL-v1` usa aritmética racional. Trunca o imposto de cada linha para duas casas; no preço incluído, base documental=bruto−imposto documental. Conserva exactos e as duas diferenças, somando os valores documentais das linhas. Base sem imposto ou bruto incluído que exijam fracções de cêntimo ficam pendentes, sem truncagem silenciosa. + +|Caso efectivamente conservado na8085, taxa artificial16%|Base documental|Imposto documental|Total| +|---|---:|---:|---:| +|Sem imposto,400+600|1000,00|160,00|1160,00| +|Incluído,464+696|1000,00|160,00|1160,00| +|Incluído,100|86,21|13,79|100,00| + +No último caso, base exacta2500/29, imposto exacto400/29, diferença base documental−exacta9/2900 e imposto exacto−documental9/2900. Existem agora **cinco cálculos**, incluindo os dois antigos byte a byte intactos; quatro eventos de regra sem alteração. Estes são cálculos/mapas internos, sem emissão de factura, dívida, pagamento ou lançamento. + +## Provas dirigidas e concorrência + +- 34 verificações de regressão com rollback: valores/modos, fracções/diferenças, soma por linha em vez de recálculo global, limites1/25/26, entradas indevidas, acesso, moeda/vigência, retirada de regra, histórico e preservação de dez tabelas. +- Três verificações de falha/recuperação: falha na gravação do recibo reverte também o cálculo; recuperar a mesma intenção não duplica. +- 29 verificações HTTP na8085: três confirmações guardadas, repetição das mesmas chaves, relatórios/checksums, quantidades exactas de registos, fonte actual e recusas de autoridade/contexto/campos. +- 16 verificações do cliente com encaminhamento ao HTTP real: uma prévia revertida, **zero confirmações**, edição de linhas, impacto exacto, bloqueio durante prévia, conservação do formulário ao paginar, histórico e recuperação. + +Total80 verificações, mais ensaio concorrente apenas na regressão. A segunda confirmação da **mesma chave** aguardou o mutex administrativo de empresa já ocupado pela primeira e atingiu o limite de espera: HTTP503, `Retry-After:5`, cerca5,17s. Após a primeira transacção libertar o bloqueio, a recuperação terminou HTTP200 com `replayed=true` e **um único cálculo**. Isto distingue-se das provas anteriores de decisões diferentes/versão alterada, que recusaram409. A retirada de regra entre prévia e confirmação também recusa409 neste incremento. Não foi alterado o limite nem removido o bloqueio. + +[Índice com SHA das provas e fontes](proofs/sgf-multi-line-included-20261110.json). As suites anteriores válidas foram reutilizadas, sem repetir operações financeiras ou reescrever os seus resultados históricos. O ensaio do cliente usa DOM simulado; não é revisão visual de navegador. + +## Preservação e limites + +Comparação de26 conjuntos de linhas originais e binding: intactos, incluindo os dois cálculos antigos e todas as concessões. Apenas três novos cálculos e respectivos recibos acrescentados. Diário27, Caixa fechada950, IVA4, regras4, bancos7, critérios4/resultados2 preservados. Sem comandos/conexões dirigidos aos seis registos protegidos ou à cache bloqueada; trabalho alheio preservado. Backup e dados privados permanecem fora do repositório. + +A configuração do repositório permanece desligada por omissão. Apenas a modalidade QA aprovada foi disponibilizada na8085 existente. Sem SGC, publicação, candidata de Compras, aceitação ou checkpoint. Taxas16%/17% artificiais; não comprovam vigência legal. Impostos/regimes diferentes por linha, isenções, créditos negativos, dedução, emissão documental por múltiplas linhas, modelos oficiais e fiscalidade operacional geral não estão entregues por este bloco. Revisão visual, carga sustentada e capacidade de implantação continuam pendentes com evidência própria. diff --git a/docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md b/docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md new file mode 100644 index 0000000..0100622 --- /dev/null +++ b/docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md @@ -0,0 +1,33 @@ +# Proposta de decisão — cálculo QA por linhas e imposto incluído + +`SGF-MULTI-LINE-INCLUDED-QA-v1`,10/11/2026. **Política aprovada e implementação entregue; ver [relatório](SGF_MULTI_LINE_INCLUDED_DELIVERY.md).** A descrição seguinte conserva a decisão proposta e os seus limites. Continuação dentro do catálogo de regras existente na8085; sem novos direitos ou concessões. O caso aprovado anterior calcula uma base por taxa artificial; não decidiu a representação documental do preço com imposto incluído. + +## Percurso funcional proposto + +No mesmo ecrã de regras fiscais QA: escolher explicitamente regra/versão/data/moeda, modalidade de preço e entre1 e25 linhas, com quantidade/preço em decimais exactos. Preparador13 vê impacto por linha e agregado, confirma o retrato de cálculo e recupera-o por chave/histórico/relatório interno. Usa apenas versões QA revistas14 já existentes e respectivos parâmetros, sem importar taxa do cliente ou escolher regra automaticamente. + +Não emite factura, reconhece dívida, cria pagamento, lançamento ou altera o livro/mapa IVA. É cálculo/mapa interno por linhas no mesmo núcleo de regras, sem demonstração/base/porta nova. Reutiliza `fiscal_rules.view/prepare/review` já aprovados; não há nova concessão nem activação fiscal legal. Os resultados antigos ficam intactos. + +## Política de representação que precisa de decisão + +- **Preço sem imposto:** base exacta quantidade×preço. Mantém a política anterior: base não representável em duas casas fica pendente, sem truncagem silenciosa. Imposto exacto por linha, imposto documental truncado para duas casas; total da linha =base documental+imposto documental. +- **Preço com imposto:** bruto exacto quantidade×preço tem de ser representável em duas casas, senão fica pendente. Base exacta =bruto/(1+taxa), imposto exacto =bruto−base exacta, ambos racionais preservados. **Proposta:** truncar o imposto por linha para duas casas e decompor a base documental como bruto−imposto documental. Preservar separadamente a diferença da base e a do imposto; não declarar base documental igual à exacta. +- Agregar **os montantes documentais das linhas**, sem recalcular um imposto de documento que substitua a soma. Preservar exactos/diferenças agregados. Regimes/taxas diferentes por linha, isenções, créditos negativos, dedução, classificação legal e modelos oficiais ficam fora deste bloco. + +Casos artificiais para conferir com regra QA16% vigente no seu intervalo (não afirmação legal): + +|Entrada|Base documental|Imposto documental|Total| +|---|---:|---:|---:| +|Sem imposto:400+600|1000,00|160,00|1160,00| +|Incluído:464+696|1000,00|160,00|1160,00| +|Incluído:100|86,21|13,79|100,00| + +No último caso, base exacta2500/29 e imposto exacto400/29. Diferença base documental−exacta =9/2900; diferença imposto exacto−documental =9/2900. São preservadas, não arredondadas para zero. [Protótipo matemático de revisão](../scripts/sgf-included-policy-preview.php) não abre aplicação/base nem altera dados. A hipótese operacional não comprova conformidade fiscal da representação. + +O caso artificial17% da sucessora mantém a vigência já aprovada, sem mudança legislativa presumida. A fonte/regra será conferida no servidor pelo mesmo contrato e algoritmo seguro; entradas não executam código/expressões livres. Ausência/conflito/versão antiga/data/moeda incompatível produzem pendência, sem taxa zero por omissão. + +## Provas e fronteiras + +Reutilizar provas válidas de regra/autoridade/source provider; acrescentar exactidão por linha/agregado, modos explícitos, diferenças racionais, quantidades/preços inválidos, limites1/25, corpo/campos indevidos, fonte/data/contexto/versão, concorrência/repetição/falha/recuperação e histórico sem reescrita. Primeiro regressão existente, depois casos completos na8085, sem repetir confirmações/emissões/recebimentos/lançamentos anteriores. + +Preservar27 lançamentos, Caixa950, IVA4, regras4/cálculos antigos2, bancos7, critérios financeiros4/resultados2, seis registos/cache/trabalho alheio. Sem novo direito, perfil, PGrole, esquema financeiro, efeito de negócio financeiro, SGC/portal, dados/acessos reais, taxa real, reabertura, publicação/candidata, checkpoint/aceitação ou capacidade de implantação. Interface/relatório identificam sempre ensaio e política exacta. Fiscalidade operacional geral, emissão por múltiplas linhas, outros impostos/modelos oficiais, visual/carga continuam separados. diff --git a/docs/SGF_NEXT_BLOCK_RIGHTS.md b/docs/SGF_NEXT_BLOCK_RIGHTS.md new file mode 100644 index 0000000..fd08fcf --- /dev/null +++ b/docs/SGF_NEXT_BLOCK_RIGHTS.md @@ -0,0 +1,49 @@ +# Próximo bloco de conclusão — decisão única de direitos QA + +Estado: **autorizado expressamente em10/11/2026 — «Autorizar o bloco QA e os direitos discriminados»; implementação em curso**. Não presumir concessão, execução ou aceitação antes das provas e do registo de aplicação. Segue a directiva executiva de continuar todas as funcionalidades em falta. Agenda/disponibilidade e cadastro/reutilização de equipamentos avançaram com direitos existentes; esta decisão não reabre essas entregas nem o circuito já concluído de18 lançamentos. + +## Bloqueio concreto + +O menu ainda identifica orçamentos comerciais/reparação, factura-recibo, recibo e nota de crédito como não implementados. Stock mínimo/políticas e devoluções físicas a fornecedores exigem funções nativas de fornecimentos/disposição que o perfil actual não possui. Não usar `billing_drafts.manage` (administração de acessos), a autorização técnica de tarefas ou `stock.view` como aprovação comercial, nota de crédito ou poder de saída física adicional. + +A regra do proprietário exige apresentar direitos novos antes de conceder acessos; as instruções locais exigem confirmação concreta de novas concessões. A directiva executiva permite implementar; não altera por si a lista de direitos dos perfis. A [matriz proposta](integration/sgf-remaining-functions-access-proposal.json) apresenta todos os destinatários e limites deste bloco para uma decisão agrupada. + +## Implementação abrangida pela decisão + +1. **Orçamentos de Comercial e de reparação:** mesmo motor versionado de proposta1000MZN e proposta de Oficina500MZN, linhas e origens explícitas, validade, preparação e revisão distintas, decisão do cliente com comprovativo, rejeição/caducidade/revisão preservadas. Conversão deliberada, única e auditada para os rascunhos existentes; sem aceitar automaticamente ou facturar por aceitar um orçamento. +2. **Documentos sintéticos adicionais:** PDF/teste de Recibo ligado à afectação efectiva400; Nota de crédito100 ligada a uma **nova** factura de teste1000; aplicação distinta no novo recebível (1000−400−100=500), sem alterar a factura original ou presumir devolução de dinheiro/stock. Factura-recibo1000 de outra origem nova, emissão e recebimento com decisões distintas e conclusão atómica, sem segundo documento/dívida. Usar exclusivamente o cálculo documental de teste já autorizado862,07+137,93=1000; não adoptar outra taxa/regra legal. Numeradores/séries QA próprios, referências, PDF/histórico, limites acumulados, anulação técnica/reversão ligada ao original em período QA aberto, relatórios e mesmo núcleo contabilístico. Não reabrir período ou alterar documentos antigos. +3. **Stock mínimo e políticas:** UI/read model sobre `SupplyPlanning` original; preparar e conferir parâmetros de artigo/armazém, mínimos e quantidades físicas/lotes de origem. Mostrar falta de configuração como desconhecida. Não transformar sugestão em compra automática ou duplicar reservas/orçamento; conversões para requisições exigem decisão própria no workflow existente. Usa direitos existentes dessa continuação; não concede funções adicionais de sugestão/reserva/importação implicitamente. +4. **Devolução física ao fornecedor:** `InventoryDispositions`/`StockWorkflow` originais, preparação, revisão distinta e execução por Armazém diferente do revisor. Artigo **novo**, recepção10UN com taxa QA20MZN, devolução2UN/40, saldo8UN/160; não consumir os artigos/exemplos actuais. Origem da recepção, elegibilidade acumulada, lote quando aplicável e comprovativo físico conferidos; a saída não reduz automaticamente dívida ou cria nota de crédito. Taxa/regras QA e reconhecimento da devolução no mesmo diário com conta compensatória própria, sem duplo consumo. Sem abate ou estorno de origens históricas por inferência. + +Todos os passos têm prévia, fundamento, versão/contexto/pessoa revalidados na confirmação, idempotência, recuperação após falha e provas de concorrência/isola­mento nas duas bases QA existentes. UI e disponibilização limitada na mesma8085 apenas depois das provas afectadas. Sem novas bases, portas, perfis, roles PostgreSQL ou demonstrações. + +## Direitos mínimos apresentados + +**15 nomes novos**, exclusivamente empresa2/unidade2 e perfis QA já existentes: + +| Família | Direitos | Destinatários | +|---|---|---| +| Orçamentos |`commercial_quotes.view/prepare/review/convert` |Comercial e Financeiro: preparação/conversão pelos preparadores; revisão pelos revisores; consulta adicional pelo perfil QA de consulta. | +| Documentos |`service_documents.view/prepare/review/issue/credit_apply` |Preparadores Comercial/Financeiro preparam; revisores conferem; Comercial prepara emissão e o revisor Financeiro pode finalizar a factura-recibo; só revisor Financeiro aplica crédito. QA consulta apenas lê. | +| Planeamento físico |`stock.planning.view/prepare/review` |Armazém consulta/prepara; conferência QA consulta/revê. Só novo artigo/armazém explicitamente mapeados. | +| Disposição física |`stock.disposition.view/prepare/review` |Armazém consulta/prepara; conferência QA consulta/revê; execução conserva `stock.operations.confirm`/warehouse já existentes e independência do revisor. | + +**Uma concessão de nome existente:** `service_invoices.issue` ao perfil QA Financeiro — revisão, exclusivamente nesta empresa/unidade, para a finalização documental da factura-recibo integrada. Não deriva da concessão de recebimentos nem é direito real. Preparação, revisão e execução mantêm as guardas originais e a nova proveniência seleccionada. + +**Seis nomes de funções nativas existentes, oito concessões QA:** actor6 recebe `supply_read`, `supply_prepare`, `inventory_disposition_read`, `inventory_disposition_prepare`; actor3 recebe `supply_read`, `supply_review`, `inventory_disposition_read`, `inventory_disposition_review`. Não recebe `warehouse` genérico. O livro continua o mesmo e a conta de leitura SELECT-only anterior continua nas três tabelas; o adaptador privilegiado revalida âmbito e funções, sem ampliar concessões desse leitor. + +Não há concessão administrativa geral ou por nome/email de pessoa. A matriz JSON é a lista exacta a instalar; nomes adicionais exigem outra apresentação, não substituição silenciosa. + +## Preservação e exclusões + +Preservar18 lançamentos e exemplos aceites, seis registos pendentes, cache bloqueada, origens/associações antigas, trabalho alheio e demonstrações anteriores. Só fontes fictícias novas neste bloco. Nada de operações/dinheiro/banco/fiscalidade reais, comunicação à AT, envio de mensagens, concessões reais/PUBLIC, salários/CIF reais, reabertura, instalação empresarial, publicação ou candidata de Compras. A aprovação não é aceitação do produto ou capacidade de implantação. + +Os modelos legais IVA/IRPS/IRPC/INSS, folha salarial, logística/importação real e regras fiscais vigentes não ficam certificados por estes documentos de teste. Mantêm validação específica das fontes e do âmbito. A revisão visual dos novos ecrãs também não é presumida a partir de DOM/HTTP. + +## Progresso após autorização + +Direitos da matriz exacta aplicados somente nas duas bases QA existentes. Orçamentos Comercial/Oficina e Stock/políticas/devolução entregues na8085; diário20, originais18 intactos. Documentos adicionais continuam candidatos, sem disponibilização. O motor original encontrou dez concessões de consulta ausentes nos dois perfis Financeiros; foram apresentadas separadamente em SGF_DOCUMENT_READ_ACCESS.md, sem concessão implícita. Essa dependência não transforma o bloco inteiro em concluído. + +## Fecho funcional do bloco autorizado + +Documentos adicionais e anulação técnica/reversão agora implementados e ligados na mesma8085; ver SGF_SERVICE_DOCUMENTS_DELIVERY.md. Consultas adicionais expressamente aprovadas e aplicadas; não há autorização pendente deste bloco. Diário25 equilibrado, saldos500/0, período aberto, originais18 intactos. Revisão visual, carga sustentada, conformidade real e implantação continuam âmbitos separados. diff --git a/docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md b/docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md new file mode 100644 index 0000000..2e6c02b --- /dev/null +++ b/docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md @@ -0,0 +1,40 @@ +# Informação operacional e consulta de ordens — SGF8085 + +10/11/2026. Entrega funcional na instalação8085 existente, sem SGC, nova demonstração, alteração de direitos ou criação de exemplos financeiros. A página inicial autenticada estava praticamente vazia; a entrada apresentava sobretudo a escolha de perfis. O trabalho existente ficava escondido e alguns textos descreviam capacidades entregues como futuras. + +## O que ficou disponível + +Em `/sgf`, cada um dos seis módulos apresenta agora o seu âmbito e uma entrada explícita para o respectivo início. Dentro da função escolhida, **Início** mostra: + +- O percurso do módulo em cinco etapas, a evidência a conservar, como interpretar a informação e a ligação ao módulo seguinte. +- Funções e atalhos já autorizados para a conta; mudar de módulo mantém a transição de contexto e a protecção de formulários existentes. A entrada de início não concede acesso às restantes funções. +- Consulta explícita das fontes existentes do módulo activo: documentos comerciais, requisições, existências, ordens/equipamentos, Caixa/indicadores ou lançamentos/cálculos fiscais internos. +- Referência, entidade, estado, valor/identificação e evidência por registo, com ligação à consulta completa e histórico. Páginas até25 registos,15 nas requisições. Não carrega outros módulos em segundo plano. +- Saldo950MZN e estado fechado da Caixa QA obtidos da fonte existente, sem somar eventos de preparação/revisão. Os restantes valores são individuais; não há total inventado entre documentos ligados, unidades ou moedas diferentes. + +Escolher uma fonte executa apenas GET nos leitores já autorizados. Sem fonte ou permissão não há consulta; falha de leitura não significa zero. A mudança de contexto descarta respostas antigas. O menu horizontal/dropdowns aceite permanece. Os filtros de consulta são identificados como leitura e não disparam o pedido de guardar alterações de negócio; não limpam alterações de outros editores nem confirmações pendentes. + +## Nova consulta de ordens de Oficina + +Oficinas → **Painel da oficina**, **Equipa** ou **Relatórios** abre uma lista SQL paginada25/100, com pesquisa literal por referência, cliente, equipamento, série ou responsável e filtro pelos estados já existentes dos apontamentos. Mostra cliente por nome, equipamento/série, responsável actual, quantidade de apontamentos actuais/sem verificação favorável e minutos apontados/verificados. Cada linha abre a ordem e as suas evidências originais. + +O leitor antigo percorria todas as ordens/apontamentos, fazia consultas repetidas e adquiria o bloqueio de escrita da empresa durante a consulta. O novo endpoint `/api/workshop/orders` aplica empresa/unidade e atribuição actual na SQL, agrega os apontamentos apenas das ordens da página e usa transacção de leitura consistente sem adquirir esse mutex. Reutiliza `workshop.view` e a política original: gestores com open/assign/authorize/verify vêem o âmbito autorizado; técnicos vêem apenas ordens actualmente atribuídas a si. Nenhum writer ou trigger foi alterado. + +Estados preservados: Aberta; Apontamentos em revisão; Ocorrências fora do âmbito; Registos verificados. Não inventa fases “Entregue”, “Paga” ou “Reparação concluída”. “Sem verificação favorável” inclui pendentes, recusas e reconhecimentos de excepção; não é uma fila exclusivamente por decidir. Versões substituídas não contam como apontamentos actuais. + +## Verificação e preservação + +- Início:50 verificações HTTP das fontes existentes,28 de renderização/autoridade/valores/escapagem e11 da entrada/cliente servido; três checksums HTTP finais dos ficheiros instalados. Provas válidas anteriores reutilizadas; as verificações26 de renderização iniciais foram substituídas pelas28 que abrangem a nova fonte de ordens, não somadas. +- Ordens:29 verificações na regressão existente,11 HTTP8085 e12 de renderização/protecção de formulários com dados efectivos. As27 ordens adicionais e apontamentos são fixtures SQL sujeitas às guardas originais, integralmente revertidas; não foram criadas ordens na8085. Estados e minutos anteriores comparados com o leitor original apenas na regressão. Filtros,25+2/100, ordenação, versões substituídas, atribuição e recusas verificados. +- Concorrência de leitura apenas na regressão: outra transacção manteve a linha da empresa bloqueada para escrita; a nova leitura terminou antes da libertação, em cerca0,093s dentro do leitor (não inclui arranque PHP). Isto comprova essa leitura sem espera pelo mutex, não a carga sustentada ou o desempenho global. +-32 conjuntos de dados originais comparados antes/depois da instalação, incluindo permissões, ordens/apontamentos/decisões/eventos, documentos e livro nativo: intactos, tal como o binding. Mantêm-se27 lançamentos, Caixa950 e cinco cálculos fiscais, sem nova operação financeira. Assets anteriores conservados; início e lista de ordens carregados separadamente. Nenhuma limpeza de cache. + +[Índice das provas e SHA das fontes](proofs/sgf-operational-information-20261110.json). Backups dos ficheiros substituídos e dados de ensaio permanecem privados, fora do repositório. Sem ligação/comando dirigido aos seis registos protegidos. Sem concessões, publicação, candidata de Compras, checkpoint ou aceitação presumida. + +## O que continua em falta + +Esta entrega melhora informação utilizável e fecha a consulta paginada de ordens; não conclui todo o produto. A [matriz](integration/sgf-solution-contract.json) mantém fiscalidade geral, motores IRPS/INSS/IRPC/ISPC/ICE, modelos oficiais/calendário, reconciliação bancária geral e rácios/séries gerais como pendentes ou parciais. + +O detalhe e os comandos originais da ordem continuam no leitor/workflow anterior; a sua materialização integral não foi optimizada por esta lista. A consulta de relatórios de Oficina é actual, não um relatório diário fechado, arquivo histórico ou exportação financeira. Abrir uma ordem continua a exigir nova autoridade e estado actual. + +Renderização em teste e respostas HTTP não são conferência visual de navegador. A via bloqueada não foi repetida. Revisão visual do conjunto, carga sustentada, arranque completo do computador, retenção de anexos e capacidade de implantação exigem provas próprias. A matriz não declara `client_presentable=true` nem produto completo. diff --git a/docs/SGF_PAYABLE_AGING_DELIVERY.md b/docs/SGF_PAYABLE_AGING_DELIVERY.md new file mode 100644 index 0000000..3cd063e --- /dev/null +++ b/docs/SGF_PAYABLE_AGING_DELIVERY.md @@ -0,0 +1,22 @@ +# Antiguidade de fornecedores — entrega8085 + +10/11/2026. Financeiro → **Mapa de antiguidade — fornecedores**, na mesma8085, com o direito existente `supplier_payables.view`. Consulta read-only; nenhuma nova autoridade, estrutura, condição, obrigação ou operação financeira. + +Escolher explicitamente uma referência, pesquisar factura/fornecedor, filtrar estado documental e consultar páginas25/100. O servidor conserva saldos originais e lê em conjunto as condições administrativas aprovadas das obrigações da página. Só acrescenta classificação: data revista ainda não ultrapassada,1–30,31–60,61–90, mais de90dias ou vencimento desconhecido. Reconhecimento pendente, saldo indeterminado e saldo liquidado no livro permanecem estados próprios; não são dívida vencida presumida. Data futura ou o próprio vencimento dão0dias; desconhecido mantém dias indeterminados. + +O vencimento e atraso originais do motor continuam separados da condição administrativa e das faixas. A data é referência de classificação de **saldos actuais**, não corte histórico, prazo legal ou recomendação de pagamento. Totais pertencem **apenas à página** e separam moedas/faixas. Desconhecido com saldo conhecido fica numa faixa monetária distinta; fonte divergente/saldo indeterminado, documento não reconhecido e saldo zero não entram nos totais por faixas. Zero no livro não comprova execução bancária. + +O CSV da página conserva referência, filtros, instante de leitura, saldo, vencimento original, condição/evidência revista, faixa e dias administrativos, com protecção dos textos contra fórmulas. Mudar referência exige nova leitura; uma resposta de referência diferente é recusada no cliente. Abrir condição/histórico ou obrigação mantém o documento escolhido e os mecanismos anteriores. + +Na8085, referência15/12/2026 classifica o saldo original100MZN na faixa1–30dias:15dias desde o prazo administrativo QA30/11. Referência30/11 dá data não ultrapassada. Nenhum prazo ou saldo foi alterado para obter o resultado. + +## Provas e limites + +-26 verificações de regressão: limites0/1/30/31/60/61/90/91dias, futuro/desconhecido/indeterminado/não reconhecido/liquidado, referência ausente/inválida, fonte efectiva25/100, preservação dos montantes/datas originais e cinco conjuntos nativos intactos. Sem fixtures financeiras ou comandos. +-12 de apresentação/CSV/contrato: referência obrigatória no mapa, ausência de indicadores antes da consulta, soma exacta de centavos, moeda e desconhecido separados, exclusões explícitas, resposta atrasada/categoria inválida recusadas e origem versus condição distintas. Renderização simulada não é revisão visual. +-14 HTTP8085: valor efectivo100MZN/15dias, dia do prazo, ausência de classificação automática, pesquisa/paginação, vazio, argumentos/contexto recusados, CSV e MIME/SHA dos ficheiros JS efectivamente servidos. Zero POSTs de negócio; entrada normal nos perfis conserva o seu registo de contexto. +-51 conjuntos actuais idênticos após instalação/consulta, incluindo as quatro versões e recibos de obrigações fiscais recém-entregues, permissões, condições de fornecedor, documentos, artigos e diário; binding intacto. Seis registos e cache não foram alvo de comandos. + +São52 verificações dirigidas. [Índice das provas](proofs/sgf-payable-aging-20261110.json). Provas anteriores reutilizadas, sem repetir reconhecimento, pagamento, revisão de vencimentos ou entrega fiscal simulada. Assets anteriores e ficheiros substituídos conservados privadamente; sem limpeza de cache, novas demonstrações, SGC, publicação, candidata de Compras, aceitação ou checkpoint. + +A SQL continua a paginar documentos antes do motor; a classificação e os totais usam só a página. Não há totais monetários globais nem filtro global por faixa nesta entrega. Os mutex/qualificação de origens do leitor financeiro original permanecem; sem alegação nova de desempenho, carga sustentada ou capacidade de implantação. Revisão visual e fiscalidade/relatórios legais continuam pendentes; matriz v21 mantém produto amplo incompleto. diff --git a/docs/SGF_PAYABLE_DUE_DELIVERY.md b/docs/SGF_PAYABLE_DUE_DELIVERY.md new file mode 100644 index 0000000..a68ed9d --- /dev/null +++ b/docs/SGF_PAYABLE_DUE_DELIVERY.md @@ -0,0 +1,39 @@ +# Vencimentos administrativos de fornecedores — entrega8085 + +10/11/2026 · `SGF-PAYABLE-DUE-QA-v1`. Percurso autorizado concluído na mesma8085: **Financeiro → Posições de fornecedores → Condições de vencimento e histórico**. A referência de atraso é escolhida explicitamente; não se presume a data nem uma condição de30 dias. + +Preparação individual13 e revisão distinta14, com evidência, fundamento, prévia anterior/posterior, aprovação/recusa, versões append-only e recibo recuperável. Uma sucessora pendente/recusada não substitui a última condição aprovada. A correcção pode declarar explicitamente o vencimento desconhecido. A lista lê em conjunto as condições aprovadas das obrigações da página e separa-as da data original; o CSV da página inclui a condição revista e a evidência protegida contra fórmulas. + +O saldo continua no motor financeiro original. `pa_obligations.due_on`, factura, principal, pagamentos, stock e diário não são modificados. A condição administrativa não é uma alteração fiscal da factura nem uma decisão de pagar. O vencimento original continua desconhecido; a referência só classifica atraso da condição revista, sem saldo histórico, juros ou execução monetária. + +## Exemplo existente e direitos + +Obrigação já reconhecida100MZN: evidência **artificial, identificada como ensaio**, data30/11/2026, preparada13 e aprovada14. Duas versões e dois recibos; não foi repetido reconhecimento de dívida.30/11 dá0 dias,01/12 dá1 dia. O saldo conserva100MZN. Este prazo não representa condição comercial real. + +Aplicados exactamente os dois direitos aprovados: `supplier_payables.due.prepare` a13 e `supplier_payables.due.review` a14, tenant1/empresa2/unidade2. São duas concessões por livro QA existente, **quatro concessões no total**, sem nova pessoa/perfil, role PostgreSQL, acesso real ou direito PUBLIC. Consulta continua a exigir `supplier_payables.view`. As novas tabelas/sequências/função negam acesso PUBLIC; nenhuma concessão SQL adicional. + +## Revalidação, recuperação e interface + +Na confirmação, o percurso confere novamente perfil/direito/contexto físico, origem autorizada, versão e saldo observados, preparação/revisão distintas e identidade humana autoritativa em `pa_access`. Duas contas da mesma pessoa são recusadas; mudança de pessoa exige nova conferência. As preparações novas conservam essa identidade no dado administrativo interno. As duas versões iniciais8085, anteriores a esse reforço, conservam os autores13/14 e4/7 nativos distintos; não foram reescritas. A prova dirigida confirma as identidades humanas distintas dos perfis e as recusas em caso de alias/mudança. + +Um recibo histórico exige o autor/contexto/origem actuais e não executa outra decisão. Resultado incerto na interface conserva corpo/chave/prévia e bloqueia descarte ou troca de documento até à recuperação. Durante edição, voltar ou mudar a consulta exige descarte explícito. Revisão começa sem decisão seleccionada e fundamento vazio; não se copia o fundamento anterior. + +As versões, auditoria e recibo são atómicos na mesma transacção nativa. A prévia usa savepoint revertido; sequências podem avançar sem criar versões. A leitura reutiliza os mutex e as guardas do mecanismo financeiro composto. Nesta entrega não se alterou a sua estratégia de bloqueio. + +## Provas reutilizáveis + +-40 verificações de regressão, repetidas somente após o reforço de identidade afectar a confirmação: preparação/revisão/recusa/correcção, datas impossíveis, versões/contexto/origem alterados, revogação de direito, recibo, repetição e falha forçada na inserção do recibo com rollback/recuperação. Original de40 conservado como histórico; contam-se40, não80. Fixtures integralmente revertidas e conjunto original comparado. +-Nove verificações dirigidas de identidade: mesma pessoa por contas diferentes, pessoa do preparador/revisor alterada, confirmação legítima e repetição; fixtures revertidas. +-Quatro de paginação efectiva:29 linhas SQL adicionais de histórico, páginas25+restante iguais à100, sem omissão/duplicação. São fixtures de leitura, não decisões executadas, e foram revertidas. +-Seis de integração da lista: condição aprovada exacta, intenção pendente sem substituição, data original intacta, página vazia, flag desligada e pin incompatível recusado503. Fixtures revertidas. +-23 HTTP8085: percurso efectivo13→14, exemplo, recibo próprio/nova sessão, repetição, datas, paginação e recusas.10 verificações finais sem comandos adicionais cobrem a lista/CSV/detalhe reforçado, recibo anterior preservado e ficheiros JS reais com MIME/SHA. Contagem dos dois comandos confirmados guardada privadamente para evitar repetição acidental. +-21 de interface, executando os handlers reais do componente com servidor simulado: escolha explícita, impacto, edição bloqueada, desconhecido, resposta incerta e recuperação da mesma operação. Renderização/handlers simulados não constituem revisão visual de navegador. +-Concorrência apenas na regressão: a primeira confirmação conserva o `FOR UPDATE` da empresa. A segunda fica à espera de `transactionid`, com `pg_blocking_pids` apontando para a primeira;503 após5,12s. Libertada a primeira, a segunda recusa409 por versão antiga. A primeira repete200 com recibo, conservando uma preparação/uma revisão. As duas versões desta fixture permanente ficam identificadas no livro de regressão; não na8085. O limite não valida capacidade de implantação nem foi eliminado nesta entrega. + +São113 verificações funcionais/contrato distintas (40+9+4+6+23+10+21), mais concorrência e58 verificações de preservação/ACL. [Índice das provas](proofs/sgf-payable-due-20261110.json). Suites alheias, antigas emissões/pagamentos/lançamentos e cadastro de artigos não foram repetidos. + +## Preservação e limitações + +Backup privado anterior.43 conjuntos originais idênticos; nos outros dois, todas as linhas anteriores de permissões/auditoria iguais, apenas duas concessões e dois eventos de vencimentos novos. Três bindings intactos, diário27, Caixa950, cálculos5, artigos e movimentos anteriores conservados. Nenhum comando dirigido aos seis registos protegidos ou à cache bloqueada. Dados/credenciais/dumps privados permanecem fora do repositório. + +Estruturas e configuração habilitadas apenas nas QA existentes; configuração do código principal desligada por omissão. Sem novas bases/portas/demonstrações, SGC, efeitos financeiros, acessos/operações reais, publicação, candidata de Compras, aceitação ou checkpoint. O percurso não administra contratos gerais, parcelas, juros, agenda de pagamentos ou múltiplas empresas reais. Não foram ensaiados milhares de condições, paginação sob escrita contínua ou arquivo binário da evidência; a evidência aqui é referência textual artificial. Revisão visual, carga sustentada, fiscalidade geral e capacidade de implantação continuam separadas. Matriz v18 mantém o produto amplo incompleto. diff --git a/docs/SGF_PAYABLE_DUE_SCOPE.md b/docs/SGF_PAYABLE_DUE_SCOPE.md new file mode 100644 index 0000000..db82e14 --- /dev/null +++ b/docs/SGF_PAYABLE_DUE_SCOPE.md @@ -0,0 +1,28 @@ +# Decisão agrupada — vencimentos revistos de fornecedores + +10/11/2026 · `SGF-PAYABLE-DUE-QA-v1`. **Autorizado expressamente e entregue; ver [relatório](SGF_PAYABLE_DUE_DELIVERY.md).** O texto seguinte conserva o âmbito da decisão. Consulta actual entregue em `SGF_PAYABLE_POSITIONS_DELIVERY.md`. Conferência SELECT-only dos dois livros existentes encontrou os direitos de leitura de13/14/17 e nenhuma concessão dos dois poderes abaixo. O motor original cria vencimento nulo e não oferece um comando para definir/corrigir condições de pagamento; acesso a pagamentos ou consulta não será usado para contornar essa ausência. + +## Percurso inteiro proposto + +Na mesma8085, Financeiro → Posições de fornecedores → obrigação individual: consultar condições/histórico, preparar data explícita ou retorno fundamentado a desconhecido, indicar referência de evidência e fundamento; outra pessoa compara o anterior/proposto e aprova ou recusa. A data é metadata administrativa revista, separada da factura e dos valores do livro original. Não escolher condições por fornecedor, presumir30 dias ou propagar a outras obrigações. + +Versões e recibos append-only no livro nativo existente, sem actualizar `pa_obligations.due_on`, factura, saldo, movimentos ou diário. A fonte monetária continua no motor original. A interface distinguirá vencimento original desconhecido de condição administrativa revista. Sem juros, alteração de principal, reserva de pagamento, calendário automático ou decisão de pagar. + +Reutilizar contexto físico, empresa/unidade, perfis QA existentes, prévia reversível/confirmar, identidade humana distinta, operação/chave/recibo, histórico e isolamento administrativos. Na confirmação revalidar permissões, perfil, binding, origem, revisão e saldo observado, versão de condição e preparador/revisor distintos. Mudança exige nova conferência; recusa e falha não aplicam parcialmente a condição. Repetição recupera recibo após revalidar contexto, sem duplicação. A consulta pode mostrar classificação de atraso por **referência explícita**, com desconhecido nulo e mesmo dia sem atraso; a referência não reconstitui saldo histórico. Condições antigas e recusadas permanecem no histórico. + +## Direitos propostos e alcance + +Tenant1/empresa2/unidade2, apenas nas bases QA8085 e regressão **já existentes**: + +|Direito SGF novo|Única função QA a receber|Poder| +|---|---|---| +|`supplier_payables.due.prepare`|Financeiro preparação13|Preparar condição individual e sucessoras; não aprovar.| +|`supplier_payables.due.review`|Financeiro revisão14|Aprovar/recusar versão alheia actual; não preparar.| + +Consulta reutiliza `supplier_payables.view` de13/14/17. Duas concessões por base, quatro no total. Sem nova pessoa/perfil, role PostgreSQL, direito PUBLIC, grant SQL ou acesso real. A criação das estruturas QA limitadas não amplia o writer monetário original. A matriz de acesso discriminada está em `integration/sgf-payable-due-access-proposal.json`. + +## Provas e exemplo delimitado + +Regressão existente: preparação/revisão e recusa, acesso indevido e empresa/unidade/module errado, revisão própria, origem/versão/saldo alterado, concorrência, repetição/recuperação e rollback sem dupla aplicação. Usar fixtures revertidas onde possível e preservar provas válidas, sem repetir suites alheias. HTTP8085 verifica o percurso efectivo individual, histórico e recibo, com a obrigação100MZN já reconhecida e evidência **artificial explicitamente identificada**: preparação13/revisão14 de vencimento30/11/2026. Consultar30/11 sem atraso,01/12 com1dia e versão anterior desconhecida sem0 presumido. Não declarar esse prazo como condição comercial real nem alterar100MZN. Correcção para desconhecido/recusa são ensaiadas só na regressão para conservar o exemplo escolhido. + +Uma única decisão cobre a implementação e as provas de todo este percurso, sem pedir autorização por campo/teste. Preservar27 lançamentos, Caixa950, artigos/catálogo/movimentos, documentos/posições/cálculos, seis registos, cache e trabalho alheio. Sem novas bases/portas/demonstrações, SGC, pagamento, efeito contabilístico, alteração de origens aceites, reabertura, instalação real, publicação, candidata de Compras, checkpoint ou aceitação presumida. Revisão visual/carga/implantação e fiscalidade geral permanecem separadas. diff --git a/docs/SGF_PAYABLE_POSITIONS_DELIVERY.md b/docs/SGF_PAYABLE_POSITIONS_DELIVERY.md new file mode 100644 index 0000000..4f7ec74 --- /dev/null +++ b/docs/SGF_PAYABLE_POSITIONS_DELIVERY.md @@ -0,0 +1,24 @@ +# Posições de fornecedores — entrega8085 + +10/11/2026. Financeiro → **Posições de fornecedores**, na instalação existente. Pesquisa literal por factura/fornecedor actual da origem, filtro de estado documental e páginas25/100. Cada linha mostra reconhecimento, estado financeiro, principal, pagamentos históricos confirmados, afectações líquidas, saldo, pendências e revisão da origem. Não se confunde factura pendente com dívida zero nem estado documental com estado de pagamento. + +A consulta selecciona IDs na SQL antes de chamar `positionsPage` do motor original, apenas para a página. Não recalcula dinheiro no adaptador. Origem divergente conserva saldo indeterminado; mais de uma identidade financeira para o documento recusa409. Isolamento/autoridade física/contexto e qualificação das origens permanecem no mecanismo financeiro existente. Sem novo direito ou writer. + +O resumo separa moedas e soma somente saldos conhecidos da página, com contagens de desconhecidos e documentos ainda não reconhecidos. CSV exporta somente a página/filtros/leitura e protege os textos livres contra fórmulas. O fornecedor é o actual da origem, não uma fotografia histórica. Cada página é uma leitura actual, sem snapshot persistente entre páginas ou saldo histórico. + +Abrir a posição transmite `document_id` ao detalhe financeiro original. O destino filtra antes de paginar e recusa documento indisponível/estrangeiro403 sem substituir por outro. A rotina original de confirmação financeira permanece idêntica, conferida após normalizar finais de linha; continua a revalidar o estado actual. + +## Provas + +-23 verificações na regressão existente: paridade com o motor, filtros,25/100 no universo conhecido, página fora do intervalo, acesso/contexto, identidade financeira ambígua e preservação. A única duplicação de identidade usada como fixture foi revertida; não houve operação financeira. +-15 verificações de apresentação/contrato/navegação: desconhecido sem zero, moedas separadas, CSV, selecção exacta e recusas. Renderização simulada não comprova revisão visual. +-19 verificações HTTP8085: valores efectivos versus motor, detalhe exacto, pesquisas, filtro, campos/tamanho recusados e outro módulo recusado. Inclui MIME e SHA dos ficheiros principal e lazy da interface realmente servidos. Zero POSTs de negócio. +-45 conjuntos actuais iguais antes/depois, incluindo obrigações/fontes/comandos, artigos e novos registos/recibos de catálogo, permissões, documentos, Caixa e diário. Instalação conserva binding; comandos financeiros e cache não foram alterados. Diário27/Caixa950/cálculos5 preservados. Nenhum comando dirigido aos seis registos protegidos. + +[Índice das provas](proofs/sgf-payable-positions-20261110.json). Provas anteriores reutilizadas; não se repetiram emissões, recebimentos, pagamentos, lançamentos ou cadastro de artigos. + +## Limitações concretas + +O motor actual reconhece obrigações com `due_on=null` e devolve atraso indeterminado. Não existe preparação/revisão de condições de vencimento para fornecedores; esta consulta não inventa prazos nem altera o livro para preencher a coluna. A consulta conserva os mutex e a qualificação global de origens do mecanismo composto. A selecção documental é paginada, mas não constitui prova de optimização integral, memória limitada de todas as dependências, grandes volumes ou carga sustentada. Não se ensaiaram mais de25 documentos nesta entrega. + +Gestão revista de vencimentos exige autoridade específica e decisão antes de qualquer concessão. Revisão visual, fiscalidade geral e capacidade de implantação permanecem pendências separadas. Sem novas bases/portas/demonstrações, SGC, efeitos financeiros, direitos reais, publicação, candidata de Compras, aceitação ou checkpoint. A matriz v17 continua a declarar o produto amplo incompleto. diff --git a/docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md b/docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md new file mode 100644 index 0000000..95150b8 --- /dev/null +++ b/docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md @@ -0,0 +1,22 @@ +# Posições de clientes e vencimentos — entrega8085 + +10/11/2026. Financeiro → **Posições de clientes e vencimentos**, na instalação existente. Consulta SQL25/100 por documento/cliente na emissão, estado e situação de vencimento; sem alterar o workflow de recebimentos, os direitos ou os exemplos financeiros. + +O utilizador indica uma referência para vencimentos. A lista apresenta o valor de origem, redução líquida, afectação líquida, saldo reconhecido, vencimento, dias de atraso conhecidos e revisão. O cliente é o preservado no documento, não um cadastro actualizado silenciosamente. Propostas ainda não abertas/recusadas têm saldo reconhecido indeterminado e não entram no total. Vencimento desconhecido mantém dias indeterminados; referência igual ao vencimento não é atraso. + +**São saldos actuais**, com todos os efeitos confirmados existentes. A referência escolhida apenas classifica o vencimento: não reconstitui saldo histórico, não aplica juros e não presume dinheiro recebido. O estado “liquidada” pode resultar de afectação/redução; não é comprovativo bancário. + +Totais pertencem ao filtro completo, calculados no servidor, não à soma da página. A SQL agrega efeitos por obrigação, incluindo reversões, antes de filtrar/paginar; não materializa todas as posições no PHP. A transacção de leitura consistente não adquire o mutex de escrita da empresa. Um marcador de registos/efeitos exige actualizar a primeira página se houver mudança entre páginas; não é snapshot histórico persistente. + +O CSV contém somente a página consultada, filtros, referência e marcador do livro, com texto protegido contra fórmulas. Abrir posição e comprovativos transmite a identidade escolhida ao percurso original. Se ela não estiver disponível, apresenta aviso e não escolhe outra posição automaticamente. Preparação/confirmação/revalidação financeira continuam no serviço original. + +## Provas e preservação + +-30 verificações na regressão existente: paridade de estado/reduções/afectações/revisão/saldo com `ServiceReceivables.position`,25/100 no universo conhecido, página fora do intervalo, pesquisa literal, autoridade/campos, vencido/não vencido/mesmo dia/desconhecido e preservação. Correcções de vencimento pelos métodos originais, preparação13/revisão14, integralmente revertidas. Não foram ensaiados milhares de posições nem uma página completa com mais de25 nesta entrega. +-16 verificações de apresentação/contrato: montantes exactos, proposta distinta de saldo, desconhecido distinto de zero, CSV/escapagem, resposta atrasada, marcador alterado e selecção exacta sem fallback. Exemplos de apresentação declarados no harness; não são movimentos financeiros ou prova visual de navegador. +-15 verificações HTTP8085: posições efectivas comparadas com o serviço original, total reconhecido uma vez por obrigação,25/100, pesquisa, vencimentos desconhecidos, CSV e recusas. Zero POSTs de negócio. +- Comparação dos conjuntos originais antes/depois da instalação, incluindo recebíveis/comandos/eventos/outbox/entregas, documentos, permissões e livro nativo; binding intacto. Diário27, Caixa950 e cinco cálculos conservados. Sem comandos dirigidos aos seis registos ou cache bloqueada. + +[Índice das provas e fontes](proofs/sgf-receivable-positions-20261110.json). Provas anteriores reutilizadas; não se repetiram emissões, recebimentos, pagamentos ou lançamentos. Código/provas preparados antes da actualização da8085. Fonte de cobertura fiscal passou a distinguir cálculo interno por linhas, emissão documental ainda limitada, conferência bancária QA e indicadores QA já entregues. + +Sem novos direitos, schemas, demos, SGC, dinheiro real, publicação, candidata de Compras, checkpoint/aceitação ou capacidade de implantação declarada. O detalhe financeiro anterior ainda materializa o conjunto original e conserva o seu mutex; esta lista não optimiza todos os leitores/writers. Relatórios históricos, cobranças/juros, bancos gerais e fiscalidade operacional geral continuam fora deste incremento. Revisão visual/carga sustentada permanecem separadas. diff --git a/docs/SGF_REMUNERATIONS_SCOPE.md b/docs/SGF_REMUNERATIONS_SCOPE.md new file mode 100644 index 0000000..64866ad --- /dev/null +++ b/docs/SGF_REMUNERATIONS_SCOPE.md @@ -0,0 +1,41 @@ +# SGF — processamento interno de remunerações QA + +Autorização expressa11/11/2026: **Autorizar o bloco QA e os três direitos apresentados**. Implementação em curso. A consulta SELECT-only confirmou ausência dos três poderes propostos nos dois livros QA existentes. Aplicar exclusivamente as14 concessões discriminadas, nos dois livros QA existentes. + +## Entrega funcional proposta + +Actualização pela directiva posterior: módulo autónomo **Recursos Humanos**, com novos perfis conforme [âmbito de autonomia](SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md); a antiga matriz Financeira abaixo foi substituída e as suas concessões retiradas. + +Um único bloco na instalação8085, no menu horizontal/dropdown existente: **Remunerações internas QA**. Sem nova demonstração/base/porta ou SGC. Abrange cadastro mínimo fictício, regras do exercício, período, processamento, revisão distinta, correcção/cancelamento, mapa conservado, histórico, recibos de comando e exportação verificável. Nenhum colaborador ou remuneração real é importado; técnicos de Oficina não se tornam trabalhadores automaticamente. + +1. Cadastro por código pseudónimo explícito e empresa/unidade, preparação e revisão distinta, estado activo/suspenso e histórico. A selecção do trabalhador é individual. Não recolher NUIT, identificação civil, morada ou conta bancária para este ensaio. +2. Conjunto versionado de parâmetros artificiais, vigência e descrição da fonte QA, preparado/revisto por pessoas distintas. Reutilizar aritmética exacta e padrões versionados existentes; não duplicar pesquisa fiscal nem inferir regras legais do caso artificial. Regras ausentes/incompletas impedem resultado completo; nunca substituir por zero. Não presumir benefícios, exclusões ou regimes reais. +3. Escolha explícita de trabalhador/período e bruto, cálculo explicável por rubrica/regra, versão e origem. Prévia de impacto sem persistência de processamento; confirmação individual fundamentada grava versão/autor/data/contexto/estado anterior-posterior e recibo idempotente. Não oferecer revisão própria. +4. Revisão distinta conserva o resultado exacto e as suas fontes/SHA. Correcção versionada e cancelamento interno exigem fundamento e nova revisão; preservar resultados anteriores e identificar os substituídos. Nenhuma alteração de regras/cadastro recalcula processamentos revistos silenciosamente. Alteração da fonte entre prévia/confirmação exige nova conferência. +5. Mapa do período com páginas25/100 e filtros SQL, detalhe individual, totais só do conjunto explicitamente identificado e estados de completude. Distinguir desconto do trabalhador de encargo patronal. Exportação conservada autentica contexto/permissão, verifica SHA e não coloca tokens no URL; recibo refere decisão técnica, não pagamento de salário. + +## Caso sintético completo + +Novo código **QA-COLABORADOR-01**, sem ligação a pessoa real; período artificial2026-11, bruto10000MZN. Parâmetros **exclusivamente de exercício**, sem representar enquadramento nem tabela legal: contribuição do trabalhador3% sobre bruto, encargo patronal4% e retenção artificial8% sobre bruto. Valores exactos:300,400,800MZN; total de descontos1100; líquido interno8900; custo interno10400. Cada rubrica mostra que a base e percentagem são do exercício QA e que não constitui cálculo legal validado de INSS/IRPS. + +Cálculo em centavos/racionais, truncagem documental de cada rubrica a duas casas e diferença exacta preservada; nenhuma truncagem intermédia de bases. Registar a política na versão. Bases/percentagens ausentes produzem resultado incompleto/líquido indeterminado e impedem aprovação como completo. Regressão cobre diferenças de centavo, vigências, cadastro suspenso, duas pessoas/períodos e isolamento; o exemplo efectivo8085 usa só o novo trabalhador fictício acima. A correcção/cancelamento é demonstrada na regressão existente; não repetir o exemplo8085 para testar ramos negativos. + +## Matriz inicial substituída pela directiva de autonomia (histórico) + +A regra local de novos direitos exige apresentá-los antes de conceder. As permissões de Caixa/IVA/lançamento não concedem administração de remunerações ou acesso a estes dados. + +|Direito novo|Perfis QA existentes|Poder| +|---|---|---| +|`remunerations.view`|13,14,15,16,17|Consulta de cadastro, regras, mapas e histórico só no contexto autorizado| +|`remunerations.prepare`|13|Preparar cadastro/regras/processamento/correcção/cancelamento, sempre com prévia/fundamento| +|`remunerations.review`|14|Revisão distinta da versão actual e fundamento| + +Sete concessões por livro existente,14 no total, tenant1/empresa2/unidade2. Só aplicação/perfis sintéticos existentes; nenhum grant PostgreSQL/PUBLIC/conta/role nova ou acesso real. Esta decisão abrange todo o bloco e os seus testes, sem pedidos separados por campo ou rubrica. + +## Provas e limites + +Revalidar sessão, pessoa, direitos, empresa/unidade/módulo, fonte, versão, vigência, dependências e revisão distinta na confirmação. Usar o mecanismo administrativo de prévia/confirmar/recuperar e a auditoria existente. Comprovar idempotência sem duplicação, concorrência com bloqueador identificado, pós-libertação409 ou resultado coerente e recuperação de falha parcial atómica apenas na regressão. Fixtures de volume e casos negativos revertidos; HTTP8085 prova o percurso positivo e leituras, sem suites alheias. + +Preservar27 lançamentos,19 contas, Caixa950, IVA/regras/mapas/obrigações/demonstrações internas já aceites tecnicamente, seis registos protegidos, cache bloqueada, demais provas e trabalho alheio. Alterações de schema limitadas às tabelas necessárias do bloco nos dois livros QA existentes, sem migrations globais. Versões/documentos anteriores e fontes efectivas ficam intactos. + +**Excluídos:** salário ou colaborador real, pagamento, reconhecimento de dívida, lançamento contabilístico salarial, criação de contas, recibo salarial legal, modelo/declaração oficial, submissão AT/INSS, activação fiscal real, reabertura, publicação, candidata de Compras, aceitação/checkpoint presumidos. Processamento sintético completo não fecha a lacuna de conformidade legal de IRPS/INSS nem integração contabilística salarial. Estas continuam discriminadas na matriz; fontes/regras legais deverão ser correspondidas antes de operação real. Visual, carga sustentada e capacidade de implantação permanecem separados. diff --git a/docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md b/docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md new file mode 100644 index 0000000..3183634 --- /dev/null +++ b/docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md @@ -0,0 +1,50 @@ +# Documentos adicionais integrados na8085 —10/11/2026 + +Bloco QA autorizado implementado na mesma instalação: entrada `http://127.0.0.1:8085/sgf`, módulo **Comercial**, funções **Recibos**, **Notas de crédito** e **Facturas-recibo**. Cada função abre o ecrã de documentos com o tipo correspondente; acesso directo autenticado `/commercial/documents`. Preservados os seis módulos, menus horizontais/dropdowns e SGC independente. Nenhuma nova demonstração/base/porta/utilizador/role PostgreSQL. + +As dez concessões de consulta foram expressamente autorizadas e aplicadas apenas aos perfis Financeiros QA13/14 e âmbitos discriminados. Os controlos documentais originais continuam efectivos; não se substituiu consulta fiscal por uma autorização genérica nem se usou outra identidade para emitir. + +## Percurso utilizável + +1. Preparação individual com origem explícita, fundamento e versão. Seleccionar factura e afectação efectiva no Recibo; não usar recebimento proposto como prova de pagamento. O selector não escolhe a primeira linha. +2. Revisão por outra pessoa. A recusa conserva a versão e o fundamento; a aprovação não emite por arrasto. +3. Nova prévia e emissão individual. O servidor volta a conferir pessoa, contexto, direitos, origem e dependências. A factura-recibo consome a intenção financeira previamente preparada por13 e a revisão distinta; o executor14 emite uma factura e conclui obrigação, recebimento e afectação na mesma transacção. A intenção conserva o autor anterior; efeitos e auditoria conservam o executor real. Sem mudança de identidade. +4. PDF de teste congelado, SHA conferido, numeração QA própria, histórico SQL25/100 e recuperação pela mesma chave. Prévia revertida pode avançar sequências, mas não grava documento/recebível. Formulário e navegação local ficam bloqueados durante a prévia/confirmação incerta. +5. Contabilidade prepara, revê e aplica cada origem no mesmo diário. O crédito nominal usa D QA-REDUCAO-CLIENTE/C QA-CLIENTES. Não é repartição fiscal certificada nem devolução de imposto. + +Comercial preparação/revisão emite o Recibo através das respectivas funções. O crédito e a factura-recibo usam Financeiro preparação e revisão, escolhendo Comercial na navegação modular. A função de consulta pode ver histórico e PDF, sem adquirir poderes de escrita. As novas origens documentais são as duas explicitamente preparadas; não se oferecem documentos históricos ou entidades protegidas como alvos. + +## Estado conservado para apresentação + +|Origem nova|Documento/efeitos|Saldo| +|---|---|---:| +|Factura de teste1000|Recibo ligado à afectação efectiva400 e crédito nominal100|500MZN| +|Outra origem1000|Factura-recibo, recebimento e afectação1000 atómicos|0MZN| + +As duas facturas usam o cálculo documental de teste já aprovado: base862,07 + imposto137,93 =1000MZN. Séries QA novas não reiniciam numeradores anteriores. O recibo e o crédito não criam outra factura. O documento fiscal real continua bloqueado. + +O mesmo diário tem **25 lançamentos equilibrados**: os20 anteriores mais factura1000, afectação400, redução100, outra factura1000 e afectação1000. Consulta individual confirmou saldo500 e0, três PDFs emitidos, zero anulações confirmadas e período QA aberto. Os18 lançamentos anteriores ao bloco e movimentos/clientes/facturas originais foram comparados novamente com o backup e estão intactos. + +## Anulação técnica e reversão + +Disponível ao seleccionar a versão emitida: Financeiro preparação prepara; Financeiro revisão confere com fundamento e prévia dos saldos anterior/posterior. Conserva PDF, numeração, factura e história. Recibo e factura-recibo revertem **a afectação**: deixam o recebimento não afectado, sem devolver dinheiro ou apagar a factura. Crédito reverte a redução100 por efeito próprio ligado ao original. A factura continua a representar a dívida; anular o agregado não anula fiscalmente a factura. + +O período QA tem de continuar aberto, incluindo na confirmação. Uma alteração financeira obriga o preparador a renovar a conferência. Repetição recupera o resultado; anulação já concluída não volta a executar. Novos efeitos exigem os seus lançamentos individuais; a conferência do período não os ignora. As reversões foram executadas apenas em regressão e revertidas; não se anularam os exemplos novos da8085. + +## Provas + +- 20 verificações dirigidas em rollback: documentos, direitos, origem, revisão distinta, falha depois do writer com rollback financeiro/PDF, recuperação, versão antiga, integridade PDF, conclusão atómica e cinco origens no mesmo diário. Prova privada `receipt-composition-b4093c6f6707/single-integration/20261110-171528-10e1c5f0/service-documents-result.json`. +- Concorrência real entre dois processos na regressão: segunda confirmação503 após o limite de espera do bloqueio de empresa; após commit da primeira,409 por versão antiga. Uma factura, obrigação, recebimento, afectação e PDF; primeira chave recuperável. `documents-concurrency/result.json`. +- 21 controlos HTTP na8085: três documentos, nove decisões com repetição, três PDFs íntegros/históricos, selector de afectação efectiva, escrita indevida403 e diário25 equilibrado. `service-documents-http-20261110.json`. +- 17 verificações de anulação/reversão em rollback, incluindo revisão distinta, repetição, impossibilidade de anular duas vezes, saldos restabelecidos, três reversões no diário, PDFs intactos e constraints nativas. `single-integration/20261110-172728-ac2008fe/document-cancellation-result.json` na regressão. +- Oito verificações do período na8085:29 factos, zero pendências, fecho/repetição/recusas em rollback e anulação recusada em período efectivamente fechado. Não houve reabertura; a instalação continua aberta. `single-integration/20261110-172510-6e7769dd/period-documents-result.json`. +- 19 controlos DOM→HTTP: prévia real revertida da anulação100, dívida500→600 explícita, factura preservada/sem reembolso, campos bloqueados, perda de resposta e recuperação com conteúdo idêntico, três versões históricas. Transportes de confirmação simulados, zero confirmações reais. `service-documents-dom-http-20261110.json`. Não é prova de navegador nem revisão visual. +- Consulta final e preservação SELECT-only: `single-integration/20261110-172904-86c06a96/service-documents-status.json` e `single-integration/20261110-172905-2acea48f/next-block-preservation.json`, na8085. Build privado e sintaxe dos ficheiros alterados aprovados. Provas anteriores reutilizadas, sem suites alheias. + +Os caminhos privados são relativos à instalação QA no run2576427d1939. A prova inicial de16 verificações/recusa403 é histórica, anterior às novas consultas autorizadas; não é a prova positiva actual nem é somada novamente. + +## Limitações separadas + +Entrega funcional **sintética/local**, não certificação fiscal ou capacidade de implantação. Sem AT, banco, dinheiro real, reembolso, edição de regras legais ou reabertura. Sem SGC, publicação, candidata de Compras, concessões reais ou checkpoint/aceitação presumidos. Os seis registos de associação e a cache bloqueada não foram alvo de comandos. + +Revisão visual continua pendente, sem repetir a via de navegador bloqueada. Carga sustentada, retenção/quotas de armazenamento, conformidade fiscal vigente e implantação empresarial exigem evidência própria. Os relatórios de Stock fora do recorte conhecido não recebem zeros ou mínimos inventados. Não se declara toda a aplicação pronta para produção por estes ensaios. diff --git a/docs/SGF_SINGLE_INTEGRATION_20261110.md b/docs/SGF_SINGLE_INTEGRATION_20261110.md new file mode 100644 index 0000000..93c066e --- /dev/null +++ b/docs/SGF_SINGLE_INTEGRATION_20261110.md @@ -0,0 +1,49 @@ +# Integração local dos seis módulos na8085 + +**Actualização posterior no mesmo dia:** o estado abaixo documenta a primeira integração13 lançamentos. Foi complementado por Oficina–Stock/devolução/valorização, Oficina–Comercial500, continuidades, trânsito/lotes/ajuste e fecho QA: agora18 lançamentos e15 contas. Ver [consolidação actual](SGF_SIX_MODULE_CLOSURE_20261110.md). As provas desta primeira fase mantêm-se intactas e não são repetidas por rotina. + +Autorização expressa: «autorizo»,10/11/2026, âmbito de `SGF_8085_NEXT_AUTHORIZATION.md`. Entrada: http://127.0.0.1:8085/sgf. Conserva `/compras` e os sete perfis anteriores; acrescenta as dez funções QA aprovadas, com escolha explícita do módulo, sessão normal e CSRF. Não depende de SGC nem de outro servidor de demonstração. + +## Percursos efectivamente disponíveis + +| Módulo | Estado observado nesta instalação | +|---|---| +| Comercial | Cliente/serviço novos explicitamente associados; rascunho, perfis e série revistos; documento de teste862,07+137,93=1000MZN e PDF íntegro. | +| Compras | C01–C10 anterior preservado; não reexecutado. A obrigação original100MZN continua no livro nativo, sem cópia para o host. | +| Stock | Leitura do mesmo livro físico; saídas, transferência imediata, contagem sem lotes e aplicação da diferença pelos writers originais; segundo artigo/armazém QA explícitos. | +| Oficinas | Ordem, autorização, tarefa,105min de trabalho e material2UN, com conferência técnica distinta; custo275 validado, sucessor295 validado e contabilizado por par de correcção. | +| Financeiro | Recebível1000, afectações400/600, reversão400 e saldo400; fornecedor100→80 após afectação20→100 após reversão20. Facto de pagamento confirmado isoladamente não abate dívida. | +| Contabilidade | Um diário nativo com âmbito SGF empresa2/unidade2;13 lançamentos equilibrados, incluindo fornecedor, afectações/reversões, vendas, custos directos e industriais e os dois pares de correcção. Lote1800→1820 mantém residual300 separado. | + +As operações de continuação financeira, revisão e lançamento foram executadas pela API HTTP da própria8085, com sessões separadas e confirmações individuais. A preparação Comercial/Oficinas/industrial usou os workflows efectivos no destino, com funções sintéticas existentes e transacções próprias. Não se apresenta essa preparação por CLI como percurso visual do utilizador. + +## Integridade e isolamento + +O diário está no livro financeiro nativo existente. A origem financeira conserva o âmbito legado1/0/0; uma ligação explícita e conferida ao host qualifica o diário1/2/2. Leituras/efeitos são bloqueados se aparecer obrigação sem ordem/requisição admitida para a unidade. Nenhuma dívida, saldo ou movimento é espelhado no host. + +A transacção da empresa no host mantém a autoridade e as origens de Comercial/Oficinas estáveis até ao commit nativo. As decisões verificam novamente pessoa, direitos, contexto, origem e versão. O diário só lê factos do host; não há transacção distribuída que grave simultaneamente dois efeitos de negócio. O histórico nativo retém autor SGF, âmbito, chave, hash do pedido, fundamento e resultado. + +Materiais directos/industriais deste recorte são explicitamente artificiais e distintos do artigo escolhido para valorização. O perímetro privado está fixado por SHA; origem desconhecida, partilhada ou alterada bloqueia a continuidade. Esta declaração controlada de QA não constitui valorização geral de materiais reais ou de produção. O comportamento exclusivo anterior do núcleo conserva-se fora desta composição explícita. + +O isolamento de código da8085 foi corrigido: o autoload de `App` utiliza a cópia privada, mesmo com o directório de dependências partilhado. Foram instalados52 ficheiros seleccionados e um build privado, com cópias anteriores. O bootstrap que montaria o conector SGC não foi copiado. O pacote original fixado de Compras e os seus bindings originais permanecem intactos. A conta de leitura de Stock conserva SELECT-only; o mapa adicional apenas discrimina funções/armazéns QA autorizados. + +## Provas dirigidas + +O índice `docs/proofs/sgf-single-integration-20261110.json` referencia os resultados privados, sem credenciais ou dados privados.92 verificações dirigidas:23 de direitos/isolamento/idempotência dos perfis;15 leituras HTTP dos módulos;10 fronteiras HTTP, CSRF e recuperação;12 de recuperação dos formulários em DOM simulado; nove do controlador de taxas; sete de custo directo/correcção; seis industriais/correcção; cinco do perímetro de materiais; cinco de bloqueio/recuperação financeira. As provas anteriores de Compras e do núcleo foram reutilizadas, não repetidas por rotina. + +O bloqueio concorrente observado é a linha de empresa do host, que serializa autoridade/origens antes de entrar no livro nativo. Uma segunda ligação atingiu o limite de aproximadamente5s, devolveu503 com `Retry-After:5`, e a mesma autoridade pôde continuar após a libertação da primeira transacção. Não criou pagamentos. Este ensaio não equivale a uma nova corrida de duas confirmações de escrita nem mede capacidade sustentada. + +A primeira conferência de perfis desta etapa falhou porque o harness antigo exigia exactamente dois clientes e ignorava os direitos mínimos de leitura entre módulos. Foi corrigido para comparar os direitos aprovados e preservar clientes/associações antes/depois;23 verificações passaram. A preparação industrial de regressão inicialmente incluiu referências de taxas no mapa de cinco origens. Esse metadado novo, ainda sem lançamento, foi corrigido apenas nessa regressão, com estado anterior/posterior privado; a8085 recebeu o mapa correcto. Não se alteraram provas antigas para as fazer passar. + +## Trabalho ainda necessário para o fecho integral + +1. Ligar o consumo/devolução de Oficinas ao mesmo livro físico, com origem, limite e recepção distintos; concluir na8085 o ciclo físico2/-1/+0,5 e valorização100/50/25. Os custos já demonstrados usam materiais QA separados; não fingir essa ligação física como concluída. +2. Concluir as continuidades previstas na matriz que ainda usam o adaptador antigo: documento comercial de origem de Oficina e comprovativos/entradas específicas do diário. A origem individual no novo diário funciona; nem todos os atalhos anteriores foram adaptados. +3. Concluir a conferência do fecho QA após resolver todas as origens/pendências. Não fechar antecipadamente o período com cenários em falta. A paginação do catálogo de origens ficou entretanto concluída: filtros por família, páginas25/100 no servidor e selecção individual preservada. O catálogo de Stock pagina os movimentos antes de aplicar o filtro de tipo; pode mostrar páginas vazias para um tipo e explica a paginação física, sem omitir ou escolher outra origem. +4. Concluir revisão visual, ensaio de arranque/recuperação completo e guião único. Transferência em trânsito, contagens por lotes e restantes recortes indicados na matriz conservam os seus limites. + +A conferência de estruturas foi actualizada para consultar o host e o livro nativo canónico; já não apresenta dívida/diário como ausentes só por não estarem no host. Não faltam as tabelas mínimas dos seis módulos. Isso não comprova todas as funcionalidades: o gate continua `client_presentable:false`, sem aceitação integral nem capacidade de implantação declarada. + +Preservados os seis registos protegidos, a cache cuja limpeza foi bloqueada, exemplos e provas anteriores, direitos reais e trabalho alheio. Sem novas bases, portas, demonstrações, roles PostgreSQL, publicação, candidata de Compras, checkpoint ou activação SGC. As flags disponibilizadas são apenas as deste recorte SGF privado expressamente autorizado. + +Adenda de paginação:127 metadados de catálogo adicionais e um de unidade estrangeira, somente na transacção de regressão revertida. São fixtures de catálogo, não ordens executadas nem custos elegíveis. Foram percorridas seis páginas de25 e duas de100, cada uma com COUNT e SELECT limitado, sem duplicação nem exposição da unidade estrangeira;275 asserções, que não devem ser apresentadas como275 percursos de negócio. Duas leituras HTTP confirmaram o HTML filtrado e a segunda página na8085. O primeiro probe HTTP não consumia o corpo e atingiu uma asserção do cliente Node; o harness corrigido consumiu as respostas e ambas passaram. Não se atribui essa falha ao servidor nem à aplicação. diff --git a/docs/SGF_SIX_MODULE_CLOSURE_20261110.md b/docs/SGF_SIX_MODULE_CLOSURE_20261110.md new file mode 100644 index 0000000..2f43e3b --- /dev/null +++ b/docs/SGF_SIX_MODULE_CLOSURE_20261110.md @@ -0,0 +1,62 @@ +# SGF8085 — consolidação funcional dos seis módulos + +10/11/2026. Uma instalação: **http://127.0.0.1:8085/sgf**, empresa/unidade QA existentes. Sem SGC, novas demonstrações, publicação ou candidata de Compras. As duas autorizações integrais de10/11 foram executadas apenas no recorte sintético aprovado. Código, interfaces e operações efectivas; esta entrega não é só documentação. + +## Resultado disponível + +Navegação actualizada por pedido expresso do utilizador: módulos alinhados horizontalmente, funções em dropdown por módulo, tanto na entrada como na aplicação. «Consultar» abre directamente cada percurso com um perfil já autorizado; a lista original dos17 perfis fica disponível separadamente. Não foram criados acessos ou efeitos para simplificar a entrada. O utilizador esclareceu depois que já consegue entrar; isso não presume revisão visual integral dos cinco módulos restantes ou da nova navegação. + +| Módulo | Percurso na mesma8085 e evidência concreta | +|---|---| +| Comercial | Cliente/serviço associados explicitamente, cálculo/revisão, documento de teste862,07+137,93=1000MZN e PDF. Recebível da mesma origem. A oficina também gera um rascunho comercial500MZN, revisto distintamente, sem emitir automaticamente outra factura. | +| Compras | C01–C10 preservado: fornecedor → requisição → propostas/avaliações → adjudicação distinta → ordem → recepção → factura conferida → obrigação100MZN. A continuação usa a mesma dívida nativa; não foi reexecutada a aquisição aceite. | +| Stock | Livro físico único, saldos e movimentos por período; entrada2UN, saída1UN e devolução recebida0,5UN efectivas. Consumo de Oficina ligado à saída existente, sem segundo abatimento. Transferência1UN em trânsito, recebida0,4+0,6 por outra função: origem0,5UN, destino1UN, trânsito0; propriedade1,5UN/75MZN conservada. Novo artigo por lotes: entrada4UN, dois lotes2+2, identificação/retenção/libertação e contagem1,5+2; ajuste distinto−0,5UN, saldo3,5UN/70MZN. | +| Oficinas | Ordem/autorização, tarefa105min e material2UN, revisão técnica, custo275→295 por par imutável; industrial1800→1820, trabalho500/CIF300 e residual300 separado. Outro apontamento liga consumo0,5 e devolução0,5 à entrega física1UN. Continuação explícita para o rascunho Comercial500. | +| Financeiro | Recebível1000, afectações400/600 e reversão400, saldo400; obrigação original de Compras100→80→100 por afectação/reversão20. Pagamento confirmado isoladamente não abate a dívida. Consultas paginadas, comprovativos e histórico por origem. | +| Contabilidade | Mesmo livro nativo, **18 lançamentos equilibrados e15 contas QA**, plano pesquisável, diário, balancete e razão, correcções por pares, recibos/atalhos antigos ligados ao mesmo núcleo. Novos lançamentos80 e10 do artigo de lotes; transferências internas não são consumo nem dívida. Valor da conta de Stock145MZN =75+70; não somar quantidades de artigos diferentes. | + +O fecho QA foi executado e recuperado com revisão distinta em transacção revertida: **18 factos, zero pendentes**. O período real da apresentação mantém-se aberto; não houve reabertura. Um movimento desconhecido fora do armazém do livro de valorização passa a pendência de fecho, não desaparece nem é classificado automaticamente como transferência interna. + +## Concorrência, falhas e isolamento + +O ensaio em dois processos identificou o bloqueio: a linha do utilizador QA no host é mantida até terminar a transacção nativa; depois existe também a serialização do tenant do livro físico. A segunda confirmação recebeu503 ao atingir o limite5s. Libertada a primeira transacção, o mesmo pedido recusou409 por versão antiga; uma nova conferência recebeu somente os400milésimos restantes. Uma expedição, duas recepções, três eventos/movimentos; nenhuma duplicação. Isto comprova esse conflito dirigido, não capacidade de carga sustentada. + +As prévias executam os serviços dentro de savepoint revertido. A confirmação volta a conferir pessoa, direitos SGF/nativos, âmbito, mapa privado fixado, versões e impacto. Falha injectada depois do writer reverte movimento, repartições e recibo juntos. Repetição da confirmação devolve o mesmo recibo; chave usada com conteúdo diferente409. Há consulta histórica por chave depois de renovar a sessão, sem executar o comando. Retorno não recebido exige recepção física explícita na origem. + +Os serviços nativos `InventoryLots`/`InventoryCounts`/`StockWorkflow` foram reutilizados sem modificar o pacote Compras fixado. O novo protocolo de trânsito acrescenta eventos auditados e escreve no mesmo `stock_movements`; não cria tabela de saldos ou outro livro. A excepção contabilística interna exige operação, parcelas, pessoa distinta, taxa revista e correspondência física reconciliadas. A conta SELECT-only anterior mantém as três tabelas. + +O núcleo QA estava em modo de funções legadas e ignorava perfis. Para aplicar as sete funções aprovadas, o instalador conservou cada função antiga num perfil QA equivalente e passou ao mecanismo nativo de perfis, auditado. Não concedeu `warehouse` ao revisor. Os dois direitos novos estão exclusivamente na função QA de conferência, limitados pelo mapa privado ao destino e artigo/armazém aprovados. Leitura de Compras/Financeiro após esta alteração foi comprovada; nenhum acesso real/PUBLIC/role PostgreSQL foi criado. + +## Provas consolidadas + +Índice verificável: [sgf-six-module-functional-review-20261110.json](proofs/sgf-six-module-functional-review-20261110.json). Ficheiros privados referenciados por caminho/hash, sem credenciais ou dumps no repositório. + +| Prova dirigida | Resultado e alcance | +|---|---| +| Ponte física Oficina–Stock |18 verificações; consumo sem segundo abatimento, devolução distinta, recuperação/falha e valorização100/50/25. Prova válida reutilizada. | +| Recuperação cliente da ponte |9 verificações de pedidos congelados; reutilizadas. | +| Oficina–Comercial |8 verificações e geração HTTP efectiva500; prova válida reutilizada. | +| Continuidade contabilística |13 leituras/atalhos/recibos; prova válida reutilizada. | +| Trânsito/lotes/ajuste |27 verificações; preparação/revisão, parcelas, isolamento, retenção/expiração, revogação, falha, recuperação e valorização80/10. Versões anteriores20 não são somadas. | +| Concorrência de recepção |Dois processos,503 durante o bloqueio,409 após libertação e conferência nova do restante; ensaio próprio, não uma contagem de percursos comerciais. | +| Reconciliação contabilística interna |6 verificações; transferência não gera despesa e o período confere fonte/destino. Apenas a última passagem da mesma prova é indexada. | +| Recuperação/retorno/permissão nativa |6 verificações adicionais, todas revertidas. | +| Preservação/conferência final |12 verificações: clientes/rascunhos e16 lançamentos anteriores idênticos, binding original intacto,145MZN,18 lançamentos e recusa de origem física desconhecida. | +| Fronteiras HTTP afectadas |20 verificações, páginas25/100, histórico, contexto antigo409, acesso indevido403, Compras/Financeiro preservados e SGC404. | +| Ecrã executado com DOM e HTTP |10 verificações: consulta/histórico e prévia HTTP reais; confirmação/perda de resposta simuladas, zero comandos de negócio confirmados pelo harness. Não é navegador nem revisão visual. | +| Cliente de confirmação |9 verificações dirigidas de recuperação, corpo imutável e recusa de alteração. | +| Fecho do período actualizado |7 verificações no conjunto actual, rollback integral. Reexecução necessária porque Stock/origens mudaram; não é uma repetição das aquisições/vendas/custos. | +| Arranque/recuperação |Prova anterior válida: reinício só do PHP8085, nova sessão,29 tabelas canónicas idênticas e SGC404. Não repetida nem apresentada como reinício do sistema operativo ou do PostgreSQL partilhado. | + +As92 provas de integração anteriores, a paginação das origens, as provas C01–C10 e as38PG/15DOM/seis HTTP de Stock anteriores continuam preservadas. As execuções intermédias corrigidas não são somadas como provas novas. Erros iniciais de harness (nomes de colunas, IDs físicos da regressão, opções DOM e rotas/estados esperados) foram corrigidos; só resultados estruturados `passed:true` são aceites. + +## Preservação e limites + +- Clientes, rascunhos de serviços e de facturação existentes na8085 foram comparados integralmente antes/depois. Os16 lançamentos anteriores continuam idênticos; só80/10 foram acrescentados. Binding/pacote originais de Compras preservados. Novos artigos e origens de entrada são QA seleccionados explicitamente; a ordem preparada para o ensaio físico não é declarada outra aquisição C01–C10 executada. +- Os seis registos protegidos não receberam associações ou alterações. As outras bases protegidas não foram ligadas pelos novos scripts. Cache bloqueada, demonstrações anteriores e trabalho alheio preservados. Dumps/backups só privados; sem commit/checkpoint/aceitação presumidos. +- Revisão visual: **o utilizador confirmou «Abre e está legível» para o novo ecrã Stock em10/11**. O controlo automático terminou por não conseguir determinar com segurança o URL no Windows; não foi contornado. Esta confirmação não é revisão visual de todos os outros ecrãs, responsividade ou acessibilidade. +- O arranque em máquina já com o cluster existente activo e o reinício do servidor8085 estão comprovados. O caminho de arranque do cluster existente parado foi preparado, mas não testado desligando PostgreSQL partilhado ou outras aplicações. Reinício completo do computador permanece sem prova. +- Carga sustentada, retenção/quotas de anexos, reconciliação geral de órfãos e capacidade de implantação continuam pendências operacionais próprias. Provas de concorrência, paginação, DOM e configuração QA não as validam. +- Cenários financeiros, contas, imposto documental e documentos continuam artificiais; sem operações bancárias, fiscalidade certificada, dados/acessos reais ou produção geral valorizada. Calendário de marcações de oficina, lembretes, documentos comerciais adicionais e relatórios legais fora das funcionalidades fixadas na matriz não foram declarados implementados por este fecho. + +**Saldo de entrega:** funcionalidades obrigatórias da matriz e ligações dos seis módulos executadas no recorte QA da mesma8085. A apresentação integral continua dependente da conferência visual restante e da aceitação explícita do conjunto. Isto não declara capacidade de implantação nem transforma o ensaio em produto operacional real certificado. diff --git a/docs/SGF_SOLUTION_DELIVERY.md b/docs/SGF_SOLUTION_DELIVERY.md new file mode 100644 index 0000000..16c71cc --- /dev/null +++ b/docs/SGF_SOLUTION_DELIVERY.md @@ -0,0 +1,46 @@ +# Directiva de entrega: SGF autónomo, completo e apresentável + +Estado actual de10/11/2026: [consolidação funcional dos seis módulos](SGF_SIX_MODULE_CLOSURE_20261110.md), [guião de apresentação](SGF_GUIAO_APRESENTACAO.md), [guia de uso](SGF_USO_LOCAL.md). Entrada única `/sgf`,18 lançamentos no diário comum, integração física Oficina–Stock, continuação Oficina–Comercial, trânsito com recepção distinta, lotes/ajuste valorizado e fecho QA integral provados. Stock confirmado visualmente pelo utilizador; revisão visual restante e aceitação integral não presumidas. O diagnóstico e as etapas abaixo são históricos e não reabrem lacunas já fechadas. Carga/implantação e reinício completo do sistema mantêm prova própria. + +10/11/2026. O proprietário determinou concluir **todos os módulos do SGF** e entregar uma solução para apresentar a potenciais clientes. Este passa a ser o objectivo de fecho do projecto, substituindo a continuação de incrementos do conector SGC como prioridade. + +## Entrega obrigatória + +Uma instalação SGF, um endereço de entrada, empresa/unidade e perfis coerentes: **Comercial, Compras, Stock, Oficinas, Financeiro e Contabilidade**. Cada módulo tem de oferecer os ecrãs, operações, consultas e histórico previstos, e os percursos entre módulos têm de conservar documento, versão, autoridade e efeito único. O produto não pode exigir o SGC a correr ou pedir ao apresentador para mudar entre demonstrações. + +O [contrato de conclusão](integration/sgf-solution-contract.json) fixa funcionalidades por módulo e oito condições de entrega. Fonte de código, menu, catálogo de relatórios ou teste antigo isolado não contam como funcionalidade entregue na instalação. Não se declara percentagem de conclusão a partir do número de testes. + +Além das funções: apresentação visual coerente, percurso comercial de ponta a ponta, dados fictícios identificados, perfis e isolamento, arranque, recuperação e guião de apresentação. As operações reais/fiscais/bancárias continuam sujeitas às suas validações e autorizações: a apresentação controlada não lhes atribui validade por inferência. + +## Conferência de partida efectuada + +A [conferência da instalação actual](proofs/sgf-solution-closure-baseline-final-20261110.json) foi executada por SQL **READ ONLY**, sem arrancar aplicações nem alterar bases, direitos, cache ou8085. Confirmou a identidade física/role do destino e confrontou tabelas com declarações reais do código, caminhos de fontes e provas existentes. + +- Compras tem o percurso aceite até obrigação em8085; conservar e reutilizar essa prova. +- Comercial, Oficinas, Financeiro e Contabilidade ainda têm estruturas de host necessárias ausentes nessa instalação. As suas provas em outras composições não equivalem à entrega no mesmo SGF. +- Stock tem leitura e relatório integrados, mas faltam comandos/interfaces SGF obrigatórios e partes do percurso de transferência. Uma tabela ausente no host não é presumida ausente do livro nativo de Stock/Compras. +- Menus de Stock e Contabilidade ainda incluem descrições e catálogos de funcionalidades por implementar; isso tem de ser resolvido com operações/consultas efectivas, não com outra página informativa. + +Estado honesto: **SGF ainda não pronto para apresentação comercial completa**. A conferência é um inventário mínimo de estruturas e fonte, não uma instalação/migração aprovada ou teste funcional dos seis módulos. A primeira projecção provisória tinha três nomes de tabela e uma referência de prova incorrectos; foi corrigida contra o código antes de ser usada como diagnóstico. Só a versão final acima é válida. + +## Ordem de fecho + +Actualização de10/11/2026:70 tabelas de composição instaladas na mesma8085, com backup e preservação comparada de dados/sequências/permissões/associações. Guardas de Comercial/Oficinas/custos/recebíveis preparadas com identidade QA exacta e flag desligada; código Stock instalado inactivo, agora também com contagem/aplicação de diferença/equipamento. A [prova consolidada](proofs/sgf-six-module-composition-20261110.json) reúne ensaios dirigidos e consultas HTTP ao percurso existente. Não há novos acessos nem efeitos financeiros na8085. A [decisão de disponibilização](SGF_8085_NEXT_AUTHORIZATION.md) apresenta os perfis sintéticos e a continuação financeira excluída da aprovação anterior. O inventário de host agora só sinaliza Financeiro/Contabilidade; não se copiará dívida nativa para eliminar um aviso de tabelas ausentes. + +1. **Composição única:** preparar o plano exacto de schema/configurações/guardas e perfis sintéticos para os seis módulos, preservando dados anteriores. Confrontar as estruturas já existentes; não executar migrations globais nem mover responsabilidades/associações por inferência. +2. **Funcionalidades:** fechar lacunas dos próprios módulos, reutilizando os motores existentes. Não substituir conclusão funcional por filtros, benchmarks ou melhorias do conector SGC. +3. **Integração:** fechar Comercial→Financeiro→Contabilidade, Compras→Stock→Financeiro→Contabilidade e Oficinas→Stock→Custos→Comercial/Contabilidade, com revisão individual e fonte única. +4. **Produto apresentável:** rever visual/navegação, erros, formulários, continuidade, relatórios e guião; testar o arranque/recuperação da mesma instalação. Não presumir revisão visual a partir de DOM simulado. +5. **Aceitação integral:** um percurso demonstrável dos seis módulos na mesma instalação e um relatório consolidado de limitações reais. Corrigir as falhas detectadas antes de declarar pronto. + +O contrato é de fecho, não uma autorização para aumentar o escopo indefinidamente. Capacidades adicionais propostas devem ser identificadas separadamente, sem reabrir decisões aceites ou produzir nova demonstração por incremento. + +## Verificação de entrega + +```powershell +& 'C:/laragon/tools/sgf-tempo/node-v24.22.1-win-arm64/node.exe' scripts/check-sgf-solution.mjs +``` + +O comando lê apenas metadados do destino conhecido e termina com código2 enquanto a aceitação integral estiver ausente/incompleta. Uma captura de inventário pode ser escrita em ficheiro novo, mas não significa aprovação. O fecho exige `docs/proofs/sgf-single-solution-acceptance.json`, ligado ao hash deste contrato, aos seis módulos/funcionalidades e a todos os critérios na instalação escolhida; a existência do ficheiro ou de tabelas não basta isoladamente. + +O [roteiro histórico](MODULE_IMPLEMENTATION_AUDIT.md), as aceitações e provas anteriores ficam preservados como evidência de componentes. Os seis registos, cache bloqueada, direitos reais e trabalho alheio continuam protegidos. Não houve activação, publicação, novo servidor/base/demonstração, concessão ou checkpoint nesta definição de entrega. diff --git a/docs/SGF_STOCK_CATALOGUE_DELIVERY.md b/docs/SGF_STOCK_CATALOGUE_DELIVERY.md new file mode 100644 index 0000000..fa7f892 --- /dev/null +++ b/docs/SGF_STOCK_CATALOGUE_DELIVERY.md @@ -0,0 +1,48 @@ +# Cadastro e revisão de artigos — entrega8085 + +10/11/2026 · `SGF-STOCK-CATALOGUE-QA-v1`, autorizado expressamente. **Stock → Artigos e revisão do catálogo**, na instalação existente. Código, interface, persistência e provas entregues; sem nova demonstração, base, porta ou SGC. + +## Percurso concluído + +Pesquisar artigos existentes e consultar o processo/ficha. Artigos anteriores sem proveniência deste percurso permanecem só para consulta. Armazém QA2 prepara código, designação, unidade e fundamento; a intenção fica versionada, sem criar artigo ou saldo. Conferência QA12 escolhe a preparação actual, confere o impacto e aprova ou recusa. Só a aprovação disponibiliza o artigo no catálogo nativo existente. + +Uma nova preparação/revisão permite corrigir **apenas a designação de artigos criados neste percurso**. Código/unidade permanecem imutáveis. Não associa automaticamente um código anterior nem edita os artigos dos exemplos aceites. Proposta recusada não altera o catálogo. Origem, preparação/revisão, autor, data e fundamento conservam-se no histórico append-only; criação/alteração, proveniência, auditoria e recibo pertencem à mesma transacção nativa. + +A confirmação revalida autoridade actual, actor nativo activo, contexto, binding, versão, artigo de origem e impacto. Mudanças exigem nova conferência. Repetir a chave devolve o resultado original; nova sessão recupera recibo próprio por GET, sem executar operação. O campo de código respeita o limite nativo30; designação150 e unidade12, sem conversões implícitas. + +Pesquisa literal e páginas25/100; histórico por processo e estado. A interface conserva campos ao paginar/cancelar prévia e exige descarte explícito antes de trocar de registo. Sem escolha automática do primeiro artigo ou decisão. Código/unidade ficam só para leitura nas correcções. Formulário e origem ficam bloqueados durante prévia/confirmação incerta. + +## Resultado conservado e acessos + +Na8085 existe **um** artigo novo `QA-CATALOGO-INTEGRADO`, unidadeUN, designação fictícia, preparado2/revisto12 em duas versões/recibos. Sem saldo inicial, movimento, lote, preço, custo, compra, dívida ou lançamento. O mesmo ID aparece no leitor físico SELECT-only e no selector original de operações; nenhuma recepção ou saída foi repetida para provar essa ligação. + +Aplicados exactamente `stock.items.prepare` ao QA2 e `stock.items.review` ao QA12: duas concessões por livro existente, quatro no total8085/regressão. Consulta reutiliza `stock.view`. Sem nova pessoa, perfil, role PostgreSQL ou concessão SQL/PUBLIC; a ligação física de leitura mantém os três SELECTs e o binding original. A capacidade SQL da ligação nativa não substitui a nova autoridade de negócio, exigida pelo adaptador antes de qualquer escrita. + +Rootflag desligada por omissão; modo aprovado disponibilizado apenas na8085 escolhida/regressão. As estruturas novas são `sc_catalogue_meta`, `sc_catalogue_versions` e `sc_catalogue_commands`, apenas nos livros QA já existentes. Writers de movimentos, políticas originais e pacotes fixados preservados. + +## Provas dirigidas + +-34 regressão: intenção versus catálogo, aprovação distinta, versão antiga, código existente/imutabilidade, recusa/correcção de designação, histórico/recibo/contexto/revogação, recuperação após falha e preservação. Fixtures revertidas. A falha no recibo reverte artigo+versão+auditoria; a mesma confirmação recupera um artigo único. +-11 limites/guardas: código30/designação150, entradas inválidas, leitor sem preparação, módulo errado, prévia forjada e tentativa de alterar tenant. +- Concorrência na regressão: primeira aprovação mantém a transacção; segunda atinge o limite após cerca5,22s e devolve503; após libertação recusa409 por versão alterada. Repetição da confirmação original devolve200/replay. Um artigo e uma aprovação, sem duplicação. A fixture concorrente fica identificada na regressão, sem movimento. +-22 HTTP8085: duas confirmações conservadas, repetições sem duplicação, catálogo/selector/leitor físico, histórico e recusas. +-15 DOM→HTTP real: uma prévia revertida, zero confirmações adicionais, campos preservados, descarte explícito, artigo anterior só de consulta, código/unidade imutáveis e recuperação. +- Três guardas de navegação: função apenas quando disponibilizada e com `stock.view`; dois checksums dos scripts efectivamente servidos. + +85 verificações de catálogo, mais concorrência e checksums. Provas anteriores válidas reutilizadas. Não se repetiram suites de outros módulos. [Índice de provas/fontes](proofs/sgf-stock-catalogue-20261110.json). + +Comparação de38 conjuntos de dados originais e três bindings: intactos. Apenas duas concessões aprovadas, um artigo e duas auditorias de comando acrescentados na8085. Diário27, Caixa950, posições, cinco cálculos, artigos/movimentos anteriores e restantes exemplos preservados. Sem comandos/conexões dirigidos aos seis registos protegidos ou limpeza/apagamento da cache. Backups/dumps/dados privados fora do repositório; sem commit/checkpoint ou aceitação presumida. + +## Falha transversal de entrega de scripts encontrada e corrigida + +A conferência dos bytes publicados detectou que o router local não incluía alguns scripts SGF novos na lista de ficheiros públicos. Em vez de JavaScript, devolvia o HTML da aplicação com200. **API200, página legível e DOM com importação do código local não tinham comprovado que o navegador recebia o cliente correcto.** As provas desses níveis continuam válidas, mas não constituem prova de carregamento do script ou funcionamento visual. + +O router passou a servir dez nomes exactos de módulos públicos, incluindo as dependências partilhadas de Stock, documentos, Caixa, IVA, regras, bancos, indicadores e entrada. A extensão é limitada à composição8085 seleccionada; o corpo anterior e a lista de ficheiros anteriores foram preservados. Não abre acesso genérico ao filesystem. Módulo desconhecido na8085 devolve404 em vez do HTML da aplicação. Não houve reinício, nova porta, limpeza de cache ou alteração do encaminhamento das outras instalações. + +17 verificações de fronteiras/comparação do router e13 HTTP confirmam MIME JavaScript, bytes por SHA, dependência comum, query string,404 e script anterior. Esta correcção é transversal de entrega; não soma outra vez provas dos workflows nem declara revisão visual concluída. [Relatório específico](SGF_CLIENT_ASSET_DELIVERY.md). + +## Limites + +Não inclui armazéns novos, importação em massa, fusão, suspensão geral, conversões de unidade, valorização ou edição de artigos anteriores. O wrapper administrativo original conserva os seus bloqueios; estes ensaios não validam carga sustentada. Leitores físicos invalidam naturalmente referências antigas quando o catálogo muda; não houve limpeza de cache. + +Revisão visual real do conjunto e capacidade de implantação continuam pendentes. A via de navegador bloqueada não foi repetida. Fiscalidade geral e restantes lacunas da matriz não são declaradas concluídas por este cadastro. Sem acessos/operações reais, publicação, candidata de Compras, aceitação ou checkpoint. diff --git a/docs/SGF_STOCK_CATALOGUE_SCOPE.md b/docs/SGF_STOCK_CATALOGUE_SCOPE.md new file mode 100644 index 0000000..4404754 --- /dev/null +++ b/docs/SGF_STOCK_CATALOGUE_SCOPE.md @@ -0,0 +1,30 @@ +# Decisão agrupada — cadastro de artigos no Stock + +10/11/2026 · `SGF-STOCK-CATALOGUE-QA-v1`. **Autorizado expressamente e entregue; ver [relatório](SGF_STOCK_CATALOGUE_DELIVERY.md).** Conferência apenas de leitura em `scripts/sgf-stock-catalogue-access-readonly.php`: o SGF apresenta/pesquisa artigos existentes, mas não tem o percurso administrativo de preparação e revisão de novos artigos. Os direitos abaixo foram aprovados e aplicados apenas nos dois livros QA existentes. O texto conserva o âmbito da decisão. A capacidade SQL da ligação nativa não constitui autorização de negócio e não será usada para contornar essa ausência. + +## Resultado funcional proposto + +Na mesma8085, Stock → Artigos: pesquisar, consultar ficha/origem/histórico e preparar um novo artigo com código, designação, unidade e fundamento explícitos. Outra pessoa confere a versão e o impacto, aprova ou recusa. Só a aprovação cria o artigo no catálogo físico existente. Guardar proposta não o disponibiliza para recepção/consumo. + +O artigo aprovado poderá ser escolhido explicitamente pelos selectores já existentes; não cria movimento, saldo inicial, lote, preço, custo, compra, dívida ou lançamento. Permitir uma correcção revista de designação **apenas dos artigos criados por este percurso**, mantendo código/unidade imutáveis e histórico anterior. Não alterar os artigos dos exemplos aceites nem atribuir empresas/unidades automaticamente a artigos antigos. Armazéns, conversões de unidade, importação em massa, fusão de duplicados e suspensão geral de artigos não fazem parte desta decisão. + +Reutilizar contexto, revisão distinta, prévia/confirmar, recibo/chave e auditoria administrativos existentes. Novo adaptador de catálogo fará a escrita delimitada e a proveniência na mesma transacção nativa; não ampliar o writer de movimentos ou os SELECTs da ligação física de leitura. Versão, actor activo, vínculo físico, autoridade, código único e descrição escolhida são novamente conferidos na confirmação. Nunca apagar artigos ou histórico. + +## Dois direitos e duas concessões por livro QA + +Tenant1/empresa2/unidade2, apenas nos livros QA **8085 e regressão já existentes**: + +|Direito SGF novo|Única função a receber|Poder delimitado| +|---|---|---| +|`stock.items.prepare`|Armazém — Compras, utilizador QA2|Preparar novo artigo ou sucessora da designação de artigo criado neste percurso; não aprovar.| +|`stock.items.review`|Conferência técnica QA, utilizador12|Rever favoravelmente/recusar preparação alheia e aplicar a decisão actual; sem preparar ou alterar artigos anteriores.| + +Consulta reutiliza `stock.view`, já existente nestas funções. **Duas concessões por base, quatro no total**, sem nova pessoa, perfil, role PostgreSQL, direito PUBLIC, acesso real ou função nativa genérica `warehouse`. A ligação nativa existente já dispõe de escrita SQL e sequência; nenhuma concessão SQL adicional é proposta. A nova autoridade de negócio será exigida mesmo assim, antes de invocar o adaptador. + +Novas estruturas de propostas, versões, proveniência e recibos somente nesses dois livros QA; nenhuma migration ou instalação numa empresa/destino real. Não se presume que os poderes de confirmar movimentos ou rever lotes autorizam administrar o catálogo. + +## Provas e exemplo de utilização + +Na regressão: direitos/contexto/empresa/unidade, preparação versus revisão, recusa de revisão própria, versão antiga, código duplicado, código/unidade imutáveis, concorrência, repetição e recuperação após falha, atomicidade artigo+proveniência e ausência de movimentos. Na8085: um artigo explicitamente novo `QA-CATALOGO-INTEGRADO`, designação fictícia, unidadeUN, **sem saldo inicial**, preparado2/revisto12, ficha e histórico. Não repetir os antigos percursos de compra/recepção/valorização. + +Preservar27 lançamentos, Caixa950, documentos e posições, artigos e movimentos anteriores, cinco cálculos, seis registos/cache e trabalho alheio. Sem nova demonstração/base/porta, SGC, valorização, acessos/operações reais, publicação, candidata de Compras, aceitação/checkpoint ou declaração de capacidade de implantação. Revisão visual/carga separadas. O âmbito inclui o percurso completo descrito, não requer aprovação por campo ou teste. diff --git a/docs/SGF_STOCK_FINAL_SCOPE.md b/docs/SGF_STOCK_FINAL_SCOPE.md new file mode 100644 index 0000000..fd6d822 --- /dev/null +++ b/docs/SGF_STOCK_FINAL_SCOPE.md @@ -0,0 +1,53 @@ +# Fecho dos percursos pendentes de Stock na mesma8085 + +Estado: **autorizado expressamente em10/11/2026 — «Autorizar este fecho QA de Stock»; implementação em curso**. É uma extensão do âmbito já executado, não outra autorização para a entrada2/saída1/devolução0,5, já concluída e valorizada100/50/25. A aprovação não é uma declaração de testes, instalação ou aceitação concluídos. + +## Porque é necessária uma decisão + +A matriz de conclusão ainda identifica transferência em trânsito/recepção distinta e contagem por lotes. A transferência actualmente utilizável é imediata e atómica; a contagem integrada é sem lotes. Não apresentar estas variantes como equivalentes. + +A autorização anterior discriminou `stock.operations.prepare` e `stock.operations.confirm`. O controlador nativo de lotes exige funções próprias: `InventoryLots::allows`/`canActivate` exigem preparação e revisão distintas; o perfil Armazém actual não tem sequer `inventory_lot_read`/`inventory_lot_prepare`, e o revisor proposto não tem `inventory_lot_review`. Conferência feita sem conceder acessos: `scripts/sgf-stock-remaining-rights-check.php` e resultado privado indexado no relatório desta entrega. + +Regra do proprietário: «Se forem necessários novos direitos, apresenta-os antes de conceder acessos.» Não usar `stock.view`, a verificação técnica de Oficina ou uma flag para contornar a falta dessas autoridades. + +## Direitos mínimos propostos + +Somente nos dois livros QA já existentes, instalação8085 e regressão, tenant1/empresa2/unidade2. Nenhuma conta de aplicação, role PostgreSQL ou acesso real novo. + +| Camada | Função existente | Direito/acesso proposto | Limite obrigatório | +|---|---|---|---| +| SGF | QA conferência técnica (`workshop_verify`, actual utilizador12) | **novo** `stock.transfers.receive` | Receber parcelas de transferências destinadas apenas ao armazém QA1; sem emitir saídas genéricas ou receber noutra unidade. | +| SGF | A mesma função QA | **novo** `stock.lots.review` | Activação/libertação e revisão de contagem apenas do novo artigo de lotes no armazém QA integrado3. Não concede `stock.operations.confirm`. | +| Núcleo físico QA | Armazém actual, actor nativo6 | `inventory_lot_read`, `inventory_lot_prepare`, `inventory_lot_hold` | Funções existentes do núcleo, novas concessões sintéticas. Preparar/identificar/reter; não aprovar a própria configuração ou libertação. | +| Núcleo físico QA | Revisor distinto, actor nativo3 | `inventory_lot_read`, `inventory_lot_review`, `inventory_lot_release`, `inventory_adjust_review` | Funções existentes; outra pessoa para activação/libertação e aplicação da diferença do lote seleccionado. Sem função nativa genérica `warehouse`. | + +A ligação privada da função12 ao actor nativo3 será explícita, fixada por hash e auditada, preservando os restantes bindings. A recepção do novo protocolo de transferência tem a nova autoridade SGF específica, identidade nativa activa e destino revalidados; não imita uma autorização genérica de armazém que o actor não possui. As permissões nativas de lotes acima são as que o código original efectivamente exige. + +Não alargar os SELECTs da conta de leitura física actual: conserva as três tabelas. A consulta específica de lotes/trânsito será servida pelo adaptador autorizado, com filtros por pessoa/empresa/unidade/armazém e campos mínimos, sem expor documentos financeiros ou dados de outras origens. + +## Percurso em trânsito a entregar + +Transferir1UN do artigo QA já valorizado, do armazém integrado3 para o armazém QA1, mantendo propriedade na mesma empresa/unidade. Antes:1,5UN/75MZN. Após expedição:0,5UN disponíveis na origem e1UN em trânsito. Após recepção:0,5UN na origem e1UN no destino, trânsito0; valor próprio total75MZN conservado. Nenhuma alteração ao artigo original do exemplo de Compras. + +Preparação/prévia, expedição e recepção são decisões explícitas. Autoridades de origem/destino e versões voltam a ser conferidas; parcelas não podem ultrapassar o enviado. Uma recusa não inventa uma devolução física: o retorno à origem necessita da sua recepção declarada. Pendências em trânsito ficam visíveis e quantificadas. Header/eventos e movimentos usam a mesma transacção e livro físico, com chave única, autor/data/fundamento/estado anterior e posterior; sem outro saldo persistido. + +A transferência interna não cria receita, consumo ou outra dívida. A reconciliação contabilística deve distinguir localização disponível e propriedade em trânsito, mantendo o valor total e a referência à taxa revista. Não omitir movimentos do fecho por serem desconhecidos: a excepção interna precisa de prova de origem, propriedade, parcelas e valor reconciliados. Sem correcção de custos directos/industriais por arrasto. + +## Percurso de lotes a entregar + +Um terceiro artigo exclusivamente fictício `QA-LOTE-INTEGRADO`, no armazém3, com4UN em dois lotes identificados de2UN. Configuração preparada por Armazém e activada distintamente; identificação, retenção e libertação rastreáveis pelos serviços originais. Sem presumir validade de lotes reais. + +Contagem explícita: lote1=1,5UN, lote2=2UN, total3,5UN; diferença−0,5UN. A contagem é observação, não ajuste automático. Outra função revê/aplica a diferença após voltar a conferir versões de lotes e livro. Taxa QA20MZN/UN preparada/revista; entrada80MZN e diferença10MZN pelo mesmo diário, com conta artificial adicional `QA-DIFERENCA-INVENTARIO`. Saldo final deste artigo3,5UN/70MZN; artigo anterior permanece1,5UN/75MZN de propriedade total. Não somar quantidades de artigos diferentes; total monetário desses dois artigos145MZN. + +Sem novos tipos de imposto, salários, dinheiro, fiscalidade real ou valorização histórica. Contas/regras novas são apenas QA e têm conferência distinta. As alterações da origem, lote, contagem, taxa ou autoridade recusam a conferência antiga. O período2026 da apresentação conserva-se aberto; o fecho completo será ensaiado com rollback, sem reabertura. + +## Provas e condições de entrega + +- Ecrãs no mesmo SGF8085, selecção explícita de origem/destino/lotes, impacto antes de confirmar e recuperação do mesmo pedido. Não criar outra demonstração. +- Isolamento de empresa/unidade/armazém, recusa de auto-revisão, revogação, lote expirado/retido, versões antigas e soma incompatível de contagem. +- Duas recepções concorrentes não excedem a parcela restante; mesma chave não duplica eventos, movimentos ou lançamentos; erro após writer reverte também o recibo e permite recuperação. +- Saldo disponível/em trânsito/próprio e quantidades por lote reconciliam com o livro único; transferências não são contabilizadas como consumo. Ajuste e valorização conservam ligação à contagem/lotes e à taxa revista. +- Reutilizar provas válidas; testar apenas as fronteiras alteradas e o percurso novo. Sem índices no destino ou alegação de carga/implantação por inferência. +- Preservar16 lançamentos actuais, percurso C01–C10, seis registos protegidos, cache bloqueada e trabalho alheio. Sem publicação, candidata de Compras, SGC, credenciais públicas, novos servidores/bases/contas ou concessões reais. + +A aprovação deste recorte abrange a implementação, as sete concessões de funções nativas acima e os dois direitos SGF apenas nos perfis QA indicados, os novos dados fictícios, regras/conta artificiais e sua disponibilização limitada depois das provas. Não será pedida nova decisão por campo/teste deste mesmo recorte. A revisão visual continua separada: a ferramenta interrompeu o controlo por não conseguir determinar o URL do navegador e não será contornada. diff --git a/docs/SGF_STOCK_INACTIVITY_DELIVERY.md b/docs/SGF_STOCK_INACTIVITY_DELIVERY.md new file mode 100644 index 0000000..c15c1bb --- /dev/null +++ b/docs/SGF_STOCK_INACTIVITY_DELIVERY.md @@ -0,0 +1,29 @@ +# Stock — produtos sem movimento na instalação existente + +Entregue em10/11/2026 na mesma8085: Stock → Relatórios → Produtos sem movimento. Usa o direito existente `stock.view`, o contexto empresa/unidade/pessoa e a ligação física SELECT-only já instalada. Sem novos direitos, bases, demonstrações, índices ou comandos de Stock. Configuração de origem desligada por omissão; disponibilização limitada à instalação QA autorizada. + +## Significado e utilização + +Escolher explicitamente o dia de corte UTC, dias consecutivos sem registo físico (1–3650), código/nome literal, armazém autorizado e página25/100. O selector de armazéns é paginado e não escolhe o primeiro registo. A opção Existências no mesmo menu abre também a consulta física existente, em vez da indicação antiga de indisponibilidade. Com corte10/11 e30 dias, o intervalo sem registo vai de12/10 às00:00 até10/11 às23:59:59…; um último movimento de11/10 ainda é elegível, um de12/10 não. + +Só se conclui sobre pares artigo/armazém com movimento físico conhecido antes do fim do corte. Cada linha tem artigo/unidade, armazém, último registo UTC, dias e quantidade física exacta no corte. Artigos sem história não são atribuídos a armazéns, nem declarados inactivos ou com saldo zero. São considerados todos os tipos de movimento físico da fonte; não se classificam automaticamente causas comerciais. Metadados são actuais. Sem totais entre unidades, valorização, reservas, bloqueios ou disponibilidade. Um corte futuro consulta apenas registos conhecidos, sem prever actividade por registar. + +## Consulta e guardas + +Agrupamento, filtros, contagem e paginação SQL, sem transportar todo o universo para PHP. Cinco SELECTs na projecção: três de revisão existente, contagem e página; exclui autenticação/guardas do host e verificação da fonte. O hash de metadados existente continua agregado na base; não se declara optimização de carga/memória dessa revisão. + +Uma transacção própria REPEATABLE READ/READ ONLY e limite SQL3s. Referência cifrada600s ligada à revisão física, pessoa/contexto/filtros/tamanho; mudança exige nova leitura. A página seguinte conserva essa referência, sem a transformar em autorização de escrita. Direitos e mapeamento de armazéns são revalidados pelo controlador existente; falhas não apresentam saldos zero. O cliente descarta respostas de consultas abandonadas. + +## Provas dirigidas + +-23 verificações na regressão existente:128 pares frios, páginas25/100, fronteira UTC30/29 dias, pesquisa literal `%_!`, história futura/desconhecida excluída, precisão superior ao inteiro seguro JavaScript, cinco SELECTs nos dois tamanhos, armazém fora do mapa, contexto/filtro/referência adulterada/expirada, validações, refusas de actor/unidade, leitura sem escrita e nova revisão após append explícito. +- As fixtures são127 artigos frios e quatro casos de fronteira/recentidade/futuro/sem história,131 artigos no total;131 movimentos iniciais mais um append de revisão **apenas na regressão**. São metadados SQL de relatório, não recepções/ordens/custos executados pelos workflows. Nenhum livro de valorização foi criado/adoptado. Linhas físicas anteriores conservadas. A regressão usa a ligação nativa existente em transacção READ ONLY para testar a projecção; a prova da role SELECT-only efectiva é HTTP na8085. Não se criou uma role de leitura na regressão. +-15 verificações HTTP reais na8085: origem SELECT-only e opção no contexto, datas/páginas/quantidades, referências inválidas/alteradas, armazém não autorizado e actor de Compras recusados. Zero POSTs de negócio; apenas a escolha normal auditada de contexto. Corte futuro explícito usado para obter história elegível sem alterar datas/movimentos existentes. +-Sete verificações do cliente: renderização estática dos campos, selector e aviso, tamanhos25/100, descarte de resposta atrasada e transporte literal do filtro. Build privado aprovado, componente diferido. Não é revisão visual de navegador nem prova de todas as interacções DOM. +-Comparação dos quatro conjuntos completos na8085 antes/depois: `stock_movements`, `stock_items`, `warehouses`, `gl_journals` idênticos. Diário continua27, incluindo os25 anteriores preservados pelo incremento Caixa. Não se repetiram os comandos válidos dos outros módulos. + +Índice verificável: [provas](proofs/sgf-stock-inactivity-20261110.json). Dados, perfis, credenciais e provas detalhadas ficam fora do repositório. Assets anteriores e alterações alheias retidos; sem limpar a cache, tocar nos seis registos, SGC, publicação, candidata, aceitação ou checkpoint. + +## Limitações + +O relatório mede ausência de registo na história conhecida, não prova ausência de actividade real ou completude histórica. Não inclui produtos sem localização histórica, exportação, classificação por vendas/consumos, alertas ou criação de compras. Carga sustentada, revisão visual, conformidade e capacidade de implantação continuam separadas. Não houve ensaio de carga nem índices neste incremento. diff --git a/docs/SGF_STOCK_OPERATIONS_DELIVERY.md b/docs/SGF_STOCK_OPERATIONS_DELIVERY.md new file mode 100644 index 0000000..5a5b8f4 --- /dev/null +++ b/docs/SGF_STOCK_OPERATIONS_DELIVERY.md @@ -0,0 +1,41 @@ +# Stock: saídas e transferências pelo mesmo livro físico + +Actualização de10/11/2026: código instalado na8085, **ainda desligado e sem novos acessos concedidos**. A continuação acrescentou contagem física sem lotes, aplicação da diferença e selector de equipamento.38 verificações PG e15 de interface, mais seis leituras/guardas HTTP dirigidas na8085. Ver `docs/proofs/sgf-six-module-composition-20261110.json`. Não fecha o conjunto dos seis módulos. Os30/11 casos abaixo descrevem a primeira versão e não são somados novamente. + +## Percurso implementado + +`/stock/operations` permite escolher explicitamente artigo, armazém e responsável, pesquisar os catálogos por páginas25/100, indicar quantidade exacta, referência, destino e fundamento. Não selecciona o primeiro resultado. Mostra o impacto antes da confirmação individual. Disponibiliza histórico paginado e recuperação pelo mesmo recibo após resposta incerta. + +O adaptador chama o `StockOperationController`/`StockWorkflow` original, fixado pelo manifesto da composição. A saída usa o mesmo `IssueMovementWriter` já aceite; foi acrescentada a distribuição0.1.0 exigida pelo adaptador, byte a byte igual à distribuição original e ao writer já existente neste repositório. Não há motor de saldos novo. + +Uma transferência é **imediata e atómica**: dois movimentos opostos na mesma transacção. Não representa expedição em trânsito com recepção posterior. A quantidade e unidade mantêm-se; não cria valorização ou lançamento contabilístico. + +A prévia executa o writer num savepoint e reverte operações, movimentos, auditoria e dependências. Sequências PostgreSQL podem avançar. A confirmação revalida contexto/pessoa/direito/mapeamento, saldo e versão; executa novamente as políticas nativas e compara o impacto. Uma alteração de dependências recusa e reverte o comando. O recibo `sgf.stock.command`, o estado anterior, o impacto, o fundamento e os efeitos físicos são gravados na mesma transacção nativa. Repetição revalida autoridade e recupera o mesmo identificador. + +O runtime mantém as guardas físicas QA existentes. Armazém de origem e destino têm de constar do mapa explícito da pessoa/empresa/unidade. Um mapa adicional de Stock, quando necessário, exige SHA configurado e caminho fixo privado; não altera o binding de Compras. Bloqueios têm limite5s e resposta503 recuperável; comandos longos têm limite15s. Nenhuma destas capacidades fica activa por omissão. + +## Direitos propostos — ainda não concedidos na8085 + +| Direito | Âmbito | Capacidade | +| --- | --- | --- | +| `stock.view` existente | Empresa/unidade e armazéns explicitamente ligados | Consultar os catálogos, saldo e histórico autorizados | +| `stock.operations.prepare` novo | Empresa/unidade e mesmos armazéns | Preparar a conferência de saída/transferência | +| `stock.operations.confirm` novo | Empresa/unidade e mesmos armazéns | Confirmar individualmente e recuperar o próprio recibo | + +Os novos direitos não estão no catálogo público nem atribuídos a contas reais. A função nativa de Armazém, operador actual e bloqueio de manutenção continuam obrigatórios. O ensaio introduziu estes nomes apenas dentro de uma transacção revertida no host de regressão. Nenhuma concessão ficou persistida. Antes de activar/atribuir acessos na8085 é necessária decisão explícita sobre estes direitos e os perfis/armazéns de destino; a implementação não escolhe utilizadores por omissão. A proposta agrupada está em `docs/SGF_8085_NEXT_AUTHORIZATION.md`. + +## Provas dirigidas + +-30 verificações PostgreSQL/controller/middleware, reutilizando `receipt-composition-b4093c6f6707`, sem outra base, porta ou demonstração. Incluem isolamento de artigo/armazém, módulo errado, injecção de contexto, paginação/pesquisa literal, prévia sem efeitos, saída única, transferência equilibrada, duas conferências da mesma versão com recusa da segunda após confirmação da primeira, repetição sem duplicação, contexto/permissão revogados, prévia adulterada, falha injectada depois do writer antes do recibo e recuperação atómica. +-11 verificações da interface executam o módulo JavaScript real num DOM simulado. Confirmam escolha explícita, recolha do formulário antes de bloquear os campos, quantidade decimal, impacto legível, ausência de confirmação automática e recuperação com a mesma prévia após perda simulada de resposta. +-Todos os dados das tabelas do host/livro nativo de regressão comparados antes/depois são idênticos após rollback. Sequências **apenas desse livro QA** podem avançar. Não foram enviados comandos nem alterados ficheiros/configuração na8085, bases protegidas ou cache de dados. + +Os casos anteriores não foram somados novamente nem reexecutados como prova nova. A prova consolidada é `docs/proofs/sgf-stock-operations-20261110.json`. + +## Limites que continuam abertos + +Contagem e aplicação da diferença estão agora implementadas para o percurso sem lotes, com escolha explícita da contagem actual e recuperação sem a aplicar duas vezes. O impacto distingue quantidade contada, variação e saldo posterior. O selector de equipamento foi acrescentado; as políticas originais de peça/equipamento continuam a decidir admissibilidade. A prova positiva nova cobre o catálogo, não todas as combinações de peças/equipamentos. Devolução, contagem por lote e recepção em trânsito mantêm workflows próprios e ainda não são disponibilizadas por este ecrã. Não há entrada manual, custo ou contabilização automática. + +Os testes de duas conferências intercaladas não constituem ensaio novo de processos simultâneos. A resposta503 de bloqueio foi implementada, mas o limite com dois processos reais não está comprovado neste incremento. A perda de resposta do cliente foi simulada; não equivale a prova TCP. Revisão visual, carga sustentada, instalação única completa e capacidade de implantação permanecem pendências separadas. O incidente do conector de navegador no Portal Investidor não foi reaberto nem enviado. + +Continuar a composição dos restantes módulos do SGF; esta entrega não constitui aceitação global, publicação ou checkpoint. diff --git a/docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md b/docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md new file mode 100644 index 0000000..99f1384 --- /dev/null +++ b/docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md @@ -0,0 +1,23 @@ +# Políticas de Stock e devolução ao fornecedor —10/11/2026 + +Implementado e disponível na mesma8085, Stock → painel/Devoluções → **Abrir políticas e devoluções ao fornecedor**, entrada directa `/stock/completion` após escolha explícita da função. Preparação por Armazém; revisão por Conferência Oficina; execução física por Armazém, pessoa diferente do revisor. Usa `SupplyPlanning`, `InventoryDispositions` e `StockWorkflow` originais, no mesmo livro. Sem nova demonstração, base, porta, utilizador ou role PostgreSQL. + +O bloco e os direitos discriminados foram expressamente autorizados pelo utilizador. A instalação anterior tinha18 lançamentos; o novo artigo QA5 recebeu10UN pelo writer de recepção original, com taxa20MZN/UN preparada e revista distintamente. A devolução2UN usa a linha de recepção exacta, quantidade cumulativa elegível e comprovativo. Termina em8UN/160MZN. Não toca nos artigos anteriores. A política aprovada fixa segurança2, mínimo5, alvo10, máximo20, consumo diário1, prazo2+1dias, horizonte14, encomenda mínima/embalagem1UN. Não há regra automática de compra. + +A saída física não reduz dívida nem emite crédito. A Contabilidade prepara/revê/aplica individualmente entrada200 e devolução40 no mesmo diário, D QA-DEVOLUCAO-FORNECEDOR/C QA-STOCK para a devolução. Esta conta QA é compensatória; não representa nota de crédito de fornecedor. O diário passou a20 lançamentos equilibrados. A valorização verifica aprovação nativa, executor distinto, origem, unidade, quantidade, operação e movimento; os factos imutáveis não incluem uma soma cumulativa mutável na sua impressão. + +## Provas novas e reutilizadas + +- 22 verificações dirigidas em transacções revertidas na regressão existente: direitos, origem estrangeira/histórica recusada, limite físico, revisão distinta, versão antiga, prévia sem movimento, revogação, falha depois do writer com rollback, repetição única, valorização200/40 e saldo160, constraints nativas e preservação. Prova privada `receipt-composition-b4093c6f6707/single-integration/20261110-162212-7fc0c484/stock-completion-result.json`. +- Concorrência real entre dois processos somente nessa regressão: bloqueio da linha de tenant nativo enquanto a primeira transacção mantém o writer aberto; segunda responde503 após o limite5s; depois da libertação responde409 por conferência antiga. Primeira chave recupera o resultado; exactamente um novo movimento. Prova `stock-completion-concurrency/result.json`. +- 11 verificações HTTP na8085, incluindo ecrã autenticado, cinco confirmações com repetição, uma devolução, origens valorizadas e diário equilibrado. Prova privada `receipt-composition-cc50a276c10c/stock-completion-http-20261110.json`. +- Comparação SELECT-only com o backup anterior ao bloco:18 lançamentos e todos os movimentos, clientes e facturas originais byte a byte intactos; binding original intacto. Prova `single-integration/20261110-162658-c0b15a30/next-block-preservation.json`. Bases dos seis registos protegidos não são alvo destes scripts; não foram relidas para decidir associações. Nenhum comando de limpeza da cache. +- Build privado aprovado; assets anteriores retidos. Provas anteriores de Stock/Compras reutilizadas, sem repetir suites ou operações aceites. + +## Limites separados + +Recorte QA manual, artigo/local/recepção fixados e pinados; não é parametrização geral de produção. Sem previsão histórica, acordos, importação, compras automáticas, abate ou estorno histórico. Listagem/estados/páginas25/100 e versões na SQL; não é ensaio de carga sustentada. Alterar o estado invalida a conferência, e as recusas transitórias conservam a mesma chave. A prévia executa o serviço original dentro de savepoint revertido; sequências podem avançar. O serviço nativo pode registar avaliação da necessidade, mas não tem regra aprovada para gerar compras neste recorte. + +Revisão visual de navegador, acessibilidade completa, carga sustentada, armazenamento/retensão e capacidade de implantação continuam pendências próprias. Resposta200 e HTML não constituem aceitação visual. Sem publicação, activação de candidata de Compras, SGC, acessos reais, aceitação ou checkpoint presumidos. Documentos adicionais do bloco continuam em desenvolvimento e não são declarados concluídos por esta entrega. + +- 17 verificações adicionais DOM→HTTP: prévia real revertida, formulário bloqueado, perda de resposta e repetição com o mesmo conteúdo (transportes de confirmação simulados; zero confirmações reais), consulta do histórico nativo de versões3/2/1. Prova privada stock-completion-dom-http-20261110.json. Não é revisão visual. diff --git a/docs/SGF_USO_LOCAL.md b/docs/SGF_USO_LOCAL.md new file mode 100644 index 0000000..b33a951 --- /dev/null +++ b/docs/SGF_USO_LOCAL.md @@ -0,0 +1,29 @@ +# Usar o SGF local na8085 + +Entrada única: http://127.0.0.1:8085/sgf. Seleccionar uma função QA e depois o módulo. A entrada antiga de Compras permanece em http://127.0.0.1:8085/compras. Não é necessário arrancar o SGC, criar outra demonstração ou conhecer palavras-passe novas. + +Este é o recorte privado autorizado, com dados fictícios. Os seis módulos e percursos obrigatórios estão integrados; a aceitação integral para apresentação continua separada da conclusão funcional. Consultar o [resultado actual e limitações](SGF_SIX_MODULE_CLOSURE_20261110.md) e o [guião dos seis módulos](SGF_GUIAO_APRESENTACAO.md). + +| Para consultar | Função e percurso | +|---|---| +| Documento comercial1000MZN e PDF | Comercial — preparação; escolher Comercial e abrir documentos emitidos. | +| Aquisição aceite até obrigação100MZN | Funções anteriores de Compras; conservar REQ-2026-000003, OC-2026-000001 e FACT-COMPRA-COMPLETA-20261104. | +| Saldo e movimentos físicos | Armazém — Compras; escolher Stock. As operações de Stock mostram o impacto antes da confirmação. | +| Ordem de oficina, tarefa e materiais | Gestão/técnico/verificador de Oficina; escolher Oficinas conforme a responsabilidade. | +| Recebível1000 e saldo400 após reversão | Financeiro — preparação/revisão; escolher Financeiro, recebíveis. | +| Obrigação original e afectação20/reversão20 | Financeiro — preparação/revisão; escolher Financeiro e obrigações a fornecedores. O saldo actual é100MZN; o histórico preserva o percurso100→80→100. | +| Custos295 e1820 e respectivas versões anteriores | Financeiro — preparação/revisão; custos directos e industriais. O residual industrial300 está separado. | +| Diário comum, pares de correcção e balancete | Contabilidade — preparação/revisão; escolher Contabilidade e diário integrado. Existem18 lançamentos equilibrados/15 contas QA. | +| Trânsito, lotes e inventário | Armazém — Compras, módulo Stock; abrir trânsito/lotes/inventário. A função Oficina — conferência técnica, escolhendo Stock, faz recepção distinta, activação/libertação e revisão de ajuste. | +| Oficina → Comercial500MZN | Financeiro — preparação/revisão, módulo Oficinas, Oficina → rascunho. O botão de continuação escolhe Comercial explicitamente e abre o mesmo rascunho; não emite automaticamente outra factura. | +| Consulta sem decisões | Função de consulta, escolhendo o módulo autorizado. | + +Para uma decisão nova, escolher explicitamente a origem e conferir o impacto. A preparação não substitui a revisão por outra função. Cada confirmação volta a verificar o estado. Não executar novamente os exemplos anteriores só para os consultar. + +Se uma resposta se perder, conservar a página e usar o mesmo botão de recuperação. Os campos ficam protegidos e a mesma chave recupera a operação original. Uma recusa de versão/contexto exige nova conferência; não alterar IDs ou criar outra chave para contornar a recusa. + +No diário, os filtros de origem têm páginas de25/100 por família. O relatório financeiro tem páginas independentes para pagamentos, propostas, afectações e eventos, conservando os filtros. Uma página vazia não autoriza escolher outro documento automaticamente. + +O lançador `scripts/abrir-sgf.ps1` conserva a instalação8085; `scripts/abrir-compras.ps1` também permanece. Arranque/reutilização e reinício dirigido do PHP já foram conferidos, com29 tabelas canónicas idênticas. Não repetir migrations globais, resets, limpeza da cache ou criação de bases/portas. Reinício completo do computador/cluster partilhado parado ainda não foi ensaiado. + +Oficina–Stock/devolução/valorização100/50/25, Oficina–Comercial500, continuidades contabilísticas, lotes/ajuste80/10 e fecho QA integral foram concluídos. O fecho foi revertido no ensaio; a apresentação mantém período aberto. O utilizador confirmou abertura/legibilidade do novo Stock. Revisão visual restante, aceitação do conjunto, carga sustentada e implantação conservam pendências próprias. Nenhuma operação real/fiscal/bancária é validada por estas provas. diff --git a/docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md b/docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md new file mode 100644 index 0000000..bc4c110 --- /dev/null +++ b/docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md @@ -0,0 +1,15 @@ +# Oficinas — agenda e equipamentos na8085 + +10/11/2026. Implementação funcional instalada na mesma8085, sem novas demonstrações, direitos, contas ou serviço SGC. Usa `workshop.view/assign/open` existentes. Não é só um catálogo de funções. + +- **Agenda:** ordem existente, técnico já atribuído e equipamento; calendário por intervalo máximo93dias, filtros/SQL25/100, disponibilidade sem sobreposição de técnico/equipamento, fronteiras adjacentes permitidas. Prévia não reserva; confirmação volta a conferir fonte/versão/contexto/direitos. Reagendamento e cancelamento fundamentados são versões append-only com histórico; cancelamento não elimina a reserva antiga. Lembretes apenas visuais (próximas24h/em curso/decorrido), sem enviar mensagens ou declarar trabalho concluído. Horários com zona explícita, armazenados em UTC e mostrados UTC+02. +- **Cadastro de equipamentos:** registo independente por cliente explicitamente associado a Oficinas, identificador, descrição/marca/modelo e série. Pesquisa literal/páginas25/100, escolha do cliente e prévia, confirmação auditada/idempotente. Identificação original imutável; sem troca automática de proprietário. Técnicos apenas vêem equipamentos das ordens que lhes estão atribuídas. Nova ordem pode seleccionar equipamento existente, por cliente/pesquisa, em vez de redigitar ID ou duplicar cadastro. O writer original da ordem é reutilizado. +- **Entrada de apresentação corrigida:** o atalho de consulta de Oficinas usa agora a função QA de gestão já autorizada. A função genérica de consulta tem `workshop.view`, mas a política original só lhe mostrava ordens atribuídas; não foi alargada para contornar essa restrição. + +Provas próprias:20 verificações de agenda em regressão, dois processos concorrentes (503 no bloqueio de empresa,409 após libertação e nova reserva adjacente sem sobreposição),14 verificações HTTP8085,15 verificações do equipamento/ordem em regressão e14 HTTP8085. Não repetidas as aquisições, pagamentos ou lançamentos anteriores. Recuperação cliente reutiliza o mecanismo já provado `WorkshopStockOperation`. + +No exemplo8085, marcação da ordem existente em12/11/2026 foi reagendada de09–10 para10–11 UTC+02, com ambas as versões preservadas. Um equipamento QA novo foi registado sem abrir ordem automaticamente. O ensaio de reutilização do ID pelo writer original foi feito e revertido na regressão. Os18 lançamentos continuam18; os livros de stock, clientes, associações protegidas e cache não foram alterados por estas funções. + +Limites: não há lembretes externos, salários, gestão completa de recursos humanos, calendário sincronizado, correcção mutável de identificações antigas, fiscalidade ou efeitos de stock/custo por agendar. O estado «intervalo decorrido» não significa serviço realizado. A agenda não reatribui técnico ou autoriza tarefa automaticamente; utiliza as decisões da ordem. A revisão visual dos novos ecrãs e carga sustentada continuam sem declaração de validação. + +As funcionalidades ainda em falta foram conferidas nos menus/código. Orçamentos comerciais/reparação, documentos adicionais de teste e política/disposição Stock estão no bloco [explicitamente autorizado em seguida](SGF_NEXT_BLOCK_RIGHTS.md). Não são apresentados como concluídos por esta entrega. diff --git a/docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md b/docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md new file mode 100644 index 0000000..2c42989 --- /dev/null +++ b/docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md @@ -0,0 +1,16 @@ +# Oficina — ordens por equipamento + +11/11/2026. Entregue na mesma8085 em **Oficinas → Equipamentos → Consultar ordens deste equipamento**. A escolha é individual, sem seleccionar o primeiro registo. Apresenta código, descrição, série e cliente actuais, ordens associadas, responsável e apontamentos. Permite pesquisa/estado/datas, páginas25/100, CSV da página, abertura da ordem exacta e regresso à pesquisa do catálogo. + +Reutiliza o catálogo e `/api/workshop/orders` com `equipment_id` validado. O servidor resolve o equipamento autorizado na mesma transacção de leitura consistente das ordens e aplica o filtro na SQL antes da paginação. Mantém contexto empresa/unidade/módulo, associação do cliente e atribuição actual do técnico. Equipamento fora do catálogo autorizado é recusado antes de expor a contagem. Equipamento sem ordens devolve a sua identificação e zero resultados, sem substituir por outro. + +Resposta de outro equipamento é recusada pelo cliente. O botão fica bloqueado enquanto há formulário, prévia ou confirmação em curso. O regresso conserva a pesquisa original. Carregamento diferido evita incorporar a lista completa de ordens no pacote principal. Identificação e apontamentos são actuais; não constituem histórico imutável de reparações, conclusão, entrega ou facturação. + +## Verificação + +-23 verificações de regressão:28 ordens/um equipamento SQL artificiais,25+2/100, filtros combinados, páginas vazias, equipamento irmão/estrangeiro, técnico atribuído, módulo/flag e parâmetros inválidos. Todas as fixtures revertidas; sete conjuntos originais iguais. Não se repetiram workflows de ordens ou cadastro. +-12 verificações dos handlers reais com respostas simuladas: segunda escolha explícita, regresso/pesquisa, filtros/páginas, abertura exacta, metadados, resposta de outro equipamento recusada e CSV protegido. Não é revisão visual. +-17 HTTP na8085 com fontes efectivas: paridade com ordens existentes, equipamento sem ordens, filtros25/100, período, recusas e atribuição. JS principal/lazy servidos com MIME e SHA correctos. +-56 conjuntos de dados idênticos antes/depois da instalação e das leituras, incluindo as cinco tabelas de demonstrações internas recém-entregues. Permissões/binding/27 lançamentos e dados anteriores intactos; zero comandos de negócio ou concessões. Seis registos e cache não foram alvo de escrita/limpeza. + +[Índice de provas](proofs/sgf-workshop-equipment-orders-20261111.json). São52 verificações dirigidas; provas anteriores reutilizadas sem repetição. Só cinco ficheiros de fonte e os assets compilados foram copiados para a instalação existente, com anteriores conservados em privado. Sem nova demonstração, base, porta, SGC, publicação, candidata de Compras, aceitação ou checkpoint. Visual, carga sustentada e implantação mantêm provas próprias pendentes. diff --git a/docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md b/docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md new file mode 100644 index 0000000..48d6ec6 --- /dev/null +++ b/docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md @@ -0,0 +1,18 @@ +# Oficina — relatório por abertura de ordens + +10/11/2026. Entregue na mesma8085: **Oficinas → Relatórios**, escolha explícita de datas de abertura, pesquisa/estado, páginas25/100 e CSV da página efectivamente consultada. Reutiliza `/api/workshop/orders`, `workshop.view` e a atribuição actual ao técnico. Nenhum novo direito, esquema ou comando de negócio. + +O intervalo usa o dia local já definido pela Oficina, `Africa/Johannesburg`: início incluído, meia-noite seguinte ao último dia excluída. A resposta identifica o intervalo UTC exacto. O filtro incide sobre `wo_orders.created_at` na SQL, antes de contar/paginar. Não corta os apontamentos pela data: mostra identificação, responsável e apontamentos **actuais** das ordens abertas no período. Não é fecho diário, relatório histórico de produção, entrega, facturação ou pagamento. + +O modo Relatórios inicia sem datas e exige ambas antes da consulta. Painel/equipa conservam a lista sem corte, podendo indicar datas. Alterar filtros sem pesquisar não muda os dados exportados: o CSV usa o pedido aplicado e a página devolvida, inclui instante de observação, zona, total de ordens do filtro e os minutos/apontamentos dessa página. Não soma materiais, custos ou tempos do universo. Textos livres são protegidos contra fórmulas e escapados com aspas/quebras de linha. Resposta atrasada ou período/página incompatíveis não são aceites como o relatório pedido. + +## Provas e preservação + +-21 verificações dirigidas na regressão:30 ordens SQL apenas de leitura de relatório, fronteiras de meia-noite/microssegundos,25+3/100, ordenação, combinação de estado/pesquisa, intervalo alargado/vazio, datas inválidas/invertidas/incompletas, atribuição do técnico e módulo recusado. Todas as fixtures revertidas, cinco conjuntos originais comparados. Não são ordens criadas pelo workflow. +-13 de apresentação/contrato/CSV: datas obrigatórias só no relatório, ausência de indicadores inventados, resposta com período/página errados recusada, fonte efectiva, limite de página e escapagem. Renderização simulada não constitui revisão visual. +-15 HTTP8085: dia local de uma ordem efectivamente existente, paridade com a lista anterior,25/100, pesquisa/estado, páginas vazias, argumentos/contexto recusados, atribuição e CSV. Ficheiros JS principal/lazy realmente servidos com MIME e SHA iguais aos instalados. +-48 conjuntos actuais idênticos antes/depois, incluindo condições de vencimento/recibos recentes, artigos, permissões, ordens e diário; binding intacto. Diário27, Caixa950, cinco cálculos e todas as operações anteriores conservados. Zero comandos de negócio ou concessões na8085; seis registos e cache não foram alvo de comandos. + +São49 verificações dirigidas, sem repetir as suites de emissão, recebimentos, Stock, Caixa, vencimentos ou lançamentos. [Índice das provas](proofs/sgf-workshop-period-20261110.json). Código compilado em privado, ficheiros anteriores conservados; nenhuma limpeza de cache/reinício, demonstração, SGC, publicação, candidata de Compras, aceitação ou checkpoint. + +O leitor continua sem mutex de escrita da empresa. A prova anterior desse comportamento foi reutilizada, não repetida nem apresentada como nova medição de carga. Carga sustentada, revisão visual e implantação permanecem separadas. Não entrega corte histórico, fecho diário imutável, PDF arquivado, períodos contabilísticos ou relatórios legais. Datas/valores são os das fontes QA existentes; esta entrega não declara o produto completo. diff --git a/docs/integration/sgf-8085-access-proposal.json b/docs/integration/sgf-8085-access-proposal.json new file mode 100644 index 0000000..f6e50e6 --- /dev/null +++ b/docs/integration/sgf-8085-access-proposal.json @@ -0,0 +1,761 @@ +{ + "version": "SGF-8085-SYNTHETIC-ACCESS-PROPOSAL-v1", + "approved": true, + "applied": true, + "installation": "cc50a276c10c", + "port": 8085, + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "new_database_roles": false, + "real_accounts": false, + "credentials_in_repository": false, + "source_policy": "Existing right names must be checked against the installed CHECK before any grant; fail on unknown names rather than broaden privileges.", + "profiles": [ + { + "id": "commercial_prepare", + "label": "QA Comercial — preparação", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "clients.view", + "scope": "company" + }, + { + "permission": "services.view", + "scope": "company" + }, + { + "permission": "service_drafts.view", + "scope": "unit" + }, + { + "permission": "billing_drafts.view", + "scope": "unit" + }, + { + "permission": "service_invoices.view", + "scope": "unit" + }, + { + "permission": "clients.create", + "scope": "company" + }, + { + "permission": "clients.edit", + "scope": "company" + }, + { + "permission": "services.create", + "scope": "company" + }, + { + "permission": "services.edit", + "scope": "company" + }, + { + "permission": "service_drafts.create", + "scope": "unit" + }, + { + "permission": "service_drafts.edit", + "scope": "unit" + }, + { + "permission": "billing_drafts.create", + "scope": "unit" + }, + { + "permission": "billing_drafts.edit", + "scope": "unit" + }, + { + "permission": "billing_drafts.confirm_internal", + "scope": "unit" + }, + { + "permission": "issuer_documents.view", + "scope": "company" + }, + { + "permission": "issuer_documents.edit", + "scope": "company" + }, + { + "permission": "client_documents.view", + "scope": "company" + }, + { + "permission": "client_documents.edit", + "scope": "company" + }, + { + "permission": "draft_documents.view", + "scope": "unit" + }, + { + "permission": "draft_documents.edit", + "scope": "unit" + }, + { + "permission": "service_fiscal.view", + "scope": "company" + }, + { + "permission": "service_fiscal.propose", + "scope": "company" + }, + { + "permission": "service_fiscal.calculate", + "scope": "unit" + }, + { + "permission": "invoice_series.view", + "scope": "company" + }, + { + "permission": "invoice_series.propose", + "scope": "company" + }, + { + "permission": "service_invoices.issue", + "scope": "unit" + }, + { + "permission": "service_invoices.correction_request", + "scope": "unit" + }, + { + "permission": "document_preview.configure", + "scope": "company" + } + ] + }, + { + "id": "commercial_review", + "label": "QA Comercial — revisão", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "clients.view", + "scope": "company" + }, + { + "permission": "services.view", + "scope": "company" + }, + { + "permission": "service_drafts.view", + "scope": "unit" + }, + { + "permission": "billing_drafts.view", + "scope": "unit" + }, + { + "permission": "service_invoices.view", + "scope": "unit" + }, + { + "permission": "service_fiscal.view", + "scope": "company" + }, + { + "permission": "service_fiscal.approve", + "scope": "company" + }, + { + "permission": "invoice_series.view", + "scope": "company" + }, + { + "permission": "invoice_series.approve", + "scope": "company" + }, + { + "permission": "issuer_documents.view", + "scope": "company" + }, + { + "permission": "client_documents.view", + "scope": "company" + }, + { + "permission": "draft_documents.view", + "scope": "unit" + } + ] + }, + { + "id": "workshop_manage", + "label": "QA Oficinas — gestão", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "clients.view", + "scope": "company" + }, + { + "permission": "workshop.view", + "scope": "unit" + }, + { + "permission": "workshop.open", + "scope": "unit" + }, + { + "permission": "workshop.assign", + "scope": "unit" + }, + { + "permission": "workshop.authorize", + "scope": "unit" + } + ] + }, + { + "id": "workshop_record", + "label": "QA Oficinas — técnico", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "workshop.view", + "scope": "unit" + }, + { + "permission": "workshop.record", + "scope": "unit" + }, + { + "permission": "stock.view", + "scope": "unit" + }, + { + "permission": "workshop_stock.view", + "scope": "unit" + }, + { + "permission": "workshop_stock.prepare", + "scope": "unit" + } + ] + }, + { + "id": "workshop_verify", + "label": "QA Oficinas — conferência", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "workshop.view", + "scope": "unit" + }, + { + "permission": "workshop.verify", + "scope": "unit" + }, + { + "permission": "stock.view", + "scope": "unit" + }, + { + "permission": "workshop_stock.view", + "scope": "unit" + } + ] + }, + { + "id": "finance_prepare", + "label": "QA Financeiro — preparação", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "service_receivables.view", + "scope": "unit" + }, + { + "permission": "service_receivables.prepare", + "scope": "unit" + }, + { + "permission": "service_receivables.allocate", + "scope": "unit" + }, + { + "permission": "service_receivables.correction_request", + "scope": "unit" + }, + { + "permission": "direct_costs.view", + "scope": "unit" + }, + { + "permission": "industrial_costs.view", + "scope": "unit" + }, + { + "permission": "industrial_costs.prepare", + "scope": "unit" + }, + { + "permission": "industrial_budgets.view", + "scope": "unit" + }, + { + "permission": "industrial_budgets.prepare", + "scope": "unit" + }, + { + "permission": "workshop_billing.view", + "scope": "unit" + }, + { + "permission": "workshop_billing.prepare", + "scope": "unit" + }, + { + "permission": "workshop_billing.generate", + "scope": "unit" + }, + { + "permission": "workshop.view", + "scope": "unit" + }, + { + "permission": "direct_costs.prepare", + "scope": "unit" + }, + { + "permission": "industrial_budget_comparisons.view", + "scope": "unit" + }, + { + "permission": "industrial_budget_comparisons.prepare", + "scope": "unit" + }, + { + "permission": "clients.view", + "scope": "company" + }, + { + "permission": "services.view", + "scope": "company" + }, + { + "permission": "service_drafts.view", + "scope": "unit" + }, + { + "permission": "service_drafts.create", + "scope": "unit" + }, + { + "permission": "billing_drafts.view", + "scope": "unit" + }, + { + "permission": "billing_drafts.create", + "scope": "unit" + }, + { + "permission": "workshop_corrections.view", + "scope": "unit" + }, + { + "permission": "workshop_corrections.request", + "scope": "unit" + }, + { + "permission": "workshop_corrections.apply", + "scope": "unit" + } + ] + }, + { + "id": "finance_review", + "label": "QA Financeiro — revisão", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "service_receivables.view", + "scope": "unit" + }, + { + "permission": "service_receivables.confirm", + "scope": "unit" + }, + { + "permission": "service_receivables.reconcile", + "scope": "unit" + }, + { + "permission": "service_receivables.correction_approve", + "scope": "unit" + }, + { + "permission": "direct_costs.view", + "scope": "unit" + }, + { + "permission": "direct_costs.validate", + "scope": "unit" + }, + { + "permission": "industrial_costs.view", + "scope": "unit" + }, + { + "permission": "industrial_budgets.view", + "scope": "unit" + }, + { + "permission": "workshop_billing.view", + "scope": "unit" + }, + { + "permission": "workshop_billing.review", + "scope": "unit" + }, + { + "permission": "workshop.view", + "scope": "unit" + }, + { + "permission": "industrial_costs.validate", + "scope": "unit" + }, + { + "permission": "industrial_budgets.validate", + "scope": "unit" + }, + { + "permission": "industrial_budget_comparisons.view", + "scope": "unit" + }, + { + "permission": "clients.view", + "scope": "company" + }, + { + "permission": "services.view", + "scope": "company" + }, + { + "permission": "workshop_corrections.view", + "scope": "unit" + }, + { + "permission": "workshop_corrections.review", + "scope": "unit" + } + ] + }, + { + "id": "accounting_prepare", + "label": "QA Contabilidade — preparação", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "catalogue_accounting.view", + "scope": "company" + }, + { + "permission": "catalogue_accounting.propose", + "scope": "company" + }, + { + "permission": "general_ledger.view", + "scope": "unit" + }, + { + "permission": "general_ledger.prepare", + "scope": "unit" + }, + { + "permission": "general_ledger.post", + "scope": "unit" + }, + { + "permission": "general_ledger.correct_prepare", + "scope": "unit" + }, + { + "permission": "general_ledger.correct_post", + "scope": "unit" + }, + { + "permission": "general_ledger.period_prepare", + "scope": "unit" + }, + { + "permission": "general_ledger.period_close", + "scope": "unit" + }, + { + "permission": "purchasing_obligations.view", + "scope": "unit" + }, + { + "permission": "purchasing_invoices.view", + "scope": "unit" + } + ] + }, + { + "id": "accounting_review", + "label": "QA Contabilidade — revisão", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "catalogue_accounting.view", + "scope": "company" + }, + { + "permission": "catalogue_accounting.approve", + "scope": "company" + }, + { + "permission": "general_ledger.view", + "scope": "unit" + }, + { + "permission": "general_ledger.validate", + "scope": "unit" + }, + { + "permission": "general_ledger.correct_validate", + "scope": "unit" + }, + { + "permission": "general_ledger.period_validate", + "scope": "unit" + }, + { + "permission": "purchasing_obligations.view", + "scope": "unit" + }, + { + "permission": "purchasing_invoices.view", + "scope": "unit" + } + ] + }, + { + "id": "read_only", + "label": "QA Consulta — seis módulos", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "clients.view", + "scope": "company" + }, + { + "permission": "services.view", + "scope": "company" + }, + { + "permission": "service_drafts.view", + "scope": "unit" + }, + { + "permission": "billing_drafts.view", + "scope": "unit" + }, + { + "permission": "service_invoices.view", + "scope": "unit" + }, + { + "permission": "workshop.view", + "scope": "unit" + }, + { + "permission": "direct_costs.view", + "scope": "unit" + }, + { + "permission": "industrial_costs.view", + "scope": "unit" + }, + { + "permission": "service_receivables.view", + "scope": "unit" + }, + { + "permission": "catalogue_accounting.view", + "scope": "company" + }, + { + "permission": "general_ledger.view", + "scope": "unit" + }, + { + "permission": "stock.view", + "scope": "unit" + } + ] + } + ], + "existing_stock_profile": { + "role": "validator", + "user_id": 2, + "rights": [ + "stock.operations.prepare", + "stock.operations.confirm", + "workshop.view", + "workshop_stock.view", + "workshop_stock.confirm" + ], + "warehouse_policy": "existing main warehouse and one new empty explicitly mapped synthetic warehouse; no foreign warehouse" + }, + "new_right_names": [ + "stock.operations.prepare", + "stock.operations.confirm" + ], + "forbidden": [ + "access.manage", + "automatic legacy module association", + "changes to six protected records", + "cache clearing", + "real money or fiscal documents", + "period reopening", + "publication", + "new demo/server/database", + "SGC connector activation" + ], + "financial_source_policy": { + "payables": "retain authoritative native obligation, no copy of debt", + "ledger": "one common ledger in existing native book with explicit SGF company/unit scope", + "native_finance_bindings": { + "finance_prepare": 4, + "finance_review": 7, + "accounting_prepare": 4, + "accounting_review": 7, + "read_only": 8 + }, + "real_permissions": false, + "sgc_server_required": false + }, + "authorization": { + "date": "2026-11-10", + "decision": "autorizo", + "scope": "docs/SGF_8085_NEXT_AUTHORIZATION.md" + }, + "integration_grants": { + "finance_prepare": { + "unit": [ + "supplier_payables.view", + "service_invoices.view" + ] + }, + "finance_review": { + "unit": [ + "supplier_payables.view", + "service_invoices.view" + ] + }, + "read_only": { + "unit": [ + "supplier_payables.view" + ] + }, + "accounting_prepare": { + "unit": [ + "service_invoices.view", + "service_receivables.view", + "workshop.view", + "direct_costs.view", + "industrial_costs.view" + ], + "company": [ + "clients.view", + "services.view" + ] + }, + "accounting_review": { + "unit": [ + "service_invoices.view", + "service_receivables.view", + "workshop.view", + "direct_costs.view", + "industrial_costs.view" + ], + "company": [ + "clients.view", + "services.view" + ] + } + }, + "integration_grants_reason": "Minimum existing read rights needed for the approved cross-module origin conferences; no new permission names or business-write authority." +} diff --git a/docs/integration/sgf-bank-reconciliation-access-proposal.json b/docs/integration/sgf-bank-reconciliation-access-proposal.json new file mode 100644 index 0000000..f568e4e --- /dev/null +++ b/docs/integration/sgf-bank-reconciliation-access-proposal.json @@ -0,0 +1,89 @@ +{ + "version": "SGF-BANK-RECONCILIATION-QA-v1", + "approved": true, + "applied": true, + "scope_file": "docs/SGF_BANK_RECONCILIATION_SCOPE.md", + "existing_books": [ + "cc50a276c10c", + "b4093c6f6707" + ], + "new_right_names": [ + "bank_reconciliation.view", + "bank_reconciliation.import", + "bank_reconciliation.prepare", + "bank_reconciliation.review" + ], + "grants_per_book": [ + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 13, + "permission": "bank_reconciliation.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 14, + "permission": "bank_reconciliation.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 15, + "permission": "bank_reconciliation.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 16, + "permission": "bank_reconciliation.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 17, + "permission": "bank_reconciliation.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 13, + "permission": "bank_reconciliation.import" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 13, + "permission": "bank_reconciliation.prepare" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 14, + "permission": "bank_reconciliation.review" + } + ], + "grants_per_book_count": 8, + "total_proposed_grants": 16, + "new_users": false, + "new_pg_roles": false, + "real_access": false, + "public_access": false, + "new_financial_effects": false, + "business_writes_applied": 0, + "authorization": { + "date": "2026-11-10", + "decision": "Autorizar a conferência bancária QA e os direitos apresentados" + }, + "implemented": true, + "grants_applied_per_book": 8, + "delivery": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md" +} diff --git a/docs/integration/sgf-cash-access-proposal.json b/docs/integration/sgf-cash-access-proposal.json new file mode 100644 index 0000000..1a78430 --- /dev/null +++ b/docs/integration/sgf-cash-access-proposal.json @@ -0,0 +1,84 @@ +{ + "version": "SGF-CASH-QA-v1", + "approved": true, + "authorization": "10/11/2026: Autorizar o ciclo Caixa QA e os direitos apresentados", + "applied": true, + "implementation": "implemented_and_http_verified_8085", + "installation": "cc50a276c10c", + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "new_host_rights": [ + { + "permission": "cash.view", + "profiles": [ + "finance_prepare", + "finance_review", + "accounting_prepare", + "accounting_review", + "read_only" + ] + }, + { + "permission": "cash.prepare", + "profiles": [ + "finance_prepare" + ] + }, + { + "permission": "cash.review", + "profiles": [ + "finance_review" + ] + }, + { + "permission": "cash.close", + "profiles": [ + "finance_review" + ] + } + ], + "profiles": { + "finance_prepare": 13, + "finance_review": 14, + "accounting_prepare": 15, + "accounting_review": 16, + "read_only": 17 + }, + "permission_names": 4, + "concessions_per_existing_qa_book": 8, + "additional_native_function_grants": 0, + "scenario": { + "opening_minor": 0, + "explicit_existing_synthetic_receipt_classification_minor": 100000, + "new_synthetic_expense_minor": 5000, + "expected_count_and_close_minor": 95000, + "currency": "MZN", + "journal_effects": "two individually reviewed origins; no new invoice, receipt or debt" + }, + "qa_accounts": [ + "QA-CAIXA", + "QA-DESPESA-CAIXA" + ], + "existing_counteraccount": "QA-LIQUIDACAO-CLIENTE", + "limits": { + "new_demo": false, + "new_database": false, + "new_port": false, + "new_user_or_profile": false, + "new_pg_role": false, + "real_access": false, + "real_money_or_payments": false, + "bank_connection": false, + "sgc": false, + "automatic_adjustment": false, + "cash_reopening": false, + "publication": false, + "purchasing_candidate": false, + "preserve_existing_25_journals": true, + "preserve_six_records_and_blocked_cache": true + }, + "scope_document": "docs/SGF_CASH_NEXT_SCOPE.md" +} \ No newline at end of file diff --git a/docs/integration/sgf-document-read-access-proposal.json b/docs/integration/sgf-document-read-access-proposal.json new file mode 100644 index 0000000..e18d2ac --- /dev/null +++ b/docs/integration/sgf-document-read-access-proposal.json @@ -0,0 +1,82 @@ +{ + "version": "SGF-DOCUMENT-READ-QA-v1", + "approved": true, + "authorization": "10/11/2026: Autorizar as10 concessões de consulta QA", + "applied": true, + "installation": "cc50a276c10c", + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "reason": "O motor original de emissão exige consulta do emitente, cliente, preparação e enquadramento fiscal; a aprovação anterior concedeu service_invoices.issue ao revisor Financeiro mas não estes direitos de consulta. Não contornar as guardas originais.", + "existing_host_rights": [ + { + "permission": "issuer_documents.view", + "scope": "company", + "profiles": [ + "finance_prepare", + "finance_review" + ] + }, + { + "permission": "client_documents.view", + "scope": "company", + "profiles": [ + "finance_prepare", + "finance_review" + ] + }, + { + "permission": "service_fiscal.view", + "scope": "company", + "profiles": [ + "finance_prepare", + "finance_review" + ] + }, + { + "permission": "draft_documents.view", + "scope": "unit", + "profiles": [ + "finance_prepare", + "finance_review" + ] + }, + { + "permission": "billing_drafts.view", + "scope": "unit", + "profiles": [ + "finance_review" + ] + }, + { + "permission": "service_drafts.view", + "scope": "unit", + "profiles": [ + "finance_review" + ] + } + ], + "concessions": 10, + "new_permission_names": 0, + "profiles": { + "finance_prepare": 13, + "finance_review": 14 + }, + "limitations": { + "existing_qa_books_only": true, + "no_new_profiles": true, + "no_real_rights": true, + "no_public_grants": true, + "no_new_demos": true, + "no_writing_document_profiles": true, + "no_fiscal_rule_change": true, + "no_publication": true, + "preserve_six_records_and_cache": true + }, + "applied_only_existing_qa_books": [ + "b4093c6f6707", + "cc50a276c10c" + ] +} \ No newline at end of file diff --git a/docs/integration/sgf-financial-indicators-access-proposal.json b/docs/integration/sgf-financial-indicators-access-proposal.json new file mode 100644 index 0000000..2f821f0 --- /dev/null +++ b/docs/integration/sgf-financial-indicators-access-proposal.json @@ -0,0 +1,81 @@ +{ + "version": "SGF-FINANCIAL-INDICATORS-QA-v1", + "approved": true, + "applied": true, + "scope_file": "docs/SGF_FINANCIAL_INDICATORS_SCOPE.md", + "existing_books": [ + "cc50a276c10c", + "b4093c6f6707" + ], + "new_right_names": [ + "financial_analysis.view", + "financial_analysis.prepare", + "financial_analysis.review" + ], + "grants_per_book": [ + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 13, + "permission": "financial_analysis.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 14, + "permission": "financial_analysis.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 15, + "permission": "financial_analysis.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 16, + "permission": "financial_analysis.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 17, + "permission": "financial_analysis.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 13, + "permission": "financial_analysis.prepare" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 14, + "permission": "financial_analysis.review" + } + ], + "grants_per_book_count": 7, + "total_proposed_grants": 14, + "new_users": false, + "new_pg_roles": false, + "real_access": false, + "public_access": false, + "new_financial_effects": false, + "business_writes_applied": 0, + "authorization": { + "date": "2026-11-10", + "decision": "Autorizar os indicadores QA e os direitos apresentados" + }, + "implemented": true, + "grants_applied_per_book": 7, + "delivery": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md" +} diff --git a/docs/integration/sgf-fiscal-expression-port.json b/docs/integration/sgf-fiscal-expression-port.json new file mode 100644 index 0000000..f611475 --- /dev/null +++ b/docs/integration/sgf-fiscal-expression-port.json @@ -0,0 +1,12 @@ +{ + "source_repository": "portal-investidor", + "source_file": "app/Services/SimulationTaxRuleExpression.php", + "source_sha256": "a976c0eb43ebb706275ca17efc9354a8d84f2dbe698375211704b158df02f7e1", + "consumer_file": "app/Modules/Services/FiscalRuleExpression.php", + "adaptations": [ + "Namespace/class only; decimal input capped24 whole/8 fractional instead of unbounded.", + "Runtime executes only the exact pinned QA percentage DAG; metadata expressions never execute." + ], + "runtime_portal_dependency": false, + "legal_knowledge_import": false +} diff --git a/docs/integration/sgf-fiscal-obligations-access-proposal.json b/docs/integration/sgf-fiscal-obligations-access-proposal.json new file mode 100644 index 0000000..1640c15 --- /dev/null +++ b/docs/integration/sgf-fiscal-obligations-access-proposal.json @@ -0,0 +1,65 @@ +{ + "version": "SGF-FISCAL-OBLIGATIONS-QA-v1", + "approved": true, + "implemented": true, + "applied": true, + "scope_file": "docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md", + "books": [ + "existing8085", + "existing_regression" + ], + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "new_host_rights": [ + "fiscal_obligations.view", + "fiscal_obligations.prepare", + "fiscal_obligations.review" + ], + "grants_per_book": [ + { + "user_id": 13, + "permission": "fiscal_obligations.view" + }, + { + "user_id": 14, + "permission": "fiscal_obligations.view" + }, + { + "user_id": 15, + "permission": "fiscal_obligations.view" + }, + { + "user_id": 16, + "permission": "fiscal_obligations.view" + }, + { + "user_id": 17, + "permission": "fiscal_obligations.view" + }, + { + "user_id": 13, + "permission": "fiscal_obligations.prepare" + }, + { + "user_id": 14, + "permission": "fiscal_obligations.review" + } + ], + "total_grants_both_books": 14, + "new_postgres_grants": 0, + "new_public_rights": 0, + "new_demo": false, + "real_operations": false, + "evidence": [ + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-223220-05f5c9f3/fiscal-obligations-access-readonly.json", + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-223221-33cdee62/fiscal-obligations-access-readonly.json" + ], + "authorization": { + "date": "2026-11-10", + "decision": "Autorizar o bloco QA e os três direitos apresentados" + }, + "delivery": "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md" +} diff --git a/docs/integration/sgf-fiscal-rules-access-proposal.json b/docs/integration/sgf-fiscal-rules-access-proposal.json new file mode 100644 index 0000000..3e95557 --- /dev/null +++ b/docs/integration/sgf-fiscal-rules-access-proposal.json @@ -0,0 +1,81 @@ +{ + "version": "SGF-FISCAL-RULES-COMMON-QA-v1", + "approved": true, + "applied": true, + "scope_file": "docs/SGF_FISCAL_RULES_COMMON_SCOPE.md", + "existing_books": [ + "cc50a276c10c", + "b4093c6f6707" + ], + "new_right_names": [ + "fiscal_rules.view", + "fiscal_rules.prepare", + "fiscal_rules.review" + ], + "grants_per_book": [ + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 13, + "permission": "fiscal_rules.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 14, + "permission": "fiscal_rules.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 15, + "permission": "fiscal_rules.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 16, + "permission": "fiscal_rules.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 17, + "permission": "fiscal_rules.view" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 13, + "permission": "fiscal_rules.prepare" + }, + { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "user_id": 14, + "permission": "fiscal_rules.review" + } + ], + "grants_per_book_count": 7, + "total_proposed_grants": 14, + "new_users": false, + "new_pg_roles": false, + "real_access": false, + "public_access": false, + "existing_iva_book_rights_unchanged": true, + "business_writes_applied": 0, + "authorization": { + "date": "2026-11-10", + "decision": "Autorizar o núcleo comum QA e os direitos apresentados" + }, + "implemented": true, + "grants_applied_per_book": 7, + "delivery": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md" +} diff --git a/docs/integration/sgf-human-resources-access-proposal.json b/docs/integration/sgf-human-resources-access-proposal.json new file mode 100644 index 0000000..40c6fad --- /dev/null +++ b/docs/integration/sgf-human-resources-access-proposal.json @@ -0,0 +1,102 @@ +{ + "version": "SGF-HUMAN-RESOURCES-AUTONOMOUS-QA-v1", + "approved": true, + "implemented": true, + "applied": true, + "module_authorized": true, + "access_authorized": true, + "scope_file": "docs/SGF_HUMAN_RESOURCES_AUTONOMY_SCOPE.md", + "supersedes_access_proposal": "docs/integration/sgf-remunerations-access-proposal.json", + "books": [ + "existing8085", + "existing_regression" + ], + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "module": "human_resources", + "profiles": [ + { + "id": "hr_prepare", + "label": "QA Recursos Humanos — preparação", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "human_resources.remunerations.view", + "scope": "unit" + }, + { + "permission": "human_resources.remunerations.prepare", + "scope": "unit" + } + ] + }, + { + "id": "hr_review", + "label": "QA Recursos Humanos — revisão", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "human_resources.remunerations.view", + "scope": "unit" + }, + { + "permission": "human_resources.remunerations.review", + "scope": "unit" + } + ] + }, + { + "id": "hr_read", + "label": "QA Recursos Humanos — consulta", + "rights": [ + { + "permission": "company.view", + "scope": "company" + }, + { + "permission": "unit.view", + "scope": "unit" + }, + { + "permission": "human_resources.remunerations.view", + "scope": "unit" + } + ] + } + ], + "new_host_rights": [ + "human_resources.remunerations.view", + "human_resources.remunerations.prepare", + "human_resources.remunerations.review" + ], + "new_profiles_per_book": 3, + "grants_per_book": 11, + "total_grants_both_books": 22, + "new_database_roles": 0, + "new_public_rights": 0, + "real_accounts": false, + "new_demo": false, + "original_financial_profile_grants_retracted": true, + "authorization": { + "date": "2026-11-11", + "decision": "Autorizar os perfis QA próprios de RH e as concessões apresentadas" + }, + "delivery": "docs/SGF_HUMAN_RESOURCES_DELIVERY.md" +} diff --git a/docs/integration/sgf-human-resources-personnel-access-proposal.json b/docs/integration/sgf-human-resources-personnel-access-proposal.json new file mode 100644 index 0000000..81a1cf7 --- /dev/null +++ b/docs/integration/sgf-human-resources-personnel-access-proposal.json @@ -0,0 +1,114 @@ +{ + "version": "SGF-HR-CONTRACTS-ATTENDANCE-LEAVE-QA-v1", + "approved": true, + "implemented": false, + "applied": false, + "scope_file": "docs/SGF_HUMAN_RESOURCES_PERSONNEL_SCOPE.md", + "module": "human_resources", + "books": [ + "existing8085", + "existing_regression" + ], + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "existing_profile_keys": [ + "hr_prepare", + "hr_review", + "hr_read" + ], + "new_host_rights": [ + "human_resources.contracts.view", + "human_resources.contracts.prepare", + "human_resources.contracts.review", + "human_resources.attendance.view", + "human_resources.attendance.prepare", + "human_resources.attendance.review", + "human_resources.leave.view", + "human_resources.leave.prepare", + "human_resources.leave.review" + ], + "grants_per_book": [ + { + "profile_key": "hr_prepare", + "permission": "human_resources.contracts.view" + }, + { + "profile_key": "hr_prepare", + "permission": "human_resources.contracts.prepare" + }, + { + "profile_key": "hr_prepare", + "permission": "human_resources.attendance.view" + }, + { + "profile_key": "hr_prepare", + "permission": "human_resources.attendance.prepare" + }, + { + "profile_key": "hr_prepare", + "permission": "human_resources.leave.view" + }, + { + "profile_key": "hr_prepare", + "permission": "human_resources.leave.prepare" + }, + { + "profile_key": "hr_review", + "permission": "human_resources.contracts.view" + }, + { + "profile_key": "hr_review", + "permission": "human_resources.contracts.review" + }, + { + "profile_key": "hr_review", + "permission": "human_resources.attendance.view" + }, + { + "profile_key": "hr_review", + "permission": "human_resources.attendance.review" + }, + { + "profile_key": "hr_review", + "permission": "human_resources.leave.view" + }, + { + "profile_key": "hr_review", + "permission": "human_resources.leave.review" + }, + { + "profile_key": "hr_read", + "permission": "human_resources.contracts.view" + }, + { + "profile_key": "hr_read", + "permission": "human_resources.attendance.view" + }, + { + "profile_key": "hr_read", + "permission": "human_resources.leave.view" + } + ], + "grant_count_per_book": 15, + "total_grants_both_books": 30, + "new_profiles": 0, + "new_users": 0, + "new_database_roles": 0, + "new_postgres_grants": 0, + "new_public_rights": 0, + "new_demo": false, + "new_sgc_dependency": false, + "new_journals": 0, + "new_payments": 0, + "evidence": [ + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-083950-2b7f6505/hr-personnel-access-readonly.json", + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-083951-b86bcdeb/hr-personnel-access-readonly.json" + ], + "authorization": { + "date": "2026-11-11", + "decision": "Autorizar o bloco e as30 concessões QA apresentadas" + } +} diff --git a/docs/integration/sgf-iva-assessment-access-proposal.json b/docs/integration/sgf-iva-assessment-access-proposal.json new file mode 100644 index 0000000..2794e36 --- /dev/null +++ b/docs/integration/sgf-iva-assessment-access-proposal.json @@ -0,0 +1,132 @@ +{ + "version": "SGF-IVA-ASSESSMENT-FORM-QA-v1", + "approved": true, + "applied": true, + "scope_file": "docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md", + "existing_books": [ + "cc50a276c10c", + "b4093c6f6707" + ], + "new_right_names": [ + "fiscal_assessments.prepare", + "fiscal_assessments.review", + "fiscal_assessments.view", + "fiscal_forms.prepare", + "fiscal_forms.review", + "fiscal_forms.view" + ], + "grants_per_book": [ + { + "user_id": 13, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_assessments.view" + }, + { + "user_id": 14, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_assessments.view" + }, + { + "user_id": 15, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_assessments.view" + }, + { + "user_id": 16, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_assessments.view" + }, + { + "user_id": 17, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_assessments.view" + }, + { + "user_id": 13, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_assessments.prepare" + }, + { + "user_id": 14, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_assessments.review" + }, + { + "user_id": 13, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_forms.view" + }, + { + "user_id": 14, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_forms.view" + }, + { + "user_id": 15, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_forms.view" + }, + { + "user_id": 16, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_forms.view" + }, + { + "user_id": 17, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_forms.view" + }, + { + "user_id": 13, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_forms.prepare" + }, + { + "user_id": 14, + "tenant_id": 1, + "company_id": 2, + "unit_id": 2, + "permission": "fiscal_forms.review" + } + ], + "grants_per_book_count": 14, + "total_proposed_grants": 28, + "new_users": false, + "new_pg_roles": false, + "real_access": false, + "public_access": false, + "business_writes_applied": 0, + "authorization": { + "date": "2026-11-10", + "decision": "Autorizar o livro/mapa IVA QA e os direitos apresentados" + }, + "implemented": true, + "delivery": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "grants_applied_per_book": 14 +} diff --git a/docs/integration/sgf-knowledge-inventory-20261110.json b/docs/integration/sgf-knowledge-inventory-20261110.json new file mode 100644 index 0000000..2a1268f --- /dev/null +++ b/docs/integration/sgf-knowledge-inventory-20261110.json @@ -0,0 +1,103 @@ +{ + "version": "SGF-KNOWLEDGE-INVENTORY-20261110-v1", + "reference_document": { + "name": "Incorporacao de impostos e analise ao SGF.docx", + "sha256": "212ae656646b6cc3cad9d077d74887de0ba8111b5c7a39bc5efc0adf20cbeb7b", + "type": "User requirements/questions; not normative legislation or activation authority" + }, + "existing_inventory": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "portal_repository": "portal-investidor", + "sources": [ + { + "file": "app/Services/SimulationTaxRuleExpression.php", + "located": true, + "sha256": "a976c0eb43ebb706275ca17efc9354a8d84f2dbe698375211704b158df02f7e1", + "sgf_operational_transfer_verified": false + }, + { + "file": "app/Services/SimulationTaxParameterResolver.php", + "located": true, + "sha256": "a4621e672ea4f38e81ff9828f4017889cc09d229c5940f6e38607acc9b27e1a8", + "sgf_operational_transfer_verified": false + }, + { + "file": "app/Services/SimulationFiscalSnapshot.php", + "located": true, + "sha256": "7dcc7d4f30c95fc30aa4fb73d771ed74e9ca8be30ae5fa82d8e310e565e0b24f", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/fiscal-rational.ts", + "located": true, + "sha256": "941408aa1cd92652507b5e675e307d0883ff25921a903e60365175d02be0cbdf", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/iva-operations.ts", + "located": true, + "sha256": "87f616ea2b3a844dea352a6c4d2db25a8de612df3e4edab4c395cc8fa0297a50", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/iva-acquisitions.ts", + "located": true, + "sha256": "b8a8c4a72e35fea7026fd01b0e8e26d44248539eca1a77e8d58ca15a72f218fb", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/iva-modelo-a.ts", + "located": true, + "sha256": "e34cac6d3c0473a64e896cfc4641c1da29382aab13de2bd0e78e1ff33c770c87", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/irpc-normal.ts", + "located": true, + "sha256": "5594b913e795e1e7d4bf1bba915af08a54c3d3a2d69a86e9ce3a966667cd01fd", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/irpc-normal-forms.ts", + "located": true, + "sha256": "504c95891ad9d06478c2fe30416cf8d05e91f027d85cacaf0d4d7de9c8f04d81", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/irpc-withholdings.ts", + "located": true, + "sha256": "b751aa1a326960516c8abfc9ade637a205c7bf053cbaa98c2058f50d56ea0c0d", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/ispc-quarter.ts", + "located": true, + "sha256": "5bce00353d58e503693550610aabcfb9e3e81c36109e831ac0532ca66ecb0e92", + "sgf_operational_transfer_verified": false + }, + { + "file": "resources/js/lib/ispc-eligibility.ts", + "located": true, + "sha256": "d8a32ee08e27c6ffcead739f9917d3196da6ab83ce6aac9163ffb74478d7e2fa", + "sgf_operational_transfer_verified": false + } + ], + "new_research": false, + "runtime_portal_dependency": false, + "normative_validity_verified": false, + "new_rates_imported": 0, + "sgf_missing_operational_areas": [ + "IVA multi-line/included-price and assessment/deduction books", + "IRPS and payroll/INSS", + "IRPC tax bridge/withholding and assessment", + "ISPC", + "ICE", + "Official form mappings and fiscal calendar", + "Bank reconciliation", + "Reconciled financial ratios/trends" + ], + "limitations": [ + "File presence/hash is not a legal or numerical validation. Existing named proofs remain historical and were not rerun.", + "IRPS/INSS/ICE complete reusable source set was not established by this limited inventory; do not infer no knowledge exists in all prior chats or archives.", + "Do not copy portal user dossiers or import projected values as effective SGF operations." + ] +} diff --git a/docs/integration/sgf-multi-line-included-proposal.json b/docs/integration/sgf-multi-line-included-proposal.json new file mode 100644 index 0000000..4216ea4 --- /dev/null +++ b/docs/integration/sgf-multi-line-included-proposal.json @@ -0,0 +1,31 @@ +{ + "version": "SGF-MULTI-LINE-INCLUDED-QA-v1", + "approved": true, + "implemented": true, + "scope_file": "docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md", + "new_right_names": [], + "new_grants": 0, + "existing_rights_reused": [ + "fiscal_rules.view", + "fiscal_rules.prepare", + "fiscal_rules.review" + ], + "policy": { + "per_line_tax": "TRUNCATE2", + "gross_included_document_base": "gross_document_minus_tax_document", + "exact_fractions_and_both_differences_preserved": true, + "aggregate": "sum_document_lines", + "net_noncent_base": "pending", + "gross_noncent_amount": "pending" + }, + "new_invoices": 0, + "new_receivables": 0, + "new_journals": 0, + "new_demo": false, + "real_legal_activation": false, + "authorization": { + "date": "2026-11-10", + "decision": "Autorizar a modalidade QA e a política apresentada" + }, + "delivery": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md" +} diff --git a/docs/integration/sgf-payable-due-access-proposal.json b/docs/integration/sgf-payable-due-access-proposal.json new file mode 100644 index 0000000..67d7d17 --- /dev/null +++ b/docs/integration/sgf-payable-due-access-proposal.json @@ -0,0 +1,44 @@ +{ + "version": "SGF-PAYABLE-DUE-QA-v1", + "approved": true, + "implemented": true, + "applied": true, + "scope_file": "docs/SGF_PAYABLE_DUE_SCOPE.md", + "books": [ + "existing8085", + "existing_regression" + ], + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "new_host_rights": [ + "supplier_payables.due.prepare", + "supplier_payables.due.review" + ], + "grants_per_book": [ + { + "user_id": 13, + "permission": "supplier_payables.due.prepare" + }, + { + "user_id": 14, + "permission": "supplier_payables.due.review" + } + ], + "total_grants_both_books": 4, + "new_postgres_grants": 0, + "new_public_rights": 0, + "new_demo": false, + "real_operations": false, + "evidence": [ + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-221055-d3c1bc55/payable-due-access-readonly.json", + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-221056-6bf7f3d6/payable-due-access-readonly.json" + ], + "authorization": { + "date": "2026-11-10", + "decision": "Autorizar o percurso QA e os dois direitos apresentados" + }, + "delivery": "docs/SGF_PAYABLE_DUE_DELIVERY.md" +} diff --git a/docs/integration/sgf-remaining-functions-access-proposal.json b/docs/integration/sgf-remaining-functions-access-proposal.json new file mode 100644 index 0000000..1b57306 --- /dev/null +++ b/docs/integration/sgf-remaining-functions-access-proposal.json @@ -0,0 +1,169 @@ +{ + "version": "SGF-REMAINING-COMMERCIAL-STOCK-QA-v1", + "approved": true, + "authorization": "10/11/2026: Autorizar o bloco QA e os direitos discriminados", + "applied": true, + "access_applied_in_existing_qa_books": true, + "functional_progress": { + "quotes": "implemented_and_http_verified_8085", + "stock_policies_supplier_return": "implemented_and_http_verified_8085", + "additional_documents": "implemented_http_verified_8085_with_rollback_tested_cancellations" + }, + "installation": "cc50a276c10c", + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "new_host_rights": [ + { + "permission": "commercial_quotes.view", + "profiles": [ + "commercial_prepare", + "commercial_review", + "finance_prepare", + "finance_review", + "read_only" + ] + }, + { + "permission": "commercial_quotes.prepare", + "profiles": [ + "commercial_prepare", + "finance_prepare" + ] + }, + { + "permission": "commercial_quotes.review", + "profiles": [ + "commercial_review", + "finance_review" + ] + }, + { + "permission": "commercial_quotes.convert", + "profiles": [ + "commercial_prepare", + "finance_prepare" + ] + }, + { + "permission": "service_documents.view", + "profiles": [ + "commercial_prepare", + "commercial_review", + "finance_prepare", + "finance_review", + "read_only" + ] + }, + { + "permission": "service_documents.prepare", + "profiles": [ + "commercial_prepare", + "finance_prepare" + ] + }, + { + "permission": "service_documents.review", + "profiles": [ + "commercial_review", + "finance_review" + ] + }, + { + "permission": "service_documents.issue", + "profiles": [ + "commercial_prepare", + "finance_review" + ] + }, + { + "permission": "service_documents.credit_apply", + "profiles": [ + "finance_review" + ] + }, + { + "permission": "stock.planning.view", + "profiles": [ + "purchasing:validator", + "workshop_verify" + ] + }, + { + "permission": "stock.planning.prepare", + "profiles": [ + "purchasing:validator" + ] + }, + { + "permission": "stock.planning.review", + "profiles": [ + "workshop_verify" + ] + }, + { + "permission": "stock.disposition.view", + "profiles": [ + "purchasing:validator", + "workshop_verify" + ] + }, + { + "permission": "stock.disposition.prepare", + "profiles": [ + "purchasing:validator" + ] + }, + { + "permission": "stock.disposition.review", + "profiles": [ + "workshop_verify" + ] + } + ], + "additional_existing_host_rights": [ + { + "permission": "service_invoices.issue", + "profiles": [ + "finance_review" + ], + "reason": "Finalização atómica da factura-recibo sintética após as decisões documental e financeira distintas; sem concessão real." + } + ], + "existing_native_functions_to_grant_only_in_qa": [ + { + "native_user_id": 6, + "functions": [ + "supply_read", + "supply_prepare", + "inventory_disposition_read", + "inventory_disposition_prepare" + ] + }, + { + "native_user_id": 3, + "functions": [ + "supply_read", + "supply_review", + "inventory_disposition_read", + "inventory_disposition_review" + ] + } + ], + "limits": { + "new_profiles": false, + "new_postgresql_roles": false, + "real_rights": false, + "public_grants": false, + "new_databases": false, + "new_demos": false, + "sgc": false, + "publication": false, + "protected_six_unchanged": true, + "blocked_cache_unchanged": true, + "existing_eighteen_journals_preserved": true + }, + "scope_document": "docs/SGF_NEXT_BLOCK_RIGHTS.md" +} \ No newline at end of file diff --git a/docs/integration/sgf-remunerations-access-proposal.json b/docs/integration/sgf-remunerations-access-proposal.json new file mode 100644 index 0000000..6efd686 --- /dev/null +++ b/docs/integration/sgf-remunerations-access-proposal.json @@ -0,0 +1,69 @@ +{ + "version": "SGF-REMUNERATIONS-QA-v1", + "approved": true, + "implemented": false, + "applied": false, + "scope_file": "docs/SGF_REMUNERATIONS_SCOPE.md", + "books": [ + "existing8085", + "existing_regression" + ], + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "new_host_rights": [ + "remunerations.view", + "remunerations.prepare", + "remunerations.review" + ], + "grants_per_book": [ + { + "user_id": 13, + "permission": "remunerations.view" + }, + { + "user_id": 14, + "permission": "remunerations.view" + }, + { + "user_id": 15, + "permission": "remunerations.view" + }, + { + "user_id": 16, + "permission": "remunerations.view" + }, + { + "user_id": 17, + "permission": "remunerations.view" + }, + { + "user_id": 13, + "permission": "remunerations.prepare" + }, + { + "user_id": 14, + "permission": "remunerations.review" + } + ], + "total_grants_both_books": 14, + "new_postgres_grants": 0, + "new_public_rights": 0, + "new_demo": false, + "real_operations": false, + "new_journals": 0, + "legal_payroll_validated": false, + "evidence": [ + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-074857-8dc00a16/remunerations-access-readonly.json", + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-074858-c159b61c/remunerations-access-readonly.json" + ], + "authorization": { + "date": "2026-11-11", + "decision": "Autorizar o bloco QA e os três direitos apresentados" + }, + "status": "superseded_by_autonomous_hr_directive", + "applied_then_revoked": true, + "superseded_by": "docs/integration/sgf-human-resources-access-proposal.json" +} diff --git a/docs/integration/sgf-solution-contract.json b/docs/integration/sgf-solution-contract.json new file mode 100644 index 0000000..24f9ea6 --- /dev/null +++ b/docs/integration/sgf-solution-contract.json @@ -0,0 +1,600 @@ +{ + "version": "SGF-SOLUTION-CLOSURE-v25", + "target": "existing8085", + "sgc_required": false, + "new_demonstrations": false, + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "required_host_tables": [ + "a_clients", + "a_services", + "a_service_drafts", + "a_billing_drafts", + "b_internal_confirmations", + "doc_records", + "document_preview_models", + "service_calculation_profiles", + "sf_proposals", + "si_invoices", + "sd_versions", + "sd_outputs", + "sd_commands", + "sd_cancellations" + ], + "required_source_files": [ + "app/Modules/Services/ServiceWorkflow.php", + "app/Modules/Services/ServiceInvoicing.php", + "resources/js/ServicesWorkspace.jsx", + "resources/js/ServiceInvoiceWorkspace.jsx", + "app/Modules/Services/ServiceDocuments.php", + "app/Modules/Services/DocumentPeriod.php", + "app/Modules/Services/BillingRegister.php", + "resources/js/BillingRegisterWorkspace.jsx", + "resources/js/WorkspaceHome.jsx", + "resources/js/workspace-overview.js" + ], + "proofs": [ + "docs/proofs/invoice-test-results.json", + "docs/proofs/sgf-six-module-functional-review-20261110.json", + "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md" + ], + "work_remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "required_host_tables": [ + "users", + "companies", + "units", + "permission_grants" + ], + "required_source_files": [ + "app/Modules/Purchasing/ReceiptRuntime.php", + "app/Modules/Purchasing/RequestController.php", + "app/Modules/Purchasing/InvoiceController.php", + "app/Modules/Purchasing/ObligationController.php", + "resources/js/WorkspaceHome.jsx", + "resources/js/workspace-overview.js" + ], + "proofs": [ + "docs/proofs/purchasing-completion-final-20261104-r2.json", + "docs/proofs/sgf-six-module-functional-review-20261110.json", + "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md" + ], + "work_remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "required_host_tables": [ + "users", + "companies", + "units" + ], + "required_source_files": [ + "app/Modules/Inventory/StockReadController.php", + "app/Modules/Inventory/CompositionStockRead.php", + "app/Modules/Inventory/StockPeriodReport.php", + "app/Modules/Workshop/WorkshopStock.php", + "app/Modules/Inventory/StockOperationsController.php", + "resources/views/stock-operations-workspace.blade.php", + "public/stock-operations-workspace.mjs", + "app/Modules/Inventory/StockFinalController.php", + "resources/views/stock-final-workspace.blade.php", + "public/sgf-stock-final.mjs", + "app/Modules/Inventory/StockInactiveReport.php", + "resources/js/StockInactiveReportWorkspace.jsx", + "resources/js/WorkspaceHome.jsx", + "resources/js/workspace-overview.js", + "app/Modules/Inventory/StockCatalogueController.php", + "resources/views/stock-catalogue-workspace.blade.php", + "public/sgf-stock-catalogue.mjs", + "config/stock_catalogue.php", + "database/sql/stock_catalogue.sql" + ], + "proofs": [ + "docs/proofs/stock-period-report-20261103.json", + "docs/proofs/sgf-stock-operations-20261110.json", + "docs/proofs/sgf-six-module-functional-review-20261110.json", + "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "docs/SGF_STOCK_CATALOGUE_DELIVERY.md" + ], + "work_remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ], + "required_native_tables": [ + "stock_movements", + "stock_operations", + "inventory_lot_allocations", + "supply_records", + "sgf_stock_transit_events", + "sc_catalogue_meta", + "sc_catalogue_versions", + "sc_catalogue_commands" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido", + "Equipamento escolhido → ordens exactas SQL25/100 com filtros, identificação actual, CSV da página e regresso ao catálogo" + ], + "required_host_tables": [ + "wo_orders", + "wo_entries", + "dc_calculations", + "ic_results", + "wb_proposals" + ], + "required_source_files": [ + "app/Modules/Workshop/WorkshopStock.php", + "resources/js/WorkshopWorkspace.jsx", + "resources/js/DirectCostsWorkspace.jsx", + "resources/js/IndustrialCostsWorkspace.jsx", + "resources/js/WorkshopBillingWorkspace.jsx", + "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "app/Modules/Workshop/WorkshopBilling.php", + "resources/js/WorkspaceHome.jsx", + "resources/js/workspace-overview.js", + "app/Modules/Workshop/WorkshopOrderRead.php", + "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "resources/js/WorkshopOrderList.jsx", + "resources/js/workshop-report-view.js", + "app/Modules/Workshop/WorkshopEquipment.php", + "resources/js/WorkshopEquipmentWorkspace.jsx" + ], + "proofs": [ + "docs/proofs/workshop-stock-closure-20261102.json", + "docs/proofs/sgf-six-module-functional-review-20261110.json", + "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md" + ], + "work_remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe", + "Mapa de antiguidade de fornecedores por referência explícita e condição administrativa revista, faixas0/1–30/31–60/61–90/>90/desconhecido, valores exactos por moeda só da página/CSV, sem alterar saldo ou vencimento original" + ], + "required_host_tables": [ + "sr_records", + "sr_effects", + "cash_events", + "cash_commands", + "br_statements", + "br_events", + "br_commands", + "an_criteria", + "an_results", + "an_commands" + ], + "required_source_files": [ + "app/Modules/Services/ServiceReceivables.php", + "app/Modules/Purchasing/SupplierPayables.php", + "resources/js/ServiceReceivablesWorkspace.jsx", + "app/Modules/Purchasing/ComposedFinance.php", + "app/Modules/Purchasing/ComposedFinanceController.php", + "app/Modules/Finance/CashWorkspace.php", + "app/Modules/Finance/Http/CashController.php", + "app/Modules/Accounting/CashLedgerSource.php", + "app/Modules/Finance/BankReconciliation.php", + "app/Modules/Finance/BankLedgerSource.php", + "app/Modules/Finance/Http/BankReconciliationController.php", + "public/sgf-bank.mjs", + "app/Modules/Finance/FinancialIndicators.php", + "app/Modules/Finance/FinancialIndicatorSource.php", + "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "public/sgf-indicators.mjs", + "resources/js/WorkspaceHome.jsx", + "resources/js/workspace-overview.js", + "app/Modules/Services/ReceivablePositionRead.php", + "app/Modules/Services/Http/ReceivablePositionReadController.php", + "resources/js/ReceivablePositionsWorkspace.jsx", + "resources/js/receivable-position-view.js", + "app/Modules/Purchasing/PayablePositionReadController.php", + "app/Modules/Purchasing/ComposedFinanceController.php", + "resources/js/PayablePositionsWorkspace.jsx", + "resources/js/payable-position-view.js", + "app/Modules/Purchasing/PayableDueController.php", + "resources/js/PayableDueWorkspace.jsx", + "database/sql/payable_due.sql", + "config/payable_due.php", + "app/Modules/Purchasing/PayableAge.php" + ], + "proofs": [ + "docs/proofs/supplier-settlements.json", + "docs/proofs/sgf-six-module-functional-review-20261110.json", + "docs/SGF_CASH_DELIVERY.md", + "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "docs/SGF_PAYABLE_AGING_DELIVERY.md" + ], + "work_remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ], + "required_native_tables": [ + "pa_obligations", + "pa_access", + "ps_payments", + "pd_due_meta", + "pd_due_versions", + "pd_due_commands" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página", + "Controlo administrativo QA de obrigação/prazo/responsável do mapa interno revisto, preparação13/revisão14, correcção/cancelamento, entrega apenas simulada com SHA verificado, histórico/recibo/SQL25/100/CSV; sem calendário legal ou submissão", + "Correspondências explícitas revistas15/16 e balanço/resultados internos parciais QA:19contas/nove pendentes, resultado congelado19linhas/SHA, história/recibo/CSV contextual, ligação exacta ao razão; capital e resultado completo não presumidos" + ], + "required_host_tables": [ + "ca_proposals", + "ca_resolutions", + "fx_versions", + "fx_commands", + "fr_events", + "fr_calculations", + "fr_commands", + "fo_meta", + "fo_versions", + "fo_commands" + ], + "required_source_files": [ + "app/Modules/Accounting/GeneralLedger.php", + "app/Modules/Accounting/GeneralLedgerController.php", + "packages/general-ledger/src/LedgerSchema.php", + "app/Modules/Accounting/ComposedLedger.php", + "app/Modules/Accounting/ComposedLedgerController.php", + "packages/general-ledger/src/StockTransitEvidence.php", + "app/Modules/Services/FiscalCoverage.php", + "app/Modules/Services/FiscalAssessment.php", + "app/Modules/Services/Http/FiscalAssessmentController.php", + "public/sgf-iva.mjs", + "app/Modules/Services/FiscalRules.php", + "app/Modules/Services/FiscalRuleExpression.php", + "app/Modules/Services/Http/FiscalRulesController.php", + "public/sgf-fiscal-rules.mjs", + "app/Modules/Services/FiscalDocumentMath.php", + "resources/js/WorkspaceHome.jsx", + "resources/js/workspace-overview.js", + "resources/js/ComposedAccountingWorkspace.jsx", + "resources/js/LedgerReportsWorkspace.jsx", + "resources/js/ledger-report-view.js", + "app/Modules/Services/FiscalObligations.php", + "app/Modules/Services/Http/FiscalObligationsController.php", + "resources/js/FiscalObligationsWorkspace.jsx", + "resources/js/fiscal-obligations-view.js", + "database/sql/fiscal_obligations.sql", + "config/fiscal_obligations.php", + "app/Modules/Accounting/InternalStatementsController.php", + "app/Modules/Accounting/InternalStatementTotals.php", + "resources/js/InternalStatementsWorkspace.jsx", + "resources/js/internal-statements-view.js", + "resources/js/statement-download.js", + "resources/js/FiscalObligationsWorkspace.jsx", + "database/sql/internal_statements.sql", + "config/internal_statements.php" + ], + "proofs": [ + "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "docs/proofs/sgf-six-module-functional-review-20261110.json", + "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md", + "docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md" + ], + "work_remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ], + "required_native_tables": [ + "gl_accounts", + "gl_periods", + "gl_journals", + "gl_lines", + "fs_meta", + "fs_versions", + "fs_results", + "fs_result_lines", + "fs_commands" + ] + }, + { + "id": "human_resources", + "label": "Recursos Humanos", + "features": [ + "Módulo autónomo com perfis QA próprios separados do Financeiro", + "Cadastro pseudónimo individual preparado e revisto", + "Regras artificiais versionadas com vigência e fonte explícitas", + "Processamento exacto por trabalhador/período com escolha de fontes e revisão distinta", + "Correcção/cancelamento versionados, prévia, histórico, recibos e recuperação", + "Mapa individual revisto conservado com SHA e exportação autenticada; filtros SQL25/100" + ], + "required_host_tables": [ + "rm_meta", + "rm_versions", + "rm_commands", + "hr_qa_profiles" + ], + "required_source_files": [ + "app/Modules/HumanResources/Remunerations.php", + "app/Modules/HumanResources/Http/RemunerationsController.php", + "app/Shared/Installation/HumanResourcesProfiles.php", + "resources/js/RemunerationsWorkspace.jsx", + "resources/js/statement-download.js" + ], + "proofs": [ + "docs/SGF_HUMAN_RESOURCES_DELIVERY.md", + "docs/proofs/sgf-human-resources-20261111.json" + ], + "work_remaining": [ + "Contratos/alterações contratuais, assiduidade, turnos, férias e ausências", + "Regimes, IRPS/INSS/modelos legais e ligação salarial a dívida/pagamentos/Contabilidade", + "Revisão visual, carga sustentada e capacidade de implantação" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração" + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam" + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único" + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas" + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas" + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC" + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria" + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados" + } + ], + "acceptance_proof": "docs/proofs/sgf-single-solution-acceptance.json", + "scope_policy": "Recorte QA dos seis módulos e requisitos amplos de produto separados explicitamente; nenhum painel de cobertura equivale à implementação dos motores ou modelos pendentes.", + "revision_note": "A prova funcional anterior conserva o contrato v1 histórico; as lacunas agora discriminadas não são aceitação nem capacidade de implantação.", + "proposed_scope_extensions": [ + { + "id": "cash", + "access": "docs/integration/sgf-cash-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "scope": "docs/SGF_CASH_NEXT_SCOPE.md", + "implemented": true + }, + { + "id": "iva_assessment_internal_form", + "scope": "docs/SGF_IVA_ASSESSMENT_NEXT_SCOPE.md", + "access": "docs/integration/sgf-iva-assessment-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + }, + { + "id": "fiscal_rules_common", + "scope": "docs/SGF_FISCAL_RULES_COMMON_SCOPE.md", + "access": "docs/integration/sgf-fiscal-rules-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + }, + { + "id": "bank_reconciliation_qa", + "scope": "docs/SGF_BANK_RECONCILIATION_SCOPE.md", + "access": "docs/integration/sgf-bank-reconciliation-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + }, + { + "id": "financial_indicators_qa", + "scope": "docs/SGF_FINANCIAL_INDICATORS_SCOPE.md", + "access": "docs/integration/sgf-financial-indicators-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + }, + { + "id": "multi_line_included_qa", + "scope": "docs/SGF_MULTI_LINE_INCLUDED_SCOPE.md", + "decision": "docs/integration/sgf-multi-line-included-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + }, + { + "id": "stock_catalogue", + "scope": "docs/SGF_STOCK_CATALOGUE_SCOPE.md", + "access": "docs/integration/sgf-stock-catalogue-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + }, + { + "id": "supplier_due_qa", + "scope": "docs/SGF_PAYABLE_DUE_SCOPE.md", + "access": "docs/integration/sgf-payable-due-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + }, + { + "id": "fiscal_obligations_qa", + "scope": "docs/SGF_FISCAL_OBLIGATIONS_SCOPE.md", + "access": "docs/integration/sgf-fiscal-obligations-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + }, + { + "id": "internal_statements_qa", + "scope": "docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md", + "access": "docs/integration/sgf-statements-access-proposal.json", + "approval_required_for_new_qa_rights": false, + "implemented": true + } + ], + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Cadastro/regras/processamento interno artificial QA em RH autónomo entregue; operações laborais gerais, cálculo legal IRPS/INSS/modelos e ligação salarial financeira/contabilística pendentes" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Controlo administrativo de prazo e entrega simulada de uma origem QA entregue; calendário legal, aplicabilidade geral, prazos por fonte/vigência e submissão continuam pendentes" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "client_delivery_sources": [ + "scripts/receipt-composition-http.php", + "scripts/sgf-selected-assets.php", + "resources/js/statement-download.js" + ], + "client_delivery_proof": "docs/SGF_CLIENT_ASSET_DELIVERY.md", + "planned_modules": [] +} diff --git a/docs/integration/sgf-statements-access-proposal.json b/docs/integration/sgf-statements-access-proposal.json new file mode 100644 index 0000000..1202a4c --- /dev/null +++ b/docs/integration/sgf-statements-access-proposal.json @@ -0,0 +1,45 @@ +{ + "version": "SGF-INTERNAL-STATEMENTS-QA-v1", + "approved": true, + "implemented": true, + "applied": true, + "scope_file": "docs/SGF_FINANCIAL_STATEMENTS_SCOPE.md", + "books": [ + "existing8085", + "existing_regression" + ], + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "new_host_rights": [ + "general_ledger.statements.prepare", + "general_ledger.statements.review" + ], + "grants_per_book": [ + { + "user_id": 15, + "permission": "general_ledger.statements.prepare" + }, + { + "user_id": 16, + "permission": "general_ledger.statements.review" + } + ], + "total_grants_both_books": 4, + "new_postgres_grants": 0, + "new_public_rights": 0, + "new_demo": false, + "real_operations": false, + "source_evidence": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/statements-source-private.json", + "evidence": [ + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-225950-b75e07c1/statements-access-readonly.json", + "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-225951-e66026dc/statements-access-readonly.json" + ], + "authorization": { + "date": "2026-11-11", + "decision": "Autorizar o bloco QA e os dois direitos apresentados" + }, + "delivery": "docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md" +} diff --git a/docs/integration/sgf-stock-catalogue-access-proposal.json b/docs/integration/sgf-stock-catalogue-access-proposal.json new file mode 100644 index 0000000..21a9055 --- /dev/null +++ b/docs/integration/sgf-stock-catalogue-access-proposal.json @@ -0,0 +1,41 @@ +{ + "version": "SGF-STOCK-CATALOGUE-QA-v1", + "approved": true, + "implemented": true, + "scope_file": "docs/SGF_STOCK_CATALOGUE_SCOPE.md", + "books": [ + "existing8085", + "existing_regression" + ], + "scope": { + "tenant_id": 1, + "company_id": 2, + "unit_id": 2 + }, + "new_host_rights": [ + "stock.items.prepare", + "stock.items.review" + ], + "grants_per_book": [ + { + "user_id": 2, + "permission": "stock.items.prepare" + }, + { + "user_id": 12, + "permission": "stock.items.review" + } + ], + "total_grants_both_books": 4, + "new_postgres_grants": 0, + "new_public_rights": 0, + "new_demo": false, + "real_operations": false, + "evidence": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-212018-5bd7aab6/stock-catalogue-access-readonly.json", + "authorization": { + "date": "2026-11-10", + "decision": "Autorizar o cadastro QA e os dois direitos apresentados" + }, + "applied": true, + "delivery": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md" +} diff --git a/docs/integration/sgf-stock-final-access-proposal.json b/docs/integration/sgf-stock-final-access-proposal.json new file mode 100644 index 0000000..9461016 --- /dev/null +++ b/docs/integration/sgf-stock-final-access-proposal.json @@ -0,0 +1,25 @@ +{ + "version": "SGF-STOCK-TRANSIT-LOTS-CLOSURE-QA-v1", + "approved": true, + "approval": "Autorização expressa do proprietário em10/11/2026: Autorizar este fecho QA de Stock", + "applied": true, + "scope": {"tenant_id": 1, "company_id": 2, "unit_id": 2}, + "installation": "cc50a276c10c", + "new_accounts": false, + "new_database_roles": false, + "real_access": false, + "new_host_rights": [ + {"permission": "stock.transfers.receive", "recipient_profile": "workshop_verify", "scope": "unit", "destination_warehouse_ids": [1]}, + {"permission": "stock.lots.review", "recipient_profile": "workshop_verify", "scope": "unit", "warehouse_ids": [3], "only_new_qa_lot_article": true} + ], + "existing_native_functions_to_grant_only_in_qa": [ + {"native_user_id": 6, "functions": ["inventory_lot_read", "inventory_lot_prepare", "inventory_lot_hold"]}, + {"native_user_id": 3, "functions": ["inventory_lot_read", "inventory_lot_review", "inventory_lot_release", "inventory_adjust_review"]} + ], + "receiving_native_identity": 3, + "receiving_authority": "explicit scoped SGF stock.transfers.receive; not a generic native warehouse grant", + "preserve_existing_select_only_reader": true, + "no_new_native_warehouse_function": true, + "fixture_examples": {"internal_transfer_milli": 1000, "owned_stock_value_unchanged_minor": 7500, "lot_receipt_milli": 4000, "lot_count_milli": 3500, "lot_unit_value_minor": 2000, "lot_adjustment_expense_minor": 1000, "lot_final_value_minor": 7000}, + "scope_document": "docs/SGF_STOCK_FINAL_SCOPE.md" +} diff --git a/docs/proofs/sgc-8085-integration-preparation-20261110.json b/docs/proofs/sgc-8085-integration-preparation-20261110.json new file mode 100644 index 0000000..1bd07e4 --- /dev/null +++ b/docs/proofs/sgc-8085-integration-preparation-20261110.json @@ -0,0 +1,104 @@ +{ + "date": "2026-11-10", + "passed": true, + "scope": "integration preparation, no installation or activation", + "preparation": { + "id": "d09aac9d09da98e61df5", + "manifest_sha256": "e7dbcff1261b239ea07f570a075e492404a9309fcfbd29287c895bcd20bc06a7", + "endpoint_files": 8, + "client_files": 3, + "configurations_disabled": true, + "source_package_installed": false, + "target_package_installed": false, + "mappings_assigned": false, + "credentials_created": false + }, + "unit": { + "command": "php vendor/phpunit/phpunit/phpunit tests/Unit/SgcInvoiceClientTest.php --no-progress", + "tests": 16, + "assertions": 46, + "passed": true, + "database_connections": 0, + "network_requests": 0, + "evidence": "Observed successful command output; fake transport, no native SGC workflow boot" + }, + "kernel": { + "result_sha256": "f1e0c8d3311db387593b75720818ba5b6a57f7f0acd6146eb0fc6b72cf09bb12", + "checks": [ + "prepared client recovers real existing native receipt through kernel", + "client performs exact context read and explicit preview/recovery only", + "native protocol retains one original UUID", + "prepared client consumes real native impact without confirming", + "client contract proof adds no invoice or audit rows", + "serialised transport states exclude bearer credential" + ], + "new_business_rows": 0, + "existing_regression_book": "da77bb730751", + "tcp_8085": false + }, + "sources": [ + { + "file": "packages/sgc-invoice-client/src/InvoiceClient.php", + "sha256": "64572c1630f4ed05086241ed519abf02b6e16b508af5b71f2f099df1ed5a72d8" + }, + { + "file": "tests/Unit/SgcInvoiceClientTest.php", + "sha256": "a52660eda916daa6baf75394c6da16847ee8126fe845e572b15fb73de8cd5652" + }, + { + "file": "scripts/prepare-sgc-8085.mjs", + "sha256": "b0078b804210f042c2cdd55eb9bfcd2677bf4ad29f48a173b5783618dbe4b051" + }, + { + "file": "scripts/sgc-invoice-client-kernel-proof.php", + "sha256": "f1527a69e786751a3cd7111cc8fd638460b50cb16b8904ef69f88181b83da5c1" + } + ], + "sgc_sources": [ + { + "file": "C:/laragon/www/compras/app/Services/SgfInvoiceGateway.php", + "sha256": "8fb4eb64f63562ca83be96584beb42de70b4f47a951900c92d14a8ab7e7b8039" + }, + { + "file": "C:/laragon/www/compras/config/sgf_invoice.php", + "sha256": "e82fea9fae8ab6fe0fb30daf21b88fd31881a20b3ecbdec28f88edc1615e8fdd" + } + ], + "reused_proofs": [ + { + "file": "docs/proofs/sgc-invoice-endpoint-20261110.json", + "sha256": "5dcdb843b18c1d33e9b1bc8351d7d0addeb780a63ae9fb374821bd414faa7c75", + "checks": 78, + "repeated": false + }, + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "checks": 63, + "repeated": false + } + ], + "preservation": { + "protected_8085_code_and_state_unchanged": true, + "protected_8085_database_writes": 0, + "sgc_pilot_database_connections": 0, + "six_records_changed": false, + "cache_cleared": false, + "new_demo": false, + "new_database": false, + "new_server": false, + "new_role": false, + "new_real_rights": false, + "activation": false, + "publication": false, + "checkpoint": false + }, + "pending": [ + "Durable unregistered source-draft/intent/approval/receipt workflow and UI in SGC before use; no existing SGC Invoice forwarding", + "Explicit approved source/target/order/line mappings", + "Approved named QA principal and private credentials/context versions", + "Disabled-code installation followed by separately approved end-to-end network trial in the same8085", + "Real use/deployment is not established by these preparation checks" + ], + "report": "docs/SGC_8085_INTEGRATION_PREPARATION.md" +} diff --git a/docs/proofs/sgc-invoice-endpoint-20261110.json b/docs/proofs/sgc-invoice-endpoint-20261110.json new file mode 100644 index 0000000..a95ed73 --- /dev/null +++ b/docs/proofs/sgc-invoice-endpoint-20261110.json @@ -0,0 +1,237 @@ +{ + "date": "2026-11-10", + "passed": true, + "checks": 78, + "tests": { + "result.json": { + "sha256": "ba882fdd91b731f128f77810e737960ca60962864c5bcc8a903858fd580163e3", + "checks": [ + "existing regression native order readable with mapped identity", + "actual invoice preview via API transport", + "preview has no budget consumption or payable", + "preview rolls all business rows back", + "successful response is not cached", + "disabled endpoint refuses even valid credentials", + "missing or wrong principal/token refused", + "missing or wrong principal/token refused", + "missing or wrong principal/token refused", + "stale context version refused", + "non-TLS remote request refused", + "non JSON refused", + "oversized body refused", + "malformed JSON refused", + "client cannot choose tenant", + "supplier cannot be silently associated", + "floating point amounts refused", + "inconsistent total refused by original writer", + "foreign native line refused", + "foreign native order refused", + "malformed idempotency key refused", + "revoked authentication version refused", + "unauthorised unit mapping refused", + "revoked prepare right refused", + "confirmation creates pending invoice only", + "lost response recovered by same confirmation", + "same request recovered through preview after commit", + "same key different content conflicts", + "confirmation must use the preview key", + "tampered preview refused", + "replay revalidates current authority", + "second individually prepared document confirmed", + "changed source invalidates earlier preview", + "failure between writer and receipt rolls invoice back", + "same operation recovers after failure", + "concurrent same-key confirmations return one create and one recovery", + "concurrent receipts identify one invoice", + "no new consumption stock obligation payment or ledger effects", + "previous accepted invoice rows preserved", + "host rows permissions budget and sequences unchanged", + "8085 host/native all rows and sequences unchanged", + "context cleaned after request" + ] + }, + "security-result.json": { + "sha256": "60b3e603926d1896ef0a37366e57f05b54f21eb68b61c14865839b09b0016ef4", + "checks": [ + "existing confirmed invoice recovered without new business write", + "foreign tenant_id principal rejected", + "foreign company_id principal rejected", + "disabled principal rejected even for replay", + "rotated credential invalidates old bearer", + "old context header rejected after rotation", + "old preview rejected with new context header", + "disabled purchasing module refuses API replay", + "same key cannot be moved to another authorised order", + "UUID case does not create a second operation", + "query cannot inject another context", + "nested unknown authority field rejected", + "transport cannot approve or consume invoice", + "legacy payment route remains unmounted", + "durable receipt recovered after preview lifetime with current authority", + "expired unapplied preview cannot write", + "lock timeout returns recoverable503 without duplicate", + "after lock release same operation recovers correct invoice", + "security and lock continuation added no invoices or audit commands", + "source configuration remains disabled with no principals" + ] + }, + "context-result.json": { + "sha256": "a8260e789c2020fcca9c9bd159e6f73c221fd9dc320fd81ccf2c4b65e377ab57", + "checks": [ + "authenticated exact order exposes current revision and lines", + "context response is uncached and excludes unrelated invoice list", + "context read requires bearer", + "context read refuses foreign tenant order", + "context read rejects query authority", + "context read rejects a request body", + "context read revalidates view grant", + "context route also remains disabled", + "POST durable replay unchanged after adding read-only context route", + "context checks add no business effects" + ] + }, + "authority-result.json": { + "sha256": "789964b5f41a4bc143caad7d69e445890125f0ceb0db49c17092fbd68495b124", + "checks": [ + "current native operator lock permits authorised recovery", + "revoked native finance function prevents receipt recovery", + "native finance fixture restored by transaction rollback", + "maintenance-only native operator cannot recover invoice receipt", + "normal recovery resumes without another business command", + "operator guard checks add no invoice or audit commands" + ] + } + }, + "transport": "Laravel HTTP kernel, actual PostgreSQL and independent PHP workers; no TCP server", + "routes": [ + "POST /api/v1/integration/sgc/invoices", + "GET /api/v1/integration/sgc/invoices/orders/{order}" + ], + "result": { + "new_pending_invoices": 4, + "concurrent_statuses": [ + 200, + 201 + ], + "lock_statuses": [ + 503, + 200 + ], + "invoice_audit_atomicity_proved": true, + "workflow": "original pending invoice writer", + "auto_financial_validation": false + }, + "isolation": { + "existing_regression_book": "da77bb730751", + "source_fixture": "SQL fixture only; not executed request/award/receipt/stock workflows", + "protected_8085_hashes": [ + "6168b382599448d71b39691ba4c347db0c8ab8a8d8dc7388283b212239ac9290", + "58e032c3a30234433adf66cc75478bd729ba0f62592637cf737e2ee23e01773b" + ], + "protected_8085_code_unchanged": true, + "prior_receipts_and_movements_sha256": "8fc992e83559200f691b253fd72e22986a972eb58f77b50bdf32d4d18d5f5dd4", + "prior_consumed_minor": 10000, + "external_payments": 0, + "six_protected_records_changed": false, + "cache_cleared": false, + "new_database": false, + "new_role": false, + "new_server": false, + "new_demo": false, + "new_real_grants": false + }, + "disabled_configuration": { + "enabled": false, + "principals": [] + }, + "publication": false, + "checkpoint": false, + "business_authority_unchanged": [ + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "sha256": "9b426476b2805094fb317cc753385ef43882fb4964883e880eac287eed89d9e0" + }, + { + "file": "packages/payables/src/Payables.php", + "sha256": "ed853f3eb48ebcf91f51a8fe9ed8e49b0e1bc30cce4852cc9394d51cff28c74d" + } + ], + "sources": [ + { + "file": "bootstrap/app.php", + "sha256": "42aacf13b19658e80b93c8b2f640133a349e9302f295b5e4c57702e352013382" + }, + { + "file": "routes/sgc.php", + "sha256": "1ca49298535c1edfe1afd494b56274ee1b25842079f4da7557755db471c73348" + }, + { + "file": "config/sgc_invoice.php", + "sha256": "bd5615a9eadd5d98e831db10ce60713d792f9b324282fdd87d52367ad2bf62a4" + }, + { + "file": "app/Shared/Context/ModuleNavigation.php", + "sha256": "88a86523568643b4f285647ab341c435819715e9b7eb963bf8a44b3a4167f6dc" + }, + { + "file": "app/Http/Middleware/AuthenticateSgcInvoice.php", + "sha256": "f99e2e26a8b09c31e1b84618a10ec6e7016a9b02a7f9049b64c5a157cc1d9888" + }, + { + "file": "app/Http/Requests/Sgc/StoreInvoiceRequest.php", + "sha256": "e2a859fe54c412cba9cd70353c98582774034996c382e1da4916abef46497d47" + }, + { + "file": "app/Http/Controllers/Api/V1/SgcInvoiceIngressController.php", + "sha256": "d590601efa6c46d9a34f77c655767758c88326e4d64f0da1b66f3174216ad782" + }, + { + "file": "app/Modules/Purchasing/SgcInvoiceIngress.php", + "sha256": "4acde7b84193b509f09d0cac5c7993b298384651443c0ce58ee9effcefc4d6bf" + }, + { + "file": "scripts/sgc-invoice-proof-bootstrap.php", + "sha256": "0988f2e9f6c2b59e057de893fafe8996070e313354d82056ad4494223f0efea7" + }, + { + "file": "scripts/sgc-invoice-proof.php", + "sha256": "725f985e8429a7456b87d44066e8339b3204255f07dc83bde2806b75158bcf85" + }, + { + "file": "scripts/sgc-invoice-security-proof.php", + "sha256": "71095d6c20fbbe0e5874741a915de6c45c37cae4149c38c027559c090f91b146" + }, + { + "file": "scripts/sgc-invoice-context-proof.php", + "sha256": "44d290770f6e3bd2127b0be96c2e663aef9b67d0d68a4e29d994a76c5f1247ec" + }, + { + "file": "scripts/sgc-invoice-authority-proof.php", + "sha256": "d5b41a4594b79e0b61a40654d831a00e2e4dc9542df94672ac755aa2565b2087" + }, + { + "file": "scripts/sgc-invoice-check.mjs", + "sha256": "7ecc6bc75d228285e3a5917b9d8db78d36ac7b3cbdaa3c9d1a42e305cf080119" + }, + { + "file": "scripts/summarize-sgc-invoice.php", + "sha256": "52f3aa34fce4d7463aa614280500ad4ef4453d43f93d78a86567a0ae2ed4a91a" + }, + { + "file": "tests/payloads/invoice_test.json", + "sha256": "1a584f2540f48be5b0e57df67796aff07fb77511f3d347a606a02a95b43a34a6" + } + ], + "report": "docs/SGC_INVOICE_ENDPOINT.md", + "limitations": [ + "QA adapter only; no real activation/deployment.", + "Kernel requests are not network/TLS/visual or sustained-load tests.", + "Initial72 checks reused; last6 exercise the strengthened native-operator recovery guard without repeating writes.", + "Synthetic bearer tokens, encrypted previews and private logs are excluded from this proof.", + "Source preparation adds SQL fixtures to the existing regression book; previous proofs and rows are preserved." + ] +} diff --git a/docs/proofs/sgc-invoice-workspace-20261110.json b/docs/proofs/sgc-invoice-workspace-20261110.json new file mode 100644 index 0000000..6400eeb --- /dev/null +++ b/docs/proofs/sgc-invoice-workspace-20261110.json @@ -0,0 +1,225 @@ +{ + "date": "2026-11-10", + "passed": true, + "scope": "SGC durable invoice source workspace for SGF intake; no operational activation", + "sqlite": { + "unique_cases": 19, + "assertions": 138, + "cases": { + "test_full_durable_cycle_and_audited_decision": 15, + "test_lost_response_reuses_same_key_cut_and_durable_intent": 7, + "test_failure_after_remote_commit_keeps_durable_sending_for_recovery": 7, + "test_revision_preserves_before_after_and_invalidates_preview": 8, + "test_no_edit_after_any_confirmation_started": 3, + "test_local_registration_is_blocked_even_when_feature_is_disabled": 3, + "test_existing_local_invoice_cannot_be_sent": 3, + "test_duplicate_source_cannot_get_second_intent": 3, + "test_stale_version_and_wrong_approval_do_not_send": 5, + "test_operator_context_and_mapping_changes_require_new_conference": 5, + "test_http_screen_csrf_and_audited_history_are_private": 16, + "test_foreign_tenant_and_warehouse_actor_cannot_read_or_write": 6, + "test_pagination_history_and_output_escaping": 10, + "test_events_and_drafts_cannot_be_deleted_or_rewritten": 5, + "test_disabled_transport_keeps_draft_and_disabled_workspace_refuses": 6, + "test_changed_remote_context_refuses_decision_before_recording_it": 8, + "test_revoked_operator_cannot_confirm_and_live_lease_prevents_duplicate_send": 5, + "test_actual_screen_forms_complete_registration_and_keep_exact_sgf_handoff": 18, + "test_missing_schema_is_refused_without_activating_or_migrating": 5 + }, + "junit": [ + { + "private_file": "source-sqlite-junit.xml", + "sha256": "ac2adb90fda1ad7837033a58ea441b038f86c9eaff0f7c2b4c1645a999a77f0d" + }, + { + "private_file": "source-screen-junit.xml", + "sha256": "3a31a747dab297e58c67e4c9f964eb3286f0e9ee5f1907fd6332064d9c73e6b8" + }, + { + "private_file": "source-readiness-junit.xml", + "sha256": "34115e3e5d18a6c526705ad17db38d9ee92a9ff5a85711303b9d0e50757eebad" + } + ], + "database": ":memory:", + "migration_scope": "only minimal synthetic fixtures and the new migration" + }, + "postgresql": { + "checks": [ + "two source persistence tables installed only in existing regression book", + "actual SGC service persists real SGF kernel preview", + "remote commit with lost response is durably uncertain at source", + "source recovers one actual pending invoice from receiver", + "first end-to-end intent creates exactly one receiver invoice", + "native SGC writer refuses claimed invoice before any local duplicate", + "concurrent source decisions yield one send and stale/in-flight refusal", + "second end-to-end intent creates only one receiver invoice", + "source has one durable decision and receipt for concurrent intent", + "PostgreSQL refuses rewriting source event history", + "all original source business rows and sequences preserved", + "receiver budget stock and payments unchanged by pending-document intake", + "8085 all host/native rows and sequences unchanged", + "exactly two individually audited decisions for two sources" + ], + "result_sha256": "17c702478dd6bb3cf81367a370a39f36f1b290ace02902f5a2b72b7680bf638a", + "source_book": "b4093c6f6707", + "receiver_book": "da77bb730751", + "new_receiver_pending_invoices": 2, + "source_concurrent_statuses": [ + 200, + 409 + ], + "remote_commit_response_loss_recovered": true, + "new_schema_only_in_regression": true, + "original_source_rows_and_sequences_preserved": true + }, + "ui": { + "route": "/facturas/envio-sgf", + "blade_http_forms_tested": true, + "history_and_list_page_size": 25, + "typescript_no_emit_exit": 0, + "private_tsx_sha256": "f8b1869842dc10832fc9d9a90678a4e620c98dd6a97b40bb0dea7b6021508e23", + "typescript_diagnostics_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "rendered_inline_scripts_syntax_checked": 3, + "visual_browser_review": false, + "full_product_build": false + }, + "preparation": { + "id": "4a6df60a8e9f8ae95c1d", + "manifest_sha256": "bb09254e16a7598fb797af1bbb69d7e4b2992e5580a248747c30c7a44ffecf04", + "endpoint_files": 8, + "client_source_files": 15, + "installed": false, + "activated": false, + "sql_privileges_approved": false + }, + "preservation": { + "8085_all_host_native_rows_and_sequences_sha256": [ + "6168b382599448d71b39691ba4c347db0c8ab8a8d8dc7388283b212239ac9290", + "58e032c3a30234433adf66cc75478bd729ba0f62592637cf737e2ee23e01773b" + ], + "8085_code_unchanged": true, + "six_records_changed": false, + "cache_cleared": false, + "pilot_schema_changed": false, + "new_operational_credentials": false, + "new_business_rights": false, + "new_database": false, + "new_role": false, + "new_demo": false, + "new_http_server": false, + "publication": false, + "checkpoint": false + }, + "sources": [ + { + "file": "C:\\laragon\\www\\sgf-tempo/packages/sgc-invoice-client/src/InvoiceClient.php", + "sha256": "64572c1630f4ed05086241ed519abf02b6e16b508af5b71f2f099df1ed5a72d8" + }, + { + "file": "C:/laragon/www/compras/app/Services/SgfInvoiceGateway.php", + "sha256": "8fb4eb64f63562ca83be96584beb42de70b4f47a951900c92d14a8ab7e7b8039" + }, + { + "file": "C:/laragon/www/compras/config/sgf_invoice.php", + "sha256": "8f90cd662cea2537b578a514a5c14a951db750360799fb12c50a65c7ef444d1d" + }, + { + "file": "C:/laragon/www/compras/app/Services/SgfInvoiceDispatches.php", + "sha256": "206fc70f4f99689cace59b3f17ca35e78683ed29bfe0f3cecd59adb6c611cf7d" + }, + { + "file": "C:/laragon/www/compras/app/Http/Controllers/SgfInvoiceDispatchController.php", + "sha256": "224bbebe0df3cd82fbab55a02408534704c170d54b536ffc0e3cd8f9ccd99942" + }, + { + "file": "C:/laragon/www/compras/database/migrations/2026_11_10_000001_create_sgf_invoice_dispatches.php", + "sha256": "91702237b3fde1026dd63b6a26dd571fa0e331235fc216dd927d340c5ecb1888" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/layout.blade.php", + "sha256": "f4da950b56cff30c57961485f83876ea4fdafa389360a3294d0f35bf025c9cec" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/document-form.blade.php", + "sha256": "8333219385743cc1826ef7ba29ee68380c08fd44e96c612f4021606d95df4360" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/index.blade.php", + "sha256": "19ce2d589f208e585f906fa3633d6881286c64a3477d997e03269c41c066ac05" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/evidence.blade.php", + "sha256": "e65a8830126061626532dc47f3536d726fd1388cca373dfc3dea5295575fa166" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/show.blade.php", + "sha256": "a5f5b4859872f18875dfbc08bbc63d9df3cf675d0d89ece7224fdb868a52b6cb" + }, + { + "file": "C:/laragon/www/compras/app/Services/InvoiceWorkflow.php", + "sha256": "4f056f195fb7a091ec4e80dead714241514e34c7998695607c6803959fd0561c" + }, + { + "file": "C:/laragon/www/compras/app/Http/Controllers/InvoiceController.php", + "sha256": "37d9e1037dca3bd142bbbae30f40aaa6d68c5cf3f08b3b22a6a2a3824e12cc7c" + }, + { + "file": "C:/laragon/www/compras/routes/web.php", + "sha256": "d842702959639234e849bc78fb029af0d8018889f6632ee178997ba79b5bea56" + }, + { + "file": "C:/laragon/www/compras/resources/js/pages/invoices/index.tsx", + "sha256": "fe9382794b440108be9d04a0a53cac1f95c58fe4abc5f673bd98d9525445e045" + }, + { + "file": "scripts/sgc-dispatch-receiver.php", + "sha256": "8919a3cb43743aacca2b797a1dd49710de809432a563f24f8037d32a3f39b872" + }, + { + "file": "scripts/sgc-dispatch-pg-bootstrap.php", + "sha256": "28265cad1a5b64875d03738c45d31a42d8aa7247c14a7e60511a68283cedc9ba" + }, + { + "file": "scripts/sgc-dispatch-pg-proof.php", + "sha256": "24ae626cc7e001f087d5379540ac9874261588723b5aa209802c611e349ab5c1" + }, + { + "file": "scripts/summarize-sgc-dispatch.php", + "sha256": "4a6b19761e5e073142b878a56997bc6a2ac21a062c5adfa2d6b25b7ac10a4c1d" + }, + { + "file": "scripts/prepare-sgc-8085.mjs", + "sha256": "57e0136bc5e4dd26c84719a6972acf5a5a936e2cc9b9debcc4bb131309eadc3b" + }, + { + "file": "C:/laragon/www/compras/tests/Integration/SgfInvoiceDispatchesTest.php", + "sha256": "0635ea1edac965da339159b9bd7c1cae9e8e304767a25ad590334205e1c186cd" + } + ], + "reused_proofs": [ + { + "file": "docs/proofs/sgc-invoice-endpoint-20261110.json", + "sha256": "5dcdb843b18c1d33e9b1bc8351d7d0addeb780a63ae9fb374821bd414faa7c75", + "repeated": false + }, + { + "file": "docs/proofs/sgc-8085-integration-preparation-20261110.json", + "sha256": "c9db703f25731610ab58f19a87917eb5a112b507159bba587dfdc8e0e8e76216", + "repeated": false + }, + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "repeated": false + } + ], + "report": "docs/SGC_INVOICE_WORKSPACE_DELIVERY.md", + "limits": [ + "Two real application kernels in independent PHP processes, intercepted CLI bridge; no TCP8085 or pilot activation.", + "The initial receiver harness inherited SGC cache metadata; isolated environment corrected and same draft/key resumed without replaying an accepted confirmation.", + "19 unique selected cases, not20 executions or a sum with old endpoint/client suites.", + "Final schema-readiness guard covered by a directed SQLite check; successful integrated business commits were not repeated.", + "No full product bundle build, visual review, sustained load, disaster recovery or deployment capacity claim.", + "Exact source migration/SQL privileges, code installation, approved mappings/principal/secret and activation still require their authorised rollout." + ] +} diff --git a/docs/proofs/sgc-invoice-workspace-final-20261110.json b/docs/proofs/sgc-invoice-workspace-final-20261110.json new file mode 100644 index 0000000..f99a7d9 --- /dev/null +++ b/docs/proofs/sgc-invoice-workspace-final-20261110.json @@ -0,0 +1,234 @@ +{ + "date": "2026-11-10", + "passed": true, + "scope": "SGC durable invoice source workspace for SGF intake; no operational activation", + "sqlite": { + "unique_cases": 20, + "assertions": 142, + "cases": { + "test_full_durable_cycle_and_audited_decision": 15, + "test_lost_response_reuses_same_key_cut_and_durable_intent": 7, + "test_failure_after_remote_commit_keeps_durable_sending_for_recovery": 7, + "test_revision_preserves_before_after_and_invalidates_preview": 8, + "test_no_edit_after_any_confirmation_started": 3, + "test_local_registration_is_blocked_even_when_feature_is_disabled": 3, + "test_existing_local_invoice_cannot_be_sent": 3, + "test_duplicate_source_cannot_get_second_intent": 3, + "test_stale_version_and_wrong_approval_do_not_send": 5, + "test_operator_context_and_mapping_changes_require_new_conference": 5, + "test_http_screen_csrf_and_audited_history_are_private": 16, + "test_foreign_tenant_and_warehouse_actor_cannot_read_or_write": 6, + "test_pagination_history_and_output_escaping": 10, + "test_events_and_drafts_cannot_be_deleted_or_rewritten": 5, + "test_disabled_transport_keeps_draft_and_disabled_workspace_refuses": 6, + "test_changed_remote_context_refuses_decision_before_recording_it": 8, + "test_revoked_operator_cannot_confirm_and_live_lease_prevents_duplicate_send": 5, + "test_actual_screen_forms_complete_registration_and_keep_exact_sgf_handoff": 18, + "test_missing_schema_is_refused_without_activating_or_migrating": 5, + "test_audit_freezes_preview_actor_and_context_fingerprint": 4 + }, + "junit": [ + { + "private_file": "source-sqlite-junit.xml", + "sha256": "ac2adb90fda1ad7837033a58ea441b038f86c9eaff0f7c2b4c1645a999a77f0d" + }, + { + "private_file": "source-screen-junit.xml", + "sha256": "3a31a747dab297e58c67e4c9f964eb3286f0e9ee5f1907fd6332064d9c73e6b8" + }, + { + "private_file": "source-readiness-junit.xml", + "sha256": "34115e3e5d18a6c526705ad17db38d9ee92a9ff5a85711303b9d0e50757eebad" + }, + { + "private_file": "source-context-audit-junit.xml", + "sha256": "3b5e352a1cb43e576e714e5753862b961cd3ceb3659fbb63b6df48c04e5dc81f" + } + ], + "database": ":memory:", + "migration_scope": "only minimal synthetic fixtures and the new migration" + }, + "postgresql": { + "checks": [ + "two source persistence tables installed only in existing regression book", + "actual SGC service persists real SGF kernel preview", + "remote commit with lost response is durably uncertain at source", + "source recovers one actual pending invoice from receiver", + "first end-to-end intent creates exactly one receiver invoice", + "native SGC writer refuses claimed invoice before any local duplicate", + "concurrent source decisions yield one send and stale/in-flight refusal", + "second end-to-end intent creates only one receiver invoice", + "source has one durable decision and receipt for concurrent intent", + "PostgreSQL refuses rewriting source event history", + "all original source business rows and sequences preserved", + "receiver budget stock and payments unchanged by pending-document intake", + "8085 all host/native rows and sequences unchanged", + "exactly two individually audited decisions for two sources" + ], + "result_sha256": "17c702478dd6bb3cf81367a370a39f36f1b290ace02902f5a2b72b7680bf638a", + "source_book": "b4093c6f6707", + "receiver_book": "da77bb730751", + "new_receiver_pending_invoices": 2, + "source_concurrent_statuses": [ + 200, + 409 + ], + "remote_commit_response_loss_recovered": true, + "new_schema_only_in_regression": true, + "original_source_rows_and_sequences_preserved": true + }, + "ui": { + "route": "/facturas/envio-sgf", + "blade_http_forms_tested": true, + "history_and_list_page_size": 25, + "typescript_no_emit_exit": 0, + "private_tsx_sha256": "f8b1869842dc10832fc9d9a90678a4e620c98dd6a97b40bb0dea7b6021508e23", + "typescript_diagnostics_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "rendered_inline_scripts_syntax_checked": 3, + "visual_browser_review": false, + "full_product_build": false + }, + "preparation": { + "id": "3bb99e866f52965dbb7d", + "manifest_sha256": "b4abf43bcaa78a674b2b79b17143d8711eae63eba231330feae962c66cd004fe", + "endpoint_files": 8, + "client_source_files": 15, + "installed": false, + "activated": false, + "sql_privileges_approved": false + }, + "preservation": { + "8085_all_host_native_rows_and_sequences_sha256": [ + "6168b382599448d71b39691ba4c347db0c8ab8a8d8dc7388283b212239ac9290", + "58e032c3a30234433adf66cc75478bd729ba0f62592637cf737e2ee23e01773b" + ], + "8085_code_unchanged": true, + "six_records_changed": false, + "cache_cleared": false, + "pilot_schema_changed": false, + "new_operational_credentials": false, + "new_business_rights": false, + "new_database": false, + "new_role": false, + "new_demo": false, + "new_http_server": false, + "publication": false, + "checkpoint": false + }, + "sources": [ + { + "file": "C:\\laragon\\www\\sgf-tempo/packages/sgc-invoice-client/src/InvoiceClient.php", + "sha256": "64572c1630f4ed05086241ed519abf02b6e16b508af5b71f2f099df1ed5a72d8" + }, + { + "file": "C:/laragon/www/compras/app/Services/SgfInvoiceGateway.php", + "sha256": "8fb4eb64f63562ca83be96584beb42de70b4f47a951900c92d14a8ab7e7b8039" + }, + { + "file": "C:/laragon/www/compras/config/sgf_invoice.php", + "sha256": "8f90cd662cea2537b578a514a5c14a951db750360799fb12c50a65c7ef444d1d" + }, + { + "file": "C:/laragon/www/compras/app/Services/SgfInvoiceDispatches.php", + "sha256": "643c77976f9a71e0e61402dcb246eb762a26bf1f8013f17528c1fb0e8d669186" + }, + { + "file": "C:/laragon/www/compras/app/Http/Controllers/SgfInvoiceDispatchController.php", + "sha256": "224bbebe0df3cd82fbab55a02408534704c170d54b536ffc0e3cd8f9ccd99942" + }, + { + "file": "C:/laragon/www/compras/database/migrations/2026_11_10_000001_create_sgf_invoice_dispatches.php", + "sha256": "91702237b3fde1026dd63b6a26dd571fa0e331235fc216dd927d340c5ecb1888" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/layout.blade.php", + "sha256": "f4da950b56cff30c57961485f83876ea4fdafa389360a3294d0f35bf025c9cec" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/document-form.blade.php", + "sha256": "8333219385743cc1826ef7ba29ee68380c08fd44e96c612f4021606d95df4360" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/index.blade.php", + "sha256": "19ce2d589f208e585f906fa3633d6881286c64a3477d997e03269c41c066ac05" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/evidence.blade.php", + "sha256": "e65a8830126061626532dc47f3536d726fd1388cca373dfc3dea5295575fa166" + }, + { + "file": "C:/laragon/www/compras/resources/views/sgf-invoices/show.blade.php", + "sha256": "a5f5b4859872f18875dfbc08bbc63d9df3cf675d0d89ece7224fdb868a52b6cb" + }, + { + "file": "C:/laragon/www/compras/app/Services/InvoiceWorkflow.php", + "sha256": "4f056f195fb7a091ec4e80dead714241514e34c7998695607c6803959fd0561c" + }, + { + "file": "C:/laragon/www/compras/app/Http/Controllers/InvoiceController.php", + "sha256": "37d9e1037dca3bd142bbbae30f40aaa6d68c5cf3f08b3b22a6a2a3824e12cc7c" + }, + { + "file": "C:/laragon/www/compras/routes/web.php", + "sha256": "d842702959639234e849bc78fb029af0d8018889f6632ee178997ba79b5bea56" + }, + { + "file": "C:/laragon/www/compras/resources/js/pages/invoices/index.tsx", + "sha256": "fe9382794b440108be9d04a0a53cac1f95c58fe4abc5f673bd98d9525445e045" + }, + { + "file": "scripts/sgc-dispatch-receiver.php", + "sha256": "8919a3cb43743aacca2b797a1dd49710de809432a563f24f8037d32a3f39b872" + }, + { + "file": "scripts/sgc-dispatch-pg-bootstrap.php", + "sha256": "28265cad1a5b64875d03738c45d31a42d8aa7247c14a7e60511a68283cedc9ba" + }, + { + "file": "scripts/sgc-dispatch-pg-proof.php", + "sha256": "24ae626cc7e001f087d5379540ac9874261588723b5aa209802c611e349ab5c1" + }, + { + "file": "scripts/summarize-sgc-dispatch.php", + "sha256": "a77dac8b28574f3ed5c9f9a372bc44d921081d6c9656fd322a385c40b2da061c" + }, + { + "file": "scripts/prepare-sgc-8085.mjs", + "sha256": "57e0136bc5e4dd26c84719a6972acf5a5a936e2cc9b9debcc4bb131309eadc3b" + }, + { + "file": "C:/laragon/www/compras/tests/Integration/SgfInvoiceDispatchesTest.php", + "sha256": "e78368849e4f4cf5bd3bcb9cd6dc52215cffecba3dd5aba76de2a7838b9cb9bd" + } + ], + "reused_proofs": [ + { + "file": "docs/proofs/sgc-invoice-endpoint-20261110.json", + "sha256": "5dcdb843b18c1d33e9b1bc8351d7d0addeb780a63ae9fb374821bd414faa7c75", + "repeated": false + }, + { + "file": "docs/proofs/sgc-8085-integration-preparation-20261110.json", + "sha256": "c9db703f25731610ab58f19a87917eb5a112b507159bba587dfdc8e0e8e76216", + "repeated": false + }, + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "repeated": false + } + ], + "report": "docs/SGC_INVOICE_WORKSPACE_DELIVERY.md", + "limits": [ + "Two real application kernels in independent PHP processes, intercepted CLI bridge; no TCP8085 or pilot activation.", + "The initial receiver harness inherited SGC cache metadata; isolated environment corrected and same draft/key resumed without replaying an accepted confirmation.", + "20 unique selected cases; earlier19-case summary preserved, duplicate executions not added.", + "Final schema-readiness and frozen actor/context audit metadata covered by directed SQLite checks; integrated commits not repeated.", + "No full product bundle build, visual review, sustained load, disaster recovery or deployment capacity claim.", + "Exact source migration/SQL privileges, code installation, approved mappings/principal/secret and activation still require their authorised rollout." + ], + "initial_proof_preserved": { + "file": "docs/proofs/sgc-invoice-workspace-20261110.json", + "sha256": "d1d8c20867604171dbd8f3926916c4df43c4571717c673d94d5aeaa66860c7dd" + } +} diff --git a/docs/proofs/sgc-preparation-20261110.json b/docs/proofs/sgc-preparation-20261110.json new file mode 100644 index 0000000..415024d --- /dev/null +++ b/docs/proofs/sgc-preparation-20261110.json @@ -0,0 +1,75 @@ +{ + "date": "2026-11-10", + "database": "sgf_tempo_dev", + "port": 55432, + "migrations": [ + { + "file": "database/migrations/2026_11_09_000001_create_sgc_integration_tables.php", + "batch": 18 + }, + { + "file": "database/migrations/2026_11_09_000002_create_sgc_analytics_tables.php", + "batch": 19 + } + ], + "calendar": { + "start": "2026-01-01", + "end": "2035-12-31", + "rows": 3652, + "first_inserted": 3652, + "repeated_inserted": 0, + "official_holidays_validated": false + }, + "unit_tests": { + "tests": 4, + "assertions": 17, + "passed": true, + "database_connections": 0 + }, + "preservation": { + "preexisting_tables": 86, + "all_rows_identical": true, + "snapshot_sha256": "700dbf44140801dd93b194cd8e8c74e7822a5aef1494036a2ccd8f851c64face", + "private_backup_sha256": "3592c2b865a9671f2df51622a4c9cd2f402e888b2b823278eac64be959ac64fe", + "data_cache_cleared": false, + "existing_six_records_changed": false, + "new_explicit_grants": false, + "new_demo": false + }, + "cache_commands": [ + "config:clear", + "route:clear" + ], + "endpoint": { + "http_status": 404, + "diagnostic_empty_json": true, + "positive_invoice_test_completed": false, + "payload_file_present": false, + "routes_registered": false, + "activated": false + }, + "sources": [ + { + "file": "app/Console/Commands/PopulateDimTempo.php", + "sha256": "9d03d0cbbc6b4d60605061b3726f02159b409b42f26853ce1b73b374a27ec581" + }, + { + "file": "app/Services/Sgc/CalendarDimension.php", + "sha256": "51884e37b7adac68e85d9aa5596414787b3c3090ca09a37cce845046d6880db9" + }, + { + "file": "database/migrations/2026_11_09_000001_create_sgc_integration_tables.php", + "sha256": "228ceea51d000a5823b8952e7ff90a3a83c1cca266bf24e220ae80427679df05" + }, + { + "file": "database/migrations/2026_11_09_000002_create_sgc_analytics_tables.php", + "sha256": "1ea905e3fd3db41d0fdbee85f4ec0db1170af9e01656460e90b00eeae0e1d25f" + }, + { + "file": "tests/Unit/CalendarDimensionTest.php", + "sha256": "a78cf6ea3057f317b215aa47ba1372b76a69156f57753b81f419bc7af95548aa" + } + ], + "report": "docs/SGC_PREPARATION_20261110.md", + "deployment_validated": false +} diff --git a/docs/proofs/sgf-accounting-reports-20261110.json b/docs/proofs/sgf-accounting-reports-20261110.json new file mode 100644 index 0000000..942b54e --- /dev/null +++ b/docs/proofs/sgf-accounting-reports-20261110.json @@ -0,0 +1,63 @@ +{ + "passed": true, + "date": "2026-11-10", + "installation": "cc50a276c10c", + "port": 8085, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-205824-49693644\\accounting-report-boundaries.json", + "sha256": "1005f1566f316bc68243934d98881bae7ecd9f29b98b4cd5db57d3f1099512b1", + "checks": 5, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\accounting-reports-http-20261110.json", + "sha256": "4c319e135af06efec59a7c2ee91e9bef922c9612d8c4d7c7212267c945e4a851", + "checks": 19, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\accounting-reports-ui-20261110.json", + "sha256": "4638512a8951522b9da1315a6353b383b49218cb7b9a39c3dbb9d47a9e509ecd", + "checks": 14, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-205938-4a3c2c51\\accounting-reports-installation.json", + "sha256": "4a637461e01d6a6854f7c948552d45572179d66afb39d0925c36160a2ba570dc", + "checks": 0, + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "resources/js/AccountingMenuWorkspace.jsx", + "sha256": "f07dbbf60dfc3c26e528b0845becc22f851e8b774e368c44f33285775e124ea2" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + }, + { + "file": "resources/js/app.jsx", + "sha256": "93f6b46ecd17d3e80f31b63c3d6f9ac0c02e015876e9a4b114fabb6875310a23" + } + ], + "business_commands": 0, + "new_grants": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false +} diff --git a/docs/proofs/sgf-bank-reconciliation-20261110.json b/docs/proofs/sgf-bank-reconciliation-20261110.json new file mode 100644 index 0000000..da15184 --- /dev/null +++ b/docs/proofs/sgf-bank-reconciliation-20261110.json @@ -0,0 +1,138 @@ +{ + "passed": true, + "installation": "cc50a276c10c", + "port": 8085, + "date": "2026-11-10", + "scope": "SGF-BANK-RECONCILIATION-QA-v1", + "checks": { + "regression": 37, + "boundaries": 11, + "http": 28, + "dom_http": 15, + "report_http": 3 + }, + "concurrency": { + "during_lock": 503, + "after_release": 409, + "duplicate_reviews": 0 + }, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-192046-f2578deb\\bank-result.json", + "sha256": "fbb32ea79f056648fb3abec481fefb9ae6d9e6f04c65fbab376af65bda0d1a04", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\bank-concurrency\\result.json", + "sha256": "01ee8e63528ded2ae51bf93836cb236b772fca9bd52be2920bfa1ed290310747", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-192150-09f32ff0\\bank-boundaries.json", + "sha256": "2c056b940aa20025ef69231ee61f02a5967bfe3c4b0032884bd3c9dc01044b56", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\bank-http-20261110.json", + "sha256": "3c98024535d925ad32fec7c7cdba246367102f0b0d1e91249f50993ee77464b9", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\bank-dom-http-20261110.json", + "sha256": "63a0fd91ce123c8a2bf50963bc950d31f709281c2ecb2b267e74eaa58285278e", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\bank-report-http-20261110.json", + "sha256": "267b05f198d51c6bcbddff4b0e4fab72e99d3abc30974bc400b04e57de592e74", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-192840-498e9270\\bank-preservation.json", + "sha256": "d48fd73347c3499275269657a6c4cfef986eeb441f763a50e48225a60c03adcd", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-192015-f8b4be42\\bank-setup.json", + "sha256": "b7a90195403019cdc84c49cd0e61b5f3eb872ae0a92f5e2d58e8f260ac419e30", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-192538-3373509f\\bank-setup.json", + "sha256": "5c845e3f20ef77748addb3a2b09dcbd27a40413285143bb33f1e92836fe06418", + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Finance/BankReconciliation.php", + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "database/sql/bank_reconciliation.sql", + "sha256": "3c05a20fd46b3292017a2ab475be3418344de5a03271c4bf53d832bd97a40535" + }, + { + "file": "resources/views/finance/bank.blade.php", + "sha256": "6277db6224b71da394c8507c1d721fba6046a7d9ba9a5b564973018a7eac5fb3" + }, + { + "file": "resources/data/bank-statement-qa.csv", + "sha256": "bd2b29a50f952c27c4df37ef1c2cca0cdc7f241fbfcf186c82aa49ce9834ffcb" + }, + { + "file": "public/sgf-bank.mjs", + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Shared/Context/ModuleNavigation.php", + "sha256": "37f201571f8faf1819fa02876697604b32115754c488b71caef8e14c92275438" + }, + { + "file": "app/Modules/Identity/Services/IdentityWorkflow.php", + "sha256": "25e0b9c00e604e9406e2c145f8e21e06a57820ff8faa73f735c7421f52569560" + }, + { + "file": "resources/js/App.jsx", + "sha256": "ce55c8b035c256358e2d7b3f9018ff0dbf7a7bc4df2300e587a456793baf43af" + }, + { + "file": "resources/js/WorkspaceNavigation.jsx", + "sha256": "4f118ed4ed9ea40931084ea41bf2fffb0454c0904aeac83bcb4c92dd0f2cebed" + }, + { + "file": "routes/web.php", + "sha256": "bb0606a61a4d771a604a0fc9d93b24d6f93b59a55becc3eca1fcc4da35b6c573" + } + ], + "source_distinction": { + "regression": "simulated provider injected only in CLI harness", + "8085_http": "actual two native QA ledger lines", + "production_reader_fallback": false + }, + "statements_retained": 1, + "statement_rows_retained": 2, + "events_retained": 7, + "journals_preserved": 27, + "new_journals": 0, + "prior_iva_versions_preserved": 4, + "prior_rule_events_preserved": 4, + "prior_calculations_preserved": 2, + "grants_per_existing_qa_book": 8, + "new_demo": false, + "sgc_dependency": false, + "real_bank_operations": false, + "account_classification_changed": false, + "cache_cleared": false, + "visual_review": false, + "deployment_validated": false, + "acceptance": false +} diff --git a/docs/proofs/sgf-billing-register-20261110.json b/docs/proofs/sgf-billing-register-20261110.json new file mode 100644 index 0000000..7691b62 --- /dev/null +++ b/docs/proofs/sgf-billing-register-20261110.json @@ -0,0 +1,41 @@ +{ + "contract_sha256": "2d809fe09bb83bb0da37d37b0528895fb050cef0376435e95bc45119dfa7d091", + "new_demo": false, + "business_commands": 0, + "cache_cleared": false, + "passed": true, + "proofs": [ + { + "kind": "register_regression", + "sha256": "e4990e8cb95a91effeaa1a839ada0f8be73636d08af4559f705b9730de4a80a3", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-174502-e0b30bb6/billing-register-result.json" + }, + { + "kind": "register_http", + "sha256": "89e8ac8e05c1af1c73e65a1f46628492edb898562d1d87afe72194472eaae1db", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/billing-register-http-20261110.json" + }, + { + "kind": "cash_rights_readonly", + "sha256": "21dc9aaf95c2e93e14f02543cad94ef783cd118b78be4c487cf4beebe6a504f9", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-174122-55d06675/cash-rights-readonly.json" + } + ], + "date": "2026-11-10", + "new_schema": 0, + "deployment_capacity_validated": false, + "cash_approval_pending": "docs/integration/sgf-cash-access-proposal.json", + "cash_implemented": false, + "new_rights": 0, + "browser_visual_review": false, + "source_hashes": { + "app/Modules/Services/BillingRegister.php": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc", + "resources/js/BillingRegisterWorkspace.jsx": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9", + "routes/web.php": "18898252f2ffe8e9be20076d57bc3e3514df7d31eebc10ee64a697d7ff7bc696", + "resources/js/BillingMenuWorkspace.jsx": "7265e433bc2bf4b15674fc3a44892dfb288b4c2a2fe1db761f4ebd3c988da65b", + "app/Modules/Services/ServiceDocuments.php": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208", + "app/Modules/Services/Http/ServiceDocumentsController.php": "f8fa96e837b586468ad6a63d83dd942bb59a474ffd1bf905942d7712cdaff2e7" + }, + "published": false, + "installation": "cc50a276c10c" +} \ No newline at end of file diff --git a/docs/proofs/sgf-cash-20261110.json b/docs/proofs/sgf-cash-20261110.json new file mode 100644 index 0000000..fc22305 --- /dev/null +++ b/docs/proofs/sgf-cash-20261110.json @@ -0,0 +1,70 @@ +{ + "cash_closed_minor": 95000, + "public_grants": 0, + "installation": "cc50a276c10c", + "source_hashes": { + "app/Modules/Finance/CashWorkspace.php": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800", + "packages/general-ledger/src/PayableJournal.php": "100feec93b22d0f1b33f659445e8ef1bea73a39b0c1fb5e4d93d70fce5d97b01", + "app/Modules/Accounting/ComposedLedger.php": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035", + "packages/general-ledger/src/LedgerSales.php": "e339bd430e9632ba0db590a34f87742d7e7fa65e030e336a5b8340a6ca9e6c18", + "app/Modules/Accounting/CashLedgerSource.php": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b", + "app/Modules/Finance/Http/CashController.php": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78", + "public/sgf-cash.mjs": "24d8b9c20939dc9695aa18514ee9a94765ad486b0aab8fb4f567e7a3923d290f" + }, + "cash_right_names": 4, + "cash_grants_per_qa_book": 8, + "cache_cleared": false, + "journal_total": 27, + "new_profiles": 0, + "source_facts": 38, + "deployment_capacity_validated": false, + "passed": true, + "proofs": [ + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-175251-07723b42/cash-result.json", + "sha256": "4a491e7b992a1e0a1239f255a08e8760dc292a40f6ac5f7e322445c92cb7ea03", + "kind": "cash_regression" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/cash-concurrency/result.json", + "sha256": "18940a1a72dc1516ced3baf3225a24a24d555ed5517a36515b1bf2a01e47a07e", + "kind": "cash_concurrency" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-175845-a7f3eb7b/cash-read-boundaries.json", + "sha256": "68b45f74dab3d11d03d2b74713105cfcab2dc7f01c3007eea12838bce20747f1", + "kind": "cash_read_boundaries" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/cash-http-20261110.json", + "sha256": "412f1f0463e15cc3bb3a4f8d6ae9af9655ed309f033e646e435e44d99368d64f", + "kind": "cash_http" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/cash-dom-http-20261110.json", + "sha256": "c2008bdb29929e5a0a4db1fe8c51eda8886fbd7b248c54127d3665e23daa810d", + "kind": "cash_dom_http" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-180324-fe0a46fd/period-cash-result.json", + "sha256": "148761ae3b369f6eaea88ce1ca71ea94c933ce7dabaed9d421b920a3ab418376", + "kind": "cash_period" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-180329-e03178dc/cash-preservation.json", + "sha256": "427f59a867549b2badd8d76eb5c8d562223a7ab9ee97712515e14a4c5aa4c39c", + "kind": "cash_preservation" + } + ], + "previous_journals_compared_unchanged": 25, + "published": false, + "cash_audit_events": 9, + "real_money": false, + "date": "2026-11-10", + "entry": "http://127.0.0.1:8085/finance/cash", + "new_demo": false, + "browser_visual_review": false, + "sgc_required": false, + "qa_period_open": true, + "pending_facts": 0 +} \ No newline at end of file diff --git a/docs/proofs/sgf-composition-footprint-20261110.json b/docs/proofs/sgf-composition-footprint-20261110.json new file mode 100644 index 0000000..ac482cc --- /dev/null +++ b/docs/proofs/sgf-composition-footprint-20261110.json @@ -0,0 +1,380 @@ +{ + "date": "2026-11-10", + "contract_sha256": "147ae1653b028c0a09f296b9c87799de25ee9bad0fcf35a35840bfb6df6bd447", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6f5c8af14412427134f1c40bda9215de68b3a66d46ed2287b59895f68b929179" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "b5ac9898f2089fad7799b9e62e5ce5db6df7fd64643e8e7e3b52c6a0e2d142a8" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "347869a371084ae91bd7d651334f67f16369495263f28b8b8348bc442f7d6534" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Compor estruturas e guardas sintéticas na instalação única", + "Conservar o documento e identidade entre Comercial e Financeiro", + "Provar o percurso por interface na mesma empresa/unidade de apresentação" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "a8f3aac4b65ceea15ac7a8e8e34510d689b09fab8c2f689c466eb90d5e8632dc" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Reutilizar o percurso aceite sem o reexecutar por rotina", + "Ligar a continuação financeira e contabilística da mesma origem", + "Conservar a composição local sem serviço SGC obrigatório" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "01c558457df290879bdf6c7f8de8cd9efccad81b940731f0aaf2489cf02bfcf7" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "6460a4235fe7b0b4602b0aeb63a47f1a97e367e93baba6aa774be5fa0dac4e6e" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Instalar a interface de saída/transferência já implementada após decisão sobre os dois direitos propostos e mapeamentos; flag continua desligada", + "Concluir trânsito/recepção e divergências onde o percurso ainda é só definido", + "Conservar reservas, unidades e ausência de saldo negativo", + "Concluir interfaces de contagens/ajustamentos/devoluções e selecção de equipamento, preservando workflows próprios" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "6460a4235fe7b0b4602b0aeb63a47f1a97e367e93baba6aa774be5fa0dac4e6e" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "acc01d934dd11f783df82b40a6ece0905a67b0d26ff0bdfc3b18c11a027d66bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "99ea0b2c0941db0a42ff1f3833225430a73efb6933b3f88a8be4481f902ac571" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Compor na mesma instalação sem perder as aprovações existentes", + "Completar o percurso oficina → stock → custo → Comercial/Contabilidade", + "Conservar a distinção entre apontamento e movimento físico" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "725bc8773a78cfd05dd0a729d885a97c0eef43854ca7ab4210b4d5e80d5d54ec" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "pa_obligations", + "pa_access", + "ps_payments" + ], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Unificar as origens autorizadas sem copiar dívida ou saldos", + "Compor a tesouraria sintética e os relatórios na mesma empresa/unidade", + "Conservar vencimento desconhecido e histórico imutável" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "8c83538a5df4625bb3a15c81e3bf618d12187eca77da5f6e73dd1b4b781c64ce" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "1fb2490fe8d4d206d2f8157803307cdd4e2f733150d0fec4993a7e229fce58cb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "gl_accounts", + "gl_periods", + "gl_journals", + "gl_lines" + ], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Compor diário e relatórios reais do núcleo na instalação única", + "Ligar Compras/Comercial/custos sem gerar lançamentos automáticos", + "Substituir os catálogos de relatórios por consultas disponíveis e verificadas" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-financial-indicators-20261110.json b/docs/proofs/sgf-financial-indicators-20261110.json new file mode 100644 index 0000000..112ae44 --- /dev/null +++ b/docs/proofs/sgf-financial-indicators-20261110.json @@ -0,0 +1,128 @@ +{ + "passed": true, + "installation": "cc50a276c10c", + "port": 8085, + "date": "2026-11-10", + "scope": "SGF-FINANCIAL-INDICATORS-QA-v1", + "checks": { + "regression": 41, + "boundaries": 12, + "http": 29, + "dom_http": 16 + }, + "concurrency": { + "during_lock": 503, + "after_release": 409, + "duplicate_reviews": 0 + }, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-193838-60b5bbb2\\analysis-result.json", + "sha256": "1de78fbd0cbed88a17606000c718ee2b9b283f3a128ed384cf8b7961c8230b41", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\analysis-concurrency\\result.json", + "sha256": "28726d58f71957875738a93739d17fd8271295161ad9d983229084c13f12f517", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-193954-bd861757\\analysis-boundaries.json", + "sha256": "7c7cb82a2d1d702cad3c4e3c381515eb5b06f6658adb928c7ad3fd3134a3d875", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\analysis-http-20261110.json", + "sha256": "ffe1600fd0d43987531041dd6a6b7abd3a71f652e5c10aa3115942661116891f", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\analysis-dom-http-20261110.json", + "sha256": "564917db91aff0327e46d3367643afe1551dbf432dffb4daa9c5c06493956bfb", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-194532-f7d97924\\analysis-preservation.json", + "sha256": "9a7c439832d0de3af45bac1ed6e53bc048fe2e50b7a0f242a18721b246cdb83a", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-193836-0b408b46\\analysis-setup.json", + "sha256": "5f758ddf82a21117f531bbf7bdba686e817eb1d588723c9bc03d64395445ecc2", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-194340-de1f0d4f\\analysis-setup.json", + "sha256": "fda2b837bdfc8d8026632afaed1476542747e6c07f53f8ce5e8505e6eae9762c", + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "database/sql/financial_indicators.sql", + "sha256": "733cd4aa90911b1e6f5de4562eb21720d5abcc829fd8d7ba91b18a1ddfd8cfbc" + }, + { + "file": "resources/views/finance/indicators.blade.php", + "sha256": "32e02d53eb00a01eb463b26cd346a387f6edae5fe70e235276047e8d529beec5" + }, + { + "file": "public/sgf-indicators.mjs", + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "app/Shared/Context/ModuleNavigation.php", + "sha256": "3cfbb05e70405cf0e5086e952b0760d3fae2d068eea0802b27f8a8c8f0258753" + }, + { + "file": "app/Modules/Identity/Services/IdentityWorkflow.php", + "sha256": "a5b4bb3a8ba910072121c539bdf547fe89a0b6024764237e0141d222b4032000" + }, + { + "file": "resources/js/App.jsx", + "sha256": "2caac918a6e5c91608b2649a1ad8b1b3e7f47bf8c54e8eac0de7908fe2c20270" + }, + { + "file": "resources/js/WorkspaceNavigation.jsx", + "sha256": "1e16e4f2b9e44aece2f77c3ea4bb20147d29aa99cea4cdd10c06fe6469773f0f" + }, + { + "file": "routes/web.php", + "sha256": "d28dd8d20ea00401b1d86003dc0b05e24eb156bb0896064467ec177a6e938717" + } + ], + "source_distinction": { + "regression": "simulated provider injected only in CLI harness", + "8085_http": "actual composed cash and ledger sources", + "production_reader_fallback": false + }, + "criteria_events_retained": 4, + "results_retained": 2, + "journals_preserved": 27, + "new_journals": 0, + "prior_iva_versions_preserved": 4, + "prior_rule_events_preserved": 4, + "prior_calculations_preserved": 2, + "prior_bank_events_preserved": 7, + "grants_per_existing_qa_book": 7, + "new_demo": false, + "sgc_dependency": false, + "real_company_criteria": false, + "payment_recommendation": false, + "cache_cleared": false, + "visual_review": false, + "deployment_validated": false, + "acceptance": false +} diff --git a/docs/proofs/sgf-fiscal-coverage-20261110.json b/docs/proofs/sgf-fiscal-coverage-20261110.json new file mode 100644 index 0000000..9d59eb6 --- /dev/null +++ b/docs/proofs/sgf-fiscal-coverage-20261110.json @@ -0,0 +1,81 @@ +{ + "passed": true, + "installation": "cc50a276c10c", + "date": "2026-11-10", + "port": 8085, + "checks": { + "regression": 17, + "http": 10, + "render": 7, + "context_lifecycle": 3, + "entry_http": 3 + }, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-182835-4be44cc1\\fiscal-coverage-result.json", + "sha256": "2396a4ae9931e5c0cd4da6489966bed0db4145d22ba03abaa891321eaaa7631d", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\fiscal-coverage-http-20261110.json", + "sha256": "40133eee3d43307aaad3ebbf5ce455b9c214f099c81f51fe3f21db4c06587931", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\fiscal-coverage-ui-20261110.json", + "sha256": "03c9c888fd3d50ba16db3ffd32bd99cac8ab7eb75c8d695e4bd8e2ef74859953", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\fiscal-context-ui-20261110.json", + "sha256": "c3a6f368e68acbc213e73c0e4a94fe323187ce9fa3be0a274c2e5ae0525c86fb", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\fiscal-entry-http-20261110.json", + "sha256": "55962f9e6779447ce5f2ac7bc0af3187bb7a6a61e216edfff99a5c02f4cae383", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-183004-13ce4bbf\\fiscal-coverage-preservation.json", + "sha256": "ffdc500507689ebfb2a6f7ef1d12d54168bfe0be084ee3ec434c1587f1623af3", + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Services/FiscalCoverage.php", + "sha256": "239c23697dcfb306fddfb1bc9b92340cfcd5f5033916bdb11f611bf23bad0eac" + }, + { + "file": "app/Modules/Services/ServiceFiscal.php", + "sha256": "754d30fbe2f3c2c0e576d445a96449f54f9be93f2ce10858bbd31a228569d942" + }, + { + "file": "app/Shared/Installation/PresentationEntry.php", + "sha256": "8eac6134b50fb1c69f2a3c585d5669f005876b19fef1a69bd343b096f46d321f" + }, + { + "file": "resources/js/FiscalCoverage.jsx", + "sha256": "31d2c451dc94a617796a692934833e0f0a25dba186fa5ac6bbb825f8c917f61b" + }, + { + "file": "resources/js/ServiceFiscalWorkspace.jsx", + "sha256": "55dd0f6e8eee3a767f6c1c6df6cb22994d0fcb30ccd6af681afc140a84b67955" + }, + { + "file": "resources/js/App.jsx", + "sha256": "9504145b4881066ac4e3e4054222c27b6f004466a4eed23052a991ba4674849c" + } + ], + "journals_preserved": 27, + "new_rights": 0, + "new_schema": 0, + "new_demo": false, + "cache_cleared": false, + "visual_review": false, + "fiscal_engines_completed": false, + "legal_activation": false, + "deployment_validated": false, + "acceptance": false +} diff --git a/docs/proofs/sgf-fiscal-obligations-20261110.json b/docs/proofs/sgf-fiscal-obligations-20261110.json new file mode 100644 index 0000000..a233ccb --- /dev/null +++ b/docs/proofs/sgf-fiscal-obligations-20261110.json @@ -0,0 +1,58 @@ +{ + "version": "SGF-FISCAL-OBLIGATIONS-QA-v1", + "passed": true, + "target": "existing8085", + "functional_contract_checks": 118, + "preservation_acl_checks": 61, + "new_right_names": 3, + "qa_grants_per_book": 7, + "total_qa_grants": 14, + "administrative_commands_8085": 4, + "new_financial_effects": 0, + "map_recreated_8085": false, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false, + "evidence": { + "regression": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-223845-a69d8003/fiscal-obligations-result.json", + "boundaries": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-224117-d4f04edc/fiscal-obligations-boundaries.json", + "ui": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/fiscal-obligations-ui-20261110.json", + "http": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/fiscal-obligations-http-20261110.json", + "entry": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/fiscal-obligations-entry-http-20261110.json", + "concurrency": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/fo-concurrency-1794343303801/result.json", + "source_preparation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-223835-5a7feadd/fiscal-obligations-source.json", + "regression_setup": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-223658-e86fe229/fiscal-obligations-setup.json", + "selected_setup": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-224409-7a20bedf/fiscal-obligations-setup.json", + "installation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-224431-d7f8536d/fiscal-obligations-installation.json", + "preservation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-224624-16075a70/fiscal-obligations-preservation.json" + }, + "evidence_sha256": { + "regression": "ca964925cba373b62e77f776d21acecbdd7868f66e7b50e5770fa142cfece6a7", + "boundaries": "cbc0cb8ca3c6ac977c1bca23c430823bcdf4f84efcf273c2e6f41bb9fc071637", + "ui": "d46b52080dcf9bd6ad9cf18cbb0a5509c3177011bed690c97e0a0551bb4ec94d", + "http": "6d10dd721f27fa31a685ae34b200fd13ef6804c457963f531e3e93927735fc44", + "entry": "2c6ce36ef463958dd6deb9b433acf0fca7a422406af38d1405c0fe70cd67e837", + "concurrency": "8350033bf93aa03c8db9611dd5d6352949ba0640b0c4322e973aba209a971eb1", + "source_preparation": "2b604fbf1993ca3a5fc2efa27076a0804b198dc1c95a85833931f42f5e7bc252", + "regression_setup": "b50a48a76ea010ca9a5fa3b6a59d66eebc735b28d9282cdefb4d41ea596b19e2", + "selected_setup": "a0551eeea96192d7767928148b1a4e46dbc8a6905c9ed490cbf33eeb51b3fbfc", + "installation": "d7e6ac094bdd408dc055e67d636be9f4a59a4fdc72a9d1f55ab31f3265d82940", + "preservation": "2ca506fd1f47fcd501ce3a7de5e45c8daf72c25da5b6dc25ff8b7b854cd6cfe8" + }, + "sources": [ + "app/Modules/Services/FiscalObligations.php", + "app/Modules/Services/Http/FiscalObligationsController.php", + "resources/js/FiscalObligationsWorkspace.jsx", + "resources/js/fiscal-obligations-view.js", + "database/sql/fiscal_obligations.sql", + "config/fiscal_obligations.php" + ], + "source_sha256": { + "app/Modules/Services/FiscalObligations.php": "bcf24717cf5828fed440b8e86f29a5ed4d325d392000240b224d155954170d1d", + "app/Modules/Services/Http/FiscalObligationsController.php": "95b03b505e588333206766b862d0f0cff006b1af1a3dc0466370ac6a7518e097", + "resources/js/FiscalObligationsWorkspace.jsx": "cf50c8eebbd9aafc985229740cc724fc8c66d176454875f0e4f346797d531fc9", + "resources/js/fiscal-obligations-view.js": "d3c1aafddac987de0a8e432746533da8832c140474f9d9d49055020f7c9e67dd", + "database/sql/fiscal_obligations.sql": "c68988342ca1ed5817485355844c2f502f5c2ca12954461c1555d5c5f2abf7fe", + "config/fiscal_obligations.php": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } +} diff --git a/docs/proofs/sgf-fiscal-rules-20261110.json b/docs/proofs/sgf-fiscal-rules-20261110.json new file mode 100644 index 0000000..af9257d --- /dev/null +++ b/docs/proofs/sgf-fiscal-rules-20261110.json @@ -0,0 +1,137 @@ +{ + "passed": true, + "installation": "cc50a276c10c", + "port": 8085, + "date": "2026-11-10", + "scope": "SGF-FISCAL-RULES-COMMON-QA-v1", + "checks": { + "regression": 39, + "parity": 8, + "boundaries": 14, + "http": 24, + "dom_http": 14, + "input": 3 + }, + "concurrency": { + "during_lock": 503, + "after_release": 409, + "duplicate_reviews": 0 + }, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-190138-e1c57bb4\\fiscal-rules-result.json", + "sha256": "4963a0f49425abbc56e25acc4e7cdd669e4315b30b6a8c76cbc37dc08b533a7a", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-190224-293fa949\\fiscal-expression-parity.json", + "sha256": "1f460cf37e91c2d8cb8a9f682c703cfa323510e6a205733ff18e28f7a4300b00", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-190357-58ef4f30\\fiscal-rules-boundaries.json", + "sha256": "e35560a6e6cdd93543dd860dc99f58384f730495868792f4a993f989322095c2", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\fiscal-rules-concurrency\\result.json", + "sha256": "399742d55c4690e8548efdd54c68007f71f24a03e99b9c0b951abb00844f5640", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\fiscal-rules-http-20261110.json", + "sha256": "20566cf071a545feaf1d18e1a3410855b8c104c1b13372efb47afdb69995b338", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\fiscal-rules-dom-http-20261110.json", + "sha256": "c22899a91898c2d5f6c5cb3c3d36ee336b8a7e68723293c88a72b612c3112fb0", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-191041-6e45b82d\\fiscal-rules-input.json", + "sha256": "5071ec0d3dc2a632f7acb567867631c2aacf5e303a0a430bf74c4fd1f287ea8f", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-191018-6b74a111\\fiscal-rules-preservation.json", + "sha256": "f970a71f22d130c61d3c81863c03b746401991d2aab8ed0a3009a96501a2b602", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-190133-7a1e5e14\\fiscal-rules-setup.json", + "sha256": "5f758ddf82a21117f531bbf7bdba686e817eb1d588723c9bc03d64395445ecc2", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-190702-f19a4794\\fiscal-rules-setup.json", + "sha256": "fda2b837bdfc8d8026632afaed1476542747e6c07f53f8ce5e8505e6eae9762c", + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Services/FiscalRules.php", + "sha256": "e7ead66818df2ba644b3b22d35707637ae05dcfcfbe28e57c0a3ed61b05180db" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "sha256": "ad648825ec55b3bdb8eca038a01c5bd685b8b62b23f79bfd7771282506e1f66a" + }, + { + "file": "database/sql/fiscal_rules.sql", + "sha256": "facb9ce8f7304d0c3df83cddaa548158cf72ff12c4d21c3ab0ccc002ae6fd8b4" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "sha256": "a14a6d71d206b64445990a197e30387acb5882f1331ac13c2f283d7de0cb2477" + }, + { + "file": "resources/views/fiscal/rules.blade.php", + "sha256": "ffdf1049c52fd7ea5a3000dd42e8c567354d430e108dc95023eab1fd5ecccf84" + }, + { + "file": "resources/data/fiscal-rule-qa.json", + "sha256": "a9273524d65cd002b0a109959e3f77e4cf41125f8d2e71769ceb5dbecdb20dec" + }, + { + "file": "app/Shared/Context/ModuleNavigation.php", + "sha256": "11b7b2cfc3eadbb7c077bc505e0c017987838d7a8fb3e2385c227a5d0a4b337d" + }, + { + "file": "app/Modules/Identity/Services/IdentityWorkflow.php", + "sha256": "b567ae24e4209b42350010527e4bf2fb5b80732179116d959ac75800c6e55284" + }, + { + "file": "resources/js/App.jsx", + "sha256": "c75f8421f81eba4674092a0c79d30b0bfd47c07c3fd0d8ec231123a0a0f141eb" + }, + { + "file": "resources/js/WorkspaceNavigation.jsx", + "sha256": "5c7dbf048d48b0e73c6f8f226d03ec35f5f3bcaa506f07fa38ed587cdf94f3e2" + }, + { + "file": "routes/web.php", + "sha256": "77047a48bd8284ff516cd0a9e05d0636daae59c586e89c2a3f104493e31fd625" + } + ], + "rule_events_retained": 4, + "calculations_retained": 2, + "prior_iva_versions_preserved": 4, + "journals_preserved": 27, + "new_journals": 0, + "grants_per_existing_qa_book": 7, + "new_demo": false, + "sgc_dependency": false, + "portal_runtime_dependency": false, + "automatic_knowledge_import": false, + "legal_activation": false, + "cache_cleared": false, + "visual_review": false, + "deployment_validated": false, + "acceptance": false +} diff --git a/docs/proofs/sgf-human-resources-20261111.json b/docs/proofs/sgf-human-resources-20261111.json new file mode 100644 index 0000000..6540475 --- /dev/null +++ b/docs/proofs/sgf-human-resources-20261111.json @@ -0,0 +1,78 @@ +{ + "passed": true, + "date": "2026-11-11", + "installation": "cc50a276c10c", + "port": 8085, + "module": "human_resources", + "autonomous": true, + "new_profiles_both_existing_books": 6, + "new_grants_both_existing_books": 22, + "individual_commands_8085": 6, + "directed_checks": 132, + "preservation_checks": 71, + "concurrency_observed": true, + "new_journals": 0, + "new_payments": 0, + "new_debt": false, + "browser_visual_review": false, + "deployment_validated": false, + "cache_cleared": false, + "new_demo": false, + "evidence": [ + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-081138-a4e9dcaa/remunerations-result.json", + "sha256": "9ac93a64316a4c7d8168e2977ddb2f4364ff9c49f6c80b9b6f070600e6cd2e4f", + "checks": 40 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-081400-da22b428/hr-boundaries.json", + "sha256": "38aae9abbe5fd0f82bea2e7f397ffc4ffc13eac0c3accf6ec877ec26e0c84a38", + "checks": 23 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/hr-concurrency-1794377543991/result.json", + "sha256": "c7993282e31433f9336433fac3fdda4d3e08fe25b30c92764577e8a6756b0a64", + "checks": 0 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/hr-prepared-ui-20261111.json", + "sha256": "48480a08f3e06d1851897c206b9eb64209c7f826ad63d5c2647b23d178fe7dff", + "checks": 16 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/hr-http-20261111.json", + "sha256": "c109f83646ae5d17d4e6da687cadfd0cb58ec29ec816000af846f1fc0b09bc8b", + "checks": 43 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-081436-73034977/hr-installation.json", + "sha256": "3d7bd8501dd412250f726041bf26ebb76bcc33d049e082444e909bf4a28886f2", + "checks": 0 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-081711-a4de93ee/hr-preservation.json", + "sha256": "a93ec450e1edd72efba43f26b35b59d5aee9828c4aabd1ee43dfa80440a0a8ad", + "checks": 71 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-081103-75214c68/hr-profiles-setup.json", + "sha256": "f2e6c90d80c710bdd0b9422894111676391dc0c8db9e217132095022754b410f", + "checks": 0 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-081104-76740a68/hr-profiles-setup.json", + "sha256": "a1ed94fe30594cb925685ee10e260c13e0e7f4e2c00343d7fac1bf3b6fa88a4a", + "checks": 0 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-081937-32c95f30/hr-finalization.json", + "sha256": "4f50520b195ad36fa6ff3423c3ae0453eed41c3f099cce768152f47e1f6af178", + "checks": 0 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/hr-final-entry-20261111.json", + "sha256": "c86d294368c9032a37c9aad269c884d3d153fe15a0124e129375781be19290fb", + "checks": 10 + } + ] +} diff --git a/docs/proofs/sgf-human-resources-personnel-preparation-20261111.json b/docs/proofs/sgf-human-resources-personnel-preparation-20261111.json new file mode 100644 index 0000000..fbf0688 --- /dev/null +++ b/docs/proofs/sgf-human-resources-personnel-preparation-20261111.json @@ -0,0 +1,28 @@ +{ + "prepared": true, + "functional_delivery_complete": false, + "approved_new_access": false, + "new_grants": 0, + "new_profiles": 0, + "installed_8085": false, + "business_writes": 0, + "cache_cleared": false, + "visual_review": false, + "evidence": [ + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-083950-2b7f6505/hr-personnel-access-readonly.json", + "sha256": "b4bc3d273a5dcf9e13bad81105f0e3334aebc3f9f8a3c19cd790079d566d452d", + "checks": 0 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-083951-b86bcdeb/hr-personnel-access-readonly.json", + "sha256": "b02dc9e3f2c35ec83b9c61d71a58f3aee49da5834b44a6b3cecec8a21fccda77", + "checks": 0 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-083927-46fc6776/hr-personnel-time-core.json", + "sha256": "d11bd4745a72ecf293cb455143b491d201e0e47ce107da8c0bc31f00a8e5c850", + "checks": 22 + } + ] +} diff --git a/docs/proofs/sgf-human-resources-preparation-20261111.json b/docs/proofs/sgf-human-resources-preparation-20261111.json new file mode 100644 index 0000000..98a9843 --- /dev/null +++ b/docs/proofs/sgf-human-resources-preparation-20261111.json @@ -0,0 +1,53 @@ +{ + "preparation_checks_passed": true, + "functional_delivery_complete": false, + "installed_8085": false, + "new_hr_access_authorized": false, + "new_hr_grants": 0, + "new_hr_profiles": 0, + "cache_cleared": false, + "visual_review": false, + "deployment_validated": false, + "evidence": [ + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/hr-prepared-ui-20261111.json", + "sha256": "48480a08f3e06d1851897c206b9eb64209c7f826ad63d5c2647b23d178fe7dff", + "checks": 16 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-075918-07b14258/hr-preauthorization.json", + "sha256": "b3fcac09d3289815eb865b00e42d1b187a948d020c18d14b683abeca65b16660", + "checks": 11 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-080054-f17dbd76/hr-preauthorization.json", + "sha256": "b3fcac09d3289815eb865b00e42d1b187a948d020c18d14b683abeca65b16660", + "checks": 11 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-080055-00cf324f/hr-preparation-preservation.json", + "sha256": "7d1720fff55621043ff926aef42859eb2ea180faa40ed705b0a1af75bc807343", + "checks": 65 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-075531-6cd6db05/remunerations-finance-retracted.json", + "sha256": "6cead3dc86f89f972d4930598db2353e8d2951e385d479088864573cb4193e8b", + "checks": 0 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-075530-6881ed14/remunerations-finance-retracted.json", + "sha256": "16ba21a5255d368f9e86e9e9e5328017817a5f83028fb944d850a90296960d41", + "checks": 0 + } + ], + "earlier_pre_separation_proof": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-075420-9718f6f1/remunerations-result.json", + "checks": 40, + "only_pre_separation_core_evidence": true, + "not_hr_integration_acceptance": true + }, + "build_manifest": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/single-hr-prepared-final-build-20261111/manifest.json", + "sha256": "096391f91f918deaeb7ed92dc3ba1745f75d2e9b884aed97b53629f8a69340fb" + } +} diff --git a/docs/proofs/sgf-iva-assessment-20261110.json b/docs/proofs/sgf-iva-assessment-20261110.json new file mode 100644 index 0000000..64c058b --- /dev/null +++ b/docs/proofs/sgf-iva-assessment-20261110.json @@ -0,0 +1,116 @@ +{ + "passed": true, + "installation": "cc50a276c10c", + "port": 8085, + "scope": "SGF-IVA-ASSESSMENT-FORM-QA-v1", + "checks": { + "regression": 38, + "boundaries": 10, + "http": 23, + "dom_http": 14 + }, + "concurrency": { + "during_lock": 503, + "after_release": 409, + "duplicate_reviews": 0 + }, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-183923-8c78d580\\iva-result.json", + "sha256": "7dc37c693d60f120a985042cea8e68d7ee2d48b4363abc71cb95a482c639a6d1", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\iva-concurrency-validated\\result.json", + "sha256": "5ea7d314ad730ae6bf6f292f5ce39fe0b13c55aca9848c7861231db36b4b0029", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-184043-8652e74e\\iva-boundaries.json", + "sha256": "236450fbd1c68c099ad41311257d675a3abc8218fd27faeb085b4ae7876a2760", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\iva-http-20261110.json", + "sha256": "a11bcdee2a2d9c730e5b1e010dc254bb69360af55372bcb9ea664de21f3480b3", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\iva-dom-http-20261110.json", + "sha256": "7cb4d77fea98a99b6c6840a5821f52620548dac422abd1d891b78e3696719d47", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-184725-50388df1\\iva-preservation.json", + "sha256": "05eb1dfd476ce0071372f8b591c4f95f48967bc449b9333a909e0e4f9bd5133c", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-183618-ef69629b\\iva-setup.json", + "sha256": "acad5d93fb12b8f5dc1c8dd1e2a275228b23fba004e9d086f58b595f344283a2", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-184414-05608767\\iva-setup.json", + "sha256": "a0f35dbfaf569b13bf7e4037e3af6ff5502e70ed08d6fd5a09832ce8e11d0e2b", + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Services/FiscalAssessment.php", + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "database/sql/fiscal_assessment.sql", + "sha256": "a64a912822c54da9fa6efbcd24f430d8466f19dca82ef747fbd3847b8a68925e" + }, + { + "file": "public/sgf-iva.mjs", + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "resources/views/fiscal/workspace.blade.php", + "sha256": "bb86dd60e1d9b3aeab97d991fe54005bd0412beccdfa6119552493eb8b16df50" + }, + { + "file": "app/Shared/Context/ModuleNavigation.php", + "sha256": "7791ae9114d2cfd9b4c6603188b561fbadfe8d754f2f362d7cb6ae2408306617" + }, + { + "file": "app/Modules/Identity/Services/IdentityWorkflow.php", + "sha256": "72a84a3924d1a78e0bbbaaf3f7f0f7f0e1e640227a1c1842eac74e3ec4c06a74" + }, + { + "file": "resources/js/App.jsx", + "sha256": "bf8dd43bbad1f96c0043b34307f2fb6959e16bf8ec6ebcccb9858462102736c0" + }, + { + "file": "resources/js/WorkspaceNavigation.jsx", + "sha256": "6c84b50f37561fd79ce3404ba7d4e25f88d233cd84645151a6fbe23ec67f9be3" + }, + { + "file": "routes/web.php", + "sha256": "bed2954af1ea0bc1537e892f04e455b3baa5f87c43670bb46688a85e8813b9c1" + } + ], + "retained_fiscal_versions": 4, + "journals_preserved": 27, + "new_journals": 0, + "deductible_vat": null, + "tax_payable": null, + "company_assessment_complete": false, + "grants_per_existing_qa_book": 14, + "new_demo": false, + "sgc_dependency": false, + "cache_cleared": false, + "official_form": false, + "official_submission": false, + "visual_review": false, + "deployment_validated": false, + "acceptance": false +} diff --git a/docs/proofs/sgf-multi-line-included-20261110.json b/docs/proofs/sgf-multi-line-included-20261110.json new file mode 100644 index 0000000..2fef717 --- /dev/null +++ b/docs/proofs/sgf-multi-line-included-20261110.json @@ -0,0 +1,97 @@ +{ + "passed": true, + "scope": "SGF-MULTI-LINE-INCLUDED-QA-v1", + "date": "2026-11-10", + "installation": "cc50a276c10c", + "port": 8085, + "checks": { + "regression": 34, + "recovery": 3, + "http": 29, + "dom_http": 16 + }, + "concurrency": { + "passed": true, + "during_lock": { + "status": 503, + "retry_after": "5", + "elapsed_seconds": 5.17136287689209 + }, + "after_release": 200, + "replayed": true, + "one_calculation": true, + "regression_only": true + }, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-195444-c0e2a14a\\document-math-result.json", + "sha256": "3b7749421c9fa836ab595cabc9dcd6e929a1ca07a4b3c9e454c977c7797b7d9f", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\document-math-concurrency\\result.json", + "sha256": "73331577472b9a937f93de098b30ef42f65e9257a0d6f2a7068fef2605a4e4c2", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-195644-d598bace\\document-math-recovery.json", + "sha256": "b86594a87d5e105b69de5efd0285dfbd30bc96331e05fe2fcad23bdfbff3a408", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\document-math-http-20261110.json", + "sha256": "6370193e2a9725fe7f7b1f9fbd56214b0d8d8641e1e54cb9714196ffe29af663", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\document-math-dom-http-20261110.json", + "sha256": "fab41b6d48b675bcc70d72e2d6f88e70dab20df7b3ed70dede166aece0b07ba6", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-200237-fe3b87c6\\document-math-preservation.json", + "sha256": "4e8ecf54b315285a14196e00e7a5c3507ab171909ef51a06e35337dbd1b1d481", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-200126-3e24e881\\document-math-installation.json", + "sha256": "676a20b372db477487104b230f367a9a04caa704ad9182f8f13156ad6034f8ba", + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "resources/views/fiscal/rules.blade.php", + "sha256": "39ca795a4469399896644add622a62a54f216ca67b822337c3cd7ec61c1e9698" + }, + { + "file": "routes/web.php", + "sha256": "ce7c248b0f110f73e2f9c1bea0b6d5f2a9fa9f24ab8cc3f99707b21692ac14af" + }, + { + "file": "config/fiscal_rules_qa.php", + "sha256": "bf0ed125e277994adb79c14c516ff779a0593eb501b7d02cd81dccae71398bca" + } + ], + "new_grants": 0, + "new_financial_effects": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false +} diff --git a/docs/proofs/sgf-next-block-progress-20261110.json b/docs/proofs/sgf-next-block-progress-20261110.json new file mode 100644 index 0000000..93b658d --- /dev/null +++ b/docs/proofs/sgf-next-block-progress-20261110.json @@ -0,0 +1,126 @@ +{ + "entry": "http://127.0.0.1:8085/sgf", + "installation": "cc50a276c10c", + "proofs": [ + { + "sha256": "1dbffaa4fd6f8ce6bf3e1c372e45084fba34c4292451834e4b4dac3f57deeaee", + "kind": "quotes_http", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/commercial-quotes-http-20261110.json" + }, + { + "sha256": "81d8cc826e5f535db204f624a9c8fd5eb4b1d8076bc283a23955df55ad97cb8d", + "kind": "schedule_http", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/workshop-schedule-http-20261110.json" + }, + { + "sha256": "040f9fed00b4b9ceba179504b14d0b87a1393716fc09503b6afd71ec636a27da", + "kind": "equipment_http", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/workshop-equipment-http-20261110.json" + }, + { + "sha256": "4df0501a71fc4c415dcb861153610fac9edf639e1b042852181e9c1519ea216b", + "kind": "stock_http", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/stock-completion-http-20261110.json" + }, + { + "sha256": "1c332b62e6bbd44b29abc989b5cedbfb1d7d09d04bf234462aa70ff1e2ff30d8", + "kind": "stock_dom_http", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/stock-completion-dom-http-20261110.json" + }, + { + "sha256": "ce1feb474c79e4e1368d0273d9c3bb88736ed9bbdeff65c7927a8ec93dff92c5", + "kind": "stock_regression", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-162212-7fc0c484/stock-completion-result.json" + }, + { + "sha256": "d7dce26e185f40e74a33455db4448b10bec025ccc94fbc8355806fc044201181", + "kind": "stock_concurrency", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/stock-completion-concurrency/result.json" + }, + { + "sha256": "a9eb79fc029d3da5f824bd77b17b891239a7ef4cf1a6a260271c2e2bbf40d435", + "kind": "documents_candidate_regression", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-163526-13905e7f/service-documents-result.json" + }, + { + "sha256": "9d2e9902930a97eda22f87861751c80d4c75e9580964f6f0850bc6101adfb48d", + "kind": "preservation", + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-162658-c0b15a30/next-block-preservation.json" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-171528-10e1c5f0/service-documents-result.json", + "kind": "documents_regression", + "sha256": "2c09d27a9bc9b9c9c68b8b6b19a5e3553a615138ef6aeee2ade7ab253bfbabac" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/documents-concurrency/result.json", + "kind": "documents_concurrency", + "sha256": "383f2aaa78c7db59563f25701e03ea99b0359d0c35c9c290b51369864c4275b6" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-172728-ac2008fe/document-cancellation-result.json", + "kind": "cancellation_regression", + "sha256": "6a5645e4c310aa1be1fefea9240a16da0e3bac2d99e67ca3d0e0c1ebc56d8ee3" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/service-documents-http-20261110.json", + "kind": "documents_http", + "sha256": "a93046d86b2bc1ad87b407c0cc15750accede96ba7c68a0e46cff86c34f615f6" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/service-documents-dom-http-20261110.json", + "kind": "documents_dom_http", + "sha256": "7abccf812e1c75ca8b96dc0a19a345023f581de30406221207b92413c567b443" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-172510-6e7769dd/period-documents-result.json", + "kind": "period_documents", + "sha256": "59826b9f3bb406cc6a5af37eeab2b5ddcd9ed7e4ecaad7f6436034c670e70471" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-172904-86c06a96/service-documents-status.json", + "kind": "final_documents_status", + "sha256": "68bd3f699e813b31800d82a4bbae894d0631493a60826bfffc51054513a07ec4" + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-172905-2acea48f/next-block-preservation.json", + "kind": "final_preservation", + "sha256": "f98371e600bbbb9c79d696b72fe1b51755362727a0338f92370e892eb7dddebf" + } + ], + "published": false, + "sgc_required": false, + "previous_journals_compared_unchanged": 18, + "cache_cleared": false, + "new_demo": false, + "deployment_capacity_validated": false, + "browser_visual_review": false, + "date": "2026-11-10", + "documents_candidate_not_installed": false, + "authorization_pending": null, + "completed_increments": [ + "workshop_schedule", + "workshop_equipment", + "commercial_workshop_quotes", + "stock_policies_supplier_return", + "receipt_credit_invoice_receipt", + "technical_cancellations_and_linked_reversals" + ], + "journal_count_at_stock_completion": 20, + "application_complete": false, + "current_journal_count": 25, + "current_selected_balances": { + "invoice_receipt": "0.00", + "credit_invoice": "500.00" + }, + "approved_next_block_functionally_delivered": true, + "contract_sha256": "4983cacdb45d001090b569e1780384a9f31fba1528b90f7e2355bb3e57ff34f2", + "source_hashes": { + "app/Modules/Services/ServiceReceivables.php": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453", + "app/Modules/Services/DocumentPeriod.php": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2", + "public/sgf-service-documents.mjs": "1d7345d1e504250fab9679170f56774a1decae0f0ba9ab9aac0e8efdec7df044", + "app/Modules/Services/ServiceDocuments.php": "ab5e2c5fbbb790fc9c2fb3e6e30cb25c017b95314e4aa59df6d9c361cc9acc96", + "app/Modules/Services/ServiceInvoicing.php": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5", + "app/Modules/Accounting/SalesLedgerSource.php": "cc5300bcce567d7d6aed9f967b48daf87bff3a106c34bdc0d03dcd10b8f89241" + } +} \ No newline at end of file diff --git a/docs/proofs/sgf-operational-information-20261110.json b/docs/proofs/sgf-operational-information-20261110.json new file mode 100644 index 0000000..5cfd204 --- /dev/null +++ b/docs/proofs/sgf-operational-information-20261110.json @@ -0,0 +1,122 @@ +{ + "passed": true, + "installation": "cc50a276c10c", + "date": "2026-11-10", + "port": 8085, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\overview-http-20261110.json", + "sha256": "b42c0d35339ed02ad3d6cdd8660a2a0bb89a280b6bda2ff10e4a5ed7175efc56", + "passed": true, + "checks": 50 + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\overview-ui-20261110.json", + "sha256": "f497bf08589115ef3bb056b1069fbdb50b7c63e9e5d24c790aacc4840bdb401b", + "passed": true, + "checks": 28 + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\overview-entry-http-20261110.json", + "sha256": "e25213942e5cce421c9ed31d2661c6357f1d76aa71e2b4b42004d308c3d6c77b", + "passed": true, + "checks": 11 + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\overview-final-http-20261110.json", + "sha256": "f357cd47e8683e5d968ef7113633e0b2fab798dc77e2f6e6ec009aa2bfc9d0c9", + "passed": true, + "checks": 3 + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-204551-708cb2cb\\workshop-orders-result.json", + "sha256": "9f20deadcd196ba437599850e92cfd980ea64444355443f75cc2ccd811b73cb0", + "passed": true, + "checks": 29 + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\workshop-orders-http-20261110.json", + "sha256": "0100d1cea07ff2c43356471fbab3d3f96e40bb0392b3fc80c7b78b43b9f3837a", + "passed": true, + "checks": 11 + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\workshop-orders-ui-20261110.json", + "sha256": "5cb13f70f6ea0caa96a367efff08a9832845d0909268fa3fafd53215e8ad96fb", + "passed": true, + "checks": 12 + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\workshop-read-lock-1794336449856\\result.json", + "sha256": "5554e73cbce64de9115a02313d98723b42cec931ffd8731cd155ade34c809adc", + "passed": true, + "checks": 0 + }, + { + "path": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-205202-210c8f94/overview-installation.json", + "sha256": "3ebb2dd6537c3393f7c43657d03e544881f8606ec928cd2bf98827e98544552a", + "passed": true, + "checks": 0 + } + ], + "sources": [ + { + "file": "resources/js/WorkspaceHome.jsx", + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ModuleNavigation.jsx", + "sha256": "e68a8353d2de8e137a102ccd3c768245ecb81fbee0ece0589eb84e40122ae7b3" + }, + { + "file": "resources/js/app.jsx", + "sha256": "2c4b7a4ffd8ab3117194126f48f0351c67723aeae5508273ac8070828c485f47" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "sha256": "8b652d8a30bcd9ea477a9af88583fccaa5897aff28fdc65014fa9393bfc16f8a" + }, + { + "file": "resources/js/WorkshopMenuWorkspace.jsx", + "sha256": "f636fdbf0425bc845506ff59b487e0212fc3e08eef2220daceed79143213279a" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "sha256": "b433c85d76609d2696c2818b34ba5c63059d673bad37c638e12d99f6ae0aac68" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "app/Shared/Installation/PresentationEntry.php", + "sha256": "7974815682b8fad6448619633d35bb29c82596cb19156d7914ade588ff678b9a" + }, + { + "file": "resources/views/sgf-presentation-entry.blade.php", + "sha256": "df557b3d42df882b7998a23ae8944a757dd7d70b718c6391777f2ba77571b395" + }, + { + "file": "resources/css/tempo-theme.css", + "sha256": "ac2ecd3a012cd2fa5b9fc527a3e264936871a55ef259388afe1f5af6f352786c" + }, + { + "file": "routes/web.php", + "sha256": "f99427a98c56875710d0aa667b658d4b33ec18a3b10cdf4d7eb6bf9336565d29" + }, + { + "file": "resources/js/context-transition.js", + "sha256": "9b21d005c7ea6529cbd84b289962e4ca6a7ed310fcdd0465e5c4ecb4479f51fa" + } + ], + "business_commands_8085": 0, + "new_grants": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false, + "full_product_complete": false +} diff --git a/docs/proofs/sgf-payable-aging-20261110.json b/docs/proofs/sgf-payable-aging-20261110.json new file mode 100644 index 0000000..4f6d93b --- /dev/null +++ b/docs/proofs/sgf-payable-aging-20261110.json @@ -0,0 +1,27 @@ +{ + "version": "SGF-PAYABLE-AGING-QA-v1", + "passed": true, + "target": "existing8085", + "regression_checks": 26, + "ui_checks": 12, + "http_checks": 14, + "original_tables_identical": 51, + "business_commands": 0, + "new_rights": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false, + "evidence": { + "regression": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-225625-14e36b63/payable-aging-result.json", + "ui": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/payable-aging-ui-20261110.json", + "http": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/payable-aging-http-20261110.json", + "installation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-225714-3c10e767/payable-aging-installation.json", + "preservation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-225759-f5487777/payable-aging-preservation.json" + }, + "sources": [ + "app/Modules/Purchasing/PayableAge.php", + "app/Modules/Purchasing/PayablePositionReadController.php", + "resources/js/PayablePositionsWorkspace.jsx", + "resources/js/payable-position-view.js" + ] +} diff --git a/docs/proofs/sgf-payable-due-20261110.json b/docs/proofs/sgf-payable-due-20261110.json new file mode 100644 index 0000000..93d7d27 --- /dev/null +++ b/docs/proofs/sgf-payable-due-20261110.json @@ -0,0 +1,49 @@ +{ + "version": "SGF-PAYABLE-DUE-QA-v1", + "passed": true, + "target": "existing8085", + "functional_checks": 113, + "preservation_acl_checks": 58, + "new_business_rights": 2, + "grants_per_book": 2, + "total_qa_grants": 4, + "confirmed_administrative_commands_8085": 2, + "new_financial_effects": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false, + "evidence": { + "regression": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-222519-ca328516/payable-due-result.json", + "identity": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-222138-ff65b1c8/payable-due-identity.json", + "pagination": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-222241-1ccd5eab/payable-due-pagination.json", + "page_integration": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-222407-f448c6e1/payable-due-list.json", + "concurrency": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/due-concurrency-1794341858739/result.json", + "http": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/payable-due-http-20261110.json", + "final_http": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/payable-due-final-http-20261110.json", + "ui": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/payable-due-ui-20261110.json", + "preservation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-222251-7c151972/payable-due-preservation.json", + "installation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-222425-0e9c9bf3/payable-due-installation.json", + "regression_setup": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-221458-cb0acadb/payable-due-setup.json", + "selected_setup": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-221825-b5939799/payable-due-setup.json" + }, + "sources": [ + "app/Modules/Purchasing/PayableDueController.php", + "app/Modules/Purchasing/PayablePositionReadController.php", + "resources/js/PayableDueWorkspace.jsx", + "resources/js/PayablePositionsWorkspace.jsx", + "resources/js/payable-position-view.js", + "database/sql/payable_due.sql", + "config/payable_due.php" + ], + "harnesses": [ + "scripts/sgf-payable-due-proof.php", + "scripts/sgf-payable-due-identity-proof.php", + "scripts/sgf-payable-due-pagination-proof.php", + "scripts/sgf-payable-due-list-proof.php", + "scripts/sgf-payable-due-http.mjs", + "scripts/sgf-payable-due-final-http.mjs", + "scripts/sgf-payable-due-ui.mjs", + "scripts/sgf-payable-due-concurrency.mjs", + "scripts/sgf-payable-due-preservation.php" + ] +} diff --git a/docs/proofs/sgf-payable-positions-20261110.json b/docs/proofs/sgf-payable-positions-20261110.json new file mode 100644 index 0000000..6191506 --- /dev/null +++ b/docs/proofs/sgf-payable-positions-20261110.json @@ -0,0 +1,29 @@ +{ + "version": "SGF-PAYABLE-POSITIONS-QA-v1", + "passed": true, + "target": "existing8085", + "regression_checks": 23, + "ui_checks": 15, + "http_checks": 19, + "business_commands_8085": 0, + "new_rights": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false, + "evidence": { + "regression": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-220530-07616d21\\payable-positions-result.json", + "ui": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\payable-positions-ui-20261110.json", + "http": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\payable-positions-http-20261110.json", + "installation": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-220903-1bf94d1f\\payable-positions-installation.json", + "preservation": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-220955-ad348ffc\\payable-positions-preservation.json" + }, + "sources": [ + "app/Modules/Purchasing/PayablePositionReadController.php", + "app/Modules/Purchasing/ComposedFinanceController.php", + "resources/js/PayablePositionsWorkspace.jsx", + "resources/js/payable-position-view.js", + "scripts/sgf-payable-positions-proof.php", + "scripts/sgf-payable-positions-ui.mjs", + "scripts/sgf-payable-positions-http.mjs" + ] +} diff --git a/docs/proofs/sgf-receivable-positions-20261110.json b/docs/proofs/sgf-receivable-positions-20261110.json new file mode 100644 index 0000000..20c8bd2 --- /dev/null +++ b/docs/proofs/sgf-receivable-positions-20261110.json @@ -0,0 +1,75 @@ +{ + "passed": true, + "installation": "cc50a276c10c", + "port": 8085, + "date": "2026-11-10", + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-211215-0a0ad6c1\\receivable-positions-result.json", + "sha256": "7f9e305a16e06578bc0541a6f217ba64c97655070a4cece918820344da215eb9", + "checks": 30, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\receivable-positions-ui-20261110.json", + "sha256": "607eae89da99d8c8367ce1a38c86f329e6a0f041bafd336aa1d0faa966d79062", + "checks": 16, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\receivable-positions-http-20261110.json", + "sha256": "90dbbcdba1e510be8cb3895aac63b6ba8e1d8abed4b9c979c16d0e8b26dde8e6", + "checks": 15, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-211623-52cb8499\\receivable-positions-installation.json", + "sha256": "53d03cd993ed302cea4817c16bfe0182c330fe85ce2aafeea974fc32a9e4fcd3", + "checks": 0, + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "resources/js/WorkspaceNavigation.jsx", + "sha256": "e8b6177a1fa7dfa906f78b9c06d32f0493ff46c4c4b569623ce06f4f0f42d4df" + }, + { + "file": "resources/js/app.jsx", + "sha256": "59bd224c883dedce891264d29919422fb347e0b7039efcd9176787137f6bec97" + }, + { + "file": "routes/web.php", + "sha256": "65ad031fc3d2ecbb6722e446aa62af4c33fb50558e8aa681491af44796c4e118" + } + ], + "business_commands_8085": 0, + "new_grants": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false +} diff --git a/docs/proofs/sgf-single-integration-20261110.json b/docs/proofs/sgf-single-integration-20261110.json new file mode 100644 index 0000000..7f88e21 --- /dev/null +++ b/docs/proofs/sgf-single-integration-20261110.json @@ -0,0 +1,109 @@ +{ + "date": "2026-11-10", + "passed": true, + "meaning": "directed synthetic integration evidence; not complete product acceptance", + "installation": "cc50a276c10c", + "port": 8085, + "entry": "http://127.0.0.1:8085/sgf", + "checks": 92, + "proofs": { + "profiles": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-133942-55f43aff/profiles-check.json", + "sha256": "ceb16e29f6498a57aff4b2c50584823e771ab960302a31de07ede0a33ab5d726", + "checks": 23 + }, + "reads": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-read-http-20261110.json", + "sha256": "b7014d494c54db9b309a8aa711f18fac3f766bc14f20accab0d3035cc7f12106", + "checks": 15 + }, + "http_boundaries": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-boundaries-http-20261110.json", + "sha256": "cd9684589c6fbbc15fa862aae23ef25ef282024d34be8cebe60edde9954bb13d", + "checks": 10 + }, + "client_recovery": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-finance-ledger-client-20261110.json", + "sha256": "8dd66173b519c4b52079c95cc8282b7756e4d523d79d1766a4646c9ceee774c8", + "checks": 12 + }, + "rate_controller": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-132247-169255fa/stock-rate-controller-result.json", + "sha256": "84d4737bbe25060adfff2ef25a5420948b163977334a1c8aa8b109717191929d", + "checks": 9 + }, + "direct_correction": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-132829-d446e90c/direct-ledger-result.json", + "sha256": "b1d25f4a6b4629be03e11977081792aa4ce10ae535b4eaa96e1fb31888589b93", + "checks": 7 + }, + "industrial_correction": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-132833-ef2bb812/industrial-ledger-result.json", + "sha256": "c88e44f9fab298e7fa65439431f1c62225276a62ebfd6df03c5e0952be0bb5a2", + "checks": 6 + }, + "material_isolation": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-133334-5449e218/material-isolation-result.json", + "sha256": "344b53d0cd7c6400a33fc0f1329388b35df275570af3358653df1359f5234b2b", + "checks": 5 + }, + "finance_lock": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-134018-356f8135/finance-lock-result.json", + "sha256": "32948d7153bb70b67cd38ff86a64ed327e6ab0fa3e551462a6de61a40669fc01", + "checks": 5 + } + }, + "effects": { + "financial": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-financial-http-progress.json", + "sha256": "db5a84d812d96a321693b6654488a81cb34b2d359a7354248195f3727ff8f857", + "completed_operations": 33 + }, + "corrections": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-corrections-http-progress.json", + "sha256": "0e1718eed4eef96e1076ceb4d740908bf977f4af8ee2bbd03c5e526b499b22ac", + "completed_operations": 14 + } + }, + "sources": { + "app/Modules/Accounting/ComposedLedger.php": "74d92e0c9dcbb940f66afc5a0f5fa29025cfa7b6691043b87fba82b428cf9901", + "app/Modules/Accounting/ComposedLedgerController.php": "4e259e52d86e05e028c8042082cf7aa05aafdf2c0a722fa019fa9aa19b515f78", + "app/Modules/Accounting/ComposedMaterialIsolation.php": "539000b2bee9ee84e12197175fcdf334ce65dd4ca6048aab128fa3d56a6b5372", + "app/Modules/Accounting/SourceBook.php": "273b6d0d52e350f045f2d2b9515da49b0522a96596699294dbab09fc75d3f78a", + "app/Modules/Purchasing/ComposedFinance.php": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16", + "app/Modules/Purchasing/ComposedFinanceController.php": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5", + "app/Modules/Purchasing/ReceiptRuntime.php": "d368d03239b457eacb61d67c28274d8ed30cf382e7a5586f8a18a7b0ab79ce18", + "app/Shared/Installation/PresentationEntry.php": "4b72f35a25ce76a7a54887ecb7f491985bc4c7b356bde34e5d377f9a5133c101", + "packages/general-ledger/src/PayableJournal.php": "184115b4e937ca163b8d8a02264104aa794bd3f6380934e5757438346d0e989e", + "packages/general-ledger/src/LedgerCycle.php": "904111b4e093e0613eb779d46fa394d6f1654906ffef7ca5ef2311ec6d5ce33a", + "packages/general-ledger/src/StockValuation.php": "f4be85e183eb33860a5a7b99f012e6e4490dc04f80deb22d452fb5048ae913fb", + "public/sgf-finance-payables.mjs": "68b772884512e353e1186fb82b9f732d7f785de3a01458198e49fe359ec5f666", + "public/sgf-composed-ledger.mjs": "13f5c2d514c09573f0844da71e766aca9b03a489b40c554235c54da17b4fb1c5" + }, + "journals": 13, + "supplier_principal_minor": 10000, + "supplier_balance_minor": 10000, + "receivable_balance_minor": 40000, + "sgc_required": false, + "new_demo": false, + "new_database": false, + "new_database_roles": false, + "cache_cleared": false, + "protected_records_changed": false, + "published": false, + "client_presentable": false, + "visual_review": false, + "deployment_capacity_validated": false, + "remaining": [ + "workshop-stock physical integration and unused return", + "selected stock receipt2/issue1/return0.5 valuation100/50/25 in8085", + "remaining commercial workshop origin and legacy accounting continuations", + "accounting origin pagination beyond25", + "complete QA period conference and close", + "visual review and full startup/recovery acceptance" + ], + "historical_proofs_reused": [ + "docs/proofs/purchasing-completion-final-20261104-r2.json", + "docs/proofs/sgf-six-module-composition-20261110.json" + ] +} diff --git a/docs/proofs/sgf-single-integration-pagination-20261110.json b/docs/proofs/sgf-single-integration-pagination-20261110.json new file mode 100644 index 0000000..8c229f9 --- /dev/null +++ b/docs/proofs/sgf-single-integration-pagination-20261110.json @@ -0,0 +1,28 @@ +{ + "passed": true, + "installation": "cc50a276c10c", + "metadata_fixture_rows": 127, + "foreign_fixture_rows": 1, + "fixture_changes_rolled_back": true, + "sql_pages": 8, + "queries_per_cost_family_page": 2, + "assertions": 275, + "http_reads": 2, + "proofs": [ + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-134326-c6a75dd3/candidate-pages-result.json", + "sha256": "692484881927980c0c95f49257cdbcbebbe7fd351484a1557227c988b2232d7f" + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-candidate-pages-http-20261110.json", + "sha256": "c7595182b8f7fecf5d64a5eaee2e8ccb047bb7ce4559b630329aeb4163565183" + } + ], + "sources": { + "app/Modules/Accounting/ComposedLedgerCandidates.php": "46d4d1922344353927a311ce9c523cd82d319ad74d55b18d984c11e65a0d7378", + "app/Modules/Accounting/ComposedLedgerController.php": "1ffa609367f45f85add52f8deb423bed18921a3e600fb7252dcf093dbaaa909e", + "resources/views/accounting/composed-ledger.blade.php": "a659fc6d59ab8a9ece06dff85fbe180292f348c03e8fb552f592b1493306d902" + }, + "visual_review": false, + "deployment_capacity_validated": false +} \ No newline at end of file diff --git a/docs/proofs/sgf-six-module-composition-20261110.json b/docs/proofs/sgf-six-module-composition-20261110.json new file mode 100644 index 0000000..b2d6930 --- /dev/null +++ b/docs/proofs/sgf-six-module-composition-20261110.json @@ -0,0 +1,249 @@ +{ + "version": "SGF-SIX-MODULE-COMPOSITION-PREPARATION-v1", + "date": "2026-11-10", + "passed": true, + "complete_sgf": false, + "installation": "cc50a276c10c", + "port": 8085, + "new_databases": 0, + "new_demonstrations": 0, + "sgc_required": false, + "composition": { + "installed": true, + "new_tables": [ + "b_internal_confirmations", + "ca_commands", + "ca_decisions", + "ca_proposals", + "ca_resolutions", + "ca_submissions", + "dc_calculations", + "dc_commands", + "dc_decisions", + "dc_events", + "dc_outbox", + "dc_rates", + "doc_history", + "doc_records", + "document_preview_model_history", + "document_preview_models", + "ib_budgets", + "ib_commands", + "ib_comparisons", + "ib_decisions", + "ib_events", + "ib_outbox", + "ic_commands", + "ic_decisions", + "ic_events", + "ic_inputs", + "ic_lots", + "ic_outbox", + "ic_products", + "ic_results", + "service_calculation_profiles", + "service_calculation_results", + "sf_activation_events", + "sf_decisions", + "sf_proposals", + "sgf_composition_schema", + "si_commands", + "si_correction_requests", + "si_counters", + "si_decisions", + "si_events", + "si_invoices", + "si_outbox", + "si_proposals", + "sr_commands", + "sr_deliveries", + "sr_effects", + "sr_events", + "sr_outbox", + "sr_records", + "wb_claims", + "wb_commands", + "wb_decisions", + "wb_events", + "wb_generations", + "wb_outbox", + "wb_proposals", + "wc_applications", + "wc_commands", + "wc_events", + "wc_outbox", + "wc_requests", + "wc_reviews", + "wo_commands", + "wo_decisions", + "wo_entries", + "wo_equipment", + "wo_events", + "wo_orders", + "wo_outbox" + ], + "previous_rows_sequences_permissions_associations_preserved": true, + "schema_sha256": "b3e0cea308710c6839125eeb98651156b745bb075a03019b457b7dc44ffde079", + "backup_sha256": "f09ad87c77aec16da8b2aea0e8c42a36052d74c4b128840b0663494ce34580db", + "new_public_function_defaults_removed": 44, + "runtime_enabled": false + }, + "evidence": { + "schema_rehearsal": [ + "composed runtime accepts only the physically verified host and schema pin", + "existing domain guard accepts the verified composition", + "synthetic invoice guard operates on the composed host", + "workshop and direct costs guards operate on the same host", + "four SQL environment guards point to the exact composed QA identity", + "a stale schema pin blocks the runtime", + "same host contains a_clients", + "same host contains a_services", + "same host contains a_service_drafts", + "same host contains a_billing_drafts", + "same host contains b_internal_confirmations", + "same host contains doc_records", + "same host contains document_preview_models", + "same host contains service_calculation_profiles", + "same host contains ca_proposals", + "same host contains sf_proposals", + "same host contains si_invoices", + "same host contains sr_records", + "same host contains wo_orders", + "same host contains dc_calculations", + "same host contains ic_results", + "same host contains ib_budgets", + "same host contains wb_proposals", + "same host contains wc_requests", + "existing grants retained without assigning new access", + "existing clients retain identifiers versions payloads and associations", + "new module whitelist accepts scoped workshop right", + "module constraints and append-only guards installed", + "four existing compatible A tables reused without rebuilding", + "rehearsal rolls back instead of installing", + "all previous rows sequences constraints and triggers unchanged after rehearsal", + "installer rejects production environment" + ], + "composed_commercial_workshop_flow": [ + "Comercial preserves service-to-billing origin in the composed host", + "new client explicitly belongs to Comercial", + "cross-module client is refused before an explicit individual association", + "Oficinas opens once and recovers the same order without duplication", + "Oficinas retains the exact explicitly associated Comercial client", + "another authorized unit cannot see the new order", + "destructive command is refused by the command-start guard without executing it", + "new drafts and workshop order do not issue invoices or create financial effects automatically", + "all original table rows unchanged after rolling back the new composed flow" + ], + "stock": [ + "HTML workspace available in explicit Stock context", + "warehouse catalogue restricted to explicit map", + "literal wildcard search and page size 100", + "item catalogue paginated at 25", + "responsible catalogue restricted to source tenant", + "query cannot inject company authority", + "Purchasing context cannot execute Stock routes", + "current physical basis from the original book", + "foreign warehouse refused", + "foreign item refused", + "original writer validates reversible preview", + "preview leaves no movements operations or audit rows", + "individual confirmation applies original physical writer", + "one issue creates exactly one physical movement", + "uncertain response recovers the same receipt without duplicate", + "audit retains actor reason before after and operation receipt", + "old ledger version requires new conference", + "receipt cannot be reused after context generation changes", + "confirmation permission revalidated even on replay", + "transfer preview validates both bound warehouses", + "two independent conferences can observe the same original revision", + "transfer confirms through the same writer", + "transfer produces two balanced physical movements atomically", + "second conference refuses the revision changed by the first confirmation", + "failure-recovery conference prepared on current state", + "failure after physical write rolls back operation movement and native audit", + "same confirmation succeeds once after transient audit failure is removed", + "equipment catalogue available without selecting a destination automatically", + "count preview uses original current ledger revision", + "count stores the physical observation without changing stock", + "only counts for the selected bound pair are offered", + "adjustment impact is calculated by the original count workflow", + "adjustment reconciles the physical book with the explicit count", + "count adjustment receipt is recovered without applying the difference twice", + "an applied count is no longer offered for a second adjustment", + "tampered conference refused", + "feature remains independently disabled", + "all preexisting host and native rows restored after isolated regression" + ], + "stock_interface": [ + "startup makes reads only", + "catalogues never auto-select item or responsible", + "unsaved draft guards page exit", + "form captured before disabling fields; exact decimal and reason retained", + "issue does not send a hidden transfer destination", + "impact shown in readable form before confirmation", + "preview never auto-confirms", + "uncertain result protects fields and permits only recovery", + "recovery uses identical sealed conference without a new key", + "recovered receipt closes confirmation", + "successful recovery releases exit guard", + "count uses freshly observed revision and permits explicit zero quantity", + "count excludes issue-only fields", + "adjustment uses the explicitly selected count rather than a manually entered difference", + "adjustment impact distinguishes difference from the counted total" + ], + "http8085": [ + "existing obligation remains 100MZN with no new settlement", + "accepted purchase receipt remains readable after shared runtime update", + "explicit transition to authorized Stock module is preserved", + "new Stock operations remain disabled without granting new rights", + "installed workspace is blocked until explicit activation", + "same server delivers the installed workspace asset" + ] + }, + "stock_code_installed": true, + "stock_operations_enabled": false, + "stock_build": { + "installed": true, + "reused_valid_private_build": true, + "entry_sha256": "722b472f838c2a981f72b95e154ff0374e6f098a192905dece9edcaa1f1b9e42" + }, + "new_access_granted": false, + "cache_cleared": false, + "financial_commands8085": 0, + "browser_visual_review": false, + "parallel_concurrency_proof_new": false, + "source_hashes": { + "app/Shared/Installation/CompositionSchema.php": "b5f24360327c3bc69b76ad551705bad1cfed504151cc85e316e0b10bb8b9533f", + "app/Shared/Installation/CompositionSchemaConnection.php": "592fb897bd2f28f300ebd9924c62f2863792853de053cdf58bac213617395d9f", + "app/Shared/Installation/CompositionSchemaBuilder.php": "6c4ee34d6670744548dd536f869a3493f19b4e0bd3d4df6e93005d5081b5f3a3", + "app/Shared/Installation/CompositionRuntime.php": "ccf478b097f874576e348755e7944e12f0a2fa68d1902a0d0f96315074ed3036", + "app/Shared/Testing/TestingDatabaseGuard.php": "d6564f90da999d679de4c27509bc394cd32883f35b0b9b4e9a2baab47ae19565", + "app/Providers/AppServiceProvider.php": "04be4fa8b938168f6296f0b061cc9eca4383eeb3374a0117112500a2c0390f88", + "app/Modules/Services/ServiceCalculation.php": "f7d2120ffb3c65a4e7f228c824598178227411b1489f689d421f03bdb909526b", + "scripts/sgf-compose-install.php": "200f67d75e153a964c6b97ddf8f990ca15f5a51b666517b2598acb539ffb14c5", + "scripts/sgf-install-stock-prepared.mjs": "b7f9e1cde2526cc81b54beade6bcca5120fe5cf5fbfb352a730a61633e67a174", + "scripts/sgf-composition-http-check.mjs": "c593a7abf3b45c24a3893abef407176f58aa0ee5ca45d246be8ecc904ae95d99", + "scripts/receipt-composition-http.php": "7acce4c5db8a748bbc6e094592387b01acef7a5b038e484375c5b4b5700386cd", + "scripts/stock-operations-client-check.mjs": "83e1b87e00e8d4777ab98bbac5dc1eeef9ae6465bd8abc624d06b97425af940b" + }, + "stock_source_hashes": { + "app/Modules/Inventory/StockOperationsController.php": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf", + "app/Modules/Purchasing/ReceiptRuntime.php": "a8f3aac4b65ceea15ac7a8e8e34510d689b09fab8c2f689c466eb90d5e8632dc", + "config/stock_operations.php": "0351259789d6909aca1a9133168f2b6ed66bf90867de88417ebc307cdeb89149", + "routes/web.php": "7a8cc3f15e9732014e866102f353c31699666ec87ecef81409485c3022703ae7", + "app/Shared/Context/ModuleNavigation.php": "0fd39d7635d9a2744111ee39a21b10d04c73b32d1df35b473b61413cd14bc555", + "app/Modules/Identity/Services/IdentityWorkflow.php": "677bdb459aa751f3af37a3b4162f40bcbc7dd8190167202441b3374f244f52b5", + "resources/js/StockMenuWorkspace.jsx": "33c4624727e752dec7a916a37c4ce5cd0c9650c1e046c411be645dd944c4db32", + "resources/views/stock-operations-workspace.blade.php": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a", + "public/stock-operations-workspace.mjs": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8", + "scripts/stock-operations-proof.php": "c7feeaca6ef31e61cc851f97ba641ddcf7d8529df9fceb3c7fec895d2d0d290a" + }, + "awaiting_decision": "docs/SGF_8085_NEXT_AUTHORIZATION.md", + "limitations": [ + "Source SQL structures and code installation do not constitute six-module acceptance", + "Finance ledger remains to be composed; no debt mirror is created to satisfy a host-table footprint", + "New QA profiles and Stock rights not granted", + "Native supplier100MZN financial effects remain excluded pending explicit approval", + "Visual review and deployment capacity unverified" + ] +} diff --git a/docs/proofs/sgf-six-module-footprint-current-20261110.json b/docs/proofs/sgf-six-module-footprint-current-20261110.json new file mode 100644 index 0000000..4405357 --- /dev/null +++ b/docs/proofs/sgf-six-module-footprint-current-20261110.json @@ -0,0 +1,374 @@ +{ + "date": "2026-11-10", + "contract_sha256": "f3e954be7917008dea88ad9fc9f539ba641f0d7a89083ad730746b5a504a1bc6", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6f5c8af14412427134f1c40bda9215de68b3a66d46ed2287b59895f68b929179" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "b5ac9898f2089fad7799b9e62e5ce5db6df7fd64643e8e7e3b52c6a0e2d142a8" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "347869a371084ae91bd7d651334f67f16369495263f28b8b8348bc442f7d6534" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual do percurso emitido na mesma8085; prova HTTP e documento1000 existentes" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "d368d03239b457eacb61d67c28274d8ed30cf382e7a5586f8a18a7b0ab79ce18" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Reutilizar C01–C10 aceite; continuação financeira e contabilística100/20/reversão agora provada na8085", + "Conferência visual e recuperação do arranque geral" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "01c558457df290879bdf6c7f8de8cd9efccad81b940731f0aaf2489cf02bfcf7" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "6460a4235fe7b0b4602b0aeb63a47f1a97e367e93baba6aa774be5fa0dac4e6e" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Concluir devolução e ligação material de Oficinas ao mesmo livro físico", + "Concluir cenário físico2/-1/+0,5 e respectiva valorização na8085", + "Transferência em trânsito/recepção distinta e contagem por lotes conservam seus recortes", + "Saídas, transferência imediata, contagem sem lotes e ajuste já disponíveis pelos writers originais" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "6460a4235fe7b0b4602b0aeb63a47f1a97e367e93baba6aa774be5fa0dac4e6e" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "acc01d934dd11f783df82b40a6ece0905a67b0d26ff0bdfc3b18c11a027d66bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "99ea0b2c0941db0a42ff1f3833225430a73efb6933b3f88a8be4481f902ac571" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Compor na mesma instalação sem perder as aprovações existentes", + "Completar o percurso oficina → stock → custo → Comercial/Contabilidade", + "Conservar a distinção entre apontamento e movimento físico" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "725bc8773a78cfd05dd0a729d885a97c0eef43854ca7ab4210b4d5e80d5d54ec" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Unificar as origens autorizadas sem copiar dívida ou saldos", + "Compor a tesouraria sintética e os relatórios na mesma empresa/unidade", + "Conservar vencimento desconhecido e histórico imutável" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "fb107c11bd1a7c1009c7c0535907ee266cf9bc9840df3915bbc24afba2b460ad" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Compor diário e relatórios reais do núcleo na instalação única", + "Ligar Compras/Comercial/custos sem gerar lançamentos automáticos", + "Substituir os catálogos de relatórios por consultas disponíveis e verificadas" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-six-module-footprint-final-20261110.json b/docs/proofs/sgf-six-module-footprint-final-20261110.json new file mode 100644 index 0000000..fa532c2 --- /dev/null +++ b/docs/proofs/sgf-six-module-footprint-final-20261110.json @@ -0,0 +1,450 @@ +{ + "date": "2026-11-10", + "contract_sha256": "ffe0460ccfc95793931f7030c7f6e14cd83b3fd853629e0ba511253f365f34df", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6f5c8af14412427134f1c40bda9215de68b3a66d46ed2287b59895f68b929179" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "b5ac9898f2089fad7799b9e62e5ce5db6df7fd64643e8e7e3b52c6a0e2d142a8" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "347869a371084ae91bd7d651334f67f16369495263f28b8b8348bc442f7d6534" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "b9cdb1cee781cf12fdeed7a29d2ddbcfb824685523393533fea9c95ec6875d98", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "583bae215b2b2e51715591558a4d975fa0aed99da937d45a158c1888749a9a62" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "b9cdb1cee781cf12fdeed7a29d2ddbcfb824685523393533fea9c95ec6875d98", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "01c558457df290879bdf6c7f8de8cd9efccad81b940731f0aaf2489cf02bfcf7" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "3cd677891d81f1694fea7dce1afe8d3eb5c088e217eb4ea3cf0a522b47761a4c" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "b9cdb1cee781cf12fdeed7a29d2ddbcfb824685523393533fea9c95ec6875d98", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual do novo Stock confirmada pelo utilizador; aceitação do conjunto separada" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "acc01d934dd11f783df82b40a6ece0905a67b0d26ff0bdfc3b18c11a027d66bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "32d35097a200b045ed416a165b661b7ceb15da90d8f2c345c54ee6503e4b970f" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "b9cdb1cee781cf12fdeed7a29d2ddbcfb824685523393533fea9c95ec6875d98", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "725bc8773a78cfd05dd0a729d885a97c0eef43854ca7ab4210b4d5e80d5d54ec" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "b9cdb1cee781cf12fdeed7a29d2ddbcfb824685523393533fea9c95ec6875d98", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "74d92e0c9dcbb940f66afc5a0f5fa29025cfa7b6691043b87fba82b428cf9901" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "b9cdb1cee781cf12fdeed7a29d2ddbcfb824685523393533fea9c95ec6875d98", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-six-module-functional-review-20261110.json b/docs/proofs/sgf-six-module-functional-review-20261110.json new file mode 100644 index 0000000..0ad7836 --- /dev/null +++ b/docs/proofs/sgf-six-module-functional-review-20261110.json @@ -0,0 +1,254 @@ +{ + "date": "2026-11-10", + "passed": true, + "meaning": "functional QA review; not presumed owner acceptance or deployment validation", + "installation": "cc50a276c10c", + "entry": "http://127.0.0.1:8085/sgf", + "port": 8085, + "contract_sha256": "ffe0460ccfc95793931f7030c7f6e14cd83b3fd853629e0ba511253f365f34df", + "sgc_required": false, + "new_demo": false, + "required_module_flows_executed_in_same_installation": [ + "commercial", + "purchasing", + "stock", + "workshop", + "finance", + "accounting" + ], + "journals": 18, + "previous_journals_preserved": 16, + "qa_accounts": 15, + "stock_value_minor": 14500, + "period_facts": 18, + "period_pending": 0, + "period_open_after_rolled_back_closure": true, + "proofs": { + "guided_menu_http": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/guided-menu-http-20261110.json", + "sha256": "5d0322be5673532de2c03b9fa618dd40fafc54cb4444ccd6f7cd939e48427c7d", + "checks": 47, + "reused_from_existing_valid_evidence": false + }, + "module_dropdown_render": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/module-dropdown-render-20261110.json", + "sha256": "b3749c5b3953206bca8384fc8202699de3dabb6f49f33a35fa061e2ba61c7a4b", + "checks": 11, + "reused_from_existing_valid_evidence": false + }, + "workshop_physical_bridge": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-135641-89b4b77c/workshop-stock-bridge-result.json", + "sha256": "de3d96b771f48f318de500c742b7d39e02e48c7bffa3c768ce01dfa2b6bbebe7", + "checks": 18, + "reused_from_existing_valid_evidence": true + }, + "workshop_client": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-workshop-stock-client-20261110.json", + "sha256": "4778a0e243fde9995c2bec644528bbea9aa7603381c927d4b4e7ce654a8dffa9", + "checks": 9, + "reused_from_existing_valid_evidence": true + }, + "workshop_commercial": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-140916-70f2bd82/workshop-commercial-result.json", + "sha256": "07f982c1b4580559f4f0431c7e3785ad22d9e16ba30ca5d956a459ff5ca433cd", + "checks": 8, + "reused_from_existing_valid_evidence": true + }, + "accounting_continuity": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/accounting-continuity-http-20261110.json", + "sha256": "e224775d36ee510a5d80004403c4b4fca8aa9e46f66eff9dec780c97766007c0", + "checks": 13, + "reused_from_existing_valid_evidence": true + }, + "stock_protocol": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-145853-1ecda5fa/stock-final-result.json", + "sha256": "0c681c3c546722099c02eae2a9a8c36b8312bf362fd83c3a8bbde071809b895d", + "checks": 27, + "reused_from_existing_valid_evidence": false + }, + "stock_concurrency": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/stock-final-concurrency-20261110/result.json", + "sha256": "c71ab7f215d2f4a80b3836f1a5d2908ea2468efdf8292092a39cb39dacbeb97c", + "checks": null, + "reused_from_existing_valid_evidence": false + }, + "stock_transit_accounting": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-150714-9d33082b/stock-transit-ledger-result.json", + "sha256": "616be2bf7a514cdfaac199c1d801d5cf978a4c2f73428aeb79846e4476876bea", + "checks": 6, + "reused_from_existing_valid_evidence": false + }, + "stock_recovery": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-151847-1fa3a3c5/stock-final-recovery-result.json", + "sha256": "742cbf8034ec4310cdfceb771416f81ec462eb502c4be39c885cd71c15078451", + "checks": 6, + "reused_from_existing_valid_evidence": false + }, + "stock_preservation": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-151019-b03f905f/stock-final-preservation-result.json", + "sha256": "558cb41eab17850c892fce1330cb88d3ea1c1828a5081c2dd1b3fd83987dd094", + "checks": 12, + "reused_from_existing_valid_evidence": false + }, + "stock_http": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-stock-final-http-progress.json", + "sha256": "0ef1d0284fbad9d4a31a87ca61eae0d57588d0f3e2c599186397ab0df9124d7d", + "checks": 0, + "reused_from_existing_valid_evidence": false + }, + "stock_http_boundaries": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/stock-final-boundaries-http-20261110.json", + "sha256": "d150c48117d6f7d0b3f0fd29b5519593c01f3cf7727830fd809822c367d9fc61", + "checks": 20, + "reused_from_existing_valid_evidence": false + }, + "stock_dom_http": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/stock-final-dom-http-20261110.json", + "sha256": "189d8905cc6c56719bf622478506d5bcc501ae6dee76853454313b80e02bb6b3", + "checks": 10, + "reused_from_existing_valid_evidence": false + }, + "current_period": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-150544-39a961e8/period-composition-result.json", + "sha256": "f227522113d6c6c1f9ddb9ac21a02ce441df2f5fd3621428da0ec3b9008886dd", + "checks": 7, + "reused_from_existing_valid_evidence": false + }, + "previous_server_restart": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-restart-proof-20261110.json", + "sha256": "803180fb780bcf1e42cfbe4c45854f74371e77a893b6e5e376093c3747385ce1", + "checks": null, + "reused_from_existing_valid_evidence": true + }, + "workshop_physical_http": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-stock-http-progress.json", + "sha256": "05e06a127dddf340a5e11e97aa439635e2063bccad96c5d40871ea45554b35e0", + "checks": null, + "reused_from_existing_valid_evidence": true + }, + "workshop_commercial_http": { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-workshop-commercial-http-progress.json", + "sha256": "60467cd70b616d245bda1284380b6133c3dc8b47a858a1b85cb7c47d62a879b8", + "checks": null, + "reused_from_existing_valid_evidence": true + } + }, + "sources": [ + { + "file": "app/Shared/Installation/PresentationEntry.php", + "sha256": "f9ec01572624f850a40849e2e6eb54f78039ca41510eae663de4a20caae3ba41" + }, + { + "file": "resources/views/sgf-presentation-entry.blade.php", + "sha256": "2f10d423e0846a2d82e487bb558cc66a69fa03c93b64ab04702ddf6ec5b5d715" + }, + { + "file": "resources/js/app.jsx", + "sha256": "0eccf25f328d1554e7defa14e55a9484ca11581cf3b6bafea24395237e40229e" + }, + { + "file": "resources/js/presentation-landing.js", + "sha256": "25651321f67fe32829cd14dbe4541f6a83b925ee1b8ae05bcc842c29c7e744ad" + }, + { + "file": "public/sgf-entry-menu.mjs", + "sha256": "e96dc783f45220dd56ba699ab40b638ad3390d59caa307199b0146328f18c6f8" + }, + { + "file": "resources/css/tempo-theme.css", + "sha256": "29640dbc88d29a7bcd2383243a169728ccc52d4a9f7faf08e6ee7187f4011459" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "sha256": "583bae215b2b2e51715591558a4d975fa0aed99da937d45a158c1888749a9a62" + }, + { + "file": "app/Modules/Identity/Services/IdentityWorkflow.php", + "sha256": "6f78d35815aee5eaa09529dbc523afdc0d4ffe7e546624e04658c1562ae1de98" + }, + { + "file": "app/Shared/Context/ModuleNavigation.php", + "sha256": "f60738e2f1ef1a87594b4a7994f2e5163fab345ccc0a320bb784c6eacb0a63b9" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerCandidates.php", + "sha256": "8e70302495c23a9dbbd2b38f73c361a890813767e07a8de90ac112735545f62e" + }, + { + "file": "packages/general-ledger/src/StockValuation.php", + "sha256": "8d5c30e6410e0d08cca2cdfb23b6a0e7c984fd32811429d236845584a2e0ea60" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "packages/general-ledger/src/PayableJournal.php", + "sha256": "d15a34802c88eb562582cc0d98f203327855b1bcb861f6ce7b01c052431d2f09" + }, + { + "file": "packages/general-ledger/src/LedgerCycle.php", + "sha256": "fa50770af8ec96e9ba2ffc3fbedfd34013a64bb99022d85bf5f0bef86bbc7767" + }, + { + "file": "public/sgf-stock-final.mjs", + "sha256": "3cd677891d81f1694fea7dce1afe8d3eb5c088e217eb4ea3cf0a522b47761a4c" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "resources/js/StockMenuWorkspace.jsx", + "sha256": "e4044ee10d5da2120f95fc62e88581b9df176317c10f02e91be51cee11240b21" + }, + { + "file": "resources/js/ModuleNavigation.jsx", + "sha256": "659e5cafc99ff9fcd4f8bc7f500da963f0cb9a4ebad2cf962649e468495cd3cd" + }, + { + "file": "resources/js/WorkshopMenuWorkspace.jsx", + "sha256": "397c749110e46953f7fde839cc2b0772b922716f2179461eddb11383909aed7d" + } + ], + "client_recovery_execution": { + "script": "scripts/sgf-stock-final-client-check.mjs", + "source_sha256": "a304f4cc4bb78a7e116eae82b6cd5eafc036d32e036357081c761a85ce4b9ae5", + "observed_passed_checks": 9, + "evidence": "structured passed:true result observed during this delivery; no visual claim" + }, + "stock_visual_review": { + "confirmed_by": "utilizador", + "response": "Abre e está legível", + "automated_browser_review": false, + "scope": "new Stock transit/lots/history screen only" + }, + "other_visual_review": "pending; user confirmed access restored, not complete visual acceptance", + "navigation": "six horizontal module dropdowns with scoped functions; explicit context selection; no new rights", + "owner_acceptance_presumed": false, + "client_presentable": false, + "deployment_capacity_validated": false, + "operational_limits": [ + "Sustained load not validated", + "Cold start of shared PostgreSQL/operating system not executed", + "Attachments retention/quotas and general orphan reconciliation not validated", + "Accounts/documents/payments remain synthetic", + "Optional catalogue features outside fixed mandatory matrix remain explicitly unavailable" + ], + "preservation": { + "existing_client_and_draft_records_compared_unchanged": true, + "protected_six_associations_unchanged": true, + "other_protected_databases_connected": false, + "original_purchasing_binding_unchanged": true, + "frozen_purchasing_bundle_changed": false, + "cache_cleared": false, + "public_rights_changed": false, + "new_postgresql_roles": false, + "publication": false, + "checkpoint": false + } +} diff --git a/docs/proofs/sgf-solution-closure-baseline-20261110.json b/docs/proofs/sgf-solution-closure-baseline-20261110.json new file mode 100644 index 0000000..3553be7 --- /dev/null +++ b/docs/proofs/sgf-solution-closure-baseline-20261110.json @@ -0,0 +1,378 @@ +{ + "date": "2026-11-10", + "contract_sha256": "3803463f430091444001de6eace1d7e6b4fbf6b96e16a650ae2d2f3ef30ea1cc", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6f5c8af14412427134f1c40bda9215de68b3a66d46ed2287b59895f68b929179" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "b5ac9898f2089fad7799b9e62e5ce5db6df7fd64643e8e7e3b52c6a0e2d142a8" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "347869a371084ae91bd7d651334f67f16369495263f28b8b8348bc442f7d6534" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-tests.json", + "present": false, + "sha256": null, + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "b_internal_confirmations", + "doc_records", + "document_preview_models", + "service_calculation_profiles", + "sf_proposals", + "si_invoices" + ], + "installation_complete": false, + "client_presentable": false, + "remaining": [ + "Compor estruturas e guardas sintéticas na instalação única", + "Conservar o documento e identidade entre Comercial e Financeiro", + "Provar o percurso por interface na mesma empresa/unidade de apresentação" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "9b426476b2805094fb317cc753385ef43882fb4964883e880eac287eed89d9e0" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "installation_complete": false, + "client_presentable": false, + "remaining": [ + "Reutilizar o percurso aceite sem o reexecutar por rotina", + "Ligar a continuação financeira e contabilística da mesma origem", + "Conservar a composição local sem serviço SGC obrigatório" + ] + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "01c558457df290879bdf6c7f8de8cd9efccad81b940731f0aaf2489cf02bfcf7" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "6460a4235fe7b0b4602b0aeb63a47f1a97e367e93baba6aa774be5fa0dac4e6e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "installation_complete": false, + "client_presentable": false, + "remaining": [ + "Disponibilizar os comandos de stock na interface SGF pelo writer comum", + "Concluir trânsito/recepção e divergências onde o percurso ainda é só definido", + "Conservar reservas, unidades e ausência de saldo negativo", + "Substituir as páginas informativas pelas operações efectivamente disponíveis" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "6460a4235fe7b0b4602b0aeb63a47f1a97e367e93baba6aa774be5fa0dac4e6e" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "acc01d934dd11f783df82b40a6ece0905a67b0d26ff0bdfc3b18c11a027d66bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "99ea0b2c0941db0a42ff1f3833225430a73efb6933b3f88a8be4481f902ac571" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "ws_orders", + "ws_entries", + "dc_calculations", + "ic_records", + "wb_drafts" + ], + "installation_complete": false, + "client_presentable": false, + "remaining": [ + "Compor na mesma instalação sem perder as aprovações existentes", + "Completar o percurso oficina → stock → custo → Comercial/Contabilidade", + "Conservar a distinção entre apontamento e movimento físico" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "725bc8773a78cfd05dd0a729d885a97c0eef43854ca7ab4210b4d5e80d5d54ec" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "sr_records", + "sr_effects", + "pa_obligations", + "pa_access", + "ps_payments" + ], + "installation_complete": false, + "client_presentable": false, + "remaining": [ + "Unificar as origens autorizadas sem copiar dívida ou saldos", + "Compor a tesouraria sintética e os relatórios na mesma empresa/unidade", + "Conservar vencimento desconhecido e histórico imutável" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "8c83538a5df4625bb3a15c81e3bf618d12187eca77da5f6e73dd1b4b781c64ce" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "1fb2490fe8d4d206d2f8157803307cdd4e2f733150d0fec4993a7e229fce58cb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "ca_proposals", + "ca_resolutions", + "gl_accounts", + "gl_periods", + "gl_entries", + "gl_lines" + ], + "installation_complete": false, + "client_presentable": false, + "remaining": [ + "Compor diário e relatórios reais do núcleo na instalação única", + "Ligar Compras/Comercial/custos sem gerar lançamentos automáticos", + "Substituir os catálogos de relatórios por consultas disponíveis e verificadas" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false, + "valid": false, + "superseded_by": "docs/proofs/sgf-solution-closure-baseline-final-20261110.json", + "correction": "Provisional footprint used incorrect workshop/ledger table names and invoice proof reference; use only the source-validated final baseline." +} diff --git a/docs/proofs/sgf-solution-closure-baseline-final-20261110.json b/docs/proofs/sgf-solution-closure-baseline-final-20261110.json new file mode 100644 index 0000000..165d1fa --- /dev/null +++ b/docs/proofs/sgf-solution-closure-baseline-final-20261110.json @@ -0,0 +1,376 @@ +{ + "date": "2026-11-10", + "contract_sha256": "12a783dd869de795e4cc67e2ce62d538d12f0707492d5b9e66dc18be5d074f52", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6f5c8af14412427134f1c40bda9215de68b3a66d46ed2287b59895f68b929179" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "b5ac9898f2089fad7799b9e62e5ce5db6df7fd64643e8e7e3b52c6a0e2d142a8" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "347869a371084ae91bd7d651334f67f16369495263f28b8b8348bc442f7d6534" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "b_internal_confirmations", + "doc_records", + "document_preview_models", + "service_calculation_profiles", + "sf_proposals", + "si_invoices" + ], + "client_presentable": false, + "remaining": [ + "Compor estruturas e guardas sintéticas na instalação única", + "Conservar o documento e identidade entre Comercial e Financeiro", + "Provar o percurso por interface na mesma empresa/unidade de apresentação" + ], + "integrated_release_verified": false + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "9b426476b2805094fb317cc753385ef43882fb4964883e880eac287eed89d9e0" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "client_presentable": false, + "remaining": [ + "Reutilizar o percurso aceite sem o reexecutar por rotina", + "Ligar a continuação financeira e contabilística da mesma origem", + "Conservar a composição local sem serviço SGC obrigatório" + ], + "integrated_release_verified": false, + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "01c558457df290879bdf6c7f8de8cd9efccad81b940731f0aaf2489cf02bfcf7" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "6460a4235fe7b0b4602b0aeb63a47f1a97e367e93baba6aa774be5fa0dac4e6e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "client_presentable": false, + "remaining": [ + "Disponibilizar os comandos de stock na interface SGF pelo writer comum", + "Concluir trânsito/recepção e divergências onde o percurso ainda é só definido", + "Conservar reservas, unidades e ausência de saldo negativo", + "Substituir as páginas informativas pelas operações efectivamente disponíveis" + ], + "integrated_release_verified": false + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "6460a4235fe7b0b4602b0aeb63a47f1a97e367e93baba6aa774be5fa0dac4e6e" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "acc01d934dd11f783df82b40a6ece0905a67b0d26ff0bdfc3b18c11a027d66bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "99ea0b2c0941db0a42ff1f3833225430a73efb6933b3f88a8be4481f902ac571" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "wo_orders", + "wo_entries", + "dc_calculations", + "ic_results", + "wb_proposals" + ], + "client_presentable": false, + "remaining": [ + "Compor na mesma instalação sem perder as aprovações existentes", + "Completar o percurso oficina → stock → custo → Comercial/Contabilidade", + "Conservar a distinção entre apontamento e movimento físico" + ], + "integrated_release_verified": false + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "725bc8773a78cfd05dd0a729d885a97c0eef43854ca7ab4210b4d5e80d5d54ec" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "sr_records", + "sr_effects", + "pa_obligations", + "pa_access", + "ps_payments" + ], + "client_presentable": false, + "remaining": [ + "Unificar as origens autorizadas sem copiar dívida ou saldos", + "Compor a tesouraria sintética e os relatórios na mesma empresa/unidade", + "Conservar vencimento desconhecido e histórico imutável" + ], + "integrated_release_verified": false + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "8c83538a5df4625bb3a15c81e3bf618d12187eca77da5f6e73dd1b4b781c64ce" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "1fb2490fe8d4d206d2f8157803307cdd4e2f733150d0fec4993a7e229fce58cb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + } + ], + "missing_host_tables": [ + "ca_proposals", + "ca_resolutions", + "gl_accounts", + "gl_periods", + "gl_journals", + "gl_lines" + ], + "client_presentable": false, + "remaining": [ + "Compor diário e relatórios reais do núcleo na instalação única", + "Ligar Compras/Comercial/custos sem gerar lançamentos automáticos", + "Substituir os catálogos de relatórios por consultas disponíveis e verificadas" + ], + "integrated_release_verified": false + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v10-20261110.json b/docs/proofs/sgf-solution-structure-v10-20261110.json new file mode 100644 index 0000000..00f958a --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v10-20261110.json @@ -0,0 +1,690 @@ +{ + "date": "2026-11-10", + "contract_sha256": "367671e1869591249c2d704472279401d3ce03fe9ca8dc93e575d7c426e4508b", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Rácios e tendências conciliados com critérios aprovados" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "24108c35d52ba68797af655a2f6e824401a51865820f81ba7afa256b29db652b" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "e7ead66818df2ba644b3b22d35707637ae05dcfcfbe28e57c0a3ed61b05180db" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "ad648825ec55b3bdb8eca038a01c5bd685b8b62b23f79bfd7771282506e1f66a" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "a14a6d71d206b64445990a197e30387acb5882f1331ac13c2f283d7de0cb2477" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v11-20261110.json b/docs/proofs/sgf-solution-structure-v11-20261110.json new file mode 100644 index 0000000..1728cf1 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v11-20261110.json @@ -0,0 +1,717 @@ +{ + "date": "2026-11-10", + "contract_sha256": "5338247db9343fb90593acd038882b013ace2c2a9d55d435f74752569ebf04a2", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "24108c35d52ba68797af655a2f6e824401a51865820f81ba7afa256b29db652b" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "e7ead66818df2ba644b3b22d35707637ae05dcfcfbe28e57c0a3ed61b05180db" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "ad648825ec55b3bdb8eca038a01c5bd685b8b62b23f79bfd7771282506e1f66a" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "a14a6d71d206b64445990a197e30387acb5882f1331ac13c2f283d7de0cb2477" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v12-20261110.json b/docs/proofs/sgf-solution-structure-v12-20261110.json new file mode 100644 index 0000000..d559d03 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v12-20261110.json @@ -0,0 +1,729 @@ +{ + "date": "2026-11-10", + "contract_sha256": "4e94b00e19ffaf15daefe0786839a5493ea44c4c1f0888818eb7b79b1c8ac2a2", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "24108c35d52ba68797af655a2f6e824401a51865820f81ba7afa256b29db652b" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "817090269660361d940c17d1b2d9428697e744717d29f85b0a44206d883f82a3", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v13-20261110.json b/docs/proofs/sgf-solution-structure-v13-20261110.json new file mode 100644 index 0000000..de541f6 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v13-20261110.json @@ -0,0 +1,847 @@ +{ + "date": "2026-11-10", + "contract_sha256": "55017d586955291a1e70d891a3e40fdfd357761d8f70cc79e81a640c4fae9a4a", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "05fbc5c21ad8dcbb6947f28436d8a7ea27033baea27abed2d43c00ad7d632c71", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "05fbc5c21ad8dcbb6947f28436d8a7ea27033baea27abed2d43c00ad7d632c71", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "05fbc5c21ad8dcbb6947f28436d8a7ea27033baea27abed2d43c00ad7d632c71", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "b433c85d76609d2696c2818b34ba5c63059d673bad37c638e12d99f6ae0aac68" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "a66cfd110602f42155fa234452365270df7e0de65ef884e231e9e2fac45dc1f8" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "05fbc5c21ad8dcbb6947f28436d8a7ea27033baea27abed2d43c00ad7d632c71", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "05fbc5c21ad8dcbb6947f28436d8a7ea27033baea27abed2d43c00ad7d632c71", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "24108c35d52ba68797af655a2f6e824401a51865820f81ba7afa256b29db652b" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "05fbc5c21ad8dcbb6947f28436d8a7ea27033baea27abed2d43c00ad7d632c71", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v14-20261110.json b/docs/proofs/sgf-solution-structure-v14-20261110.json new file mode 100644 index 0000000..1811243 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v14-20261110.json @@ -0,0 +1,869 @@ +{ + "date": "2026-11-10", + "contract_sha256": "3d918757d649f3f6e2468d3dc37b25614d81259bc11a8878e1eef8b56ee35a5d", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "b433c85d76609d2696c2818b34ba5c63059d673bad37c638e12d99f6ae0aac68" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "8b652d8a30bcd9ea477a9af88583fccaa5897aff28fdc65014fa9393bfc16f8a" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "24108c35d52ba68797af655a2f6e824401a51865820f81ba7afa256b29db652b" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v15-20261110.json b/docs/proofs/sgf-solution-structure-v15-20261110.json new file mode 100644 index 0000000..7008c6d --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v15-20261110.json @@ -0,0 +1,896 @@ +{ + "date": "2026-11-10", + "contract_sha256": "d76f60559a54e326ce5d5a3008546025c41fb64c8f0b6be588e86ec9379526e6", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "b433c85d76609d2696c2818b34ba5c63059d673bad37c638e12d99f6ae0aac68" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "8b652d8a30bcd9ea477a9af88583fccaa5897aff28fdc65014fa9393bfc16f8a" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v16-20261110.json b/docs/proofs/sgf-solution-structure-v16-20261110.json new file mode 100644 index 0000000..ca8d450 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v16-20261110.json @@ -0,0 +1,928 @@ +{ + "date": "2026-11-10", + "contract_sha256": "75f0b966b49447fd3ab22b35115cb42068ed7026bf8c4e5160c71cf75aac5510", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "b433c85d76609d2696c2818b34ba5c63059d673bad37c638e12d99f6ae0aac68" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "8b652d8a30bcd9ea477a9af88583fccaa5897aff28fdc65014fa9393bfc16f8a" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v17-20261110.json b/docs/proofs/sgf-solution-structure-v17-20261110.json new file mode 100644 index 0000000..b48c899 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v17-20261110.json @@ -0,0 +1,955 @@ +{ + "date": "2026-11-10", + "contract_sha256": "d330c95f5d3d926a7a797f58717bda092b3bbe5d1993b90d601a70a1bb1fbb7e", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "b433c85d76609d2696c2818b34ba5c63059d673bad37c638e12d99f6ae0aac68" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "8b652d8a30bcd9ea477a9af88583fccaa5897aff28fdc65014fa9393bfc16f8a" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "c90ccc8d8c4b82b79cc09c3c7285e08920aaa88e9df035c9405e243fc0532b7c" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "39fdb94987d2af398442249161be0dabd173031b23989d3a4fc725b794e3650a" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "e29e75cc7cea60db64bf8162122037b5fef9a2f7cd2fb28992e08c7d770db666" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v18-20261110.json b/docs/proofs/sgf-solution-structure-v18-20261110.json new file mode 100644 index 0000000..ed79ca8 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v18-20261110.json @@ -0,0 +1,982 @@ +{ + "date": "2026-11-10", + "contract_sha256": "8d9a7deef828a41ce33a3e570c89e5cb4ec55eaf97d7988566d54cc01c4dd1be", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "b433c85d76609d2696c2818b34ba5c63059d673bad37c638e12d99f6ae0aac68" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "8b652d8a30bcd9ea477a9af88583fccaa5897aff28fdc65014fa9393bfc16f8a" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "5c18f289edb8e95299ac1f4d1a632e91876f7fb8e963dc6be562414705c15d4b" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "3c7c68d45ecead585c187a2e6c3b6fb5a43dd1476d87b48d7d546da7ce47c03e" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "0f887420a6f2f08db251a7777d9ab7e49c6d2bc6f4405dc4b96d9df1d4c38f3a" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v19-20261110.json b/docs/proofs/sgf-solution-structure-v19-20261110.json new file mode 100644 index 0000000..607e1bd --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v19-20261110.json @@ -0,0 +1,994 @@ +{ + "date": "2026-11-10", + "contract_sha256": "8551bbb59e7a6d1de7ed0aef0697ae517c2c08624859f09aa6be99cc504b5b47", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "bdfb494b93933ff0397f685c9632f9e00f6746405dfeefe9a8a58fa75722f798" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "25dd8f625ddc77984ef697496de6487c1c8fc3684256ac1890066308faeb0e1a" + }, + { + "file": "resources/js/workshop-report-view.js", + "present": true, + "sha256": "c255887925e9dc6e9d021008e857aa673dc01c8a8fd3651ecbe298d3bdec0f36" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "present": true, + "sha256": "1455e3dc4203c81f49ddaba05a7bf7de10cfd9b3bb4dbd2eb4480aa05b7ccf08", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "5c18f289edb8e95299ac1f4d1a632e91876f7fb8e963dc6be562414705c15d4b" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "3c7c68d45ecead585c187a2e6c3b6fb5a43dd1476d87b48d7d546da7ce47c03e" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "0f887420a6f2f08db251a7777d9ab7e49c6d2bc6f4405dc4b96d9df1d4c38f3a" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v20-20261110.json b/docs/proofs/sgf-solution-structure-v20-20261110.json new file mode 100644 index 0000000..ab96d17 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v20-20261110.json @@ -0,0 +1,1031 @@ +{ + "date": "2026-11-10", + "contract_sha256": "019c88eb62e9f37f618758934963050b37b8c2e5dbf54503c476df8950411bf2", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Controlo administrativo de prazo e entrega simulada de uma origem QA entregue; calendário legal, aplicabilidade geral, prazos por fonte/vigência e submissão continuam pendentes" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "bdfb494b93933ff0397f685c9632f9e00f6746405dfeefe9a8a58fa75722f798" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "25dd8f625ddc77984ef697496de6487c1c8fc3684256ac1890066308faeb0e1a" + }, + { + "file": "resources/js/workshop-report-view.js", + "present": true, + "sha256": "c255887925e9dc6e9d021008e857aa673dc01c8a8fd3651ecbe298d3bdec0f36" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "present": true, + "sha256": "1455e3dc4203c81f49ddaba05a7bf7de10cfd9b3bb4dbd2eb4480aa05b7ccf08", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "5c18f289edb8e95299ac1f4d1a632e91876f7fb8e963dc6be562414705c15d4b" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "3c7c68d45ecead585c187a2e6c3b6fb5a43dd1476d87b48d7d546da7ce47c03e" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "0f887420a6f2f08db251a7777d9ab7e49c6d2bc6f4405dc4b96d9df1d4c38f3a" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página", + "Controlo administrativo QA de obrigação/prazo/responsável do mapa interno revisto, preparação13/revisão14, correcção/cancelamento, entrega apenas simulada com SHA verificado, histórico/recibo/SQL25/100/CSV; sem calendário legal ou submissão" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + }, + { + "file": "app/Modules/Services/FiscalObligations.php", + "present": true, + "sha256": "bcf24717cf5828fed440b8e86f29a5ed4d325d392000240b224d155954170d1d" + }, + { + "file": "app/Modules/Services/Http/FiscalObligationsController.php", + "present": true, + "sha256": "95b03b505e588333206766b862d0f0cff006b1af1a3dc0466370ac6a7518e097" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "cf50c8eebbd9aafc985229740cc724fc8c66d176454875f0e4f346797d531fc9" + }, + { + "file": "resources/js/fiscal-obligations-view.js", + "present": true, + "sha256": "d3c1aafddac987de0a8e432746533da8832c140474f9d9d49055020f7c9e67dd" + }, + { + "file": "database/sql/fiscal_obligations.sql", + "present": true, + "sha256": "c68988342ca1ed5817485355844c2f502f5c2ca12954461c1555d5c5f2abf7fe" + }, + { + "file": "config/fiscal_obligations.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md", + "present": true, + "sha256": "028e3172128435924dc9594b68b25560cb0b30495f27c522864a94a1f8cc9504", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v21-20261110.json b/docs/proofs/sgf-solution-structure-v21-20261110.json new file mode 100644 index 0000000..ad54776 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v21-20261110.json @@ -0,0 +1,1043 @@ +{ + "date": "2026-11-10", + "contract_sha256": "152e27add6b20e016fb768dd6df33822fe154db4e041d6a85f3c9bcfd7a6f9a3", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Controlo administrativo de prazo e entrega simulada de uma origem QA entregue; calendário legal, aplicabilidade geral, prazos por fonte/vigência e submissão continuam pendentes" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "bdfb494b93933ff0397f685c9632f9e00f6746405dfeefe9a8a58fa75722f798" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "25dd8f625ddc77984ef697496de6487c1c8fc3684256ac1890066308faeb0e1a" + }, + { + "file": "resources/js/workshop-report-view.js", + "present": true, + "sha256": "c255887925e9dc6e9d021008e857aa673dc01c8a8fd3651ecbe298d3bdec0f36" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "present": true, + "sha256": "1455e3dc4203c81f49ddaba05a7bf7de10cfd9b3bb4dbd2eb4480aa05b7ccf08", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe", + "Mapa de antiguidade de fornecedores por referência explícita e condição administrativa revista, faixas0/1–30/31–60/61–90/>90/desconhecido, valores exactos por moeda só da página/CSV, sem alterar saldo ou vencimento original" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "1d147f14826793f97605ea442c6a93612a55caf265311a86824611650dfc0005" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "e52fa90276e6251b38bb48f83a11ec2b11ac819ffa3c2239da9712e50ce66343" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "ff4b32f404cd575ddc134cf373ae6ded5d0abae8641b14b591c0f5a856437938" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Purchasing/PayableAge.php", + "present": true, + "sha256": "19f4a3994b679b74275e72e906251c8d32ee9950ebedfae07bd4002abe740a9e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_AGING_DELIVERY.md", + "present": true, + "sha256": "a0af96108702bbb687a655de744e78e01a3e7c98365244410eb291fcb7a5fafa", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página", + "Controlo administrativo QA de obrigação/prazo/responsável do mapa interno revisto, preparação13/revisão14, correcção/cancelamento, entrega apenas simulada com SHA verificado, histórico/recibo/SQL25/100/CSV; sem calendário legal ou submissão" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + }, + { + "file": "app/Modules/Services/FiscalObligations.php", + "present": true, + "sha256": "bcf24717cf5828fed440b8e86f29a5ed4d325d392000240b224d155954170d1d" + }, + { + "file": "app/Modules/Services/Http/FiscalObligationsController.php", + "present": true, + "sha256": "95b03b505e588333206766b862d0f0cff006b1af1a3dc0466370ac6a7518e097" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "cf50c8eebbd9aafc985229740cc724fc8c66d176454875f0e4f346797d531fc9" + }, + { + "file": "resources/js/fiscal-obligations-view.js", + "present": true, + "sha256": "d3c1aafddac987de0a8e432746533da8832c140474f9d9d49055020f7c9e67dd" + }, + { + "file": "database/sql/fiscal_obligations.sql", + "present": true, + "sha256": "c68988342ca1ed5817485355844c2f502f5c2ca12954461c1555d5c5f2abf7fe" + }, + { + "file": "config/fiscal_obligations.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md", + "present": true, + "sha256": "028e3172128435924dc9594b68b25560cb0b30495f27c522864a94a1f8cc9504", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v22-20261111.json b/docs/proofs/sgf-solution-structure-v22-20261111.json new file mode 100644 index 0000000..1b67bcb --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v22-20261111.json @@ -0,0 +1,1090 @@ +{ + "date": "2026-11-10", + "contract_sha256": "b83cfb6c1ba0e5fec189d31dea9788c209a6b8047a1c517d3809eef743853c82", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Controlo administrativo de prazo e entrega simulada de uma origem QA entregue; calendário legal, aplicabilidade geral, prazos por fonte/vigência e submissão continuam pendentes" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "bdfb494b93933ff0397f685c9632f9e00f6746405dfeefe9a8a58fa75722f798" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "25dd8f625ddc77984ef697496de6487c1c8fc3684256ac1890066308faeb0e1a" + }, + { + "file": "resources/js/workshop-report-view.js", + "present": true, + "sha256": "c255887925e9dc6e9d021008e857aa673dc01c8a8fd3651ecbe298d3bdec0f36" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "present": true, + "sha256": "1455e3dc4203c81f49ddaba05a7bf7de10cfd9b3bb4dbd2eb4480aa05b7ccf08", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe", + "Mapa de antiguidade de fornecedores por referência explícita e condição administrativa revista, faixas0/1–30/31–60/61–90/>90/desconhecido, valores exactos por moeda só da página/CSV, sem alterar saldo ou vencimento original" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "1d147f14826793f97605ea442c6a93612a55caf265311a86824611650dfc0005" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "e52fa90276e6251b38bb48f83a11ec2b11ac819ffa3c2239da9712e50ce66343" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "ff4b32f404cd575ddc134cf373ae6ded5d0abae8641b14b591c0f5a856437938" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Purchasing/PayableAge.php", + "present": true, + "sha256": "19f4a3994b679b74275e72e906251c8d32ee9950ebedfae07bd4002abe740a9e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_AGING_DELIVERY.md", + "present": true, + "sha256": "a0af96108702bbb687a655de744e78e01a3e7c98365244410eb291fcb7a5fafa", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página", + "Controlo administrativo QA de obrigação/prazo/responsável do mapa interno revisto, preparação13/revisão14, correcção/cancelamento, entrega apenas simulada com SHA verificado, histórico/recibo/SQL25/100/CSV; sem calendário legal ou submissão", + "Correspondências explícitas revistas15/16 e balanço/resultados internos parciais QA:19contas/nove pendentes, resultado congelado19linhas/SHA, história/recibo/CSV contextual, ligação exacta ao razão; capital e resultado completo não presumidos" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + }, + { + "file": "app/Modules/Services/FiscalObligations.php", + "present": true, + "sha256": "bcf24717cf5828fed440b8e86f29a5ed4d325d392000240b224d155954170d1d" + }, + { + "file": "app/Modules/Services/Http/FiscalObligationsController.php", + "present": true, + "sha256": "95b03b505e588333206766b862d0f0cff006b1af1a3dc0466370ac6a7518e097" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "resources/js/fiscal-obligations-view.js", + "present": true, + "sha256": "d3c1aafddac987de0a8e432746533da8832c140474f9d9d49055020f7c9e67dd" + }, + { + "file": "database/sql/fiscal_obligations.sql", + "present": true, + "sha256": "c68988342ca1ed5817485355844c2f502f5c2ca12954461c1555d5c5f2abf7fe" + }, + { + "file": "config/fiscal_obligations.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Accounting/InternalStatementsController.php", + "present": true, + "sha256": "b05990c7fdb195957808b63a77599eaaabed5c5eb435dd8e7dd13d662c22fd8d" + }, + { + "file": "app/Modules/Accounting/InternalStatementTotals.php", + "present": true, + "sha256": "f13f6f356100e49786869196c54343fd629f6a1d38fa81665eb768dae4731601" + }, + { + "file": "resources/js/InternalStatementsWorkspace.jsx", + "present": true, + "sha256": "2fadac08cd6162dd06d056bfc4fb00479e9a5d930fdde10c1a4b57507982e76e" + }, + { + "file": "resources/js/internal-statements-view.js", + "present": true, + "sha256": "f1f7b44f2dc1679e537d77522e0fc5ee84da23068ef3157677bc20a8409d3464" + }, + { + "file": "resources/js/statement-download.js", + "present": true, + "sha256": "56e81a93af939e7b04f76347912af1093110afe48dd69549287561149b3999a0" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "database/sql/internal_statements.sql", + "present": true, + "sha256": "2beccb0bb7d91f48e5a5e386c1e010fb7d95cbfa90aa35a68e28dc63ae6a6f6b" + }, + { + "file": "config/internal_statements.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md", + "present": true, + "sha256": "028e3172128435924dc9594b68b25560cb0b30495f27c522864a94a1f8cc9504", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md", + "present": true, + "sha256": "53a07e2c0517de5b6881584c598b2f1d1d5665422ac2f8f5932626822daf3359", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v23-20261111.json b/docs/proofs/sgf-solution-structure-v23-20261111.json new file mode 100644 index 0000000..b19418e --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v23-20261111.json @@ -0,0 +1,1107 @@ +{ + "date": "2026-11-10", + "contract_sha256": "1fa5e56b6ccbae40a269a5a76d1d6a9bd0637ebad2ca15c98cb1e66bedac10f6", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Controlo administrativo de prazo e entrega simulada de uma origem QA entregue; calendário legal, aplicabilidade geral, prazos por fonte/vigência e submissão continuam pendentes" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido", + "Equipamento escolhido → ordens exactas SQL25/100 com filtros, identificação actual, CSV da página e regresso ao catálogo" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "f741a5ac6c16376ea26b1bacd7b5e706b14a4548a26f59c4a96ec882fb6a6fa5" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "4cad148295acb7c462425fa300dccc7c203255f5f616163575e205bfc8a80e34" + }, + { + "file": "resources/js/workshop-report-view.js", + "present": true, + "sha256": "28125af02e25b4befb9914c5dee753dae4164100f3c0f94988c332a50021bf9f" + }, + { + "file": "app/Modules/Workshop/WorkshopEquipment.php", + "present": true, + "sha256": "37223b732865f273831d0bf61a464137cc53526a4c55750347450ba20bcbbc37" + }, + { + "file": "resources/js/WorkshopEquipmentWorkspace.jsx", + "present": true, + "sha256": "450af001d385f42cd4d0ae42b34fa6b373cdd6811d94f8f4a1f5de2c591ed843" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "present": true, + "sha256": "1455e3dc4203c81f49ddaba05a7bf7de10cfd9b3bb4dbd2eb4480aa05b7ccf08", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md", + "present": true, + "sha256": "4d4249613a1b3fc339f342c3854eda08bf1ec280c0681eb3b2551469b2da2b36", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe", + "Mapa de antiguidade de fornecedores por referência explícita e condição administrativa revista, faixas0/1–30/31–60/61–90/>90/desconhecido, valores exactos por moeda só da página/CSV, sem alterar saldo ou vencimento original" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "1d147f14826793f97605ea442c6a93612a55caf265311a86824611650dfc0005" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "e52fa90276e6251b38bb48f83a11ec2b11ac819ffa3c2239da9712e50ce66343" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "ff4b32f404cd575ddc134cf373ae6ded5d0abae8641b14b591c0f5a856437938" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Purchasing/PayableAge.php", + "present": true, + "sha256": "19f4a3994b679b74275e72e906251c8d32ee9950ebedfae07bd4002abe740a9e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_AGING_DELIVERY.md", + "present": true, + "sha256": "a0af96108702bbb687a655de744e78e01a3e7c98365244410eb291fcb7a5fafa", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página", + "Controlo administrativo QA de obrigação/prazo/responsável do mapa interno revisto, preparação13/revisão14, correcção/cancelamento, entrega apenas simulada com SHA verificado, histórico/recibo/SQL25/100/CSV; sem calendário legal ou submissão", + "Correspondências explícitas revistas15/16 e balanço/resultados internos parciais QA:19contas/nove pendentes, resultado congelado19linhas/SHA, história/recibo/CSV contextual, ligação exacta ao razão; capital e resultado completo não presumidos" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "192e29600321e00abd6290f96a15fa529268b4ee312e7501436ef40f2a25502d" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + }, + { + "file": "app/Modules/Services/FiscalObligations.php", + "present": true, + "sha256": "bcf24717cf5828fed440b8e86f29a5ed4d325d392000240b224d155954170d1d" + }, + { + "file": "app/Modules/Services/Http/FiscalObligationsController.php", + "present": true, + "sha256": "95b03b505e588333206766b862d0f0cff006b1af1a3dc0466370ac6a7518e097" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "resources/js/fiscal-obligations-view.js", + "present": true, + "sha256": "d3c1aafddac987de0a8e432746533da8832c140474f9d9d49055020f7c9e67dd" + }, + { + "file": "database/sql/fiscal_obligations.sql", + "present": true, + "sha256": "c68988342ca1ed5817485355844c2f502f5c2ca12954461c1555d5c5f2abf7fe" + }, + { + "file": "config/fiscal_obligations.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Accounting/InternalStatementsController.php", + "present": true, + "sha256": "b05990c7fdb195957808b63a77599eaaabed5c5eb435dd8e7dd13d662c22fd8d" + }, + { + "file": "app/Modules/Accounting/InternalStatementTotals.php", + "present": true, + "sha256": "f13f6f356100e49786869196c54343fd629f6a1d38fa81665eb768dae4731601" + }, + { + "file": "resources/js/InternalStatementsWorkspace.jsx", + "present": true, + "sha256": "2fadac08cd6162dd06d056bfc4fb00479e9a5d930fdde10c1a4b57507982e76e" + }, + { + "file": "resources/js/internal-statements-view.js", + "present": true, + "sha256": "f1f7b44f2dc1679e537d77522e0fc5ee84da23068ef3157677bc20a8409d3464" + }, + { + "file": "resources/js/statement-download.js", + "present": true, + "sha256": "56e81a93af939e7b04f76347912af1093110afe48dd69549287561149b3999a0" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "database/sql/internal_statements.sql", + "present": true, + "sha256": "2beccb0bb7d91f48e5a5e386c1e010fb7d95cbfa90aa35a68e28dc63ae6a6f6b" + }, + { + "file": "config/internal_statements.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md", + "present": true, + "sha256": "028e3172128435924dc9594b68b25560cb0b30495f27c522864a94a1f8cc9504", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md", + "present": true, + "sha256": "53a07e2c0517de5b6881584c598b2f1d1d5665422ac2f8f5932626822daf3359", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v24-20261111.json b/docs/proofs/sgf-solution-structure-v24-20261111.json new file mode 100644 index 0000000..2ba97fb --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v24-20261111.json @@ -0,0 +1,1107 @@ +{ + "date": "2026-11-10", + "contract_sha256": "4eb0ad4c5c93d928d43bd57b485ae8cc93475710b4b11e51f4729e5dea477e60", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Controlo administrativo de prazo e entrega simulada de uma origem QA entregue; calendário legal, aplicabilidade geral, prazos por fonte/vigência e submissão continuam pendentes" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido", + "Equipamento escolhido → ordens exactas SQL25/100 com filtros, identificação actual, CSV da página e regresso ao catálogo" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "f741a5ac6c16376ea26b1bacd7b5e706b14a4548a26f59c4a96ec882fb6a6fa5" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "4cad148295acb7c462425fa300dccc7c203255f5f616163575e205bfc8a80e34" + }, + { + "file": "resources/js/workshop-report-view.js", + "present": true, + "sha256": "28125af02e25b4befb9914c5dee753dae4164100f3c0f94988c332a50021bf9f" + }, + { + "file": "app/Modules/Workshop/WorkshopEquipment.php", + "present": true, + "sha256": "37223b732865f273831d0bf61a464137cc53526a4c55750347450ba20bcbbc37" + }, + { + "file": "resources/js/WorkshopEquipmentWorkspace.jsx", + "present": true, + "sha256": "450af001d385f42cd4d0ae42b34fa6b373cdd6811d94f8f4a1f5de2c591ed843" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "present": true, + "sha256": "1455e3dc4203c81f49ddaba05a7bf7de10cfd9b3bb4dbd2eb4480aa05b7ccf08", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md", + "present": true, + "sha256": "4d4249613a1b3fc339f342c3854eda08bf1ec280c0681eb3b2551469b2da2b36", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe", + "Mapa de antiguidade de fornecedores por referência explícita e condição administrativa revista, faixas0/1–30/31–60/61–90/>90/desconhecido, valores exactos por moeda só da página/CSV, sem alterar saldo ou vencimento original" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "1d147f14826793f97605ea442c6a93612a55caf265311a86824611650dfc0005" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "e52fa90276e6251b38bb48f83a11ec2b11ac819ffa3c2239da9712e50ce66343" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "ff4b32f404cd575ddc134cf373ae6ded5d0abae8641b14b591c0f5a856437938" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Purchasing/PayableAge.php", + "present": true, + "sha256": "19f4a3994b679b74275e72e906251c8d32ee9950ebedfae07bd4002abe740a9e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_AGING_DELIVERY.md", + "present": true, + "sha256": "a0af96108702bbb687a655de744e78e01a3e7c98365244410eb291fcb7a5fafa", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página", + "Controlo administrativo QA de obrigação/prazo/responsável do mapa interno revisto, preparação13/revisão14, correcção/cancelamento, entrega apenas simulada com SHA verificado, histórico/recibo/SQL25/100/CSV; sem calendário legal ou submissão", + "Correspondências explícitas revistas15/16 e balanço/resultados internos parciais QA:19contas/nove pendentes, resultado congelado19linhas/SHA, história/recibo/CSV contextual, ligação exacta ao razão; capital e resultado completo não presumidos" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + }, + { + "file": "app/Modules/Services/FiscalObligations.php", + "present": true, + "sha256": "bcf24717cf5828fed440b8e86f29a5ed4d325d392000240b224d155954170d1d" + }, + { + "file": "app/Modules/Services/Http/FiscalObligationsController.php", + "present": true, + "sha256": "95b03b505e588333206766b862d0f0cff006b1af1a3dc0466370ac6a7518e097" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "resources/js/fiscal-obligations-view.js", + "present": true, + "sha256": "d3c1aafddac987de0a8e432746533da8832c140474f9d9d49055020f7c9e67dd" + }, + { + "file": "database/sql/fiscal_obligations.sql", + "present": true, + "sha256": "c68988342ca1ed5817485355844c2f502f5c2ca12954461c1555d5c5f2abf7fe" + }, + { + "file": "config/fiscal_obligations.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Accounting/InternalStatementsController.php", + "present": true, + "sha256": "b05990c7fdb195957808b63a77599eaaabed5c5eb435dd8e7dd13d662c22fd8d" + }, + { + "file": "app/Modules/Accounting/InternalStatementTotals.php", + "present": true, + "sha256": "f13f6f356100e49786869196c54343fd629f6a1d38fa81665eb768dae4731601" + }, + { + "file": "resources/js/InternalStatementsWorkspace.jsx", + "present": true, + "sha256": "2fadac08cd6162dd06d056bfc4fb00479e9a5d930fdde10c1a4b57507982e76e" + }, + { + "file": "resources/js/internal-statements-view.js", + "present": true, + "sha256": "f1f7b44f2dc1679e537d77522e0fc5ee84da23068ef3157677bc20a8409d3464" + }, + { + "file": "resources/js/statement-download.js", + "present": true, + "sha256": "74b3c132c288d807064276010ecb8ee1db54ac8d1649c5972a3e67dbc22f74d5" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "database/sql/internal_statements.sql", + "present": true, + "sha256": "2beccb0bb7d91f48e5a5e386c1e010fb7d95cbfa90aa35a68e28dc63ae6a6f6b" + }, + { + "file": "config/internal_statements.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md", + "present": true, + "sha256": "028e3172128435924dc9594b68b25560cb0b30495f27c522864a94a1f8cc9504", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md", + "present": true, + "sha256": "53a07e2c0517de5b6881584c598b2f1d1d5665422ac2f8f5932626822daf3359", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v25-20261111.json b/docs/proofs/sgf-solution-structure-v25-20261111.json new file mode 100644 index 0000000..a2006c7 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v25-20261111.json @@ -0,0 +1,1169 @@ +{ + "date": "2026-11-11", + "contract_sha256": "a341fc3b64a7cae053a4375f62bc2ad966c282a4396aa65c88d5cc3deda23dc6", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Cadastro/regras/processamento interno artificial QA em RH autónomo entregue; operações laborais gerais, cálculo legal IRPS/INSS/modelos e ligação salarial financeira/contabilística pendentes" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Controlo administrativo de prazo e entrega simulada de uma origem QA entregue; calendário legal, aplicabilidade geral, prazos por fonte/vigência e submissão continuam pendentes" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido", + "Equipamento escolhido → ordens exactas SQL25/100 com filtros, identificação actual, CSV da página e regresso ao catálogo" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "f741a5ac6c16376ea26b1bacd7b5e706b14a4548a26f59c4a96ec882fb6a6fa5" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "4cad148295acb7c462425fa300dccc7c203255f5f616163575e205bfc8a80e34" + }, + { + "file": "resources/js/workshop-report-view.js", + "present": true, + "sha256": "28125af02e25b4befb9914c5dee753dae4164100f3c0f94988c332a50021bf9f" + }, + { + "file": "app/Modules/Workshop/WorkshopEquipment.php", + "present": true, + "sha256": "37223b732865f273831d0bf61a464137cc53526a4c55750347450ba20bcbbc37" + }, + { + "file": "resources/js/WorkshopEquipmentWorkspace.jsx", + "present": true, + "sha256": "450af001d385f42cd4d0ae42b34fa6b373cdd6811d94f8f4a1f5de2c591ed843" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "present": true, + "sha256": "1455e3dc4203c81f49ddaba05a7bf7de10cfd9b3bb4dbd2eb4480aa05b7ccf08", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md", + "present": true, + "sha256": "4d4249613a1b3fc339f342c3854eda08bf1ec280c0681eb3b2551469b2da2b36", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe", + "Mapa de antiguidade de fornecedores por referência explícita e condição administrativa revista, faixas0/1–30/31–60/61–90/>90/desconhecido, valores exactos por moeda só da página/CSV, sem alterar saldo ou vencimento original" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "1d147f14826793f97605ea442c6a93612a55caf265311a86824611650dfc0005" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "e52fa90276e6251b38bb48f83a11ec2b11ac819ffa3c2239da9712e50ce66343" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "ff4b32f404cd575ddc134cf373ae6ded5d0abae8641b14b591c0f5a856437938" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Purchasing/PayableAge.php", + "present": true, + "sha256": "19f4a3994b679b74275e72e906251c8d32ee9950ebedfae07bd4002abe740a9e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_AGING_DELIVERY.md", + "present": true, + "sha256": "a0af96108702bbb687a655de744e78e01a3e7c98365244410eb291fcb7a5fafa", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página", + "Controlo administrativo QA de obrigação/prazo/responsável do mapa interno revisto, preparação13/revisão14, correcção/cancelamento, entrega apenas simulada com SHA verificado, histórico/recibo/SQL25/100/CSV; sem calendário legal ou submissão", + "Correspondências explícitas revistas15/16 e balanço/resultados internos parciais QA:19contas/nove pendentes, resultado congelado19linhas/SHA, história/recibo/CSV contextual, ligação exacta ao razão; capital e resultado completo não presumidos" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + }, + { + "file": "app/Modules/Services/FiscalObligations.php", + "present": true, + "sha256": "bcf24717cf5828fed440b8e86f29a5ed4d325d392000240b224d155954170d1d" + }, + { + "file": "app/Modules/Services/Http/FiscalObligationsController.php", + "present": true, + "sha256": "95b03b505e588333206766b862d0f0cff006b1af1a3dc0466370ac6a7518e097" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "resources/js/fiscal-obligations-view.js", + "present": true, + "sha256": "d3c1aafddac987de0a8e432746533da8832c140474f9d9d49055020f7c9e67dd" + }, + { + "file": "database/sql/fiscal_obligations.sql", + "present": true, + "sha256": "c68988342ca1ed5817485355844c2f502f5c2ca12954461c1555d5c5f2abf7fe" + }, + { + "file": "config/fiscal_obligations.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Accounting/InternalStatementsController.php", + "present": true, + "sha256": "b05990c7fdb195957808b63a77599eaaabed5c5eb435dd8e7dd13d662c22fd8d" + }, + { + "file": "app/Modules/Accounting/InternalStatementTotals.php", + "present": true, + "sha256": "f13f6f356100e49786869196c54343fd629f6a1d38fa81665eb768dae4731601" + }, + { + "file": "resources/js/InternalStatementsWorkspace.jsx", + "present": true, + "sha256": "2fadac08cd6162dd06d056bfc4fb00479e9a5d930fdde10c1a4b57507982e76e" + }, + { + "file": "resources/js/internal-statements-view.js", + "present": true, + "sha256": "f1f7b44f2dc1679e537d77522e0fc5ee84da23068ef3157677bc20a8409d3464" + }, + { + "file": "resources/js/statement-download.js", + "present": true, + "sha256": "74b3c132c288d807064276010ecb8ee1db54ac8d1649c5972a3e67dbc22f74d5" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "database/sql/internal_statements.sql", + "present": true, + "sha256": "2beccb0bb7d91f48e5a5e386c1e010fb7d95cbfa90aa35a68e28dc63ae6a6f6b" + }, + { + "file": "config/internal_statements.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md", + "present": true, + "sha256": "028e3172128435924dc9594b68b25560cb0b30495f27c522864a94a1f8cc9504", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md", + "present": true, + "sha256": "53a07e2c0517de5b6881584c598b2f1d1d5665422ac2f8f5932626822daf3359", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + }, + { + "id": "human_resources", + "label": "Recursos Humanos", + "features": [ + "Módulo autónomo com perfis QA próprios separados do Financeiro", + "Cadastro pseudónimo individual preparado e revisto", + "Regras artificiais versionadas com vigência e fonte explícitas", + "Processamento exacto por trabalhador/período com escolha de fontes e revisão distinta", + "Correcção/cancelamento versionados, prévia, histórico, recibos e recuperação", + "Mapa individual revisto conservado com SHA e exportação autenticada; filtros SQL25/100" + ], + "sources": [ + { + "file": "app/Modules/HumanResources/Remunerations.php", + "present": true, + "sha256": "e4837d6e8147f64cea09559ff28d6e15675d4944a2d96d673c4070b0efd8ca41" + }, + { + "file": "app/Modules/HumanResources/Http/RemunerationsController.php", + "present": true, + "sha256": "3725145a623791a6c2825d8aaa4f122ba7b80908c08603211d6d9e74df76e801" + }, + { + "file": "app/Shared/Installation/HumanResourcesProfiles.php", + "present": true, + "sha256": "f908b8cf9f7121bb9b43733f507771a60aa34eb553df8342c250e5b39940ae93" + }, + { + "file": "resources/js/RemunerationsWorkspace.jsx", + "present": true, + "sha256": "25e37e188467303c4a7101b0f64c5b56ef9e352014ce5089828c3d74e352773a" + }, + { + "file": "resources/js/statement-download.js", + "present": true, + "sha256": "74b3c132c288d807064276010ecb8ee1db54ac8d1649c5972a3e67dbc22f74d5" + } + ], + "existing_evidence": [ + { + "file": "docs/SGF_HUMAN_RESOURCES_DELIVERY.md", + "present": true, + "sha256": "9abec5254b56a6863f25fc26bc257a7152235a4e1d2e43a9db8a1621cb3d79a1", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-human-resources-20261111.json", + "present": true, + "sha256": "7c2104d708a95acd61ac5687c8f21045d368d39cc1aa3679495f7590053956ed", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Contratos/alterações contratuais, assiduidade, turnos, férias e ausências", + "Regimes, IRPS/INSS/modelos legais e ligação salarial a dívida/pagamentos/Contabilidade", + "Revisão visual, carga sustentada e capacidade de implantação" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v25-final-20261111.json b/docs/proofs/sgf-solution-structure-v25-final-20261111.json new file mode 100644 index 0000000..0a76d5f --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v25-final-20261111.json @@ -0,0 +1,1169 @@ +{ + "date": "2026-11-11", + "contract_sha256": "a341fc3b64a7cae053a4375f62bc2ad966c282a4396aa65c88d5cc3deda23dc6", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Cadastro/regras/processamento interno artificial QA em RH autónomo entregue; operações laborais gerais, cálculo legal IRPS/INSS/modelos e ligação salarial financeira/contabilística pendentes" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Controlo administrativo de prazo e entrega simulada de uma origem QA entregue; calendário legal, aplicabilidade geral, prazos por fonte/vigência e submissão continuam pendentes" + }, + { + "id": "bank_reconciliation", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Conferência de duas linhas/extracto artificial QA entregue; integração e reconciliação bancária gerais/reais pendentes" + }, + { + "id": "financial_analysis", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Dois critérios/indicadores QA com fontes conciliadas entregues; rácios gerais, critérios reais e tendências/séries permanecem pendentes" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Cadastro de artigos: preparação/revisão distintas, criação sem saldo, designação revista só dos novos, código/unidade imutáveis, histórico/recibos e catálogo canónico único" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "present": true, + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "present": true, + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "present": true, + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "present": true, + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "present": true, + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_CATALOGUE_DELIVERY.md", + "present": true, + "sha256": "43ce839f882aeeab48b272d5eb1a4e34fcd855052db88643a8ca7b08556631f8", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Ordens SQL25/100 por cliente/equipamento/responsável/estado, apontamentos actuais e minutos verificados, sem mutex de escrita na leitura", + "Relatório por período de abertura explícito/dia local, SQL25/100 antes de paginar, apontamentos actuais e CSV da página sem fecho histórico presumido", + "Equipamento escolhido → ordens exactas SQL25/100 com filtros, identificação actual, CSV da página e regresso ao catálogo" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Workshop/WorkshopOrderRead.php", + "present": true, + "sha256": "f741a5ac6c16376ea26b1bacd7b5e706b14a4548a26f59c4a96ec882fb6a6fa5" + }, + { + "file": "app/Modules/Workshop/Http/WorkshopOrderReadController.php", + "present": true, + "sha256": "f4dd027d01c0730603f3b312c2eb263740f6ebe1c969c762ed99d19520fab584" + }, + { + "file": "resources/js/WorkshopOrderList.jsx", + "present": true, + "sha256": "4cad148295acb7c462425fa300dccc7c203255f5f616163575e205bfc8a80e34" + }, + { + "file": "resources/js/workshop-report-view.js", + "present": true, + "sha256": "28125af02e25b4befb9914c5dee753dae4164100f3c0f94988c332a50021bf9f" + }, + { + "file": "app/Modules/Workshop/WorkshopEquipment.php", + "present": true, + "sha256": "37223b732865f273831d0bf61a464137cc53526a4c55750347450ba20bcbbc37" + }, + { + "file": "resources/js/WorkshopEquipmentWorkspace.jsx", + "present": true, + "sha256": "450af001d385f42cd4d0ae42b34fa6b373cdd6811d94f8f4a1f5de2c591ed843" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_PERIOD_REPORT_DELIVERY.md", + "present": true, + "sha256": "1455e3dc4203c81f49ddaba05a7bf7de10cfd9b3bb4dbd2eb4480aa05b7ccf08", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_EQUIPMENT_ORDERS_DELIVERY.md", + "present": true, + "sha256": "4d4249613a1b3fc339f342c3854eda08bf1ec280c0681eb3b2551469b2da2b36", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário", + "Extracto artificial QA e correspondências individuais com duas linhas nativas existentes, revisão distinta, conferência final e relatório", + "Critérios e indicadores QA950/100=9,5 vezes e50/1000=5%, revisão distinta, fontes conciliadas/retrato/fracção/histórico/relatório", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Posições actuais de clientes por documento, pesquisa/estado/vencimento, SQL25/100, saldos reconhecidos e CSV da página; referência de vencimento distinta de saldo histórico", + "Posições de fornecedores por factura/fornecedor e estado documental, SQL25/100, saldos do motor original, CSV da página e abertura exacta do documento; vencimento desconhecido sem atraso presumido", + "Condições administrativas QA de vencimento: preparação13/revisão14, evidência/fundamento, prévia/revalidação de origem/saldo/versão/pessoa, recusa/correcção, histórico25/100, recibo e recuperação; lista/CSV separam original e condição revista, referência explícita no detalhe", + "Mapa de antiguidade de fornecedores por referência explícita e condição administrativa revista, faixas0/1–30/31–60/61–90/>90/desconhecido, valores exactos por moeda só da página/CSV, sem alterar saldo ou vencimento original" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "6e654a03cb47e52dedd52ce6b2abf89a61d22860e4ffae92fd393027a54cc445" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + }, + { + "file": "app/Modules/Finance/BankReconciliation.php", + "present": true, + "sha256": "5421141bcc6e15bda142687661774238c7203c85d0ef30696fd4d56dd5b9e328" + }, + { + "file": "app/Modules/Finance/BankLedgerSource.php", + "present": true, + "sha256": "f33779fa736aea7027d4cf449ba85c5982bfb565c75bbb2a477d7a2666820338" + }, + { + "file": "app/Modules/Finance/Http/BankReconciliationController.php", + "present": true, + "sha256": "896d710db108364263a1404d2aafdc52eff4d3b6efcbe3dbdbcca1978befffe4" + }, + { + "file": "public/sgf-bank.mjs", + "present": true, + "sha256": "959214ad94bb5c29ffdaf1c8fd3e59bf8718837d4c1fe9bea52d3b04c261a80b" + }, + { + "file": "app/Modules/Finance/FinancialIndicators.php", + "present": true, + "sha256": "9785a801c5fb13b34770bdd05c828f5e924058eb872a7b33bc1db139a3024d10" + }, + { + "file": "app/Modules/Finance/FinancialIndicatorSource.php", + "present": true, + "sha256": "72039264cbb25ae2eaa5eaa4af488aabf7061a4a298c09490b8957b4ade55731" + }, + { + "file": "app/Modules/Finance/Http/FinancialIndicatorsController.php", + "present": true, + "sha256": "5245064e40b884e432afa2fd26d45fddccebf6963ef3366f585b93ee78f12cca" + }, + { + "file": "public/sgf-indicators.mjs", + "present": true, + "sha256": "62442edc14da534e0166cc65074389e74cc67246cd5922994628644fd900cbb9" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "app/Modules/Services/ReceivablePositionRead.php", + "present": true, + "sha256": "6aaf0211c4427643c84152edc6a7937dbf48387e2844d3bbe31328d1f6789f2d" + }, + { + "file": "app/Modules/Services/Http/ReceivablePositionReadController.php", + "present": true, + "sha256": "9e8be7c2cd328aebb31cbe0be4eeaaa6abc02d5bff0895aeb90c24ab8d134541" + }, + { + "file": "resources/js/ReceivablePositionsWorkspace.jsx", + "present": true, + "sha256": "5bd8ee952aeee87d733ab434cfe50b675d84c0341ade4f354c93e4acfdbe57e9" + }, + { + "file": "resources/js/receivable-position-view.js", + "present": true, + "sha256": "08f8cb6d17b79b89ee8cebe4723458c1a4979d8fb824afa0b585ae51076e6f21" + }, + { + "file": "app/Modules/Purchasing/PayablePositionReadController.php", + "present": true, + "sha256": "1d147f14826793f97605ea442c6a93612a55caf265311a86824611650dfc0005" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "14f8e440d8bd383913129586338521313952f2f14136a34871253dec0fa6b4b4" + }, + { + "file": "resources/js/PayablePositionsWorkspace.jsx", + "present": true, + "sha256": "e52fa90276e6251b38bb48f83a11ec2b11ac819ffa3c2239da9712e50ce66343" + }, + { + "file": "resources/js/payable-position-view.js", + "present": true, + "sha256": "ff4b32f404cd575ddc134cf373ae6ded5d0abae8641b14b591c0f5a856437938" + }, + { + "file": "app/Modules/Purchasing/PayableDueController.php", + "present": true, + "sha256": "39e4c95b910d3b1c4aa227c7c2aa0ee24399921b9578748b95ff5aa519f34db7" + }, + { + "file": "resources/js/PayableDueWorkspace.jsx", + "present": true, + "sha256": "c7127bbb2edc6c5acce57a55a6c3c5b94969170ccf1adb7236fee80d45f555ae" + }, + { + "file": "database/sql/payable_due.sql", + "present": true, + "sha256": "faefce9f2c11da613293e23bbc2f630bdae345d872cca7f3c2923c77fea0f75e" + }, + { + "file": "config/payable_due.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Purchasing/PayableAge.php", + "present": true, + "sha256": "19f4a3994b679b74275e72e906251c8d32ee9950ebedfae07bd4002abe740a9e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BANK_RECONCILIATION_DELIVERY.md", + "present": true, + "sha256": "acf53f7dfd2ec0f501cef8b1623e090c1e1b8a07a8591f4f042fb8a0b7b57273", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_INDICATORS_DELIVERY.md", + "present": true, + "sha256": "9902e37504dd9f5e1d3c73cd78a283e29be00ffb33ce13ca8f76577d1163cc65", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_RECEIVABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "7a829a6dcae28fa42e996874b7b65ed1de325dbab5bae12a28731ecfc2e849d7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_POSITIONS_DELIVERY.md", + "present": true, + "sha256": "2c0a25051540527e87ca9abfa43db5efb288e0b40e2315e06f6084693ce9c8c3", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_DUE_DELIVERY.md", + "present": true, + "sha256": "614ca3b1c432760e1911112951f138cf72d5d8370173336472529e9549e965b8", + "not_reexecuted": true + }, + { + "file": "docs/SGF_PAYABLE_AGING_DELIVERY.md", + "present": true, + "sha256": "a0af96108702bbb687a655de744e78e01a3e7c98365244410eb291fcb7a5fafa", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Conferência de extracto QA entregue; reconciliação bancária geral/real permanece fora do recorte" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada", + "Cálculo QA por1–25linhas sem/com imposto incluído, representação por linha aprovada, exactos/diferenças e mapa interno recuperável", + "Início operacional com percurso, evidências, funções autorizadas e consulta explícita das fontes existentes", + "Submenus distintos e balancete/razão por datas explícitas, escolha de conta por linha, páginas25/100, detecção de mudança de livro e CSV da página", + "Controlo administrativo QA de obrigação/prazo/responsável do mapa interno revisto, preparação13/revisão14, correcção/cancelamento, entrega apenas simulada com SHA verificado, histórico/recibo/SQL25/100/CSV; sem calendário legal ou submissão", + "Correspondências explícitas revistas15/16 e balanço/resultados internos parciais QA:19contas/nove pendentes, resultado congelado19linhas/SHA, história/recibo/CSV contextual, ligação exacta ao razão; capital e resultado completo não presumidos" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "b96749981b0d4daa7861eab62c5de60c96c5ffa9c72183f88bb89e2d4f7bdf2b" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "55f8b947bd63cffdc63f268a88ac441fda62e144d1a2013eaa954d57970fa716" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "520edda0a013d41a80e3e3b56a1b39597a4f1b6407d78118f8855277ffeb3548" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "cc7e05e18c4bbc98a70a19eabdd653bbb44a717214f8d8ee19cd2ffeb1c032c9" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "5c865e018c5b7d3b3f1232b80e81da379c729ea816a9e08a73225e76a678502c" + }, + { + "file": "app/Modules/Services/FiscalDocumentMath.php", + "present": true, + "sha256": "6e67b7b1363d24b38ca0dd40c4349dba82a4b746965f0af8aa7d65bb4f8f731d" + }, + { + "file": "resources/js/WorkspaceHome.jsx", + "present": true, + "sha256": "32bf2871a52ad5a5632aec4c2eb74207f49a8765d351ebfa5c14b475d54ebd4f" + }, + { + "file": "resources/js/workspace-overview.js", + "present": true, + "sha256": "92064f89b5da89782803ac62be2468134f94aa580fe11c7f6ceb6fba1fbd2dcb" + }, + { + "file": "resources/js/ComposedAccountingWorkspace.jsx", + "present": true, + "sha256": "9288df122be42e436bb4aeffc3e84cc1687ba82ab4cab07754d27d507d0012ac" + }, + { + "file": "resources/js/LedgerReportsWorkspace.jsx", + "present": true, + "sha256": "312157b80599e0f4407520ec968460af9c31913f79365ef6efa9a243c98ba419" + }, + { + "file": "resources/js/ledger-report-view.js", + "present": true, + "sha256": "54a1eb09ab4b451b4aa34d8b0c1ebc3d8cea91da4f242ea25a0c4b3bb1db79b4" + }, + { + "file": "app/Modules/Services/FiscalObligations.php", + "present": true, + "sha256": "bcf24717cf5828fed440b8e86f29a5ed4d325d392000240b224d155954170d1d" + }, + { + "file": "app/Modules/Services/Http/FiscalObligationsController.php", + "present": true, + "sha256": "95b03b505e588333206766b862d0f0cff006b1af1a3dc0466370ac6a7518e097" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "resources/js/fiscal-obligations-view.js", + "present": true, + "sha256": "d3c1aafddac987de0a8e432746533da8832c140474f9d9d49055020f7c9e67dd" + }, + { + "file": "database/sql/fiscal_obligations.sql", + "present": true, + "sha256": "c68988342ca1ed5817485355844c2f502f5c2ca12954461c1555d5c5f2abf7fe" + }, + { + "file": "config/fiscal_obligations.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + }, + { + "file": "app/Modules/Accounting/InternalStatementsController.php", + "present": true, + "sha256": "b05990c7fdb195957808b63a77599eaaabed5c5eb435dd8e7dd13d662c22fd8d" + }, + { + "file": "app/Modules/Accounting/InternalStatementTotals.php", + "present": true, + "sha256": "f13f6f356100e49786869196c54343fd629f6a1d38fa81665eb768dae4731601" + }, + { + "file": "resources/js/InternalStatementsWorkspace.jsx", + "present": true, + "sha256": "2fadac08cd6162dd06d056bfc4fb00479e9a5d930fdde10c1a4b57507982e76e" + }, + { + "file": "resources/js/internal-statements-view.js", + "present": true, + "sha256": "f1f7b44f2dc1679e537d77522e0fc5ee84da23068ef3157677bc20a8409d3464" + }, + { + "file": "resources/js/statement-download.js", + "present": true, + "sha256": "74b3c132c288d807064276010ecb8ee1db54ac8d1649c5972a3e67dbc22f74d5" + }, + { + "file": "resources/js/FiscalObligationsWorkspace.jsx", + "present": true, + "sha256": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90" + }, + { + "file": "database/sql/internal_statements.sql", + "present": true, + "sha256": "2beccb0bb7d91f48e5a5e386c1e010fb7d95cbfa90aa35a68e28dc63ae6a6f6b" + }, + { + "file": "config/internal_statements.php", + "present": true, + "sha256": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + }, + { + "file": "docs/SGF_MULTI_LINE_INCLUDED_DELIVERY.md", + "present": true, + "sha256": "7b1d6bbdea15845cf7f4fe104057a4053be1c0f0edb2186da26d11dded45b690", + "not_reexecuted": true + }, + { + "file": "docs/SGF_OPERATIONAL_INFORMATION_DELIVERY.md", + "present": true, + "sha256": "a4c82f636f5c78874a6cc89bbc42e115c8cc48df0fb957f4a8bc81472d9738f0", + "not_reexecuted": true + }, + { + "file": "docs/SGF_ACCOUNTING_REPORTS_DELIVERY.md", + "present": true, + "sha256": "f910d6d11fd7f4e18a675fea9f07a50137bbed72d4718bb7ec6e022bdd1959e5", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_OBLIGATIONS_DELIVERY.md", + "present": true, + "sha256": "028e3172128435924dc9594b68b25560cb0b30495f27c522864a94a1f8cc9504", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FINANCIAL_STATEMENTS_DELIVERY.md", + "present": true, + "sha256": "53a07e2c0517de5b6881584c598b2f1d1d5665422ac2f8f5932626822daf3359", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + }, + { + "id": "human_resources", + "label": "Recursos Humanos", + "features": [ + "Módulo autónomo com perfis QA próprios separados do Financeiro", + "Cadastro pseudónimo individual preparado e revisto", + "Regras artificiais versionadas com vigência e fonte explícitas", + "Processamento exacto por trabalhador/período com escolha de fontes e revisão distinta", + "Correcção/cancelamento versionados, prévia, histórico, recibos e recuperação", + "Mapa individual revisto conservado com SHA e exportação autenticada; filtros SQL25/100" + ], + "sources": [ + { + "file": "app/Modules/HumanResources/Remunerations.php", + "present": true, + "sha256": "e4837d6e8147f64cea09559ff28d6e15675d4944a2d96d673c4070b0efd8ca41" + }, + { + "file": "app/Modules/HumanResources/Http/RemunerationsController.php", + "present": true, + "sha256": "3725145a623791a6c2825d8aaa4f122ba7b80908c08603211d6d9e74df76e801" + }, + { + "file": "app/Shared/Installation/HumanResourcesProfiles.php", + "present": true, + "sha256": "f908b8cf9f7121bb9b43733f507771a60aa34eb553df8342c250e5b39940ae93" + }, + { + "file": "resources/js/RemunerationsWorkspace.jsx", + "present": true, + "sha256": "25e37e188467303c4a7101b0f64c5b56ef9e352014ce5089828c3d74e352773a" + }, + { + "file": "resources/js/statement-download.js", + "present": true, + "sha256": "74b3c132c288d807064276010ecb8ee1db54ac8d1649c5972a3e67dbc22f74d5" + } + ], + "existing_evidence": [ + { + "file": "docs/SGF_HUMAN_RESOURCES_DELIVERY.md", + "present": true, + "sha256": "1494d29938565bb6f5d552385d7b32dd0a8f22856755bf638d81cf9a41a65f60", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-human-resources-20261111.json", + "present": true, + "sha256": "95d0c3fcd407cfbb03226288b94570478b906fdf3cb50c26a9960956f536cf29", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Contratos/alterações contratuais, assiduidade, turnos, férias e ausências", + "Regimes, IRPS/INSS/modelos legais e ligação salarial a dívida/pagamentos/Contabilidade", + "Revisão visual, carga sustentada e capacidade de implantação" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v6-20261110.json b/docs/proofs/sgf-solution-structure-v6-20261110.json new file mode 100644 index 0000000..2cf0e8d --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v6-20261110.json @@ -0,0 +1,542 @@ +{ + "date": "2026-11-10", + "contract_sha256": "04d29d2239fc2e0a60b4f330c51b6d7d872a31076bbe70c67c316ecadad898e5", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "4985076af1c9a78a7ebaa7fa59f57ec9fea68b8b9f47c2c8db446729c2cd160e", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v7-20261110.json b/docs/proofs/sgf-solution-structure-v7-20261110.json new file mode 100644 index 0000000..1be61a2 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v7-20261110.json @@ -0,0 +1,613 @@ +{ + "date": "2026-11-10", + "contract_sha256": "85917791b0247724c0b4ccc654b59f2fad9c0c40fd12c71a4f6d0f4f7f2a9b2c", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IVA: livros de apuramento/dedução/regularizações e cobertura além do caso delimitado" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Reconciliação bancária operacional" + }, + { + "id": "financial_analysis", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Rácios e tendências conciliados com critérios aprovados" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "239c23697dcfb306fddfb1bc9b92340cfcd5f5033916bdb11f611bf23bad0eac" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v8-20261110.json b/docs/proofs/sgf-solution-structure-v8-20261110.json new file mode 100644 index 0000000..709de24 --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v8-20261110.json @@ -0,0 +1,635 @@ +{ + "date": "2026-11-10", + "contract_sha256": "a55bcfd4b9e17c4f7ebd77a41b8d2016c8e5d4d2497d38bd9571a8e6d986aad8", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Reconciliação bancária operacional" + }, + { + "id": "financial_analysis", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Rácios e tendências conciliados com critérios aprovados" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "24108c35d52ba68797af655a2f6e824401a51865820f81ba7afa256b29db652b" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-solution-structure-v9-20261110.json b/docs/proofs/sgf-solution-structure-v9-20261110.json new file mode 100644 index 0000000..930941c --- /dev/null +++ b/docs/proofs/sgf-solution-structure-v9-20261110.json @@ -0,0 +1,662 @@ +{ + "date": "2026-11-10", + "contract_sha256": "96bbc4af18835db9533c0b3babc23dd81aa1e22dfc2654dc629725fc6fd099d3", + "installation": "cc50a276c10c", + "port": 8085, + "sgc_dependency": false, + "full_product_complete": false, + "product_backlog": [ + { + "id": "fiscal_books", + "status": "partial", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Livro/mapa interno QA de uma origem entregue; IVA geral, dedução/regularizações e restantes tratamentos pendentes" + }, + { + "id": "irps_inss", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPS, remunerações e INSS operacionais" + }, + { + "id": "irpc", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "IRPC operacional: ajustamentos, retenções, apuramento" + }, + { + "id": "ispc_ice", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Motores operacionais ISPC e ICE" + }, + { + "id": "official_forms", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Modelos oficiais com correspondências e reconciliação de campos" + }, + { + "id": "fiscal_calendar", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Calendário legal e outras obrigações por fonte/vigência" + }, + { + "id": "bank_reconciliation", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Reconciliação bancária operacional" + }, + { + "id": "financial_analysis", + "status": "pending", + "implemented": false, + "basis": "docs/SERVICE_INVOICE_CALCULATION_SPEC.md sections10/11", + "description": "Rácios e tendências conciliados com critérios aprovados" + } + ], + "modules": [ + { + "id": "commercial", + "label": "Comercial", + "features": [ + "Clientes e serviços com associação explícita", + "Rascunho, revisão e cálculo", + "Documento emitido de teste e PDF", + "Continuação para recebível sem duplicação", + "Orçamentos Comercial/Oficina, revisão e aceitação documentadas e conversão explícita em rascunho", + "Recibo de afectação efectiva400, crédito nominal100/saldo500 e factura-recibo1000 atómica", + "PDF/SHA, histórico e anulação técnica com reversão explícita e diário único", + "Registo unificado autorizado com seis famílias, filtros/SQL25/100 e projecções limitadas à página" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceWorkflow.php", + "present": true, + "sha256": "6ea92e6216bec9a0062d9f152d1b758a5527360456589a98f525429402cb94da" + }, + { + "file": "app/Modules/Services/ServiceInvoicing.php", + "present": true, + "sha256": "4138afb7e001b546a65552d71223cd642154945b8021e1ff45ef60a34076e3c5" + }, + { + "file": "resources/js/ServicesWorkspace.jsx", + "present": true, + "sha256": "c170b195ccd78aa74f283a91b0b4f4cd93aea4677e3aaefe0b50af46f7e31669" + }, + { + "file": "resources/js/ServiceInvoiceWorkspace.jsx", + "present": true, + "sha256": "8f95d1775b30a6de73d65eed428595d74cc837755d8157bf8db49a1151350277" + }, + { + "file": "app/Modules/Services/ServiceDocuments.php", + "present": true, + "sha256": "816a85be48b05043f41f9c7d5f842a3fa139b21aaedf9c944d811f74be6f6208" + }, + { + "file": "app/Modules/Services/DocumentPeriod.php", + "present": true, + "sha256": "100c6cf038b925126ddc8fb50adde2f6d938b4d68df6d0ab193f16ea28836ae2" + }, + { + "file": "app/Modules/Services/BillingRegister.php", + "present": true, + "sha256": "115e7832b63e5c29be716140629845137d951da7790421832e1ab4058fb468cc" + }, + { + "file": "resources/js/BillingRegisterWorkspace.jsx", + "present": true, + "sha256": "9ab8d036c9e6d20c588427f5a8c44677810e9713722baea908b8f6b76c8b79f9" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/invoice-test-results.json", + "present": true, + "sha256": "fd04a58727ffddf1c62ac184d7834b81228dce2abb5b0c1e397505df6a45237c", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_SERVICE_DOCUMENTS_DELIVERY.md", + "present": true, + "sha256": "a13713f05feaa84766dfaba870e72c98d23aa6223389d1627de911b0856cd15d", + "not_reexecuted": true + }, + { + "file": "docs/SGF_BILLING_REGISTER_DELIVERY.md", + "present": true, + "sha256": "fec1b76003187d5f38c8aca8804c26129e312e517dd5b2247f54f0019c2cccf6", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual, aceitação explícita e conformidade real; emissão fiscal real permanece bloqueada", + "Carga sustentada, armazenamento e capacidade de implantação exigem evidências próprias" + ] + }, + { + "id": "purchasing", + "label": "Compras", + "features": [ + "Fornecedores e revisão distinta", + "Requisição até adjudicação e ordem", + "Recepção, documento e stock único", + "Factura conferida e obrigação reconhecida" + ], + "sources": [ + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "present": true, + "sha256": "8770340b1a5c6ec39b3ccb306b38e9c699d68db33d53237423cf631e512b6439" + }, + { + "file": "app/Modules/Purchasing/RequestController.php", + "present": true, + "sha256": "c5c2b78866be7f0e1c8ba09c1af538cefcfbafbea81197833b50821bba0fcbca" + }, + { + "file": "app/Modules/Purchasing/InvoiceController.php", + "present": true, + "sha256": "85eb46ca7e65c60bed190e592a134eb48db5762c0a90b8cd4c9d6fbd81198ba4" + }, + { + "file": "app/Modules/Purchasing/ObligationController.php", + "present": true, + "sha256": "347e629306d6c1f4873afac547d13fae8ade7b268906d1312a64e7eb6998e701" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-final-20261104-r2.json", + "present": true, + "sha256": "3b63f8653e8dbb089dfdd8f389e271c022a070d5a67a9f9d4585ce84438070cc", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ], + "accepted_component_journey": true + }, + { + "id": "stock", + "label": "Stock", + "features": [ + "Artigos, armazéns e saldos físicos", + "Entradas, saídas e origem autorizada", + "Transferência e conferência de destino", + "Contagens, ajustamentos e devoluções", + "Movimentos e relatórios por período", + "Política QA revista e devolução ao fornecedor por origem exacta; saldo8UN/160MZN no mesmo diário", + "Inactividade de pares com história conhecida, corte UTC explícito, filtros e SQL25/100 sem inferir ausência de actividade" + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockReadController.php", + "present": true, + "sha256": "fd02da1a69f76bf1412f86f43095a0a476de49fbb7d2676a51fe3b29ec66e298" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "present": true, + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "app/Modules/Inventory/StockPeriodReport.php", + "present": true, + "sha256": "d491541c6c60bc0c661dc442b8db8ce2e757f1d63dfc4982a3631f820cfb7a06" + }, + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "app/Modules/Inventory/StockOperationsController.php", + "present": true, + "sha256": "34d6c7be4787f4f18ca2a7aa322f6f33dfb281e22336599234c93c655ab4eebf" + }, + { + "file": "resources/views/stock-operations-workspace.blade.php", + "present": true, + "sha256": "5db67c2d23e5557548c38c82a0d7f6e373651849727b2237dcd73f620309c25a" + }, + { + "file": "public/stock-operations-workspace.mjs", + "present": true, + "sha256": "56734499e8dd89da293c2edadd0d0e10ac6d06b39f0365e5ae29a8d14e9176d8" + }, + { + "file": "app/Modules/Inventory/StockFinalController.php", + "present": true, + "sha256": "9d9a242fd8e143fe076ece0454d0789eabf543e37b332db4806d905fad13cdda" + }, + { + "file": "resources/views/stock-final-workspace.blade.php", + "present": true, + "sha256": "93764471a215735a65e5c07121989b1e2f6499f631a11b9f1177bb0fe8def232" + }, + { + "file": "public/sgf-stock-final.mjs", + "present": true, + "sha256": "77860b080015528fe4dd84b2e71787ee668da9be17653c64a5d8e64539b5e592" + }, + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "present": true, + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "present": true, + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/stock-period-report-20261103.json", + "present": true, + "sha256": "0ab25af6ffbff7d8ecb651df833771b082bf92d52f97eca4278fcac3fd5ea53b", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-stock-operations-20261110.json", + "present": true, + "sha256": "1d241fcfb81fcf6c1b374f9fc89c1f2130b17eee1314f06b4b8e4b59c476fc08", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_POLICIES_SUPPLIER_RETURN.md", + "present": true, + "sha256": "b8848090f1e19a6b7c19b684e07c0c95cafcf93bd1551357f6afc314f4413d8b", + "not_reexecuted": true + }, + { + "file": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "present": true, + "sha256": "8773d53140b291c01358b8e71c584373cd0e440f67d0107e64ecea13980990d3", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Revisão visual dos novos relatórios e aceitação explícita do conjunto; carga sustentada e implantação separadas" + ] + }, + { + "id": "workshop", + "label": "Oficinas", + "features": [ + "Ordem e autorização", + "Apontamentos e revisão distinta", + "Material consumido ligado ao movimento físico", + "Custo directo/industrial e correcção", + "Continuação comercial por documento de origem", + "Agenda sem sobreposição, reagendamento e cancelamento versionados", + "Cadastro independente e reutilização de equipamento por cliente" + ], + "sources": [ + { + "file": "app/Modules/Workshop/WorkshopStock.php", + "present": true, + "sha256": "c32024ab178a3db29881902199df4e313be34dd770fc14d55a537656bf6f8fa6" + }, + { + "file": "resources/js/WorkshopWorkspace.jsx", + "present": true, + "sha256": "a1ceca33c234a759b2abf13b087bba8d18d70187ad9161e59425a5932771f8bb" + }, + { + "file": "resources/js/DirectCostsWorkspace.jsx", + "present": true, + "sha256": "dcaf72fe36d80e16bab8e962f939168650336d1450fc3eb01693acb7ef7e0743" + }, + { + "file": "resources/js/IndustrialCostsWorkspace.jsx", + "present": true, + "sha256": "51a3307fa8b2526efa12d8a16fee613bdfe141d02bd6e5af4535d581ed69c6d0" + }, + { + "file": "resources/js/WorkshopBillingWorkspace.jsx", + "present": true, + "sha256": "601853bd383ef2d1891158e89578bbc7f26ae1c7220bc11ddfadf1868cb63afc" + }, + { + "file": "app/Modules/Workshop/ComposedWorkshopStockBook.php", + "present": true, + "sha256": "01e7392827a6453a357a0b5140905757bbee76f57fae6aa9906490788690e24c" + }, + { + "file": "app/Modules/Workshop/WorkshopBilling.php", + "present": true, + "sha256": "0eac2d91fd9f006aa8c198e3652731142aee478c93de090c408df54cdd9d292d" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/workshop-stock-closure-20261102.json", + "present": true, + "sha256": "261d5502f6905d53ef8046e0e900c9bee159c8c9d7e1bae4fddd8ec698313bf7", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_WORKSHOP_AGENDA_EQUIPMENT.md", + "present": true, + "sha256": "3a44a1b6ac5148013c815cc6cc8919f08c15ac911f900aac5b4a0af903589481", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "finance", + "label": "Financeiro", + "features": [ + "Obrigações a fornecedores", + "Recebíveis de clientes", + "Liquidação/afectação e reversão sintéticas", + "Posição financeira e comprovativos por origem", + "Caixa QA explícita0+1000-50=950, revisão distinta, contagem e fecho sem reabertura", + "Extracto paginado, recuperação de confirmação e classificação/despesa no mesmo diário" + ], + "sources": [ + { + "file": "app/Modules/Services/ServiceReceivables.php", + "present": true, + "sha256": "79e1669b2423c521c0c56ab6fb07e085daef88eff8a41748c424e4a827f1e453" + }, + { + "file": "app/Modules/Purchasing/SupplierPayables.php", + "present": true, + "sha256": "c17d44391cbd96941aa2dd2eac2cb167508faf6453c6a1e86333846df1ba18b9" + }, + { + "file": "resources/js/ServiceReceivablesWorkspace.jsx", + "present": true, + "sha256": "f5207e5f16a4477034185d0cdeb79adc8e462cbf04d5e3fd7e633b8a5433b0ba" + }, + { + "file": "app/Modules/Purchasing/ComposedFinance.php", + "present": true, + "sha256": "ad7e837c7608ab85c59eb169bcdd986b08c5802a0f218926e1abd7568e8a4b16" + }, + { + "file": "app/Modules/Purchasing/ComposedFinanceController.php", + "present": true, + "sha256": "b417a6dc7d05f7b5ea1c8c043dcf647cb4ba5e09855d0030affa232728fce5b5" + }, + { + "file": "app/Modules/Finance/CashWorkspace.php", + "present": true, + "sha256": "bb65060461cea3a844cf523733644cdd630f35ba02c82aeed961821ec5970800" + }, + { + "file": "app/Modules/Finance/Http/CashController.php", + "present": true, + "sha256": "982c161f3ef7314a9671f848f0da2aefb27841e40003ef47102d0d956c08cd78" + }, + { + "file": "app/Modules/Accounting/CashLedgerSource.php", + "present": true, + "sha256": "3a46162f0a62363dd4015f0047a5bff95e8aebd8dd3007fc705daaf58c10684b" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/supplier-settlements.json", + "present": true, + "sha256": "b1bce9218f99ea2d8c780022ad385e169c7acc5778ac6950236eef0630f77ad0", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_CASH_DELIVERY.md", + "present": true, + "sha256": "bfb40ff8849eb4d9fcb641aa9b8d07bbaab89326a0f89b8f113cf0bf62653093", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md" + ] + }, + { + "id": "accounting", + "label": "Contabilidade", + "features": [ + "Plano e classificação", + "Diário com preparação/revisão distintas", + "Razão e balancete", + "Estorno/correcção e conferência de período", + "Fecho sintético com rastreabilidade de origens", + "Livro de saídas e mapa interno IVA QA da origem FR seleccionada, revisão distinta, histórico e CSV congelado", + "Catálogo de fontes/regras versionadas QA, revisão distinta, vigência/conflitos/retirada e cálculo explicável com história preservada" + ], + "sources": [ + { + "file": "app/Modules/Accounting/GeneralLedger.php", + "present": true, + "sha256": "a87d2ea373bc61bb139aa29f2a354b9813bbb2d6229a389f8d37fd8e9890edec" + }, + { + "file": "app/Modules/Accounting/GeneralLedgerController.php", + "present": true, + "sha256": "afd2f4ae55e694f0aff18233a6f5cf52444e185aaf5a159865f74f0243783f2a" + }, + { + "file": "packages/general-ledger/src/LedgerSchema.php", + "present": true, + "sha256": "d9c203286b3a04121c265dffe412d2e86726ec4bb9222a2ed654d0296d86b5d8" + }, + { + "file": "app/Modules/Accounting/ComposedLedger.php", + "present": true, + "sha256": "2e4dfa133831c78514e1010d373db905b892c315c455ba53b997edd514cac035" + }, + { + "file": "app/Modules/Accounting/ComposedLedgerController.php", + "present": true, + "sha256": "136844950032de0f66cce26adc9a75a7092ffac52025c236fc22c94b23e9b1ce" + }, + { + "file": "packages/general-ledger/src/StockTransitEvidence.php", + "present": true, + "sha256": "eb8eee3bb293e77689eb6470ac8aa12049b2ff8cfb8694d3e15eb17e097d2da6" + }, + { + "file": "app/Modules/Services/FiscalCoverage.php", + "present": true, + "sha256": "24108c35d52ba68797af655a2f6e824401a51865820f81ba7afa256b29db652b" + }, + { + "file": "app/Modules/Services/FiscalAssessment.php", + "present": true, + "sha256": "c2dbe192e6bba75f6fc4057bbb51c85bcf024a674cfeb7950ba239e700213128" + }, + { + "file": "app/Modules/Services/Http/FiscalAssessmentController.php", + "present": true, + "sha256": "425278110af20393e71de446f9b0ce41408352216249e5055de4165b075631e4" + }, + { + "file": "public/sgf-iva.mjs", + "present": true, + "sha256": "1d8c2a07f606799257c090ab2ddb52c309553a15009d6fb457dbbf4770c51ee6" + }, + { + "file": "app/Modules/Services/FiscalRules.php", + "present": true, + "sha256": "e7ead66818df2ba644b3b22d35707637ae05dcfcfbe28e57c0a3ed61b05180db" + }, + { + "file": "app/Modules/Services/FiscalRuleExpression.php", + "present": true, + "sha256": "108d8401c9c2bc85b3e5f663322c3d36c267afd8529b370e7e16c1d565a3daef" + }, + { + "file": "app/Modules/Services/Http/FiscalRulesController.php", + "present": true, + "sha256": "ad648825ec55b3bdb8eca038a01c5bd685b8b62b23f79bfd7771282506e1f66a" + }, + { + "file": "public/sgf-fiscal-rules.mjs", + "present": true, + "sha256": "a14a6d71d206b64445990a197e30387acb5882f1331ac13c2f283d7de0cb2477" + } + ], + "existing_evidence": [ + { + "file": "docs/proofs/purchasing-completion-matrix-final-20261104.json", + "present": true, + "sha256": "94cb37698f294c38df9b94a5535ba4b9ac63907f94dab1e7f951a3bb27623083", + "not_reexecuted": true + }, + { + "file": "docs/proofs/sgf-six-module-functional-review-20261110.json", + "present": true, + "sha256": "64e49bf8681f7555bbff1ccfcb12ad8f42056833c964f3a3a1354ba8160abed7", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_COVERAGE_DELIVERY.md", + "present": true, + "sha256": "4951a2514253c71710fd18ddbffcec1c4dca597355b1cf998b1c7f1b38078506", + "not_reexecuted": true + }, + { + "file": "docs/SGF_IVA_ASSESSMENT_DELIVERY.md", + "present": true, + "sha256": "b0a3ae64699352242108d782a229f0250344f238fad02e14e6fa4dbb520e997f", + "not_reexecuted": true + }, + { + "file": "docs/SGF_FISCAL_RULES_COMMON_DELIVERY.md", + "present": true, + "sha256": "10f8801d59dbdeb3f075e5bf2650e4538097a92151db516e3c0357ac011c8a5a", + "not_reexecuted": true + } + ], + "missing_host_tables": [], + "missing_native_tables": [], + "integrated_release_verified": false, + "client_presentable": false, + "remaining": [ + "Conferência visual restante e aceitação explícita do conjunto; ver SGF_SIX_MODULE_CLOSURE_20261110.md", + "Backlog fiscal/financeiro amplo pendente; tabela de cobertura não implementa motores ou modelos" + ] + } + ], + "release_gates": [ + { + "id": "single_installation", + "criterion": "Os seis módulos operam no mesmo endereço e empresa/unidade, sem mudar de demonstração", + "passed": false + }, + { + "id": "functional_modules", + "criterion": "Cada funcionalidade obrigatória tem ecrã/comando/consulta executável; menus e fontes isoladas não bastam", + "passed": false + }, + { + "id": "cross_module_flows", + "criterion": "Os documentos atravessam as fronteiras mantendo origem, versão, autoridade e efeito único", + "passed": false + }, + { + "id": "access_isolation", + "criterion": "Perfis explícitos e recusas de empresa/unidade/módulo/permissão verificadas", + "passed": false + }, + { + "id": "presentation", + "criterion": "Percurso de apresentação coerente, dados fictícios identificados, linguagem e navegação revistas", + "passed": false + }, + { + "id": "operations", + "criterion": "Arranque e recuperação da instalação entregues e ensaiados, sem depender do SGC", + "passed": false + }, + { + "id": "commercial_honesty", + "criterion": "Documentos de teste não são emissão fiscal; capacidade de implantação exige prova própria", + "passed": false + }, + { + "id": "preservation", + "criterion": "Seis registos, cache, dados aprovados e trabalho alheio preservados", + "passed": false + } + ], + "client_presentable": false, + "limits": [ + "Host-table footprint is a minimum projection, not a full migration/dependency validation.", + "Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.", + "Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready." + ], + "database_writes": 0, + "application_bootstraps": 0, + "cache_cleared": false, + "new_demo": false, + "new_rights": false +} diff --git a/docs/proofs/sgf-statements-20261111.json b/docs/proofs/sgf-statements-20261111.json new file mode 100644 index 0000000..8a5df77 --- /dev/null +++ b/docs/proofs/sgf-statements-20261111.json @@ -0,0 +1,75 @@ +{ + "version": "SGF-INTERNAL-STATEMENTS-QA-v1", + "passed": true, + "target": "existing8085", + "core_checks": 152, + "compatibility_download_checks": 5, + "existing_fiscal_ui_checks_reused": 22, + "preservation_acl_checks": 65, + "new_right_names": 2, + "qa_grants_per_book": 2, + "total_qa_grants": 4, + "administrative_commands_8085": 4, + "mapping_versions": 2, + "result_versions": 2, + "frozen_results": 1, + "frozen_lines": 19, + "unclassified_accounts": 9, + "new_accounts": 0, + "new_journals": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false, + "evidence": { + "regression": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-055742-599ba3f2/statements-result.json", + "boundaries": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-060256-39e5bc78/statements-boundaries.json", + "snapshot_failure": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-060706-6e84beb9/statements-snapshot-failure.json", + "concurrency": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/statements-concurrency-1794369694260/result.json", + "ui": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/statements-ui-20261111.json", + "http": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/statements-http-20261111.json", + "download": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/statements-download-proof-20261111.json", + "final_http": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/statements-final-http-20261111.json", + "fiscal_map_download": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/fiscal-map-download-proof-20261111.json", + "fiscal_compatibility_ui": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/fiscal-obligations-download-ui-20261111.json", + "regression_setup": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-055508-e724dbbc/statements-setup.json", + "selected_setup": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-060417-cb04ea5b/statements-setup.json", + "installation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-073116-5f71ef3d/statements-installation.json", + "preservation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-072958-f49bf78c/statements-preservation.json" + }, + "evidence_sha256": { + "regression": "e1365012a16612317b4418a24fcb0beae9951c59055fe89108d405b61965174c", + "boundaries": "76d8801a1214251589c2b98167669d4b506af03c4ee9fa1908409b4a714866c3", + "snapshot_failure": "d0631024c01969a76fdcb6d9f15f47ce4150b1fdd97d714a4323f3d506ff8f3b", + "concurrency": "95398de71d0e41787afec9c1ea826dc59b78ca136d97302322245139bcf4fc5e", + "ui": "230886a2eb740659c26930ade078cc835ddd101c96ac474b8f587475382b7800", + "http": "89ec8432aa0d8e5b5169409beece92f608e19652442cd2a24b54440988c2e6dd", + "download": "74a83c10c9d69aa3f3bc2b9ac88f14b272fef4f3c8edcee5be75c5e791d36ee8", + "final_http": "8c9d41dad651d7e0e6cac19f62b6cd93bdaa03b8c44aeef0aa56abb93805b748", + "fiscal_map_download": "3989c5d1962546371ba91a63e2523e75aa1845dda10f6dde28864f3e0d71ad25", + "fiscal_compatibility_ui": "d46b52080dcf9bd6ad9cf18cbb0a5509c3177011bed690c97e0a0551bb4ec94d", + "regression_setup": "5fb1c4a4388bb786bd08478d10aca7a54362e427ee1b0b738a6ed33f8b1e1e3f", + "selected_setup": "1853a8ab4bcaa0011826f7b45962ee662abcb4e123799dd4853570b8afc2d74c", + "installation": "4d5bebc25e66b20675c45ed38d02b832a4267a8f144a5a95d3d75bdbe9842d62", + "preservation": "d7d77fc88c2c9525d87be5912c96018f0c51bd4b2772a9e2a0300ba2d606d0a6" + }, + "sources": [ + "app/Modules/Accounting/InternalStatementsController.php", + "app/Modules/Accounting/InternalStatementTotals.php", + "resources/js/InternalStatementsWorkspace.jsx", + "resources/js/internal-statements-view.js", + "resources/js/statement-download.js", + "resources/js/FiscalObligationsWorkspace.jsx", + "database/sql/internal_statements.sql", + "config/internal_statements.php" + ], + "source_sha256": { + "app/Modules/Accounting/InternalStatementsController.php": "b05990c7fdb195957808b63a77599eaaabed5c5eb435dd8e7dd13d662c22fd8d", + "app/Modules/Accounting/InternalStatementTotals.php": "f13f6f356100e49786869196c54343fd629f6a1d38fa81665eb768dae4731601", + "resources/js/InternalStatementsWorkspace.jsx": "2fadac08cd6162dd06d056bfc4fb00479e9a5d930fdde10c1a4b57507982e76e", + "resources/js/internal-statements-view.js": "f1f7b44f2dc1679e537d77522e0fc5ee84da23068ef3157677bc20a8409d3464", + "resources/js/statement-download.js": "56e81a93af939e7b04f76347912af1093110afe48dd69549287561149b3999a0", + "resources/js/FiscalObligationsWorkspace.jsx": "77e6741581305739000e6ac4923f350464338d93346a860bf840ff08d7740b90", + "database/sql/internal_statements.sql": "2beccb0bb7d91f48e5a5e386c1e010fb7d95cbfa90aa35a68e28dc63ae6a6f6b", + "config/internal_statements.php": "39fc036c111c443ee2bc8fac28d94dde055083b2abfb955d5aa9299b90b26cdb" + } +} diff --git a/docs/proofs/sgf-stock-catalogue-20261110.json b/docs/proofs/sgf-stock-catalogue-20261110.json new file mode 100644 index 0000000..bf4385c --- /dev/null +++ b/docs/proofs/sgf-stock-catalogue-20261110.json @@ -0,0 +1,173 @@ +{ + "passed": true, + "date": "2026-11-10", + "installation": "cc50a276c10c", + "port": 8085, + "checks": { + "regression": 34, + "boundaries": 11, + "http": 22, + "dom_http": 15, + "navigation": 3, + "served_checksums": 2, + "asset_routing": 17, + "asset_http": 13 + }, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-213243-02cebb42\\stock-catalogue-result.json", + "sha256": "3d04772edfba1dd4565451c60541c6d0a37f2db0db2754bbcc31a181ba4a8104", + "checks": 34, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-213644-443f1ddd\\stock-catalogue-boundaries.json", + "sha256": "c7aadf52b0d264e0a75331d91f0a3ad05ebfac4ee62fa7dd7e38747896265d14", + "checks": 11, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\catalogue-concurrency-1794339220350\\result.json", + "sha256": "0b55a713be701fd5c565aa89991c03cfc0f937799ad8e13c6b365d0c067b33a3", + "checks": 0, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-213118-57514c10\\stock-catalogue-setup.json", + "sha256": "48be6ee19c9ef408f4f7fc22c9e333867ac54987ccc70ff8eb7ddecb8fd91b29", + "checks": 0, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-213745-50744fa0\\stock-catalogue-setup.json", + "sha256": "dc3c5c8ce1e396c232650be5c44c48b29e951778de8094046fdf15bcfd64a7d7", + "checks": 0, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-213753-3e397d45\\stock-catalogue-setup.json", + "sha256": "d288a0b518c033722ba3fbad3d17d2c7b4295eee02bc50a83bb0a9d76b067400", + "checks": 0, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\stock-catalogue-http-20261110.json", + "sha256": "cd0773771f62eb81f6538a6ed262fb363a9c5382ce87466a7f028eeb469799c8", + "checks": 22, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\stock-catalogue-dom-http-20261110.json", + "sha256": "995c1677f39e70d7e57901c2316e7d01d127fe24aa289bdc40e40f7f0e710674", + "checks": 15, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\stock-catalogue-navigation-20261110.json", + "sha256": "5d5b25688636729966f2c6f85ea10050d3062084454eecd1137b69e677cc450e", + "checks": 3, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\stock-catalogue-assets-20261110.json", + "sha256": "7b207e1f005f375fec1582f99f75b2c8fbd8361909ee8711ff699ef7f2531552", + "checks": 2, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-214445-dc4f79de\\stock-catalogue-preservation.json", + "sha256": "ae7fd34c1cd205f60deeef360be02129850396381b7bb30be0eeacf5e0772149", + "checks": 0, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-214607-6709696b\\stock-catalogue-installation.json", + "sha256": "a4ccf97112501e1ccaf59d4c7700341ff1777bec08f310ced1192afcd7d82a9e", + "checks": 0, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\asset-routing-boundaries-20261110.json", + "sha256": "fd770a55b827cd37c64908b2e0e16b76b86d78cfcd3f0ea10f73a7356e40314e", + "checks": 17, + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\assets-http-20261110.json", + "sha256": "7bc8c39c196d3bfe6f90a50f473041ffcb1036b8339de8c2fe5277dfe5933b06", + "checks": 13, + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockCatalogueController.php", + "sha256": "ab0a6bc52e635d5bcdee9163b68b1ec6fc14b717de7b887fcc832336961fa128" + }, + { + "file": "app/Modules/Purchasing/ReceiptRuntime.php", + "sha256": "b08af1f8772cbfafa1f85e37c832f2c6916ccabcfbd4a8bb98c5ef7690c3f07f" + }, + { + "file": "app/Shared/Context/ModuleNavigation.php", + "sha256": "830e2b8423058a85e1319bc9c92da35efe57c9e19d4cc1be99b6154df7301dac" + }, + { + "file": "app/Modules/Identity/Services/IdentityWorkflow.php", + "sha256": "fe6105fb8e0cb01082f6d3525bbf45a319d9a0da2506df1af644eaf9f5cf4f75" + }, + { + "file": "resources/js/app.jsx", + "sha256": "05b94fc91a5b3e190f37488a66a230f984487eeb8f8842a1d6c8c00ba450a620" + }, + { + "file": "resources/js/WorkspaceNavigation.jsx", + "sha256": "fe9e2cd5d9bea500bde816e53f99dd62df68da58b8983c4a3dbc6268f35b46cf" + }, + { + "file": "resources/js/StockMenuWorkspace.jsx", + "sha256": "ab1f88de113d61854ae09e07e7aac98aa6ac43ab135f975bc20e7941a27d2fdd" + }, + { + "file": "resources/views/stock-catalogue-workspace.blade.php", + "sha256": "4381f81a9aea04fe851f8c7af17cafb98f6bef7cbf52ac2630b0b9912a3dca57" + }, + { + "file": "public/sgf-stock-catalogue.mjs", + "sha256": "10eedcf1881ad3f1cb71813f0547eae7246795273457cc33b1a2a6761a0a2d6e" + }, + { + "file": "config/stock_catalogue.php", + "sha256": "811f844cecf76bff62da943042a272301cb8a7f7a586c4f2648ef5b8e5e31dae" + }, + { + "file": "database/sql/stock_catalogue.sql", + "sha256": "c9c99cec20e6ca60cf0ee56210eac9a2365a2ef9b482698929a70295d2893809" + }, + { + "file": "routes/web.php", + "sha256": "a5a726160f212ccc5dbfb70d513bdabd447e340e8515e2658142c25ce784bef9" + }, + { + "file": "scripts/receipt-composition-http.php", + "sha256": "f62d8a8ea68026e630510ccfb9ae14a1e67ecd55eca9f0384db59d0c5549279f" + }, + { + "file": "scripts/sgf-selected-assets.php", + "sha256": "fdb89f69981eb04a194016732edca808d5d2482b20455f791203736c48016b57" + }, + { + "file": "scripts/sgf-stock-catalogue-setup.php", + "sha256": "dce7f161c76aef4718815b6991b6c3e8f4464a8f28a39466ec8f398791dab3c5" + } + ], + "new_host_grants_per_book": 2, + "total_new_host_grants": 4, + "new_public_grants": 0, + "new_items_8085": 1, + "new_stock_movements": 0, + "new_journals": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false +} diff --git a/docs/proofs/sgf-stock-inactivity-20261110.json b/docs/proofs/sgf-stock-inactivity-20261110.json new file mode 100644 index 0000000..cef617f --- /dev/null +++ b/docs/proofs/sgf-stock-inactivity-20261110.json @@ -0,0 +1,72 @@ +{ + "passed": true, + "date": "2026-11-10", + "installation": "cc50a276c10c", + "port": 8085, + "regression_checks": 23, + "http_checks": 15, + "client_checks": 7, + "projection_selects_per_page": { + "25": 5, + "100": 5 + }, + "evidence": [ + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-b4093c6f6707\\single-integration\\20261110-181533-d853d6ad\\stock-inactive-result.json", + "sha256": "1bb193bb9bd78b714e931b63ec31ae72d1cf8646b69beb2657ef38ff17fb117b", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\stock-inactive-http-20261110.json", + "sha256": "60dab98b897cabd8ae507a1d24291572bbeee36d135cdcd626439ad6d7d320f5", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\stock-inactive-ui-20261110.json", + "sha256": "f08b95965eaa3aa7b45fb2fbd83020de9a8c04f26435740c348d8493e42f659c", + "passed": true + }, + { + "path": "C:\\laragon\\data\\sgf-tempo\\run\\modular-regression-2576427d1939\\private\\receipt-composition-cc50a276c10c\\single-integration\\20261110-181716-7f1ec0a6\\stock-inactive-preservation.json", + "sha256": "48bf58349b1afb1d33b1bcec9608dd73368f9095a0e11fe79846909e8297e196", + "passed": true + } + ], + "sources": [ + { + "file": "app/Modules/Inventory/StockInactiveReport.php", + "sha256": "7db06225fb7bb40c9a4140383caef6dd96c430e3a3e1be8ef7fed2253f2681e2" + }, + { + "file": "app/Modules/Inventory/CompositionStockRead.php", + "sha256": "81bdb5fb374a60974bbad421c461cd12079951fb426361200692a838e413b49e" + }, + { + "file": "resources/js/StockInactiveReportWorkspace.jsx", + "sha256": "5d4f7222bf325550cebddbd00198515da55f15f52b4c99ecaef19f45820a165e" + }, + { + "file": "resources/js/StockMenuWorkspace.jsx", + "sha256": "192f4f1a7a9e8920ceb38fa035e8871d22d4b6ba10b7a2644c55637c64d12b74" + }, + { + "file": "config/receipt_qa.php", + "sha256": "eedca2c93e90906e2989f59757ada449a2d727baab73b812f6b2e8b9a8b84717" + }, + { + "file": "app/Modules/Identity/Services/IdentityWorkflow.php", + "sha256": "c5e95b3df82b1e817cab9eda48a2379c8e309e29c4154668fa90d6ae5265c124" + } + ], + "selected_business_commands": 0, + "journals_preserved": 27, + "new_rights": 0, + "new_demo": false, + "cache_cleared": false, + "visual_review": false, + "load_validated": false, + "deployment_validated": false, + "accepted": false, + "delivery_report": "docs/SGF_STOCK_INACTIVITY_DELIVERY.md", + "consolidated_report": "docs/SGF_CURRENT_DELIVERY_20261110.md" +} diff --git a/docs/proofs/sgf-stock-operations-20261110.json b/docs/proofs/sgf-stock-operations-20261110.json new file mode 100644 index 0000000..8afbe85 --- /dev/null +++ b/docs/proofs/sgf-stock-operations-20261110.json @@ -0,0 +1,97 @@ +{ + "version": "SGF-STOCK-OPERATIONS-PREPARED-v1", + "date": "2026-11-10", + "passed": true, + "pg": { + "checks": [ + "HTML workspace available in explicit Stock context", + "warehouse catalogue restricted to explicit map", + "literal wildcard search and page size 100", + "item catalogue paginated at 25", + "responsible catalogue restricted to source tenant", + "query cannot inject company authority", + "Purchasing context cannot execute Stock routes", + "current physical basis from the original book", + "foreign warehouse refused", + "foreign item refused", + "original writer validates reversible preview", + "preview leaves no movements operations or audit rows", + "individual confirmation applies original physical writer", + "one issue creates exactly one physical movement", + "uncertain response recovers the same receipt without duplicate", + "audit retains actor reason before after and operation receipt", + "old ledger version requires new conference", + "receipt cannot be reused after context generation changes", + "confirmation permission revalidated even on replay", + "transfer preview validates both bound warehouses", + "two independent conferences can observe the same original revision", + "transfer confirms through the same writer", + "transfer produces two balanced physical movements atomically", + "second conference refuses the revision changed by the first confirmation", + "failure-recovery conference prepared on current state", + "failure after physical write rolls back operation movement and native audit", + "same confirmation succeeds once after transient audit failure is removed", + "tampered conference refused", + "feature remains independently disabled", + "all preexisting host and native rows restored after isolated regression" + ], + "transactions_rolled_back": true, + "book": "b4093c6f6707", + "previous_rows_identical": true, + "qa_sequences_may_advance": true + }, + "interface": { + "checks": [ + "startup makes reads only", + "catalogues never auto-select item or responsible", + "unsaved draft guards page exit", + "form captured before disabling fields; exact decimal and reason retained", + "issue does not send a hidden transfer destination", + "impact shown in readable form before confirmation", + "preview never auto-confirms", + "uncertain result protects fields and permits only recovery", + "recovery uses identical sealed conference without a new key", + "recovered receipt closes confirmation", + "successful recovery releases exit guard" + ], + "dom_simulation": true, + "visual_review": false + }, + "private_build": { + "passed": true, + "entry_sha256": "722b472f838c2a981f72b95e154ff0374e6f098a192905dece9edcaa1f1b9e42", + "manifest_sha256": "fbe58be712763935e353605e55f4fd4da18fbd4732830e035766ec762bc09aaa" + }, + "source_hashes": { + "app/Modules/Inventory/StockOperationsController.php": "860321cf232be6b829a3bcae2657c9ac840b5ea25c996bf82b7f0223def959c5", + "app/Modules/Purchasing/ReceiptRuntime.php": "a8f3aac4b65ceea15ac7a8e8e34510d689b09fab8c2f689c466eb90d5e8632dc", + "config/stock_operations.php": "0351259789d6909aca1a9133168f2b6ed66bf90867de88417ebc307cdeb89149", + "routes/web.php": "7a8cc3f15e9732014e866102f353c31699666ec87ecef81409485c3022703ae7", + "app/Shared/Context/ModuleNavigation.php": "0fd39d7635d9a2744111ee39a21b10d04c73b32d1df35b473b61413cd14bc555", + "app/Modules/Identity/Services/IdentityWorkflow.php": "677bdb459aa751f3af37a3b4162f40bcbc7dd8190167202441b3374f244f52b5", + "resources/js/StockMenuWorkspace.jsx": "33c4624727e752dec7a916a37c4ce5cd0c9650c1e046c411be645dd944c4db32", + "resources/views/stock-operations-workspace.blade.php": "ae079a34a6fea2b4bf4a2dbaba44a1f7972657bd9e43b1da29488fa0737fdbdc", + "public/stock-operations-workspace.mjs": "bf60a2c46608f6fca93b70211d170f598551dc2d0884e62e280447f0cdaaf48b", + "scripts/stock-operations-proof.php": "a7fbfa6f96b25f8b8a1b2e214c70cfbcf1bb15929ec534e97225d242e59bd840", + "scripts/stock-operations-client-check.mjs": "3349b8bef330bb82ae1e59a93a23531d6b66c57fa5ade131294e4871a532a1a8", + "packages/inventory-issue/releases/0.1.0/manifest.json": "3c24d9e9fb99bfbb274077495c3a42ddb7eccab06b5c6a956cd739b2bc9b56f0" + }, + "prepared_package": "ac204a6bf15315d0e634", + "installed8085": false, + "activated": false, + "rights_granted": false, + "new_demos": 0, + "new_databases": 0, + "sgc_required": false, + "limitations": [ + "No new parallel-process concurrency or lock-timeout proof", + "Transfer is immediate atomic, no transit reception", + "Count/adjustment/returns and equipment selector still pending in this interface", + "No visual acceptance or deployment-capacity evidence", + "Six-module composition not complete" + ], + "private_diagnostics": { + "ssr_probe": "interrupted while development dependency optimizer was pending; not counted as passed", + "production_client_build": "passed independently; private output only" + } +} diff --git a/docs/proofs/sgf-workshop-equipment-orders-20261111.json b/docs/proofs/sgf-workshop-equipment-orders-20261111.json new file mode 100644 index 0000000..32c54e8 --- /dev/null +++ b/docs/proofs/sgf-workshop-equipment-orders-20261111.json @@ -0,0 +1,39 @@ +{ + "passed": true, + "date": "2026-11-11", + "installation": "cc50a276c10c", + "port": 8085, + "evidence": [ + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261111-074106-867d5916/equipment-orders-result.json", + "sha256": "5f63b4ee44257f4feb7279ce3d778355c905ade43a636bae3dcd3bed89d07007", + "checks": 23 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/equipment-orders-ui-20261111.json", + "sha256": "19dcab02124658e470ed1d7b95554b44a1967a3744393989f8e172c67cab735b", + "checks": 12 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/equipment-orders-http-20261111.json", + "sha256": "cec1295f6ab34628ec83acaa6271c41c0ced11cf7fa8b45036debafaea4a3232", + "checks": 17 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-074653-1680702c/equipment-orders-installation.json", + "sha256": "e5637a6ccc9abf3ac9fff798fa4780be55aef932f9d5eb2089c34215719d165a", + "checks": 56 + }, + { + "file": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261111-074750-492f6c0a/equipment-orders-preservation.json", + "sha256": "86d9e09fd477d09ad416b327e3b8e3d942c488d7d710815b17590e2e901a4812", + "checks": 56 + } + ], + "directed_checks": 52, + "business_commands_8085": 0, + "new_grants": 0, + "cache_cleared": false, + "visual_review": false, + "deployment_validated": false +} diff --git a/docs/proofs/sgf-workshop-period-20261110.json b/docs/proofs/sgf-workshop-period-20261110.json new file mode 100644 index 0000000..d407535 --- /dev/null +++ b/docs/proofs/sgf-workshop-period-20261110.json @@ -0,0 +1,27 @@ +{ + "version": "SGF-WORKSHOP-OPENING-PERIOD-QA-v1", + "passed": true, + "target": "existing8085", + "regression_checks": 21, + "ui_checks": 13, + "http_checks": 15, + "original_tables_identical": 48, + "business_commands": 0, + "new_rights": 0, + "new_demo": false, + "browser_visual_review": false, + "deployment_validated": false, + "evidence": { + "regression": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-b4093c6f6707/single-integration/20261110-222954-a3239460/workshop-period-result.json", + "ui": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/workshop-period-ui-20261110.json", + "http": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/workshop-period-http-20261110.json", + "installation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-223035-3a46a21d/workshop-period-installation.json", + "preservation": "C:/laragon/data/sgf-tempo/run/modular-regression-2576427d1939/private/receipt-composition-cc50a276c10c/single-integration/20261110-223116-7608f084/workshop-period-preservation.json" + }, + "sources": [ + "app/Modules/Workshop/WorkshopOrderRead.php", + "resources/js/WorkshopOrderList.jsx", + "resources/js/WorkshopMenuWorkspace.jsx", + "resources/js/workshop-report-view.js" + ] +} diff --git a/packages/general-ledger/bootstrap.php b/packages/general-ledger/bootstrap.php index fe89f38..0eb666c 100644 --- a/packages/general-ledger/bootstrap.php +++ b/packages/general-ledger/bootstrap.php @@ -13,6 +13,7 @@ require_once __DIR__.'/src/CycleSchema.php'; require_once __DIR__.'/src/PayableJournal.php'; require_once __DIR__.'/src/StockSchema.php'; require_once __DIR__.'/src/StockValuation.php'; +require_once __DIR__.'/src/StockTransitEvidence.php'; require_once __DIR__.'/src/StockComposition.php'; require_once __DIR__.'/src/StockWorkspace.php'; require_once __DIR__.'/src/CycleWorkspace.php'; diff --git a/packages/general-ledger/src/CostCorrectionSchema.php b/packages/general-ledger/src/CostCorrectionSchema.php index 67de1bd..4e19206 100644 --- a/packages/general-ledger/src/CostCorrectionSchema.php +++ b/packages/general-ledger/src/CostCorrectionSchema.php @@ -7,7 +7,7 @@ final class CostCorrectionSchema public static function install(ConnectionInterface $db):void { LedgerSchema::assertSynthetic($db); - if(!str_starts_with($db->getDatabaseName(),'payable_sgf_')||!$db->getSchemaBuilder()->hasTable('gl_cost_sources')||$db->getSchemaBuilder()->hasTable('gl_cost_correction_capability')||$db->table('gl_proposals')->exists()||$db->table('gl_journals')->exists())throw new \LogicException('New empty SGF cost correction book required'); + if((!str_starts_with($db->getDatabaseName(),'payable_sgf_')&&!LedgerSchema::externalHostComposition($db))||!$db->getSchemaBuilder()->hasTable('gl_cost_sources')||$db->getSchemaBuilder()->hasTable('gl_cost_correction_capability')||$db->table('gl_proposals')->exists()||$db->table('gl_journals')->exists())throw new \LogicException('New empty SGF cost correction book required'); $db->transaction(function()use($db){ $db->statement('CREATE TABLE gl_cost_correction_capability(id int PRIMARY KEY CHECK(id=1),contract text NOT NULL)');$db->table('gl_cost_correction_capability')->insert(['id'=>1,'contract'=>'COMMON-LEDGER-DIRECT-COST-CORRECTION-SIM-v1']);$db->unprepared('CREATE TRIGGER gl_immutable BEFORE UPDATE OR DELETE ON gl_cost_correction_capability FOR EACH ROW EXECUTE FUNCTION gl_append_only()'); $db->unprepared(<<<'SQL' diff --git a/packages/general-ledger/src/IndustrialCorrectionSchema.php b/packages/general-ledger/src/IndustrialCorrectionSchema.php index e78d741..0d9a98d 100644 --- a/packages/general-ledger/src/IndustrialCorrectionSchema.php +++ b/packages/general-ledger/src/IndustrialCorrectionSchema.php @@ -7,7 +7,7 @@ final class IndustrialCorrectionSchema public static function install(ConnectionInterface $db):void { LedgerSchema::assertSynthetic($db); - if(!str_starts_with($db->getDatabaseName(),'payable_sgf_')||!$db->getSchemaBuilder()->hasTable('gl_industrial_sources')||$db->getSchemaBuilder()->hasTable('gl_industrial_correction_capability')||$db->table('gl_proposals')->exists()||$db->table('gl_journals')->exists())throw new \LogicException('New empty SGF cost correction book required'); + if((!str_starts_with($db->getDatabaseName(),'payable_sgf_')&&!LedgerSchema::externalHostComposition($db))||!$db->getSchemaBuilder()->hasTable('gl_industrial_sources')||$db->getSchemaBuilder()->hasTable('gl_industrial_correction_capability')||$db->table('gl_proposals')->exists()||$db->table('gl_journals')->exists())throw new \LogicException('New empty SGF cost correction book required'); $db->transaction(function()use($db){ $db->statement('CREATE TABLE gl_industrial_correction_capability(id int PRIMARY KEY CHECK(id=1),contract text NOT NULL)');$db->table('gl_industrial_correction_capability')->insert(['id'=>1,'contract'=>'COMMON-LEDGER-INDUSTRIAL-CORRECTION-SIM-v1']);$db->unprepared('CREATE TRIGGER gl_immutable BEFORE UPDATE OR DELETE ON gl_industrial_correction_capability FOR EACH ROW EXECUTE FUNCTION gl_append_only()'); $db->unprepared(<<<'SQL' diff --git a/packages/general-ledger/src/LedgerCycle.php b/packages/general-ledger/src/LedgerCycle.php index e103984..04e15cf 100644 --- a/packages/general-ledger/src/LedgerCycle.php +++ b/packages/general-ledger/src/LedgerCycle.php @@ -14,11 +14,11 @@ trait LedgerCycle $this->cycleReady(); if(in_array($kind,['allocation.confirmed','allocation.reversed'],true)){ $table=$kind==='allocation.confirmed'?'ps_allocations':'ps_reversals'; - $row=$this->db->table($table)->where($this->scope)->where('id',$id)->first()??throw new \DomainException('Efeito financeiro confirmado indisponível.'); + $row=$this->db->table($table)->where(str_starts_with($table,'ps_')?$this->financialScope():$this->scope)->where('id',$id)->first()??throw new \DomainException('Efeito financeiro confirmado indisponível.'); $base=($this->source)((int)$row->obligation_id); if($kind==='allocation.confirmed'){ - $decision=$this->db->table('ps_allocation_decisions')->where($this->scope)->where('id',$row->decision_id)->where('decision','confirmed')->first(); - $payment=$this->db->table('ps_payment_decisions')->where($this->scope)->where('payment_id',$row->payment_id)->where('decision','confirmed')->first(); + $decision=$this->db->table('ps_allocation_decisions')->where($this->financialScope())->where('id',$row->decision_id)->where('decision','confirmed')->first(); + $payment=$this->db->table('ps_payment_decisions')->where($this->financialScope())->where('payment_id',$row->payment_id)->where('decision','confirmed')->first(); if(!$decision||!$payment)throw new \DomainException('Afectação sem confirmação financeira compatível.'); }else{ if(!$this->q('journals')->where('snapshot->source->effect_type','allocation.confirmed')->where('snapshot->source->source_id',(int)$row->allocation_id)->exists())throw new \DomainException('Contabilize primeiro a afectação original.'); @@ -51,7 +51,7 @@ trait LedgerCycle $hash=$this->hash([$action,$d]);$old=$this->q('commands')->where(['actor_id'=>$this->actor,'command_key'=>$key])->first(); if($old){if(!hash_equals($old->request_hash,$hash))throw new \DomainException('Chave repetida com conteúdo diferente.');return json_decode($old->result,true,128,JSON_THROW_ON_ERROR)+['replayed'=>true];} if($action==='effect-prepare'){ - if(!in_array($d['effect_type'],['allocation.confirmed','allocation.reversed','sales.invoice','receivable.allocated','receivable.reversed','workshop.direct_cost.validated','industry.cost.validated','stock.receipt','stock.issue','stock.return'],true))throw new \InvalidArgumentException('Seleccione uma origem contabilística suportada.'); + if(!in_array($d['effect_type'],['allocation.confirmed','allocation.reversed','sales.invoice','receivable.allocated','receivable.reversed','receivable.reduced','receivable.reduction_reversed','cash.receipt_classified','cash.expense_confirmed','workshop.direct_cost.validated','industry.cost.validated','stock.receipt','stock.issue','stock.return','stock.adjustment','stock.supplier_return'],true))throw new \InvalidArgumentException('Seleccione uma origem contabilística suportada.'); $result=$this->prepare(['obligation_id'=>$d['source_id']]+$d,$reason,$d['effect_type']); }elseif(str_starts_with($action,'correct-'))$result=$this->correction($action,$d,$reason); else $result=$this->periodCommand($action,$d,$reason); @@ -94,21 +94,21 @@ trait LedgerCycle { $period=$this->latest('periods','period_key',$key);if($period->status!=='open')throw new \DomainException('Período já fechado.'); $h=hash_init('sha256');$pending=0;$facts=0; - foreach($this->db->table('pa_obligations')->where($this->scope)->orderBy('id')->cursor() as $o){ + foreach($this->db->table('pa_obligations')->where($this->financialScope())->orderBy('id')->cursor() as $o){ $source=($this->source)((int)$o->id);hash_update($h,Payables::canonical($source));$facts++; if(!$this->q('journals')->where('obligation_id',$o->id)->where('snapshot->source->effect_type','supplier_payable.recognised')->exists())$pending++; } foreach(['ps_allocations'=>'allocation.confirmed','ps_reversals'=>'allocation.reversed'] as $table=>$type){ - foreach($this->db->table($table)->where($this->scope)->orderBy('id')->cursor() as $r){$facts++;hash_update($h,Payables::canonical((array)$r));if(!$this->q('journals')->where('snapshot->source->effect_type',$type)->where('snapshot->source->source_id',(int)$r->id)->exists())$pending++;} + foreach($this->db->table($table)->where(str_starts_with($table,'ps_')?$this->financialScope():$this->scope)->orderBy('id')->cursor() as $r){$facts++;hash_update($h,Payables::canonical((array)$r));if(!$this->q('journals')->where('snapshot->source->effect_type',$type)->where('snapshot->source->source_id',(int)$r->id)->exists())$pending++;} } foreach(['ps_payments'=>'ps_payment_decisions','ps_proposals'=>'ps_allocation_decisions','ps_reversal_proposals'=>'ps_reversals'] as $table=>$decisions){ $fk=match($table){'ps_payments'=>'payment_id','ps_proposals'=>'proposal_id',default=>'proposal_id'}; - $pending+=$this->db->table($table)->where($this->scope)->whereNotIn('id',$this->db->table($decisions)->where($this->scope)->select($fk))->count(); + $pending+=$this->db->table($table)->where(str_starts_with($table,'ps_')?$this->financialScope():$this->scope)->whereNotIn('id',$this->db->table($decisions)->where($this->financialScope())->select($fk))->count(); } $latest=$this->q('proposals')->selectRaw('max(id)')->groupBy('effect_key'); $pending+=$this->q('proposals')->whereIn('id',$latest)->whereNotIn('id',$this->q('journals')->select('proposal_id'))->whereNotIn('id',$this->q('reviews')->where('decision','rejected')->select('proposal_id'))->count(); foreach(['gl_accounts','gl_rules','gl_journals','gl_lines','gl_proposals','gl_reviews','gl_corrections','gl_correction_results','ps_payments','ps_payment_decisions','ps_proposals','ps_allocation_decisions','ps_reversal_proposals'] as $table){ - hash_update($h,$table);foreach($this->db->table($table)->where($this->scope)->orderBy('id')->cursor() as $r)hash_update($h,Payables::canonical((array)$r)); + hash_update($h,$table);foreach($this->db->table($table)->where(str_starts_with($table,'ps_')?$this->financialScope():$this->scope)->orderBy('id')->cursor() as $r)hash_update($h,Payables::canonical((array)$r)); } $sum=$this->q('lines')->whereIn('journal_id',$this->q('journals')->where('snapshot->period->period_key',$key)->select('id'))->selectRaw('coalesce(sum(debit_minor),0) d,coalesce(sum(credit_minor),0) c')->first(); if((int)$sum->d!==(int)$sum->c)throw new \DomainException('Diário desequilibrado.'); @@ -157,8 +157,8 @@ trait LedgerCycle $partReviews=$this->q('reviews')->whereIn('proposal_id',$ids)->get()->keyBy('proposal_id');$groups=array_map(fn($c)=>$c->id,$result['corrections']);$groupResults=$this->q('correction_results')->whereIn('correction_id',$groups)->get()->keyBy('correction_id'); $latestParts=$this->q('proposals')->whereIn('effect_key',$parts->pluck('effect_key')->all())->selectRaw('effect_key,max(capture_version) v')->groupBy('effect_key')->pluck('v','effect_key'); foreach($result['corrections'] as $c){$a=$parts->get($c->reverse_proposal_id);$b=$parts->get($c->replacement_proposal_id);$c->reverse_snapshot=$a->snapshot;$c->replacement_snapshot=$b->snapshot;$c->reverse_review=$partReviews->get($c->reverse_proposal_id);$c->replacement_review=$partReviews->get($c->replacement_proposal_id);$c->result=$groupResults->get($c->id);$c->current=(int)$a->capture_version===(int)$latestParts->get($a->effect_key)&&(int)$b->capture_version===(int)$latestParts->get($b->effect_key);} - foreach(['ps_allocations'=>'allocations','ps_reversals'=>'reversals'] as $table=>$name){$p=$this->db->table($table)->where($this->scope)->orderByDesc('id')->paginate(25,['*'],'page',$page);$result[$name]=$p->items();$result['pagination'][$name]=['page'=>$page,'last_page'=>$p->lastPage(),'total'=>$p->total()];} - $p=$this->db->table('pa_obligations')->where($this->scope)->orderByDesc('id')->paginate(25,['*'],'page',$page);$result['obligations']=$p->items();$result['pagination']['obligations']=['page'=>$page,'last_page'=>$p->lastPage(),'total'=>$p->total()]; + foreach(['ps_allocations'=>'allocations','ps_reversals'=>'reversals'] as $table=>$name){$p=$this->db->table($table)->where(str_starts_with($table,'ps_')?$this->financialScope():$this->scope)->orderByDesc('id')->paginate(25,['*'],'page',$page);$result[$name]=$p->items();$result['pagination'][$name]=['page'=>$page,'last_page'=>$p->lastPage(),'total'=>$p->total()];} + $p=$this->db->table('pa_obligations')->where($this->financialScope())->orderByDesc('id')->paginate(25,['*'],'page',$page);$result['obligations']=$p->items();$result['pagination']['obligations']=['page'=>$page,'last_page'=>$p->lastPage(),'total'=>$p->total()]; return $result; } } diff --git a/packages/general-ledger/src/LedgerSales.php b/packages/general-ledger/src/LedgerSales.php index 39cdbc1..6ad8be6 100644 --- a/packages/general-ledger/src/LedgerSales.php +++ b/packages/general-ledger/src/LedgerSales.php @@ -5,7 +5,7 @@ trait LedgerSales private function captureSales(array $s,string $date,object $period,object $rule,int $amount):array { if(!$this->db->getSchemaBuilder()->hasTable('gl_sales_capability'))throw new \DomainException('Schema comercial não composto.'); - $kind=$s['effect_type'];$codes=match($kind){'sales.invoice'=>['QA-CLIENTES','QA-VENDAS','QA-IMPOSTO-TESTE'],'receivable.allocated'=>['QA-LIQUIDACAO-CLIENTE','QA-CLIENTES'],'receivable.reversed'=>['QA-CLIENTES','QA-LIQUIDACAO-CLIENTE']}; + $kind=$s['effect_type'];$codes=match($kind){'sales.invoice'=>['QA-CLIENTES','QA-VENDAS','QA-IMPOSTO-TESTE'],'receivable.allocated'=>['QA-LIQUIDACAO-CLIENTE','QA-CLIENTES'],'receivable.reversed'=>['QA-CLIENTES','QA-LIQUIDACAO-CLIENTE'],'receivable.reduced'=>['QA-REDUCAO-CLIENTE','QA-CLIENTES'],'receivable.reduction_reversed'=>['QA-CLIENTES','QA-REDUCAO-CLIENTE'],'cash.receipt_classified'=>['QA-CAIXA','QA-LIQUIDACAO-CLIENTE'],'cash.expense_confirmed'=>['QA-DESPESA-CAIXA','QA-CAIXA']}; $ids=[$rule->debit_account_id,$rule->credit_account_id];if($kind==='sales.invoice')$ids[]=$rule->tax_account_id; if(count(array_unique($ids))!==count($ids))throw new \DomainException('Contas de movimento distintas necessárias.'); $accounts=$this->q('accounts')->whereIn('id',$ids)->get()->keyBy('id');$lines=[]; @@ -14,6 +14,6 @@ trait LedgerSales $value=$kind==='sales.invoice'&&$n>0?($n===1?$s['base_minor']:$s['tax_minor']):$amount; $lines[]=['account_id'=>$a->id,'account_code'=>$a->code,'debit_minor'=>$n===0?$value:0,'credit_minor'=>$n===0?0:$value]; } - return ['contract'=>'COMMON-LEDGER-SALES-RECEIVABLE-SIM-v1','source'=>$s,'journal_on'=>$date,'currency'=>'MZN','period'=>(array)$period,'rule'=>(array)$rule,'lines'=>$lines,'impact'=>['debit_minor'=>$amount,'credit_minor'=>$amount,'financial_balance_changed'=>false,'payment_created'=>false]]; + return ['contract'=>str_starts_with($kind,'cash.')?'SGF-CASH-QA-v1':'COMMON-LEDGER-SALES-RECEIVABLE-SIM-v1','source'=>$s,'journal_on'=>$date,'currency'=>'MZN','period'=>(array)$period,'rule'=>(array)$rule,'lines'=>$lines,'impact'=>['debit_minor'=>$amount,'credit_minor'=>$amount,'financial_balance_changed'=>false,'payment_created'=>false]]; } } diff --git a/packages/general-ledger/src/LedgerSchema.php b/packages/general-ledger/src/LedgerSchema.php index f470ada..2e94f8f 100644 --- a/packages/general-ledger/src/LedgerSchema.php +++ b/packages/general-ledger/src/LedgerSchema.php @@ -16,6 +16,14 @@ final class LedgerSchema if($role->elevated)throw new \LogicException('LEDGER_ELEVATED_ROLE_REFUSED'); } + public static function externalHostComposition(ConnectionInterface $db):bool + { + self::assertSynthetic($db); + if(!preg_match('/^compras_receipts_test_([a-f0-9]{12})$/D',$db->getDatabaseName(),$m)||!$db->getSchemaBuilder()->hasTable('gl_sgf_composition'))return false; + $r=$db->table('gl_sgf_composition')->first(); + return $r&&$r->contract==='SGF-8085-NATIVE-COMPOSITION-v1'&&$r->host_database==='budget_core_sgf_'.$m[1]&&(int)$r->company_id>0&&(int)$r->unit_id>0; + } + public static function install(ConnectionInterface $db): void { self::assertSynthetic($db); diff --git a/packages/general-ledger/src/PayableJournal.php b/packages/general-ledger/src/PayableJournal.php index 9948b87..a499278 100644 --- a/packages/general-ledger/src/PayableJournal.php +++ b/packages/general-ledger/src/PayableJournal.php @@ -13,7 +13,16 @@ final class PayableJournal public const POLICY='QA-PAYABLE-JOURNAL-v1'; private const SUPPLIER_EFFECTS=['supplier_payable.recognised','allocation.confirmed','allocation.reversed','journal.reversal','journal.replacement']; public function __construct(private ConnectionInterface $db,private array $scope,private int $actor,private string $person, - private \Closure $authorize,private \Closure $readAuthorize,private \Closure $source,private ?\Closure $checkpoint=null,private ?\Closure $additionalSource=null,private ?\Closure $additionalPeriod=null) {} + private \Closure $authorize,private \Closure $readAuthorize,private \Closure $source,private ?\Closure $checkpoint=null,private ?\Closure $additionalSource=null,private ?\Closure $additionalPeriod=null,private ?array $financialScope=null) { + if($financialScope!==null){ + if(array_keys($financialScope)!==['tenant_id','company_id','unit_id']||$financialScope['tenant_id']!==$scope['tenant_id']||!is_int($financialScope['company_id'])||!is_int($financialScope['unit_id'])||$financialScope['company_id']<0||$financialScope['unit_id']<0)throw new \InvalidArgumentException('Qualified financial source scope required.'); + LedgerSchema::assertSynthetic($db); + if(!$db->getSchemaBuilder()->hasTable('gl_sgf_composition'))throw new \DomainException('Explicit SGF financial binding is absent.'); + $binding=$db->table('gl_sgf_composition')->where('tenant_id',$scope['tenant_id'])->first(); + if(!$binding||(int)$binding->company_id!==$scope['company_id']||(int)$binding->unit_id!==$scope['unit_id']||(int)$binding->financial_company_id!==$financialScope['company_id']||(int)$binding->financial_unit_id!==$financialScope['unit_id'])throw new \DomainException('Financial/ledger context binding changed.'); + } + } + private function financialScope():array{return $this->financialScope??$this->scope;} private function q(string $table){return $this->db->table('gl_'.$table)->where($this->scope);} private function hash(array $data):string{return hash('sha256',Payables::canonical($data));} private function step(string $name):void{if($this->checkpoint!==null)($this->checkpoint)($name);} @@ -26,9 +35,9 @@ final class PayableJournal private function capture(int $obligation,string $date,string $periodKey,string $ruleKey,string $kind='supplier_payable.recognised'):array { if(!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/D',$date,$m)||!checkdate((int)$m[2],(int)$m[3],(int)$m[1]))throw new \InvalidArgumentException('Data de lançamento explícita inválida.'); - $sales=in_array($kind,['sales.invoice','receivable.allocated','receivable.reversed'],true);$cost=in_array($kind,['workshop.direct_cost.validated','workshop.direct_cost.reversal','workshop.direct_cost.replacement'],true); + $sales=in_array($kind,['sales.invoice','receivable.allocated','receivable.reversed','receivable.reduced','receivable.reduction_reversed','cash.receipt_classified','cash.expense_confirmed'],true);$cost=in_array($kind,['workshop.direct_cost.validated','workshop.direct_cost.reversal','workshop.direct_cost.replacement'],true); $industrial=in_array($kind,['industry.cost.validated','industry.cost.reversal','industry.cost.replacement'],true); - $stock=in_array($kind,['stock.receipt','stock.issue','stock.return'],true); + $stock=in_array($kind,['stock.receipt','stock.issue','stock.return','stock.adjustment','stock.supplier_return'],true); $s=$sales||$cost||$industrial||$stock?($this->additionalSource!==null?($this->additionalSource)($kind,$obligation):throw new \DomainException('Origem adicional não composta neste host.')):($kind==='supplier_payable.recognised'?($this->source)($obligation):$this->cycleSource($kind,$obligation)); $amount=$s['amount_minor']??$s['principal_minor']??null; if(($s['effect_type']??'')!==$kind||!($s['compatible']??false)||($s['currency']??'')!=='MZN'||!is_int($amount)||$amount<=0||(!$sales&&($s['tax_minor']??null)!==0)||($s['history_imported']??true))throw new \DomainException('Fonte incompatível com o ensaio contabilístico.'); @@ -38,11 +47,11 @@ final class PayableJournal if($stock){if(!$this->db->getSchemaBuilder()->hasTable('gl_stock_books')||($s['stock_movement_on']??'')!==$date)throw new \DomainException('Valorização não composta ou data física incompatível.');} if($industrial&&($s['period_start']<$period->starts_on||$s['period_end']>$period->ends_on))throw new \DomainException('Período não contém o lote seleccionado.'); if($period->status!=='open'||$date<$period->starts_on||$date>$period->ends_on)throw new \DomainException('Período fechado, incompatível ou alterado.'); - if($rule->policy!==($stock?'QA-STOCK-VALUATION-v1':($sales?'QA-SALES-JOURNAL-v1':($cost?'QA-DIRECT-COST-JOURNAL-v1':($industrial?'QA-INDUSTRIAL-JOURNAL-v1':self::POLICY))))||$rule->effect_type!==$s['effect_type']||$rule->currency!=='MZN'||$date<$rule->valid_from||$date>$rule->valid_to)throw new \DomainException('Regra contabilística ausente, incompatível ou fora de vigência.'); - if($sales)return $this->captureSales($s,$date,$period,$rule,$amount); + if($rule->policy!==(str_starts_with($kind,'cash.')?'QA-CASH-JOURNAL-v1':($stock?'QA-STOCK-VALUATION-v1':($sales?'QA-SALES-JOURNAL-v1':($cost?'QA-DIRECT-COST-JOURNAL-v1':($industrial?'QA-INDUSTRIAL-JOURNAL-v1':self::POLICY)))))||$rule->effect_type!==$s['effect_type']||$rule->currency!=='MZN'||$date<$rule->valid_from||$date>$rule->valid_to)throw new \DomainException('Regra contabilística ausente, incompatível ou fora de vigência.'); + if(str_starts_with($kind,'cash.')&&($s['permitted_period_key']!==$periodKey||$s['cash_effect_on']!==$date))throw new \DomainException('Data ou período de Caixa incompatível.');if($sales)return $this->captureSales($s,$date,$period,$rule,$amount); $accounts=$this->q('accounts')->whereIn('id',[$rule->debit_account_id,$rule->credit_account_id])->get()->keyBy('id'); $debit=$accounts->get($rule->debit_account_id);$credit=$accounts->get($rule->credit_account_id); - $codes=match($kind){'stock.receipt'=>['QA-STOCK','QA-COMPENSACAO-STOCK'],'stock.issue'=>['QA-CONSUMO-STOCK','QA-STOCK'],'stock.return'=>['QA-STOCK','QA-CONSUMO-STOCK'],'industry.cost.validated','industry.cost.replacement'=>['QA-CUSTO-INDUSTRIAL','QA-COMPENSACAO-INDUSTRIAL'],'industry.cost.reversal'=>['QA-COMPENSACAO-INDUSTRIAL','QA-CUSTO-INDUSTRIAL'],'supplier_payable.recognised','journal.replacement'=>['QA-CONTRAPARTIDA','QA-FORNECEDOR'],'allocation.confirmed'=>['QA-FORNECEDOR','QA-LIQUIDACAO'],'allocation.reversed'=>['QA-LIQUIDACAO','QA-FORNECEDOR'],'journal.reversal'=>['QA-FORNECEDOR','QA-CONTRAPARTIDA'],'workshop.direct_cost.validated','workshop.direct_cost.replacement'=>['QA-CUSTO-DIRECTO','QA-COMPENSACAO-ENSAIO'],'workshop.direct_cost.reversal'=>['QA-COMPENSACAO-ENSAIO','QA-CUSTO-DIRECTO'],default=>throw new \DomainException('Tipo de efeito não autorizado.')}; + $codes=match($kind){'stock.supplier_return'=>['QA-DEVOLUCAO-FORNECEDOR','QA-STOCK'],'stock.adjustment'=>['QA-DIFERENCA-INVENTARIO','QA-STOCK'],'stock.receipt'=>['QA-STOCK','QA-COMPENSACAO-STOCK'],'stock.issue'=>['QA-CONSUMO-STOCK','QA-STOCK'],'stock.return'=>['QA-STOCK','QA-CONSUMO-STOCK'],'industry.cost.validated','industry.cost.replacement'=>['QA-CUSTO-INDUSTRIAL','QA-COMPENSACAO-INDUSTRIAL'],'industry.cost.reversal'=>['QA-COMPENSACAO-INDUSTRIAL','QA-CUSTO-INDUSTRIAL'],'supplier_payable.recognised','journal.replacement'=>['QA-CONTRAPARTIDA','QA-FORNECEDOR'],'allocation.confirmed'=>['QA-FORNECEDOR','QA-LIQUIDACAO'],'allocation.reversed'=>['QA-LIQUIDACAO','QA-FORNECEDOR'],'journal.reversal'=>['QA-FORNECEDOR','QA-CONTRAPARTIDA'],'workshop.direct_cost.validated','workshop.direct_cost.replacement'=>['QA-CUSTO-DIRECTO','QA-COMPENSACAO-ENSAIO'],'workshop.direct_cost.reversal'=>['QA-COMPENSACAO-ENSAIO','QA-CUSTO-DIRECTO'],default=>throw new \DomainException('Tipo de efeito não autorizado.')}; if(!$debit||!$credit||!$debit->is_movement||!$credit->is_movement||$debit->currency!=='MZN'||$credit->currency!=='MZN'||$debit->id===$credit->id||[$debit->code,$credit->code]!==$codes)throw new \DomainException('Contas artificiais de movimento incompatíveis.'); return ['contract'=>$stock?'COMMON-LEDGER-STOCK-VALUATION-SIM-v1':($industrial?($kind==='industry.cost.validated'?'COMMON-LEDGER-INDUSTRIAL-SIM-v1':'COMMON-LEDGER-INDUSTRIAL-CORRECTION-SIM-v1'):($cost?($kind==='workshop.direct_cost.validated'?'COMMON-LEDGER-DIRECT-COST-SIM-v1':'COMMON-LEDGER-DIRECT-COST-CORRECTION-SIM-v1'):($kind==='supplier_payable.recognised'?'COMMON-LEDGER-PAYABLE-SIM-v1':'COMMON-LEDGER-COMPLETE-CYCLE-SIM-v1'))),'source'=>$s,'journal_on'=>$date,'currency'=>'MZN','period'=>(array)$period,'rule'=>(array)$rule, 'lines'=>[['account_id'=>$debit->id,'account_code'=>$debit->code,'debit_minor'=>$amount,'credit_minor'=>0],['account_id'=>$credit->id,'account_code'=>$credit->code,'debit_minor'=>0,'credit_minor'=>$amount]], @@ -136,8 +145,8 @@ final class PayableJournal ($this->readAuthorize)();$journals=$this->q('journals');$focus=null;$salesInvoice=null;$costOrder=null;$industrialLot=null; if(isset($options['industrial_lot_id'])){$industrialLot=$this->id($options['industrial_lot_id']);if($this->additionalSource===null||!$this->db->getSchemaBuilder()->hasTable('gl_industrial_sources')||!$this->db->table('gl_industrial_sources')->where($this->scope)->where('lot_id',$industrialLot)->exists())throw new \DomainException('Lote fora do âmbito composto.');$journals->whereIn('snapshot->source->effect_type',['industry.cost.validated','industry.cost.reversal','industry.cost.replacement'])->where('snapshot->source->industrial_lot_id',$industrialLot);} if(isset($options['cost_order_id'])){$costOrder=$this->id($options['cost_order_id']);if($this->additionalSource===null||!$this->db->getSchemaBuilder()->hasTable('gl_cost_sources')||!$this->db->table('gl_cost_sources')->where($this->scope)->where('order_id',$costOrder)->exists())throw new \DomainException('Custo fora do âmbito composto.');$journals->where('snapshot->source->cost_order_id',$costOrder);} - if(isset($options['sales_invoice_id'])){$salesInvoice=$this->id($options['sales_invoice_id']);if($this->additionalSource===null||!$this->db->table('si_invoices')->where($this->scope)->where('id',$salesInvoice)->exists())throw new \DomainException('Commercial source unavailable in this scope.');$journals->where('snapshot->source->invoice_id',$salesInvoice);} - if(isset($options['obligation_id'])){$focus=$this->id($options['obligation_id']);if(!$this->db->table('pa_obligations')->where($this->scope)->where('id',$focus)->exists())throw new \DomainException('Obrigação indisponível neste contexto.');$journals->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS);} + if(isset($options['sales_invoice_id'])){$salesInvoice=$this->id($options['sales_invoice_id']);if($this->additionalSource===null||(!LedgerSchema::externalHostComposition($this->db)&&!$this->db->table('si_invoices')->where($this->scope)->where('id',$salesInvoice)->exists()))throw new \DomainException('Commercial source unavailable in this scope.');$journals->where('snapshot->source->invoice_id',$salesInvoice);} + if(isset($options['obligation_id'])){$focus=$this->id($options['obligation_id']);if(!$this->db->table('pa_obligations')->where($this->financialScope())->where('id',$focus)->exists())throw new \DomainException('Obrigação indisponível neste contexto.');$journals->where('obligation_id',$focus)->whereIn('snapshot->source->effect_type',self::SUPPLIER_EFFECTS);} if(isset($options['period_key'])){if(!is_string($options['period_key'])||strlen($options['period_key'])>80)throw new \InvalidArgumentException('Período inválido.');$this->latest('periods','period_key',$options['period_key']);$journals->whereRaw("snapshot->'period'->>'period_key' = ?",[$options['period_key']]);} $account=null;if(isset($options['account_id'])){$account=$this->q('accounts')->where('id',$this->id($options['account_id']))->first()??throw new \DomainException('Conta indisponível neste contexto.');$journals->whereIn('id',$this->q('lines')->where('account_id',$account->id)->select('journal_id'));} $pager=(clone $journals)->orderByDesc('id')->paginate(25,['*'],'page',$page);$ids=array_map(fn($j)=>$j->id,$pager->items()); @@ -157,3 +166,4 @@ final class PayableJournal }); } } + diff --git a/packages/general-ledger/src/SalesSchema.php b/packages/general-ledger/src/SalesSchema.php index ca78dca..7e89cf4 100644 --- a/packages/general-ledger/src/SalesSchema.php +++ b/packages/general-ledger/src/SalesSchema.php @@ -6,7 +6,7 @@ final class SalesSchema public static function install(ConnectionInterface $db):void { LedgerSchema::assertSynthetic($db); - if(!str_starts_with($db->getDatabaseName(),'payable_sgf_'))throw new \LogicException('Sales host composition required'); + if(!str_starts_with($db->getDatabaseName(),'payable_sgf_')&&!LedgerSchema::externalHostComposition($db))throw new \LogicException('Sales host composition required'); if($db->table('gl_proposals')->exists()||$db->table('gl_journals')->exists()||$db->getSchemaBuilder()->hasTable('gl_sales_capability'))throw new \LogicException('Fresh empty sales ledger required; no upgrade/reset'); $db->transaction(function()use($db){ $db->statement('ALTER TABLE gl_rules ADD COLUMN tax_account_id bigint NULL'); diff --git a/packages/general-ledger/src/StockSupplierReturnEvidence.php b/packages/general-ledger/src/StockSupplierReturnEvidence.php new file mode 100644 index 0000000..7ed2558 --- /dev/null +++ b/packages/general-ledger/src/StockSupplierReturnEvidence.php @@ -0,0 +1,31 @@ +db)||$op->kind!=='supplier_return')throw new \DomainException('Devolução ao fornecedor fora da composição QA.'); + $proof=$snapshot['disposition']??null; + $approved=is_array($proof)?$this->db->table('supply_records')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$proof['approved_id']??0,'kind'=>'stock_event','state'=>'approved'])->first():null; + $executed=$approved?$this->db->table('supply_records')->where(['tenant_id'=>$this->scope['tenant_id'],'record_key'=>$approved->record_key,'version'=>$approved->version+1,'state'=>'executed','kind'=>'stock_event'])->first():null; + if(!$approved||!$executed)throw new \DomainException('Faltam aprovação e execução nativas.'); + $data=json_decode($approved->data,true,64,JSON_THROW_ON_ERROR);$done=json_decode($executed->data,true,64,JSON_THROW_ON_ERROR);$review=json_decode($approved->snapshot,true,64,JSON_THROW_ON_ERROR); + $source=$review['source']??null;$receiptItem=$source['receipt']['item_id']??null; + $line=is_int($receiptItem)?$this->db->table('goods_receipt_items')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$receiptItem])->first():null; + $receipt=$line?$this->db->table('goods_receipts')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$line->goods_receipt_id])->first():null; + $total=$this->db->table('stock_movements')->where(['tenant_id'=>$this->scope['tenant_id'],'movement_type'=>'supplier_return'])->where('snapshot->disposition->source->receipt->item_id',$receiptItem)->sum('quantity_milli'); + if(!$line||!$receipt||$receipt->confirmed_at===null||$line->stock_item_id!==$m->stock_item_id||$receipt->warehouse_id!==$m->warehouse_id + ||($data['kind']??null)!=='supplier_return'||($data['stock_item_id']??null)!==$m->stock_item_id||($data['warehouse_id']??null)!==$m->warehouse_id||($data['receipt_item_id']??null)!==$receiptItem + ||($data['quantity_milli']??null)!==abs((int)$m->quantity_milli)||($done['stock_operation_id']??null)!==$op->id||$executed->created_by!==$m->moved_by + ||$approved->created_by===$executed->created_by||($review['reviewed_by']??null)!==$approved->created_by||in_array($approved->created_by,$review['contributors']??[],true) + ||($proof['record_key']??null)!==$approved->record_key||$op->idempotency_key!==$approved->record_key||($proof['financial_correction_created']??null)!==false + ||Payables::canonical($proof['source']??null)!==Payables::canonical($source)||abs((int)$total)>(int)$line->accepted_milli) + throw new \DomainException('Devolução sem origem, quantidade, revisão e execução reconciliadas.'); + return ['supplier_return_approved'=>(array)$approved,'supplier_return_executed'=>(array)$executed,'supplier_receipt_item'=>(array)$line,'supplier_receipt'=>(array)$receipt]; + } +} diff --git a/packages/general-ledger/src/StockTransitEvidence.php b/packages/general-ledger/src/StockTransitEvidence.php new file mode 100644 index 0000000..740154e --- /dev/null +++ b/packages/general-ledger/src/StockTransitEvidence.php @@ -0,0 +1,44 @@ +db->table('stock_movements')->where(['tenant_id'=>$this->scope['tenant_id'],'stock_item_id'=>$book->stock_item_id,'warehouse_id'=>$book->warehouse_id])->sum('quantity_milli'); + $owned=$physical;$transit=0; + if(!LedgerSchema::externalHostComposition($this->db)||!$this->db->getSchemaBuilder()->hasTable('sgf_stock_transit_events'))return ['owned_quantity_milli'=>$owned,'in_transit_milli'=>0]; + foreach($this->db->table('sgf_stock_transit_events')->where($this->scope)->where(['stock_item_id'=>$book->stock_item_id,'source_warehouse_id'=>$book->warehouse_id,'action'=>'dispatch'])->orderBy('id')->cursor() as $event){ + $movement=$this->db->table('stock_movements')->where('id',$event->movement_id)->first();$proof=$movement?$this->movement($movement):null;if(!$proof)throw new \DomainException('Propriedade interna não conferida.'); + $returned=(int)$this->db->table('sgf_stock_transit_events')->where($this->scope)->where(['transfer_key'=>$event->transfer_key,'action'=>'return'])->sum('quantity_milli');$owned+=(int)$event->quantity_milli-$returned;$transit+=$proof['remaining_milli']; + } + foreach($this->db->table('stock_movements')->where(['tenant_id'=>$this->scope['tenant_id'],'stock_item_id'=>$book->stock_item_id])->where('warehouse_id','<>',$book->warehouse_id)->orderBy('id')->cursor() as $outside)if($this->movement($outside)===null)throw new \DomainException('Movimento fora do livro de origem exige conferência própria.'); + return ['owned_quantity_milli'=>$owned,'in_transit_milli'=>$transit]; + } + public function movement(object $m):?array + { + $snapshot=json_decode($m->snapshot,true);if(!isset($snapshot['sgf_internal_transit']))return null; + if(!LedgerSchema::externalHostComposition($this->db)||!$this->db->getSchemaBuilder()->hasTable('sgf_stock_transit_events'))throw new \DomainException('Trânsito sem protocolo autorizado.'); + $t=$snapshot['sgf_internal_transit'];$events=$this->db->table('sgf_stock_transit_events')->where($this->scope)->where('transfer_key',$t['transfer_key']??'')->orderBy('id'); + $dispatch=(clone $events)->where('action','dispatch')->first();if(!$dispatch||$dispatch->stock_item_id!==$m->stock_item_id||($t['scope']??null)!==$this->scope)throw new \DomainException('Origem de propriedade em trânsito não demonstrada.'); + $book=$this->db->table('gl_stock_books')->where($this->scope)->where(['stock_item_id'=>$m->stock_item_id,'warehouse_id'=>$dispatch->source_warehouse_id])->first(); + if(!$book)throw new \DomainException('Trânsito sem livro de origem.');$rate=$this->db->table('gl_stock_rates')->where($this->scope)->where('book_id',$book->id)->orderByDesc('version')->first();$review=$rate?$this->db->table('gl_stock_rate_reviews')->where($this->scope)->where('rate_id',$rate->id)->where('decision','approved')->first():null; + if(!$rate||!$review||$rate->prepared_by===$review->reviewed_by||$rate->person===$review->person)throw new \DomainException('Trânsito sem taxa de propriedade revista.'); + $done=0;$seen=0;$found=false;$hash=hash_init('sha256'); + foreach($events->cursor() as $e){ + if($e->action==='dispatch')$seen++;else $done+=(int)$e->quantity_milli; + $row=$this->db->table('stock_movements')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$e->movement_id])->first();$s=$row?json_decode($row->snapshot,true):null; + $warehouse=$e->action==='receive'?$dispatch->destination_warehouse_id:$dispatch->source_warehouse_id;$sign=$e->action==='dispatch'?-1:1; + if(!$row||$e->stock_item_id!==$dispatch->stock_item_id||$e->source_warehouse_id!==$dispatch->source_warehouse_id||$e->destination_warehouse_id!==$dispatch->destination_warehouse_id||$row->stock_item_id!==$dispatch->stock_item_id||$row->warehouse_id!==$warehouse||(int)$row->quantity_milli!==$sign*(int)$e->quantity_milli||$row->moved_by!==$e->actor_id||$row->movement_type!==($sign<0?'transfer_out':'transfer_in')||($s['sgf_internal_transit']['transfer_key']??null)!==$dispatch->transfer_key||($s['sgf_internal_transit']['scope']??null)!==$this->scope||($e->action==='receive'&&($e->actor_id===$dispatch->actor_id||$e->host_actor_id===$dispatch->host_actor_id)))throw new \DomainException('Parcela interna sem correspondência física ou independência.'); + $operation=$this->db->table('stock_operations')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$row->stock_operation_id])->first(); + if(!$operation||$operation->kind!=='transfer'||$operation->idempotency_key!==$e->command_key||$operation->payload_hash!==$e->request_hash||$operation->created_by!==$e->actor_id||(int)$operation->quantity_milli!==(int)$e->quantity_milli||Payables::canonical(json_decode($e->snapshot,true))!==Payables::canonical($s))throw new \DomainException('Transferência sem operação única auditada.'); + if($row->id===$m->id)$found=true;hash_update($hash,Payables::canonical([(array)$e,(array)$row,(array)$operation])); + } + if(!$found||$seen!==1||$done>(int)$dispatch->quantity_milli)throw new \DomainException('Trânsito duplicado ou sobre-recebido.'); + return ['classification'=>'internal_owned_transfer','journal_required'=>false,'transfer_key'=>$dispatch->transfer_key,'remaining_milli'=>(int)$dispatch->quantity_milli-$done,'unit_minor'=>$rate->unit_minor,'rate_id'=>$rate->id,'evidence_hash'=>hash_final($hash),'ownership_change_minor'=>0]; + } +} diff --git a/packages/general-ledger/src/StockValuation.php b/packages/general-ledger/src/StockValuation.php index 080caf3..e47b34f 100644 --- a/packages/general-ledger/src/StockValuation.php +++ b/packages/general-ledger/src/StockValuation.php @@ -1,6 +1,8 @@ 'stock.receipt','issue'=>'stock.issue','workshop_return_in'=>'stock.return']; - public function __construct(private ConnectionInterface $db,private array $scope,private int $actor,private string $person,private \Closure $authorize,private \Closure $readAuthorize){} + public const TYPES=['receipt_in'=>'stock.receipt','issue'=>'stock.issue','workshop_return_in'=>'stock.return','adjustment_out'=>'stock.adjustment','supplier_return'=>'stock.supplier_return']; + public function __construct(private ConnectionInterface $db,private array $scope,private int $actor,private string $person,private \Closure $authorize,private \Closure $readAuthorize,private ?\Closure $materialIsolation=null){} private function q(string $name){return $this->db->table('gl_stock_'.$name)->where($this->scope);} private function hash(mixed $v):string{return hash('sha256',Payables::canonical($v));} private function ready():void{ LedgerSchema::assertSynthetic($this->db);($this->readAuthorize)(); if(!$this->db->getSchemaBuilder()->hasTable('gl_stock_books'))throw new \DomainException('Valorização de stock não composta.'); - if($this->db->table('gl_journals')->where($this->scope)->whereIn('snapshot->source->effect_type',['workshop.direct_cost.validated','industry.cost.validated'])->exists())throw new \DomainException('Livro exclusivo: custos directos/industriais já reconhecidos; não duplicar material.'); + if($this->materialIsolation!==null)($this->materialIsolation)(); + elseif($this->db->table('gl_journals')->where($this->scope)->whereIn('snapshot->source->effect_type',['workshop.direct_cost.validated','industry.cost.validated'])->exists())throw new \DomainException('Livro exclusivo: custos directos/industriais já reconhecidos; não duplicar material.'); } private function book(int $id):object{return $this->q('books')->where('id',$id)->first()??throw new \DomainException('Livro de stock fora do contexto.');} private function movements(object $book){return $this->db->table('stock_movements')->where(['tenant_id'=>$this->scope['tenant_id'],'stock_item_id'=>$book->stock_item_id,'warehouse_id'=>$book->warehouse_id]);} @@ -27,6 +30,10 @@ final class StockValuation }); // No list of all movement IDs, and no transfer of prior full snapshots. $prior=$this->movements($book)->where('id','<',$movement->id); + // Internal transfers carry ownership without an expense journal. Validate every excluded + // physical row, not merely a caller-supplied movement label. + foreach((clone $prior)->whereNotNull('snapshot->sgf_internal_transit')->cursor() as $internal)(new StockTransitEvidence($this->db,$this->scope))->movement($internal); + $prior->whereNull('snapshot->sgf_internal_transit'); $count=(clone $prior)->count();if($count===0)return []; $qualified=[];foreach($this->scope as$key=>$value)$qualified['j.'.$key]=$value; $valued=$this->db->table('gl_journals as j')->join('stock_movements as pm',function($join)use($book,$movement){ @@ -92,7 +99,7 @@ SQL); $this->ready();if(!in_array($kind,self::TYPES,true))throw new \DomainException('Tipo físico não suportado.'); $m=$this->db->table('stock_movements')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$id])->first()??throw new \DomainException('Movimento físico fora do contexto.'); $b=$this->q('books')->where(['stock_item_id'=>$m->stock_item_id,'warehouse_id'=>$m->warehouse_id])->first()??throw new \DomainException('Artigo/armazém não composto neste contexto.'); - if((self::TYPES[$m->movement_type]??null)!==$kind||(int)$m->quantity_milli===0||($kind==='stock.issue')!==((int)$m->quantity_milli<0))throw new \DomainException('Tipo/sinal físico incompatível.'); + if((self::TYPES[$m->movement_type]??null)!==$kind||(int)$m->quantity_milli===0||in_array($kind,['stock.issue','stock.adjustment','stock.supplier_return'],true)!==((int)$m->quantity_milli<0))throw new \DomainException('Tipo/sinal físico incompatível.'); $rate=$this->q('rates')->where('book_id',$b->id)->orderByDesc('version')->first();$review=$rate?$this->q('rate_reviews')->where('rate_id',$rate->id)->first():null; if(!$rate||!$review||$review->decision!=='approved'||$review->reviewed_by===$rate->prepared_by||$review->person===$rate->person)throw new \DomainException('Taxa actual completa e validada distintamente obrigatória.'); $item=$this->db->table('stock_items')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$m->stock_item_id])->first(); @@ -106,18 +113,35 @@ SQL); $dependencies=['line'=>(array)$line,'receipt'=>(array)$receipt]; }else{ $operation=$this->db->table('stock_operations')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$m->stock_operation_id])->first(); - if(!$operation||$operation->stock_item_id!==$m->stock_item_id||$operation->warehouse_id!==$m->warehouse_id||(int)$operation->quantity_milli!==abs((int)$m->quantity_milli)||$operation->created_by!==$m->moved_by)throw new \DomainException('Operação física incompatível.'); + if(!$operation||$operation->stock_item_id!==$m->stock_item_id||$operation->warehouse_id!==$m->warehouse_id||($kind!=='stock.adjustment'&&(int)$operation->quantity_milli!==abs((int)$m->quantity_milli))||$operation->created_by!==$m->moved_by)throw new \DomainException('Operação física incompatível.'); $dependencies=['operation'=>(array)$operation]; - if($kind==='stock.issue'){ + if($kind==='stock.supplier_return'){ + $dependencies+=(new StockSupplierReturnEvidence($this->db,$this->scope))->check($m,$operation,$snapshot); + }elseif($kind==='stock.adjustment'){ + if(!LedgerSchema::externalHostComposition($this->db)||$operation->kind!=='adjustment')throw new \DomainException('Ajuste fora do recorte composto.'); + $count=$this->db->table('stock_operations')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$operation->count_id])->first();$plan=json_decode($operation->snapshot,true)['lot_adjustment']??null;$observed=$count?json_decode($count->snapshot,true)['lot_count']??null:null; + if(!$count||$count->kind!=='count'||$count->created_by===$operation->created_by||$count->responsible_id===$operation->created_by||$count->stock_item_id!==$m->stock_item_id||$count->warehouse_id!==$m->warehouse_id||!$plan||!$observed||$plan['source_hash']!==$observed['source_hash']||$plan['increase_milli']!==0||$plan['decrease_milli']!==abs((int)$m->quantity_milli)||(int)$operation->quantity_milli!==(int)$count->quantity_milli||(int)$count->quantity_milli-(int)$operation->theoretical_milli!==(int)$m->quantity_milli)throw new \DomainException('Ajuste sem contagem por lotes e revisão distinta.'); + $partition=$this->db->table('inventory_lot_allocations')->where(['tenant_id'=>$this->scope['tenant_id'],'stock_movement_id'=>$m->id])->sum('quantity_milli'); + if((int)$partition!==(int)$m->quantity_milli)throw new \DomainException('Ajuste não reconciliado com os lotes.'); + $dependencies+=['count'=>(array)$count,'lot_plan'=>$plan]; + }elseif($kind==='stock.issue'){ if($operation->kind!=='issue'||!preg_match('/^[a-f0-9]{64}$/D',$snapshot['inventory_operation_hash']??''))throw new \DomainException('Saída original incompleta.'); }else{ $parentId=$snapshot['delivery_movement_id']??null;$parent=is_int($parentId)?$this->movements($b)->where('id',$parentId)->first():null; $total=$this->movements($b)->where('movement_type','workshop_return_in')->where('snapshot->delivery_movement_id',$parentId)->sum('quantity_milli'); if(!$parent||$parent->movement_type!=='issue'||$operation->kind!=='workshop_return'||!($snapshot['received_in_warehouse']??false)||$total>abs((int)$parent->quantity_milli)||!preg_match('/^[a-f0-9]{64}$/D',$snapshot['inventory_return_hash']??''))throw new \DomainException('Devolução sem saída original/limite/recepção física.'); + if(isset($snapshot['workshop_origin'])){ + if(!LedgerSchema::externalHostComposition($this->db)||!$this->db->getSchemaBuilder()->hasTable('ws_returns')||($snapshot['scope']??null)!==$this->scope)throw new \DomainException('Devolução Oficina sem composição física qualificada.'); + $claim=$this->db->table('ws_returns')->where($this->scope)->where('movement_id',$m->id)->first();$proposal=$claim?$this->db->table('ws_proposals')->where($this->scope)->where('id',$claim->proposal_id)->first():null;$frozen=$proposal?json_decode($proposal->snapshot,true):null; + $allocated=$this->db->table('ws_allocations')->where($this->scope)->where('movement_id',$parentId)->sum('quantity_milli'); + if(!$claim||!$proposal||$claim->preparer_id===$claim->confirmer_id||$claim->parent_movement_id!==$parentId||(int)$claim->quantity_milli!==(int)$m->quantity_milli||$claim->operation_id!==$operation->id||$claim->source_hash!==$proposal->source_hash||$claim->entry_id!==($snapshot['workshop_origin']['id']??null)||$proposal->entry_version!==($snapshot['workshop_origin']['version']??null)||($frozen['delivery']['movement']['id']??null)!==$parentId||$allocated+$total>abs((int)$parent->quantity_milli)||Payables::canonical($frozen['entry']??null)!==Payables::canonical($snapshot['workshop_origin'])||($snapshot['verification']['actor_id']??null)===($snapshot['workshop_origin']['actor_id']??null))throw new \DomainException('Devolução Oficina sem proposta individual, revisão distinta ou limite reconciliado.'); + $dependencies+=['parent'=>(array)$parent,'workshop_proposal'=>(array)$proposal,'workshop_return'=>(array)$claim]; + }else{ $declaration=$this->db->table('supply_records')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$snapshot['declaration_id']??0])->first(); $confirmation=$declaration?$this->db->table('supply_records')->where(['tenant_id'=>$this->scope['tenant_id'],'record_key'=>$declaration->record_key,'version'=>2,'state'=>'confirmed'])->first():null; if(!$declaration||!$confirmation||$declaration->created_by===$confirmation->created_by||$confirmation->created_by!==$m->moved_by)throw new \DomainException('Devolução sem conferência distinta.'); $dependencies+=['parent'=>(array)$parent,'declaration'=>(array)$declaration,'confirmation'=>(array)$confirmation]; + } } } try{$amount=BigRational::of((string)abs((int)$m->quantity_milli))->multipliedBy($rate->unit_minor)->dividedBy(1000)->toBigInteger()->toInt();}catch(\Throwable $e){throw new \DomainException('Valor não representável em cêntimos exactos.',0,$e);} @@ -145,18 +169,32 @@ SQL); ->orderByRaw("snapshot->'source'->>'stock_movement_id',id")->get()->keyBy('movement_id'); foreach($batch as$m){ $facts++;hash_update($h,Payables::canonical((array)$m)); + try{$internal=(new StockTransitEvidence($this->db,$this->scope))->movement($m);if($internal!==null){hash_update($h,Payables::canonical($internal));continue;}}catch(\DomainException){$pending++;continue;} try{$s=$this->source(self::TYPES[$m->movement_type]??'',(int)$m->id);hash_update($h,$s['stock_fact_revision']);$j=$posted->get($m->id);if(!$j||json_decode($j->fact_revision??'null',true)!==$s['stock_fact_revision'])$pending++;}catch(\DomainException){$pending++;} } }); + // A transferred item may be at a destination outside its origin valuation book. + // Any unrelated out-of-book movement is an explicit pending fact, never silently + // omitted from closing or classified as an internal transfer by label alone. + if(LedgerSchema::externalHostComposition($this->db))foreach($this->db->table('stock_movements')->where(['tenant_id'=>$this->scope['tenant_id'],'stock_item_id'=>$b->stock_item_id])->where('warehouse_id','<>',$b->warehouse_id)->whereDate('moved_at','>=',$period['starts_on'])->whereDate('moved_at','<=',$period['ends_on'])->orderBy('id')->cursor() as $outside){ + hash_update($h,Payables::canonical((array)$outside));try{if((new StockTransitEvidence($this->db,$this->scope))->movement($outside)===null)$pending++;}catch(\DomainException){$pending++;} + } + } + // Destination movements are outside the origin valuation book, but remain part of + // the same ownership proof and must not disappear from period conference. + if(LedgerSchema::externalHostComposition($this->db)&&$this->db->getSchemaBuilder()->hasTable('sgf_stock_transit_events'))foreach($this->db->table('sgf_stock_transit_events')->where($this->scope)->orderBy('id')->cursor() as $event){ + hash_update($h,Payables::canonical((array)$event));$m=$this->db->table('stock_movements')->where(['tenant_id'=>$this->scope['tenant_id'],'id'=>$event->movement_id])->first(); + try{if(!$m||(new StockTransitEvidence($this->db,$this->scope))->movement($m)===null)$pending++;}catch(\DomainException){$pending++;} } return ['facts'=>$facts,'pending'=>$pending,'hash'=>hash_final($h)]; } public function page(int $bookId,int $page=1,int $perPage=25):array { $this->ready();if($page<1||$page>100000||!in_array($perPage,[25,100],true))throw new \InvalidArgumentException('Página inválida.');$b=$this->book($bookId);$q=$this->movements($b);$count=(clone$q)->count();$rows=[]; - foreach($q->orderBy('id')->offset(($page-1)*$perPage)->limit($perPage)->get()as$m){$row=['movement'=>(array)$m,'source'=>null,'pending'=>null];try{$row['source']=$this->source(self::TYPES[$m->movement_type]??'',(int)$m->id);}catch(\DomainException $e){$row['pending']=$e->getMessage();}$rows[]=$row;} + foreach($q->orderBy('id')->offset(($page-1)*$perPage)->limit($perPage)->get()as$m){$row=['movement'=>(array)$m,'source'=>null,'pending'=>null];try{$internal=(new StockTransitEvidence($this->db,$this->scope))->movement($m);if($internal!==null)$row['nonfinancial_reconciliation']=$internal;else $row['source']=$this->source(self::TYPES[$m->movement_type]??'',(int)$m->id);}catch(\DomainException $e){$row['pending']=$e->getMessage();}$rows[]=$row;} $rates=$this->q('rates')->where('book_id',$b->id)->orderByDesc('version')->limit(25)->get();$reviews=$this->q('rate_reviews')->whereIn('rate_id',$rates->pluck('id')->all())->get()->keyBy('rate_id');foreach($rates as$r)$r->review=$reviews->get($r->id); - return ['book'=>(array)$b,'rows'=>$rows,'pagination'=>['page'=>$page,'per_page'=>$perPage,'total'=>$count],'rates'=>$rates->all(),'physical_balance_milli'=>(int)$this->movements($b)->sum('quantity_milli'),'does_not_move_stock'=>true]; + try{$ownership=(new StockTransitEvidence($this->db,$this->scope))->ownership($b);}catch(\DomainException $e){$ownership=['owned_quantity_milli'=>null,'in_transit_milli'=>null,'ownership_pending'=>$e->getMessage()];} + return ['book'=>(array)$b,'rows'=>$rows,'pagination'=>['page'=>$page,'per_page'=>$perPage,'total'=>$count],'rates'=>$rates->all(),'physical_balance_milli'=>(int)$this->movements($b)->sum('quantity_milli'),'ownership'=>$ownership,'does_not_move_stock'=>true]; } public function receipt(string $key):?array { diff --git a/packages/inventory-issue/releases/0.1.0/bootstrap.php b/packages/inventory-issue/releases/0.1.0/bootstrap.php new file mode 100644 index 0000000..72950d0 --- /dev/null +++ b/packages/inventory-issue/releases/0.1.0/bootstrap.php @@ -0,0 +1,2 @@ +db->transactionLevel()<1)throw new \LogicException('Issue requires the host transaction');($this->authorize)();} + private function operation(int $id,bool $lock=false): object + { + $q=$this->db->table($this->schema.'.stock_operations')->where(['tenant_id'=>$this->tenant,'id'=>$id]);if($lock)$q->lockForUpdate(); + $o=$q->first();abort_unless($o,403);abort_unless($o->kind==='issue'&&(int)$o->created_by===$this->actor&&BigDecimal::of((string)$o->quantity_milli)->isPositive(),409,'Operação incompatível com saída física.');return $o; + } + public function preview(int $id): string {$this->host();return hash('sha256',json_encode((array)$this->operation($id),JSON_THROW_ON_ERROR));} + public function write(int $id,string $hash): array + { + $this->host();$o=$this->operation($id); + // Lock the actual warehouse, including a previously empty book. Hosts keep their company lock too. + $warehouse=$this->db->table($this->schema.'.warehouses')->where(['tenant_id'=>$this->tenant,'id'=>$o->warehouse_id])->lockForUpdate()->first();abort_unless($warehouse,403); + abort_unless($this->db->table($this->schema.'.stock_items')->where(['tenant_id'=>$this->tenant,'id'=>$o->stock_item_id])->lockForUpdate()->first(),403); + $o=$this->operation($id,true);abort_unless(hash_equals($hash,hash('sha256',json_encode((array)$o,JSON_THROW_ON_ERROR))),409,'A operação mudou. Faça nova conferência.'); + $q=$this->db->table($this->schema.'.stock_movements')->where(['tenant_id'=>$this->tenant,'stock_operation_id'=>$id,'warehouse_id'=>$o->warehouse_id]);$old=$q->first(); + $negative=(string)BigDecimal::of((string)$o->quantity_milli)->negated(); + if($old){$s=json_decode($old->snapshot,true,512,JSON_THROW_ON_ERROR);abort_unless(($s['inventory_operation_hash']??null)===$hash&&(string)$old->quantity_milli===$negative&&(int)$old->stock_item_id===(int)$o->stock_item_id,409,'Saída existente incompatível; exige reconciliação.');return ['movement_id'=>(int)$old->id,'replayed'=>true];} + $book=$this->db->table($this->schema.'.stock_movements')->where(['tenant_id'=>$this->tenant,'stock_item_id'=>$o->stock_item_id,'warehouse_id'=>$o->warehouse_id])->selectRaw('coalesce(sum(quantity_milli),0) AS quantity,coalesce(max(id),0) AS revision')->first(); + abort_unless((string)$o->ledger_version===(string)$book->revision,409,'O livro mudou. Faça nova conferência.'); + abort_unless(BigDecimal::of((string)$book->quantity)->isGreaterThanOrEqualTo((string)$o->quantity_milli),409,'Stock físico insuficiente.'); + $snapshot=json_decode($o->snapshot,true,512,JSON_THROW_ON_ERROR)+['reference'=>$o->reference,'reason'=>$o->reason,'inventory_operation_hash'=>$hash]; + $movement=$this->db->table($this->schema.'.stock_movements')->insertGetId(['tenant_id'=>$this->tenant,'stock_operation_id'=>$id,'stock_item_id'=>$o->stock_item_id,'warehouse_id'=>$o->warehouse_id, + 'equipment_id'=>$o->equipment_id??null,'spare_part_id'=>$o->spare_part_id??null,'moved_by'=>$this->actor,'movement_type'=>'issue','quantity_milli'=>$negative,'snapshot'=>json_encode($snapshot,JSON_THROW_ON_ERROR),'moved_at'=>now()]); + return ['movement_id'=>(int)$movement,'replayed'=>false]; + } +} diff --git a/packages/inventory-write/releases/0.1.0/bootstrap.php b/packages/inventory-write/releases/0.1.0/bootstrap.php new file mode 100644 index 0000000..9bb87f7 --- /dev/null +++ b/packages/inventory-write/releases/0.1.0/bootstrap.php @@ -0,0 +1,2 @@ +checkHost();$facts=$this->facts($lineId,false); + return ['source_hash'=>$this->hash($facts),'receipt_item_id'=>$lineId,'accepted_milli'=>(string)$facts['accepted_milli'], + 'stock_item_id'=>(int)$facts['stock_item_id'],'warehouse_id'=>(int)$facts['warehouse_id']]; + } + + public function write(int $lineId, string $expectedHash): array + { + $this->checkHost();$facts=$this->facts($lineId,true);$hash=$this->hash($facts); + abort_unless(hash_equals($hash,$expectedHash),409,'A origem da recepção mudou. Faça nova conferência.'); + $existing=$this->db->table($this->schema.'.stock_movements')->where(['tenant_id'=>$this->tenant,'goods_receipt_item_id'=>$lineId])->first(); + if($existing){ + $snapshot=json_decode($existing->snapshot,true,512,JSON_THROW_ON_ERROR); + abort_unless(($snapshot['inventory_source_hash']??null)===$hash && (string)$existing->quantity_milli===(string)$facts['accepted_milli'] + && (int)$existing->stock_item_id===(int)$facts['stock_item_id'] && (int)$existing->warehouse_id===(int)$facts['warehouse_id'],409,'A origem já tem movimento incompatível ou histórico não reconciliado.'); + return ['movement_id'=>(int)$existing->id,'source_hash'=>$hash,'replayed'=>true]; + } + $lineSnapshot=json_decode($facts['line_snapshot'],true,512,JSON_THROW_ON_ERROR); + $receiptSnapshot=json_decode($facts['receipt_snapshot'],true,512,JSON_THROW_ON_ERROR); + $snapshot=[...$lineSnapshot,'receipt_id'=>(int)$facts['receipt_id'],'order_number'=>$receiptSnapshot['order']['number']??null, + 'delivery_note'=>$facts['delivery_note'],'reference'=>$facts['delivery_note'],'inventory_source_hash'=>$hash]; + $id=$this->db->table($this->schema.'.stock_movements')->insertGetId([ + 'tenant_id'=>$this->tenant,'goods_receipt_item_id'=>$lineId,'stock_item_id'=>$facts['stock_item_id'],'warehouse_id'=>$facts['warehouse_id'], + 'moved_by'=>$this->actor,'quantity_milli'=>$facts['accepted_milli'],'movement_type'=>'receipt_in','moved_at'=>now(), + 'equipment_id'=>$lineSnapshot['equipment_id']??null,'spare_part_id'=>$lineSnapshot['spare_part_id']??null, + 'snapshot'=>json_encode($snapshot,JSON_THROW_ON_ERROR), + ]); + return ['movement_id'=>(int)$id,'source_hash'=>$hash,'replayed'=>false]; + } + + private function checkHost(): void + { + if($this->db->transactionLevel()<1)throw new \LogicException('Receipt movement requires the host atomic transaction'); + ($this->authorize)(); + } + + private function facts(int $lineId,bool $lock): array + { + $q=$this->db->table($this->schema.'.goods_receipt_items AS l') + ->join($this->schema.'.goods_receipts AS r',fn($j)=>$j->on('r.id','=','l.goods_receipt_id')->on('r.tenant_id','=','l.tenant_id')) + ->where('l.tenant_id',$this->tenant)->where('l.id',$lineId) + ->select(['l.id','l.tenant_id','l.stock_item_id','l.received_milli','l.accepted_milli','l.rejected_milli','l.quality_status','l.notes', + 'l.snapshot AS line_snapshot','r.id AS receipt_id','r.purchase_order_id','r.received_by','r.warehouse_id','r.delivery_note','r.received_on','r.confirmed_at','r.snapshot AS receipt_snapshot']); + if($lock)$q->lockForUpdate(); + $row=$q->first();abort_unless($row,403,'Origem fora do âmbito autorizado.');$facts=(array)$row; + $snapshot=json_decode($facts['line_snapshot'],true,512,JSON_THROW_ON_ERROR); + abort_unless((int)$facts['received_by']===$this->actor,403,'A origem não pertence ao recebedor desta confirmação.'); + abort_unless($facts['confirmed_at']!==null && ($snapshot['physical']??false)===true && (int)$facts['accepted_milli']>0 + && $facts['stock_item_id']!==null && $facts['warehouse_id']!==null,409,'A origem não comprova quantidade física aceite.'); + return $facts; + } + + private function hash(array $facts): string + { + // Parent status/number are intentionally absent: finalisation occurs later in the same transaction. + return hash('sha256',json_encode($facts,JSON_THROW_ON_ERROR)); + } +} diff --git a/packages/sgc-invoice-client/src/InvoiceClient.php b/packages/sgc-invoice-client/src/InvoiceClient.php new file mode 100644 index 0000000..187722b --- /dev/null +++ b/packages/sgc-invoice-client/src/InvoiceClient.php @@ -0,0 +1,130 @@ +512 || !preg_match('/^[a-f0-9]{64}$/D',$context) + || array_keys($sourceScope)!==['tenant_id'] || !is_int($sourceScope['tenant_id']) || $sourceScope['tenant_id']<1 + || array_keys($targetScope)!==['tenant_id','company_id','unit_id']) throw new InvalidArgumentException('Invalid explicit connection.'); + foreach ($targetScope as $value) if(!is_int($value)||$value<1)throw new InvalidArgumentException('Invalid target scope.'); + $this->binding=self::hash([$origin,$client,$context,$sourceScope,$targetScope]); + } + + public function __debugInfo(): array { return ['origin'=>$this->origin,'client'=>$this->client,'credentials'=>'[redacted]']; } + + /** Caller supplies reviewed mappings and an unregistered source draft, never an existing SGC invoice. */ + public function intent(array $mapping, array $draft, string $key, string $reason): array + { + self::keys($mapping,['id','version','source_scope','target_scope','order_id','lines','authority']); + self::keys($draft,['reference','version','state','document']); + if(!is_string($mapping['id']??null)||$mapping['id']===''||!is_int($mapping['version']??null)||$mapping['version']<1 + ||($mapping['source_scope']??null)!==$this->sourceScope||($mapping['target_scope']??null)!==$this->targetScope + ||($mapping['authority']??null)!=='sgf_only'||!is_int($mapping['order_id']??null)||$mapping['order_id']<1 + ||!is_array($mapping['lines']??null)||($draft['state']??null)!=='unregistered_draft' + ||!is_string($draft['reference']??null)||$draft['reference']===''||!is_string($draft['version']??null)||$draft['version']==='' + ||!preg_match('/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[1-8][a-fA-F0-9]{3}-[89aAbB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$/D',$key) + ||strlen($reason)<10||strlen($reason)>2000)throw new InvalidArgumentException('Explicit draft authority/mapping required.'); + $document=$draft['document']??[]; self::keys($document,['number','issued_on','subtotal','tax','total','items']); + foreach(['number','issued_on','subtotal','tax','total']as$field)if(!is_string($document[$field]??null)||$document[$field]==='')throw new InvalidArgumentException('Document field required.'); + foreach(['subtotal','tax','total']as$field)if(!preg_match('/^\d{1,12}\.\d{2}$/D',$document[$field]))throw new InvalidArgumentException('Exact decimal strings required.'); + if(!is_array($document['items']??null)||count($document['items'])<1||count($document['items'])>50)throw new InvalidArgumentException('Explicit lines required.'); + $lines=[];$used=[]; + foreach($document['items']as$item){ + self::keys($item,['source_line_ref','quantity','unit','unit_price','tax_rate']); + $ref=$item['source_line_ref']??null;$id=is_string($ref)?($mapping['lines'][$ref]??null):null; + if(!is_int($id)||$id<1||in_array($id,$used,true))throw new InvalidArgumentException('Missing or repeated explicit line mapping.'); + foreach(['quantity','unit','unit_price','tax_rate']as$field)if(!is_string($item[$field]??null)||$item[$field]==='')throw new InvalidArgumentException('Exact line fields required.'); + $used[]=$id;unset($item['source_line_ref']);$lines[]=['purchase_order_item_id'=>$id]+$item; + } + $document['items']=$lines; + $intent=['schema'=>1,'binding'=>$this->binding,'mapping_hash'=>self::hash($mapping),'source_reference'=>$draft['reference'], + 'source_version'=>$draft['version'],'source_hash'=>self::hash($draft),'operation_key'=>strtolower($key), + 'order_id'=>$mapping['order_id'],'reason'=>$reason,'data'=>$document]; + if(strlen(json_encode($intent,JSON_THROW_ON_ERROR))>65536)throw new InvalidArgumentException('Document too large.'); + return $intent+['intent_hash'=>self::hash($intent)]; + } + + public static function mappingHash(array $mapping): string { return self::hash($mapping); } + + public function preview(array $intent, string $currentSourceVersion, string $currentMappingHash): array + { + $this->verifyIntent($intent,$currentSourceVersion,$currentMappingHash); + $base=['intent'=>$intent,'phase'=>'draft']; + $context=$this->call('GET','/api/v1/integration/sgc/invoices/orders/'.$intent['order_id'],null,$intent['operation_key']); + if(($context['status']??0)!==200)return $this->failed($base,$context,false); + $view=$context['json']??[]; + if(($view['order']['id']??null)!==$intent['order_id']||!preg_match('/^[a-f0-9]{64}$/D',$view['revision']??''))throw new LogicException('Unexpected order context.'); + $r=$this->call('POST','/api/v1/integration/sgc/invoices',['phase'=>'preview','order_id'=>$intent['order_id'], + 'revision'=>$view['revision'],'reason'=>$intent['reason'],'data'=>$intent['data']],$intent['operation_key']); + if(($r['status']??0)!==200)return $this->failed($base,$r,false); + if(isset($r['json']['invoice_id']))return $this->received($base,$r,true); + if(!is_string($r['json']['preview']??null)||!is_array($r['json']['impact']??null))throw new LogicException('Unexpected preview response.'); + $state=['intent'=>$intent,'phase'=>'previewed','preview'=>$r['json']['preview'],'impact'=>$r['json']['impact']]; + return $state+['approval_hash'=>self::hash($state)]; + } + + /** The human decision must refer to this exact impact, source version and intention. */ + public function confirm(array $state, string $approvedHash, string $currentSourceVersion, string $currentMappingHash): array + { + $this->verifyIntent($state['intent']??[],$currentSourceVersion,$currentMappingHash); + $original=['intent'=>$state['intent'],'phase'=>'previewed','preview'=>$state['preview']??null,'impact'=>$state['impact']??null]; + if(!in_array($state['phase']??null,['previewed','uncertain'],true)||!is_string($original['preview'])||!is_array($original['impact']) + ||!is_string($state['approval_hash']??null)||!hash_equals(self::hash($original),$state['approval_hash']) + ||!hash_equals($state['approval_hash'],$approvedHash))throw new LogicException('Individual approval of current impact required.'); + $intent=$state['intent']; + $r=$this->call('POST','/api/v1/integration/sgc/invoices',['phase'=>'confirm','order_id'=>$intent['order_id'],'preview'=>$state['preview']],$intent['operation_key']); + if(!in_array($r['status']??0,[200,201],true))return $this->failed($state,$r,true); + return $this->received($state,$r,($r['status']??0)===200); + } + + private function received(array $state,array $r,bool $replay):array + { + $receipt=$r['json']??[]; + if(!is_int($receipt['invoice_id']??null)||$receipt['invoice_id']<1||($receipt['order_id']??null)!==$state['intent']['order_id'] + ||($receipt['status']??null)!=='pending'||($receipt['replayed']??null)!==$replay) { + // A malformed successful response may follow a committed write. Keep its original key/cut. + return array_replace($state,['phase'=>'uncertain','http_status'=>$r['status'],'error'=>'UNVERIFIED_RECEIPT']); + } + return array_replace($state,['phase'=>'received_pending','receipt'=>$receipt,'http_status'=>$r['status']]); + } + + private function verifyIntent(array $intent,string $sourceVersion,string $mappingHash):void + { + $hash=$intent['intent_hash']??null;unset($intent['intent_hash']); + if(!is_string($hash)||!hash_equals(self::hash($intent),$hash)||($intent['binding']??null)!==$this->binding + ||($intent['source_version']??null)!==$sourceVersion||($intent['mapping_hash']??null)!==$mappingHash)throw new LogicException('Source, mapping or connection changed; confer again.'); + } + private function call(string $method,string $path,?array $body,string $key):array + { + try{return ($this->transport)($method,$this->origin.$path,['Accept'=>'application/json','Authorization'=>'Bearer '.$this->token, + 'X-SGC-Client'=>$this->client,'X-SGC-Context'=>$this->context,'X-Idempotency-Key'=>$key],$body);} + catch(\Throwable){return ['status'=>0,'json'=>null];} // Never expose transport messages or credentials. + } + private function failed(array $state,array $response,bool $mayHaveCommitted):array + { + $status=$response['status']??0; + $phase=in_array($status,[401,403],true)?'blocked':($status===409?'review_required': + ($mayHaveCommitted&&($status===0||$status>=500||($status>=200&&$status<400))?'uncertain':'not_sent')); + return array_replace($state,['phase'=>$phase,'http_status'=>$status,'error'=>'DELIVERY_NOT_CONFIRMED']); + } + private static function keys(array $value,array $allowed):void + {if(array_diff(array_keys($value),$allowed))throw new InvalidArgumentException('Unknown fields.');} + private static function hash(mixed $value):string + { + $normalise=function($v)use(&$normalise){if(!is_array($v))return $v;if(!array_is_list($v))ksort($v,SORT_STRING);foreach($v as&$item)$item=$normalise($item);return $v;}; + return hash('sha256',json_encode($normalise($value),JSON_THROW_ON_ERROR|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)); + } +} diff --git a/public/sgf-bank.mjs b/public/sgf-bank.mjs new file mode 100644 index 0000000..8f7ada9 --- /dev/null +++ b/public/sgf-bank.mjs @@ -0,0 +1,18 @@ +import {confirmation} from './sgf-stock-final.mjs'; +const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content; +const states={imported:'Importado',statement_reviewed:'Extracto revisto',statement_refused:'Extracto recusado',match_prepared:'Correspondência proposta',match_reviewed:'Correspondência revista',match_refused:'Correspondência recusada',finalized:'Conferência finalizada'};let state,page=1,preview=null,operationKey=null,preparing=false,reading=false,readGeneration=0,dirty=false; +const money=n=>(n/100).toFixed(2),el=(tag,text,parent)=>{const e=document.createElement(tag);if(text!==null)e.textContent=text;parent.append(e);return e;},error=e=>$('message').textContent=e.message; +async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return{status:r.status,value:await r.json()};} +const op=confirmation(async body=>{if(operationKey){const old=await api('/api/bank-reconciliation/receipts/'+operationKey);if(old.status===200)return{status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/bank-reconciliation/confirm',body);}); +function locked(){const busy=Boolean(preparing||reading||preview||op.pending);$('fields').disabled=busy||!state||Boolean(state.blocked)||Boolean(state.finalized)||!Object.values(state.capabilities).some(Boolean);$('filters').disabled=busy;$('refresh').disabled=busy;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;} +function input(id,label,type='text',value='',required=true){const l=el('label',label,$('inputs')),e=el(type==='textarea'?'textarea':'input',null,l);e.id=id;e.required=required;if(type!=='textarea')e.type=type;e.value=value;return e;} +function select(id,label,choices){const l=el('label',label,$('inputs')),s=el('select',null,l);s.id=id;s.required=true;s.append(new Option('Escolher…',''));for(const [value,text]of choices)s.append(new Option(text,String(value)));return s;} +function fields(){if(preview||op.pending)return;$('inputs').replaceChildren();const action=$('action').value;if(action==='import'){input('account','Conta fictícia do exercício');input('currency','Moeda');input('period','Período','month');input('opening','Abertura explícita');input('closing','Fecho explícito');input('csv','Conteúdo CSV artificial','textarea').maxLength=8192;const sample=el('button','Carregar exemplo QA aprovado',$('inputs'));sample.id='example';sample.type='button';sample.onclick=()=>{$('account').value='QA-BANCO-LOCAL';$('currency').value='MZN';$('period').value='2026-11';$('opening').value='0.00';$('closing').value='0.00';$('csv').value=state.template_csv;dirty=true;};const file=input('csvFile','Ou ler o ficheiro CSV QA','file','',false);file.accept='.csv,text/csv';file.onchange=async()=>{if(preview||op.pending)return;try{const f=file.files?.[0];if(!f)return;if(f.size>8192)throw Error('O CSV QA não pode exceder8192 bytes.');preparing=true;locked();const text=await f.text();$('csv').value=text;dirty=true;}catch(e){error(e);}finally{preparing=false;locked();}};el('p','Saldo intermédio−20 faz parte do exercício; não autoriza descoberto nem representa caixa disponível.',$('inputs'));} + if(action==='review_statement'||action==='review_match')select('decision','Decisão explícita',[['reviewed','Rever favoravelmente'],['refused','Recusar']]);if(action==='prepare_match'){select('rowReference','Linha de extracto',state.rows.filter(r=>r.status==='unmatched').map(r=>[r.reference,r.reference+' · débito '+money(r.debit_minor)+' / crédito '+money(r.credit_minor)]));select('ledgerLine','Linha contabilística autorizada',state.source_lines.map(l=>[l.id,'Diário '+l.journal_id+' · '+l.account_code+' · débito '+money(l.debit_minor)+' / crédito '+money(l.credit_minor)]));}if(action==='review_match')select('proposal','Proposta exacta por rever',state.pending_proposals.map(p=>[p.id,'v'+p.version+' · '+p.data.row_reference+' → diário '+p.data.journal_id]));if(action==='finalize')el('p','Exige duas correspondências revistas, sem propostas pendentes e diferença0. Não cria lançamentos.',$('inputs'));} +function actions(){const prior=$('action').value;$('action').replaceChildren(new Option('Escolher…',''));for(const [value,label,right]of [['import','Importar extracto artificial','import'],['review_statement','Rever extracto importado','review'],['prepare_match','Propor correspondência individual','prepare'],['review_match','Rever correspondência exacta','review'],['finalize','Conferência final','review']])if(state.capabilities[right])$('action').append(new Option(label,value));if([...$('action').children].some(o=>o.value===prior))$('action').value=prior;fields();} +async function load(){if(preview||op.pending)throw Error('Conclua ou cancele a conferência em curso.');const generation=++readGeneration,old=JSON.stringify([state?.version,state?.capabilities]);reading=true;locked();try{const r=await api('/api/bank-reconciliation?'+new URLSearchParams({page,per_page:$('size').value,...($('status').value?{status:$('status').value}:{})}));if(generation!==readGeneration)return;if(r.status!==200)throw Error(r.value.message??'Consulta indisponível.');state=r.value;$('summary').replaceChildren();if(state.blocked)el('p',state.blocked,$('summary'));if(state.statement)el('p',state.statement.account_ref+' · '+state.statement.currency+' · '+state.statement.period+' · abertura '+money(state.statement.opening_minor)+' · fecho '+money(state.statement.closing_minor)+' · '+(state.finalized?'Finalizada':state.statement_reviewed?'Extracto revisto':'Por rever')+' · versão '+state.version,$('summary'));else el('p','Sem extracto importado. A ausência não representa saldo bancário zero.',$('summary'));el('p','Origem: '+(state.source_kind==='native_qa_ledger'?'duas linhas efectivas do diário QA':'origem do exercício indisponível')+'. Sem classificação bancária real ou novos efeitos financeiros.',$('summary'));$('download').replaceChildren();if(state.report_available){const a=el('a','Descarregar relatório interno QA', $('download'));a.href='/api/bank-reconciliation/report';} + $('rows').replaceChildren();for(const row of state.rows){const tr=el('tr',null,$('rows'));el('td',row.reference+' · '+row.date,tr);el('td',money(row.debit_minor),tr);el('td',money(row.credit_minor),tr);el('td',({unmatched:'Por propor',pending:'Por rever',matched:'Conferida'})[row.status]+(row.match?' → diário '+row.match.data.journal_id:''),tr);}$('events').replaceChildren();for(const row of state.history?.data??[]){const tr=el('tr',null,$('events'));el('td','v'+row.version+' · autor '+row.actor_id,tr);el('td',states[row.state]+' · '+row.created_at,tr);el('td',row.reason,tr);const td=el('td',null,tr);el('button','Consultar versão',td).onclick=()=>{if(preview||op.pending)return;$('detail').textContent=JSON.stringify({versao:row.version,anterior:row.previous_id,preparador:row.prepared_by,autor:row.actor_id,fundamento:row.reason,estado:states[row.state],dados:row.data},null,2);};}const p=state.history;$('page').textContent=p?'Página '+p.current_page+' de '+p.last_page+' · '+p.total+' eventos':'Sem histórico';$('previous').disabled=!p||page<=1;$('next').disabled=!p||page>=p.last_page;if(old!==JSON.stringify([state.version,state.capabilities]))actions();}catch(e){if(generation===readGeneration){state=null;$('summary').textContent=e.message;$('rows').replaceChildren();$('events').replaceChildren();$('download').replaceChildren();}throw e;}finally{if(generation===readGeneration){reading=false;locked();}}} +$('operation').onsubmit=async e=>{e.preventDefault();if(preparing)return;preparing=true;locked();try{const action=$('action').value;if(!action)throw Error('Escolha a acção.');let data={},proposal=null;if(action==='import')data={account_ref:$('account').value,currency:$('currency').value,period:$('period').value,opening:$('opening').value,closing:$('closing').value,csv:$('csv').value};if(action==='review_statement'||action==='review_match')data={decision:$('decision').value};if(action==='prepare_match')data={row_reference:$('rowReference').value,ledger_line_id:Number($('ledgerLine').value)};if(action==='review_match')proposal=Number($('proposal').value);operationKey=crypto.randomUUID();const body={action,version:state.version,data,reason:$('reason').value,...(proposal?{proposal_id:proposal}:{})};const r=await api('/api/bank-reconciliation/preview',body,operationKey);if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;$('referenceKey').value=operationKey;$('impact').replaceChildren();el('h3','Impacto antes da confirmação',$('impact'));const i=preview.impact;el('p','Versão anterior '+i.before_version+' → '+i.after_version+' · '+states[i.state],$('impact'));if(i.data?.row_reference)el('p',i.data.row_reference+' → diário '+i.data.journal_id+' / linha '+i.data.ledger_line_id,$('impact'));if(i.existing_statement)el('p','Mesmo extracto já importado; sem novo evento ou linhas.',$('impact'));el('p','Pagamentos/lançamentos novos: nenhum. Operação bancária real: não.',$('impact'));}catch(e){error(e);}finally{preparing=false;locked();}}; +async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;dirty=false;$('impact').replaceChildren();$('receipt').textContent='Versão '+r.version+' · '+states[r.state]+(r.replayed?' · recuperada sem duplicação':' · registada');await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}} +$('recovery').onsubmit=async e=>{e.preventDefault();try{const key=$('referenceKey').value.trim().toLowerCase(),r=await api('/api/bank-reconciliation/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message??'Sem recibo confirmado desta função para a referência.');$('receipt').textContent='Recibo histórico: versão '+r.value.result.version+' · '+states[r.value.result.state]+'. Nenhuma operação repetida.';if(op.pending&&key===operationKey)await finish(true);}catch(e){error(e);}}; +$('action').onchange=fields;$('operation').oninput=()=>{dirty=true;};$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('refresh').onclick=()=>load().catch(error);$('apply').onclick=()=>{page=1;load().catch(error);};$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(dirty||preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error); diff --git a/public/sgf-cash.mjs b/public/sgf-cash.mjs new file mode 100644 index 0000000..a832617 --- /dev/null +++ b/public/sgf-cash.mjs @@ -0,0 +1,15 @@ +import {confirmation} from './sgf-stock-final.mjs'; +const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content; +const kindNames={open:'Abertura',receipt:'Classificação de recebimento',expense:'Despesa',count:'Contagem'},actionNames={prepare:'Preparar',review:'Conferir proposta',close:'Fechar Caixa'},states={proposed:'Proposto',approved:'Aprovado',refused:'Recusado',closed:'Fechado'};let state,selected=null,page=1,preview=null,preparing=false,dirty=false,operationKey=null;const money=n=>(n/100).toFixed(2)+' MZN'; +function el(tag,text,parent){const n=document.createElement(tag);if(text!==null)n.textContent=text;parent.append(n);return n;}function error(e){$('message').textContent=e.message;} +async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return{status:r.status,value:await r.json()};} +const op=confirmation(async body=>{if(operationKey){const old=await api('/api/cash/receipts/'+operationKey);if(old.status===200)return {status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/cash/confirm',body);});function locked(){const b=Boolean(preparing||preview||op.pending);$('fields').disabled=b||Boolean(state?.blocked)||Boolean(state?.basis?.closed);$('filters').disabled=b;$('refresh').disabled=b;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;} +function input(name,label,type='text',value=''){const l=el('label',label,$('inputs')),i=el('input',null,l);i.id='field-'+name;i.type=type;i.required=true;i.value=value;return i;} +function form(){const action=$('action').value,kind=$('kind').value;$('inputs').replaceChildren();$('selection').textContent=selected?`Versão ${selected.version}, autor ${selected.actor_id}: ${selected.reason}`:'';if(action==='prepare'&&kind){input('date','Data do facto','date');input('evidence','Comprovativo fictício da declaração');if(kind==='open'){input('label','Identificação da Caixa QA');input('opening','Saldo inicial explícito (MZN)','text','0.00');}if(kind==='receipt'){const l=el('label','Recebimento efectivo de origem', $('inputs')),s=el('select',null,l);s.id='field-receipt_id';s.required=true;el('option','Escolher…',s).value='';el('option',`Recebimento ${state.binding.receipt_id} · factura ${state.binding.invoice_id} · ${money(state.binding.amount_minor)}`,s).value=String(state.binding.receipt_id);const l2=el('label','Meio de custódia explicitamente conferido', $('inputs')),m=el('select',null,l2);m.id='field-medium';m.required=true;el('option','Escolher…',m).value='';el('option','Numerário de teste',m).value='cash';}if(kind==='expense'){input('amount','Despesa QA (MZN)','text','50.00');input('beneficiary','Beneficiário fictício');input('description','Descrição da despesa');}if(kind==='count')input('counted','Numerário contado (MZN)');}else if(action==='review'){const l=el('label','Decisão explícita', $('inputs')),s=el('select',null,l);s.id='field-decision';s.required=true;for(const [v,t]of [['','Escolher…'],['approved','Aprovar'],['refused','Recusar']])el('option',t,s).value=v;}if(selected&&selected.dependencies?.balance_minor!==undefined){const before=selected.dependencies.balance_minor,delta=selected.action==='review'&&selected.state==='approved'?(selected.kind==='receipt'?selected.data.amount_minor:selected.kind==='expense'?-selected.data.amount_minor:0):0;el('p','Saldo antes desta decisão: '+money(before),$('selection'));el('p','Saldo após esta decisão: '+money(before+delta),$('selection'));}if(selected){for(const [k,v]of Object.entries(selected.data)){const titles={date:'Data',evidence:'Comprovativo',label:'Caixa',opening:'Saldo inicial',receipt_id:'Recebimento',medium:'Meio de custódia',amount:'Despesa',amount_minor:'Montante',beneficiary:'Beneficiário',description:'Descrição',counted:'Contagem declarada',counted_minor:'Contagem'};el('p',(titles[k]??k)+': '+(k.endsWith('_minor')?money(v):v==='cash'?'Numerário de teste':String(v)),$('selection'));}}} +function actions(row=null){selected=row;$('action').replaceChildren();$('kind').replaceChildren();el('option','Escolher…',$('action')).value='';el('option','Escolher…',$('kind')).value='';const b=state.basis,c=state.capabilities;if(!b||b.closed){form();locked();return;}if(!row){if(c.prepare){el('option','Preparar', $('action')).value='prepare';for(const k of b.open?['receipt','expense','count']:['open']){if(k==='receipt'&&b.received||k==='expense'&&b.expense_recorded)continue;el('option',kindNames[k],$('kind')).value=k;}}}else{el('option',kindNames[row.kind],$('kind')).value=row.kind;$('kind').value=row.kind;if(c.review&&row.action==='prepare'&&!row.decision&&row.actor_id!==state.actor_id)el('option','Conferir esta proposta', $('action')).value='review';if(c.close&&row.action==='review'&&row.kind==='count'&&row.state==='approved')el('option','Fechar pela contagem revista', $('action')).value='close';}form();locked();} +async function load(){const q=new URLSearchParams({page,per_page:$('size').value});for(const [id,key]of [['filterKind','kind'],['from','from'],['to','to']])if($(id).value)q.set(key,$(id).value);const r=await api('/api/cash?'+q);if(r.status!==200)throw Error(r.value.message??'Consulta recusada.');state=r.value;$('summary').textContent=state.blocked??`${state.basis.closed?'Fechada':state.basis.open?'Aberta':'Abertura por confirmar'} · saldo registado ${money(state.basis.balance_minor)} · versão ${state.basis.revision}. Origem permitida: recebimento ${state.binding.receipt_id}, ${money(state.binding.amount_minor)}. Sem saldo bancário ou dinheiro real.`;const p=state.period;$('period').textContent=p?`Intervalo: abertura ${money(p.opening_minor)}; entradas ${money(p.receipts_minor)}; despesas ${money(p.expenses_minor)}; fecho ${money(p.closing_minor)}.`:'Resumo por conferir: origem bloqueada.';$('events').replaceChildren();for(const row of state.events.data){const tr=el('tr',null,$('events'));for(const value of [row.version+' · '+(row.data.date??row.created_at),kindNames[row.kind]+' / '+actionNames[row.action],states[row.state],money(row.data.counted_minor??row.data.amount_minor??0),row.actor_id+' · '+row.reason])el('td',value,tr);const td=el('td',null,tr);el('button','Escolher esta versão',td).onclick=()=>actions(row);if(row.decision)el('p','Decisão: '+states[row.decision.state],td);if(row.command_reference)el('small','Referência: '+row.command_reference,td);}const pg=state.events;$('page').textContent=`Página ${pg.current_page} de ${pg.last_page} · ${pg.total} eventos`;$('previous').disabled=page<=1;$('next').disabled=page>=pg.last_page;actions();} +function data(){const read=k=>$('field-'+k)?.value??'';if($('action').value==='close')return{};if($('action').value==='review')return{decision:read('decision')};const d={date:read('date'),evidence:read('evidence')};for(const k of {open:['label','opening'],receipt:['receipt_id','medium'],expense:['amount','beneficiary','description'],count:['counted']}[$('kind').value]??[])d[k]=k==='receipt_id'?Number(read(k)):read(k);return d;} +$('operation').onsubmit=async e=>{e.preventDefault();if(preparing)return;preparing=true;locked();try{$('message').textContent='';const key=crypto.randomUUID();const r=await api('/api/cash/preview',{action:$('action').value,kind:$('kind').value,proposal_id:selected?.id??null,version:state.basis.revision,data:data(),reason:$('reason').value},key);if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;operationKey=key;$('referenceKey').value=key;$('impact').replaceChildren();for(const [t,v]of [['Acção',actionNames[preview.impact.action]],['Tipo',kindNames[preview.impact.kind]],['Estado posterior',states[preview.impact.state]],['Saldo anterior',money(preview.impact.before_minor)],['Saldo posterior',money(preview.impact.after_minor)],['Variação',money(preview.impact.change_minor)],['Dinheiro real','Não'],['Nova factura ou recibo','Não'],['Lançamento automático','Não']])el('p',t+': '+v,$('impact'));}catch(e){error(e);}finally{preparing=false;locked();}}; +async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;dirty=false;$('impact').replaceChildren();$('receipt').textContent=`Versão ${r.version} confirmada: ${states[r.state]}. ${r.replayed?'Recuperada sem duplicação.':''}`;await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}} +$('recovery').onsubmit=async e=>{e.preventDefault();try{const key=$('referenceKey').value.trim().toLowerCase(),r=await api('/api/cash/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message||'Ainda não há recibo confirmado para esta referência e função.');$('receipt').textContent='Recibo histórico recuperado: versão '+r.value.result.version+', '+states[r.value.result.state]+'. Nenhuma operação repetida.';if(op.pending&&key===operationKey)await finish(true);}catch(e){error(e);}}; +$('operation').oninput=()=>{dirty=true;};$('action').onchange=$('kind').onchange=form;$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('refresh').onclick=()=>load().catch(error);$('apply').onclick=()=>{page=1;load().catch(error);};$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(dirty||preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error); diff --git a/public/sgf-composed-ledger.mjs b/public/sgf-composed-ledger.mjs new file mode 100644 index 0000000..7e5f2a0 --- /dev/null +++ b/public/sgf-composed-ledger.mjs @@ -0,0 +1,15 @@ +const root=document.querySelector('#composed-ledger'),message=document.querySelector('#ledger-result');let pending=false,uncertain=false,current=null,dirty=false; +for(const form of document.querySelectorAll('[data-ledger-action]')){ + form.addEventListener('input',()=>{dirty=true;}); + form.onsubmit=async event=>{event.preventDefault();if(pending)return;if(uncertain&¤t!==form){message.textContent='Recupere primeiro a operação pendente.';return;}let action=form.dataset.ledgerAction,body=uncertain?JSON.parse(form.dataset.signature):Object.fromEntries(new FormData(form)); + if(action==='capture'){if(uncertain)action=form.dataset.sentAction;else{const [kind,id]=body.source.split('|');delete body.source;body.rule_key=kind;if(kind==='supplier_payable.recognised'){action='prepare';body.obligation_id=Number(id);}else{action='effect-prepare';body.effect_type=kind;body.source_id=Number(id);}}} + for(const key of ['obligation_id','source_id','proposal_id','journal_id','correction_id','check_id','book_id','rate_id','version'])if(key in body)body[key]=Number(body[key]); + if(body.unit_amount!==undefined){if(!/^\d{1,6}(\.\d{1,2})?$/.test(body.unit_amount)){message.textContent='Indique um custo unitário positivo com até duas casas decimais.';return;}const [whole,fraction='']=body.unit_amount.split('.');body.unit_minor=Number(whole)*100+Number(fraction.padEnd(2,'0'));delete body.unit_amount;} + const signature=JSON.stringify(body);if(!uncertain&&form.dataset.signature!==signature){form.dataset.key=crypto.randomUUID();form.dataset.signature=signature;form.dataset.sentAction=action;} + current=form;pending=true;for(const field of document.querySelectorAll('input,textarea,select,button'))field.disabled=true;message.textContent='A revalidar a fotografia, as permissões e as dependências…'; + try{const response=await fetch('/api/general-ledger/'+(form.hasAttribute('data-stock-rate')?'stock/':'')+action,{method:'POST',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':root.dataset.csrf,'X-SGF-Context':root.dataset.context,'Idempotency-Key':form.dataset.key},body:signature});const value=await response.json();if(!response.ok){if(response.status>=500)throw Error('Resposta incerta');uncertain=false;message.textContent=value.message||'Conferência recusada. Actualize o documento.';return;}uncertain=false;dirty=false;location.reload();} + catch{uncertain=true;message.textContent='Resultado ainda incerto. Recupere pelo mesmo botão com a mesma chave; os campos ficam protegidos.';} + finally{pending=false;for(const field of document.querySelectorAll('input,textarea,select,button'))field.disabled=uncertain&&!(field.tagName==='BUTTON'&&form.contains(field));} + }; +} +window.addEventListener('beforeunload',event=>{if(pending||uncertain||dirty){event.preventDefault();event.returnValue='';}}); diff --git a/public/sgf-entry-menu.mjs b/public/sgf-entry-menu.mjs new file mode 100644 index 0000000..cd420ae --- /dev/null +++ b/public/sgf-entry-menu.mjs @@ -0,0 +1,3 @@ +const menus=[...document.querySelectorAll('.module-choice')]; +for(const menu of menus){menu.addEventListener('toggle',()=>{if(menu.open)for(const other of menus)if(other!==menu)other.open=false;});menu.addEventListener('keydown',event=>{if(event.key==='Escape'){menu.open=false;menu.querySelector('summary').focus();}});} +document.addEventListener('pointerdown',event=>{if(!event.target.closest('.module-choices'))for(const menu of menus)menu.open=false;}); diff --git a/public/sgf-finance-payables.mjs b/public/sgf-finance-payables.mjs new file mode 100644 index 0000000..7b50f1e --- /dev/null +++ b/public/sgf-finance-payables.mjs @@ -0,0 +1,13 @@ +const root=document.querySelector('#finance-payables'),message=document.querySelector('#result');let pending=false,uncertain=false,pendingForm=null,dirty=false; +for(const form of document.querySelectorAll('[data-action]')){ + const source=form.querySelector('[data-source]');if(source)source.onchange=()=>{form.elements.source_revision.value=source.selectedOptions[0]?.dataset.revision||'';}; + form.addEventListener('input',()=>{dirty=true;});form.onsubmit=async event=>{event.preventDefault();if(pending)return;if(uncertain&&pendingForm!==form){message.textContent='Recupere primeiro a operação pendente.';return;}let body=uncertain?JSON.parse(form.dataset.signature):Object.fromEntries(new FormData(form));for(const key of ['obligation_id','payment_id','proposal_id','allocation_id'])if(key in body)body[key]=Number(body[key]); + if(form.dataset.action==='allocation-prepare'&&!uncertain){body.allocations=[{obligation_id:body.obligation_id,source_revision:body.source_revision,amount:body.amount}];delete body.obligation_id;delete body.source_revision;delete body.amount;} + const signature=JSON.stringify(body);if(uncertain&&form.dataset.signature!==signature){message.textContent='Recupere primeiro a operação com os mesmos dados.';return;}if(form.dataset.signature!==signature){form.dataset.signature=signature;form.dataset.key=crypto.randomUUID();} + pending=true;pendingForm=form;for(const field of document.querySelectorAll('input,select,textarea,button'))field.disabled=true;message.textContent='A revalidar e registar a decisão…'; + try{const response=await fetch('/api/finance/payables/'+form.dataset.action,{method:'POST',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':root.dataset.csrf,'X-SGF-Context':root.dataset.context,'Idempotency-Key':form.dataset.key},body:signature});const value=await response.json();if(!response.ok){if(response.status>=500)throw Error('Resultado incerto');uncertain=false;message.textContent=value.message||'Operação recusada. Actualize a conferência.';return;}uncertain=false;dirty=false;location.reload();} + catch{uncertain=true;message.textContent='Resposta incerta. Conserve esta página e recupere pelo mesmo botão, sem alterar os dados.';} + finally{pending=false;for(const field of document.querySelectorAll('input,select,textarea,button'))field.disabled=uncertain&&!(field.tagName==='BUTTON'&&form.contains(field));} + }; +} +window.addEventListener('beforeunload',event=>{if(pending||uncertain||dirty){event.preventDefault();event.returnValue='';}}); diff --git a/public/sgf-fiscal-rules.mjs b/public/sgf-fiscal-rules.mjs new file mode 100644 index 0000000..a10a6af --- /dev/null +++ b/public/sgf-fiscal-rules.mjs @@ -0,0 +1,25 @@ +import {confirmation} from './sgf-stock-final.mjs'; +const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content; +const states={prepared:'Preparado',reviewed:'Revisto',refused:'Recusado',withdrawn:'Retirado',determined_qa:'Calculado QA',pending:'Pendente',pending_representation:'Representação pendente'};let state,page=1,selected=null,preview=null,operationKey=null,preparing=false,reading=false,readGeneration=0,dirty=false,documentRowCounter=0; +const el=(tag,text,parent)=>{const n=document.createElement(tag);if(text!==null)n.textContent=text;parent.append(n);return n;};const error=e=>$('message').textContent=e.message; +async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return{status:r.status,value:await r.json()};} +const op=confirmation(async body=>{if(operationKey){const old=await api('/api/fiscal-rules/receipts/'+operationKey);if(old.status===200)return{status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/fiscal-rules/confirm',body);}); +function locked(){const busy=Boolean(preparing||reading||preview||op.pending);$('fields').disabled=busy||!state||(!state.capabilities.prepare&&!state.capabilities.review);$('filters').disabled=busy;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;} +function input(id,label,type='text',value='',required=true){const l=el('label',label,$('inputs')),i=el(type==='textarea'?'textarea':'input',null,l);i.id=id;if(type!=='textarea')i.type=type;i.value=value;i.required=required;return i;} +function select(id,label,choices){const l=el('label',label,$('inputs')),s=el('select',null,l);s.id=id;s.required=true;for(const [v,t]of choices)s.append(new Option(t,v));return s;} +function documentLine(){if(preview||op.pending||preparing)return;const table=$('documentLines');if(table.children.length>=25){error(Error('Máximo25 linhas.'));return;}const id=++documentRowCounter,tr=el('tr',null,table);tr.dataset.rowId=String(id);for(const [name,label]of [['reference','Referência'],['quantity','Quantidade'],['price','Preço unitário']]){const td=el('td',null,tr),i=el('input',null,td);i.id='doc-'+name+'-'+id;i.required=true;i.type='text';i.maxLength=name==='reference'?80:25;i.setAttribute('aria-label',label+' linha '+id);}const td=el('td',null,tr),b=el('button','Retirar linha',td);b.type='button';b.onclick=()=>{if(preview||op.pending||preparing)return;if(table.children.length<=1){error(Error('Mantenha pelo menos uma linha.'));return;}tr.remove();dirty=true;};} +function documentFields(){el('p',selected?'Regra escolhida: '+selected.rule_key+' · versão '+selected.rule_version:'Sem regra escolhida: resultado pendente, sem taxa zero por omissão.',$('inputs'));select('documentMode','Modalidade explícita',[['','Escolher…'],['net_added','Preço sem imposto'],['gross_included','Preço com imposto incluído']]);input('documentOn','Data para conferir a vigência','date','');select('documentCurrency','Moeda',[['','Escolher…'],['MZN','MZN']]);el('p','Política: imposto truncado por linha; no incluído, base documental = bruto − imposto documental. Exactos e ambas as diferenças preservados. Totais somam as linhas.',$('inputs'));const wrap=el('div',null,$('inputs')),table=el('table',null,wrap),head=el('thead',null,table),tr=el('tr',null,head);for(const title of ['Referência','Quantidade','Preço unitário','Linha'])el('th',title,tr);const body=el('tbody',null,table);body.id='documentLines';documentRowCounter=0;documentLine();const add=el('button','Adicionar linha', $('inputs'));add.id='addDocumentLine';add.type='button';add.onclick=()=>{documentLine();dirty=true;};} +function fields(){if(preview||op.pending)return;$('inputs').replaceChildren();const action=$('action').value;if(!action)return; + if(action==='prepare'){input('ruleKey','Chave da regra (nova chave ou sucessora)','text',selected?.rule_key??'').maxLength=80;const template=select('template','Definição a preparar',[['','Escolher…'],...state.fixtures.cases.map(c=>[c.fixture_id,c.definition.label]),['pending','Referência ainda não conferida']]);const container=el('div',null,$('inputs'));container.id='templateFields';template.onchange=()=>{container.replaceChildren();if(template.value==='pending'){const l=el('label','Nome da referência',container),name=el('input',null,l);name.id='metadataLabel';name.required=true;const lf=el('label','Família',container),family=el('select',null,lf);family.id='metadataFamily';family.required=true;for(const [v,t]of [['','Escolher…'],['iva','IVA'],['irps','IRPS'],['irpc','IRPC'],['ispc','ISPC'],['ice','ICE'],['inss','INSS'],['other','Outros']])family.append(new Option(t,v));for(const [id,title]of [['sourceReference','Referência da fonte'],['conditions','Condições conhecidas ou lacunas']]){const label=el('label',title,container),i=el('textarea',null,label);i.id=id;i.required=true;}}else{const d=state.fixtures.cases.find(c=>c.fixture_id===template.value)?.definition;if(d)el('p','Família QA artificial · MZN · taxa de ensaio '+d.parameters.rate+' × base · vigência '+d.valid_from+' a '+(d.valid_until??'sem termo neste caso')+' · representação em duas casas, truncagem. Fonte '+d.source_reference+'.',container);}};} + if(action==='calculate_document')documentFields(); + if(action==='review'||action==='withdraw'){el('p',selected?'Versão escolhida: '+selected.rule_key+' · v'+selected.rule_version+' · revisão actual '+selected.current_revision:'Escolha explicitamente uma versão no catálogo antes da decisão.',$('inputs'));if(action==='review')select('decision','Decisão',[['','Escolher…'],['reviewed','Rever favoravelmente'],['refused','Recusar']]);} + if(action==='calculate'){el('p',selected?'Regra escolhida: '+selected.rule_key+' · versão '+selected.rule_version:'Sem regra escolhida: o resultado será uma pendência, nunca taxa zero.',$('inputs'));input('base','Base decimal explícita','text','');input('on','Data para verificar vigência','date','');select('currency','Moeda',[['','Escolher…'],['MZN','MZN'],['USD','USD (incompatível neste ensaio)']]);}} +function actions(){const before=$('action').value;$('action').replaceChildren(new Option('Escolher…',''));for(const [a,label,right]of [['prepare','Preparar regra/fonte','prepare'],['review','Rever versão escolhida','review'],['withdraw','Retirar versão revista','review'],['calculate','Calcular com versão escolhida','prepare'],...(state.document_enabled?[['calculate_document','Calcular por linhas / imposto incluído','prepare']]:[])])if(state.capabilities[right])$('action').append(new Option(label,a));if([...$('action').children].some(o=>o.value===before))$('action').value=before;fields();} +async function choose(id,type){if(preview||op.pending||preparing||reading)return;try{const r=await api('/api/fiscal-rules/records/'+id+'?type='+type);if(r.status!==200)throw Error(r.value.message??'Versão indisponível.');$('detail').textContent=JSON.stringify(r.value,null,2);$('documentDownload').replaceChildren();if(type==='calculation'&&r.value.result.price_mode){const a=el('a','Descarregar mapa interno por linhas',$('documentDownload'));a.href='/api/fiscal-rules/documents/'+id+'/report';}if(type==='rule'){selected=r.value;$('selection').textContent='Escolhida '+selected.rule_key+' · v'+selected.rule_version+' · '+states[selected.state]+(selected.id!==selected.current_version_event_id?' · versão já alterada':'');if(['review','withdraw','calculate','calculate_document'].includes($('action').value))fields();}else{selected=null;if(['calculate','calculate_document'].includes($('action').value))fields();$('selection').textContent='Cálculo histórico escolhido; a regra de outro registo não é seleccionada automaticamente.';}}catch(e){error(e);}} +async function load(){if(op.pending||preview)throw Error('Conclua ou cancele a conferência em curso.');const generation=++readGeneration,oldCapabilities=JSON.stringify(state?.capabilities);reading=true;locked();try{const view=$('view').value,query={view,page,per_page:$('size').value};if(view!=='calculations'){if($('filterKey').value)query.rule_key=$('filterKey').value;if($('family').value)query.family=$('family').value;if($('filterState').value)query.state=$('filterState').value;if($('search').value)query.search=$('search').value;}if(view==='history'&&!query.rule_key)throw Error('Indique a chave para consultar o seu histórico.');const r=await api('/api/fiscal-rules?'+new URLSearchParams(query));if(generation!==readGeneration)return;if(r.status!==200)throw Error(r.value.message??'Catálogo indisponível.');state=r.value;$('events').replaceChildren();for(const row of state.records.data){const tr=el('tr',null,$('events')),isCalc=view==='calculations';el('td',isCalc?'Cálculo #'+row.id:row.rule_key+' · '+row.definition.label,tr);el('td',isCalc?states[row.result.state]:'v'+row.rule_version+' / r'+row.revision+' · '+states[row.state],tr);el('td',isCalc?'Imposto: '+(row.result.tax_document??'indeterminado')+' · total: '+(row.result.total??'indeterminado'):row.definition.source_reference+' · '+(row.definition.valid_from??'vigência desconhecida')+' a '+(row.definition.valid_until??'sem termo')+' · '+(row.definition.source_status==='qa_checked'?'fonte artificial conferida':'fonte pendente'),tr);const td=el('td',null,tr);el('button',isCalc?'Consultar cálculo':'Escolher esta versão',td).onclick=()=>choose(row.id,isCalc?'calculation':'rule');if(row.command_reference)el('small','Referência: '+row.command_reference,td);}const p=state.records;$('page').textContent='Página '+p.current_page+' de '+p.last_page+' · '+p.total+' registos';$('previous').disabled=page<=1;$('next').disabled=page>=p.last_page;if(oldCapabilities!==JSON.stringify(state.capabilities))actions();}catch(e){if(generation===readGeneration){state=null;selected=null;$('events').replaceChildren();$('detail').textContent='';$('selection').textContent='Consulta não conferida; reabra o contexto autorizado.';}throw e;}finally{if(generation===readGeneration){reading=false;locked();}}} +$('operation').onsubmit=async e=>{e.preventDefault();if(preparing)return;preparing=true;locked();try{const action=$('action').value;if(!action)throw Error('Escolha a acção.');const data={};let body={action,revision:0,data,reason:$('reason').value};if(action==='prepare'){const key=$('ruleKey').value;if(!/^[A-Z0-9_-]{3,80}$/.test(key))throw Error('Indique uma chave válida.');const stream=await api('/api/fiscal-rules?'+new URLSearchParams({view:'history',rule_key:key,per_page:25}));if(stream.status!==200)throw Error(stream.value.message??'Versão da regra não conferida.');body.rule_key=key;body.revision=stream.value.stream_revision;const template=$('template').value,c=state.fixtures.cases.find(c=>c.fixture_id===template);if(c)body.data={...c.definition,source_sha256:state.fixtures.sha256};else if(template==='pending')body.data={label:$('metadataLabel').value,family:$('metadataFamily').value,operation:'metadata_only',conditions:$('conditions').value,currency:null,unit:'none',precision:2,rounding:'DOWN',priority:0,source_reference:$('sourceReference').value,source_sha256:null,source_status:'pending',valid_from:null,valid_until:null,parameters:{},expression:{}};else throw Error('Escolha a definição explicitamente.');} + else if(action==='calculate_document')body.data={rule_event_id:selected?.id??null,currency:$('documentCurrency').value,date:$('documentOn').value,price_mode:$('documentMode').value,lines:[...$('documentLines').children].map(row=>({reference:$('doc-reference-'+row.dataset.rowId).value,quantity:$('doc-quantity-'+row.dataset.rowId).value,unit_price:$('doc-price-'+row.dataset.rowId).value}))};else if(action==='calculate')body.data={rule_event_id:selected?.id??null,base:$('base').value,date:$('on').value,currency:$('currency').value};else{if(!selected)throw Error('Escolha uma versão no catálogo.');body.rule_key=selected.rule_key;body.revision=selected.current_revision;body.target_id=selected.id;body.data=action==='review'?{decision:$('decision').value}:{};} + operationKey=crypto.randomUUID();const r=await api('/api/fiscal-rules/preview',body,operationKey);if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;$('referenceKey').value=operationKey;$('impact').replaceChildren();el('h3','Impacto antes da confirmação',$('impact'));const i=preview.impact;if(i.result){if(i.result.price_mode){for(const [k,v]of [['Estado',states[i.result.state]],['Base documental',i.result.base_document??'indeterminada'],['Imposto documental',i.result.tax_document??'indeterminado'],['Total',i.result.total??'indeterminado'],['Diferença da base (fracção)',i.result.base_difference_fraction??'indeterminada'],['Diferença do imposto (fracção)',i.result.tax_difference_fraction??'indeterminada']])el('p',k+': '+v,$('impact'));const table=el('table',null,$('impact')),head=el('thead',null,table),h=el('tr',null,head);for(const title of ['Linha','Base exacta','Imposto exacto','Base documental','Imposto documental','Total','Diferenças base / imposto'])el('th',title,h);const body=el('tbody',null,table);for(const line of i.result.lines){const row=el('tr',null,body);for(const v of [line.reference,line.base_exact_fraction,line.tax_exact_fraction,line.base_document??'indeterminada',line.tax_document,line.total_document??'indeterminado',(line.base_difference_fraction??'indeterminada')+' / '+line.tax_difference_fraction])el('td',v,row);}}else for(const [k,v]of [['Resultado',states[i.result.state]],['Base',i.result.base??body.data.base],['Imposto exacto',i.result.tax_exact??'indeterminado'],['Imposto documental',i.result.tax_document??'indeterminado'],['Diferença preservada',i.result.difference??'indeterminada'],['Total',i.result.total??'indeterminado']])el('p',k+': '+v,$('impact'));for(const p of i.result.pending)el('p',p,$('impact'));if(i.result.steps)el('pre',JSON.stringify(i.result.steps,null,2),$('impact'));}else{el('p','Estado anterior: '+(states[i.before.state]??'sem preparação')+'; posterior: '+states[i.after.state]+' · versão '+i.after.rule_version+' · revisão '+i.after.revision,$('impact'));el('p','Fonte: '+i.definition.source_reference+'; conferida apenas para ensaio artificial: '+(i.source_checked?'sim':'não'),$('impact'));el('p','Vigência: '+(i.definition.valid_from??'desconhecida')+' a '+(i.definition.valid_until??'sem termo'),$('impact'));}el('p','Novos lançamentos: nenhum. Activação legal: não. Documentos existentes: inalterados.',$('impact'));}catch(e){error(e);}finally{preparing=false;locked();}}; +async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;dirty=false;selected=null;$('selection').textContent='Escolha uma versão para a próxima operação.';$('impact').replaceChildren();$('receipt').textContent=(r.type==='rule'?'Regra revisão '+r.revision:'Cálculo #'+r.id)+' · '+states[r.state]+(r.replayed?' · recuperado sem duplicação':' · registado');fields();await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}} +$('recovery').onsubmit=async e=>{e.preventDefault();try{const key=$('referenceKey').value.trim().toLowerCase(),r=await api('/api/fiscal-rules/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message??'Sem recibo desta função para a referência.');$('receipt').textContent='Recibo histórico '+r.value.result.type+' #'+r.value.result.id+'. Nenhuma operação repetida.';if(op.pending&&key===operationKey)await finish(true);}catch(e){error(e);}}; +$('action').onchange=fields;$('operation').oninput=()=>{dirty=true;};$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('apply').onclick=()=>{page=1;load().catch(error);};$('refresh').onclick=()=>load().catch(error);$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(dirty||preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error); diff --git a/public/sgf-indicators.mjs b/public/sgf-indicators.mjs new file mode 100644 index 0000000..4d3b978 --- /dev/null +++ b/public/sgf-indicators.mjs @@ -0,0 +1,20 @@ +import {confirmation} from './sgf-stock-final.mjs'; +const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content; +const states={prepared:'Preparado',reviewed:'Revisto',refused:'Recusado',pending:'Pendente',determined_qa:'Determinado QA'};let state,page=1,selected=null,preview=null,operationKey=null,preparing=false,reading=false,readGeneration=0,dirty=false; +const el=(tag,text,parent)=>{const e=document.createElement(tag);if(text!==null)e.textContent=text;parent.append(e);return e;},error=e=>$('message').textContent=e.message; +const money=v=>{if(v===null||v===undefined)return 'indeterminado';const n=BigInt(v),a=n<0n?-n:n;return(n<0n?'-':'')+a/100n+'.'+String(a%100n).padStart(2,'0');}; +async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return{status:r.status,value:await r.json()};} +const op=confirmation(async body=>{if(operationKey){const old=await api('/api/financial-indicators/receipts/'+operationKey);if(old.status===200)return{status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/financial-indicators/confirm',body);}); +function locked(){const b=Boolean(preparing||reading||preview||op.pending);$('fields').disabled=b||!state||(!state.capabilities.prepare&&!state.capabilities.review);$('filters').disabled=b;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;} +function input(id,label,type='text',value=''){const l=el('label',label,$('inputs')),e=el(type==='textarea'?'textarea':'input',null,l);e.id=id;e.required=true;if(type!=='textarea')e.type=type;e.value=value;return e;} +function fields(){if(preview||op.pending)return;$('inputs').replaceChildren();const action=$('action').value;if(action==='prepare'){const l=el('label','Critério a preparar',$('inputs')),s=el('select',null,l);s.id='metric';s.required=true;s.append(new Option('Escolher…',''));for(const [key,t]of Object.entries(state.templates))s.append(new Option(t.label,key));const detail=el('p','Escolha explicitamente o indicador e as suas fontes.',$('inputs'));s.onchange=()=>{const t=state.templates[s.value];if(t)detail.textContent=t.label+' · retrato '+t.as_of+' · MZN · '+(s.value==='cash_coverage_single_payable'?'Caixa / saldo credor QA-FORNECEDOR; sem valor absoluto.':'Despesa / entradas da mesma Caixa ×100; não margem de resultados.');};input('scopeDescription','Âmbito e limites deste critério','textarea');input('evidence','Evidência da conferência das fontes','textarea');} + if(action==='review'){el('p',selected?'Versão escolhida: '+selected.definition.label+' · v'+selected.version+' (actual '+selected.current_version+')':'Escolha uma versão do critério no catálogo.',$('inputs'));const l=el('label','Decisão explícita',$('inputs')),s=el('select',null,l);s.id='decision';s.required=true;for(const [v,t]of [['','Escolher…'],['reviewed','Rever favoravelmente'],['refused','Recusar']])s.append(new Option(t,v));} + if(action==='calculate'){el('p',selected?'Critério escolhido: '+selected.definition.label+' · versão '+selected.version:'Sem critério escolhido: o resultado será uma pendência, nunca zero.',$('inputs'));input('asOf','Data explícita do retrato','date');}} +function actions(){const prior=$('action').value;$('action').replaceChildren(new Option('Escolher…',''));for(const [value,label,right]of [['prepare','Preparar critério','prepare'],['review','Rever versão escolhida','review'],['calculate','Calcular pelo critério escolhido','prepare']])if(state.capabilities[right])$('action').append(new Option(label,value));if([...$('action').children].some(o=>o.value===prior))$('action').value=prior;fields();} +async function choose(id,type){if(preview||op.pending||preparing||reading)return;try{const r=await api('/api/financial-indicators/records/'+id+'?type='+type);if(r.status!==200)throw Error(r.value.message??'Registo indisponível.');$('detail').textContent=JSON.stringify(r.value,null,2);$('download').replaceChildren();if(type==='criterion'){selected=r.value;$('selection').textContent='Escolhida '+selected.definition.label+' · versão '+selected.version+' · '+states[selected.state];}else{selected=null;$('selection').textContent='Resultado histórico escolhido; critério não adoptado automaticamente.';const a=el('a','Descarregar relatório interno deste retrato',$('download'));a.href='/api/financial-indicators/results/'+id+'/report';}if(['calculate','review'].includes($('action').value))fields();}catch(e){error(e);}} +async function load(){if(preview||op.pending)throw Error('Conclua ou cancele a conferência em curso.');const generation=++readGeneration,oldCaps=JSON.stringify(state?.capabilities);reading=true;locked();try{const view=$('view').value,q={view,page,per_page:$('size').value};if(view!=='results'){if($('filterMetric').value)q.metric=$('filterMetric').value;if($('filterState').value)q.state=$('filterState').value;if($('search').value)q.search=$('search').value;}if(view==='history'&&!q.metric)throw Error('Escolha o indicador para consultar o histórico.');const r=await api('/api/financial-indicators?'+new URLSearchParams(q));if(generation!==readGeneration)return;if(r.status!==200)throw Error(r.value.message??'Consulta indisponível.');state=r.value;$('summary').replaceChildren();if(state.source.available){for(const [label,key]of [['Caixa QA','cash_balance_minor'],['Saldo credor QA-FORNECEDOR','supplier_credit_minor'],['Entradas da Caixa','cash_receipts_minor'],['Despesa da Caixa','cash_expenses_minor']])el('p',label+': '+money(state.source[key])+' MZN',$('summary'));el('p','Retrato '+state.source.as_of+' · '+(state.source.kind==='composed_qa_sources'?'fontes efectivas conciliadas do QA':'origem de ensaio')+'. Sem novos movimentos.',$('summary'));}else el('p','Fontes indisponíveis/não conciliadas. Montantes indeterminados; não são zero.',$('summary')); + if($('filterMetric').children.length===1)for(const [key,t]of Object.entries(state.templates))$('filterMetric').append(new Option(t.label,key));$('events').replaceChildren();for(const row of state.records.data){const isResult=view==='results',tr=el('tr',null,$('events'));el('td',isResult?'Resultado #'+row.id:row.definition.label,tr);el('td',isResult?states[row.result.state]:'v'+row.version+' · '+states[row.state],tr);el('td',isResult?(row.result.value??'indeterminado')+' '+(row.result.unit??'')+' · '+row.input.as_of:row.definition.as_of+' · '+row.definition.currency,tr);const td=el('td',null,tr);el('button',isResult?'Consultar resultado':'Escolher esta versão',td).onclick=()=>choose(row.id,isResult?'result':'criterion');}const p=state.records;$('page').textContent='Página '+p.current_page+' de '+p.last_page+' · '+p.total+' registos';$('previous').disabled=page<=1;$('next').disabled=page>=p.last_page;if(oldCaps!==JSON.stringify(state.capabilities))actions();}catch(e){if(generation===readGeneration){state=null;selected=null;$('summary').textContent=e.message;$('events').replaceChildren();$('download').replaceChildren();$('detail').textContent='';}throw e;}finally{if(generation===readGeneration){reading=false;locked();}}} +$('operation').onsubmit=async e=>{e.preventDefault();if(preparing)return;preparing=true;locked();try{const action=$('action').value;if(!action)throw Error('Escolha a acção.');let body={action,version:0,data:{},reason:$('reason').value};if(action==='prepare'){const metric=$('metric').value;if(!state.templates[metric])throw Error('Escolha o indicador.');const version=await api('/api/financial-indicators?'+new URLSearchParams({view:'history',metric,per_page:25}));if(version.status!==200)throw Error(version.value.message??'Versão não conferida.');const {label,...template}=state.templates[metric];body={...body,metric,version:version.value.metric_version,data:{...template,scope_description:$('scopeDescription').value,evidence:$('evidence').value}};}else if(action==='review'){if(!selected)throw Error('Escolha a versão do critério.');body={...body,metric:selected.metric,version:selected.current_version,data:{decision:$('decision').value}};}else body.data={criterion_id:selected?.id??null,as_of:$('asOf').value};operationKey=crypto.randomUUID();const r=await api('/api/financial-indicators/preview',body,operationKey);if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;$('referenceKey').value=operationKey;$('impact').replaceChildren();el('h3','Impacto antes da confirmação',$('impact'));const i=preview.impact;if(i.result){const v=i.result;for(const [key,value]of [['Estado',states[v.state]],['Numerador MZN',money(v.numerator_minor)],['Denominador MZN',money(v.denominator_minor)],['Fracção exacta',v.exact_fraction??'indeterminada'],['Representação',(v.value??'indeterminada')+' '+(v.unit==='percent'?'%':v.unit==='times'?'vezes':'')],['Diferença preservada',v.representation_difference_fraction??'indeterminada'],['Série anterior/tendência','Indeterminadas']])el('p',key+': '+value,$('impact'));for(const reason of v.pending)el('p',reason,$('impact'));}else{el('p',i.definition.label+' · estado anterior '+(states[i.before_state]??'sem preparação')+' → '+states[i.state]+' · versão '+i.after_version,$('impact'));el('p','Fontes explicitamente escolhidas: '+i.definition.numerator+' / '+i.definition.denominator+' · direcção '+i.definition.denominator_direction,$('impact'));}el('p','Novos efeitos financeiros: nenhum. Critério da empresa real/recomendação de pagamento: não.',$('impact'));}catch(e){error(e);}finally{preparing=false;locked();}}; +async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;dirty=false;selected=null;$('selection').textContent='Escolha outra versão para a próxima operação.';$('impact').replaceChildren();$('receipt').textContent=(r.type==='result'?'Resultado #'+r.id:'Critério versão '+r.version)+' · '+states[r.state]+(r.replayed?' · recuperado sem duplicação':' · registado');fields();await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}} +$('recovery').onsubmit=async e=>{e.preventDefault();try{const key=$('referenceKey').value.trim().toLowerCase(),r=await api('/api/financial-indicators/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message??'Sem recibo desta função para a referência.');$('receipt').textContent='Recibo histórico '+r.value.result.type+' #'+r.value.result.id+'. Nenhuma operação repetida.';if(op.pending&&key===operationKey)await finish(true);}catch(e){error(e);}}; +$('action').onchange=fields;$('operation').oninput=()=>{dirty=true;};$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('apply').onclick=()=>{page=1;load().catch(error);};$('refresh').onclick=()=>load().catch(error);$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(dirty||preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error); diff --git a/public/sgf-iva.mjs b/public/sgf-iva.mjs new file mode 100644 index 0000000..2371d8a --- /dev/null +++ b/public/sgf-iva.mjs @@ -0,0 +1,17 @@ +import {confirmation} from './sgf-stock-final.mjs'; +const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content; +const kinds={assessment:'Livro de saídas QA',form:'Mapa interno QA'},states={prepared:'Preparado',reviewed:'Revisto',refused:'Recusado'};let state,page=1,preview=null,operationKey=null,preparing=false,dirty=false,reading=false,readGeneration=0; +const el=(tag,text,parent)=>{const n=document.createElement(tag);if(text!==null)n.textContent=text;parent.append(n);return n;};const error=e=>$('message').textContent=e.message; +async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return{status:r.status,value:await r.json()};} +const op=confirmation(async body=>{if(operationKey){const old=await api('/api/fiscal-assessments/receipts/'+operationKey);if(old.status===200)return{status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/fiscal-assessments/confirm',body);}); +function locked(){const busy=Boolean(preparing||reading||preview||op.pending);$('fields').disabled=busy||Boolean(state?.blocked)||Boolean(state?.capabilities&&!Object.values(state.capabilities).some(Boolean));$('filters').disabled=busy;$('refresh').disabled=busy;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;} +function form(){if(preview||op.pending)return;const kind=$('kind').value,previous=$('action').value;$('action').replaceChildren(new Option('Escolher…',''));for(const [value,label]of [['prepare','Preparar'],['review','Rever versão actual']])if(state?.capabilities[kind+'_'+value])$('action').append(new Option(label,value));if([...$('action').children].some(o=>o.value===previous))$('action').value=previous;fields();} +function fields(){if(preview||op.pending)return;$('inputs').replaceChildren();const kind=$('kind').value,action=$('action').value;if(!kind||!action)return;const current=state[kind];el('p','Versão actual: '+(current?.version??0)+' · '+(states[current?.state]??'Sem preparação'),$('inputs')); + if(action==='review'){el('p','Reveja a preparação actual e o seu fundamento: '+(current?.reason??'não existe preparação'),$('inputs'));const label=el('label','Decisão explícita',$('inputs')),select=el('select',null,label);select.id='decision';select.required=true;for(const [v,t]of [['','Escolher…'],['reviewed','Rever favoravelmente'],['refused','Recusar']])select.append(new Option(t,v));} + else{const label=el('label','Origem de ensaio', $('inputs')),select=el('select',null,label);select.id='source';select.required=true;select.append(new Option('Escolher documento…',''));if(state.source)select.append(new Option(state.source.document_reference+' · '+state.source.total+' '+state.source.currency,String(state.source.output_id)));const l=el('label','Evidência da classificação',$('inputs')),e=el('textarea',null,l);e.id='evidence';e.minLength=10;e.maxLength=2000;e.required=true;el('p','Período explícito da origem: '+state.period+' · classificação apenas de saída QA, sem dedução ou imposto a pagar presumidos.', $('inputs'));}} +async function load(){if(op.pending||preview)throw Error('Conclua ou cancele a conferência actual antes de actualizar.');const generation=++readGeneration,previousState=state;reading=true;locked();try{const r=await api('/api/fiscal-assessments?'+new URLSearchParams({page,per_page:$('size').value,...($('filterKind').value?{kind:$('filterKind').value}:{})}));if(generation!==readGeneration)return;if(r.status!==200)throw Error(r.value.message??'Consulta indisponível.');state=r.value;$('summary').replaceChildren();if(state.blocked)el('p',state.blocked,$('summary'));else{el('p','Origem '+state.source.document_reference+' · base '+state.source.base+' MZN · IVA documental '+state.source.vat+' MZN · total '+state.source.total+' MZN.', $('summary'));el('p','Livro: '+(states[state.assessment?.state]??'por preparar')+(state.assessment_current_review?' · revisão actual':' · exige conferência')+'; mapa: '+(states[state.form?.state]??'por preparar')+(state.form_current_review?' · revisão actual':' · não exportável'),$('summary'));}el('p','IVA dedutível: indeterminado. Imposto a pagar: indeterminado. Sem novos lançamentos ou transmissão oficial.',$('summary'));$('download').replaceChildren();if(state.form_current_review){const a=el('a','Descarregar mapa interno QA (CSV)', $('download'));a.href='/api/fiscal-assessments/forms/'+state.form.id+'/export';el('p','SHA256: '+state.form.data.artifact_sha256,$('download'));} + $('events').replaceChildren();for(const row of state.events.data){const tr=el('tr',null,$('events'));el('td',kinds[row.kind]+' · v'+row.version+' · autor '+row.actor_id,tr);el('td',states[row.state]+' · '+row.created_at,tr);el('td',row.reason,tr);const td=el('td',null,tr),button=el('button','Ver esta versão',td);button.type='button';button.onclick=()=>{if(preview||op.pending||preparing)return;$('detail').textContent=JSON.stringify({objecto:kinds[row.kind],versao:row.version,anterior:row.previous_id,estado:states[row.state],preparador:row.prepared_by,autor:row.actor_id,fundamento:row.reason,registo:row.data,dependencias:row.dependencies},null,2);};if(row.command_reference)el('small','Referência: '+row.command_reference,td);}const pg=state.events;$('page').textContent='Página '+pg.current_page+' de '+pg.last_page+' · '+pg.total+' versões';$('previous').disabled=page<=1;$('next').disabled=page>=pg.last_page;const signature=s=>JSON.stringify([s?.blocked,s?.capabilities,s?.assessment?.id,s?.form?.id,s?.source?.document_sha256]);if(!previousState||signature(previousState)!==signature(state))form();}catch(e){if(generation===readGeneration){state={blocked:e.message};$('summary').textContent=e.message;$('events').replaceChildren();$('download').replaceChildren();$('inputs').replaceChildren();}throw e;}finally{if(generation===readGeneration){reading=false;locked();}}} +$('operation').onsubmit=async e=>{e.preventDefault();if(preparing)return;preparing=true;locked();try{const kind=$('kind').value,action=$('action').value;if(!kind||!action)throw Error('Escolha objecto e acção.');operationKey=crypto.randomUUID();const data=action==='review'?{decision:$('decision').value}:{source_output_id:Number($('source').value),period:state.period,classification:'qa_output_only',evidence:$('evidence').value};const r=await api('/api/fiscal-assessments/preview',{kind,action,version:state[kind]?.version??0,reason:$('reason').value,data},operationKey);if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;$('referenceKey').value=operationKey;$('impact').replaceChildren();el('h3','Impacto antes da confirmação',$('impact'));const i=preview.impact;for(const [label,value]of [['Estado anterior',states[i.before.state]??'Sem preparação'],['Estado posterior',states[i.after.state]],['Versão posterior',i.after.version],['Documento',i.source.document_reference],['Base MZN',i.source.base],['IVA documental MZN',i.source.vat],['Total MZN',i.source.total],['IVA dedutível','Indeterminado'],['Imposto a pagar','Indeterminado'],['Novos lançamentos','Nenhum'],['Declaração oficial','Não']])el('p',label+': '+value,$('impact'));}catch(e){error(e);}finally{preparing=false;locked();}}; +async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;dirty=false;$('impact').replaceChildren();$('receipt').textContent=kinds[r.kind]+' · versão '+r.version+' · '+states[r.state]+(r.replayed?' · recuperada sem duplicação':' · decisão registada');await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}} +$('recovery').onsubmit=async e=>{e.preventDefault();try{const key=$('referenceKey').value.trim().toLowerCase(),r=await api('/api/fiscal-assessments/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message??'Sem recibo confirmado desta função para a referência.');$('receipt').textContent='Recibo histórico: '+kinds[r.value.result.kind]+' · versão '+r.value.result.version+' · '+states[r.value.result.state]+'. Nenhuma operação repetida.';if(op.pending&&key===operationKey)await finish(true);}catch(e){error(e);}}; +$('kind').onchange=form;$('action').onchange=fields;$('operation').oninput=()=>{dirty=true;};$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('refresh').onclick=()=>load().catch(error);$('apply').onclick=()=>{page=1;load().catch(error);};$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(dirty||preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error); diff --git a/public/sgf-service-documents.mjs b/public/sgf-service-documents.mjs new file mode 100644 index 0000000..867ef3e --- /dev/null +++ b/public/sgf-service-documents.mjs @@ -0,0 +1,18 @@ +import {confirmation} from './sgf-stock-final.mjs'; +const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content;let state,selected=null,page=1,history=null,preview=null,documentKey=null,preparing=false; +const labels={receipt:'Recibo',credit_note:'Nota de crédito',invoice_receipt:'Factura-recibo'}; +async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return {status:r.status,value:await r.json()};} +const op=confirmation(body=>api('/api/service-documents/confirm',body));function error(e){$('message').textContent=e.message;}function el(tag,text,parent){const n=document.createElement(tag);if(text!==null)n.textContent=text;parent.append(n);return n;} +function locked(){const b=Boolean(preparing||preview||op.pending);$('fields').disabled=b;$('filters').disabled=b;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;} +function field(id,label,type='text',value=''){const l=el('label',label,$('inputs')),i=el('input',null,l);i.id=id;i.type=type;i.required=true;i.value=value;return i;} +async function options(select,view,invoice=null){const q=new URLSearchParams({view,per_page:100});if(invoice)q.set('invoice_id',invoice);const r=await api('/api/service-documents/options?'+q);if(r.status!==200)throw Error(r.value.message??'Consulta recusada.');select.replaceChildren();el('option','Escolher…',select).value='';for(const row of r.value.records.data)el('option',view==='invoices'?`${row.series_code}/${row.number}`:`Afectação ${row.id} · ${row.amount} MZN`,select).value=String(row.id);} +async function form(){if(!state)return;const action=$('action').value,kind=$('kind').value;$('inputs').replaceChildren();if(action==='prepare'){ + if(kind==='invoice_receipt'){el('p','Nova origem QA: base862,07 + imposto documental137,93 =1000,00 MZN. A emissão e a afectação só ocorrem na confirmação final.', $('inputs'));field('payment_date','Data do recebimento fictício','date');field('evidence','Comprovativo da intenção financeira');} + else{const l=el('label','Factura de origem nova', $('inputs')),i=el('select',null,l);i.id='invoice';i.required=true;await options(i,'invoices');if(kind==='receipt'){const l=el('label','Afectação efectiva', $('inputs')),a=el('select',null,l);a.id='allocation';a.required=true;el('option','Escolha primeiro a factura',a).value='';i.onchange=()=>{if(i.value)options(a,'allocations',i.value).catch(error);else{a.replaceChildren();el('option','Escolha primeiro a factura',a).value='';}};}else{field('amount','Crédito nominal QA (MZN)','text','100.00');field('explanation','Motivo da redução; sem devolução de dinheiro');}} + }else if(action==='review'){const l=el('label','Decisão de revisão', $('inputs')),s=el('select',null,l);s.id='decision';s.required=true;for(const [v,t]of [['','Escolher…'],['approved','Aprovar'],['refused','Recusar']])el('option',t,s).value=v;}} +function choose(row){selected=row;documentKey=row?.document_key??crypto.randomUUID();$('selected').textContent=row?`${labels[row.kind]} · ${row.document_key}, versão ${row.version}, ${row.state}.`:'Novo processo: escolha explicitamente o documento e a origem.';$('action').replaceChildren();el('option','Escolher…',$('action')).value='';const c=state.capabilities??{};if(c.prepare&&!row)el('option','Preparar', $('action')).value='prepare';if(c.review&&row?.state==='draft'&&row.prepared_by!==state.actor_id)el('option','Conferir versão', $('action')).value='review';if(c.issue&&row?.state==='approved'&&($('kind').value!=='credit_note'||c.credit_apply))el('option','Emitir esta versão', $('action')).value='issue';const events=row?.output?.cancellations??[],last=events.at(-1);if(row?.state==='issued'&&last?.state!=='cancelled'){if(c.cancel_prepare)el('option','Preparar anulação técnica',$('action')).value='cancel_prepare';if(c.cancel_review&&last?.state==='prepared'&&last.actor_id!==state.actor_id)el('option','Conferir anulação técnica',$('action')).value='cancel_review';}form().catch(error);} +async function load(){const q=new URLSearchParams({kind:$('kind').value,page,per_page:$('size').value});if(history)q.set('document_key',history);const r=await api('/api/service-documents?'+q);if(r.status!==200)throw Error(r.value.message??'Consulta recusada.');state=r.value;$('records').replaceChildren();for(const row of state.documents.data){const tr=el('tr',null,$('records'));el('td',row.document_key,tr);el('td',`${row.version} / ${row.state}`,tr);el('td',`${row.actor_id} · ${row.created_at}`,tr);const td=el('td',null,tr);el('button','Escolher versão',td).onclick=()=>choose(row);el('button','Histórico',td).onclick=()=>{history=row.document_key;page=1;load().catch(error);};if(row.output){for(const event of row.output.cancellations??[])el('p','Anulação '+event.version+': '+event.state+' · '+event.actor_id+' · '+event.reason,td);const a=el('a','Abrir PDF de teste',td);a.href='/api/service-documents/'+row.output.id+'/pdf';a.target='_blank';a.rel='noopener';}}const p=state.documents;$('page').textContent=`Página ${p.current_page} de ${p.last_page}`;$('previous').disabled=page<=1;$('next').disabled=page>=p.last_page;$('new').hidden=!state.capabilities?.prepare;choose(null);} +function data(){const v=id=>$(id)?.value??'';if($('action').value==='review')return {decision:v('decision')};if(['issue','cancel_prepare','cancel_review'].includes($('action').value))return {};if($('kind').value==='invoice_receipt'){const src=state.inputs.invoice_receipt;return {draft_id:src.draft_id,series_id:src.series_id,rule_id:src.rule_id,payment_date:v('payment_date'),evidence:v('evidence')};}return $('kind').value==='receipt'?{invoice_id:Number(v('invoice')),allocation_id:Number(v('allocation'))}:{invoice_id:Number(v('invoice')),amount:v('amount'),explanation:v('explanation')};} +$('operation').onsubmit=async e=>{e.preventDefault();if(preparing)return;preparing=true;locked();try{$('message').textContent='';const r=await api('/api/service-documents/preview',{kind:$('kind').value,action:$('action').value,document_key:documentKey,version:selected?.version??0,reason:$('reason').value,data:data()},crypto.randomUUID());if(r.status!==200)throw Error(r.value.message??'Conferência recusada.');preview=r.value;$('impact').replaceChildren();for(const [k,t]of [['kind','Documento'],['action','Acção'],['state','Estado posterior'],['amount','Montante MZN'],['financial_reduction','Redução do recebível'],['atomic_invoice_receipt','Emissão/recebimento/afectação atómicos'],['balance_increase_on_confirmation','Aumento da dívida ao confirmar anulação'],['balance_before','Dívida anterior'],['balance_after_confirmation','Dívida após confirmar anulação'],['invoice_preserved','Factura original preservada'],['money_refunded','Devolução de dinheiro']])if(preview.impact[k]!==undefined)el('p',`${t}: ${typeof preview.impact[k]==='boolean'?(preview.impact[k]?'Sim':'Não'):(labels[preview.impact[k]]??preview.impact[k])}`,$('impact'));locked();}catch(e){error(e);}finally{preparing=false;locked();}}; +async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;$('receipt').textContent=`Versão ${r.version} confirmada. ${r.replayed?'Recuperada sem duplicação.':''}`;$('impact').replaceChildren();await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}} +$('action').onchange=()=>form().catch(error);$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('new').onclick=()=>choose(null);$('all').onclick=()=>{history=null;page=1;load().catch(error);};$('refresh').onclick=()=>load().catch(error);for(const id of ['kind','size'])$(id).onchange=()=>{history=null;page=1;load().catch(error);};$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(preview||op.pending){e.preventDefault();e.returnValue='';}});$('kind').value=document.querySelector('meta[name="document-kind"]').content;load().catch(error); diff --git a/public/sgf-stock-catalogue.mjs b/public/sgf-stock-catalogue.mjs new file mode 100644 index 0000000..1c4242c --- /dev/null +++ b/public/sgf-stock-catalogue.mjs @@ -0,0 +1,15 @@ +import {confirmation} from './sgf-stock-final.mjs'; +const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content,states={prepared:'Preparado',approved:'Aprovado',refused:'Recusado'}; +let state,page=1,selected=null,recordKey=null,version=0,preview=null,operationKey=null,reading=false,preparing=false,generation=0,dirty=false,activeAction=''; +const el=(tag,text,parent)=>{const n=document.createElement(tag);if(text!==null)n.textContent=text;parent.append(n);return n;},error=e=>$('message').textContent=e.message; +async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return{status:r.status,value:await r.json()};} +const op=confirmation(async body=>{if(operationKey){const old=await api('/api/stock-catalogue/receipts/'+operationKey);if(old.status===200)return{status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/stock-catalogue/confirm',body);}); +function locked(){const busy=Boolean(reading||preparing||preview||op.pending);$('filters').disabled=busy;$('fields').disabled=busy||!state||(!state.capabilities.prepare&&!state.capabilities.review);$('confirm').hidden=!preview||op.uncertain;$('cancel').hidden=!preview||op.pending;$('retry').hidden=!op.uncertain;$('new').hidden=!state?.capabilities.prepare;$('discard').disabled=busy;} +function fields(){if(preview||op.pending)return;$('inputs').replaceChildren();activeAction=$('action').value;if($('action').value==='prepare'){for(const [id,label,value]of [['code','Código',selected?.data?.code??''],['name','Designação',selected?.data?.name??''],['unit','Unidade explícita',selected?.data?.unit??'']]){const l=el('label',label,$('inputs')),i=el('input',null,l);i.id=id;i.value=value;i.required=true;i.maxLength={code:30,name:150,unit:12}[id];i.readOnly=Boolean(selected?.stock_item_id&&id!=='name');}el('p','Código e unidade explícitos; sem quantidade, preço ou saldo inicial.',$('inputs'));}else if($('action').value==='review'){el('p','Conferir a preparação v'+version+': '+JSON.stringify(selected.data),$('inputs'));const l=el('label','Decisão explícita',$('inputs')),s=el('select',null,l);s.id='decision';s.required=true;for(const [v,t]of [['','Escolher…'],['approved','Aprovar e aplicar ao catálogo'],['refused','Recusar sem alterar o catálogo']])s.append(new Option(t,v));}} +function actions(){const prior=$('action').value;$('action').replaceChildren(new Option('Escolher…',''));if(recordKey&&state?.capabilities.prepare)$('action').append(new Option('Preparar artigo ou nova designação','prepare'));if(selected?.state==='prepared'&&state?.capabilities.review)$('action').append(new Option('Rever preparação escolhida','review'));if([...$('action').children].some(o=>o.value===prior))$('action').value=prior;fields();} +async function choose(row){if(reading||preparing||preview||op.pending)return;if(dirty){error(Error('Conclua a preparação ou descarte explicitamente os campos antes de escolher outro registo.'));return;}try{$('detail').textContent=JSON.stringify(row,null,2);if(!row.record_key){selected=null;recordKey=null;version=0;$('selection').textContent='Artigo anterior: apenas consulta, sem alterar ou associar automaticamente.';actions();return;}reading=true;locked();const r=await api('/api/stock-catalogue?view=history&record_key='+row.record_key+'&per_page=25');if(r.status!==200)throw Error(r.value.message??'Processo indisponível.');selected=r.value.records.data[0];if(!selected)throw Error('Sem versão actual conferida.');recordKey=selected.record_key;version=selected.version;$('selection').textContent='Processo escolhido '+recordKey+' · versão actual '+version+' · '+states[selected.state];$('detail').textContent=JSON.stringify(selected,null,2);actions();}catch(e){selected=null;recordKey=null;version=0;actions();error(e);}finally{reading=false;locked();}} +async function load(){if(preview||op.pending)throw Error('Conclua ou descarte a conferência em curso.');const current=++generation;reading=true;locked();try{const view=$('view').value,q={view,page,per_page:$('size').value};if($('search').value)q.search=$('search').value;if(view!=='items'&&$('state').value)q.state=$('state').value;if(view==='history'){if(!recordKey)throw Error('Escolha primeiro um processo com histórico.');q.record_key=recordKey;}const r=await api('/api/stock-catalogue?'+new URLSearchParams(q));if(current!==generation)return;if(r.status!==200)throw Error(r.value.message??'Consulta indisponível.');const old=JSON.stringify(state?.capabilities);state=r.value;$('records').replaceChildren();for(const row of state.records.data){const tr=el('tr',null,$('records')),d=row.data??row;el('td',d.code+' · '+d.name,tr);el('td',d.unit,tr);el('td',row.record_key?(states[row.state]??'Artigo deste percurso'):'Artigo anterior — só consulta',tr);el('td',row.version??'Sem versão neste percurso',tr);el('button',row.record_key?'Conferir processo actual':'Consultar artigo',el('td',null,tr)).onclick=()=>choose(row);}const p=state.records;$('page').textContent=p.total+' registo(s) · página '+p.current_page+' de '+p.last_page;$('previous').disabled=page<=1;$('next').disabled=page>=p.last_page;if(old!==JSON.stringify(state.capabilities))actions();}catch(e){if(current===generation){state=null;selected=null;recordKey=null;$('records').replaceChildren();$('selection').textContent='Consulta não conferida. Reabra a função autorizada.';}throw e;}finally{if(current===generation){reading=false;locked();}}} +$('new').onclick=()=>{if(reading||preview||op.pending||preparing)return;if(dirty){error(Error('Descarte os campos não guardados antes de iniciar outro artigo.'));return;}$('reason').value='';selected=null;recordKey=crypto.randomUUID();version=0;$('detail').textContent='';$('selection').textContent='Novo artigo a preparar; ainda não existe no catálogo.';actions();$('action').value='prepare';fields();};$('action').onchange=()=>{if(dirty){$('action').value=activeAction;error(Error('Descarte os campos antes de mudar de acção.'));return;}fields();};$('operation').oninput=e=>{if(e.target?.id!=='action')dirty=true;}; +$('operation').onsubmit=async e=>{e.preventDefault();if(preparing||preview||op.pending)return;preparing=true;locked();try{const action=$('action').value;if(!recordKey||!action)throw Error('Escolha um processo ou prepare um novo artigo.');const data=action==='prepare'?{code:$('code').value,name:$('name').value,unit:$('unit').value}:{decision:$('decision').value};operationKey=crypto.randomUUID();const r=await api('/api/stock-catalogue/preview',{action,record_key:recordKey,version,data,reason:$('reason').value},operationKey);if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;$('referenceKey').value=operationKey;$('impact').replaceChildren();el('h3','Impacto antes da confirmação',$('impact'));const i=preview.impact;el('p','Estado anterior: '+(states[i.before.state]??'sem processo')+' · versão '+i.before.version,$('impact'));el('p','Estado posterior: '+states[i.after.state]+' · versão '+i.after.version,$('impact'));el('pre',JSON.stringify({artigo_anterior:i.before.item,dados_conferidos:i.after.data},null,2),$('impact'));el('p',i.creates_article?'Será criado um artigo sem saldo inicial.':i.renames_owned_article?'Será alterada apenas a designação deste artigo; código/unidade mantidos.':'A intenção/decisão será conservada sem alterar o catálogo físico.',$('impact'));el('p','Movimentos:0 · lançamentos:0 · preço/custo/lote: nenhum.',$('impact'));}catch(e){error(e);}finally{preparing=false;locked();}}; +async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;selected=null;recordKey=null;version=0;dirty=false;$('reason').value='';$('impact').replaceChildren();$('detail').textContent='';$('receipt').textContent='Versão '+r.version+' · '+states[r.state]+(r.replayed?' · recuperada sem duplicação':' · registada');$('selection').textContent='Escolha explicitamente o próximo artigo/processo.';actions();await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}} +$('discard').onclick=()=>{if(preview||op.pending||reading||preparing)return;dirty=false;selected=null;recordKey=null;version=0;$('reason').value='';$('detail').textContent='';$('selection').textContent='Campos descartados; escolha um processo ou prepare um novo artigo.';$('action').value='';actions();locked();};$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('apply').onclick=()=>{page=1;load().catch(error);};$('refresh').onclick=()=>load().catch(error);$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};$('recovery').onsubmit=async e=>{e.preventDefault();try{const key=$('referenceKey').value.trim().toLowerCase(),r=await api('/api/stock-catalogue/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message??'Sem recibo próprio nesta função.');$('receipt').textContent='Recibo histórico v'+r.value.result.version+'. Nenhuma operação repetida.';if(op.pending&&key===operationKey)await finish(true);}catch(e){error(e);}};window.addEventListener('beforeunload',e=>{if(dirty||preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error); diff --git a/public/sgf-stock-completion.mjs b/public/sgf-stock-completion.mjs new file mode 100644 index 0000000..8c61c4a --- /dev/null +++ b/public/sgf-stock-completion.mjs @@ -0,0 +1,22 @@ +import {confirmation} from './sgf-stock-final.mjs'; +const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content; +let state,selected=null,page=1,preview=null,historyKey=null; +async function api(path,body){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return {status:r.status,value:await r.json()};} +const op=confirmation(body=>api('/api/stock-completion/confirm',body)); +function error(e){$('message').textContent=e.message;} +function el(tag,text,parent){const n=document.createElement(tag);if(text!==null)n.textContent=text;parent.append(n);return n;} +function locked(){const busy=Boolean(preview||op.pending);$('filters').disabled=busy;$('fields').disabled=busy;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;} +function field(name,label,type='text',value='',required=true){const l=el('label',label,$('inputs')),i=el('input',null,l);i.id='f-'+name;i.type=type;i.required=required;i.value=value;if(type==='number'){i.min=0;i.step='0.001';}return i;} +function form(){if(!state)return;const a=$('action').value;$('inputs').replaceChildren();if(a==='save'&&$('family').value==='policy'){ + const d=selected?.data??{};for(const [key,label,initial] of [['safety_milli','Segurança (UN)',2],['minimum_stock_milli','Mínimo físico (UN)',5],['target_milli','Alvo (UN)',10],['maximum_milli','Máximo (UN)',20],['daily_milli','Consumo diário conferido (UN)',1],['minimum_milli','Encomenda mínima (UN)',1],['pack_milli','Embalagem (UN)',1]])field(key,label,'number',d[key]===undefined?initial:d[key]/1000); + for(const [key,label,initial] of [['lead_days','Prazo de fornecimento (dias)',2],['approval_days','Prazo de aprovação (dias)',1],['horizon_days','Horizonte (dias)',14]]){const i=field(key,label,'number',d[key]??initial);i.step='1';} + field('basis','Evidência dos parâmetros','text',d.basis??''); +}else if(a==='save'){field('quantity','Quantidade a devolver (UN)','number',selected?.data?.quantity_milli?selected.data.quantity_milli/1000:'');field('reference','Referência da guia','text',selected?.data?.reference??'');} +else if(a==='execute'){field('event_on','Data efectiva da saída','date','');field('evidence','Comprovativo da saída','text','');}} +function choose(row){selected=row;$('selected').textContent=row?`Processo ${row.record_key}, versão ${row.version}, estado ${row.state}.`:'Novo processo: a origem é a recepção QA indicada acima.';if(row){for(const [key,label]of [['safety_milli','Segurança UN'],['minimum_stock_milli','Mínimo UN'],['target_milli','Alvo UN'],['maximum_milli','Máximo UN'],['daily_milli','Consumo diário UN'],['lead_days','Prazo dias'],['horizon_days','Horizonte dias'],['basis','Evidência'],['quantity_milli','Quantidade UN'],['reference','Guia']])if(row.data[key]!==undefined)el('span',' · '+label+': '+(key.endsWith('_milli')?row.data[key]/1000:row.data[key]),$('selected'));el('span',' · Autor '+row.created_by+' · '+row.created_at+' · '+row.reason,$('selected'));}$('action').replaceChildren();el('option','Escolher…',$('action')).value=''; + const allowed=[];if(!state.reviewer&&(!row||['draft','returned'].includes(row.state)))allowed.push(['save','Guardar preparação']);if(state.reviewer&&row){if(['draft','returned'].includes(row.state))allowed.push(['approve','Aprovar versão']);if(['draft','returned','approved'].includes(row.state))allowed.push(['return','Devolver para revisão']);if(row.state==='approved'&&$('family').value==='policy')allowed.push(['suspend','Suspender política']);}if(!state.reviewer&&row?.state==='approved'&&$('family').value==='supplier_return')allowed.push(['execute','Registar saída física']);for(const [v,t]of allowed)el('option',t,$('action')).value=v;form();} +async function load(){const q=new URLSearchParams({family:$('family').value,page,per_page:$('size').value});if($('state').value)q.set('state',$('state').value);if(historyKey)q.set('record_key',historyKey);const r=await api('/api/stock-completion?'+q);if(r.status!==200)throw Error(r.value.message??'Consulta recusada.');state=r.value;$('summary').textContent=`${state.basis.item.code} — ${state.basis.item.name}; ${state.basis.warehouse.name}. Saldo físico: ${state.basis.balance.quantity/1000} ${state.basis.item.unit}. Linha de recepção: ${state.receipt_item_id}.`;$('records').replaceChildren();for(const row of state.records.data){const tr=el('tr',null,$('records'));for(const value of [row.record_key,row.version,row.state])el('td',String(value),tr);const td=el('td',null,tr),b=el('button','Escolher esta versão',td);b.onclick=()=>choose(row);const h=el('button','Ver versões',td);h.onclick=()=>{historyKey=row.record_key;page=1;load().catch(error);};}const p=state.records;$('page').textContent=`Página ${p.current_page} de ${p.last_page} · ${p.total} processos`;$('previous').disabled=page<=1;$('next').disabled=page>=p.last_page;$('new').hidden=state.reviewer;choose(null);} +function data(){const read=k=>$('f-'+k)?.value??'';if($('action').value==='save'&&$('family').value==='policy'){const d={stock_item_id:state.basis.item.id,warehouse_id:state.basis.warehouse.id,forecast_method:'manual',purpose:'internal',origin:'local',basis:read('basis')};for(const k of ['safety_milli','minimum_stock_milli','target_milli','maximum_milli','daily_milli','minimum_milli','pack_milli']){const raw=read(k);if(!/^\d{1,9}(\.\d{1,3})?$/.test(raw))throw Error('Use quantidades exactas até três casas decimais.');const [whole,fraction='']=raw.split('.');d[k]=Number(whole)*1000+Number(fraction.padEnd(3,'0'));};for(const k of ['lead_days','approval_days','horizon_days'])d[k]=Number(read(k));return d;}if($('action').value==='save')return {kind:'supplier_return',stock_item_id:state.basis.item.id,warehouse_id:state.basis.warehouse.id,receipt_item_id:state.receipt_item_id,quantity:read('quantity'),reference:read('reference')};if($('action').value==='execute')return {event_on:read('event_on'),evidence:read('evidence')};return {};} +$('operation').onsubmit=async e=>{e.preventDefault();try{$('message').textContent='';const r=await api('/api/stock-completion/preview',{family:$('family').value,action:$('action').value,operation_key:crypto.randomUUID(),record_key:selected?.record_key??null,version:selected?.version??0,revision:state.revision,reason:$('reason').value,data:data()});if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;$('impact').replaceChildren();const dl=el('dl',null,$('impact'));for(const [label,value]of [['Acção',preview.impact.action],['Estado posterior',preview.impact.state],['Físico anterior',preview.impact.before_milli/1000],['Físico posterior',preview.impact.after_milli/1000],['Variação física',preview.impact.physical_change_milli/1000],['Crédito financeiro','Não criado'],['Contabilidade','Conferência separada no mesmo diário']]){el('dt',label,dl);el('dd',String(value),dl);}locked();}catch(e){error(e);}}; +async function finish(retry=false){try{locked();const v=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;$('receipt').textContent=`Confirmado: ${v.record_key}, versão ${v.version}. ${v.replayed?'Recibo recuperado, sem repetir operação.':''}`;$('impact').replaceChildren();await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}} +$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('action').onchange=form;$('new').onclick=()=>choose(null);$('all').onclick=()=>{historyKey=null;page=1;load().catch(error);};$('refresh').onclick=()=>load().catch(error);for(const id of ['family','state','size'])$(id).onchange=()=>{page=1;historyKey=null;load().catch(error);};$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error); diff --git a/public/sgf-stock-final.mjs b/public/sgf-stock-final.mjs new file mode 100644 index 0000000..09b412b --- /dev/null +++ b/public/sgf-stock-final.mjs @@ -0,0 +1,44 @@ +export function confirmation(send){let frozen=null,busy=false,uncertain=false;return { + get pending(){return Boolean(frozen)},get uncertain(){return uncertain},get busy(){return busy}, + async run(body){if(busy)throw Error('A confirmação está em curso.');if(frozen&&JSON.stringify(body)!==JSON.stringify(frozen))throw Error('Recupere a confirmação anterior.');frozen??=JSON.parse(JSON.stringify(body));busy=true;uncertain=true;try{const r=await send(frozen);if(r.status>=400&&r.status<500){frozen=null;uncertain=false;throw Error(r.value?.message??'Decisão recusada. Actualize e confira novamente.');}if(r.status<200||r.status>=300||!r.value||typeof r.value.replayed!=='boolean')throw Error('Resultado incerto. Recupere a mesma confirmação.');frozen=null;uncertain=false;return r.value;}finally{busy=false;}}, + retry(){if(!frozen)throw Error('Sem confirmação pendente.');return this.run(frozen);} +};} + +if(typeof document!=='undefined'&&document.getElementById('transfers')){ + const $=id=>document.getElementById(id),text=(tag,value)=>{const el=document.createElement(tag);el.textContent=String(value);return el;};let state=null,preview=null,page=1,historyPage=1; + const api=async(path,body)=>{const r=await fetch(path,{method:body?'POST':'GET',credentials:'same-origin',headers:{Accept:'application/json',...(body?{'Content-Type':'application/json','X-CSRF-TOKEN':document.querySelector('meta[name=csrf-token]').content,'X-SGF-Context':document.querySelector('meta[name=sgf-context]').content}:{})},body:body?JSON.stringify(body):undefined});let value;try{value=await r.json();}catch{value=null;}return {status:r.status,value};}; + const op=confirmation(body=>api('/api/stock-final/confirm',body)); + const lock=()=>{const pending=op.pending||op.busy;document.body.dataset.sgfOperationUncertain=String(op.uncertain);$('fields').disabled=pending||Boolean(preview);$('refresh').disabled=pending;$('confirm').hidden=!preview||pending;$('retry').hidden=!op.uncertain||op.busy;$('cancel').hidden=!preview||pending;}; + const failure=e=>{$('message').textContent=e.message;lock();}; + const field=(id,label,type='text',value='')=>{const l=text('label',label);const el=document.createElement('input');el.id=id;el.type=type;el.value=value;el.required=true;l.append(el);$('inputs').append(l);return el;}; + const select=(id,label,rows)=>{const l=text('label',label),el=document.createElement('select');el.id=id;el.required=true;el.append(new Option('Escolher…',''));for(const [value,name]of rows)el.append(new Option(name,String(value)));l.append(el);$('inputs').append(l);return el;}; + async function history(){const r=await api('/api/stock-final/history?'+new URLSearchParams({page:historyPage,per_page:25}));if(r.status!==200)throw Error(r.value?.message??'Histórico indisponível.');const v=r.value;$('history').replaceChildren(...v.data.map(row=>text('p',row.created_at+' · autor QA '+row.host_author_id+' · '+row.result.impact.action+' · '+row.reason)));$('historyPage').textContent='Página '+v.current_page+' de '+v.last_page;$('historyPrevious').disabled=historyPage<=1;$('historyNext').disabled=historyPage>=v.last_page;} + const p=()=>state.policy,lotContext=()=>state.basis.lot_context; + const qty=v=>(v/1000).toLocaleString('pt-MZ',{maximumFractionDigits:3}); + function renderFields(){preview=null;$('impact').replaceChildren();$('inputs').replaceChildren();const a=$('action').value;if(!a){lock();return;} + if(['dispatch','receive','return'].includes(a)){ + if(a==='dispatch'){select('item','Artigo',state.items.filter(i=>i.id===p().transfer_item_id).map(i=>[i.id,i.code+' · '+i.name]));select('source','Armazém de origem',state.warehouses.filter(w=>w.id===p().source_warehouse_id).map(w=>[w.id,w.name]));select('destination','Armazém de destino',state.warehouses.filter(w=>w.id===p().destination_warehouse_id).map(w=>[w.id,w.name]));} + else select('transfer','Transferência da página actual',state.transfers.data.filter(t=>t.remaining_milli>0).map(t=>[t.transfer_key,t.transfer_key+' · por receber '+qty(t.remaining_milli)]));field('quantity','Quantidade (UN)','text'); + }else if(a==='prepare'){ + select('item','Artigo a controlar',state.items.filter(i=>i.id===p().lot_item_id).map(i=>[i.id,i.code+' · '+i.name]));select('warehouse','Armazém',state.warehouses.filter(w=>w.id===p().source_warehouse_id).map(w=>[w.id,w.name])); + field('days','Validade mínima restante (dias)','number','0');for(let n=1;n<=2;n++){field('code'+n,'Código do lote '+n);field('expires'+n,'Validade do lote '+n,'date');field('lotQty'+n,'Quantidade do lote '+n);} + }else if(a==='activate'){select('setup','Configuração preparada',state.lot_records.filter(r=>r.kind==='lot_setup'&&r.state==='draft').map(r=>[r.id,'Configuração #'+r.id+' · versão '+r.version]));} + else if(['identify','hold','release'].includes(a)){select('lot','Lote', (lotContext()?.lots??[]).map(l=>[l.id,(l.data.code??'Sem código')+' · '+l.state+' · '+qty(l.quantity_milli)+'UN']));if(a==='identify'){field('code','Código');field('expires','Validade','date');}} + else if(a==='count'){field('quantity','Total explicitamente contado (UN)');for(const l of lotContext()?.lots??[])field('count'+l.id,'Quantidade contada · '+(l.data.code??'#'+l.id));} + else if(a==='adjustment')select('count','Contagem por rever',state.counts.map(c=>[c.id,c.reference+' · '+qty(c.quantity_milli)+'UN']));lock(); + } + function data(){const a=$('action').value,value=id=>$(id).value,num=id=>Number(value(id));if(['dispatch','receive','return'].includes(a)){const transfer=a==='dispatch'?null:state.transfers.data.find(t=>t.transfer_key===value('transfer'));if(a!=='dispatch'&&!transfer)throw Error('Escolha uma transferência.');return {quantity:value('quantity'),transfer_key:transfer?.transfer_key??crypto.randomUUID(),stock_item_id:a==='dispatch'?num('item'):transfer.stock_item_id,source_warehouse_id:a==='dispatch'?num('source'):transfer.source_warehouse_id,destination_warehouse_id:a==='dispatch'?num('destination'):transfer.destination_warehouse_id};} + if(a==='prepare')return {version:0,data:{stock_item_id:num('item'),warehouse_id:num('warehouse'),requires_expiry:true,minimum_remaining_days:num('days'),lots:[1,2].map(n=>({code:value('code'+n),expires_on:value('expires'+n),quantity:value('lotQty'+n)}))}}; + if(a==='activate'){const r=state.lot_records.find(r=>r.id===num('setup'));return {record_key:r.record_key,version:r.version,data:{}};} + if(['identify','hold','release'].includes(a)){const l=lotContext().lots.find(l=>l.id===num('lot'));return {record_key:l.record_key,version:l.version,data:a==='identify'?{code:value('code'),expires_on:value('expires'),no_expiry:false}:{lot_id:l.id,warehouse_id:p().source_warehouse_id,state_version:l.state_version}};} + if(a==='count'){const context=state.lot_count_source;if(!context)throw Error('Active e confira os lotes antes da contagem.');return {quantity:value('quantity'),ledger_version:state.basis.lot_balance.version,lot_version:context.hash,lot_counts:context.lots.map(l=>({lot_id:l.lot_id,quantity:value('count'+l.lot_id)}))};} + if(a==='adjustment')return {quantity:'0.000',ledger_version:state.basis.lot_balance.version,count_id:num('count')};throw Error('Escolha uma operação.');} + async function load(){if(op.pending)throw Error('Recupere a confirmação pendente antes de actualizar.');const r=await api('/api/stock-final?'+new URLSearchParams({page,per_page:$('size').value,search:$('search').value}));if(r.status!==200)throw Error(r.value?.message??'Não foi possível consultar o contexto.');state=r.value;preview=null;$('message').textContent='';$('summary').replaceChildren(text('p','Artigo de transferência: origem '+qty(state.basis.transfer_source.quantity)+'UN; destino '+qty(state.basis.transfer_destination.quantity)+'UN. Trânsito total: '+qty(state.in_transit_milli)+'UN.'),text('p','Artigo por lotes: físico '+qty(state.basis.lot_balance.quantity)+'UN; disponível por qualidade '+(lotContext()?qty(lotContext().usable_milli)+'UN':'por conferir')+'.')); + $('action').replaceChildren(new Option('Escolher…',''));for(const [a,label]of(state.reviewer?[['receive','Receber transferência'],['activate','Activar controlo por lotes'],['release','Libertar lote'],['adjustment','Rever e aplicar diferença contada']]:[['dispatch','Expedir transferência'],['return','Receber retorno à origem'],['prepare','Preparar controlo por lotes'],['identify','Identificar/corrigir lote'],['hold','Reter lote'],['count','Registar contagem por lote']]))$('action').append(new Option(label,a)); + $('transfers').replaceChildren();for(const t of state.transfers.data){const tr=document.createElement('tr');for(const v of[t.transfer_key,qty(t.quantity_milli),qty(t.remaining_milli)])tr.append(text('td',v));$('transfers').append(tr);}$('page').textContent='Página '+state.transfers.current_page+' de '+state.transfers.last_page;$('previous').disabled=page<=1;$('next').disabled=page>=state.transfers.last_page; + $('lots').replaceChildren(...(lotContext()?.lots??[]).map(l=>text('p',(l.data.code??'Sem código')+' · '+qty(l.quantity_milli)+'UN · '+l.state+' · validade '+(l.data.expires_on??'sem prazo')+' · versão '+l.version)));renderFields();await history();} + $('historyPrevious').onclick=()=>{historyPage--;history().catch(failure);};$('historyNext').onclick=()=>{historyPage++;history().catch(failure);}; + $('action').onchange=renderFields;$('operation').onsubmit=async e=>{e.preventDefault();try{$('fields').disabled=true;const r=await api('/api/stock-final/preview',{action:$('action').value,operation_key:crypto.randomUUID(),revision:state.revision,reason:$('reason').value,data:data()});if(r.status!==200)throw Error(r.value?.message??'Conferência recusada.');preview=r.value.preview;const labels={action:'Operação',quantity_milli:'Quantidade em milésimos',physical_before_milli:'Físico anterior em milésimos',physical_after_milli:'Físico posterior em milésimos',in_transit_after_milli:'Em trânsito após decisão em milésimos',ownership_change_milli:'Alteração da propriedade em milésimos',physical_change_milli:'Diferença física em milésimos',counted_milli:'Contado em milésimos',state:'Estado após decisão',financial_posting:'Cria lançamento automático',stock_item_id:'Artigo',warehouse_id:'Armazém'};const dl=document.createElement('dl');for(const [k,v]of Object.entries(r.value.impact)){dl.append(text('dt',labels[k]??k),text('dd',typeof v==='boolean'?(v?'Sim':'Não'):v));}$('impact').replaceChildren(text('h3','Impacto desta decisão'),dl);lock();}catch(e){$('fields').disabled=false;failure(e);}}; + async function submit(retry=false){try{$('message').textContent='';const promise=retry?op.retry():op.run({preview});lock();const result=await promise;$('receipt').textContent='Decisão registada. '+(result.replayed?'Recibo recuperado. ':'')+(result.event_id?'Evento #'+result.event_id:result.operation_id?'Operação #'+result.operation_id:'Registo #'+result.record_id);preview=null;await load();}catch(e){failure(e);}finally{lock();}} + $('confirm').onclick=()=>submit();$('retry').onclick=()=>submit(true);$('cancel').onclick=()=>{preview=null;$('impact').replaceChildren();lock();};$('refresh').onclick=()=>load().catch(failure);$('previous').onclick=()=>{page--;load().catch(failure);};$('next').onclick=()=>{page++;load().catch(failure);};$('size').onchange=$('search').onchange=()=>{page=1;load().catch(failure);};window.addEventListener('beforeunload',e=>{if(op.pending||preview){e.preventDefault();e.returnValue='';}});load().catch(failure); +} diff --git a/public/stock-operations-workspace.mjs b/public/stock-operations-workspace.mjs new file mode 100644 index 0000000..4688bb4 --- /dev/null +++ b/public/stock-operations-workspace.mjs @@ -0,0 +1,22 @@ +const root=document.querySelector('#stock-workspace'),form=document.querySelector('#operation'),fields=document.querySelector('#fields'); +const status=document.querySelector('#status'),impactSection=document.querySelector('#impact-section'),confirm=document.querySelector('#confirm'); +let busy=false,preview=null,uncertain=false,dirty=false,epoch=0,historyPage=1; +const pickers=[]; +const node=(tag,text)=>{const el=document.createElement(tag);if(text!==undefined)el.textContent=text;return el;}; +function message(text){status.textContent=text;} +function lock(value){busy=value;fields.disabled=value||uncertain;confirm.disabled=value;document.querySelectorAll('button').forEach(b=>{if(!fields.contains(b)&&b!==confirm)b.disabled=value||uncertain;});} +async function api(path,data){const r=await fetch('/api/stock-operations'+path,{method:data?'POST':'GET',headers:{Accept:'application/json','X-SGF-Context':root.dataset.context,'X-CSRF-TOKEN':root.dataset.csrf,...(data?{'Content-Type':'application/json'}:{})},...(data?{body:JSON.stringify(data)}:{})});let body;try{body=await r.json();}catch{body={};}if(!r.ok){const error=Error(body.message||'Operação indisponível.');error.status=r.status;throw error;}return body;} +function invalidate(){if(uncertain)return;epoch++;preview=null;impactSection.hidden=true;} +function selected(){const d=Object.fromEntries(new FormData(form));for(const p of pickers){if(p.wrap.hidden||p.name==='equipment_id'&&!d[p.name])delete d[p.name];else d[p.name]=Number(d[p.name]);}if(d.kind!=='transfer')delete d.destination_warehouse_id;if(d.kind!=='issue')delete d.destination;return d;} +async function loadPicker(p,page=1){const request=++p.epoch;if(p.view==='counts'&&(!form.elements.stock_item_id.value||!form.elements.warehouse_id.value))return;const filters={view:p.view,page,search:p.search.value};if(p.view==='counts'){filters.stock_item_id=form.elements.stock_item_id.value;filters.warehouse_id=form.elements.warehouse_id.value;}const value=await api('?'+new URLSearchParams(filters));if(request!==p.epoch)return;p.page=value.current_page;p.next.disabled=!value.next_page_url;p.previous.disabled=page<=1;const selected=p.select.selectedOptions[0];const remembered=selected?.value?selected.cloneNode(true):null;p.select.replaceChildren(node('option','Seleccione explicitamente'));p.select.options[0].value='';if(remembered)p.select.append(remembered);for(const row of value.data){if(remembered&&remembered.value===String(row.id))continue;const option=node('option',`${row.code?row.code+' · ':''}${row.name}${row.unit?' ('+row.unit+')':''}`);option.value=row.id;p.select.append(option);}if(remembered)p.select.value=remembered.value;} +for(const [name,label,view] of [['warehouse_id','Armazém de origem','warehouses'],['destination_warehouse_id','Armazém de destino','warehouses'],['stock_item_id','Artigo','items'],['responsible_id','Responsável','people'],['equipment_id','Equipamento de destino (quando exigido)','equipment'],['count_id','Contagem física por ajustar','counts']]){ + const wrap=node('div'),search=node('input'),select=node('select'),next=node('button','Mais resultados'),previous=node('button','Resultados anteriores'),find=node('button','Pesquisar');search.placeholder='Pesquisar '+label.toLowerCase();search.setAttribute('aria-label',search.placeholder);select.name=name;select.required=name!=='equipment_id';for(const b of [next,previous,find])b.type='button';const title=node('label',label);title.append(select);wrap.append(title,search,find,previous,next);document.querySelector('#pickers').append(wrap);const p={name,view,wrap,search,select,next,previous,page:1,epoch:0};pickers.push(p);find.onclick=()=>loadPicker(p).catch(e=>message(e.message));next.onclick=()=>loadPicker(p,p.page+1).catch(e=>message(e.message));previous.onclick=()=>loadPicker(p,p.page-1).catch(e=>message(e.message)); +} +function kind(){const k=form.elements.kind.value;for(const p of pickers){const visible=p.name==='destination_warehouse_id'?k==='transfer':p.name==='equipment_id'?k==='issue':p.name==='count_id'?k==='adjustment':true;p.wrap.hidden=!visible;p.select.disabled=!visible;p.select.required=visible&&p.name!=='equipment_id';}document.querySelector('#destination-field').hidden=k!=='issue';form.elements.destination.required=k==='issue';form.elements.destination.disabled=k!=='issue';document.querySelector('#quantity-field').hidden=k==='adjustment';form.elements.quantity.disabled=k==='adjustment';form.elements.quantity.required=k!=='adjustment';} +form.addEventListener('input',()=>{dirty=true;invalidate();});form.addEventListener('change',e=>{dirty=true;invalidate();kind();if(['kind','warehouse_id','stock_item_id'].includes(e.target?.name)){const p=pickers.find(p=>p.name==='count_id');p.select.value='';if(!p.wrap.hidden)loadPicker(p).catch(e=>message(e.message));}}); +form.onsubmit=async e=>{e.preventDefault();if(busy||uncertain)return;invalidate();const stamp=epoch,data=selected();lock(true);message('A conferir o saldo e o impacto…');try{const basisKeys=['kind','warehouse_id','stock_item_id','responsible_id','destination_warehouse_id','count_id'];const basis=await api('/basis?'+new URLSearchParams(Object.fromEntries(Object.entries(data).filter(([k])=>basisKeys.includes(k)))));if(data.kind==='count')data.ledger_version=basis.basis.balances.find(b=>b.warehouse_id===data.warehouse_id).version;const result=await api('/preview',{operation_key:crypto.randomUUID(),revision:basis.revision,data});if(stamp!==epoch)return;preview=result.preview;const dl=document.querySelector('#impact');dl.replaceChildren();for(const [label,value] of [['Operação',({transfer:'Transferência física',issue:'Saída para utilização',count:'Registar contagem física',adjustment:'Aplicar diferença da contagem'})[data.kind]],['Artigo',basis.basis.item.name],['Quantidade',`${data.kind==='adjustment'?result.impact.quantity_milli/1000:data.quantity} ${basis.basis.item.unit}`],['Origem',basis.basis.warehouses.find(w=>w.id===data.warehouse_id)?.name],['Destino',data.kind==='transfer'?basis.basis.warehouses.find(w=>w.id===data.destination_warehouse_id)?.name:data.kind==='issue'?data.destination:'Mesmo armazém'],['Saldo físico anterior',String(result.impact.theoretical_milli/1000)+' '+basis.basis.item.unit],['Variação na origem',String(result.impact.change_milli/1000)+' '+basis.basis.item.unit],['Saldo físico posterior',String(result.impact.quantity_after_milli/1000)+' '+basis.basis.item.unit],...(data.kind==='transfer'?[['Saldo posterior no destino',String(result.impact.destination_quantity_after_milli/1000)+' '+basis.basis.item.unit]]:[]),['Responsável',basis.basis.responsible.name],['Referência',result.impact.reference],['Fundamento',result.impact.reason],['Contabilidade','Sem lançamento contabilístico']])dl.append(node('dt',label),node('dd',String(value)));impactSection.hidden=false;message('Confira os dados antes de confirmar.');}catch(error){message(error.message);}finally{lock(false);}}; +confirm.onclick=async()=>{if(busy||!preview)return;lock(true);message(uncertain?'A recuperar a mesma operação…':'A confirmar…');try{const result=await api('/confirm',{preview});uncertain=false;preview=null;impactSection.hidden=true;dirty=false;form.reset();kind();message(`Operação ${result.reference} registada${result.replayed?' (recibo recuperado)':''}.`);historyPage=1;await history();}catch(error){if(!error.status||error.status>=500){uncertain=true;message('Resultado ainda por confirmar. Use o mesmo botão para recuperar; os campos ficam protegidos.');}else{uncertain=false;invalidate();message(error.message);}}finally{lock(false);}}; +async function history(){const value=await api('?'+new URLSearchParams({view:'history',page:historyPage}));const container=document.querySelector('#history');const table=node('table'),head=node('tr');for(const text of ['Referência','Operação','Quantidade','Data'])head.append(node('th',text));table.append(head);for(const row of value.data){const tr=node('tr');for(const text of [row.reference,({transfer:'Transferência',issue:'Saída',count:'Contagem',adjustment:'Ajuste'})[row.kind]||row.kind,String(row.quantity_milli/1000),row.recorded_at])tr.append(node('td',text));table.append(tr);}container.replaceChildren(table);document.querySelector('#older').disabled=!value.next_page_url;document.querySelector('#newer').disabled=historyPage<=1;} +document.querySelector('#refresh').onclick=()=>{historyPage=1;history().catch(e=>message(e.message));};document.querySelector('#older').onclick=()=>{historyPage++;history().catch(e=>message(e.message));};document.querySelector('#newer').onclick=()=>{historyPage=Math.max(1,historyPage-1);history().catch(e=>message(e.message));}; +window.addEventListener('beforeunload',e=>{if(uncertain||preview||busy||dirty){e.preventDefault();e.returnValue='';}}); +kind();Promise.all([...pickers.map(p=>loadPicker(p)),history()]).catch(e=>message(e.message)); diff --git a/resources/css/tempo-theme.css b/resources/css/tempo-theme.css index 6fbddf0..05daadc 100644 --- a/resources/css/tempo-theme.css +++ b/resources/css/tempo-theme.css @@ -57,4 +57,16 @@ th{background:#f8fafc;color:#475569}th,td{border-color:var(--tempo-line);line-he .module-menu summary{cursor:pointer;padding:.5rem}.module-menu details>div{position:absolute;top:100%;left:0;background:#fff;box-shadow:0 6px 24px #1232;min-width:240px;max-height:65vh;overflow:auto;padding:.5rem} .module-menu details button{display:block;width:100%;text-align:left;margin:.2rem 0}.module-menu [aria-current=page]{background:#e1efed;font-weight:700} .context-transition-backdrop{position:fixed;inset:0;background:#10233299;z-index:100;display:grid;place-items:center;padding:1rem}.context-transition-dialog{max-width:580px;max-height:90vh;overflow:auto} +.module-dropdowns{gap:.35rem;justify-content:flex-start;border-top:1px solid var(--tempo-line)} +.module-dropdowns>details>summary{list-style:none;display:flex;align-items:center;gap:1.4rem;padding:.75rem 1.1rem;border-radius:6px;font-weight:600;color:#334155} +.module-dropdowns>details>summary::-webkit-details-marker{display:none}.module-dropdowns>details>summary:hover,.module-dropdowns>details[open]>summary{background:#e2e8ef} +.module-dropdowns>details.active-module>summary{background:#dcece7;color:#175346;box-shadow:inset 0 -3px #28715d} +.module-dropdowns>details>div{z-index:40;min-width:280px;max-width:min(380px,90vw);border:1px solid #d9e1e8;border-radius:8px;padding:.6rem;box-shadow:0 12px 30px #10233226} +.module-dropdowns .module-dropdown-title{margin:.2rem .5rem .7rem;font-weight:700;color:#175346}.module-dropdowns section{margin:0;padding:.35rem 0;border:0;border-top:1px solid #edf1f4;background:transparent;box-shadow:none} +.module-dropdowns section h2{margin:.4rem .5rem;font-size:.75rem;letter-spacing:.04em;text-transform:uppercase;color:#64748b}.module-dropdowns section button{border:0;background:transparent;color:#253c4b;padding:.65rem .7rem;border-radius:5px} +.module-dropdowns section button:hover,.module-dropdowns section button:focus-visible{background:#edf5f1}.module-dropdowns section button[aria-current=page]{background:#dcece7;color:#175346} +@media(max-width:700px){.module-dropdowns{gap:.15rem}.module-dropdowns>details>summary{padding:.65rem .7rem;gap:.7rem}.module-dropdowns>details>div{left:.6rem;right:.6rem;width:auto;max-width:none;max-height:60vh}.module-topbar .topbar-account{flex-wrap:wrap;gap:.7rem}} @media(max-width:700px){.module-topbar,.module-menu{padding:.6rem}.module-topbar nav{width:100%}.module-menu details>div{position:fixed;left:1rem;right:1rem;top:auto;min-width:0}.module-topbar .topbar-account{display:flex}} + +/* Authorized module overview: no business totals inferred from page rows. */ +.overview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px}.overview-grid article{border:1px solid #d8e2df;border-radius:10px;padding:18px;background:#fff}.overview-grid article button{display:block;text-align:left;margin:8px 0;max-width:100%}.overview-steps{display:flex;flex-wrap:wrap;gap:12px 30px;padding:18px 30px;background:#edf4f1;border-radius:8px}.overview-steps li{padding:6px 10px}.overview-definitions{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:20px}.overview-definitions dt{font-weight:650;margin-bottom:8px}.overview-definitions dd{margin:0;line-height:1.6}.workspace-overview td small{display:block;margin-top:6px}.workspace-overview .table-scroll{overflow-x:auto}.workspace-overview .actions{flex-wrap:wrap} diff --git a/resources/data/bank-statement-qa.csv b/resources/data/bank-statement-qa.csv new file mode 100644 index 0000000..9156463 --- /dev/null +++ b/resources/data/bank-statement-qa.csv @@ -0,0 +1,3 @@ +date,reference,debit,credit,balance +2026-11-10,QA-OUT-20,20.00,0.00,-20.00 +2026-11-10,QA-IN-20,0.00,20.00,0.00 diff --git a/resources/data/fiscal-rule-qa.json b/resources/data/fiscal-rule-qa.json new file mode 100644 index 0000000..31bfca2 --- /dev/null +++ b/resources/data/fiscal-rule-qa.json @@ -0,0 +1,90 @@ +{ + "contract": "SGF-FISCAL-RULES-COMMON-QA-v1", + "not_tax_law": true, + "cases": [ + { + "fixture_id": "QA1", + "definition": { + "family": "qa", + "operation": "percentage_add", + "currency": "MZN", + "unit": "money", + "precision": 2, + "rounding": "DOWN", + "priority": 10, + "source_reference": "QA-FISCAL-COMMON-20261110", + "source_status": "qa_checked", + "conditions": "Somente ensaio aritmético artificial; não enquadra contribuintes ou documentos.", + "expression": { + "language": "investmoz.tax.v1", + "nodes": { + "base": { + "op": "input", + "key": "base" + }, + "rate": { + "op": "parameter", + "key": "rate" + }, + "raw": { + "op": "multiply", + "args": [ + "base", + "rate" + ] + } + }, + "result": "raw" + }, + "label": "Regra artificial até Novembro", + "parameters": { + "rate": "0.16" + }, + "valid_from": "2026-01-01", + "valid_until": "2026-11-30" + } + }, + { + "fixture_id": "QA2", + "definition": { + "family": "qa", + "operation": "percentage_add", + "currency": "MZN", + "unit": "money", + "precision": 2, + "rounding": "DOWN", + "priority": 10, + "source_reference": "QA-FISCAL-COMMON-20261110", + "source_status": "qa_checked", + "conditions": "Somente ensaio aritmético artificial; não enquadra contribuintes ou documentos.", + "expression": { + "language": "investmoz.tax.v1", + "nodes": { + "base": { + "op": "input", + "key": "base" + }, + "rate": { + "op": "parameter", + "key": "rate" + }, + "raw": { + "op": "multiply", + "args": [ + "base", + "rate" + ] + } + }, + "result": "raw" + }, + "label": "Regra artificial desde Dezembro", + "parameters": { + "rate": "0.17" + }, + "valid_from": "2026-12-01", + "valid_until": null + } + } + ] +} diff --git a/resources/js/AccountingMenuWorkspace.jsx b/resources/js/AccountingMenuWorkspace.jsx index 3fde2f8..dae945f 100644 --- a/resources/js/AccountingMenuWorkspace.jsx +++ b/resources/js/AccountingMenuWorkspace.jsx @@ -1,12 +1,15 @@ import React,{useEffect,useState} from 'react'; import {accountingSections,visibleAccountingSections,accountingFunctions,accountingReports,filterAccountingReports} from './accounting-menu.js'; -export default function AccountingMenuWorkspace({section,context,can,navigate}) { +const ComposedAccountingWorkspace=React.lazy(()=>import('./ComposedAccountingWorkspace.jsx')); + +export default function AccountingMenuWorkspace({section,context,can,navigate,api}) { const [search,setSearch]=useState(''),[family,setFamily]=useState(''),[selected,setSelected]=useState(null); useEffect(()=>{setSearch('');setFamily('');setSelected(null);},[context.token]); if(!visibleAccountingSections(can).length)return

Contabilidade

Sem contexto autorizado para esta consulta.

; const active=accountingSections.find(s=>s.id===section);if(!active)return null; const unit=context.unit?.id??null; + if(context.accounting?.single_book&&unit!==null&&can('general_ledger.view',unit))return A abrir Contabilidade…

}>
; const action=f=>f.target&&(!f.unit||unit!==null)&&can(f.permission,f.unit?unit:null)?:null; const functions=accountingFunctions.filter(f=>f.section===section); const report=accountingReports.find(r=>r.id===selected); diff --git a/resources/js/BillingMenuWorkspace.jsx b/resources/js/BillingMenuWorkspace.jsx index a24ba4a..e428fc8 100644 --- a/resources/js/BillingMenuWorkspace.jsx +++ b/resources/js/BillingMenuWorkspace.jsx @@ -2,6 +2,7 @@ import React,{useEffect,useState} from 'react'; import {billingSections,visibleBillingSections,filterBillingRows,loadBillingRows} from './billing-menu.js'; const emptyFilters={search:'',type:'',currency:'',series:'',state:'',author:'',from:'',to:''}; +const BillingRegisterWorkspace=React.lazy(()=>import('./BillingRegisterWorkspace.jsx')); const value=v=>v===null||v===undefined||v===''?'Por determinar':String(v); export function BillingDocumentList({rows,filters,onFilter,onClear,onOpen}) { @@ -25,13 +26,16 @@ export default function BillingMenuWorkspace({section,context,can,api,navigate}) useEffect(()=>{setSelected(null);},[section]); useEffect(()=>{ let current=true;setResult(null);setSelected(null); - if(unit===null||active?.pending)return ()=>{current=false;}; + if(unit===null||active?.pending||(context.service_documents_enabled&&['billing_all','service_invoices'].includes(section)))return ()=>{current=false;}; setLoading(true); loadBillingRows(api,can,unit).then(r=>{if(current){setResult(r);setLoading(false);}}); return ()=>{current=false;}; },[context.token,section,reload]); if(!active)return null; + if(context.service_documents_enabled&&['billing_all','service_invoices'].includes(section))return A abrir documentos…

}>
; const newDraft=()=>navigate('billing_drafts',null,true); + const additional={billing_receipts:'receipt',billing_credit_notes:'credit_note',billing_invoice_receipts:'invoice_receipt'}[section]; + if(additional&&context.service_documents_enabled&&unit&&can('service_documents.view',unit))return

COMERCIAL

{active.label}

Preparação, revisão distinta, prévia e emissão individual de teste. Os recibos usam afectações efectivas; o crédito exige decisão financeira própria.

Abrir {active.label.toLowerCase()}

Sem validade fiscal e sem movimento de dinheiro real.

; return

FACTURAÇÃO

{active.label}

Documentos comerciais no contexto de {context.company.name}{context.unit?` / ${context.unit.name}`:''}.

Demonstração — sem validade fiscal. A entrada e a consulta não habilitam emissão real. Documento, dívida e recebimento são factos distintos.

diff --git a/resources/js/BillingRegisterWorkspace.jsx b/resources/js/BillingRegisterWorkspace.jsx new file mode 100644 index 0000000..8f7306d --- /dev/null +++ b/resources/js/BillingRegisterWorkspace.jsx @@ -0,0 +1,14 @@ +import React,{useEffect,useState} from 'react'; +const labels={invoice:'Factura de teste',invoice_receipt:'Factura-recibo',receipt:'Recibo',credit_note:'Nota de crédito',quote:'Orçamento',draft:'Rascunho'}; +const states={issued:'Emitido em teste',cancelled:'Anulado tecnicamente',draft:'Rascunho',approved:'Aprovado',rejected:'Recusado',refused:'Recusado',customer_accepted:'Aceite pelo cliente',customer_declined:'Recusado pelo cliente',converted:'Convertido',confirmed_internal:'Confirmado internamente',superseded:'Histórico / substituído'}; +export default function BillingRegisterWorkspace({api,context,navigate,section}){ + const initial={kind:section==='service_invoices'?'invoice':'',search:'',currency:'',series:'',author:'',state:'',from:'',to:'',per_page:'25'}; + const [form,setForm]=useState(initial),[query,setQuery]=useState({...initial,page:1}),[result,setResult]=useState(null),[error,setError]=useState(''),[loading,setLoading]=useState(false); + useEffect(()=>{setForm(initial);setQuery({...initial,page:1});setResult(null);},[context.token,section]); + useEffect(()=>{let current=true;setLoading(true);setError('');setResult(null);const p=new URLSearchParams(Object.entries(query).filter(([,v])=>v!==''));api('/api/service-documents/register?'+p).then(v=>{if(current)setResult(v);}).catch(e=>{if(current)setError(e.message);}).finally(()=>{if(current)setLoading(false);});return()=>{current=false;};},[context.token,query]); + const input=(key,value)=>setForm(v=>({...v,[key]:value}));const rows=result?.documents?.data??[],p=result?.documents; + return

COMERCIAL

{section==='service_invoices'?'Facturas':'Todos os documentos'}

Consulta unificada por empresa, unidade e módulo. Factura-recibo representa a sua factura de origem uma única vez nesta lista. Rascunhos e orçamentos não são facturas.

+
{e.preventDefault();setQuery({...form,page:1});}}>
+ {error&&

{error}

}{loading&&

A consultar…

}{p&&<>

{p.total} registo(s); página {p.current_page} de {p.last_page}.

{rows.map(r=>)}
Referência / tipoClienteEmissãoMontante documentalEstadoSaldo da factura de origemConsultar
{r.reference}{labels[r.kind]} · versão {r.version}{r.client??'Por determinar'}{r.nuit&&NUIT {r.nuit}}{r.document_on??'Não emitido'}{r.amount!==null?`${r.amount} ${r.currency}`:r.base!==null?`Base ${r.base} ${r.currency}; imposto por conferir`:'Não determinado'}{states[r.document_state]??r.document_state}{r.balance_available?`${r.origin_invoice_balance} MZN`:r.invoice_id?'Sem consulta financeira autorizada ou sem reconhecimento':'Não aplicável'}{r.pdf_url&&PDF de teste}{r.print_url&&Orçamento desta versão}{r.kind==='draft'&&}{['receipt','credit_note','invoice_receipt'].includes(r.kind)&&Histórico e decisões}
{rows.length===0&&

Nenhum documento corresponde aos filtros autorizados.

}} +

Sem validade fiscal. Os montantes de facturas, recibos, créditos e bases não são somados. O saldo mostrado é o da factura de origem, podendo repetir-se em documentos ligados; não representa dinheiro recebido nem saldo próprio do recibo. Datas de emissão excluem rascunhos e orçamentos não emitidos.

; +} diff --git a/resources/js/CommercialQuotesWorkspace.jsx b/resources/js/CommercialQuotesWorkspace.jsx new file mode 100644 index 0000000..7b432bc --- /dev/null +++ b/resources/js/CommercialQuotesWorkspace.jsx @@ -0,0 +1,23 @@ +import React,{useEffect,useMemo,useState}from'react';import{WorkshopStockOperation}from'./workshop-stock-operation.js'; +const qs=x=>new URLSearchParams(Object.fromEntries(Object.entries(x).filter(([,v])=>v!==''&&v!==null))).toString(); +function Options({api,view,label,value,onChange,disabled,context}){const[q,setQ]=useState({search:'',page:1,per_page:25}),[search,setSearch]=useState(''),[p,setP]=useState(null);useEffect(()=>{let active=true;api('/api/commercial-quotes/options?'+qs({view,...q})).then(v=>{if(active)setP(v.records);}).catch(()=>{if(active)setP(null);});return()=>{active=false;};},[context.token,view,q]);return
{p&&<>}
;} +const labels={draft:'Preparado — por rever',approved:'Revisto — aguarda cliente',rejected:'Revisão recusada',customer_accepted:'Aceite pelo cliente',customer_declined:'Recusado pelo cliente',converted:'Convertido em rascunho'}; +export default function CommercialQuotesWorkspace({api,context,can,navigate,actorId}){ + const[query,setQuery]=useState({page:1,per_page:25,state:'',search:''}),[search,setSearch]=useState(''),[data,setData]=useState(null),[form,setForm]=useState(null),[origin,setOrigin]=useState(null),[preview,setPreview]=useState(null),[error,setError]=useState(''),[notice,setNotice]=useState(''),[busy,setBusy]=useState(false),[history,setHistory]=useState(null),[historyKey,setHistoryKey]=useState(null);const command=useMemo(()=>new WorkshopStockOperation(api),[context.token]),has=right=>can('commercial_quotes.'+right,context.unit?.id),locked=busy||command.uncertain; + const load=async()=>setData((await api('/api/commercial-quotes?'+qs(query))).quotes);useEffect(()=>{let active=true;api('/api/commercial-quotes?'+qs(query)).then(v=>{if(active)setData(v.quotes);}).catch(e=>{if(active)setError(e.message);});return()=>{active=false;};},[query,context.token]); + function start(row=null,action='prepare'){setError('');setPreview(null);setOrigin(row);const content=row?.data;setForm({action,quote_key:row?.quote_key??crypto.randomUUID(),version:row?.version??0,reason:'',data:action==='prepare'?{client_id:content?.client_id??'',currency:'MZN',valid_until:content?.valid_until??'',source_order_id:content?.source_order_id??null,description:content?.description??'',lines:content?.lines?.map(l=>({service_id:l.service_id,quantity:l.quantity,unit_price:l.unit_price,description:l.description??''}))??[{service_id:'',quantity:'',unit_price:'',description:''}]}:action==='review'?{decision:''}:action==='customer'?{decision:'',representative:'',capacity:'',evidence:'',decided_on:''}:{}});} + const update=(key,value)=>setForm(f=>({...f,data:{...f.data,[key]:value}}));const line=(i,key,value)=>setForm(f=>({...f,data:{...f.data,lines:f.data.lines.map((l,n)=>n===i?{...l,[key]:value}:l)}})); + async function prepare(e){e.preventDefault();setBusy(true);setError('');try{const input=JSON.parse(JSON.stringify(form));if(input.action==='prepare'){input.data.client_id=Number(input.data.client_id);input.data.source_order_id=input.data.source_order_id?Number(input.data.source_order_id):null;input.data.lines=input.data.lines.map(l=>({...l,service_id:Number(l.service_id)}));}setPreview(await api('/api/commercial-quotes/preview','POST',input,undefined,crypto.randomUUID()));}catch(e){setError(e.message);}finally{setBusy(false);}} + async function confirm(retry=false){setBusy(true);setError('');try{const r=retry?await command.retry():await command.send('/api/commercial-quotes/confirm',{preview:preview.preview},crypto.randomUUID());setNotice(`Orçamento v${r.version}: ${labels[r.state]}${r.replayed?' · recibo recuperado':''}.`);setForm(null);setPreview(null);await load();}catch(e){setError(e.message);if(!command.uncertain)setPreview(null);}finally{setBusy(false);}} + async function viewHistory(key,page=1){try{const result=await api('/api/commercial-quotes/'+key+'/history?page='+page);setHistory(result.events);setHistoryKey(key);}catch(e){setError(e.message);}} + return

Orçamentos comerciais e de reparação

Proposta própria, revisão distinta e decisão documentada do cliente. Os valores são base antes de impostos: a conferência fiscal ocorre no seu percurso. Aceitar não emite factura, autoriza trabalho ou cria dívida/stock.

{error&&

{error}

}{notice&&

{notice}

}
{e.preventDefault();setQuery({...query,search,page:1});}}>
{has('prepare')&&} + {data&&<>
{data.data.map(row=>)}
Referência/clienteBase propostaValidade/estadoAcções
ORC-QA-{row.quote_key.slice(0,8).toUpperCase()}{row.client_name} · v{row.version}{row.data.base} MZN{row.data.valid_until}{row.expired?'Caducado · ':''}{labels[row.state]}Versão para impressão{!row.conversion&&has('prepare')&&(!row.source_order_id||can('workshop_billing.view',context.unit?.id))&&}{row.state==='draft'&&has('review')&&(!row.source_order_id||can('workshop_billing.view',context.unit?.id))&&row.prepared_by!==actorId&&}{row.state==='approved'&&has('prepare')&&(!row.source_order_id||can('workshop_billing.view',context.unit?.id))&&!row.expired&&}{row.state==='customer_accepted'&&has('convert')&&(!row.source_order_id||can('workshop_billing.view',context.unit?.id))&&!row.expired&&}{row.conversion&&navigate&&}
{!data.data.length&&

Sem orçamentos visíveis para estes filtros.

} Página {data.current_page}/{data.last_page} } + {form&&

{form.action==='prepare'?'Preparar proposta':form.action==='review'?'Revisão distinta':form.action==='customer'?'Decisão do cliente':'Conversão explícita'}

+ {form.action==='prepare'?<>update('client_id',value)}/>
@endif +

Conferências e impacto

@foreach($data['proposals'] as $p)@php($snapshot=json_decode($p->snapshot,true))@php($review=collect($data['reviews'])->first(fn($r)=>$r->proposal_id===$p->id))@php($part=collect($data['cycle']['corrections'])->contains(fn($c)=>in_array($p->id,[$c->reverse_proposal_id,$c->replacement_proposal_id],true)))

Conferência #{{ $p->id }} · versão {{ $p->capture_version }}

{{ $p->reason }}

Data {{ $snapshot['journal_on'] }} · {{ $snapshot['period']['period_key'] }} · Preparador #{{ $p->prepared_by }}

@foreach($snapshot['lines'] as $line)@endforeach
Conta artificialDébitoCrédito
{{ $line['account_code'] }}{{ $money($line['debit_minor']) }}{{ $money($line['credit_minor']) }}

Impacto equilibrado: {{ $money($snapshot['impact']['debit_minor']) }}. Sem alterar o saldo financeiro ou executar outro movimento físico.

+@if($review)

Decisão: {{ $review->decision==='approved'?'Aprovada':'Recusada' }} por outra função · {{ $review->reason }}

@endif +@if($p->journal_id)

Aplicada no lançamento #{{ $p->journal_id }}.

@elseif(!$part&&$data['permissions']['validate']&&!$review&&$p->prepared_by!==$data['current_actor'])
+@elseif(!$part&&$data['permissions']['post']&&$review?->decision==='approved'&&$p->prepared_by===$data['current_actor'])
@endif
@endforeach
+

Lançamentos

Débitos {{ $money($data['totals']['debit_minor']) }} · Créditos {{ $money($data['totals']['credit_minor']) }}

@foreach($data['journals'] as $j)@endforeach
LançamentoDataValorFundamento
#{{ $j->id }}{{ $j->journal_on }}{{ $money($j->debit_minor) }}{{ $j->reason }}
+

Correcção por estorno e substituição

@if($data['permissions']['correct_prepare'])
@endif +@foreach($data['cycle']['corrections'] as $c)@php($reverse=json_decode($c->reverse_snapshot,true))@php($replace=json_decode($c->replacement_snapshot,true))

Correcção #{{ $c->id }} do original #{{ $c->original_journal_id }}

Estorno {{ $money($reverse['impact']['debit_minor']) }} e substituição {{ $money($replace['impact']['debit_minor']) }} · {{ $c->reason }}

@if($c->result)

Par aplicado nos lançamentos #{{ $c->result->reverse_journal_id }}/#{{ $c->result->replacement_journal_id }}.

@elseif($data['permissions']['correct_validate']&&!$c->reverse_review&&$c->prepared_by!==$data['current_actor'])
@elseif($data['permissions']['correct_post']&&$c->reverse_review?->decision==='approved'&&$c->replacement_review?->decision==='approved'&&$c->prepared_by===$data['current_actor'])
@endif
@endforeach
+

Conferência e fecho do período de ensaio

@if($data['permissions']['period_prepare'])
@endif +@foreach($data['cycle']['period_checks'] as $check)@php($snapshot=json_decode($check->snapshot,true))@php($review=collect($data['cycle']['period_reviews'])->first(fn($r)=>$r->check_id===$check->id))@php($closed=collect($data['cycle']['period_closures'])->contains(fn($r)=>$r->check_id===$check->id))

{{ $check->period_key }} · conferência #{{ $check->id }}

{{ $snapshot['facts'] }} factos · {{ $snapshot['pending'] }} pendências · Débitos {{ $money($snapshot['debit_minor']) }} / Créditos {{ $money($snapshot['credit_minor']) }}

@if($closed)

Período QA fechado; sem reabertura neste percurso.

@elseif($data['permissions']['period_validate']&&!$review&&$check->prepared_by!==$data['current_actor'])
@elseif($data['permissions']['period_close']&&$review?->decision==='approved'&&$check->prepared_by===$data['current_actor'])
@endif
@endforeach
+

Stock · taxa de valorização

@foreach($data['stock'] as $s)

Artigo #{{ $s['book']['stock_item_id'] }} · Armazém #{{ $s['book']['warehouse_id'] }} · Saldo físico {{ $s['physical_balance_milli']/1000 }} {{ $s['book']['unit_code'] }}

@if(isset($s['ownership']['owned_quantity_milli']))

Propriedade total deste artigo {{ $s['ownership']['owned_quantity_milli']/1000 }} {{ $s['book']['unit_code'] }}, incluindo {{ $s['ownership']['in_transit_milli']/1000 }} em trânsito. Transferências internas não são consumo.

@else

{{ $s['ownership']['ownership_pending']??'Propriedade por conferir' }}

@endif @if($data['permissions']['prepare'])
@endif @foreach($s['rates'] as $rate)

Taxa v{{ $rate->version }} · {{ $money($rate->unit_minor) }}/UN · {{ $rate->review?->decision??'Por rever' }}

@if($data['permissions']['validate']&&!$rate->review&&$rate->prepared_by!==$data['current_actor'])
@endif @endforeach
@endforeach
+

Plano de contas de teste

Contas artificiais do mesmo diário e desta empresa/unidade. A classificação comercial tem proposta e revisão próprias; consultar uma conta não cria um lançamento.

@foreach($data['chart_accounts'] as $account)@endforeach
CódigoDesignaçãoMoedaConsulta
{{ $account->code }}{{ $account->label }}{{ $account->currency }}Abrir razão desta conta
@php($p=$data['chart_pagination'])
+ diff --git a/resources/views/commercial/documents.blade.php b/resources/views/commercial/documents.blade.php new file mode 100644 index 0000000..502494a --- /dev/null +++ b/resources/views/commercial/documents.blade.php @@ -0,0 +1,4 @@ +Comercial · documentos de teste +← Voltar ao SGF · Escolher função

Comercial · documentos de teste

TESTE — SEM VALIDADE FISCAL. Sem dinheiro real. Preparação, revisão e emissão são decisões individuais; a entrada não executa operações.

+
ProcessoVersão / estadoAutor e dataConsultar

+

Decisão individual

diff --git a/resources/views/commercial/quote-print.blade.php b/resources/views/commercial/quote-print.blade.php new file mode 100644 index 0000000..dca4c8e --- /dev/null +++ b/resources/views/commercial/quote-print.blade.php @@ -0,0 +1 @@ +Orçamento QA · v{{ $quote->version }}Voltar ao SGF

Orçamento de teste

SEM VALIDADE FISCAL · {{ $quote->state==='draft'?'Rascunho — aguarda revisão':$quote->state }}. Não é factura, recibo, autorização de trabalho ou dívida.

Cliente: {{ $quote->dependencies['client']['name'] }}

{{ $quote->data['description'] }}

Validade: {{ $quote->data['valid_until'] }} · Versão {{ $quote->version }}

@if($quote->dependencies['origin'])

Ordem de origem: {{ $quote->dependencies['origin']['order']['reference'] }}

@endif@foreach($quote->data['lines'] as $line)@endforeach
Serviço/descriçãoQuantidadePreço unitário proposto
{{ $line['service_name'] }}
{{ $line['description']??'' }}
{{ $line['quantity'] }}{{ $line['unit_price'] }} MZN

Base proposta: {{ $quote->data['base'] }} MZN

Impostos sujeitos a conferência separada. Não se presumiu imposto zero nem este valor como total fiscal.

@if(isset($quote->data['customer']))

Decisão documentada

{{ $quote->data['customer']['decision'] }} · {{ $quote->data['customer']['representative'] }} · {{ $quote->data['customer']['capacity'] }} · {{ $quote->data['customer']['decided_on'] }}

Evidência: {{ $quote->data['customer']['evidence'] }}

@endif

Referência integral: {{ $quote->quote_key }} · Autor do evento {{ $quote->actor_id }} · {{ $quote->created_at }}

diff --git a/resources/views/finance/bank.blade.php b/resources/views/finance/bank.blade.php new file mode 100644 index 0000000..860676c --- /dev/null +++ b/resources/views/finance/bank.blade.php @@ -0,0 +1,4 @@ +SGF · Conferência de extracto QA

Conferência de extracto QA

Extracto artificial interno, sem banco ou dinheiro real. Comparação limitada a duas linhas de liquidação existentes. Não altera a classificação da conta, cria pagamentos ou reabre Caixa/período.

+

Extracto e correspondências

Referência/dataDébito MZNCrédito MZNEstado/correspondência

Histórico

Versão/autorEstado/dataFundamentoConferência

+

Decisão individual

+

Recuperar referência

diff --git a/resources/views/finance/cash.blade.php b/resources/views/finance/cash.blade.php new file mode 100644 index 0000000..c3e5084 --- /dev/null +++ b/resources/views/finance/cash.blade.php @@ -0,0 +1,5 @@ +Financeiro · Caixa QA +← Voltar ao SGF · Escolher função

Financeiro · Caixa QA

SIMULAÇÃO — SEM DINHEIRO REAL. Classificação de custódia explícita; uma factura ou recibo, por si só, não comprova numerário entregue. Não há ajuste automático nem reabertura.

+

Caixa e origem actual

A consultar…
+

Decisão individual

As origens confirmadas exigem preparação, revisão e aplicação individuais na Contabilidade. O fecho de Caixa recusa origens por valorizar; não lança automaticamente.

+

Extracto e histórico

Versão / dataTipo / acçãoEstadoValor/contagemAutor / fundamentoDecisão

O resumo do intervalo inclui todas as entradas e saídas confirmadas. O filtro por tipo limita o histórico; não altera o saldo da Caixa. A data é a do facto declarado; os instantes do histórico conservam a sua zona.

diff --git a/resources/views/finance/composed-payables.blade.php b/resources/views/finance/composed-payables.blade.php new file mode 100644 index 0000000..fc81cdd --- /dev/null +++ b/resources/views/finance/composed-payables.blade.php @@ -0,0 +1,16 @@ +Financeiro · fornecedores · SGF
+@php($money=fn($n)=>$n===null?'Indeterminado':number_format($n/100,2,',',' ').' MZN') +@php($prepare=$data['profile']==='finance_prepare') @php($review=$data['profile']==='finance_review') +Voltar ao SGF · Trocar função QA

Financeiro · obrigações a fornecedores

Ensaio sintético, sem dinheiro real. A preparação e a confirmação são distintas. Confirmar um pagamento não reduz a dívida; a afectação confirmada reduz o saldo. A Contabilidade exige o seu próprio percurso.

+

+

Obrigações originais

@foreach($data['obligations'] as $o)@endforeach
DocumentoPrincipalSaldoVencimento
{{ $o['document_number'] }} · #{{ $o['id'] }}{{ $money($o['principal_minor']) }}{{ $money($o['balance_minor']) }}{{ $o['due_on']??'Desconhecido; atraso indeterminado' }}
+@if($prepare&&count($data['obligations']))

Preparar facto de pagamento

@endif +

Pagamentos

@foreach($data['money']['payments'] as $p)

{{ $p['fact_reference'] }} · {{ $money($p['amount_minor']) }}

Estado: {{ $p['state'] }} · {{ $p['reason'] }}

@if($review&&!$p['decision'])
@endif
@endforeach +@php($paymentPage=$data['money']['pagination']['payments'])
+@if($prepare)

Propor afectação

@endif +

Conferência de afectações

@foreach($data['money']['proposals'] as $p)

{{ $p['reference'] }}

@foreach(json_decode($p['plan'],true) as $line)

Obrigação #{{ $line['obligation_id'] }} · {{ $money($line['amount_minor']) }}

@endforeach

{{ $p['reason'] }} · {{ $p['decision']?->decision??'Por conferir' }}

@if($review&&!$p['decision'])
@endif
@endforeach
+

Afectações confirmadas e reversões

@foreach($data['money']['allocations'] as $a)@php($reversed=collect($data['money']['reversals'])->contains(fn($r)=>$r->allocation_id===$a->id))@php($proposal=collect($data['money']['reversal_proposals'])->first(fn($r)=>$r->allocation_id===$a->id))

Afectação #{{ $a->id }} · Obrigação #{{ $a->obligation_id }} · {{ $money($a->amount_minor) }} · {{ $reversed?'Revertida':'Confirmada' }}

@if($prepare&&!$reversed&&!$proposal)@php($ob=collect($data['obligations'])->first(fn($o)=>$o['id']===$a->obligation_id))@if($ob)
@endif @endif @if($review&&$proposal&&!$reversed)

Proposta #{{ $proposal->id }} · {{ $proposal->reason }}

@endif
@endforeach
+

Pesquisar e percorrer os registos

+@foreach(['page'=>['Obrigações',$data['pagination']],'payments_page'=>['Pagamentos',$data['money']['pagination']['payments']],'allocation_proposals_page'=>['Propostas de afectação',$data['money']['pagination']['allocation_proposals']],'allocations_page'=>['Afectações',$data['money']['pagination']['allocations']],'payment_events_page'=>['Histórico',$data['money']['pagination']['payment_events']]] as $parameter=>[$label,$p])@endforeach +

Histórico financeiro

@foreach($data['money']['events'] as $event)

Evento #{{ $event->id }} · {{ $event->created_at }}

{{ $event->kind??$event->action??'Registo auditado' }}

@endforeach
+
diff --git a/resources/views/finance/indicators.blade.php b/resources/views/finance/indicators.blade.php new file mode 100644 index 0000000..44dcc94 --- /dev/null +++ b/resources/views/finance/indicators.blade.php @@ -0,0 +1,4 @@ +SGF · Indicadores QA

Critérios e indicadores financeiros QA

Hipóteses limitadas do exercício — não são rácios gerais de liquidez/margem ou recomendação de pagamento. Escolha e revisão de fontes não atribuem critérios automaticamente a outras contas/empresas. Sem série anterior validada, a tendência fica indeterminada.

+

Fontes conciliadas

Catálogo e histórico

RegistoVersão/estadoRetrato/resultadoConferência

Nenhuma versão escolhida.

+

Operação individual

+

Recuperar referência

diff --git a/resources/views/fiscal/rules.blade.php b/resources/views/fiscal/rules.blade.php new file mode 100644 index 0000000..392a486 --- /dev/null +++ b/resources/views/fiscal/rules.blade.php @@ -0,0 +1,5 @@ +SGF · Regras fiscais QA +

Regras fiscais versionadas QA

Ensaio artificial, sem activação legal. As taxas16% e17% deste contrato não representam uma mudança legislativa. Referências não conferidas ficam pendentes. Os cálculos por linhas não emitem factura, dívida, pagamento ou lançamento. Nenhuma regra é atribuída automaticamente a serviços, contribuintes ou documentos.

+

Catálogo e histórico

RegistoVersão/estadoFonte/vigência ou resultadoConferência

Versão escolhida

Nenhuma versão escolhida.

+

Operação individual

+

Recuperar referência

diff --git a/resources/views/fiscal/workspace.blade.php b/resources/views/fiscal/workspace.blade.php new file mode 100644 index 0000000..de9e245 --- /dev/null +++ b/resources/views/fiscal/workspace.blade.php @@ -0,0 +1,7 @@ +SGF · Livro e mapa interno de IVA QA +

Livro e mapa interno de IVA QA

Ensaio sintético — sem validade fiscal. Apenas a origem escolhida; não é apuramento integral da empresa ou declaração oficial. Dedução e imposto a pagar ficam indeterminados.

+

Estado actual

+

Decisão individual

+

Recuperar referência

+

Histórico e fundamentos

Versão e autorEstado/dataFundamentoConferência

+ diff --git a/resources/views/sgf-presentation-entry.blade.php b/resources/views/sgf-presentation-entry.blade.php new file mode 100644 index 0000000..f36b4f8 --- /dev/null +++ b/resources/views/sgf-presentation-entry.blade.php @@ -0,0 +1,21 @@ + +SGF Tempo — módulos e funções + +
+
TEMPO · SGF

Escolha o módulo

Abra o dropdown do módulo e escolha Consultar ou a função com que pretende trabalhar. Cada entrada mantém a sua identidade e permissões.

+ +
Apresentação local com dados fictícios

Comercial, Compras, Stock, Oficinas, Financeiro e Contabilidade@if(isset($tour['human_resources'])) e Recursos Humanos@endif usam a mesma instalação. Consultar não repete operações. Preparação e revisão exigem funções distintas; nenhuma entrada concede direitos novos.

+

O que pode acompanhar em cada módulo

Comece pela consulta. Dentro da função escolhida, use Início para ver o percurso, consultar os registos existentes e abrir documentos e histórico. As listas respeitam as permissões da função e a empresa/unidade seleccionadas.

@foreach($tour as $module=>$route)

{{ $route['label'] }}

{{ $route['description'] }}.

@csrf
@endforeach
+

Documento, movimento físico, dívida e lançamento têm evidências distintas. Preparar não equivale a aprovar ou pagar; cada continuação mantém a origem e exige a função responsável.

+

Entrada anterior de Compras

+
Todos os perfis e módulos autorizados
@foreach($profiles as $key=>$profile)

{{ $profile['label'] }}

@csrf
@endforeach
+ +
diff --git a/resources/views/stock-catalogue-workspace.blade.php b/resources/views/stock-catalogue-workspace.blade.php new file mode 100644 index 0000000..52f47f8 --- /dev/null +++ b/resources/views/stock-catalogue-workspace.blade.php @@ -0,0 +1,6 @@ +Stock · Artigos e revisão +
← Voltar ao SGF · Escolher função

Artigos de Stock

Consultar o catálogo, preparar um artigo e submetê-lo a revisão distinta. Só a aprovação disponibiliza o novo artigo; não cria saldo inicial, movimento, lote ou custo.

+

Catálogo e processos

Código / designaçãoUnidadeOrigem / estadoVersãoConsultar
+

Ficha e decisão

Escolha explicitamente um artigo/processo ou prepare um novo.

+

Recuperar uma decisão

Recuperar um recibo histórico não repete a operação. Código e unidade de artigos já criados são imutáveis; apenas a designação de artigos deste percurso pode ser corrigida por nova revisão. Artigos anteriores permanecem só para consulta.

+

Instalação QA com dados fictícios. Nenhuma compra, dívida, pagamento ou lançamento é gerado por este cadastro.

diff --git a/resources/views/stock-completion-workspace.blade.php b/resources/views/stock-completion-workspace.blade.php new file mode 100644 index 0000000..2f090bf --- /dev/null +++ b/resources/views/stock-completion-workspace.blade.php @@ -0,0 +1,6 @@ +Stock · políticas e devoluções +
← Voltar ao SGF · Escolher função

Stock · políticas e devoluções ao fornecedor

Recorte QA autorizado. Política, aprovação e execução são decisões distintas. A devolução física não cria nota de crédito nem altera a dívida ao fornecedor.

+

Origem e saldo actual

A consultar…
+

Consultar processos

ProcessoVersãoEstadoConferência

+

Decisão individual

Escolha um processo ou prepare um novo.

+ diff --git a/resources/views/stock-final-workspace.blade.php b/resources/views/stock-final-workspace.blade.php new file mode 100644 index 0000000..091cd80 --- /dev/null +++ b/resources/views/stock-final-workspace.blade.php @@ -0,0 +1,8 @@ +Stock · trânsito e inventário +
← Mudar função ou módulo · Voltar ao SGF

Stock · transferências e inventário

Operações sintéticas no mesmo livro físico. A contagem não aplica diferenças automaticamente. A transferência mantém propriedade e só disponibiliza no destino a quantidade efectivamente recebida.

+

Estado actual

A consultar…
+

Decisão individual

+

Transferências

ReferênciaEnviadoPor receber/devolver

+

Lotes e conferências

Uma retenção ou alteração de identificação invalida a conferência anterior. A libertação e o ajuste exigem outra pessoa. O histórico conserva a identificação original e cada decisão posterior.

+

Histórico auditado

+ diff --git a/resources/views/stock-operations-workspace.blade.php b/resources/views/stock-operations-workspace.blade.php new file mode 100644 index 0000000..147bcf6 --- /dev/null +++ b/resources/views/stock-operations-workspace.blade.php @@ -0,0 +1,17 @@ + +Operações de Stock · SGF + +
+Voltar ao SGF

Operações de Stock

Saída para utilização, transferência ou inventário físico nos armazéns autorizados. A transferência regista saída e entrada na mesma confirmação; não representa trânsito. A contagem observa a quantidade; aplicar a diferença exige uma confirmação própria. Artigos por lotes mantêm o percurso específico de contagem por lote.

+

+

Preparar operação

+ + +
+ + + +
+ +

Histórico

+
diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..128f8fa --- /dev/null +++ b/routes/api.php @@ -0,0 +1,37 @@ +group(function () { + + // Receber fatura do SGC + Route::post('/invoices', [SgcIntegrationController::class, 'storeInvoice']) + ->name('integration.invoices.store'); + + // Receber confirmação de pagamento + Route::post('/payments', [SgcIntegrationController::class, 'storePayment']) + ->name('integration.payments.store'); + + // Consultar estado de fatura + Route::get('/invoices/{sgfDocNumber}/status', [SgcIntegrationController::class, 'getInvoiceStatus']) + ->name('integration.invoices.status'); + + // Estatísticas de integração + Route::get('/stats', [SgcIntegrationController::class, 'getStats']) + ->name('integration.stats'); +}); + +// Analytics (relatórios de gastos) +Route::prefix('v1/analytics')->group(function () { + Route::get('/spend/monthly', [AnalyticsController::class, 'getMonthlySpend']); + Route::get('/spend/by-supplier', [AnalyticsController::class, 'getSpendBySupplier']); + Route::get('/spend/by-category', [AnalyticsController::class, 'getSpendByCategory']); +}); \ No newline at end of file diff --git a/routes/sgc.php b/routes/sgc.php new file mode 100644 index 0000000..1d17d12 --- /dev/null +++ b/routes/sgc.php @@ -0,0 +1,11 @@ +middleware(AuthenticateSgcInvoice::class)->name('integration.sgc.invoices'); +Route::get('v1/integration/sgc/invoices/orders/{order}', [SgcInvoiceIngressController::class, 'context']) + ->whereNumber('order')->middleware(AuthenticateSgcInvoice::class)->name('integration.sgc.invoices.context'); diff --git a/routes/web.php b/routes/web.php index 128ee1e..9601682 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,7 +9,10 @@ if (app()->environment('browser-testing')) { require base_path('scripts/product-demo-entry.php'); } -Route::get('/', function () { +Route::get('/sgf',[\App\Shared\Installation\PresentationEntry::class,'screen']); +Route::post('/sgf/entrar',[\App\Shared\Installation\PresentationEntry::class,'enter']); +Route::get('/', function (\Illuminate\Http\Request $r) { + if(config('sgf_single_installation.enabled')===true&&!$r->user())return redirect('/sgf'); return view('app'); }); @@ -27,6 +30,83 @@ Route::middleware(['auth', RequireValidSession::class])->group(function (): void Route::post('/api/logout', [IdentityController::class, 'logout']); Route::post('/api/context', [IdentityController::class, 'select']); Route::middleware(RequireCompanyContext::class)->group(function (): void { + Route::get('/finance/indicators',[\App\Modules\Finance\Http\FinancialIndicatorsController::class,'screen']); + Route::get('/api/financial-indicators',[\App\Modules\Finance\Http\FinancialIndicatorsController::class,'index']); + Route::get('/api/financial-indicators/records/{id}',[\App\Modules\Finance\Http\FinancialIndicatorsController::class,'detail'])->whereNumber('id'); + Route::get('/api/financial-indicators/results/{id}/report',[\App\Modules\Finance\Http\FinancialIndicatorsController::class,'report'])->whereNumber('id'); + Route::get('/api/financial-indicators/receipts/{key}',[\App\Modules\Finance\Http\FinancialIndicatorsController::class,'receipt']); + Route::post('/api/financial-indicators/preview',[\App\Modules\Finance\Http\FinancialIndicatorsController::class,'preview']); + Route::post('/api/financial-indicators/confirm',[\App\Modules\Finance\Http\FinancialIndicatorsController::class,'confirm']); + Route::get('/finance/bank',[\App\Modules\Finance\Http\BankReconciliationController::class,'screen']); + Route::get('/api/bank-reconciliation',[\App\Modules\Finance\Http\BankReconciliationController::class,'index']); + Route::get('/api/bank-reconciliation/report',[\App\Modules\Finance\Http\BankReconciliationController::class,'report']); + Route::get('/api/bank-reconciliation/receipts/{key}',[\App\Modules\Finance\Http\BankReconciliationController::class,'receipt']); + Route::post('/api/bank-reconciliation/preview',[\App\Modules\Finance\Http\BankReconciliationController::class,'preview']); + Route::post('/api/bank-reconciliation/confirm',[\App\Modules\Finance\Http\BankReconciliationController::class,'confirm']); + Route::get('/fiscal/rules',[\App\Modules\Services\Http\FiscalRulesController::class,'screen']); + Route::get('/api/fiscal-rules',[\App\Modules\Services\Http\FiscalRulesController::class,'index']); + Route::get('/api/fiscal-rules/documents/{id}/report',[\App\Modules\Services\Http\FiscalRulesController::class,'documentReport'])->whereNumber('id'); + Route::get('/api/fiscal-rules/records/{id}',[\App\Modules\Services\Http\FiscalRulesController::class,'detail'])->whereNumber('id'); + Route::get('/api/fiscal-rules/receipts/{key}',[\App\Modules\Services\Http\FiscalRulesController::class,'receipt']); + Route::post('/api/fiscal-rules/preview',[\App\Modules\Services\Http\FiscalRulesController::class,'preview']); + Route::post('/api/fiscal-rules/confirm',[\App\Modules\Services\Http\FiscalRulesController::class,'confirm']); + Route::get('/fiscal/iva',[\App\Modules\Services\Http\FiscalAssessmentController::class,'screen']); + Route::get('/api/hr-personnel',[\App\Modules\HumanResources\Http\PersonnelController::class,'index']); + Route::post('/api/hr-personnel/preview',[\App\Modules\HumanResources\Http\PersonnelController::class,'preview']); + Route::post('/api/hr-personnel/confirm',[\App\Modules\HumanResources\Http\PersonnelController::class,'confirm']); + Route::get('/api/hr-personnel/receipts/{key}',[\App\Modules\HumanResources\Http\PersonnelController::class,'receipt']); + Route::get('/api/hr-personnel/exports/{id}',[\App\Modules\HumanResources\Http\PersonnelController::class,'export'])->whereNumber('id'); + Route::get('/api/remunerations',[\App\Modules\HumanResources\Http\RemunerationsController::class,'index']); + Route::post('/api/remunerations/preview',[\App\Modules\HumanResources\Http\RemunerationsController::class,'preview']); + Route::post('/api/remunerations/confirm',[\App\Modules\HumanResources\Http\RemunerationsController::class,'confirm']); + Route::get('/api/remunerations/receipts/{key}',[\App\Modules\HumanResources\Http\RemunerationsController::class,'receipt']); + Route::get('/api/remunerations/exports/{id}',[\App\Modules\HumanResources\Http\RemunerationsController::class,'export'])->whereNumber('id'); + Route::get('/api/fiscal-obligations',[\App\Modules\Services\Http\FiscalObligationsController::class,'index']); + Route::post('/api/fiscal-obligations/preview',[\App\Modules\Services\Http\FiscalObligationsController::class,'preview']); + Route::post('/api/fiscal-obligations/confirm',[\App\Modules\Services\Http\FiscalObligationsController::class,'confirm']); + Route::get('/api/fiscal-obligations/receipts/{key}',[\App\Modules\Services\Http\FiscalObligationsController::class,'receipt']); + Route::get('/api/fiscal-assessments',[\App\Modules\Services\Http\FiscalAssessmentController::class,'index']); + Route::get('/api/fiscal-assessments/receipts/{key}',[\App\Modules\Services\Http\FiscalAssessmentController::class,'receipt']); + Route::get('/api/fiscal-assessments/forms/{id}/export',[\App\Modules\Services\Http\FiscalAssessmentController::class,'export'])->whereNumber('id'); + Route::post('/api/fiscal-assessments/preview',[\App\Modules\Services\Http\FiscalAssessmentController::class,'preview']); + Route::post('/api/fiscal-assessments/confirm',[\App\Modules\Services\Http\FiscalAssessmentController::class,'confirm']); + Route::get('/finance/cash',[\App\Modules\Finance\Http\CashController::class,'screen']); + Route::get('/api/cash',[\App\Modules\Finance\Http\CashController::class,'index']); + Route::get('/api/cash/receipts/{key}',[\App\Modules\Finance\Http\CashController::class,'receipt']); + Route::post('/api/cash/preview',[\App\Modules\Finance\Http\CashController::class,'preview']); + Route::post('/api/cash/confirm',[\App\Modules\Finance\Http\CashController::class,'confirm']); + Route::get('/commercial/documents',[\App\Modules\Services\Http\ServiceDocumentsController::class,'screen']); + Route::get('/api/service-documents',[\App\Modules\Services\Http\ServiceDocumentsController::class,'index']); + Route::get('/api/service-documents/register',[\App\Modules\Services\Http\ServiceDocumentsController::class,'register']); + Route::get('/api/service-documents/options',[\App\Modules\Services\Http\ServiceDocumentsController::class,'options']); + Route::get('/api/service-documents/{id}/pdf',[\App\Modules\Services\Http\ServiceDocumentsController::class,'pdf'])->whereNumber('id'); + Route::post('/api/service-documents/preview',[\App\Modules\Services\Http\ServiceDocumentsController::class,'preview']); + Route::post('/api/service-documents/confirm',[\App\Modules\Services\Http\ServiceDocumentsController::class,'confirm']); + Route::post('/api/general-ledger/stock/{action}',[\App\Modules\Accounting\ComposedLedgerController::class,'stockCommand'])->where('action','rate-prepare|rate-review'); + Route::get('/finance/payables',[\App\Modules\Purchasing\ComposedFinanceController::class,'index']); + Route::get('/api/finance/payable-positions',[\App\Modules\Purchasing\PayablePositionReadController::class,'index']); + Route::get('/api/finance/payable-due',[\App\Modules\Purchasing\PayableDueController::class,'index']); + Route::get('/api/finance/payable-due/receipts/{key}',[\App\Modules\Purchasing\PayableDueController::class,'receipt']); + Route::post('/api/finance/payable-due/preview',[\App\Modules\Purchasing\PayableDueController::class,'preview']); + Route::post('/api/finance/payable-due/confirm',[\App\Modules\Purchasing\PayableDueController::class,'confirm']); + Route::get('/api/finance/payables',[\App\Modules\Purchasing\ComposedFinanceController::class,'index']); + Route::post('/api/finance/payables/{action}',[\App\Modules\Purchasing\ComposedFinanceController::class,'command'])->where('action','payment-prepare|payment-decide|allocation-prepare|allocation-decide|reversal-prepare|reversal-confirm'); + Route::get('/stock/operations',[\App\Modules\Inventory\StockOperationsController::class,'screen']); + Route::get('/stock/final',[\App\Modules\Inventory\StockFinalController::class,'screen']); + Route::get('/stock/completion',[\App\Modules\Inventory\StockCompletionController::class,'screen']); + Route::get('/api/stock-completion',[\App\Modules\Inventory\StockCompletionController::class,'index']); + Route::get('/api/stock-completion/receipts/{key}',[\App\Modules\Inventory\StockCompletionController::class,'receipt']); + Route::post('/api/stock-completion/preview',[\App\Modules\Inventory\StockCompletionController::class,'preview']); + Route::post('/api/stock-completion/confirm',[\App\Modules\Inventory\StockCompletionController::class,'confirm']); + Route::get('/api/stock-final',[\App\Modules\Inventory\StockFinalController::class,'index']); + Route::get('/api/stock-final/history',[\App\Modules\Inventory\StockFinalController::class,'history']); + Route::get('/api/stock-final/receipts/{key}',[\App\Modules\Inventory\StockFinalController::class,'receipt']); + Route::post('/api/stock-final/preview',[\App\Modules\Inventory\StockFinalController::class,'preview']); + Route::post('/api/stock-final/confirm',[\App\Modules\Inventory\StockFinalController::class,'confirm']); + Route::get('/api/stock-operations',[\App\Modules\Inventory\StockOperationsController::class,'index']); + Route::get('/api/stock-operations/basis',[\App\Modules\Inventory\StockOperationsController::class,'basis']); + Route::post('/api/stock-operations/preview',[\App\Modules\Inventory\StockOperationsController::class,'preview']); + Route::post('/api/stock-operations/confirm',[\App\Modules\Inventory\StockOperationsController::class,'confirm']); Route::get('/geography/segments/{kind}',[\App\Shared\Geography\GeographyController::class,'segmentScreen']); Route::get('/api/geography/segments/{kind}',[\App\Shared\Geography\GeographyController::class,'segments']); Route::get('/geography/{kind}/{id}',[\App\Shared\Geography\GeographyController::class,'screen'])->whereNumber('id'); @@ -101,6 +181,11 @@ Route::middleware(['auth', RequireValidSession::class])->group(function (): void Route::post('/api/workshop-stock/{id}/confirm',[\App\Modules\Workshop\Http\WorkshopStockController::class,'confirm'])->whereNumber('id'); Route::post('/api/workshop-stock/{id}/withdraw',[\App\Modules\Workshop\Http\WorkshopStockController::class,'withdraw'])->whereNumber('id'); Route::post('/api/workshop-stock/{id}/reject',[\App\Modules\Workshop\Http\WorkshopStockController::class,'reject'])->whereNumber('id'); + Route::get('/stock/catalogue',[\App\Modules\Inventory\StockCatalogueController::class,'screen']); + Route::get('/api/stock-catalogue',[\App\Modules\Inventory\StockCatalogueController::class,'index']); + Route::get('/api/stock-catalogue/receipts/{key}',[\App\Modules\Inventory\StockCatalogueController::class,'receipt']); + Route::post('/api/stock-catalogue/preview',[\App\Modules\Inventory\StockCatalogueController::class,'preview']); + Route::post('/api/stock-catalogue/confirm',[\App\Modules\Inventory\StockCatalogueController::class,'confirm']); Route::get('/api/stock-read', [\App\Modules\Inventory\StockReadController::class,'index']); Route::get('/api/module-records', [\App\Modules\Identity\Http\ModuleRecordsController::class,'index']); Route::post('/api/module-records/preview', [\App\Modules\Identity\Http\ModuleRecordsController::class,'preview']); @@ -114,6 +199,11 @@ Route::middleware(['auth', RequireValidSession::class])->group(function (): void Route::get('/general-ledger/costs', [\App\Modules\Accounting\CostLedgerController::class,'index']); Route::get('/api/general-ledger/costs', [\App\Modules\Accounting\CostLedgerController::class,'index']); Route::get('/general-ledger/{kind}', [\App\Modules\Accounting\GeneralLedgerController::class,'report'])->whereIn('kind',['trial-balance','account-ledger']); + Route::get('/api/general-ledger/statements',[\App\Modules\Accounting\InternalStatementsController::class,'index']); + Route::post('/api/general-ledger/statements/preview',[\App\Modules\Accounting\InternalStatementsController::class,'preview']); + Route::post('/api/general-ledger/statements/confirm',[\App\Modules\Accounting\InternalStatementsController::class,'confirm']); + Route::get('/api/general-ledger/statements/receipts/{key}',[\App\Modules\Accounting\InternalStatementsController::class,'receipt']); + Route::get('/api/general-ledger/statements/exports/{id}',[\App\Modules\Accounting\InternalStatementsController::class,'export'])->whereNumber('id'); Route::get('/api/general-ledger/{kind}', [\App\Modules\Accounting\GeneralLedgerController::class,'report'])->whereIn('kind',['trial-balance','account-ledger']); Route::get('/general-ledger/flow', [\App\Modules\Accounting\GeneralLedgerController::class,'flow']); Route::get('/general-ledger/cycle', [\App\Modules\Accounting\GeneralLedgerController::class,'cycle']); @@ -141,7 +231,16 @@ Route::middleware(['auth', RequireValidSession::class])->group(function (): void Route::post('/api/workshop-billing/orders/{id}/proposals', [\App\Modules\Workshop\Http\WorkshopBillingController::class,'prepare'])->whereNumber('id'); Route::post('/api/workshop-billing/orders/{id}/reviews', [\App\Modules\Workshop\Http\WorkshopBillingController::class,'review'])->whereNumber('id'); Route::post('/api/workshop-billing/orders/{id}/generations', [\App\Modules\Workshop\Http\WorkshopBillingController::class,'generate'])->whereNumber('id'); + Route::get('/api/workshop/orders',[\App\Modules\Workshop\Http\WorkshopOrderReadController::class,'index']); Route::get('/api/workshop', [\App\Modules\Workshop\Http\WorkshopController::class,'index']); + Route::get('/api/workshop-schedule',[\App\Modules\Workshop\Http\WorkshopScheduleController::class,'index']); + Route::get('/api/workshop-equipment',[\App\Modules\Workshop\Http\WorkshopEquipmentController::class,'index']); + Route::post('/api/workshop-equipment/preview',[\App\Modules\Workshop\Http\WorkshopEquipmentController::class,'preview']); + Route::post('/api/workshop-equipment/confirm',[\App\Modules\Workshop\Http\WorkshopEquipmentController::class,'confirm']); + Route::get('/api/workshop-schedule/options',[\App\Modules\Workshop\Http\WorkshopScheduleController::class,'options']); + Route::get('/api/workshop-schedule/{key}/history',[\App\Modules\Workshop\Http\WorkshopScheduleController::class,'history']); + Route::post('/api/workshop-schedule/preview',[\App\Modules\Workshop\Http\WorkshopScheduleController::class,'preview']); + Route::post('/api/workshop-schedule/confirm',[\App\Modules\Workshop\Http\WorkshopScheduleController::class,'confirm']); Route::get('/api/direct-costs', [\App\Modules\Workshop\Http\DirectCostsController::class,'index']); Route::get('/api/industrial-budgets', [\App\Modules\Industry\Http\IndustrialBudgetsController::class,'index']); Route::post('/api/industrial-budgets/lots/{id}/proposals', [\App\Modules\Industry\Http\IndustrialBudgetsController::class,'propose'])->whereNumber('id'); @@ -158,11 +257,18 @@ Route::middleware(['auth', RequireValidSession::class])->group(function (): void Route::post('/api/workshop/orders', [\App\Modules\Workshop\Http\WorkshopController::class,'open']); Route::post('/api/workshop/orders/{id}/entries', [\App\Modules\Workshop\Http\WorkshopController::class,'record'])->whereNumber('id'); Route::post('/api/workshop/entries/{id}/verification', [\App\Modules\Workshop\Http\WorkshopController::class,'verify'])->whereNumber('id'); + Route::get('/api/service-receivables/positions',[\App\Modules\Services\Http\ReceivablePositionReadController::class,'index']); Route::get('/api/service-receivables', [\App\Modules\Services\Http\ServiceReceivablesController::class,'index']); Route::post('/api/service-receivables/proposals', [\App\Modules\Services\Http\ServiceReceivablesController::class,'propose']); Route::post('/api/service-receivables/{id}/decision', [\App\Modules\Services\Http\ServiceReceivablesController::class,'decide'])->whereNumber('id'); Route::post('/api/service-receivables/{id}/allocate', [\App\Modules\Services\Http\ServiceReceivablesController::class,'allocate'])->whereNumber('id'); Route::get('/api/service-invoices', [\App\Modules\Services\Http\ServiceInvoiceController::class,'index']); + Route::get('/commercial-quotes/{key}/print',[\App\Modules\Services\Http\CommercialQuotesController::class,'print']); + Route::get('/api/commercial-quotes',[\App\Modules\Services\Http\CommercialQuotesController::class,'index']); + Route::get('/api/commercial-quotes/options',[\App\Modules\Services\Http\CommercialQuotesController::class,'options']); + Route::get('/api/commercial-quotes/{key}/history',[\App\Modules\Services\Http\CommercialQuotesController::class,'history']); + Route::post('/api/commercial-quotes/preview',[\App\Modules\Services\Http\CommercialQuotesController::class,'preview']); + Route::post('/api/commercial-quotes/confirm',[\App\Modules\Services\Http\CommercialQuotesController::class,'confirm']); Route::post('/api/service-invoices/proposals', [\App\Modules\Services\Http\ServiceInvoiceController::class,'propose']); Route::post('/api/service-invoices/proposals/{id}/decision', [\App\Modules\Services\Http\ServiceInvoiceController::class,'decide'])->whereNumber('id'); Route::post('/api/service-invoices/drafts/{id}/review', [\App\Modules\Services\Http\ServiceInvoiceController::class,'review'])->whereNumber('id'); diff --git a/scripts/abrir-sgf.ps1 b/scripts/abrir-sgf.ps1 new file mode 100644 index 0000000..6a092d6 --- /dev/null +++ b/scripts/abrir-sgf.ps1 @@ -0,0 +1,7 @@ +$ErrorActionPreference = 'Stop' +Push-Location -LiteralPath 'C:\laragon\www\sgf-tempo' +try { + & 'C:\laragon\tools\sgf-tempo\node-v24.22.1-win-arm64\node.exe' 'scripts/launch-sgf.mjs' 'start' + if ($LASTEXITCODE -ne 0) { throw 'Não foi possível abrir o SGF existente. Os dados e a cache foram preservados.' } + Write-Output 'Abrir o SGF: http://127.0.0.1:8085/sgf' +} finally { Pop-Location } diff --git a/scripts/check-sgf-solution.mjs b/scripts/check-sgf-solution.mjs new file mode 100644 index 0000000..7ac4076 --- /dev/null +++ b/scripts/check-sgf-solution.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import assert from 'node:assert/strict'; +import {spawnSync} from 'node:child_process'; + +// Internal release gate, not a product menu. Read-only metadata; never bootstrap/migrate an application. +const contractFile='docs/integration/sgf-solution-contract.json',contract=JSON.parse(fs.readFileSync(contractFile)); +assert.equal(contract.sgc_required,false);assert.equal(contract.new_demonstrations,false); +assert.deepEqual(contract.modules.map(x=>x.id),['commercial','purchasing','stock','workshop','finance','accounting','human_resources']); +const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')); +assert.equal(run.id,'2576427d1939');assert.equal(run.port,55439); +const root=path.join(run.root,'private/receipt-composition-cc50a276c10c'),state=JSON.parse(fs.readFileSync(path.join(root,'state.json'))),server=JSON.parse(fs.readFileSync(path.join(root,'demo-server.json'))); +assert.equal(state.tag,'cc50a276c10c');assert.equal(server.port,8085); +const hash=file=>crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); +// Keep the footprint tied to real declarations, rather than guessed prefixes. +const declarations=['database/migrations','database/sql','packages/payables/src','packages/general-ledger/src'].flatMap(dir=>fs.readdirSync(dir).filter(f=>/\.(php|sql)$/.test(f)).map(f=>fs.readFileSync(path.join(dir,f),'utf8'))).join('\n')+'\n'+fs.readFileSync('scripts/sgf-document-cancellation-schema.php','utf8')+'\n'+fs.readFileSync('app/Shared/Installation/HumanResourcesProfiles.php','utf8'); +const known=new Set(['users','companies','units','permission_grants','a_clients','a_services','a_service_drafts','a_billing_drafts', + ...[...declarations.matchAll(/(?:CREATE TABLE\s+|Schema::create\(['"])([a-z0-9_]+)/gi)].map(x=>x[1])]); +for(const m of contract.modules){for(const table of m.required_host_tables)assert(known.has(table),'Undeclared table '+table);for(const file of [...m.required_source_files,...m.proofs])assert(fs.existsSync(file),'Missing recorded source/evidence '+file);} +const sql=`BEGIN READ ONLY; +SELECT json_build_object('db',current_database(),'role',current_user,'cluster',current_setting('cluster_name'),'elevated',rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls) FROM pg_roles WHERE rolname=current_user; +SELECT json_build_object('tables',json_agg(tablename ORDER BY tablename)) FROM pg_tables WHERE schemaname='public'; +COMMIT;`; +const result=spawnSync('C:/Program Files/PostgreSQL/17/bin/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',state.env.DB_USERNAME,'-d',state.env.DB_DATABASE,'-Atq','-v','ON_ERROR_STOP=1'],{input:sql,encoding:'utf8',windowsHide:true,env:{...process.env,PGPASSWORD:state.env.DB_PASSWORD}}); +if(result.status!==0)throw Error('Selected presentation metadata unavailable; no ready result may be inferred'); +const [identity,metadata]=result.stdout.trim().split(/\r?\n/).map(JSON.parse); +assert.equal(identity.db,'budget_core_sgf_cc50a276c10c');assert.equal(identity.role,'ir_composition_cc50a276c10c');assert.equal(identity.cluster,'sgf_modular_regression_'+run.id);assert.equal(identity.elevated,false); +const tables=new Set(metadata.tables); +const native=state.nativeConnection; +assert.equal(native.database,'compras_receipts_test_cc50a276c10c');assert.equal(native.username,'payable_history_native_cc50a276c10c'); +const nr=spawnSync('C:/Program Files/PostgreSQL/17/bin/psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',native.username,'-d',native.database,'-Atq','-v','ON_ERROR_STOP=1'],{input:sql,encoding:'utf8',windowsHide:true,env:{...process.env,PGPASSWORD:native.password}}); +assert.equal(nr.status,0,'Canonical native book metadata unavailable');const [ni,nm]=nr.stdout.trim().split(/\r?\n/).map(JSON.parse);assert.equal(ni.db,native.database);assert.equal(ni.role,native.username);assert.equal(ni.cluster,identity.cluster);assert.equal(ni.elevated,false);const nativeTables=new Set(nm.tables); +const modules=contract.modules.map(module=>({id:module.id,label:module.label,features:module.features, + sources:module.required_source_files.map(file=>({file,present:fs.existsSync(file),sha256:fs.existsSync(file)?hash(file):null})), + existing_evidence:module.proofs.map(file=>({file,present:fs.existsSync(file),sha256:fs.existsSync(file)?hash(file):null,not_reexecuted:true})), + missing_host_tables:module.required_host_tables.filter(x=>!tables.has(x)), + missing_native_tables:(module.required_native_tables??[]).filter(x=>!nativeTables.has(x)), + integrated_release_verified:false,client_presentable:false,remaining:module.work_remaining})); +const purchasing=JSON.parse(fs.readFileSync('docs/proofs/purchasing-completion-final-20261104-r2.json')); +modules.find(m=>m.id==='purchasing').accepted_component_journey=purchasing.passed===true&&purchasing.installation.id===state.tag + &&purchasing.sources.every(source=>fs.existsSync(path.join(state.app,source.file))&&hash(path.join(state.app,source.file))===source.sha256); +const acceptance=fs.existsSync(contract.acceptance_proof)?JSON.parse(fs.readFileSync(contract.acceptance_proof)):null; +// No file-presence/schema/table-count shortcut can declare the six modules accepted. +const ready=acceptance?.passed===true&&acceptance.contract_sha256===hash(contractFile)&&acceptance.installation==='cc50a276c10c' + &&contract.release_gates.every(g=>acceptance.gates?.[g.id]===true) + &&modules.every(m=>m.sources.every(s=>s.present)&&m.missing_host_tables.length===0&&m.missing_native_tables.length===0) + &&contract.modules.every(m=>acceptance.modules?.[m.id]?.same_installation===true&&m.features.every(f=>acceptance.modules[m.id].features?.[f]===true)); +const output={date:'2026-11-11',contract_sha256:hash(contractFile),installation:'cc50a276c10c',port:8085,sgc_dependency:false, + full_product_complete:(contract.product_backlog?.length>0&&contract.product_backlog.every(x=>x.implemented===true))??false,product_backlog:contract.product_backlog??[], + modules,release_gates:contract.release_gates.map(g=>({...g,passed:ready?true:false})),client_presentable:ready, + limits:['Host-table footprint is a minimum projection, not a full migration/dependency validation.','Native purchasing/stock tables live in their existing separate native book; this scan does not call that missing.','Source/evidence presence does not mean a feature is integrated, visually accepted or deployment-ready.'], + database_writes:0,application_bootstraps:0,cache_cleared:false,new_demo:false,new_rights:false}; +const out=process.argv[2];if(out){assert(/^docs\/proofs\/[a-z0-9-]+\.json$/.test(out)&&!fs.existsSync(out),'Fresh proof required');fs.writeFileSync(out,JSON.stringify(output,null,2)+'\n');} +console.log(JSON.stringify({modules:modules.length,client_presentable:ready,modules_missing_host_structures:modules.filter(m=>m.missing_host_tables.length).map(m=>m.id),database_writes:0,new_demo:false,output:out??null})); +if(!out&&!ready)process.exitCode=2; diff --git a/scripts/launch-sgf.mjs b/scripts/launch-sgf.mjs new file mode 100644 index 0000000..5c2466a --- /dev/null +++ b/scripts/launch-sgf.mjs @@ -0,0 +1,14 @@ +import fs from 'node:fs';import path from 'node:path';import assert from 'node:assert/strict';import {spawnSync} from 'node:child_process'; +const action=process.argv[2]??'start';assert(['start','status'].includes(action),'Only start/status; no reset, migration or provisioning'); +const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'));assert.equal(run.id,'2576427d1939');assert.equal(run.port,55439);const expected='C:/laragon/data/sgf-tempo/run/modular-regression-'+run.id;assert.equal(path.resolve(run.root),path.resolve(expected));assert.equal(path.resolve(run.cluster),path.resolve(expected+'/postgres'));assert.equal(path.resolve(fs.realpathSync(run.cluster)),path.resolve(run.cluster)); +const s=JSON.parse(fs.readFileSync(expected+'/private/receipt-composition-cc50a276c10c/state.json'));assert.equal(s.tag,'cc50a276c10c');assert.equal(s.singleInstallation,true);const pg='C:/Program Files/PostgreSQL/17/bin/'; +const call=(exe,args,options={})=>spawnSync(exe,args,{encoding:'utf8',windowsHide:true,...options});let status=call(pg+'pg_ctl.exe',['-D',run.cluster,'status']);assert([0,3].includes(status.status),'Owned database status unavailable; preserve its files'); +if(status.status===3&&action==='start'){ + const ready=call(pg+'pg_isready.exe',['-h','127.0.0.1','-p','55439']);assert.notEqual(ready.status,0,'Unexpected service on owned cluster port; do not replace it'); + const started=call(pg+'pg_ctl.exe',['-D',run.cluster,'-l',expected+'/postgres.log','-w','-t','20','start']);assert.equal(started.status,0,'Existing database did not start; no recovery files were deleted');status=call(pg+'pg_ctl.exe',['-D',run.cluster,'status']); +} +if(status.status!==0){console.log(JSON.stringify({running:false,entry:'http://127.0.0.1:8085/sgf',new_database:false}));process.exit(2);} +const check=call(pg+'psql.exe',['-X','-h','127.0.0.1','-p','55439','-U',s.env.DB_USERNAME,'-d',s.env.DB_DATABASE,'-Atq','-v','ON_ERROR_STOP=1','-c',"BEGIN READ ONLY; SELECT current_database()||'|'||current_user||'|'||current_setting('cluster_name'); COMMIT;"],{env:{...process.env,PGPASSWORD:s.env.DB_PASSWORD}});assert.equal(check.status,0,'Selected application database unavailable');assert.equal(check.stdout.trim(),'budget_core_sgf_cc50a276c10c|ir_composition_cc50a276c10c|sgf_modular_regression_'+run.id,'Wrong database identity; no fallback'); +if(action==='start'){const started=call('C:/laragon/tools/sgf-tempo/node-v24.22.1-win-arm64/node.exe',[path.resolve('scripts/purchasing-single-installation.mjs'),'start'],{cwd:path.resolve('.')});assert.equal(started.status,0,'Existing8085 application did not start');} +let response;for(let attempt=0;attempt<12;attempt++){try{response=await fetch('http://127.0.0.1:8085/sgf',{signal:AbortSignal.timeout(3000)});if(response.status===200)break;await response.text();}catch{}await new Promise(resolve=>setTimeout(resolve,250));} +assert(response&&response.status===200,'SGF entry did not become available');assert.equal(response.headers.get('x-sgf-installation'),s.tag);await response.text();console.log(JSON.stringify({running:true,entry:'http://127.0.0.1:8085/sgf',same_installation:true,sgc_required:false,new_database:false,new_demo:false,cache_cleared:false})); diff --git a/scripts/prepare-sgc-8085.mjs b/scripts/prepare-sgc-8085.mjs new file mode 100644 index 0000000..45e8ea9 --- /dev/null +++ b/scripts/prepare-sgc-8085.mjs @@ -0,0 +1,65 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import assert from 'node:assert/strict'; + +// Prepare/check only. Deliberately has no install, activate, migrate, restart or grant action. +const command=process.argv[2]??'check';assert(['check','prepare'].includes(command)); +const repo=process.cwd(),run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')); +assert.equal(run.id,'2576427d1939');assert.equal(run.port,55439); +const root=path.join(run.root,'private/receipt-composition-cc50a276c10c'),state=JSON.parse(fs.readFileSync(path.join(root,'state.json'))),server=JSON.parse(fs.readFileSync(path.join(root,'demo-server.json'))); +assert.equal(state.tag,'cc50a276c10c');assert.equal(server.port,8085);assert.equal(state.singleInstallation,true); +const hash=p=>crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex'),digest=v=>crypto.createHash('sha256').update(JSON.stringify(v)).digest('hex'),normalise=p=>fs.readFileSync(p,'utf8').replaceAll('\r\n','\n'); +const prior=JSON.parse(fs.readFileSync('docs/proofs/purchasing-completion-final-20261104-r2.json')); +for(const source of prior.sources)assert.equal(hash(path.join(state.app,source.file)),source.sha256,'Protected8085 '+source.file); +const endpoint=JSON.parse(fs.readFileSync('docs/proofs/sgc-invoice-endpoint-20261110.json')); +assert(endpoint.passed&&endpoint.checks===78);for(const source of endpoint.sources)assert.equal(hash(source.file),source.sha256,'Accepted endpoint '+source.file); +const files=['app/Http/Middleware/AuthenticateSgcInvoice.php','app/Http/Requests/Sgc/StoreInvoiceRequest.php','app/Http/Controllers/Api/V1/SgcInvoiceIngressController.php','app/Modules/Purchasing/SgcInvoiceIngress.php','config/sgc_invoice.php','routes/sgc.php','bootstrap/app.php','app/Shared/Context/ModuleNavigation.php']; +const additions={ + 'bootstrap/app.php':" api: __DIR__.'/../routes/sgc.php',\n", + 'app/Shared/Context/ModuleNavigation.php':" if (preg_match('~^api/v1/integration/sgc/invoices(?:/orders/[1-9][0-9]*)?$~D', $path)) return 'purchasing';\n" +}; +const entries=files.map(file=>{const target=path.join(state.app,file),exists=fs.existsSync(target);if(additions[file])assert.equal(normalise(file).replace(additions[file],''),normalise(target),'Only additive routing patch allowed');else assert(!exists,'Unexpected endpoint installation: '+file);return {file,before_sha256:exists?hash(target):null,after_sha256:hash(file)};}); +assert(/'enabled'\s*=>\s*false/.test(normalise('config/sgc_invoice.php'))&&/'principals'\s*=>\s*\[\]/.test(normalise('config/sgc_invoice.php'))); +const sgcRoot='C:/laragon/www/compras',sgcFiles=['app/Services/SgfInvoiceGateway.php','config/sgf_invoice.php','app/Services/SgfInvoiceDispatches.php','app/Http/Controllers/SgfInvoiceDispatchController.php', + 'database/migrations/2026_11_10_000001_create_sgf_invoice_dispatches.php',...['layout','document-form','index','evidence','show'].map(x=>'resources/views/sgf-invoices/'+x+'.blade.php'), + 'app/Services/InvoiceWorkflow.php','app/Http/Controllers/InvoiceController.php','routes/web.php','resources/js/pages/invoices/index.tsx']; +const clientFiles=[{source:'packages/sgc-invoice-client/src/InvoiceClient.php',target:'client/InvoiceClient.php'},...sgcFiles.map(file=>({source:path.join(sgcRoot,file),target:'sgc/'+file}))].map(x=>({...x,sha256:hash(x.source)})); +assert(/'enabled'\s*=>\s*false/.test(normalise(path.join(sgcRoot,'config/sgf_invoice.php')))); +assert(/'workspace_enabled'\s*=>\s*false/.test(normalise(path.join(sgcRoot,'config/sgf_invoice.php')))); +const plan={schema:1,installation:'cc50a276c10c',port:8085,endpoint_files:entries,client_files:clientFiles.map(({target,sha256})=>({target,sha256})), + source_endpoint_proof_sha256:hash('docs/proofs/sgc-invoice-endpoint-20261110.json'), + modes:{prepared_only:true,install:false,activate:false,publish:false,new_database:false,new_demo:false}, + endpoint_configuration:{enabled:false,principals:[]},sgc_configuration:{enabled:false,workspace_enabled:false,connections:[],client_package:null,client_package_sha256:null}, + source_schema:{migration:'2026_11_10_000001_create_sgf_invoice_dispatches',pilot_applied:false,source_regression_book_only:'b4093c6f6707'}, + sql_privileges_to_review:{approved:false,role:null,database:null,dispatches:['SELECT','INSERT','UPDATE'],events:['SELECT','INSERT'],sequences:['USAGE','SELECT'],public_grants:false}, + source_existing_files_require_manual_merge:['app/Services/InvoiceWorkflow.php','app/Http/Controllers/InvoiceController.php','routes/web.php','resources/js/pages/invoices/index.tsx'], + review_required:['Approve installation of the implemented SGC draft workspace/schema and disabled code; no blanket migration or existing-invoice forwarding', + 'Explicit source tenant and SGF tenant/company/unit mapping; reviewed order and line IDs', + 'Named QA principal with existing company.view/unit.view/purchasing_invoices.view/prepare rights, auth/context versions', + 'Private credential provisioning, separately approved disabled-code installation and subsequent activation decision', + 'End-to-end SGC transport to this same8085 installation after approval; no deployment claim from kernel/fake tests']}; +const id=digest(plan).slice(0,20),stage=path.join(root,'prepared-sgc-invoice',id),protectedFiles=['state.json','binding.json','demo-server.json']; +const before=protectedFiles.map(file=>({file,sha256:hash(path.join(root,file))})); +if(command==='prepare'){ + if(fs.existsSync(stage)){assert(fs.existsSync(path.join(stage,'manifest.json')),'Incomplete preparation preserved; inspect manually');assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'manifest.json'))),plan);} + else{ + fs.mkdirSync(stage,{recursive:true});for(const entry of entries){const out=path.join(stage,'8085',entry.file);fs.mkdirSync(path.dirname(out),{recursive:true});fs.copyFileSync(entry.file,out);if(entry.before_sha256){const backup=path.join(stage,'before',entry.file);fs.mkdirSync(path.dirname(backup),{recursive:true});fs.copyFileSync(path.join(state.app,entry.file),backup);}} + for(const entry of clientFiles){const out=path.join(stage,entry.target);fs.mkdirSync(path.dirname(out),{recursive:true});fs.copyFileSync(entry.source,out);} + fs.writeFileSync(path.join(stage,'mapping.template.json'),JSON.stringify({selected:false,id:null,version:null,authority:'sgf_only',source_scope:{tenant_id:null},target_scope:{tenant_id:null,company_id:null,unit_id:null},order_id:null,lines:{}},null,2)+'\n'); + fs.writeFileSync(path.join(stage,'principal.template.json'),JSON.stringify({selected:false,enabled:false,client_id:null,token_sha256:null,user_id:null,auth_version:null,tenant_id:null,company_id:null,unit_id:null,context_version:null},null,2)+'\n'); + fs.writeFileSync(path.join(stage,'source-mapping.template.json'),JSON.stringify({approved:false,label:null,approved_by:null,approved_at:null,source_supplier_id:null,mapping:{id:null,version:null,authority:'sgf_only',source_scope:{tenant_id:null},target_scope:{tenant_id:null,company_id:null,unit_id:null},order_id:null,lines:{}}},null,2)+'\n'); + fs.writeFileSync(path.join(stage,'manifest.json'),JSON.stringify(plan,null,2)+'\n'); + } +} +assert(fs.existsSync(path.join(stage,'manifest.json')),'Preparation must exist before check'); +assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'manifest.json'))),plan); +for(const entry of entries)assert.equal(hash(path.join(stage,'8085',entry.file)),entry.after_sha256); +for(const entry of clientFiles)assert.equal(hash(path.join(stage,entry.target)),entry.sha256); +assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'mapping.template.json'))),{selected:false,id:null,version:null,authority:'sgf_only',source_scope:{tenant_id:null},target_scope:{tenant_id:null,company_id:null,unit_id:null},order_id:null,lines:{}}); +assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'principal.template.json'))),{selected:false,enabled:false,client_id:null,token_sha256:null,user_id:null,auth_version:null,tenant_id:null,company_id:null,unit_id:null,context_version:null}); +assert.deepEqual(JSON.parse(fs.readFileSync(path.join(stage,'source-mapping.template.json'))),{approved:false,label:null,approved_by:null,approved_at:null,source_supplier_id:null,mapping:{id:null,version:null,authority:'sgf_only',source_scope:{tenant_id:null},target_scope:{tenant_id:null,company_id:null,unit_id:null},order_id:null,lines:{}}}); +for(const source of prior.sources)assert.equal(hash(path.join(state.app,source.file)),source.sha256); +for(const source of before)assert.equal(hash(path.join(root,source.file)),source.sha256); +for(const entry of entries)assert.equal(fs.existsSync(path.join(state.app,entry.file))?hash(path.join(state.app,entry.file)):null,entry.before_sha256); +console.log(JSON.stringify({passed:true,command,preparation_id:id,endpoint_files:entries.length,client_files:clientFiles.length,target_code_changed:false,target_state_changed:false,credentials_created:false,mappings_assigned:false,installed:false,activated:false,new_demo:false})); diff --git a/scripts/receipt-composition-http.php b/scripts/receipt-composition-http.php index a697db6..224a773 100644 --- a/scripts/receipt-composition-http.php +++ b/scripts/receipt-composition-http.php @@ -1,7 +1,11 @@ make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true,64,JSON_THROW_ON_ERROR); +require_once __DIR__.'/sgf-selected-assets.php'; +$assetPath=parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH); +if(is_string($assetPath)&&sgfSelectedAsset($s,$assetPath)!==null)return false; +if(is_file($s['app'].'/public'.parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH))&&preg_match('~^/(purchasing-exit\.mjs|stock-operations-workspace\.mjs|sgf-finance-payables\.mjs|sgf-composed-ledger\.mjs|purchasing-board\.mjs|purchasing-presenter\.mjs|stock-receipt-focus\.mjs|obligation-evidence\.mjs|invoice-matching\.mjs|request-history\.mjs|purchasing-focus\.mjs|receipt-workspace\.mjs|order-workspace\.mjs|sourcing-workspace\.mjs|supplier-workspace\.mjs|supplier-documents\.mjs|supplier-picker-model\.mjs|purchasing-dossier\.mjs|request-workspace\.mjs|geography-workspace\.mjs|supplier-obligation-workspace\.mjs|supplier-invoice-workspace\.mjs|sgf-module-context\.js|build/assets/[A-Za-z0-9_.-]+)$~',parse_url($_SERVER['REQUEST_URI'],PHP_URL_PATH)))return false; +if(($s['singleInstallation']??false)===true&&($s['tag']??null)==='cc50a276c10c'&&is_string($assetPath)&&str_ends_with($assetPath,'.mjs')){http_response_code(404);header('Content-Type: text/plain; charset=UTF-8');echo 'Recurso de cliente indisponível.';return;} +$loader=require $s['app'].'/vendor/autoload.php';if(($s['singleInstallation']??false)===true){$loader->setPsr4('App\\',$s['app'].'/app');$map=[];foreach($loader->getClassMap() as $class=>$file)if(str_starts_with($class,'App\\')){$local=$s['app'].'/app/'.str_replace('\\','/',substr($class,4)).'.php';if(is_file($local))$map[$class]=$local;}$loader->addClassMap($map);}$app=require $s['app'].'/bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();$f=json_decode(file_get_contents($s['root'].'/fixture.json'),true,64,JSON_THROW_ON_ERROR); Illuminate\Support\Facades\Route::middleware('web')->get('/qa/enter/{role}',function(Illuminate\Http\Request $r,string $role)use($f,$s){abort_unless(in_array($role,['actor','validator','issuer',...(($s['invoices']??false)?['reviewer','auditor']:[]),...(($s['requests']??false)?['requester','approver']:[])],true),404);$user=App\Models\User::findOrFail($f[$role]);auth()->login($user);$r->session()->put('auth_version',$user->auth_version);$token=bin2hex(random_bytes(20));$scope=$f['scope'];if($r->query('foreign'))$scope['unit_id']=$f['foreign_unit'];$r->session()->put('company_context',$scope+['token'=>$token,'module_id'=>$r->query('module','purchasing')]);if($r->boolean('open')&&($s['stockComposition']??false)&&$r->query('module')==='stock')return redirect('/');if($r->boolean('open')&&($s['integrated']??false)&&!$r->has('geography')&&!$r->has('obligations'))return redirect('/purchasing/workspace');if($r->boolean('open')&&($s['geography']??false)&&in_array($r->query('geography'),['company','unit','client'],true))return redirect('/geography/'.$r->query('geography').'/'.($r->query('geography')==='company'?$f['company']:($r->query('geography')==='unit'?$f['unit']:$f['client'])));if($r->boolean('open')&&($s['obligations']??false)&&$r->boolean('obligations'))return redirect('/purchasing/obligations');if($r->boolean('open')&&($s['invoices']??false)&&in_array($role,['issuer','reviewer','auditor'],true))return redirect('/purchasing/invoices');if($r->boolean('open'))return redirect(($s['requests']??false)&&in_array($role,['requester','approver'],true)?'/purchasing/requests':(($s['suppliers']??false)&&$role!=='validator'?'/purchasing/suppliers':(($s['sourcing']??false)&&$role!=='validator'?'/purchasing/sourcing':(($s['orders']??false)&&$role!=='validator'?'/purchasing/orders':'/purchasing/receipts'))));return response()->json(['csrf'=>csrf_token(),'context'=>$token]);}); if(($s['singleInstallation']??false)===true){ App\Modules\Purchasing\BudgetAdapter::guard(); diff --git a/scripts/sgc-dispatch-pg-bootstrap.php b/scripts/sgc-dispatch-pg-bootstrap.php new file mode 100644 index 0000000..51af216 --- /dev/null +++ b/scripts/sgc-dispatch-pg-bootstrap.php @@ -0,0 +1,38 @@ +$sgc,'APP_ENV'=>'testing','APP_KEY'=>$private['app_key'],'APP_DEBUG'=>'false','DB_CONNECTION'=>'pgsql','DB_HOST'=>'127.0.0.1','DB_PORT'=>'55439','DB_DATABASE'=>$n['database'],'DB_USERNAME'=>$n['username'],'DB_PASSWORD'=>$n['password'],'DB_URL'=>'', + 'SESSION_DRIVER'=>'array','CACHE_STORE'=>'array','QUEUE_CONNECTION'=>'sync','COMMON_PAYABLES_ENABLED'=>'false','COMMON_PAYABLES_HISTORY_ENABLED'=>'false','COMMON_PAYABLES_HISTORY_DECISIONS_ENABLED'=>'false', + 'APP_CONFIG_CACHE'=>$root.'/config.php','APP_SERVICES_CACHE'=>$root.'/services.php','APP_PACKAGES_CACHE'=>$root.'/packages.php','APP_ROUTES_CACHE'=>$root.'/routes.php']as$k=>$v){putenv($k.'='.$v);$_ENV[$k]=$v;$_SERVER[$k]=$v;} +foreach(['logs','framework/views','framework/sessions','framework/cache/data']as$d)if(!is_dir($root.'/storage/'.$d))mkdir($root.'/storage/'.$d,0700,true); +require $sgc.'/vendor/autoload.php';$app=require $sgc.'/bootstrap/app.php';$app->addAbsoluteCachePathPrefix('C:');$app->useStoragePath($root.'/storage');$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +$db=Illuminate\Support\Facades\DB::connection();$identity=$db->selectOne("SELECT current_database() db,current_user role,current_setting('cluster_name') cluster,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user"); +if($identity->db!==$n['database']||$identity->role!==$n['username']||$identity->cluster!=='sgf_modular_regression_'.$run['id']||$identity->elevated)throw new LogicException('Physical source identity refused'); +$actor=App\Models\User::where('tenant_id',1)->where('purchasing_role','finance')->findOrFail(4);auth()->guard('web')->setUser($actor); +$receiverRoot=json_decode(file_get_contents($run['root'].'/private/sgc-invoice-current.json'),true)['root']; +$receiverSecret=json_decode(file_get_contents($receiverRoot.'/client.json'),true);$fixture=json_decode(file_get_contents($receiverRoot.'/source-fixture.json'),true); +$localOrder=$db->table('purchase_orders')->where('tenant_id',1)->first(); +$mapping=['id'=>'QA-INTERAPP','version'=>1,'source_scope'=>['tenant_id'=>1],'target_scope'=>['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],'authority'=>'sgf_only','order_id'=>$fixture['order'],'lines'=>['QA-SOURCE-LINE'=>$fixture['line']]]; +config(['sgf_invoice.workspace_enabled'=>true,'sgf_invoice.enabled'=>true,'sgf_invoice.client_package'=>$repo.'/packages/sgc-invoice-client/src/InvoiceClient.php','sgf_invoice.client_package_sha256'=>hash_file('sha256',$repo.'/packages/sgc-invoice-client/src/InvoiceClient.php'), + 'sgf_invoice.connections'=>['qa'=>['enabled'=>true,'client_id'=>'synthetic','token'=>$receiverSecret['token'],'context_version'=>$receiverSecret['context'],'origin'=>'http://127.0.0.1:8085','source_scope'=>['tenant_id'=>1], + 'target_scope'=>['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],'mappings'=>['map'=>['approved'=>true,'label'=>'QA bridge only','source_supplier_id'=>(int)$localOrder->supplier_id,'mapping'=>$mapping]]]]]); +Illuminate\Support\Facades\Http::preventStrayRequests();$bridgeCalls=0; +Illuminate\Support\Facades\Http::fake(function(Illuminate\Http\Client\Request $r)use($receiverRoot,$receiverSecret,$repo,$root,&$bridgeCalls){ + if(!str_starts_with($r->url(),'http://127.0.0.1:8085/api/v1/integration/sgc/invoices'))throw new LogicException('Unexpected bridge destination'); + $headers=[];foreach(['Authorization','X-SGC-Client','X-SGC-Context','X-Idempotency-Key']as$h)$headers[$h]=$r->header($h)[0]??null; + if($headers['Authorization']!=='Bearer '.$receiverSecret['token']||$headers['X-SGC-Context']!==$receiverSecret['context']||$headers['X-SGC-Client']!=='synthetic')return Illuminate\Support\Facades\Http::response([],401); + if(($r['phase']??null)==='confirm'&&getenv('SGC_BRIDGE_DELAY')==='true'){file_put_contents($root.'/sending-ready','ready');usleep(1800000);} + $name='source-'.substr(basename($root),-12).'-'.getmypid().'-'.(++$bridgeCalls);$job=$name.'.json';$out=$name.'-result.json'; + file_put_contents($receiverRoot.'/'.$job,json_encode(['method'=>$r->method(),'path'=>parse_url($r->url(),PHP_URL_PATH),'body'=>$r->data(),'headers'=>$headers],JSON_THROW_ON_ERROR)); + $p=new Symfony\Component\Process\Process(['C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',$repo.'/scripts/sgc-dispatch-receiver.php',$job,$out],$repo,['SGC_INVOICE_PROOF_ROOT'=>$receiverRoot],null,30);$p->run(); + if(!$p->isSuccessful()||!is_file($receiverRoot.'/'.$out)){file_put_contents($root.'/receiver-error.log',$p->getOutput().$p->getErrorOutput());throw new RuntimeException('Receiver kernel incomplete');} + $response=json_decode(file_get_contents($receiverRoot.'/'.$out),true); + if(($r['phase']??null)==='confirm'&&getenv('SGC_BRIDGE_LOSE')==='true'){putenv('SGC_BRIDGE_LOSE=false');throw new Illuminate\Http\Client\ConnectionException('Synthetic source response lost after receiver commit');} + return Illuminate\Support\Facades\Http::response($response['data'],$response['status']); +}); +$service=app(App\Services\SgfInvoiceDispatches::class); diff --git a/scripts/sgc-dispatch-pg-proof.php b/scripts/sgc-dispatch-pg-proof.php new file mode 100644 index 0000000..45b6ba8 --- /dev/null +++ b/scripts/sgc-dispatch-pg-proof.php @@ -0,0 +1,53 @@ +dispatch($actor,$job['id'],$job['version'],'confirm','Confirmação individual sintética de concorrência.',$job['approval']);$result=['status'=>200,'phase'=>$row->phase,'receipt'=>json_decode($row->receipt,true)];} + catch(Symfony\Component\HttpKernel\Exception\HttpException $e){$result=['status'=>$e->getStatusCode()];} + file_put_contents($root.'/'.basename($argv[3]),json_encode($result,JSON_THROW_ON_ERROR));exit; +} +if(is_file($root.'/result.json'))throw new LogicException('Preserve completed source proof'); +$checks=[];$ok=function($v,$name)use(&$checks,$root){if(!$v)throw new RuntimeException($name);$checks[]=$name;file_put_contents($root.'/progress.json',json_encode($checks));}; +$snapshot=function($c,?array $tables=null,?array $sequences=null){$tables??=array_column($c->select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename');$out=[];foreach($tables as$t){if(!preg_match('/^[a-z_][a-z0-9_]*$/D',$t))throw new LogicException('Table');$out[$t]=(array)$c->selectOne('SELECT count(*) rows,md5(coalesce(string_agg(row_to_json(x)::text,chr(10) ORDER BY row_to_json(x)::text),\'\')) fingerprint FROM "'.$t.'" x');}$q=$c->table('pg_sequences')->where('schemaname','public');if($sequences!==null)$q->whereIn('sequencename',$sequences);$out['sequences']=$q->orderBy('sequencename')->get(['sequencename','last_value']);return hash('sha256',json_encode($out,JSON_THROW_ON_ERROR));}; +$selected=json_decode(file_get_contents($run['root'].'/private/receipt-composition-cc50a276c10c/state.json'),true);$receiver=json_decode(file_get_contents($run['root'].'/private/receipt-composition-da77bb730751/state.json'),true); +config(['database.connections.protected_host'=>array_replace(config('database.connections.pgsql'),['host'=>'127.0.0.1','port'=>55439,'database'=>$selected['env']['DB_DATABASE'],'username'=>$selected['env']['DB_USERNAME'],'password'=>$selected['env']['DB_PASSWORD'],'url'=>null]), + 'database.connections.protected_native'=>$selected['nativeConnection'],'database.connections.receiver_verify'=>$receiver['nativeConnection']]); +$protectedBefore=[$snapshot(DB::connection('protected_host')),$snapshot(DB::connection('protected_native'))]; +$tables=array_column($db->select("SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename NOT IN ('sgf_invoice_dispatches','sgf_invoice_dispatch_events') ORDER BY tablename"),'tablename'); +$seq=array_column($db->select("SELECT sequencename FROM pg_sequences WHERE schemaname='public' AND sequencename NOT LIKE 'sgf_invoice_dispatch%' ORDER BY sequencename"),'sequencename'); +if(Schema::hasTable('sgf_invoice_dispatches')&&!is_file($root.'/progress.json'))throw new LogicException('Only this interrupted proof may resume; never reset previous fixtures'); +$sourceBefore=$snapshot($db,$tables,$seq);if(!Schema::hasTable('sgf_invoice_dispatches')){$m=require $sgc.'/database/migrations/2026_11_10_000001_create_sgf_invoice_dispatches.php';$m->up();} +$ok(Schema::hasTable('sgf_invoice_dispatches')&&Schema::hasTable('sgf_invoice_dispatch_events'),'two source persistence tables installed only in existing regression book'); +$receiverDb=DB::connection('receiver_verify');$receivedBefore=$receiverDb->table('invoices')->count();$budgetBefore=$receiverDb->table('budget_consumptions')->sum('amount_minor');$movesBefore=$receiverDb->table('stock_movements')->count();$paymentsBefore=$receiverDb->table('external_payments')->count(); +$data=function($suffix)use($root){$ref='QA-SOURCE-'.substr(basename($root),-12).'-'.$suffix;return ['reference'=>$ref,'connection_id'=>'qa','mapping_id'=>'map','reason'=>'Recepção documental sintética, autoridade única SGF.','document'=>[ + 'number'=>$ref,'issued_on'=>'2026-11-10','subtotal'=>'100.00','tax'=>'0.00','total'=>'100.00','items'=>[['source_line_ref'=>'QA-SOURCE-LINE','quantity'=>'1.000','unit'=>'UN','unit_price'=>'100.00','tax_rate'=>'0.00']]]];}; +$existing=$db->table('sgf_invoice_dispatches')->where('tenant_id',1)->where('reference',$data('lost')['reference'])->first(); +$id=$existing?->id??$service->create($actor,$data('lost'));$version=$existing?->version??1;$row=$service->dispatch($actor,$id,$version,'preview','Conferir impacto real no livro receptor QA.',null); +$ok($row->phase==='previewed','actual SGC service persists real SGF kernel preview');$key=$row->operation_key; +putenv('SGC_BRIDGE_LOSE=true');$row=$service->dispatch($actor,$id,$row->version,'confirm','Aprovar individualmente o impacto recebido.',$service->presentation($actor,$row)['approval_hash']); +$ok($row->phase==='uncertain'&&(bool)$row->ever_sending,'remote commit with lost response is durably uncertain at source'); +$row=$service->dispatch($actor,$id,$row->version,'recover','Recuperar a mesma intenção e o recibo original.',null); +$ok($row->phase==='received_pending'&&$row->operation_key===$key&&json_decode($row->receipt,true)['replayed'],'source recovers one actual pending invoice from receiver'); +$ok($receiverDb->table('invoices')->count()===$receivedBefore+1,'first end-to-end intent creates exactly one receiver invoice'); +$local=App\Models\PurchaseOrder::where('tenant_id',1)->first(); +try{app(App\Services\InvoiceWorkflow::class)->register($actor,$local,['number'=>$data('lost')['document']['number']]);throw new RuntimeException('Unexpected local invoice');} +catch(Illuminate\Validation\ValidationException $e){$ok(str_contains($e->getMessage(),'Documento reservado'),'native SGC writer refuses claimed invoice before any local duplicate');} +$id2=$service->create($actor,$data('race'));$row2=$service->dispatch($actor,$id2,1,'preview','Conferir segundo impacto para concorrência.',null); +file_put_contents($root.'/race.json',json_encode(['id'=>$id2,'version'=>$row2->version,'approval'=>$service->presentation($actor,$row2)['approval_hash']],JSON_THROW_ON_ERROR)); +$a=new Symfony\Component\Process\Process([PHP_BINARY,__FILE__,'--worker','race.json','race-a.json'],$repo,['SGC_DISPATCH_PG_ROOT'=>$root,'SGC_BRIDGE_DELAY'=>'true'],null,40);$a->start(); +for($i=0;$i<160&&!is_file($root.'/sending-ready');$i++)usleep(50000);if(!is_file($root.'/sending-ready'))throw new RuntimeException('First sender did not reach network'); +$b=new Symfony\Component\Process\Process([PHP_BINARY,__FILE__,'--worker','race.json','race-b.json'],$repo,['SGC_DISPATCH_PG_ROOT'=>$root],null,40);$b->start(); +foreach([$a,$b]as$p){$p->wait();if(!$p->isSuccessful()){file_put_contents($root.'/worker-error.log',$p->getOutput().$p->getErrorOutput());throw new RuntimeException('Sender worker incomplete');}} +$ra=json_decode(file_get_contents($root.'/race-a.json'),true);$rb=json_decode(file_get_contents($root.'/race-b.json'),true); +$ok($ra['status']===200&&$ra['phase']==='received_pending'&&$rb['status']===409,'concurrent source decisions yield one send and stale/in-flight refusal'); +$ok($receiverDb->table('invoices')->count()===$receivedBefore+2,'second end-to-end intent creates only one receiver invoice'); +$row2=$service->row($actor,$id2);$ok($row2->phase==='received_pending'&&$db->table('sgf_invoice_dispatch_events')->where('dispatch_id',$id2)->where('action','decision.confirmed')->count()===1,'source has one durable decision and receipt for concurrent intent'); +$db->beginTransaction();try{$db->table('sgf_invoice_dispatch_events')->where('dispatch_id',$id2)->update(['action'=>'tampered']);throw new RuntimeException('Audit mutable');}catch(Illuminate\Database\QueryException){$db->rollBack();$ok(true,'PostgreSQL refuses rewriting source event history');} +$ok($snapshot($db,$tables,$seq)===$sourceBefore,'all original source business rows and sequences preserved'); +$ok($receiverDb->table('budget_consumptions')->sum('amount_minor')===$budgetBefore&&$receiverDb->table('stock_movements')->count()===$movesBefore&&$receiverDb->table('external_payments')->count()===$paymentsBefore,'receiver budget stock and payments unchanged by pending-document intake'); +$ok([$snapshot(DB::connection('protected_host')),$snapshot(DB::connection('protected_native'))]===$protectedBefore,'8085 all host/native rows and sequences unchanged'); +$ok($db->table('sgf_invoice_dispatch_events')->where('action','decision.confirmed')->count()===2,'exactly two individually audited decisions for two sources'); +file_put_contents($root.'/result.json',json_encode(['passed'=>true,'checks'=>$checks,'source_book'=>'b4093c6f6707','receiver_book'=>'da77bb730751','new_receiver_pending_invoices'=>2, + 'source_original_fingerprint'=>$sourceBefore,'protected_8085_fingerprints'=>$protectedBefore,'source_statuses'=>[200,409],'new_database'=>false,'new_role'=>false,'new_server'=>false,'tcp_8085'=>false, + 'two_independent_application_kernels'=>true,'pilot_migration'=>false,'activation'=>false,'publication'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL; diff --git a/scripts/sgc-dispatch-receiver.php b/scripts/sgc-dispatch-receiver.php new file mode 100644 index 0000000..6a66884 --- /dev/null +++ b/scripts/sgc-dispatch-receiver.php @@ -0,0 +1,9 @@ +'HTTP_AUTHORIZATION','X-SGC-Client'=>'HTTP_X_SGC_CLIENT','X-SGC-Context'=>'HTTP_X_SGC_CONTEXT']as$from=>$to)$headers[$to]=$input['headers'][$from]??null; +$r=$send($input['body']??[],$input['headers']['X-Idempotency-Key']??null,$headers,$input['path'],null,$input['method']); +file_put_contents($proofRoot.'/'.basename($argv[2]),json_encode($r,JSON_THROW_ON_ERROR)); diff --git a/scripts/sgc-invoice-authority-proof.php b/scripts/sgc-invoice-authority-proof.php new file mode 100644 index 0000000..d3fb8cc --- /dev/null +++ b/scripts/sgc-invoice-authority-proof.php @@ -0,0 +1,22 @@ +'confirm','order_id'=>$cut['order_id'],'preview'=>$p['data']['preview']];$key=$cut['operation_key']; +$checks=[];$ok=function($v,$name)use(&$checks){if(!$v)throw new RuntimeException($name);$checks[]=$name;}; +$invoices=$native->table('invoices')->orderBy('id')->get()->toJson();$events=$native->table('audit_logs')->count(); +$ok($send($body,$key)['status']===200,'current native operator lock permits authorised recovery'); +$nativeRuntime=DB::connection('receipt_native');$role=$nativeRuntime->table('users')->where('id',$cut['actor'])->value('purchasing_role'); +$nativeRuntime->beginTransaction();try{$nativeRuntime->table('users')->where('id',$cut['actor'])->update(['purchasing_role'=>'warehouse']);$ok($send($body,$key)['status']===403,'revoked native finance function prevents receipt recovery');}finally{$nativeRuntime->rollBack();} +$ok($nativeRuntime->table('users')->where('id',$cut['actor'])->value('purchasing_role')===$role,'native finance fixture restored by transaction rollback'); +$app->instance(Tempo\Purchasing\Embedded\Services\OrganizationAccess::class,new class extends Tempo\Purchasing\Embedded\Services\OrganizationAccess{ + public function maintenanceOnly(Tempo\Purchasing\Embedded\Models\User $user):bool{return true;} +}); +try{$ok($send($body,$key)['status']===403,'maintenance-only native operator cannot recover invoice receipt');} +finally{$app->forgetInstance(Tempo\Purchasing\Embedded\Services\OrganizationAccess::class);} +$ok($send($body,$key)['status']===200,'normal recovery resumes without another business command'); +$ok($invoices===$native->table('invoices')->orderBy('id')->get()->toJson()&&$events===$native->table('audit_logs')->count(),'operator guard checks add no invoice or audit commands'); +file_put_contents($proofRoot.'/authority-result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_business_rows'=>0,'previous_context_sha256'=>hash_file('sha256',$proofRoot.'/context-result.json'), + 'sources'=>[['file'=>'app/Modules/Purchasing/SgcInvoiceIngress.php','sha256'=>hash_file('sha256',$repo.'/app/Modules/Purchasing/SgcInvoiceIngress.php')]]],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); +echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL; diff --git a/scripts/sgc-invoice-check.mjs b/scripts/sgc-invoice-check.mjs new file mode 100644 index 0000000..308cd4a --- /dev/null +++ b/scripts/sgc-invoice-check.mjs @@ -0,0 +1,23 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import {spawnSync} from 'node:child_process'; + +// No provisioning, server launch, database migration, real credentials or grants. +const repo=process.cwd(),run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')); +if(run.id!=='2576427d1939'||run.port!==55439||path.resolve(run.root)!==path.resolve('C:/laragon/data/sgf-tempo/run/modular-regression-'+run.id))throw Error('Known existing QA cluster only'); +const pointer=path.join(run.root,'private/sgc-invoice-current.json'); +if(process.argv.includes('--new')){ + const root=path.join(run.root,'private','sgc-invoice-proof-'+crypto.randomBytes(6).toString('hex')); + fs.mkdirSync(root);fs.writeFileSync(path.join(root,'client.json'),JSON.stringify({token:crypto.randomBytes(32).toString('hex'),context:crypto.randomBytes(32).toString('hex')})); + fs.writeFileSync(pointer,JSON.stringify({root})); + for(const [script,result]of [['sgc-invoice-proof.php','result.json'],['sgc-invoice-security-proof.php','security-result.json'],['sgc-invoice-context-proof.php','context-result.json'],['sgc-invoice-authority-proof.php','authority-result.json']]){ + const r=spawnSync('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['scripts/'+script],{cwd:repo,env:{...process.env,SGC_INVOICE_PROOF_ROOT:root},windowsHide:true,encoding:'utf8',timeout:180000}); + fs.writeFileSync(path.join(root,script+'.log'),(r.stdout??'')+(r.stderr??'')); + if(r.status!==0||!fs.existsSync(path.join(root,result))||JSON.parse(fs.readFileSync(path.join(root,result))).passed!==true)throw Error('Proof incomplete; inspect private log, preserve accepted steps before retrying'); + } +} +const {root}=JSON.parse(fs.readFileSync(pointer));if(path.dirname(path.resolve(root))!==path.resolve(run.root,'private')||!/^sgc-invoice-proof-[a-f0-9]{12}$/.test(path.basename(root)))throw Error('Private proof root invalid'); +const results=['result.json','security-result.json','context-result.json','authority-result.json'].map(file=>JSON.parse(fs.readFileSync(path.join(root,file)))); +if(results.some(x=>x.passed!==true))throw Error('Incomplete proof'); +console.log(JSON.stringify({passed:true,checks:results.reduce((n,x)=>n+x.checks.length,0),existing_regression_book:'da77bb730751',new_demo:false,new_server:false,endpoint_enabled_in_source:false,credentials_printed:false})); diff --git a/scripts/sgc-invoice-client-kernel-proof.php b/scripts/sgc-invoice-client-kernel-proof.php new file mode 100644 index 0000000..21535f7 --- /dev/null +++ b/scripts/sgc-invoice-client-kernel-proof.php @@ -0,0 +1,33 @@ +table('invoices')->orderBy('id')->get()->toJson();$auditBefore=$native->table('audit_logs')->count(); +$calls=[]; +$client=new InvoiceClient('synthetic',$secret['token'],$secret['context'],['tenant_id'=>91],$f['scope'], + function($method,$url,$headers,$body)use($send,&$calls){$calls[]=['method'=>$method,'path'=>parse_url($url,PHP_URL_PATH),'key'=>$headers['X-Idempotency-Key']]; + $response=$send($body??[],$headers['X-Idempotency-Key'],[],parse_url($url,PHP_URL_PATH),null,$method); + return ['status'=>$response['status'],'json'=>$response['data']];}); +$mapping=['id'=>'QA-PROTOCOL-MAPPING','version'=>1,'source_scope'=>['tenant_id'=>91],'target_scope'=>$f['scope'], + 'order_id'=>$cut['order_id'],'lines'=>[],'authority'=>'sgf_only']; +$document=$cut['data'];foreach($document['items']as$i=>&$line){$ref='QA-SOURCE-LINE-'.$i;$mapping['lines'][$ref]=$line['purchase_order_item_id'];unset($line['purchase_order_item_id']);$line=['source_line_ref'=>$ref]+$line;}unset($line); +$draft=['reference'=>'QA-PROTOCOL-DRAFT','version'=>'QA-v1','state'=>'unregistered_draft','document'=>$document]; +$intent=$client->intent($mapping,$draft,$cut['operation_key'],$cut['reason']); +$received=$client->preview($intent,'QA-v1',InvoiceClient::mappingHash($mapping)); +$ok($received['phase']==='received_pending'&&$received['receipt']['replayed'],'prepared client recovers real existing native receipt through kernel'); +$ok(count($calls)===2&&$calls[0]['method']==='GET'&&$calls[1]['method']==='POST','client performs exact context read and explicit preview/recovery only'); +$ok($calls[0]['key']===$cut['operation_key']&&$calls[1]['key']===$cut['operation_key'],'native protocol retains one original UUID'); +$draft['document']['number']='QA-SGC-CLIENT-PREVIEW-'.substr(basename($proofRoot),-12); +$fresh=$client->intent($mapping,$draft,(string)Illuminate\Support\Str::uuid(),$cut['reason']); +$preview=$client->preview($fresh,'QA-v1',InvoiceClient::mappingHash($mapping)); +$ok($preview['phase']==='previewed'&&isset($preview['approval_hash'])&&$preview['impact']['budget_consumption_minor']===0,'prepared client consumes real native impact without confirming'); +$ok($invoiceBefore===$native->table('invoices')->orderBy('id')->get()->toJson()&&$auditBefore===$native->table('audit_logs')->count(),'client contract proof adds no invoice or audit rows'); +$ok(!str_contains(json_encode([$received,$preview]),$secret['token']),'serialised transport states exclude bearer credential'); +file_put_contents($proofRoot.'/client-kernel-result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_business_rows'=>0,'kernel_http'=>true,'tcp_8085'=>false, + 'new_demo'=>false,'new_database'=>false,'sender_class_sha256'=>hash_file('sha256',$repo.'/packages/sgc-invoice-client/src/InvoiceClient.php'), + 'reused_endpoint_proof_sha256'=>hash_file('sha256',$repo.'/docs/proofs/sgc-invoice-endpoint-20261110.json')],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); +echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL; diff --git a/scripts/sgc-invoice-context-proof.php b/scripts/sgc-invoice-context-proof.php new file mode 100644 index 0000000..8d04dd9 --- /dev/null +++ b/scripts/sgc-invoice-context-proof.php @@ -0,0 +1,21 @@ +table('invoices')->count();$beforeAudit=$native->table('audit_logs')->count(); +$checks=[];$ok=function($v,$name)use(&$checks){if(!$v)throw new RuntimeException($name);$checks[]=$name;}; +$get=fn($path,$headers=[],$raw=null)=>$send([],null,$headers,$path,$raw,'GET'); +$r=$get($url);$ok($r['status']===200&&$r['data']['order']['id']===$fixture['order']&&strlen($r['data']['revision'])===64,'authenticated exact order exposes current revision and lines'); +$ok($r['no_store']&&!isset($r['data']['invoices']),'context response is uncached and excludes unrelated invoice list'); +$ok($get($url,['HTTP_AUTHORIZATION'=>null])['status']===401,'context read requires bearer'); +$ok($get('/api/v1/integration/sgc/invoices/orders/'.$nativeFixture['foreign_order'])['status']===403,'context read refuses foreign tenant order'); +$ok($get($url.'?unit_id=1')['status']===422,'context read rejects query authority'); +$ok($get($url,[],'{}')['status']===422,'context read rejects a request body'); +$host->beginTransaction();try{$host->table('permission_grants')->where('user_id',$f['issuer'])->where('permission','purchasing_invoices.view')->delete();$ok($get($url)['status']===403,'context read revalidates view grant');}finally{$host->rollBack();} +config(['sgc_invoice.enabled'=>false]);$ok($get($url)['status']===404,'context route also remains disabled');config(['sgc_invoice.enabled'=>true]); +$cut=json_decode(Illuminate\Support\Facades\Crypt::decryptString(json_decode(file_get_contents($proofRoot.'/initial-preview.json'),true)['data']['preview']),true); +$retry=['phase'=>'preview','order_id'=>$fixture['order'],'revision'=>$cut['revision'],'reason'=>$cut['reason'],'data'=>$cut['data']]; +$ok($send($retry,$cut['operation_key'])['status']===200,'POST durable replay unchanged after adding read-only context route'); +$ok($native->table('invoices')->count()===$before&&$native->table('audit_logs')->count()===$beforeAudit,'context checks add no business effects'); +file_put_contents($proofRoot.'/context-result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_business_rows'=>0,'previous_security_sha256'=>hash_file('sha256',$proofRoot.'/security-result.json')],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); +echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL; diff --git a/scripts/sgc-invoice-proof-bootstrap.php b/scripts/sgc-invoice-proof-bootstrap.php new file mode 100644 index 0000000..8267bc1 --- /dev/null +++ b/scripts/sgc-invoice-proof-bootstrap.php @@ -0,0 +1,38 @@ +$value) if (preg_match('/^(APP_|DB_|SGF_|SESSION_|CACHE_|QUEUE_|BCRYPT_)/', $key)) { putenv($key.'='.$value); $_ENV[$key] = $value; } +$isolated = ['APP_BASE_PATH'=>$repo, 'APP_CONFIG_CACHE'=>$proofRoot.'/config.php', 'APP_ROUTES_CACHE'=>$proofRoot.'/routes.php', + 'APP_SERVICES_CACHE'=>$proofRoot.'/services.php', 'APP_PACKAGES_CACHE'=>$proofRoot.'/packages.php', 'SESSION_DRIVER'=>'array','CACHE_STORE'=>'array']; +foreach ($isolated as $key=>$value) { putenv($key.'='.$value); $_ENV[$key]=$value; } +foreach (['storage/logs','storage/framework/views','storage/framework/sessions','storage/framework/cache/data'] as $dir) if (!is_dir($proofRoot.'/'.$dir)) mkdir($proofRoot.'/'.$dir, 0700, true); +require $repo.'/vendor/autoload.php'; +$app = require $repo.'/bootstrap/app.php'; $app->addAbsoluteCachePathPrefix('C:'); $app->useStoragePath($proofRoot.'/storage'); +$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); +App\Modules\Purchasing\BudgetAdapter::guard(); +$f = json_decode(file_get_contents($s['root'].'/fixture.json'), true); +$nativeFixture = json_decode(file_get_contents($s['root'].'/native-fixture.json'), true); +$secret = json_decode(file_get_contents($proofRoot.'/client.json'), true); +$principal = ['enabled'=>true,'token_sha256'=>hash('sha256',$secret['token']), 'user_id'=>$f['issuer'], 'auth_version'=>1, + 'tenant_id'=>$f['tenant'],'company_id'=>$f['company'],'unit_id'=>$f['unit'],'context_version'=>$secret['context']]; +config(['sgc_invoice.enabled'=>true, 'sgc_invoice.principals'=>['synthetic'=>$principal], 'database.connections.sgc_verify'=>$s['nativeConnection'], + 'receipt_qa.requests_enabled'=>true]); // Test-only audited-origin reader; no request command is dispatched. +$native = Illuminate\Support\Facades\DB::connection('sgc_verify'); +$host = Illuminate\Support\Facades\DB::connection(); +$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); +$send = function (array $body, ?string $key=null, array $headers=[], string $path='/api/v1/integration/sgc/invoices', ?string $raw=null, string $method='POST') use ($secret,$kernel,$proofRoot) { + $server = ['REMOTE_ADDR'=>'127.0.0.1','HTTP_ACCEPT'=>'application/json','CONTENT_TYPE'=>'application/json', + 'HTTP_AUTHORIZATION'=>'Bearer '.$secret['token'],'HTTP_X_SGC_CLIENT'=>'synthetic','HTTP_X_SGC_CONTEXT'=>$secret['context'], + 'HTTP_X_IDEMPOTENCY_KEY'=>$key??(string)Illuminate\Support\Str::uuid()]; + foreach ($headers as $name=>$value) { if ($value === null) unset($server[$name]); else $server[$name]=$value; } + $request = Illuminate\Http\Request::create($path, $method, [], [], [], $server, $method==='GET'?$raw:($raw??json_encode($body, JSON_THROW_ON_ERROR))); + $response = $kernel->handle($request); $kernel->terminate($request,$response); + if ($response->getStatusCode()>=500) file_put_contents($proofRoot.'/last-error.json',$response->getContent()); + return ['status'=>$response->getStatusCode(),'data'=>json_decode($response->getContent(),true),'no_store'=>str_contains((string)$response->headers->get('Cache-Control'),'no-store')]; +}; diff --git a/scripts/sgc-invoice-proof.php b/scripts/sgc-invoice-proof.php new file mode 100644 index 0000000..cef0cdb --- /dev/null +++ b/scripts/sgc-invoice-proof.php @@ -0,0 +1,135 @@ +select("SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"),'tablename'); + $out=[]; + foreach ($tables as $table) { if (!preg_match('/^[a-z_][a-z0-9_]*$/D',$table)) throw new LogicException('Table'); + $out[$table]=(array)$db->selectOne('SELECT count(*) rows, md5(coalesce(string_agg(row_to_json(t)::text, chr(10) ORDER BY row_to_json(t)::text),\'\')) fingerprint FROM "'.$table.'" t'); } + $out['sequences']=$db->select("SELECT sequencename,last_value FROM pg_sequences WHERE schemaname='public' ORDER BY sequencename"); + return hash('sha256',json_encode($out,JSON_THROW_ON_ERROR)); +}; +$selected=json_decode(file_get_contents($run['root'].'/private/receipt-composition-cc50a276c10c/state.json'),true); +config(['database.connections.sgc_protected_host'=>array_replace(config('database.connections.pgsql'),[ + 'host'=>'127.0.0.1','port'=>55439,'database'=>$selected['env']['DB_DATABASE'],'username'=>$selected['env']['DB_USERNAME'],'password'=>$selected['env']['DB_PASSWORD'],'url'=>null]), + 'database.connections.sgc_protected_native'=>$selected['nativeConnection']]); +$protectedBefore=[$snapshot(DB::connection('sgc_protected_host')),$snapshot(DB::connection('sgc_protected_native'))]; +$hostBefore=$snapshot($host); +$financialTables=['budget_entries','budget_consumptions','stock_movements','pa_obligations']; +$effectsBefore=[]; foreach ($financialTables as $table) if ($native->getSchemaBuilder()->hasTable($table)) $effectsBefore[$table]=$native->table($table)->count(); +$existingInvoices=$native->table('invoices')->orderBy('id')->get()->toJson(); +$priorIds=$native->table('invoices')->pluck('id')->all(); +// SQL fixtures only: a distinct order/accepted line to exercise invoice intake. These +// are not claimed as newly executed request/award/receipt/physical-stock workflows. +$fixtureFile=$proofRoot.'/source-fixture.json'; +if(!is_file($fixtureFile)){ + $sourceOrder=$native->table('purchase_orders')->where('tenant_id',$nativeFixture['tenant'])->first(); + $sourceLine=$native->table('purchase_order_items')->where('purchase_order_id',$sourceOrder->id)->first(); + $copy=function($table,$sourceId,$replace=[])use($native){$row=(array)$native->table($table)->where('id',$sourceId)->first();unset($row['id']);return $native->table($table)->insertGetId(array_replace($row,$replace));}; + $fixture=$native->transaction(function()use($native,$copy,$sourceOrder,$sourceLine,$proofRoot,$f){ + $ref='QA-SGC-'.substr(basename($proofRoot),-12); + $request=$copy('requisitions',$sourceOrder->requisition_id,['number'=>$ref]); + $requestLine=$copy('requisition_items',$sourceLine->requisition_item_id,['requisition_id'=>$request]); + $oldAward=$native->table('purchase_awards')->where('id',$sourceOrder->purchase_award_id)->first(); + $quote=$copy('quotations',$oldAward->quotation_id,['requisition_id'=>$request]); + $award=$copy('purchase_awards',$oldAward->id,['requisition_id'=>$request,'quotation_id'=>$quote]); + $order=$copy('purchase_orders',$sourceOrder->id,['number'=>$ref,'requisition_id'=>$request,'purchase_award_id'=>$award]); + $line=$copy('purchase_order_items',$sourceLine->id,['purchase_order_id'=>$order,'requisition_item_id'=>$requestLine]); + $receiptSource=$native->table('goods_receipt_items')->where('purchase_order_item_id',$sourceLine->id)->first(); + $receipt=$copy('goods_receipts',$receiptSource->goods_receipt_id,['purchase_order_id'=>$order,'number'=>$ref,'idempotency_key'=>(string)Str::uuid(),'delivery_note'=>$ref]); + $copy('goods_receipt_items',$receiptSource->id,['goods_receipt_id'=>$receipt,'purchase_order_item_id'=>$line]); + $oldOrigin=$native->table('audit_logs')->where('requisition_id',$sourceOrder->requisition_id)->first(); + $copy('audit_logs',$oldOrigin->id,['requisition_id'=>$request,'action'=>'sgf.request.command','before'=>null, + 'after'=>json_encode(['fixture_only'=>true,'request_scope'=>hash('sha256',json_encode($f['scope'],JSON_THROW_ON_ERROR))])]); + return ['request'=>$request,'order'=>$order,'line'=>$line,'receipt'=>$receipt,'sql_fixture_only'=>true]; + });file_put_contents($fixtureFile,json_encode($fixture,JSON_THROW_ON_ERROR)); +} +$fixture=json_decode(file_get_contents($fixtureFile),true); +$order=$native->table('purchase_orders')->where('id',$fixture['order'])->first(); +$line=$native->table('purchase_order_items')->where('id',$fixture['line'])->first(); +$orderId=(int)$order->id; +Route::post('/api/_sgc_test_state', fn(Illuminate\Http\Request $r)=>app(App\Modules\Purchasing\InvoiceController::class)->show($orderId,app(App\Modules\Purchasing\ReceiptRuntime::class))) + ->middleware(App\Http\Middleware\AuthenticateSgcInvoice::class); +$state=fn()=> $send([],null,[], '/api/_sgc_test_state'); +$document=function(string $suffix)use($line,$proofRoot) { + return ['number'=>'QA-SGC-'.basename($proofRoot).'-'.$suffix, 'issued_on'=>'2026-11-10','subtotal'=>'100.00','tax'=>'0.00','total'=>'100.00', + 'items'=>[['purchase_order_item_id'=>(int)$line->id,'quantity'=>'1.000','unit'=>$line->unit,'unit_price'=>'100.00','tax_rate'=>'0.00']]]; +}; +$preview=function(string $suffix, ?string $key=null)use($state,$send,$orderId,$document){ + $view=$state(); if($view['status']!==200)throw new RuntimeException('Native read refused: '.$view['status']); + $body=['phase'=>'preview','order_id'=>$orderId,'revision'=>$view['data']['revision'],'reason'=>'Conferência sintética SGC, sem aprovação financeira.', 'data'=>$document($suffix)]; + $key??=(string)Str::uuid();return [$send($body,$key),$key,$body]; +}; +$confirm=fn($p,$key)=>$send(['phase'=>'confirm','order_id'=>$orderId,'preview'=>$p],$key); +$firstState=$state(); $ok($firstState['status']===200,'existing regression native order readable with mapped identity'); +[$p,$key,$body]=$preview('main'); +file_put_contents($proofRoot.'/initial-preview.json', json_encode($p,JSON_THROW_ON_ERROR)); +$ok($p['status']===200&&isset($p['data']['preview']),'actual invoice preview via API transport'); +$ok($p['data']['impact']['budget_consumption_minor']===0&&!$p['data']['impact']['payable_created'],'preview has no budget consumption or payable'); +$ok($native->table('invoices')->count()===count($priorIds),'preview rolls all business rows back'); +$ok($p['no_store'],'successful response is not cached'); +config(['sgc_invoice.enabled'=>false]);$ok($send($body,$key)['status']===404,'disabled endpoint refuses even valid credentials');config(['sgc_invoice.enabled'=>true]); +foreach ([['HTTP_AUTHORIZATION'=>null],['HTTP_AUTHORIZATION'=>'Bearer '.str_repeat('x',64)],['HTTP_X_SGC_CLIENT'=>'unknown']] as $headers) + $ok($send($body,$key,$headers)['status']===401,'missing or wrong principal/token refused'); +$ok($send($body,$key,['HTTP_X_SGC_CONTEXT'=>str_repeat('0',64)])['status']===409,'stale context version refused'); +$ok($send($body,$key,['REMOTE_ADDR'=>'192.0.2.1'])['status']===403,'non-TLS remote request refused'); +$ok($send($body,$key,['CONTENT_TYPE'=>'text/plain'])['status']===415,'non JSON refused'); +$ok($send($body,$key,[], '/api/v1/integration/sgc/invoices',str_repeat('x',131073))['status']===413,'oversized body refused'); +$ok($send($body,$key,[], '/api/v1/integration/sgc/invoices','{bad')['status']===422,'malformed JSON refused'); +$bad=$body; $bad['tenant_id']=1;$ok($send($bad,$key)['status']===422,'client cannot choose tenant'); +$bad=$body; $bad['data']['supplier_id']=1;$ok($send($bad,$key)['status']===422,'supplier cannot be silently associated'); +$bad=$body; $bad['data']['total']=100.0;$ok($send($bad,$key)['status']===422,'floating point amounts refused'); +$bad=$body; $bad['data']['total']='101.00';$ok($send($bad,$key)['status']===422,'inconsistent total refused by original writer'); +$bad=$body; $bad['data']['items'][0]['purchase_order_item_id']=$nativeFixture['foreign_order_line'];$ok($send($bad,$key)['status']===422,'foreign native line refused'); +$bad=$body; $bad['order_id']=$nativeFixture['foreign_order'];$ok($send($bad,$key)['status']===403,'foreign native order refused'); +$ok($send($body,'not-a-uuid')['status']===422,'malformed idempotency key refused'); +$changed=$principal; $changed['auth_version']=2;config(['sgc_invoice.principals.synthetic'=>$changed]);$ok($send($body,$key)['status']===401,'revoked authentication version refused'); +$changed=$principal;$changed['unit_id']=$f['foreign_unit'];config(['sgc_invoice.principals.synthetic'=>$changed]);$ok($send($body,$key)['status']===403,'unauthorised unit mapping refused'); +config(['sgc_invoice.principals.synthetic'=>$principal]); +$host->beginTransaction();try{$host->table('permission_grants')->where('user_id',$f['issuer'])->where('permission','purchasing_invoices.prepare')->delete();$ok($send($body,$key)['status']===403,'revoked prepare right refused');}finally{$host->rollBack();} +$result=$confirm($p['data']['preview'],$key);$ok($result['status']===201&&$result['data']['status']==='pending','confirmation creates pending invoice only'); +$invoiceId=$result['data']['invoice_id']; +$replay=$confirm($p['data']['preview'],$key);$ok($replay['status']===200&&$replay['data']['invoice_id']===$invoiceId&&$replay['data']['replayed'],'lost response recovered by same confirmation'); +$ok($send($body,$key)['status']===200,'same request recovered through preview after commit'); +$bad=$body; $bad['reason']='Outro fundamento sintético, conteúdo diferente.';$ok($send($bad,$key)['status']===409,'same key different content conflicts'); +$ok($confirm($p['data']['preview'],(string)Str::uuid())['status']===409,'confirmation must use the preview key'); +$ok($confirm(substr($p['data']['preview'],0,-4).'xxxx',$key)['status']===409,'tampered preview refused'); +$host->beginTransaction();try{$host->table('permission_grants')->where('user_id',$f['issuer'])->where('permission','purchasing_invoices.prepare')->delete();$ok($confirm($p['data']['preview'],$key)['status']===403,'replay revalidates current authority');}finally{$host->rollBack();} +[$stale,$staleKey]=$preview('stale');[$fresh,$freshKey]=$preview('fresh'); +$ok($confirm($fresh['data']['preview'],$freshKey)['status']===201,'second individually prepared document confirmed'); +$ok($confirm($stale['data']['preview'],$staleKey)['status']===409,'changed source invalidates earlier preview'); +[$failure,$failureKey]=$preview('failure');$beforeFailure=$native->table('invoices')->count();$inject=true; +Tempo\Purchasing\Embedded\Models\AuditLog::creating(function($log)use(&$inject){if($inject&&$log->action==='sgf.invoice.command')throw new RuntimeException('Synthetic failure immediately before durable receipt');}); +$failed=$confirm($failure['data']['preview'],$failureKey);$inject=false; +$ok($failed['status']===500&&$native->table('invoices')->count()===$beforeFailure,'failure between writer and receipt rolls invoice back'); +$ok($confirm($failure['data']['preview'],$failureKey)['status']===201,'same operation recovers after failure'); +[$parallel,$parallelKey]=$preview('parallel'); +file_put_contents($proofRoot.'/job.json',json_encode(['body'=>['phase'=>'confirm','order_id'=>$orderId,'preview'=>$parallel['data']['preview']],'key'=>$parallelKey],JSON_THROW_ON_ERROR)); +$workers=[];foreach([0,1]as$i){$process=new Symfony\Component\Process\Process([PHP_BINARY,__FILE__,'--worker','job.json','worker-'.$i.'.json'],$repo,['SGC_INVOICE_PROOF_ROOT'=>$proofRoot],null,45);$process->start();$workers[]=$process;} +foreach($workers as$process){$process->wait();if(!$process->isSuccessful())throw new RuntimeException('Worker failed; inspect private proof output');} +$parallelResults=[];foreach([0,1]as$i)$parallelResults[]=json_decode(file_get_contents($proofRoot.'/worker-'.$i.'.json'),true); +$statuses=array_column($parallelResults,'status');sort($statuses);$ok($statuses===[200,201],'concurrent same-key confirmations return one create and one recovery'); +$ok($parallelResults[0]['data']['invoice_id']===$parallelResults[1]['data']['invoice_id'],'concurrent receipts identify one invoice'); +$afterEffects=[];foreach(array_keys($effectsBefore)as$table)$afterEffects[$table]=$native->table($table)->count(); +$ok($effectsBefore===$afterEffects,'no new consumption stock obligation payment or ledger effects'); +$ok($native->table('invoices')->whereIn('id',$priorIds)->orderBy('id')->get()->toJson()===$existingInvoices,'previous accepted invoice rows preserved'); +$ok($snapshot($host)===$hostBefore,'host rows permissions budget and sequences unchanged'); +$protectedAfter=[$snapshot(DB::connection('sgc_protected_host')),$snapshot(DB::connection('sgc_protected_native'))]; +$ok($protectedBefore===$protectedAfter,'8085 host/native all rows and sequences unchanged'); +$cleared=false;try{app(App\Shared\Context\CompanyContext::class)->identifiers();}catch(Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException){$cleared=true;} +$ok($cleared,'context cleaned after request'); +file_put_contents($proofRoot.'/result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_pending_invoices'=>$native->table('invoices')->count()-count($priorIds), + 'concurrent_statuses'=>$statuses,'protected_8085_fingerprints'=>$protectedAfter,'database_reused'=>'receipt-composition-da77bb730751','new_database'=>false,'new_role'=>false,'new_server'=>false, + 'kernel_http'=>true,'tcp_http'=>false,'sql_source_fixture'=>$fixture,'activation'=>false,'publication'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); +echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL; diff --git a/scripts/sgc-invoice-security-proof.php b/scripts/sgc-invoice-security-proof.php new file mode 100644 index 0000000..434d452 --- /dev/null +++ b/scripts/sgc-invoice-security-proof.php @@ -0,0 +1,45 @@ +'preview','order_id'=>$orderId,'revision'=>$cut['revision'],'reason'=>$cut['reason'],'data'=>$cut['data']]; +$confirmation=['phase'=>'confirm','order_id'=>$orderId,'preview'=>$p['data']['preview']]; +$checks=[];$ok=function($value,$name)use(&$checks,$proofRoot){if(!$value)throw new RuntimeException($name);$checks[]=$name;file_put_contents($proofRoot.'/security-progress.json',json_encode($checks));}; +$beforeInvoices=$native->table('invoices')->orderBy('id')->get()->toJson();$beforeAudit=$native->table('audit_logs')->count(); +$r=$send($confirmation,$key);$ok($r['status']===200,'existing confirmed invoice recovered without new business write');$invoice=$r['data']['invoice_id']; +foreach(['tenant_id','company_id']as$field){$bad=$principal;$bad[$field]=999999;config(['sgc_invoice.principals.synthetic'=>$bad]);$ok($send($confirmation,$key)['status']===403,'foreign '.$field.' principal rejected');} +$bad=$principal;$bad['enabled']=false;config(['sgc_invoice.principals.synthetic'=>$bad]);$ok($send($confirmation,$key)['status']===401,'disabled principal rejected even for replay'); +$bad=$principal;$bad['token_sha256']=hash('sha256',str_repeat('r',64));config(['sgc_invoice.principals.synthetic'=>$bad]);$ok($send($confirmation,$key)['status']===401,'rotated credential invalidates old bearer'); +$bad=$principal;$bad['context_version']=hash('sha256','new synthetic context');config(['sgc_invoice.principals.synthetic'=>$bad]); +$ok($send($confirmation,$key)['status']===409,'old context header rejected after rotation'); +$ok($send($confirmation,$key,['HTTP_X_SGC_CONTEXT'=>$bad['context_version']])['status']===409,'old preview rejected with new context header'); +config(['sgc_invoice.principals.synthetic'=>$principal]); +config(['modules.companies.'.$f['tenant'].':'.$f['company']=>['contracted'=>[],'enabled'=>[]]]); +$ok($send($confirmation,$key)['status']===403,'disabled purchasing module refuses API replay');config(['modules.companies'=>[]]); +$oldOrder=$native->table('purchase_orders')->where('tenant_id',$nativeFixture['tenant'])->where('id','<>',$orderId)->first(); +$bad=$body;$bad['order_id']=(int)$oldOrder->id;$ok($send($bad,$key)['status']===409,'same key cannot be moved to another authorised order'); +$ok($send($confirmation,strtoupper($key))['status']===200,'UUID case does not create a second operation'); +$ok($send($body,$key,[], '/api/v1/integration/sgc/invoices?company_id=1')['status']===422,'query cannot inject another context'); +$bad=$body;$bad['data']['items'][0]['tenant_id']=1;$ok($send($bad,$key)['status']===422,'nested unknown authority field rejected'); +$bad=$body;$bad['phase']='validate';$ok($send($bad,$key)['status']===422,'transport cannot approve or consume invoice'); +$ok($send($body,$key,[], '/api/v1/integration/sgc/payments')['status']===404,'legacy payment route remains unmounted'); +$expired=$cut;$expired['observed']=time()-601;$old=Crypt::encryptString(json_encode($expired)); +$ok($send(['phase'=>'confirm','order_id'=>$orderId,'preview'=>$old],$key)['status']===200,'durable receipt recovered after preview lifetime with current authority'); +$expired['operation_key']=(string)Str::uuid();$old=Crypt::encryptString(json_encode($expired)); +$ok($send(['phase'=>'confirm','order_id'=>$orderId,'preview'=>$old],$expired['operation_key'])['status']===409,'expired unapplied preview cannot write'); +config(['database.connections.sgc_blocker'=>config('database.connections.'.DB::getDefaultConnection())]);$blocker=DB::connection('sgc_blocker'); +file_put_contents($proofRoot.'/lock-job.json',json_encode(['body'=>$confirmation,'key'=>$key],JSON_THROW_ON_ERROR)); +$worker=function(string $output)use($proofRoot,$repo){$p=new Symfony\Component\Process\Process([PHP_BINARY,__DIR__.'/sgc-invoice-proof.php','--worker','lock-job.json',$output],$repo,['SGC_INVOICE_PROOF_ROOT'=>$proofRoot],null,20);$p->run();if(!$p->isSuccessful())throw new RuntimeException('Lock worker failed');return json_decode(file_get_contents($proofRoot.'/'.$output),true);}; +$blocker->beginTransaction();try{$blocker->select('SELECT pg_advisory_xact_lock(hashtextextended(?,0))',['sgc.invoice:'.$key]);$blocked=$worker('blocked.json');}finally{$blocker->rollBack();} +$ok($blocked['status']===503&&($blocked['data']['error']??null)==='RETRY_SAME_KEY','lock timeout returns recoverable503 without duplicate'); +$recovered=$worker('unblocked.json');$ok($recovered['status']===200&&$recovered['data']['invoice_id']===$invoice,'after lock release same operation recovers correct invoice'); +$ok($beforeInvoices===$native->table('invoices')->orderBy('id')->get()->toJson()&&$beforeAudit===$native->table('audit_logs')->count(),'security and lock continuation added no invoices or audit commands'); +$defaults=require $repo.'/config/sgc_invoice.php';$ok($defaults['enabled']===false&&$defaults['principals']===[],'source configuration remains disabled with no principals'); +file_put_contents($proofRoot.'/security-result.json',json_encode(['passed'=>true,'checks'=>$checks,'new_business_rows'=>0,'lock_statuses'=>[$blocked['status'],$recovered['status']], + 'base_proof_preserved_sha256'=>hash_file('sha256',$proofRoot.'/result.json'),'no_real_activation'=>true],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR)); +echo json_encode(['passed'=>true,'checks'=>count($checks)]).PHP_EOL; diff --git a/scripts/sgf-accounting-continuity-http.mjs b/scripts/sgf-accounting-continuity-http.mjs new file mode 100644 index 0000000..4c22100 --- /dev/null +++ b/scripts/sgf-accounting-continuity-http.mjs @@ -0,0 +1,10 @@ +import fs from 'node:fs';import assert from 'node:assert/strict';import {session} from './sgf-single-http.mjs'; +const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c';const old=JSON.parse(fs.readFileSync(root+'/single-financial-http-progress.json')),checks=[];const s=await session('sgf:accounting_prepare','accounting'); +for(const path of ['/general-ledger','/general-ledger/workspace','/api/general-ledger/sales?invoice_id=1','/api/general-ledger/costs?order_id=1','/api/general-ledger/industrial?lot_id=1','/api/catalogue-accounting']){const r=await s.get(path);const body=await r.text();assert.equal(r.status,200,path+': '+body.slice(0,120));if(path.includes('/sales?'))assert.equal(JSON.parse(body).journals.length,4);if(path.includes('/costs?')||path.includes('/industrial?'))assert.equal(JSON.parse(body).journals.length,3);checks.push(path+' reads qualified shared book');} +let r=await s.get('/api/general-ledger?account_q=QA-STOCK&accounts_per_page=100');assert.equal(r.status,200);let data=await r.json();assert.equal(data.chart_accounts.length,1);assert.equal(data.chart_accounts[0].code,'QA-STOCK');checks.push('native chart exposes exact stock account with bounded search');const id=data.chart_accounts[0].id; +r=await s.get('/general-ledger/account-ledger?from_on=2026-01-01&to_on=2026-12-31&account_id='+id);assert.equal(r.status,200);const html=await r.text();assert(html.includes('QA-STOCK'));checks.push('chart account opens actual account ledger'); +r=await s.get('/api/general-ledger?account_q=%25');assert.equal(r.status,200);data=await r.json();assert.equal(data.chart_accounts.length,0);checks.push('chart wildcard is literal rather than an unrestricted search'); +for(const [url,expected]of [['/api/general-ledger/receipts/'+old.operations['sale1000-post'].key,old.operations['sale1000-post'].result.journal_id],['/api/general-ledger/financial-receipts/settlement/'+old.operations.payment20.key,old.operations.payment20.result.payment_id]]){r=await s.get(url);assert.equal(r.status,200);data=await r.json();assert(JSON.stringify(data).includes(String(expected)));checks.push('legacy receipt reads its existing native command without executing it');} +r=await s.get('/general-ledger/flow?source=2');assert.equal(r.status,302);assert(r.headers.get('location').includes('obligation_id=2'));await r.text();checks.push('supplier source retains qualified obligation in common journal continuation'); +r=await s.get('/api/general-ledger/sales?invoice_id=999999');assert.equal(r.status,403);await r.text();checks.push('unknown sales source is refused without falling back to another invoice'); +const proof=root+'/accounting-continuity-http-20261110.json';fs.writeFileSync(proof,JSON.stringify({passed:true,checks,new_business_commands:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof})); diff --git a/scripts/sgf-accounting-report-boundaries.php b/scripts/sgf-accounting-report-boundaries.php new file mode 100644 index 0000000..d873df6 --- /dev/null +++ b/scripts/sgf-accounting-report-boundaries.php @@ -0,0 +1,5 @@ +setUser($user);$session=app('session')->driver();$session->start();$session->put('company_context',$f['scope']+['module_id'=>'accounting','token'=>'report-boundaries']);$r=Illuminate\Http\Request::create('/api/general-ledger/account-ledger','GET');$r->attributes->set('sgf_module','accounting');$r->setLaravelSession($session);$r->setUserResolver(fn()=>$user);$app->instance('request',$r);app(CompanyContext::class)->selectFor($user->id,1,2,2);$controller=app(ComposedLedgerController::class);$base=['from_on'=>'2026-11-10','to_on'=>'2026-11-10']; +foreach([[$base+['account_id'=>2147483647],403,'foreign or missing account denied instead of500'],[$base,422,'explicit account required instead of500'],[$base+['account_id'=>0],422,'invalid identifier rejected'],[$base+['company_id'=>3],422,'scope override rejected']]as[$query,$expected,$name]){Auth::forgetGuards();Auth::shouldUse('web');Auth::guard('web')->setUser($user);$r->setUserResolver(fn()=>$user);$r->query->replace($query);$actual=null;try{$controller->report($r,'account-ledger',app(ComposedLedger::class));}catch(Symfony\Component\HttpKernel\Exception\HttpExceptionInterface$e){$actual=$e->getStatusCode();}catch(Illuminate\Validation\ValidationException){$actual=422;}$ok($actual===$expected,$name);} +Auth::forgetGuards();Auth::shouldUse('web');Auth::guard('web')->setUser($user);$r->setUserResolver(fn()=>$user);$r->query->replace($base);$response=$controller->report($r,'trial-balance',app(ComposedLedger::class));$ok($response->getStatusCode()===200&&$response->getData(true)['kind']==='trial_balance','valid read unaffected');file_put_contents($work.'/accounting-report-boundaries.json',json_encode(['passed'=>true,'checks'=>$checks,'business_commands'=>0,'new_rights'=>0],JSON_PRETTY_PRINT));echo json_encode(['passed'=>true,'checks'=>count($checks),'proof'=>$work.'/accounting-report-boundaries.json']).PHP_EOL; diff --git a/scripts/sgf-accounting-reports-http.mjs b/scripts/sgf-accounting-reports-http.mjs new file mode 100644 index 0000000..8684874 --- /dev/null +++ b/scripts/sgf-accounting-reports-http.mjs @@ -0,0 +1,4 @@ +import fs from 'node:fs';import assert from 'node:assert/strict';import {session} from './sgf-single-http.mjs';import {ledgerReportQuery,checkedReport,reportBasis,reportPageCsv,minorAmount} from '../resources/js/ledger-report-view.js'; +const s=await session('sgf:read_only','accounting'),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);},form={kind:'trial-balance',from_on:'2026-11-10',to_on:'2026-11-10',per_page:'25',account_id:''}; +const get=async(q,basis=null)=>{const r=await s.get('/api/general-ledger/'+q.kind+'?'+new URLSearchParams(q.params));assert.equal(r.status,200);return checkedReport(await r.json(),q,basis);};const query=ledgerReportQuery(form),trial=await get(query);ok(trial.basis.journals===27,'existing27 journal basis preserved');ok(trial.rows.length>0&&trial.rows.length<=25,'bounded real account rows');ok(trial.totals.debit_minor===trial.totals.credit_minor,'actual filtered debit and credit balanced');const row=trial.rows.find(r=>r.code==='QA-CAIXA');assert(row);ok(minorAmount(row.closing_net_minor)==='950,00','cash actual ledger closing950');const ledgerQuery=ledgerReportQuery({...form,kind:'account-ledger',account_id:String(row.account_id)}),ledger=await get(ledgerQuery);ok(ledger.account.code==='QA-CAIXA'&&ledger.closing_net_minor===row.closing_net_minor,'drilldown explicit same account same closing');ok(ledger.rows.length>0&&ledger.rows.every(r=>r.review_id&&r.proposal_id&&r.reason),'original proposal review evidence present');ok(ledger.rows.at(-1).balance_minor===ledger.closing_net_minor,'running balance matches closing on complete known page');ok(reportBasis(ledger)===reportBasis(trial),'same native basis through same date');const larger=await get(ledgerReportQuery({...form,per_page:'100'}));ok(JSON.stringify(larger.rows)===JSON.stringify(trial.rows)&&JSON.stringify(larger.totals)===JSON.stringify(trial.totals),'page sizes25 and100 actual known accounts equivalent');const csv=reportPageCsv(trial);ok(csv.includes('Apenas a pagina consultada')&&csv.includes("'QA-CAIXA")&&csv.includes('950,00'),'CSV page only exact values and safe account text');const unsafe=structuredClone(ledger);unsafe.rows[0].reason=' =HYPERLINK("bad")';ok(reportPageCsv(unsafe).includes("' =HYPERLINK"),'CSV formula-like reason protected');assert.throws(()=>checkedReport({...trial,basis:{...trial.basis,last_journal_id:999999}},query,reportBasis(trial)));ok(true,'changed book marker demands new conference without DB mutation');assert.throws(()=>checkedReport(trial,{...query,params:{...query.params,to_on:'2026-11-09'}}));ok(true,'late response different dates refused');assert.throws(()=>ledgerReportQuery({...form,kind:'account-ledger'}));ok(true,'no account auto selection');assert.throws(()=>ledgerReportQuery({...form,from_on:''}));ok(true,'no dates assumed');assert.throws(()=>ledgerReportQuery({...form,account_id:'9007199254740993'}));ok(true,'unsafe identifier refused');assert.throws(()=>minorAmount(Number.MAX_SAFE_INTEGER+2));ok(true,'unsafe amount refused');if(!process.argv.includes('--before-install')){const missing=await s.get('/api/general-ledger/account-ledger?from_on=2026-11-10&to_on=2026-11-10&account_id=2147483647');ok(missing.status===403||missing.status===409,'foreign nonexistent account refused');const wrong=await session('sgf:read_only','finance'),denied=await wrong.get('/api/general-ledger/trial-balance?from_on=2026-11-10&to_on=2026-11-10');ok([403,409].includes(denied.status),'wrong module refused');} +const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=run.root+'/private/receipt-composition-cc50a276c10c',file=root+(process.argv.includes('--before-install')?'/accounting-reports-preinstall-20261110.json':'/accounting-reports-http-20261110.json');fs.writeFileSync(root+'/accounting-reports-samples-private.json',JSON.stringify({trial,ledger}));fs.writeFileSync(file,JSON.stringify({passed:true,checks,business_commands:0,new_grants:0,new_demo:false,server_read_engine_unchanged:true,http_error_mapping_corrected:!process.argv.includes('--before-install')},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:file})); diff --git a/scripts/sgf-accounting-reports-ui.mjs b/scripts/sgf-accounting-reports-ui.mjs new file mode 100644 index 0000000..34244de --- /dev/null +++ b/scripts/sgf-accounting-reports-ui.mjs @@ -0,0 +1,3 @@ +import fs from 'node:fs';import assert from 'node:assert/strict';import {createRequire} from 'node:module';import {pathToFileURL} from 'node:url';import {transformWithOxc} from 'vite';import React from 'react';import {renderToStaticMarkup} from 'react-dom/server'; +const require=createRequire(import.meta.url),root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',samples=JSON.parse(fs.readFileSync(root+'/accounting-reports-samples-private.json'));const load=async name=>{let source=fs.readFileSync('resources/js/'+name,'utf8').replace("from 'react'",'from '+JSON.stringify(pathToFileURL(require.resolve('react')).href));for(const file of ['ledger-report-view.js','accounting-menu.js'])source=source.replace("from './"+file+"'",'from '+JSON.stringify(pathToFileURL(process.cwd()+'/resources/js/'+file).href));const compiled=await transformWithOxc(source,name,{jsx:{runtime:'classic'}});return import('data:text/javascript;base64,'+Buffer.from(compiled.code).toString('base64'));};const {default:Reports,LedgerReportTable}=await load('LedgerReportsWorkspace.jsx'),{default:Workspace}=await load('ComposedAccountingWorkspace.jsx'),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);};for(const [kind,data]of Object.entries(samples)){const html=renderToStaticMarkup(React.createElement(LedgerReportTable,{data,onAccount(){}}));ok(data.rows.every(r=>html.includes(String(kind==='trial'?r.code:r.journal_id))),kind+' actual rows rendered');ok(!html.includes('NaN')&&!html.includes('undefined'),kind+' exact present values');ok(kind==='trial'?html.includes('Abrir razão desta conta'):html.includes('revisão #'),kind+' explicit source continuation');}const form=renderToStaticMarkup(React.createElement(Reports,{context:{token:'test'},api(){throw Error('No automatic query');}}));ok(form.includes('data-sgf-read-filters="true"'),'read filter not business editor');ok(!form.includes('2026-01-01')&&form.includes('type="date" required="" value=""'),'no fixed or implicit reporting dates');ok(form.includes('Todas as contas do balancete')&&!form.includes('type="number"'),'account selected from rows without ID typing'); +const context={token:'scope',company:{name:'Empresa QA'},unit:{id:2,name:'Unidade QA'},fiscal_qa_enabled:true,fiscal_rules_qa_enabled:true,bank_qa_enabled:true};const outputs={};for(const section of ['accounting_panel','accounting_daily','accounting_review','accounting_closing','accounting_configuration'])outputs[section]=renderToStaticMarkup(React.createElement(Workspace,{section,context,can:()=>true,navigate(){}}));ok(new Set(Object.values(outputs)).size===5,'five sections distinct instead of duplicated panel');ok(outputs.accounting_closing.includes('não fecha nem reabre'),'closing consultation no implicit command');ok(outputs.accounting_review.includes('conferir extracto')&&outputs.accounting_configuration.includes('Fontes e regras'),'authorized cross-module existing functions exposed');const restricted=renderToStaticMarkup(React.createElement(Workspace,{section:'accounting_review',context,can:()=>false,navigate(){}}));ok(!restricted.includes('conferir extracto')&&!restricted.includes('Livro e mapa'),'restricted fiscal and bank links hidden');const escaped=structuredClone(samples.trial);escaped.rows[0].label='';ok(!renderToStaticMarkup(React.createElement(LedgerReportTable,{data:escaped})).includes(''},unit:{id:2,name:'Unidade QA'},modules:[{id:'commercial',label:'Comercial'}],single_installation:true};const html=renderToStaticMarkup(React.createElement(Home,{context,groups:[{id:'billing',label:'Facturação',items:[{id:'billing_all',label:'Todos os documentos'}]}]}));ok(html.includes('Do primeiro registo')&&html.includes('Orçamento e rascunho não são receita'),'meaning and interpretation shown');ok(html.includes('Todos os documentos')&&!html.includes('Diário integrado'),'only supplied authorized functions shown');ok(!html.includes(''}}));ok(!escaped.includes('