This commit is contained in:
2026-10-30 23:20:27 +02:00
parent 2c0be7ac5e
commit 680959cc87
19 changed files with 1528 additions and 15 deletions
@@ -0,0 +1,49 @@
# Pacote funcional de acesso — aplicação autorizada, 30/10/2026
## Resultado
Aplicado o manifesto expressamente confirmado para a conta existente #1 **Osmane Nala — nala@tempo.co.mz**, em **Tempo #1 → Tempo, Lda. #1 → SEGI #1**, na base pública sgf_db. **82 direitos finais: 25 na empresa e 57 na unidade. Seis preservados e 76 acrescentados**, sem direitos fora desse âmbito, alteração de credencial, nova conta ou mudança de dados de negócio por comandos de teste. O manifesto aprovado foi preservado e o seu SHA-256 consta da auditoria e do recibo.
Proposta original: [documento](SGF_FUNCTIONAL_ACCESS_PROPOSAL_20261030.md) e [manifesto exacto](SGF_FUNCTIONAL_ACCESS_PROPOSAL_20261030.json). Estes ficheiros continuam a identificar a proposta aprovada; o estado de aplicação é documentado neste relatório e nos recibos, sem reescrever o conteúdo cuja impressão digital foi auditada.
## Catálogo e atomicidade
A primeira tentativa foi revertida: o CHECK permission_grants_scope_check da instalação pública não admitia 12 nomes do pacote. Leitura posterior confirmou os seis direitos originais e zero registos de aplicação parcial. Esta omissão da verificação inicial do catálogo foi corrigida antes da aplicação final; não se declarou sucesso da tentativa revertida.
A operação final conservou integralmente a expressão anterior do CHECK e acrescentou somente, com unidade obrigatória, os nomes já aprovados: purchasing_budget.view/create/approve/record/consume, purchasing_budget.reinforcement.propose/review, supplier_payables.view e workshop_corrections.view/request/review/apply. São 12 nomes exactos, não um wildcard ou permissão universal. A mudança do catálogo e as 76 inserções decorreram **na mesma transacção**, com bloqueio, verificação de identidade/âmbito, comparação do estado anterior e auditoria. O total final foi confrontado com o manifesto. Foram avaliados os 82 candidatos válidos e os 82 de âmbito invertido; os invertidos foram recusados pela nova expressão.
Esta é uma alteração delimitada da restrição de catálogo da tabela de acessos, necessária para representar os direitos aprovados; não foi migração de módulos nem remoção de guardas da aplicação. A fonte de autorização é a confirmação expressa do pacote de 82 direitos, não uma decisão nova sobre emissão ou operações financeiras. O procedimento administrativo foi executado pelo proprietário da base, porque a aplicação usa sgf_app. Não se concedeu ALTER à conta da aplicação nem se alteraram privilégios dos roles PostgreSQL.
Auditoria: identity.functional_access_package_granted, ID **12**, com operador, motivo, âmbito, manifesto SHA-256 e conjunto de direitos. Antes da aplicação final foi criada cópia privada de permission_grants/identity_audit em `/var/backups/sgf-functional-access-20261030/permissions-audit-before-final.dump`, modo 0600 e directório 0700. Catálogo e geração do SQL pelo pg_restore validados; **não houve ensaio de restauro numa base separada nem reposição sobre histórico activo**. A definição anterior da restrição e o recibo estão no mesmo directório. Qualquer reversão futura deve retirar apenas o delta desta operação e reconciliar acessos/histórico posteriores; não restaurar o dump integral por omissão.
## Verificação técnica após aplicar
As consultas foram realizadas no servidor com actor/contexto explícitos e transacções READ ONLY. Não foi criada uma sessão HTTP para representar o utilizador, nem lidas cookies/sessões privadas. Não se conhece nem se pediu a palavra-passe. Estas provas não substituem utilização interactiva autenticada no navegador.
- 82 direitos efectivos confrontados com o manifesto, sem duplicações.
- Navegação renderizada com as concessões efectivas: **Início, Comercial, Compras, Stock, Oficinas, Financeiro, Contabilidade, Configuração**. Comercial contém Clientes/Serviços e Facturação, incluindo Facturas; sem criar outro menu.
- Antes do pacote: Início, Compras, Stock, Contabilidade e Configuração. A diferença decorre das concessões, não de alteração de código do menu.
- Assets públicos JS/CSS comparados por bytes com os locais: iguais.
- **19 consultas de diagnóstico**: Clientes, Serviços, prestações e rascunhos, preparação documental do emitente/cliente/rascunho, metadados de instruções de pagamento, classificação contabilística, perfis fiscais e configuração da pré-visualização disponíveis. Valores completos de instruções não foram incluídos em relatórios.
- Facturas devolvem test_enabled=false e real_blocked=true. Recebimentos, oficinas, conversão/correcções de oficina, custos e orçamento industrial devolvem o bloqueio de ambiente previsto. Não foram simuladas essas capacidades como disponíveis no público.
- Guardas de emissão, obrigações e controlo orçamental comum recusam o público com **403**, mesmo com os direitos atribuídos. Não foram instaladas tabelas pa_*, ps_* ou cb_*, alterado APP_ENV ou executado comando fiscal/bancário.
- Página inicial e código funcional não foram alterados por esta operação. Não houve publicação, push, aceitação de módulos ou checkpoint de encerramento.
Provas: [aplicação](proofs/functional-access-applied-20261030.json), [consultas do servidor](proofs/functional-access-server-checks-20261030.json), [navegação/assets/entradas](proofs/functional-access-navigation-20261030.json). O teste de navegação reutiliza scripts/functional-access-check.mjs; os dois ficheiros functional-access-*-remote.py são modelos administrativos desta operação, com placeholders, e **não comandos gerais de instalação nem procedimentos de retry**. O primeiro corresponde à tentativa revertida; não deve ser reexecutado.
## Como entrar e o que testar
No público: abrir https://sgf.tempo.co.mz/, actualizar a página e seleccionar Tempo, Lda./SEGI. A sessão consulta as concessões actuais; não é necessário trocar a palavra-passe. Pode utilizar os percursos públicos acima com dados sintéticos, observando versões, pré-requisitos e separação entre preparação/decisão. Facturas e outros destinos de ensaio podem aparecer no menu e apresentar o bloqueio esperado: o direito não habilita esse ambiente.
Entradas locais verificadas por HTTP 200:
| Endereço | Finalidade e entrada |
| --- | --- |
| http://127.0.0.1:8002/avaliar | Demonstração integrada: escolher a função sintética existente para oficinas, custos, orçamento industrial, emissão e recebimentos de teste. Outra identidade continua necessária para decisões independentes. |
| http://127.0.0.1:8003/ | Controlo orçamental comum; usar as contas sintéticas protegidas de preparação, validação, operação e consulta já existentes. Sem expor credenciais no relatório. |
| http://127.0.0.1:8006/demo | Obrigações/pagamentos no SGF: escolher Preparar, Validar ou Consultar. |
| http://127.0.0.1:8005/demo | Mesmo percurso em Compras autónomo, com as respectivas identidades/contextos. |
Não foram copiadas a conta pública ou as concessões para bases sintéticas. As identidades de ensaio já existentes são distintas. Endereços 127.0.0.1 só funcionam no computador de desenvolvimento; não são acesso remoto para avaliadores externos. O domínio público continua sem esses percursos de ensaio habilitados.
Limitação final: verificação técnica de concessões, consultas, guardas, assets e renderização estática aprovada. A observação visual e a confirmação de Comercial na sessão interactiva do responsável após actualizar a página continuam por efectuar; não houve afirmação de revisão visual.
@@ -0,0 +1,977 @@
{
"version": "SGF-FUNCTIONAL-DEVELOPMENT-ACCESS-PROPOSAL-20261030",
"status": "proposal_only_not_applied",
"target": {
"id": 1,
"name": "Osmane Nala",
"email": "nala@tempo.co.mz"
},
"scope": {
"tenant_id": 1,
"tenant_name": "Tempo",
"company_id": 1,
"company_name": "Tempo, Lda.",
"unit_id": 1,
"unit_name": "SEGI"
},
"public_environment": "production",
"public_database": "sgf_db",
"release": "/var/www/sgf-deploy/releases/20261030-201723-2c0be7ac5e34",
"summary": {
"total": 82,
"company": 25,
"unit": 57,
"existing": 6,
"additional": 76,
"availability": {
"public": 36,
"demo_conditional_activation": 1,
"demo_only": 36,
"public_blocked_notice": 9
}
},
"entries": [
{
"permission": "company.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Identity/Services/Permissions.php",
"already_granted": true
},
{
"permission": "company.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Identity/Services/Permissions.php",
"already_granted": true
},
{
"permission": "unit.create",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Identity/Services/Permissions.php",
"already_granted": true
},
{
"permission": "access.manage",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Identity/Services/Permissions.php",
"already_granted": true
},
{
"permission": "unit.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Identity/Services/Permissions.php",
"already_granted": true
},
{
"permission": "unit.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Identity/Services/Permissions.php",
"already_granted": true
},
{
"permission": "clients.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "clients.create",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "clients.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "clients.manage",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "services.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "services.create",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "services.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "services.manage",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "service_drafts.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "service_drafts.create",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "service_drafts.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "service_drafts.manage",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "billing_drafts.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "billing_drafts.create",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "billing_drafts.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "billing_drafts.manage",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/ServicePermissions.php",
"already_granted": false
},
{
"permission": "issuer_documents.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/DocumentPreparation.php",
"already_granted": false
},
{
"permission": "issuer_documents.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/DocumentPreparation.php",
"already_granted": false
},
{
"permission": "client_documents.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/DocumentPreparation.php",
"already_granted": false
},
{
"permission": "client_documents.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/DocumentPreparation.php",
"already_granted": false
},
{
"permission": "draft_documents.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/DocumentPreparation.php",
"already_granted": false
},
{
"permission": "draft_documents.edit",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/DocumentPreparation.php",
"already_granted": false
},
{
"permission": "document_preview.configure",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/DocumentPreview.php",
"already_granted": false
},
{
"permission": "billing_drafts.confirm_internal",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public",
"source": "app/Modules/Services/InternalConfirmation.php",
"already_granted": false
},
{
"permission": "catalogue_accounting.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/CatalogueAccounting.php",
"already_granted": false
},
{
"permission": "catalogue_accounting.propose",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/CatalogueAccounting.php",
"already_granted": false
},
{
"permission": "catalogue_accounting.approve",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/CatalogueAccounting.php",
"already_granted": false
},
{
"permission": "service_fiscal.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServiceFiscal.php",
"already_granted": false
},
{
"permission": "service_fiscal.propose",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServiceFiscal.php",
"already_granted": false
},
{
"permission": "service_fiscal.approve",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "public",
"source": "app/Modules/Services/ServiceFiscal.php",
"already_granted": false
},
{
"permission": "service_fiscal.calculate",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_conditional_activation",
"source": "app/Modules/Services/ServiceFiscal.php",
"already_granted": false
},
{
"permission": "invoice_series.propose",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceInvoicing.php",
"already_granted": false
},
{
"permission": "invoice_series.approve",
"tenant_id": 1,
"company_id": 1,
"unit_id": null,
"scope": "company",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceInvoicing.php",
"already_granted": false
},
{
"permission": "service_invoices.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Services/ServiceInvoicing.php",
"already_granted": false
},
{
"permission": "service_invoices.issue",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceInvoicing.php",
"already_granted": false
},
{
"permission": "service_invoices.correction_request",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceInvoicing.php",
"already_granted": false
},
{
"permission": "service_receivables.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Services/ServiceReceivables.php",
"already_granted": false
},
{
"permission": "service_receivables.prepare",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceReceivables.php",
"already_granted": false
},
{
"permission": "service_receivables.confirm",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceReceivables.php",
"already_granted": false
},
{
"permission": "service_receivables.allocate",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceReceivables.php",
"already_granted": false
},
{
"permission": "service_receivables.reconcile",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceReceivables.php",
"already_granted": false
},
{
"permission": "service_receivables.correction_request",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceReceivables.php",
"already_granted": false
},
{
"permission": "service_receivables.correction_approve",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Services/ServiceReceivables.php",
"already_granted": false
},
{
"permission": "workshop.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Workshop/WorkshopWorkflow.php",
"already_granted": false
},
{
"permission": "workshop.open",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopWorkflow.php",
"already_granted": false
},
{
"permission": "workshop.assign",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopWorkflow.php",
"already_granted": false
},
{
"permission": "workshop.authorize",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopWorkflow.php",
"already_granted": false
},
{
"permission": "workshop.record",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopWorkflow.php",
"already_granted": false
},
{
"permission": "workshop.verify",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopWorkflow.php",
"already_granted": false
},
{
"permission": "workshop_billing.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Workshop/WorkshopBilling.php",
"already_granted": false
},
{
"permission": "workshop_billing.prepare",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopBilling.php",
"already_granted": false
},
{
"permission": "workshop_billing.review",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopBilling.php",
"already_granted": false
},
{
"permission": "workshop_billing.generate",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopBilling.php",
"already_granted": false
},
{
"permission": "workshop_corrections.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Workshop/WorkshopCorrections.php",
"already_granted": false
},
{
"permission": "workshop_corrections.request",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopCorrections.php",
"already_granted": false
},
{
"permission": "workshop_corrections.review",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopCorrections.php",
"already_granted": false
},
{
"permission": "workshop_corrections.apply",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/WorkshopCorrections.php",
"already_granted": false
},
{
"permission": "direct_costs.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Workshop/DirectCosts.php",
"already_granted": false
},
{
"permission": "direct_costs.prepare",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/DirectCosts.php",
"already_granted": false
},
{
"permission": "direct_costs.validate",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Workshop/DirectCosts.php",
"already_granted": false
},
{
"permission": "industrial_costs.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Industry/IndustrialCosts.php",
"already_granted": false
},
{
"permission": "industrial_costs.prepare",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Industry/IndustrialCosts.php",
"already_granted": false
},
{
"permission": "industrial_costs.validate",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Industry/IndustrialCosts.php",
"already_granted": false
},
{
"permission": "industrial_budgets.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Industry/IndustrialBudgets.php",
"already_granted": false
},
{
"permission": "industrial_budgets.prepare",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Industry/IndustrialBudgets.php",
"already_granted": false
},
{
"permission": "industrial_budgets.validate",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Industry/IndustrialBudgets.php",
"already_granted": false
},
{
"permission": "industrial_budget_comparisons.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "public_blocked_notice",
"source": "app/Modules/Industry/IndustrialBudgets.php",
"already_granted": false
},
{
"permission": "industrial_budget_comparisons.prepare",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Industry/IndustrialBudgets.php",
"already_granted": false
},
{
"permission": "purchasing_budget.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Purchasing/BudgetWorkspace.php",
"already_granted": false
},
{
"permission": "purchasing_budget.create",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Purchasing/BudgetWorkspace.php",
"already_granted": false
},
{
"permission": "purchasing_budget.approve",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Purchasing/BudgetWorkspace.php",
"already_granted": false
},
{
"permission": "purchasing_budget.record",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Purchasing/BudgetWorkspace.php",
"already_granted": false
},
{
"permission": "purchasing_budget.consume",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Purchasing/BudgetWorkspace.php",
"already_granted": false
},
{
"permission": "purchasing_budget.reinforcement.propose",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Purchasing/BudgetWorkspace.php",
"already_granted": false
},
{
"permission": "purchasing_budget.reinforcement.review",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Purchasing/BudgetWorkspace.php",
"already_granted": false
},
{
"permission": "supplier_payables.view",
"tenant_id": 1,
"company_id": 1,
"unit_id": 1,
"scope": "unit",
"availability": "demo_only",
"source": "app/Modules/Purchasing/SupplierPayables.php",
"already_granted": false
}
],
"qa_supplier_actions": [
"view",
"prepare",
"validate",
"payments_view",
"payment_prepare",
"payment_confirm",
"allocation_prepare",
"allocation_confirm"
],
"qa_supplier_store": "pa_access; synthetic contexts only, not public sgf_db",
"separation_of_duties_preserved": true,
"real_fiscal_emission": false,
"bank_execution": false,
"public_payables_enabled": false,
"permission_changes": 0,
"source_comparison": [
{
"file": "app/Modules/Services/ServiceWorkflow.php",
"identical": true
},
{
"file": "app/Modules/Services/ServicePermissions.php",
"identical": true
},
{
"file": "app/Modules/Services/DocumentPreparation.php",
"identical": true
},
{
"file": "app/Modules/Services/DocumentPreview.php",
"identical": true
},
{
"file": "app/Modules/Services/InternalConfirmation.php",
"identical": true
},
{
"file": "app/Modules/Services/CatalogueAccounting.php",
"identical": true
},
{
"file": "app/Modules/Services/ServiceFiscal.php",
"identical": true
},
{
"file": "app/Modules/Services/ServiceInvoicing.php",
"identical": true
},
{
"file": "app/Modules/Services/ServiceReceivables.php",
"identical": true
},
{
"file": "app/Modules/Services/ServiceCalculation.php",
"identical": true
},
{
"file": "app/Modules/Workshop/WorkshopWorkflow.php",
"identical": true
},
{
"file": "app/Modules/Workshop/WorkshopBilling.php",
"identical": true
},
{
"file": "app/Modules/Workshop/WorkshopCorrections.php",
"identical": true
},
{
"file": "app/Modules/Workshop/DirectCosts.php",
"identical": true
},
{
"file": "app/Modules/Industry/IndustrialCosts.php",
"identical": true
},
{
"file": "app/Modules/Industry/IndustrialBudgets.php",
"identical": true
},
{
"file": "app/Modules/Purchasing/BudgetWorkspace.php",
"identical": true
},
{
"file": "app/Modules/Purchasing/BudgetAdapter.php",
"identical": true
},
{
"file": "app/Modules/Purchasing/SupplierPayables.php",
"identical": true
},
{
"file": "app/Shared/Testing/TestingDatabaseGuard.php",
"identical": true
},
{
"file": "app/Modules/Identity/Services/Permissions.php",
"identical": true
}
],
"qa_supplier_roles": {
"preparacao": [
"view",
"prepare",
"payments_view",
"payment_prepare",
"allocation_prepare"
],
"validacao": [
"view",
"validate",
"payments_view",
"payment_confirm",
"allocation_confirm"
],
"consulta": [
"view",
"payments_view"
]
}
}
@@ -0,0 +1,93 @@
# Proposta única de acesso funcional — 30/10/2026
**Estado posterior:** pacote confirmado e aplicado em 30/10/2026. Ver [relatório da operação](SGF_FUNCTIONAL_ACCESS_DELIVERY_20261030.md), incluindo a adaptação estrita de 12 nomes em falta no catálogo da base, os 76 direitos acrescentados e os bloqueios públicos preservados. O manifesto JSON original foi conservado para conferência do SHA auditado. O texto abaixo é a proposta aprovada, não uma indicação de que a aplicação permanece pendente.
**Estado: proposta para confirmação, zero alterações de acessos.** Origem: pedido expresso do responsável para inventariar o pacote completo de desenvolvimento/teste; não é autorização de o aplicar. Uma confirmação futura refere-se ao conjunto exacto do [manifesto](SGF_FUNCTIONAL_ACCESS_PROPOSAL_20261030.json), não a permissões universais ou a futuros módulos.
## Destino e operação proposta
Conta existente #1 **Osmane Nala — nala@tempo.co.mz**. Organização **Tempo #1**, empresa **Tempo, Lda. #1**, unidade **SEGI #1**; public sgf_db, APP_ENV production. Confirmados no servidor em transacção READ ONLY. Não se equiparam organização, empresa e unidade. Não altera conta, palavra-passe, outras unidades, outras empresas ou dados de negócio.
Proposta: completar por inserção aditiva e auditada o conjunto de **82 direitos**, sendo **25 da empresa** e **57 exclusivamente de SEGI**. **Seis já existem; 76 seriam acrescentados**, sem os recriar ou revogar. Não há curingas. Prévia à execução: revalidar release, guardas, identidade e concessões; apresentar qualquer diferença concreta antes de escrever. Fazer backup dos acessos/auditoria e validar o restauro possível; executar numa transacção com contexto fixo e idempotência, recusando alterações concorrentes incompatíveis. Preservar os bloqueios reais. Esta etapa apenas documenta o desenho.
Uma permissão de comando de ensaio atribuída à conta pública fica **sem execução nesse ambiente** quando a guarda exigir a demonstração. Definir o pacote integral de uma vez não converte sgf_db em base QA nem autoriza remover guardas, trocar APP_ENV, copiar fixtures, importar dados ou criar novas instâncias. Os esquemas pa_*, ps_* e cb_* não existem na base pública conferida; esta proposta não os instala.
## O que fica disponível onde
- **36 direitos** suportam operações já existentes na instalação pública: administração delimitada, clientes/serviços, prestações/rascunhos, preparação documental, instruções de pagamento do emitente, pré-visualização, confirmação interna sem validade fiscal, classificação contabilística e propostas/decisões de perfis fiscais. Criar apenas dados sintéticos identificados como teste e manter as versões/histórico. A concessão não assegura que todos os dados/documentos prévios estejam preenchidos nem elimina revisão independente.
- **9 direitos** abrem os percursos cuja consulta pública devolve aviso de indisponibilidade de ensaio; não dão acesso a documentos de teste inexistentes. Incluem Facturas, recebimentos, oficinas, custeio e orçamento/comparações industriais.
- **37 direitos** são operações ou entrada estritamente dependentes dos ambientes de demonstração, incluindo cálculo fiscal com perfil activado em ensaio. O servidor continua a recusá-las fora dos destinos permitidos. Nenhuma permissão transforma a emissão de teste em emissão fiscal real, nem reconciliação simulada em ligação bancária.
| Âmbito | Direitos exactos | Utilização actual | Fonte |
| --- | --- | --- | --- |
| Empresa (unidade NULL) | `company.view`, `company.edit` | Operação existente no público, com dados sintéticos | [Permissions.php](../app/Modules/Identity/Services/Permissions.php) |
| Empresa (unidade NULL) | `unit.create` | Operação existente no público, com dados sintéticos | [Permissions.php](../app/Modules/Identity/Services/Permissions.php) |
| Empresa (unidade NULL) | `access.manage` | Operação existente no público, com dados sintéticos | [Permissions.php](../app/Modules/Identity/Services/Permissions.php) |
| SEGI (unidade #1) | `unit.view`, `unit.edit` | Operação existente no público, com dados sintéticos | [Permissions.php](../app/Modules/Identity/Services/Permissions.php) |
| Empresa (unidade NULL) | `clients.view`, `clients.create`, `clients.edit`, `clients.manage` | Operação existente no público, com dados sintéticos | [ServicePermissions.php](../app/Modules/Services/ServicePermissions.php) |
| Empresa (unidade NULL) | `services.view`, `services.create`, `services.edit`, `services.manage` | Operação existente no público, com dados sintéticos | [ServicePermissions.php](../app/Modules/Services/ServicePermissions.php) |
| SEGI (unidade #1) | `service_drafts.view`, `service_drafts.create`, `service_drafts.edit`, `service_drafts.manage` | Operação existente no público, com dados sintéticos | [ServicePermissions.php](../app/Modules/Services/ServicePermissions.php) |
| SEGI (unidade #1) | `billing_drafts.view`, `billing_drafts.create`, `billing_drafts.edit`, `billing_drafts.manage`, `billing_drafts.confirm_internal` | Operação existente no público, com dados sintéticos | [ServicePermissions.php](../app/Modules/Services/ServicePermissions.php) |
| Empresa (unidade NULL) | `issuer_documents.view`, `issuer_documents.edit` | Operação existente no público, com dados sintéticos | [DocumentPreparation.php](../app/Modules/Services/DocumentPreparation.php) |
| Empresa (unidade NULL) | `client_documents.view`, `client_documents.edit` | Operação existente no público, com dados sintéticos | [DocumentPreparation.php](../app/Modules/Services/DocumentPreparation.php) |
| SEGI (unidade #1) | `draft_documents.view`, `draft_documents.edit` | Operação existente no público, com dados sintéticos | [DocumentPreparation.php](../app/Modules/Services/DocumentPreparation.php) |
| Empresa (unidade NULL) | `document_preview.configure` | Operação existente no público, com dados sintéticos | [DocumentPreview.php](../app/Modules/Services/DocumentPreview.php) |
| Empresa (unidade NULL) | `catalogue_accounting.view`, `catalogue_accounting.propose`, `catalogue_accounting.approve` | Operação existente no público, com dados sintéticos | [CatalogueAccounting.php](../app/Modules/Services/CatalogueAccounting.php) |
| Empresa (unidade NULL) | `service_fiscal.view`, `service_fiscal.propose`, `service_fiscal.approve` | Operação existente no público, com dados sintéticos | [ServiceFiscal.php](../app/Modules/Services/ServiceFiscal.php) |
| SEGI (unidade #1) | `service_fiscal.calculate` | Cálculo demonstrável com perfil activado em ensaio; sem activação pública | [ServiceFiscal.php](../app/Modules/Services/ServiceFiscal.php) |
| Empresa (unidade NULL) | `invoice_series.propose`, `invoice_series.approve` | Só execução na demonstração própria | [ServiceInvoicing.php](../app/Modules/Services/ServiceInvoicing.php) |
| SEGI (unidade #1) | `service_invoices.view` | No público, apenas informação/aviso de bloqueio | [ServiceInvoicing.php](../app/Modules/Services/ServiceInvoicing.php) |
| SEGI (unidade #1) | `service_invoices.issue`, `service_invoices.correction_request` | Só execução na demonstração própria | [ServiceInvoicing.php](../app/Modules/Services/ServiceInvoicing.php) |
| SEGI (unidade #1) | `service_receivables.view` | No público, apenas informação/aviso de bloqueio | [ServiceReceivables.php](../app/Modules/Services/ServiceReceivables.php) |
| SEGI (unidade #1) | `service_receivables.prepare`, `service_receivables.confirm`, `service_receivables.allocate`, `service_receivables.reconcile`, `service_receivables.correction_request`, `service_receivables.correction_approve` | Só execução na demonstração própria | [ServiceReceivables.php](../app/Modules/Services/ServiceReceivables.php) |
| SEGI (unidade #1) | `workshop.view` | No público, apenas informação/aviso de bloqueio | [WorkshopWorkflow.php](../app/Modules/Workshop/WorkshopWorkflow.php) |
| SEGI (unidade #1) | `workshop.open`, `workshop.assign`, `workshop.authorize`, `workshop.record`, `workshop.verify` | Só execução na demonstração própria | [WorkshopWorkflow.php](../app/Modules/Workshop/WorkshopWorkflow.php) |
| SEGI (unidade #1) | `workshop_billing.view` | No público, apenas informação/aviso de bloqueio | [WorkshopBilling.php](../app/Modules/Workshop/WorkshopBilling.php) |
| SEGI (unidade #1) | `workshop_billing.prepare`, `workshop_billing.review`, `workshop_billing.generate` | Só execução na demonstração própria | [WorkshopBilling.php](../app/Modules/Workshop/WorkshopBilling.php) |
| SEGI (unidade #1) | `workshop_corrections.view` | No público, apenas informação/aviso de bloqueio | [WorkshopCorrections.php](../app/Modules/Workshop/WorkshopCorrections.php) |
| SEGI (unidade #1) | `workshop_corrections.request`, `workshop_corrections.review`, `workshop_corrections.apply` | Só execução na demonstração própria | [WorkshopCorrections.php](../app/Modules/Workshop/WorkshopCorrections.php) |
| SEGI (unidade #1) | `direct_costs.view` | No público, apenas informação/aviso de bloqueio | [DirectCosts.php](../app/Modules/Workshop/DirectCosts.php) |
| SEGI (unidade #1) | `direct_costs.prepare`, `direct_costs.validate` | Só execução na demonstração própria | [DirectCosts.php](../app/Modules/Workshop/DirectCosts.php) |
| SEGI (unidade #1) | `industrial_costs.view` | No público, apenas informação/aviso de bloqueio | [IndustrialCosts.php](../app/Modules/Industry/IndustrialCosts.php) |
| SEGI (unidade #1) | `industrial_costs.prepare`, `industrial_costs.validate` | Só execução na demonstração própria | [IndustrialCosts.php](../app/Modules/Industry/IndustrialCosts.php) |
| SEGI (unidade #1) | `industrial_budgets.view` | No público, apenas informação/aviso de bloqueio | [IndustrialBudgets.php](../app/Modules/Industry/IndustrialBudgets.php) |
| SEGI (unidade #1) | `industrial_budgets.prepare`, `industrial_budgets.validate` | Só execução na demonstração própria | [IndustrialBudgets.php](../app/Modules/Industry/IndustrialBudgets.php) |
| SEGI (unidade #1) | `industrial_budget_comparisons.view` | No público, apenas informação/aviso de bloqueio | [IndustrialBudgets.php](../app/Modules/Industry/IndustrialBudgets.php) |
| SEGI (unidade #1) | `industrial_budget_comparisons.prepare` | Só execução na demonstração própria | [IndustrialBudgets.php](../app/Modules/Industry/IndustrialBudgets.php) |
| SEGI (unidade #1) | `purchasing_budget.view`, `purchasing_budget.create`, `purchasing_budget.approve`, `purchasing_budget.record`, `purchasing_budget.consume`, `purchasing_budget.reinforcement.propose`, `purchasing_budget.reinforcement.review` | Só execução na demonstração própria | [BudgetWorkspace.php](../app/Modules/Purchasing/BudgetWorkspace.php) |
| SEGI (unidade #1) | `supplier_payables.view` | Só execução na demonstração própria | [SupplierPayables.php](../app/Modules/Purchasing/SupplierPayables.php) |
## Demonstrações e identidades
| Destino | Percursos | Autoridade |
| --- | --- | --- |
| http://127.0.0.1:8002/avaliar | Oficinas, custeio, orçamento industrial, emissão/recebimentos de teste | Identidades sintéticas existentes de proprietário, preparação, validação, técnico e verificação; mantém a separação exigida pelo percurso |
| http://127.0.0.1:8003 | Controlo orçamental comum de Compras | Contexto/contas sintéticos próprios de preparação, validação, operação e consulta; credenciais privadas existentes, não incluídas no relatório |
| http://127.0.0.1:8006/demo | Obrigações/pagamentos/afectações no SGF | Entrada por funções sintéticas existentes, contexto QA e pa_access próprios |
| http://127.0.0.1:8005/demo | Mesmo percurso no Compras autónomo | Identidades/contexto próprios de Compras; não é a conta Tempo/SEGI da base pública |
A conta pública não é silenciosamente replicada para essas bases. IDs #1 iguais em bases diferentes não representam identidade ou autorização comum. Para a primeira passagem autónoma não se depende de pessoas reais: alternar funções de ensaio existentes. Poder preparar e aprovar na conta de desenvolvimento **não autoriza aprovar o seu próprio facto**; os controlos de actor/pessoa distintos continuam efectivos.
Obrigações/pagamentos usam **dois níveis**, não direitos inventados como supplier_payables.prepare ou supplier_payments.approve. O gateway SGF é supplier_payables.view em SEGI. O segundo nível é a tabela pa_access, apenas nos contextos sintéticos próprios. Conjunto exacto existente: view, prepare, validate, payments_view, payment_prepare, payment_confirm, allocation_prepare, allocation_confirm. Distribuição a preservar:
- Preparação: view, prepare, payments_view, payment_prepare, allocation_prepare.
- Validação: view, validate, payments_view, payment_confirm, allocation_confirm.
- Consulta: view, payments_view.
Estas acções **não seriam inseridas na instalação pública**. Não se cria person mapping para a conta real nem se fundem funções. A aprovação do pacote geral não habilita o percurso público de obrigações e pagamentos; apenas elimina pedidos fragmentados de direitos do catálogo SGF e conserva o catálogo completo de ensaio como referência.
## Limites preservados e lacunas
Emissão fiscal real permanece bloqueada em ServiceInvoicing.guard/ServiceCalculation.isolated e TestingDatabaseGuard. O nome service_invoices.issue refere-se ao comando existente de teste. Não existe neste pacote uma autorização de emissão real. Séries e regras da emissão de teste exigem igualmente a guarda e as decisões prévias.
Nenhuma operação bancária é ligada ou executada. service_receivables.reconcile corresponde a reconciliação **simulada**; registar instruções bancárias no emitente é preparação documental, não movimentação de dinheiro. As correcções de recebimentos já existentes são distintas das correcções de afectações a fornecedores, que continuam não implementadas. Propostas/aprovações fiscais não activam perfis reais; a única activação existente é proofActivation, protegida para ensaio e sem endpoint de activação real.
Stock e as secções contabilísticas de apresentação não têm novos direitos operacionais a inventar: usam as condições de visibilidade existentes, incluindo company.view. Razão, lançamentos, fecho, stock SGF integrado, facturas-recibo, recibos fiscais e notas de crédito não se tornam funcionalidades entregues por aparecerem no menu. Não foram incluídos direitos de módulos futuros ou de outros produtos.
A interface genérica de Acessos actualmente lista só os seis direitos administrativos de Permissions.COMPANY/UNIT; manage nas quatro áreas de Services permite gestão delimitada dessas áreas, não um administrador universal. O pacote completo exige provisão específica auditada se confirmado; não cabe no comando identity:provision local, que continua protegido e sem desactivar a sua guarda. A ausência de configuração universal pela interface é uma lacuna do produto, não uma regra organizacional nova.
## Evidência e próxima acção
Conferidos 21 ficheiros de autorização/regras/guardas da instalação publicada contra os locais por hash normalizado: todos iguais. O manifesto preserva a release verificada, identidade, âmbito, cada direito, fonte, utilização e se já existe. As consultas administrativas foram READ ONLY e não revelaram credenciais. Não foi usada uma sessão autenticada da conta para comandos de teste, nem se alterou a conta.
Depois de confirmar **este pacote de 82 direitos, mantendo as guardas e contextos acima**, aplicar numa operação única, revalidar o delta, comparar a navegação antes/depois e verificar a sessão pública. Ensaios financeiros continuam nas bases sintéticas, com segunda identidade para decisões independentes. Não há nesta proposta publicação, código funcional novo, alteração da página inicial, aceitação de módulos ou checkpoint de encerramento.
+4
View File
@@ -135,3 +135,7 @@ Uma futura migração operacional terá de resolver cadastros/IDs definitivos, c
Sem pagamentos, cobranças, afectações, devoluções, notas de crédito, redução/anulação financeira, lançamentos, efeitos adicionais de stock/orçamento ou activação real. Vencimento desconhecido é suportado; a revisão de uma data sustentada e planos de prestações ficam para recorte próprio. O importador SGF é um recibo de QA preparado pelos serviços, não um circuito geral de documentos em produção ou integração remota.
Próximo passo: a equipa testa as duas interfaces e relata função, origem/captura, passos e resultado observado; corrigir deltas sem apagar originais. Aceitação e checkpoint exclusivamente local só depois da decisão do responsável. Sem commit de encerramento, push ou publicação nesta entrega.
## Retoma da interface — 30/10/2026
A página de obrigações passou a mostrar afectações novas e saldo por liquidar da posição canónica, preservando o principal. Percurso HTTP/HTML repetido em Compras e SGF: 11 600 MZN → pagamento confirmado sem abatimento → afectação confirmada de 4 000 → saldo 7 600; vencimento/dias nulos. Provas anteriores preservadas. Resultados, correcções de navegação e bloqueio visual no [relatório de pagamentos](SUPPLIER_SETTLEMENT_DELIVERY.md), secção Consolidação do percurso. Sem habilitação pública, mudança de permissões ou encerramento presumido.
+37
View File
@@ -46,3 +46,40 @@ Estas tabelas **não constituem uma migração aplicada a desenvolvimento ou pro
- Fazer o ensaio visual curto nas duas demonstrações e recolher dificuldades da equipa. Depois decidir aceitação e eventual checkpoint exclusivamente local. Não foi criado checkpoint de encerramento.
Esta entrega complementa [obrigações a fornecedores](SUPPLIER_PAYABLE_DELIVERY.md); não substitui as aceitações e limitações anteriores.
## Consolidação do percurso — 30/10/2026
Retoma expressamente autorizada para corrigir utilização nas interfaces existentes, sem novo módulo, habilitação pública ou alteração da página inicial limpa. Esta secção complementa a entrega anterior; não declara aceitação ou encerramento.
**Correcções efectuadas:**
- Trocar função em pagamentos devolvia sempre às obrigações. A entrada QA conserva agora `next=payments`, limitado a destinos fixos permitidos. Destino externo é recusado; sem parâmetro, permanece o comportamento anterior. Sessão renovada e identidades/permissões revalidadas como antes.
- A página de obrigações mostrava o principal mas omitia saldo após afectações novas quando não existia histórico importado. Mostra agora principal preservado, afectações confirmadas e saldo da posição canónica existente, igual à página de pagamentos; nenhum segundo cálculo foi criado.
- Entrada e etapas explicam o percurso completo, alternância entre Preparar e Validar e a diferença entre confirmar pagamento e afectar dívida. Versão e validador ficam legíveis sem abrir JSON. «Saldo canónico» passa a «saldo por liquidar».
- Pagamentos totalmente afectados deixam de ser oferecidos para novas afectações, permanecendo em consulta/histórico. Não se alterou o comando no servidor nem se criou correcção/reversão.
- Detalhes de validação passam a ser apresentados com foco no erro; indicação de processamento e âncora da secção de resultado após sucesso. Repetição do mesmo conteúdo conserva a chave. JavaScript exercitado com DOM/fetch simulados em VM; não é execução visual em navegador.
| Verificação automática | Compras | SGF |
| --- | --- | --- |
| Factura efectivamente conferida e obrigação reconhecida | 11 600 MZN | 11 600 MZN |
| Pagamento proposto | 4 000 MZN | 4 000 MZN |
| Confirmação por outra identidade, antes de afectar | Dívida 11 600; por afectar 4 000 MZN | Dívida 11 600; por afectar 4 000 MZN |
| Proposta de afectação ainda não confirmada | Dívida 11 600 MZN | Dívida 11 600 MZN |
| Afectação confirmada por outra identidade | Saldo 7 600; por afectar zero | Saldo 7 600; por afectar zero |
| Repetição com novas chaves | Um pagamento e uma afectação | Um pagamento e uma afectação |
| Saída/reentrada em Consulta | Histórico/saldo recuperados | Histórico/saldo recuperados |
| Vencimento e dias de atraso | Nulos; atraso indeterminado | Nulos; atraso indeterminado |
| Ambas as páginas | Principal 11 600, afectado 4 000, saldo 7 600 | Principal 11 600, afectado 4 000, saldo 7 600 |
| Observação visual | Bloqueada | Bloqueada |
Prova nova: `docs/proofs/supplier-settlements-interface-20261030.json`. As páginas e disponibilidade dos formulários foram conferidas por HTTP/HTML; os comandos foram enviados aos mesmos endpoints protegidos dos formulários. Sintaxe do JavaScript renderizado e feedback/reutilização da chave/âncora foram verificados separadamente. Não se automatizaram cliques, disposição, scroll ou submissão num navegador real. A origem mantém base 10 000 e imposto documental 1 600, sem recálculo.
Repetição: `C:/laragon/bin/python/python-3.10/python.exe scripts/settlements-interface-run.py`, a partir de sgf-tempo. O runner verifica portos livres 8007/8008, provisiona bases/roles descartáveis, instala o schema só nessas bases, corre `settlements-interface-check.mjs` e remove os seus processos/bases/roles no finally. Não reinicia 8005/8006. Nessas demonstrações persistentes confirmou-se somente entrada/consulta/saída, ligações e perfil de consulta sem formulários: `scripts/settlements-persistent-access.mjs` e `docs/proofs/supplier-settlements-persistent-access-20261030.json`. Nenhum comando financeiro foi executado nos cenários persistentes; preservados os desenvolvimentos posteriores de Compras.
As provas anteriores não foram regravadas. O SHA do núcleo SupplierSettlements.php foi comparado com a prova anterior: igual. Motor e adaptadores de autorização/contexto não mudaram; as provas anteriores de concorrência, falha/recuperação e isolamento foram reutilizadas, sem repetir toda a bateria técnica. Sintaxe PHP, check existente de navegação/página inicial limpa e builds das duas aplicações conferidos nesta retoma.
Ficheiros de aplicação alterados: `packages/payables/ui/page.php`, `packages/payables/ui/settlements.php`, `scripts/payables-demo-entry.php`. Acrescentados os três scripts de verificação acima e provas/documentação. Sem dependências, permissões, schema habitual, dados reais, publicação, push ou checkpoint de encerramento.
**Bloqueio visual concreto:** a skill Computer Use inicializou, mas `sky.list_apps()` devolveu «Computer Use native pipe is unavailable: failed to connect native pipe (OS error 2)». A skill Browser indicada no catálogo não estava instalada no caminho fornecido. Não se contornaram os bloqueios nem se declararam ecrãs observados. Falta revisão em navegador, incluindo legibilidade, foco, scroll, ecrã estreito e mudança de função. Correcções entregues para revisão, sem aceitação presumida.
Mantêm-se os limites anteriores: sem bancos/caixa, correcções de afectações, novos pagamentos sobre obrigações importadas, reconciliação, contabilidade, stock ou orçamento adicional. Página inicial preservada sem cartões/resumos. O percurso não foi habilitado no domínio público.
@@ -0,0 +1 @@
{"applied": true, "final": 82, "company": 25, "unit": 57, "audit_id": 12, "existing_preserved": 6, "added": 76, "approved_manifest_sha256": "7f1aec6e47ea6558f81ace9400b78d9c5b7ae5dad7f238eafa9b1ed6cbde345c", "scope": {"tenant_id": 1, "company_id": 1, "unit_id": 1}, "target": "nala@tempo.co.mz", "backup": "/var/backups/sgf-functional-access-20261030/permissions-audit-before-final.dump", "backup_catalogue_and_sql_readable": true, "constraint_added_unit_names": ["purchasing_budget.approve", "purchasing_budget.consume", "purchasing_budget.create", "purchasing_budget.record", "purchasing_budget.reinforcement.propose", "purchasing_budget.reinforcement.review", "purchasing_budget.view", "supplier_payables.view", "workshop_corrections.apply", "workshop_corrections.request", "workshop_corrections.review", "workshop_corrections.view"], "wrong_scope_candidates_rejected": 82, "qa_guards_unchanged": true, "public_payables_enabled": false, "bank_execution": false, "real_fiscal_emission": false, "checked_at": "2026-10-30T20:37:46.477031+00:00", "release": "/var/www/sgf-deploy/releases/20261030-201723-2c0be7ac5e34", "initial_attempt_rolled_back": true, "restored_over_live_history": false}
@@ -0,0 +1,56 @@
{
"checked_at": "2026-10-30T20:38:58.433Z",
"manifest_matches_all_82": true,
"menus_before": [
"Início",
"Compras",
"Stock",
"Contabilidade",
"Configuração"
],
"menus_after": [
"Início",
"Comercial",
"Compras",
"Stock",
"Oficinas",
"Financeiro",
"Contabilidade",
"Configuração"
],
"commercial_contains_billing_and_invoices": true,
"duplicates": false,
"public_assets": [
{
"path": "/build/assets/app-ClqDwpNN.css",
"identical_local": true,
"sha256": "9a0dba687f88fed720a4ae34d9273d3b71f177ac7e3f46d1b0210cfe9f7f6a98"
},
{
"path": "/build/assets/app-DqvWPK3d.js",
"identical_local": true,
"sha256": "b042eb3d5fee8611474d44505b3cd56501dd82a1b84b1f5c36c6d30314e58172"
}
],
"demo_entries": [
{
"url": "http://127.0.0.1:8002/avaliar",
"status": 200
},
{
"url": "http://127.0.0.1:8003/",
"status": 200
},
{
"url": "http://127.0.0.1:8005/demo",
"status": 200
},
{
"url": "http://127.0.0.1:8006/demo",
"status": 200
}
],
"verification": "public HTTP/asset comparison, static navigation with effective grants, and server read-only probes; no interactive authenticated browser test",
"visual_review": false,
"public_test_writes": false
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
{
"checked_at": "2026-10-30T20:25:22.540Z",
"core_unchanged": true,
"core_sha256": "156bd40f3080de66a73c0befd975990e9d7010c2a232a86d751d4db3af4fae93",
"hosts": [
{
"host": "compras",
"passed": true,
"kind": "HTTP pages, form availability, submitted commands and JavaScript syntax; not browser execution",
"stages": [
{
"stage": "recognised",
"debt_minor": 1160000
},
{
"stage": "payment_confirmed_before_allocation",
"debt_minor": 1160000,
"unallocated_minor": 400000
},
{
"stage": "allocation_confirmed",
"debt_minor": 760000,
"unallocated_minor": 0
}
],
"principal_minor": 1160000,
"due_on": null,
"days_overdue": null,
"payments": 1,
"allocations": 1,
"repeat_recovered": true,
"history_after_reentry": true,
"canonical_balance_on_both_pages": true,
"role_destination_retained": true,
"external_redirect_refused": true,
"read_only_without_forms": true,
"visual_review": false
},
{
"host": "sgf",
"passed": true,
"kind": "HTTP pages, form availability, submitted commands and JavaScript syntax; not browser execution",
"stages": [
{
"stage": "recognised",
"debt_minor": 1160000
},
{
"stage": "payment_confirmed_before_allocation",
"debt_minor": 1160000,
"unallocated_minor": 400000
},
{
"stage": "allocation_confirmed",
"debt_minor": 760000,
"unallocated_minor": 0
}
],
"principal_minor": 1160000,
"due_on": null,
"days_overdue": null,
"payments": 1,
"allocations": 1,
"repeat_recovered": true,
"history_after_reentry": true,
"canonical_balance_on_both_pages": true,
"role_destination_retained": true,
"external_redirect_refused": true,
"read_only_without_forms": true,
"visual_review": false
}
],
"visual_blocker": "Computer Use native pipe is unavailable: failed to connect native pipe (OS error 2)",
"public_enabled": false,
"bank_execution": false,
"accepted_proof_preserved": true
}
@@ -0,0 +1,20 @@
{
"checked_at": "2026-10-30T20:25:31.633Z",
"hosts": [
{
"host": "compras",
"entry_and_links": true,
"payment_records": 0,
"obligations": 0,
"kind": "existing demo, query role only; no financial commands"
},
{
"host": "sgf",
"entry_and_links": true,
"payment_records": 0,
"obligations": 0,
"kind": "existing demo, query role only; no financial commands"
}
],
"visual_review": false
}
+1 -1
View File
@@ -13,7 +13,7 @@
<?php $history=json_decode($p->snapshot,true)['_history_import']??null; if($history): ?><p>Histórico proposto: confirmados <?= $money($history['confirmed_minor']) ?> · pendentes <?= $money($history['pending_minor']) ?> · rejeitados <?= $money($history['rejected_minor']) ?>. Os pendentes e rejeitados não reduzem o saldo.</p><?php endif ?>
<?php if($data['permissions']['validate']&&(!$history||($data['history_import_available']??false))): ?><form data-action="<?= $history?'validate_history':'validate' ?>"><input type="hidden" name="id" value="<?= $e($p->id) ?>"><label>Fundamento da decisão<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button><?= $history?'Validar passagem e preservar histórico':'Validar revisão exacta e reconhecer' ?></button></form><?php endif ?></article><?php endforeach ?></section>
<section><h2>Obrigações reconhecidas</h2><?php if(!$data['obligations']): ?><p>Nenhuma obrigação reconhecida. Proposta ou factura conferida não é decisão de reconhecimento.</p><?php endif ?><div class="grid">
<?php foreach($data['obligations'] as $o): ?><article><h3>Obrigação #<?= $e($o['id']) ?></h3><p class="amount"><?= $money($o['principal_minor']) ?></p><p>Principal reconhecido — vencimento pendente</p><?php if($o['history_imported']??false): ?><p>Pagamentos históricos confirmados: <?= $money($o['confirmed_payments_minor']) ?></p><p>Pendentes: <?= $money($o['pending_payments_minor']) ?> · Rejeitados: <?= $money($o['rejected_payments_minor']) ?> — sem abatimento.</p><p><strong>Saldo por liquidar: <?= $o['balance_minor']===null?'Pendente de revisão':$money($o['balance_minor']) ?></strong></p><?php if($data['history_decisions_available']??false): ?><p>Importação original preservada. A conferência independente dos pendentes actualiza este saldo uma única vez.</p><p><a href="/pagamentos">Conferir pagamentos pendentes →</a></p><?php else: ?><p>Importação original preservada. Conferência de pendentes não activada nesta instalação.</p><?php endif ?><?php endif ?><?php if($o['source_conflict']??false): ?><p role="alert">Origem ou histórico divergente; saldo não presumido. O principal original permanece preservado.</p><?php endif ?><p>Atraso indeterminado · Dias de atraso: sem valor</p><p>Validador #<?= $e($o['recognised_by']) ?> · <?= $e($o['created_at']) ?></p><details><summary>Origem, versão e decisão</summary><pre><?= $e(json_encode($o,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)) ?></pre></details></article><?php endforeach ?></div></section>
<?php foreach($data['obligations'] as $o): ?><article><h3>Obrigação #<?= $e($o['id']) ?></h3><p class="amount"><?= $money($o['principal_minor']) ?></p><p>Principal reconhecido — vencimento pendente</p><?php if($o['history_imported']??false): ?><p>Pagamentos históricos confirmados: <?= $money($o['confirmed_payments_minor']) ?></p><p>Pendentes: <?= $money($o['pending_payments_minor']) ?> · Rejeitados: <?= $money($o['rejected_payments_minor']) ?> — sem abatimento.</p><p><strong>Saldo por liquidar: <?= $o['balance_minor']===null?'Pendente de revisão':$money($o['balance_minor']) ?></strong></p><?php if($data['history_decisions_available']??false): ?><p>Importação original preservada. A conferência independente dos pendentes actualiza este saldo uma única vez.</p><p><a href="/pagamentos">Conferir pagamentos pendentes →</a></p><?php else: ?><p>Importação original preservada. Conferência de pendentes não activada nesta instalação.</p><?php endif ?><?php endif ?><?php if(!($o['history_imported']??false)): ?><p>Afectações confirmadas neste percurso: <?= $money($o['allocated_payments_minor']??0) ?></p><p><strong>Saldo por liquidar: <?= $o['balance_minor']===null?'Pendente de revisão':$money($o['balance_minor']) ?></strong></p><?php endif ?><?php if($o['source_conflict']??false): ?><p role="alert">Origem ou histórico divergente; saldo não presumido. O principal original permanece preservado.</p><?php endif ?><p>Atraso indeterminado · Dias de atraso: sem valor</p><p>Validador #<?= $e($o['recognised_by']) ?> · <?= $e($o['created_at']) ?></p><details><summary>Origem, versão e decisão</summary><pre><?= $e(json_encode($o,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)) ?></pre></details></article><?php endforeach ?></div></section>
<section><h2>Histórico durável</h2><?php foreach($data['events'] as $event): ?><details><summary>#<?= $e($event->id) ?> · <?= $e($event->kind) ?> · actor #<?= $e($event->actor_id) ?> · <?= $e($event->created_at) ?></summary><pre><?= $e($event->payload) ?></pre></details><?php endforeach ?></section>
<p>Sem execução de novos pagamentos, cobrança, efeitos de stock, novo consumo orçamental ou lançamento automático. A passagem de histórico não confirma pagamentos pendentes.</p>
</main><script>
+7 -7
View File
@@ -2,7 +2,7 @@
$e = static fn ($v) => htmlspecialchars((string) $v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$money = static fn ($v) => $v === null ? 'Indeterminado' : number_format(intdiv((int) $v, 100), 0, '.', ' ').'.'.str_pad((string) abs((int) $v % 100), 2, '0', STR_PAD_LEFT).' MZN';
$eligible = array_values(array_filter($data['obligations'], static fn ($o) => ! $o['source_conflict'] && ! $o['history_imported']));
$confirmed = array_values(array_filter($data['payments'], static fn ($p) => $p['state'] === 'confirmed'));
$confirmed = array_values(array_filter($data['payments'], static fn ($p) => $p['state'] === 'confirmed' && $p['unallocated_minor'] > 0));
$options = static function () use ($eligible, $e, $money): void {
foreach ($eligible as $o) echo '<option value="'.$e($o['id']).'" data-revision="'.$e($o['source_revision']).'">#'.$e($o['id']).' · '.$e($o['origin_snapshot']['supplier_name'] ?? 'Fornecedor').' · '.$e($money($o['balance_minor'])).'</option>';
};
@@ -10,14 +10,14 @@ $options = static function () use ($eligible, $e, $money): void {
<!doctype html><html lang="pt"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Pagamentos e afectações a fornecedores — ensaio</title><style>
body{font:17px system-ui;color:#172f29;background:#f4f6f5;margin:0}main{max-width:1100px;margin:auto;padding:24px}section,header{background:white;padding:24px;border:1px solid #dbe4df;border-radius:12px;margin:18px 0}h1{font-size:28px}h2{font-size:22px}p,li{line-height:1.6}label{display:block;margin:14px 0}input,textarea,select,button{font:inherit;padding:10px;max-width:100%}input,textarea,select{display:block;width:min(100%,600px);box-sizing:border-box}button{background:#125a4b;color:white;border:0;border-radius:7px;margin:6px;cursor:pointer}button:disabled{opacity:.5;cursor:wait}.notice{background:#fff3cf;padding:16px}a{color:#125a4b}.scroll{overflow:auto}table{border-collapse:collapse;width:100%}th,td{text-align:left;padding:12px;border-bottom:1px solid #ddd}pre{white-space:pre-wrap;overflow-wrap:anywhere}.line{border:1px solid #ddd;padding:16px;margin:12px 0}#error{color:#8d251e}article{border-top:1px solid #ddd;padding:16px 0}
</style></head><body><main>
<header><a href="<?= $e($settings['obligations_url']) ?>">← Obrigações a fornecedores</a> · <a href="/demo">Trocar função ou sair</a><p><?= $e($settings['host']) ?> · <?= $e($settings['actor']) ?></p><h1>Pagamentos e afectações a fornecedores</h1><p class="notice"><strong>ENSAIO SINTÉTICO — SEM MOVIMENTAÇÃO DE DINHEIRO.</strong> Propor não confirma; confirmar um pagamento não abate automaticamente uma dívida. Só uma afectação confirmada reduz o saldo. Não há execução bancária, reconciliação, stock, orçamento adicional ou lançamento contabilístico.</p><p>As referências de pagamento e evidência são artificiais e começam por SIM-. Não introduza dados bancários ou comprovativos reais.</p></header>
<p id="status" role="status"></p><p id="error" role="alert"></p><button id="reload">Actualizar consulta</button>
<section><h2>Obrigações e saldo canónico</h2><div class="scroll"><table><thead><tr><th>Origem / fornecedor</th><th>Principal</th><th>Afectado neste percurso</th><th>Saldo</th><th>Vencimento</th></tr></thead><tbody><?php foreach ($data['obligations'] as $o): ?><tr><td>#<?= $e($o['id']) ?> · <?= $e($o['origin_snapshot']['number'] ?? '') ?><br><?= $e($o['origin_snapshot']['supplier_name'] ?? '') ?><details><summary>Versão e decisão</summary><pre><?= $e(json_encode($o, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre></details></td><td><?= $money($o['principal_minor']) ?></td><td><?= $money($o['allocated_payments_minor']) ?></td><td><?= $money($o['balance_minor']) ?><?= $o['source_conflict'] ? ' — origem divergente' : '' ?></td><td>Pendente<br>Atraso indeterminado<br>Dias de atraso: sem valor</td></tr><?php endforeach ?></tbody></table></div><?php if (! $data['obligations']): ?><p>Reconheça primeiro a obrigação no percurso existente. Não é criada uma dívida por registar um pagamento.</p><?php endif ?></section>
<header><a href="<?= $e($settings['obligations_url']) ?>">← Obrigações a fornecedores</a> · <a href="/demo?next=payments">Trocar função ou sair</a><p><?= $e($settings['host']) ?> · <?= $e($settings['actor']) ?></p><h1>Pagamentos e afectações a fornecedores</h1><p class="notice"><strong>ENSAIO SINTÉTICO — SEM MOVIMENTAÇÃO DE DINHEIRO.</strong> Propor não confirma; confirmar um pagamento não abate automaticamente uma dívida. Só uma afectação confirmada reduz o saldo. Não há execução bancária, reconciliação, stock, orçamento adicional ou lançamento contabilístico.</p><p>As referências de pagamento e evidência são artificiais e começam por SIM-. Não introduza dados bancários ou comprovativos reais.</p></header>
<p id="status" role="status"></p><p id="error" role="alert" tabindex="-1"></p><button id="reload">Actualizar consulta</button>
<section id="obligation-balances"><h2>Obrigações e saldo por liquidar</h2><div class="scroll"><table><thead><tr><th>Origem / fornecedor</th><th>Principal</th><th>Afectado neste percurso</th><th>Saldo</th><th>Vencimento</th></tr></thead><tbody><?php foreach ($data['obligations'] as $o): ?><tr><td>#<?= $e($o['id']) ?> · <?= $e($o['origin_snapshot']['number'] ?? '') ?><br><?= $e($o['origin_snapshot']['supplier_name'] ?? '') ?><br>Versão <?= $e($o['source_revision']) ?> · Validador #<?= $e($o['recognised_by']) ?><details><summary>Versão e decisão</summary><pre><?= $e(json_encode($o, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre></details></td><td><?= $money($o['principal_minor']) ?></td><td><?= $money($o['allocated_payments_minor']) ?></td><td><?= $money($o['balance_minor']) ?><?= $o['source_conflict'] ? ' — origem divergente' : '' ?></td><td>Pendente<br>Atraso indeterminado<br>Dias de atraso: sem valor</td></tr><?php endforeach ?></tbody></table></div><?php if (! $data['obligations']): ?><p>Reconheça primeiro a obrigação no percurso existente. Não é criada uma dívida por registar um pagamento.</p><?php endif ?></section>
<?php if ($data['permissions']['payment_prepare'] && $eligible): ?><section><h2>1. Propor pagamento sintético</h2><form data-action="payment-prepare"><label>Obrigação de referência / fornecedor<select name="obligation_id" data-origin><?php $options(); ?></select></label><input type="hidden" name="source_revision"><label>Montante do pagamento (MZN)<input name="amount" required inputmode="decimal" pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="4000.00"></label><input type="hidden" name="currency" value="MZN"><label>Data factual do pagamento simulado<input type="date" name="paid_on" required></label><label>Referência estável do facto<input name="fact_reference" required pattern="SIM-[A-Z0-9][A-Z0-9._-]{2,95}" placeholder="SIM-PAY-001"></label><label>Evidência artificial de origem<input name="evidence_reference" required pattern="SIM-[A-Z0-9][A-Z0-9._-]{2,95}" placeholder="SIM-EVID-001"></label><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Propor sem afectar dívida</button></form></section><?php endif ?>
<section><h2>2. Pagamentos propostos e decisões</h2><?php foreach ($data['payments'] as $p): ?><article><h3>Pagamento #<?= $e($p['id']) ?> · <?= $e($p['fact_reference']) ?></h3><p><?= $money($p['amount_minor']) ?> · Data simulada <?= $e($p['paid_on']) ?> · Estado: <?= $e(['proposed'=>'Proposto','confirmed'=>'Confirmado em ensaio','rejected'=>'Rejeitado'][$p['state']]) ?></p><p>Por afectar: <?= $money($p['unallocated_minor']) ?> · Reconciliação bancária: não implementada.</p><details><summary>Preparação e evidência preservadas</summary><pre><?= $e($p['snapshot']) ?></pre><?php if ($p['decision']): ?><pre><?= $e(json_encode($p['decision'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre><?php endif ?></details>
<section id="payments"><h2>2. Pagamentos propostos e decisões</h2><p>A confirmação exige outra identidade. Depois de confirmar, troque para Preparar para propor a afectação; a dívida ainda não foi reduzida.</p><?php foreach ($data['payments'] as $p): ?><article><h3>Pagamento #<?= $e($p['id']) ?> · <?= $e($p['fact_reference']) ?></h3><p><?= $money($p['amount_minor']) ?> · Data simulada <?= $e($p['paid_on']) ?> · Estado: <?= $e(['proposed'=>'Proposto','confirmed'=>'Confirmado em ensaio','rejected'=>'Rejeitado'][$p['state']]) ?></p><p>Por afectar: <?= $money($p['unallocated_minor']) ?> · Reconciliação bancária: não implementada.</p><details><summary>Preparação e evidência preservadas</summary><pre><?= $e($p['snapshot']) ?></pre><?php if ($p['decision']): ?><pre><?= $e(json_encode($p['decision'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?></pre><?php endif ?></details>
<?php if ($data['permissions']['payment_confirm'] && ! $p['decision'] && (int) $p['prepared_by'] !== $data['current_actor']): ?><form data-action="payment-decide"><input type="hidden" name="payment_id" value="<?= $e($p['id']) ?>"><label>Decisão explícita<select name="decision"><option value="confirmed">Confirmar com evidência sintética</option><option value="rejected">Rejeitar</option></select></label><label>Evidência artificial da conferência<input name="evidence_reference" required pattern="SIM-[A-Z0-9][A-Z0-9._-]{2,95}" placeholder="SIM-CONF-001"></label><label>Fundamento da conferência<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Registar decisão independente</button></form><?php endif ?></article><?php endforeach ?><?php if (! $data['payments']): ?><p>Sem pagamentos propostos neste contexto.</p><?php endif ?></section>
<?php if ($data['permissions']['allocation_prepare'] && $confirmed && $eligible): ?><section><h2>3. Propor afectações</h2><form data-action="allocation-prepare"><label>Pagamento confirmado<select name="payment_id"><?php foreach ($confirmed as $p): ?><option value="<?= $e($p['id']) ?>">#<?= $e($p['id']) ?> · <?= $e($p['fact_reference']) ?> · Por afectar <?= $money($p['unallocated_minor']) ?></option><?php endforeach ?></select></label><label>Referência estável da afectação<input name="reference" required pattern="SIM-[A-Z0-9][A-Z0-9._-]{2,95}" placeholder="SIM-ALLOC-001"></label><div data-lines><div class="line" data-line><label>Obrigação<select data-obligation><?php $options(); ?></select></label><label>Valor a afectar (MZN)<input data-amount required inputmode="decimal" pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="4000.00"></label><button type="button" data-remove>Remover linha</button></div></div><button type="button" data-add>Adicionar outra obrigação</button><label>Fundamento<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><p>Mesmo fornecedor e moeda. O excedente fica por afectar. Não existe reembolso/crédito automático nem edição de uma afectação confirmada.</p><button>Submeter afectações sem abater saldo</button></form></section><?php endif ?>
<section><h2>4. Propostas de afectação</h2><?php foreach ($data['allocation_proposals'] as $p): ?><article><h3>Proposta #<?= $e($p['id']) ?> · <?= $e($p['reference']) ?> · Pagamento #<?= $e($p['payment_id']) ?></h3><p>Preparador #<?= $e($p['prepared_by']) ?> · <?= $e($p['reason']) ?></p><ul><?php foreach (json_decode($p['plan'], true) as $line): ?><li>Obrigação #<?= $e($line['obligation_id']) ?>: <?= $money($line['amount_minor']) ?> · versão <?= $e($line['source_revision']) ?></li><?php endforeach ?></ul><p>Estado: <?= $p['decision'] ? $e($p['decision']->decision === 'confirmed' ? 'Afectação confirmada' : 'Rejeitada') : 'Proposta — sem efeito no saldo' ?></p>
<section id="allocation-proposals"><h2>4. Propostas de afectação</h2><p>A proposta não reduz a dívida. Troque para Validar para confirmar a afectação com outra identidade.</p><?php foreach ($data['allocation_proposals'] as $p): ?><article><h3>Proposta #<?= $e($p['id']) ?> · <?= $e($p['reference']) ?> · Pagamento #<?= $e($p['payment_id']) ?></h3><p>Preparador #<?= $e($p['prepared_by']) ?> · <?= $e($p['reason']) ?></p><ul><?php foreach (json_decode($p['plan'], true) as $line): ?><li>Obrigação #<?= $e($line['obligation_id']) ?>: <?= $money($line['amount_minor']) ?> · versão <?= $e($line['source_revision']) ?></li><?php endforeach ?></ul><p>Estado: <?= $p['decision'] ? $e($p['decision']->decision === 'confirmed' ? 'Afectação confirmada' : 'Rejeitada') : 'Proposta — sem efeito no saldo' ?></p>
<?php if ($data['permissions']['allocation_confirm'] && ! $p['decision'] && (int) $p['prepared_by'] !== $data['current_actor']): ?><form data-action="allocation-decide"><input type="hidden" name="proposal_id" value="<?= $e($p['id']) ?>"><label>Decisão<select name="decision"><option value="confirmed">Confirmar estas afectações</option><option value="rejected">Rejeitar</option></select></label><label>Fundamento da decisão<textarea name="reason" required minlength="3" maxlength="2000"></textarea></label><button>Confirmar decisão e revalidar saldos</button></form><?php endif ?></article><?php endforeach ?></section>
<section><h2>Histórico durável</h2><?php foreach ($data['events'] as $event): ?><details><summary>#<?= $e($event->id) ?> · <?= $e($event->kind) ?> · actor #<?= $e($event->actor_id) ?> · <?= $e($event->created_at) ?></summary><pre><?= $e($event->payload) ?></pre></details><?php endforeach ?></section>
<p>Correcções, reversões, reconciliação e novos pagamentos sobre documentos com histórico importado ficam para âmbito próprio. Os originais e as decisões confirmadas não são editados ou apagados.</p>
@@ -27,6 +27,6 @@ document.querySelector('#reload').onclick=()=>location.reload();
for(const selector of document.querySelectorAll('[data-origin]')){const sync=()=>selector.form.elements.source_revision.value=selector.selectedOptions[0]?.dataset.revision??'';selector.onchange=sync;sync();}
for(const form of document.querySelectorAll('[data-action]')){
const add=form.querySelector('[data-add]');if(add){const lines=form.querySelector('[data-lines]');const template=lines.firstElementChild.cloneNode(true);add.onclick=()=>{if(lines.children.length<50)lines.append(template.cloneNode(true));};lines.onclick=e=>{if(e.target.matches('[data-remove]')&&lines.children.length>1)e.target.closest('[data-line]').remove();};}
form.onsubmit=async e=>{e.preventDefault();let body=Object.fromEntries(new FormData(form));for(const key of ['obligation_id','payment_id','proposal_id'])if(key in body)body[key]=Number(body[key]);if(form.dataset.action==='allocation-prepare')body.allocations=[...form.querySelectorAll('[data-line]')].map(line=>{const select=line.querySelector('[data-obligation]');return {obligation_id:Number(select.value),source_revision:select.selectedOptions[0]?.dataset.revision??'',amount:line.querySelector('[data-amount]').value};});const signature=JSON.stringify(body);if(form.dataset.signature!==signature){form.dataset.signature=signature;form.dataset.key=crypto.randomUUID();}const buttons=[...form.querySelectorAll('button')];buttons.forEach(b=>b.disabled=true);document.querySelector('#error').textContent='';try{const r=await fetch(settings.api+'/'+form.dataset.action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':settings.csrf,'X-SGF-Context':settings.context??'','Idempotency-Key':form.dataset.key},body:signature});const d=await r.json();if(!r.ok){document.querySelector('#error').textContent=d.message??'Não foi possível concluir; consulte o histórico.';return;}location.reload();}catch{document.querySelector('#error').textContent='Não foi possível confirmar o resultado. Actualize a consulta e confira o histórico antes de repetir.';}finally{buttons.forEach(b=>b.disabled=false);}};
form.onsubmit=async e=>{e.preventDefault();let body=Object.fromEntries(new FormData(form));for(const key of ['obligation_id','payment_id','proposal_id'])if(key in body)body[key]=Number(body[key]);if(form.dataset.action==='allocation-prepare')body.allocations=[...form.querySelectorAll('[data-line]')].map(line=>{const select=line.querySelector('[data-obligation]');return {obligation_id:Number(select.value),source_revision:select.selectedOptions[0]?.dataset.revision??'',amount:line.querySelector('[data-amount]').value};});const signature=JSON.stringify(body);if(form.dataset.signature!==signature){form.dataset.signature=signature;form.dataset.key=crypto.randomUUID();}const buttons=[...form.querySelectorAll('button')];buttons.forEach(b=>b.disabled=true);document.querySelector('#error').textContent='';document.querySelector('#status').textContent='A processar. Aguarde antes de repetir.';try{const r=await fetch(settings.api+'/'+form.dataset.action,{method:'POST',credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':settings.csrf,'X-SGF-Context':settings.context??'','Idempotency-Key':form.dataset.key},body:signature});const d=await r.json();if(!r.ok){document.querySelector('#error').textContent=Object.values(d.errors??{}).flat().join(' ')||d.message||'Não foi possível concluir; consulte o histórico.';document.querySelector('#error').focus();return;}location.hash=form.dataset.action==='allocation-decide'?'obligation-balances':form.dataset.action==='allocation-prepare'?'allocation-proposals':'payments';location.reload();}catch{document.querySelector('#error').textContent='Não foi possível confirmar o resultado. Actualize a consulta e confira o histórico antes de repetir.';document.querySelector('#error').focus();}finally{document.querySelector('#status').textContent='';buttons.forEach(b=>b.disabled=false);}};
}
</script></body></html>
+41
View File
@@ -0,0 +1,41 @@
import pathlib,subprocess,json,os,hashlib,datetime
plan=PLAN
expected=EXPECTED_HASHES
root=pathlib.Path('/var/www/html/sgf.tempo.co.mz')
for f,h in expected.items():
if hashlib.sha256((root/f).read_bytes()).hexdigest()!=h:raise RuntimeError('Authorisation source changed; stop before writing: '+f)
assert plan['target']['id']==1 and plan['target']['email']=='nala@tempo.co.mz' and len(plan['entries'])==82
php=r'''
chdir('/var/www/html/sgf.tempo.co.mz');require 'vendor/autoload.php';$p=json_decode(stream_get_contents(STDIN),true,64,JSON_THROW_ON_ERROR);$a=require 'bootstrap/cache/config.php';$c=$a['database']['connections'][$a['database']['default']];if($c['database']!=='sgf_db'||$a['app']['env']!=='production'||$a['app']['url']!=='https://sgf.tempo.co.mz')throw new Exception('Destination changed');
$db=new PDO('pgsql:host='.$c['host'].';port='.$c['port'].';dbname='.$c['database'],$c['username'],$c['password'],[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);$db->beginTransaction();$db->exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');$db->exec("SET LOCAL lock_timeout='5s'");$db->exec("SET LOCAL statement_timeout='30s'");$db->exec('SELECT pg_advisory_xact_lock(72644101)');$db->exec('LOCK TABLE permission_grants IN SHARE ROW EXCLUSIVE MODE');
$q=$db->query('select id,name,email,auth_version from users where id=1 for update');$u=$q->fetch(PDO::FETCH_ASSOC);if(!$u||$u['email']!=='nala@tempo.co.mz'||$u['name']!=='Osmane Nala')throw new Exception('Identity changed');
foreach(['tenants'=>['name'=>'Tempo'],'companies'=>['name'=>'Tempo, Lda.','tenant_id'=>1],'units'=>['name'=>'SEGI','tenant_id'=>1,'company_id'=>1]] as $t=>$fields){$row=$db->query('select * from '.$t.' where id=1 for update')->fetch(PDO::FETCH_ASSOC);foreach($fields as $k=>$v)if(!$row||$row[$k]!=$v)throw new Exception('Scope changed');}
if(!$db->query('select 1 from company_memberships where user_id=1 and tenant_id=1 and company_id=1')->fetchColumn())throw new Exception('Membership changed');
$allBefore=$db->query('select * from permission_grants order by id')->fetchAll(PDO::FETCH_ASSOC);$before=array_values(array_filter($allBefore,fn($g)=>$g['user_id']==1&&$g['tenant_id']==1&&$g['company_id']==1));if(count($before)!==6)throw new Exception('Expected six original rights; review changed state before retry');
$wanted=[];foreach($p['entries'] as $e){if($e['tenant_id']!==1||$e['company_id']!==1||!in_array($e['unit_id'],[null,1],true))throw new Exception('Out-of-scope manifest');$k=$e['permission'].'@'.($e['unit_id']??'company');if(isset($wanted[$k]))throw new Exception('Duplicate manifest right');$wanted[$k]=$e;}
foreach($before as $g){$k=$g['permission'].'@'.($g['unit_id']??'company');if(!isset($wanted[$k])||!$wanted[$k]['already_granted'])throw new Exception('Original grant changed');}
if(getenv('SGF_ACCESS_MODE')==='preview'){$db->rollBack();echo json_encode(['preview_passed'=>true,'existing'=>6,'expected_additions'=>76,'target'=>$u,'effective_database'=>'sgf_db']);exit;}
$tables=$db->query("select tablename from pg_tables where schemaname='public' order by tablename")->fetchAll(PDO::FETCH_COLUMN);$count=function()use($db,$tables){$r=[];foreach($tables as $t)$r[$t]=(int)$db->query('select count(*) from "'.str_replace('"','""',$t).'"')->fetchColumn();return $r;};$countsBefore=$count();$q=$db->prepare('insert into permission_grants (user_id,tenant_id,company_id,unit_id,permission,created_at,updated_at) values (1,1,1,?,?,now(),now()) returning id');$added=[];
foreach($wanted as $k=>$e){$matches=array_filter($before,fn($g)=>$g['permission']===$e['permission']&&($g['unit_id']===null?null:(int)$g['unit_id'])===$e['unit_id']);if($matches)continue;$q->execute([$e['unit_id'],$e['permission']]);$added[]=['id'=>(int)$q->fetchColumn(),'permission'=>$e['permission'],'unit_id'=>$e['unit_id']];}if(count($added)!==76)throw new Exception('Unexpected addition count');
$q=$db->prepare('insert into identity_audit (action,actor_id,tenant_id,company_id,unit_id,subject_id,details,created_at) values (?,?,?,?,?,?,?,now()) returning id');$q->execute(['identity.functional_access_package_granted',1,1,1,1,1,json_encode(['operator'=>'Tempo — confirmação expressa do pacote de 82 direitos em 30/10/2026','reason'=>'SGF-FUNCTIONAL-DEVELOPMENT-ACCESS-PROPOSAL-20261030; conservar seis direitos e acrescentar 76; sem levantar guardas fiscais, bancárias ou de QA; manifesto SHA256 '.$p['approved_manifest_sha256'],'grants'=>array_map(fn($e)=>['permission'=>$e['permission'],'unit_id'=>$e['unit_id']],$added)],JSON_THROW_ON_ERROR)]);$audit=(int)$q->fetchColumn();
$allAfter=$db->query('select * from permission_grants order by id')->fetchAll(PDO::FETCH_ASSOC);$byId=array_column($allAfter,null,'id');foreach($allBefore as $g)if(($byId[$g['id']]??null)!==$g)throw new Exception('Previous grant changed');$actual=array_values(array_filter($allAfter,fn($g)=>$g['user_id']==1&&$g['tenant_id']==1&&$g['company_id']==1));if(count($actual)!==82)throw new Exception('Final package size mismatch');foreach($actual as $g)if(!isset($wanted[$g['permission'].'@'.($g['unit_id']??'company')]))throw new Exception('Extra grant detected');
$afterCounts=$count();foreach($countsBefore as $t=>$n){$delta=$t==='permission_grants'?76:($t==='identity_audit'?1:0);if($afterCounts[$t]!==$n+$delta)throw new Exception('Unexpected table effect');}
$db->commit();echo json_encode(['applied'=>true,'database'=>'sgf_db','user'=>['id'=>1,'name'=>$u['name'],'email'=>$u['email']],'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'existing_preserved'=>6,'added'=>76,'final'=>82,'added_grants'=>$added,'audit_id'=>$audit,'all_previous_grants_preserved'=>true,'other_table_counts_preserved'=>true,'public_tables_checked'=>count($tables),'schema_changes'=>0,'credential_changes'=>0,'qa_guards_removed'=>false,'public_payables_enabled'=>false,'fiscal_real_enabled'=>false,'bank_execution_enabled'=>false]);
'''
body=dict(plan);body['approved_manifest_sha256']=MANIFEST_SHA
payload=json.dumps(body);env=dict(os.environ);env['SGF_ACCESS_MODE']='preview'
r=subprocess.run(['php','-r',php],input=payload,capture_output=True,text=True,env=env)
if r.returncode:raise RuntimeError('Readiness verification failed; no apply attempted; output withheld')
preview=json.loads(r.stdout)
if MODE=='preview':print(json.dumps(preview));raise SystemExit(0)
backup=pathlib.Path('/var/backups/sgf-functional-access-20261030');backup.mkdir(mode=0o700,exist_ok=True);os.chmod(backup,0o700);archive=backup/'permissions-audit-before.dump'
if archive.exists():raise RuntimeError('Existing operation backup; inspect receipt before any retry')
r=subprocess.run(['runuser','-u','postgres','--','pg_dump','-Fc','--dbname=sgf_db','-t','public.permission_grants','-t','public.identity_audit'],capture_output=True,check=True);fd=os.open(archive,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
with os.fdopen(fd,'wb') as f:f.write(r.stdout)
subprocess.run(['pg_restore','--list',str(archive)],capture_output=True,check=True)
# Validate restore syntax and content readability; do not restore over live history.
subprocess.run(['pg_restore','--file=-',str(archive)],capture_output=True,check=True)
env['SGF_ACCESS_MODE']='apply';r=subprocess.run(['php','-r',php],input=payload,capture_output=True,text=True,env=env)
if r.returncode:raise RuntimeError('Transaction refused/rolled back; inspect current grants before retry; sensitive output withheld')
receipt=json.loads(r.stdout);receipt.update(checked_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),manifest_sha256=MANIFEST_SHA,release=str(root.resolve()),backup=str(archive),backup_catalogue_and_sql_readable=True,restore_over_live_history=False)
f=backup/'receipt.json';f.write_text(json.dumps(receipt,ensure_ascii=False,indent=2));os.chmod(f,0o600);print(json.dumps(receipt))
@@ -0,0 +1,59 @@
import pathlib,json,subprocess,hashlib,os,datetime,re
plan=PLAN
expected=EXPECTED
root=pathlib.Path('/var/www/html/sgf.tempo.co.mz')
for f,h in expected.items():
if hashlib.sha256((root/f).read_bytes()).hexdigest()!=h:raise RuntimeError('Source changed; refuse')
def pg(sql):
r=subprocess.run(['runuser','-u','postgres','--','psql','-X','-At','-v','ON_ERROR_STOP=1','--dbname=sgf_db'],input=sql,capture_output=True,text=True)
if r.returncode:raise RuntimeError('Scoped SQL failed: '+r.stderr[-1200:])
return r.stdout
row=json.loads(pg("BEGIN READ ONLY; SELECT json_build_object('count',(select count(*) from permission_grants where user_id=1 and company_id=1 and tenant_id=1),'definition',pg_get_constraintdef(oid)) FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check';ROLLBACK;").splitlines()[1])
assert row['count']==6
old=row['definition'];assert old.startswith('CHECK (') and old.endswith(')');expression=old[7:-1] if old.startswith('CHECK ((') else old[7:-1]
# Strip exactly CHECK ( ... ), retaining balanced internal parentheses.
expression=old[len('CHECK ('):-1]
allowed=set(re.findall(r"'([a-z_]+\.[a-z_.]+)'",old));missing=sorted({e['permission'] for e in plan['entries']}-allowed)
expectedMissing=['purchasing_budget.approve','purchasing_budget.consume','purchasing_budget.create','purchasing_budget.record','purchasing_budget.reinforcement.propose','purchasing_budget.reinforcement.review','purchasing_budget.view','supplier_payables.view','workshop_corrections.apply','workshop_corrections.request','workshop_corrections.review','workshop_corrections.view']
assert missing==sorted(expectedMissing),'Unexpected constraint delta'
q=lambda x:"'"+str(x).replace("'","''")+"'"
new_expression='('+expression+') OR (permission IN ('+','.join(q(x) for x in missing)+') AND unit_id IS NOT NULL)'
values=','.join('('+q(e['permission'])+','+('NULL::bigint' if e['unit_id'] is None else '1::bigint')+')' for e in plan['entries'])
wrong=','.join('('+q(e['permission'])+','+('1::bigint' if e['unit_id'] is None else 'NULL::bigint')+')' for e in plan['entries'])
assert pg('SELECT bool_and('+new_expression+') FROM (VALUES '+values+') AS p(permission,unit_id);').strip()=='t'
assert pg('SELECT bool_and(NOT ('+new_expression+')) FROM (VALUES '+wrong+') AS p(permission,unit_id);').strip()=='t'
backup=pathlib.Path('/var/backups/sgf-functional-access-20261030');backup.mkdir(mode=0o700,exist_ok=True);os.chmod(backup,0o700)
archive=backup/'permissions-audit-before-final.dump'
if archive.exists():raise RuntimeError('Final backup already exists; inspect receipt before retry')
r=subprocess.run(['runuser','-u','postgres','--','pg_dump','-Fc','--dbname=sgf_db','-t','public.permission_grants','-t','public.identity_audit'],capture_output=True,check=True)
with os.fdopen(os.open(archive,os.O_CREAT|os.O_EXCL|os.O_WRONLY,0o600),'wb') as f:f.write(r.stdout)
subprocess.run(['pg_restore','--list',str(archive)],capture_output=True,check=True);subprocess.run(['pg_restore','--file=-',str(archive)],capture_output=True,check=True)
oldFile=backup/'constraint-before.sql';oldFile.write_text('ALTER TABLE public.permission_grants ADD CONSTRAINT permission_grants_scope_check '+old+';\n');os.chmod(oldFile,0o600)
manifest=json.dumps([{'permission':e['permission'],'unit_id':e['unit_id']} for e in plan['entries']]);detail=json.dumps({'operator':'Tempo — confirmação expressa de 82 direitos em 30/10/2026','reason':'Pacote aprovado; acrescentar 76 e preservar seis. Adaptação estrita da lista de 12 direitos aprovados ausentes no catálogo; sem tabelas/guardas QA ou operações fiscais/bancárias. Manifesto SHA256 '+MANIFEST_SHA,'grants':json.loads(manifest)})
sql='''BEGIN ISOLATION LEVEL REPEATABLE READ;
SET LOCAL lock_timeout='5s';SET LOCAL statement_timeout='30s';
SELECT pg_advisory_xact_lock(72644101);
LOCK TABLE permission_grants IN ACCESS EXCLUSIVE MODE;
CREATE TEMP TABLE intended_access ON COMMIT DROP AS SELECT * FROM jsonb_to_recordset('''+q(manifest)+'''::jsonb) AS x(permission text,unit_id bigint);
CREATE TEMP TABLE access_before ON COMMIT DROP AS SELECT * FROM permission_grants;
CREATE TEMP TABLE audit_before_count ON COMMIT DROP AS SELECT count(*) AS n FROM identity_audit;
DO $guard$ BEGIN
IF (SELECT count(*) FROM intended_access)<>82 OR (SELECT count(*) FROM permission_grants WHERE user_id=1 AND tenant_id=1 AND company_id=1)<>6 THEN RAISE EXCEPTION 'Changed grant state'; END IF;
IF NOT EXISTS(SELECT 1 FROM users WHERE id=1 AND email='nala@tempo.co.mz' AND name='Osmane Nala') OR NOT EXISTS(SELECT 1 FROM tenants WHERE id=1 AND name='Tempo') OR NOT EXISTS(SELECT 1 FROM companies WHERE id=1 AND tenant_id=1 AND name='Tempo, Lda.') OR NOT EXISTS(SELECT 1 FROM units WHERE id=1 AND tenant_id=1 AND company_id=1 AND name='SEGI') OR NOT EXISTS(SELECT 1 FROM company_memberships WHERE user_id=1 AND tenant_id=1 AND company_id=1) THEN RAISE EXCEPTION 'Identity/scope changed'; END IF;
IF EXISTS(SELECT 1 FROM permission_grants g WHERE g.user_id=1 AND g.tenant_id=1 AND g.company_id=1 AND NOT EXISTS(SELECT 1 FROM intended_access x WHERE x.permission=g.permission AND x.unit_id IS NOT DISTINCT FROM g.unit_id)) THEN RAISE EXCEPTION 'Unapproved existing grant'; END IF;
END $guard$;
ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check;
ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK ('''+new_expression+''');
INSERT INTO permission_grants(user_id,tenant_id,company_id,unit_id,permission,created_at,updated_at)
SELECT 1,1,1,x.unit_id,x.permission,now(),now() FROM intended_access x WHERE NOT EXISTS(SELECT 1 FROM permission_grants g WHERE g.user_id=1 AND g.tenant_id=1 AND g.company_id=1 AND g.permission=x.permission AND g.unit_id IS NOT DISTINCT FROM x.unit_id);
INSERT INTO identity_audit(action,actor_id,tenant_id,company_id,unit_id,subject_id,details,created_at) VALUES ('identity.functional_access_package_granted',1,1,1,1,1,'''+q(detail)+'''::jsonb,now());
DO $verify$ BEGIN
IF (SELECT count(*) FROM permission_grants WHERE user_id=1 AND tenant_id=1 AND company_id=1)<>82 OR (SELECT count(*) FROM permission_grants)<>(SELECT count(*)+76 FROM access_before) OR (SELECT count(*) FROM identity_audit)<>(SELECT n+1 FROM audit_before_count) THEN RAISE EXCEPTION 'Unexpected delta'; END IF;
IF EXISTS(SELECT * FROM access_before EXCEPT SELECT * FROM permission_grants) THEN RAISE EXCEPTION 'Previous grants modified'; END IF;
IF EXISTS(SELECT 1 FROM permission_grants g WHERE g.user_id=1 AND g.tenant_id=1 AND g.company_id=1 AND NOT EXISTS(SELECT 1 FROM intended_access x WHERE x.permission=g.permission AND x.unit_id IS NOT DISTINCT FROM g.unit_id)) THEN RAISE EXCEPTION 'Unapproved resulting grant'; END IF;
END $verify$;
COMMIT;
SELECT json_build_object('applied',true,'final',(select count(*) from permission_grants where user_id=1 and tenant_id=1 and company_id=1),'company',(select count(*) from permission_grants where user_id=1 and tenant_id=1 and company_id=1 and unit_id is null),'unit',(select count(*) from permission_grants where user_id=1 and tenant_id=1 and company_id=1 and unit_id=1),'audit_id',(select max(id) from identity_audit where action='identity.functional_access_package_granted'));
'''
output=pg(sql);receipt=json.loads(output.strip().splitlines()[-1]);receipt.update(existing_preserved=6,added=76,approved_manifest_sha256=MANIFEST_SHA,scope={'tenant_id':1,'company_id':1,'unit_id':1},target='nala@tempo.co.mz',backup=str(archive),backup_catalogue_and_sql_readable=True,constraint_added_unit_names=missing,wrong_scope_candidates_rejected=82,qa_guards_unchanged=True,public_payables_enabled=False,bank_execution=False,real_fiscal_emission=False,checked_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),release=str(root.resolve()),initial_attempt_rolled_back=True,restored_over_live_history=False)
f=backup/'receipt.json';f.write_text(json.dumps(receipt,ensure_ascii=False,indent=2));os.chmod(f,0o600);print(json.dumps(receipt))
+7
View File
@@ -0,0 +1,7 @@
import fs from 'node:fs/promises';import assert from 'node:assert/strict';import{createServer}from'vite';import{createHash}from'node:crypto';import React from'react';import{renderToStaticMarkup}from'react-dom/server';
const plan=JSON.parse(await fs.readFile('docs/SGF_FUNCTIONAL_ACCESS_PROPOSAL_20261030.json','utf8'));const check=JSON.parse(await fs.readFile('docs/proofs/functional-access-server-checks-20261030.json','utf8'));assert.equal(check.grants.length,82);for(const e of plan.entries)assert(check.grants.some(g=>g.permission===e.permission&&g.unit_id===e.unit_id));assert(Object.values(check.checks).every(v=>v.status==='ok'));assert(Object.values(check.guards).every(v=>v.refused===true&&v.status===403));
const server=await createServer({server:{middlewareMode:true},appType:'custom'});let before,after,nav;
try{const{navigationGroups,navigationTopics,default:Navigation}=await server.ssrLoadModule('/resources/js/WorkspaceNavigation.jsx');const canFrom=gs=>(p,u=null)=>gs.some(g=>g.permission===p&&g.unit_id===u);before=navigationTopics(navigationGroups(canFrom(plan.entries.filter(e=>e.already_granted)),1)).map(t=>t.label);const groups=navigationGroups(canFrom(check.grants),1);const topics=navigationTopics(groups);after=topics.map(t=>t.label);assert.deepEqual(after,['Início','Comercial','Compras','Stock','Oficinas','Financeiro','Contabilidade','Configuração']);assert(topics.find(t=>t.id==='commercial').sections.some(g=>g.id==='billing'&&g.items.some(i=>i.id==='service_invoices')));const ids=groups.flatMap(g=>g.items.map(i=>i.id));assert.equal(new Set(ids).size,ids.length);nav=renderToStaticMarkup(React.createElement(Navigation,{groups,page:'home',navigate:()=>{},user:{name:'Conta autorizada',email:'nala@tempo.co.mz'},busy:false,logout:()=>{}}));assert(nav.includes('topic-commercial'));}finally{await server.close();}
const root=await fetch('https://sgf.tempo.co.mz/',{signal:AbortSignal.timeout(15000)});assert.equal(root.status,200);const html=await root.text();const urls=[...new Set([...html.matchAll(/(?:src|href)="([^"]*\/build\/[^" ]+)"/g)].map(m=>m[1]))];const assets=[];for(const url of urls){const r=await fetch(url,{signal:AbortSignal.timeout(15000)});assert.equal(r.status,200);const bytes=Buffer.from(await r.arrayBuffer());const local=await fs.readFile('public'+new URL(url).pathname);assert(bytes.equals(local));assets.push({path:new URL(url).pathname,identical_local:true,sha256:createHash('sha256').update(bytes).digest('hex')});}
const demos=[];for(const url of ['http://127.0.0.1:8002/avaliar','http://127.0.0.1:8003/','http://127.0.0.1:8005/demo','http://127.0.0.1:8006/demo']){const r=await fetch(url,{signal:AbortSignal.timeout(10000)});demos.push({url,status:r.status});assert.equal(r.status,200);}
const proof={checked_at:new Date().toISOString(),manifest_matches_all_82:true,menus_before:before,menus_after:after,commercial_contains_billing_and_invoices:true,duplicates:false,public_assets:assets,demo_entries:demos,verification:'public HTTP/asset comparison, static navigation with effective grants, and server read-only probes; no interactive authenticated browser test',visual_review:false,public_test_writes:false};await fs.writeFile('docs/proofs/functional-access-navigation-20261030.json',JSON.stringify(proof,null,2)+'\n');console.log(JSON.stringify({menus:after,account_rights:82,server_checks:Object.keys(check.checks).length,guards_preserved:check.guards,demo_entries:demos}));
+9 -7
View File
@@ -20,23 +20,25 @@ $guardDemo = static function (): void {
Route::middleware('web')->group(function () use ($guardDemo, $isSgf, $destination) {
Route::get('/demo', function (Request $r) use ($guardDemo, $isSgf, $destination) {
$guardDemo();
$next = $r->query('next') === 'payments' ? 'payments' : 'obligations';
$continue = $next === 'payments' ? '/supplier-payments' : $destination;
$e = static fn ($v) => htmlspecialchars((string) $v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$title = $isSgf ? 'SGF — demonstração' : 'Compras — demonstração';
$html = '<!doctype html><html lang="pt"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>'.$title.'</title><style>body{font:18px system-ui;background:#f3f6f5;color:#142b28;margin:0}main{max-width:850px;margin:auto;padding:30px}section{background:white;padding:24px;border-radius:12px;margin:20px 0}button,a{font:inherit}button{padding:14px;background:#125a4b;color:white;border:0;border-radius:8px;cursor:pointer}form{margin:18px 0}a{color:#125a4b}</style><main><h1>'.$title.'</h1><p>Entre para experimentar e identificar melhorias. Não precisa de conta nem de palavra-passe.</p><section><h2>Escolha a função que quer experimentar</h2><p>Todos os dados são sintéticos. A função escolhida determina as acções disponíveis.</p>';
foreach (['preparacao' => ['Preparar', 'Consultar a factura e propor a obrigação.'], 'validacao' => ['Validar', 'Rever a proposta preparada por outra identidade sintética.'], 'consulta' => ['Consultar', 'Explorar valores, origem e histórico, sem alterar dados.']] as $role => [$label, $description]) {
$html .= '<form method="post" action="/demo/entrar"><input type="hidden" name="_token" value="'.$e(csrf_token()).'"><input type="hidden" name="role" value="'.$role.'"><button>'.$label.'</button> '.$description.'</form>';
foreach (['preparacao' => ['Preparar', 'Consultar a factura e propor a obrigação, o pagamento sintético ou a afectação.'], 'validacao' => ['Validar', 'Rever e decidir propostas preparadas por outra identidade sintética.'], 'consulta' => ['Consultar', 'Explorar valores, origem e histórico, sem alterar dados.']] as $role => [$label, $description]) {
$html .= '<form method="post" action="/demo/entrar"><input type="hidden" name="_token" value="'.$e(csrf_token()).'"><input type="hidden" name="role" value="'.$role.'"><input type="hidden" name="next" value="'.$next.'"><button>'.$label.'</button> '.$description.'</form>';
}
$html .= '</section><section><h2>O que experimentar</h2><p>Factura: base de 10 000 MZN + imposto documental de 1 600 MZN = 11 600 MZN. Preparar, trocar para validar e consultar o resultado. Repetir deve recuperar a mesma obrigação.</p><p>O vencimento permanece pendente e o atraso indeterminado. Este percurso não executa pagamentos.</p><p>As funções usam identidades de demonstração partilhadas; não identificam a pessoa real que está a testar. O cenário e o histórico são partilhados e conservados entre visitas.</p></section>';
$html .= '</section><section><h2>O que experimentar</h2><p>Factura: base de 10 000 MZN + imposto documental de 1 600 MZN = 11 600 MZN. Preparar, trocar para validar e consultar o resultado. Repetir deve recuperar a mesma obrigação.</p><p>Depois, em Pagamentos e afectações: preparar 4 000 MZN, confirmar com outra identidade e conferir que a dívida continua em 11 600 MZN. Só a afectação confirmada de 4 000 MZN reduz o saldo para 7 600 MZN.</p><p>O vencimento permanece pendente e o atraso indeterminado. Não há execução bancária. Datas e referências do ensaio devem ser sintéticas; o cenário partilhado conserva as operações anteriores, sem reiniciar os valores a cada visita.</p><p>As funções usam identidades de demonstração partilhadas; não identificam a pessoa real que está a testar. O cenário e o histórico são partilhados e conservados entre visitas.</p></section>';
if ($r->user()) {
$html .= '<p>Sessão actual: '.$e($r->user()->name).' · <a href="'.$destination.'">Continuar o percurso</a> · <a href="/">Abrir a aplicação</a></p><form method="post" action="/demo/sair"><input type="hidden" name="_token" value="'.$e(csrf_token()).'"><button>Sair da demonstração</button></form>';
$html .= '<p>Sessão actual: '.$e($r->user()->name).' · <a href="'.$continue.'">Continuar o percurso</a> · <a href="/">Abrir a aplicação</a></p><form method="post" action="/demo/sair"><input type="hidden" name="_token" value="'.$e(csrf_token()).'"><button>Sair da demonstração</button></form>';
}
return response($html.'</main></html>')->header('Cache-Control', 'no-store')->header('X-Robots-Tag', 'noindex, nofollow');
});
Route::post('/demo/entrar', function (Request $r) use ($guardDemo, $isSgf, $destination) {
$guardDemo();
$d = $r->validate(['role' => 'required|in:preparacao,validacao,consulta']);
abort_if(array_diff(array_keys($r->all()), ['role', '_token']) !== [], 422);
$d = $r->validate(['role' => 'required|in:preparacao,validacao,consulta', 'next' => 'sometimes|in:payments,obligations']);
abort_if(array_diff(array_keys($r->all()), ['role', '_token', 'next']) !== [], 422);
// No arbitrary user identifier or permissions may be supplied by a visitor.
$u = User::where('email', $d['role'].'-obrigacoes@example.test')->firstOrFail();
$grants = DB::table('pa_access')->where(['user_id' => $u->id, 'action' => 'view'])->get();
@@ -58,7 +60,7 @@ Route::middleware('web')->group(function () use ($guardDemo, $isSgf, $destinatio
$r->session()->put('company_context', ['tenant_id' => (int) $g->tenant_id, 'company_id' => (int) $g->company_id, 'unit_id' => (int) $g->unit_id, 'token' => bin2hex(random_bytes(24))]);
\App\Modules\Identity\Services\Audit::record('demo.entered', $u->id, $g->tenant_id, $g->company_id, $g->unit_id, details: ['role' => $d['role'], 'synthetic' => true]);
}
return redirect($destination);
return redirect(($d['next'] ?? '') === 'payments' ? '/supplier-payments' : $destination);
})->middleware('throttle:30,1');
Route::post('/demo/sair', function (Request $r) use ($guardDemo) {
+62
View File
@@ -0,0 +1,62 @@
import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
import {randomUUID,createHash} from 'node:crypto';
import vm from 'node:vm';
const hosts=[];
async function checkFormFeedback(html) {
const form={dataset:{action:'payment-prepare'},querySelector:()=>null,querySelectorAll:()=>[]};
const error={textContent:'',focused:false,focus(){this.focused=true;}},status={textContent:''},reload={},location={hash:'',reloads:0,reload(){this.reloads++;}};
let fail=true;const keys=[];
const sandbox={document:{querySelector:s=>({'#error':error,'#status':status,'#reload':reload}[s]),querySelectorAll:s=>s==='[data-action]'?[form]:[]},FormData:class extends Map{constructor(){super([['obligation_id','1'],['amount','4000.00']]);}},crypto:{randomUUID},location,fetch:async(url,options)=>{keys.push(options.headers['Idempotency-Key']);return {ok:!fail,json:async()=>fail?{message:'Dados inválidos',errors:{amount:['Use um montante sustentado.']}}:{payment_id:1}};}};
vm.runInNewContext(html.match(/<script>([\s\S]*?)<\/script>/)[1],sandbox);
await form.onsubmit({preventDefault(){}});assert.equal(error.textContent,'Use um montante sustentado.');assert(error.focused);assert.equal(status.textContent,'');assert.equal(location.reloads,0);
fail=false;await form.onsubmit({preventDefault(){}});assert.equal(keys[0],keys[1]);assert.equal(location.hash,'payments');assert.equal(location.reloads,1);
}
for(const mode of ['compras','sgf']) {
const base=`http://127.0.0.1:${mode==='compras'?8007:8008}`;
const obligations=mode==='compras'?'/obrigacoes-fornecedores':'/supplier-payables';
function client(){const cookies=new Map();let csrf='',context='';return {
async request(path,method='GET',body,form=false,key){
const r=await fetch(base+path,{redirect:'manual',signal:AbortSignal.timeout(20000),method,headers:{Connection:'close',Accept:'application/json','Content-Type':form?'application/x-www-form-urlencoded':'application/json',Cookie:[...cookies].map(([k,v])=>`${k}=${v}`).join('; '),'X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body===undefined?{}:{body:form?new URLSearchParams(body):JSON.stringify(body)})});
for(const c of r.headers.getSetCookie()){const p=c.split(';',1)[0],i=p.indexOf('=');cookies.set(p.slice(0,i),p.slice(i+1));}
const html=await r.text();let data;try{data=JSON.parse(html);}catch{}
csrf=html.match(/name="_token" value="([^"]+)"/)?.[1]??data?.csrf??csrf;
const embedded=html.match(/const settings=(.*);/);if(embedded){const s=JSON.parse(embedded[1]);csrf=s.csrf;context=s.context??context;new vm.Script(html.match(/<script>([\s\S]*?)<\/script>/)[1]);}
if(data?.context?.token)context=data.context.token;
return {status:r.status,location:r.headers.get('location'),html,data};
},
async enter(role,next='obligations'){const page=await this.request('/demo?next='+next);assert.equal(page.status,200);assert(page.html.includes(`name="next" value="${next}"`));const r=await this.request('/demo/entrar','POST',{role,next,_token:csrf},true);assert.equal(r.status,302);assert.equal(new URL(r.location,base).pathname,next==='payments'?'/supplier-payments':obligations);if(mode==='sgf')assert.equal((await this.request('/api/session')).status,200);},
async page(path,action){const r=await this.request(path);assert.equal(r.status,200);if(action)assert(r.html.includes(`data-action="${action}"`),mode+' missing form '+action);return r.html;},
async submit(api,action,body){const r=await this.request(api+'/'+action,'POST',body,false,randomUUID());assert.equal(r.status,200,mode+' '+action+' '+r.html.slice(0,180));return r.data;},
async position(){const r=await this.request('/api/supplier-payments');assert.equal(r.status,200);return r.data;},
async leave(){await this.page('/demo');const r=await this.request('/demo/sair','POST',{_token:csrf},true);assert.equal(r.status,302);}
};}
const p=client(),v=client(),q=client();await p.enter('preparacao');await v.enter('validacao');await q.enter('consulta');
let html=await p.page(obligations,'prepare');const form=html.match(/<form data-action="prepare">([\s\S]*?)<\/form>/)[1];const sourceId=Number(form.match(/<option value="(\d+)"/)[1]);
const proposal=await p.submit('/api/supplier-payables','prepare',{id:sourceId,reason:'Percurso sintético completo pelas páginas de ensaio'});
await v.page(obligations,'validate');await v.submit('/api/supplier-payables','validate',{id:proposal.proposal_id,reason:'Conferência independente da origem e valores documentais'});
let data=await q.position();let obligation=data.obligations[0];assert.equal(obligation.principal_minor,1160000);assert.equal(obligation.balance_minor,1160000);assert.equal(obligation.due_on,null);assert.equal(obligation.days_overdue,null);
const stages=[{stage:'recognised',debt_minor:obligation.balance_minor}];
await p.enter('preparacao','payments');html=await p.page('/supplier-payments','payment-prepare');assert(html.includes('/demo?next=payments'));assert(html.includes('Versão '+obligation.source_revision));
await checkFormFeedback(html);
const paymentBody={obligation_id:obligation.id,source_revision:obligation.source_revision,amount:'4000.00',currency:'MZN',paid_on:new Date().toISOString().slice(0,10),fact_reference:'SIM-INTERFACE-PAY-001',evidence_reference:'SIM-INTERFACE-EVID-001',reason:'Pagamento sintético de 4 000 MZN sem execução bancária'};
const payment=await p.submit('/api/supplier-payments','payment-prepare',paymentBody);assert.equal((await p.submit('/api/supplier-payments','payment-prepare',paymentBody)).payment_id,payment.payment_id);
await v.enter('validacao','payments');await v.page('/supplier-payments','payment-decide');const decision={payment_id:payment.payment_id,decision:'confirmed',evidence_reference:'SIM-INTERFACE-CONF-001',reason:'Conferência independente de evidência artificial'};
await v.submit('/api/supplier-payments','payment-decide',decision);data=await q.position();assert.equal(data.obligations[0].balance_minor,1160000);assert.equal(data.payments[0].unallocated_minor,400000);stages.push({stage:'payment_confirmed_before_allocation',debt_minor:1160000,unallocated_minor:400000});
html=await q.page(obligations);assert(/Saldo por liquidar: 11 600\.00 MZN/.test(html));
await p.page('/supplier-payments','allocation-prepare');const plan={payment_id:payment.payment_id,reference:'SIM-INTERFACE-ALLOC-001',allocations:[{obligation_id:obligation.id,source_revision:obligation.source_revision,amount:'4000.00'}],reason:'Afectação explícita sem segundo efeito na obrigação'};
const allocation=await p.submit('/api/supplier-payments','allocation-prepare',plan);assert.equal((await q.position()).obligations[0].balance_minor,1160000);
await v.page('/supplier-payments','allocation-decide');const confirmation={proposal_id:allocation.proposal_id,decision:'confirmed',reason:'Validação independente do saldo e afectação'};await v.submit('/api/supplier-payments','allocation-decide',confirmation);
assert.equal((await p.submit('/api/supplier-payments','allocation-prepare',plan)).proposal_id,allocation.proposal_id);await v.submit('/api/supplier-payments','allocation-decide',confirmation);
data=await q.position();obligation=data.obligations[0];assert.equal(obligation.principal_minor,1160000);assert.equal(obligation.balance_minor,760000);assert.equal(obligation.due_on,null);assert.equal(obligation.days_overdue,null);assert.equal(data.payments.length,1);assert.equal(data.allocations.length,1);assert.equal(data.payments[0].unallocated_minor,0);stages.push({stage:'allocation_confirmed',debt_minor:760000,unallocated_minor:0});
html=await q.page('/supplier-payments');assert(html.includes('7 600.00 MZN'));assert(html.includes('Atraso indeterminado'));assert(html.includes('Dias de atraso: sem valor'));assert(!html.includes('data-action='));
html=await q.page(obligations);assert(/Saldo por liquidar: 7 600\.00 MZN/.test(html));assert(html.includes('11 600.00 MZN'));assert(html.includes('4 000.00 MZN'));assert(html.includes('Atraso indeterminado'));
await p.page('/supplier-payments');assert(!(await p.page('/supplier-payments')).includes('data-action="allocation-prepare"'),'Exhausted payment should not be offered for new allocation');
await q.leave();await q.enter('consulta','payments');data=await q.position();assert.equal(data.obligations[0].balance_minor,760000);assert.equal(data.allocations.length,1);assert(data.events.length>=4);
await q.page('/demo?next=payments');const bad=await q.request('/demo/entrar','POST',{role:'consulta',next:'https://example.test'},true);assert.equal(bad.status,422);
await p.leave();await v.leave();await q.leave();
hosts.push({host:mode,passed:true,kind:'HTTP pages, form availability, submitted commands and JavaScript syntax; not browser execution',stages,principal_minor:1160000,due_on:null,days_overdue:null,payments:1,allocations:1,repeat_recovered:true,history_after_reentry:true,canonical_balance_on_both_pages:true,role_destination_retained:true,external_redirect_refused:true,read_only_without_forms:true,visual_review:false});
console.log(mode+': full synthetic sequence passed; single payment/allocation, debt 11600 before allocation and 7600 after.');
}
const core=await fs.readFile('packages/payables/src/SupplierSettlements.php');const coreSha=createHash('sha256').update(core).digest('hex');const accepted=JSON.parse(await fs.readFile('docs/proofs/supplier-settlements.json','utf8'));assert(accepted.hosts.every(h=>h.core_sha256===coreSha),'Financial core changed since accepted proof');
await fs.writeFile('docs/proofs/supplier-settlements-interface-20261030.json',JSON.stringify({checked_at:new Date().toISOString(),core_unchanged:true,core_sha256:coreSha,hosts,visual_blocker:'Computer Use native pipe is unavailable: failed to connect native pipe (OS error 2)',public_enabled:false,bank_execution:false,accepted_proof_preserved:true},null,2)+'\n');
+23
View File
@@ -0,0 +1,23 @@
import os,socket,subprocess,time,json,pathlib
root=pathlib.Path('C:/laragon/www/sgf-tempo');os.chdir(root)
php='C:/Users/macbook/.config/investmoz/php-8.5.11/php.exe';node='C:/laragon/tools/sgf-tempo/node-v24.22.1-win-arm64/node.exe';processes=[];created=False
for port in [8007,8008]:
s=socket.socket();s.settimeout(.3);busy=s.connect_ex(('127.0.0.1',port))==0;s.close()
if busy:raise RuntimeError('Proof port occupied; no process stopped')
try:
subprocess.run([php,'scripts/payables-provision.php','--proof'],check=True);created=True
for mode,port in [('compras',8007),('sgf',8008)]:
settings=pathlib.Path(f'C:/laragon/data/sgf-tempo/secrets/payables-proof-{mode}.json');config=json.loads(settings.read_text(encoding='utf-8-sig'));env=dict(os.environ);env['PAYABLE_SETTINGS_FILE']=str(settings).replace('\\','/')
executable=php if mode=='compras' else 'C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe'
p=subprocess.Popen([executable,'-S',f'127.0.0.1:{port}','-t','public',str(root/'scripts/payables-router.php')],cwd=config['HOST_ROOT'],env=env,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,creationflags=subprocess.CREATE_NO_WINDOW);processes.append(p)
for attempt in range(60):
s=socket.socket();s.settimeout(.2);ready=s.connect_ex(('127.0.0.1',port))==0;s.close()
if ready:break
time.sleep(.1)
if not ready:raise RuntimeError('Disposable HTTP host failed')
subprocess.run([php,'scripts/settlements-install.php',mode,'proof'],check=True)
subprocess.run([node,'scripts/settlements-interface-check.mjs'],check=True)
finally:
for p in processes:
if p.poll() is None:p.terminate();p.wait(timeout=15)
if created:subprocess.run([php,'scripts/payables-provision.php','--cleanup-proof'],check=True)
@@ -0,0 +1,4 @@
import fs from 'node:fs/promises';import assert from 'node:assert/strict';import vm from 'node:vm';import{randomUUID}from'node:crypto';
const s=await fs.readFile('scripts/settlements-interface-check.mjs','utf8');const definition=s.slice(s.indexOf(' function client()'),s.indexOf(' const p=client()'));const hosts=[];
for(const mode of ['compras','sgf']){const base=`http://127.0.0.1:${mode==='compras'?8005:8006}`,obligations=mode==='compras'?'/obrigacoes-fornecedores':'/supplier-payables';const make=eval('('+definition.trim().replace(/^function client/,'function')+')');const c=make();await c.enter('consulta','payments');const page=await c.page('/supplier-payments');assert(page.includes('/demo?next=payments'));assert(!page.includes('data-action='));const data=await c.position();await c.page(obligations);hosts.push({host:mode,entry_and_links:true,payment_records:data.payments.length,obligations:data.obligations.length,kind:'existing demo, query role only; no financial commands'});await c.leave();}
await fs.writeFile('docs/proofs/supplier-settlements-persistent-access-20261030.json',JSON.stringify({checked_at:new Date().toISOString(),hosts,visual_review:false},null,2)+'\n');console.log('Existing demonstration pages passed; no financial commands.');