diff --git a/README.md b/README.md index f3ae4a6..51649ad 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ Crie um pedido, guarde o rascunho, confira os valores e submeta. Entre com as co ## Entrega actual +- Registo de pagamentos externos por empresa, com conferência independente, + liquidação parcial/total de facturas e revalidação da versão bancária. Sem + consumo duplicado do orçamento ou execução de transferências. Migração 000012 + aplicada após backup/restauro verificados; três cenários concorrentes PostgreSQL + aprovados. Interface `/pagamentos`; demonstração `/consulta/pagamentos`. + - Dados bancários por fornecedor/empresa: versões propostas por Procurement, duas validações distintas por Finanças, dados sensíveis cifrados, consultas restritas e auditoria sem números de conta. Alterações exigem duas novas @@ -40,7 +46,7 @@ Crie um pedido, guarde o rascunho, confira os valores e submeta. Entre com as co Migração 000009 aplicada após backup e restauro verificados; cinco cenários concorrentes PostgreSQL aprovados. Sem dotações fictícias na base real. Centros sem dotação mantêm o circuito de desenvolvimento sem controlo. - Reforços e pagamentos continuam pendentes; não constitui validação + Reforços e estornos continuam pendentes; não constitui validação operacional integral do orçamento. Interface em `/orcamentos`. - Equipamentos e peças ligados aos artigos existentes, compatibilidades, criticidade configurada, mínimos explícitos e alertas, saídas rastreáveis e validação técnica. Finanças é obrigatória para equipamento em qualquer valor; a adjudicação/emissão de investimento continua bloqueada até definição do limite próprio. Migração 000008 aplicada no lote 9 após backup recuperável e verificação da preservação. Demonstração em `/consulta/equipamentos`. @@ -75,7 +81,7 @@ O IVA é informado pelo utilizador e calculado no servidor. A taxa inicial de 0% ## Próximas etapas -Revisão visual dos módulos entregues; reposição de stock; registo de pagamentos externos; reforços orçamentais, correcções de facturas e pagamentos; moedas e câmbio; subscrições, licenças e Docker. Equipamentos, peças, validação técnica e alertas de mínimo já estão implementados; a adjudicação de equipamento aguarda a definição da regra de investimento. Periodicidade dos inventários, tolerâncias e investigação de divergências exigem decisão da empresa. A política de periodicidade, validade e levantamento de conflitos e os procedimentos de revisão/cancelamento de adjudicações continuam por definir. +Revisão visual dos módulos entregues; reposição de stock; conciliação de pagamentos externos; reforços orçamentais, correcções de facturas e estornos; moedas e câmbio; subscrições, licenças e Docker. Equipamentos, peças, validação técnica e alertas de mínimo já estão implementados; a adjudicação de equipamento aguarda a definição da regra de investimento. Periodicidade dos inventários, tolerâncias e investigação de divergências exigem decisão da empresa. A política de periodicidade, validade e levantamento de conflitos e os procedimentos de revisão/cancelamento de adjudicações continuam por definir. O cadastro guarda referências e ficheiros anexos no disco privado `supplier_documents`, em `storage/app/supplier-documents`, fora da pasta pública e sem URLs públicas. As fichas abrem pelo directório **Fornecedores → Ver ficha → Abrir ficha, edição e documentos**. As versões antigas e documentos existentes são preservados na auditoria e no armazenamento; nesta entrega não há eliminação de documentos, antivírus, quotas acumuladas ou política de retenção. A validação do NUIT é de formato e unicidade, sem consulta a uma autoridade fiscal. Além das declarações por operação, existem declarações pessoais por fornecedor e processo; a periodicidade anual e a política de validade/levantamento ainda não foram definidas. As propostas e avaliações registadas não são editáveis nesta etapa. Os pesos 40/20/15/15/10 são provisórios e ficam guardados por proposta. A comparação liga à fase de adjudicação e emissão. A ordem exige novas aprovações do valor final e revalidação no servidor. As propostas usam MZN; a multimoeda fica para a fase correspondente. diff --git a/app/Http/Controllers/ExternalPaymentController.php b/app/Http/Controllers/ExternalPaymentController.php new file mode 100644 index 0000000..6a3e4a8 --- /dev/null +++ b/app/Http/Controllers/ExternalPaymentController.php @@ -0,0 +1,57 @@ +user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403); + $canManage = $request->user()->purchasing_role === PurchasingRole::Finance; + $payments = ExternalPayment::query()->latest('id')->paginate(15)->through(fn ($payment) => [...$payment->toArray(), 'external_reference' => $canManage ? $payment->external_reference : null, 'evidence_reference' => $canManage ? $payment->evidence_reference : null]); + if ($canManage && $payments->count() > 0) { + app(RequisitionWorkflow::class)->audit($request->user(), 'payment.viewed', null, null, ['payment_ids' => $payments->pluck('id')->all()]); + } + $invoices = Invoice::query()->where('status', 'validated')->latest('id')->get()->map(function (Invoice $invoice) use ($workflow) { + $supplier = Supplier::query()->findOrFail($invoice->supplier_id); + $bank = SupplierBankDetail::query()->where('supplier_id', $supplier->id)->latest('revision')->first(); + + return ['id' => $invoice->id, 'number' => $invoice->number, 'supplier_id' => $supplier->id, 'supplier_name' => $supplier->name, 'total_minor' => $invoice->total_minor, 'outstanding_minor' => $workflow->outstanding($invoice), 'bank_id' => $bank->id ?? 0, 'bank_revision' => $bank->revision ?? 0, 'bank_state' => app(SupplierBankWorkflow::class)->state($supplier)]; + }); + Inertia::encryptHistory(); + $response = Inertia::render('payments/index', ['invoices' => $invoices, 'payments' => $payments, 'canManage' => $canManage])->toResponse($request); + $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); + + return $response; + } + + public function store(Request $request, Invoice $invoice, ExternalPaymentWorkflow $workflow): RedirectResponse + { + abort_unless($request->user()->purchasing_role === PurchasingRole::Finance, 403); + $data = $request->validate(['supplier_bank_detail_id' => 'required|integer|min:1', 'amount' => ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/'], 'paid_on' => 'required|date_format:Y-m-d', 'external_reference' => 'required|string|max:150', 'evidence_reference' => 'required|string|max:1000']); + $workflow->register($request->user(), $invoice, $data); + + return to_route('payments.index'); + } + + public function review(Request $request, ExternalPayment $payment, ExternalPaymentWorkflow $workflow): RedirectResponse + { + $data = $request->validate(['decision' => 'required|in:confirmed,rejected', 'reason' => 'required|string|max:2000', 'independently_verified' => 'accepted']); + $workflow->review($request->user(), $payment, $data['decision'], $data['reason']); + + return back(); + } +} diff --git a/app/Models/ExternalPayment.php b/app/Models/ExternalPayment.php new file mode 100644 index 0000000..e295541 --- /dev/null +++ b/app/Models/ExternalPayment.php @@ -0,0 +1,42 @@ + $bank_snapshot + */ +class ExternalPayment extends Model +{ + use BelongsToTenant; + + protected $guarded = ['id']; + + protected $attributes = ['status' => 'pending']; + + protected $hidden = ['external_reference', 'reference_hash', 'payload_hash', 'evidence_reference', 'bank_snapshot', 'review_reason']; + + protected function casts(): array + { + return ['external_reference' => 'encrypted', 'evidence_reference' => 'encrypted', 'bank_snapshot' => 'encrypted:array', 'review_reason' => 'encrypted']; + } + + protected static function booted(): void + { + static::updating(function (ExternalPayment $payment) { + if ($payment->getRawOriginal('status') !== 'pending' || array_diff(array_keys($payment->getDirty()), ['status', 'reviewed_by', 'reviewed_at', 'review_reason', 'updated_at']) !== []) { + throw new \LogicException('Preservar o registo externo e a conferência.'); + } + }); + static::deleting(fn () => throw new \LogicException('Preservar o histórico de pagamentos externos.')); + } +} diff --git a/app/Services/ConflictGuard.php b/app/Services/ConflictGuard.php index 71931c5..1badcef 100644 --- a/app/Services/ConflictGuard.php +++ b/app/Services/ConflictGuard.php @@ -86,6 +86,7 @@ class ConflictGuard 'award.created', 'award.approved', 'award.rejected', 'purchase_order.issued', 'equipment.technical_approved', 'equipment.technical_rejected', 'invoice.created', 'invoice.validated', + 'payment.registered', 'payment.confirmed', 'payment.rejected', ])->pluck('user_id')) ->unique()->values()->all(); $ids = []; diff --git a/app/Services/ExternalPaymentWorkflow.php b/app/Services/ExternalPaymentWorkflow.php new file mode 100644 index 0000000..d96ca88 --- /dev/null +++ b/app/Services/ExternalPaymentWorkflow.php @@ -0,0 +1,136 @@ +id() === $user->id && $user->tenant_id === $tenantId && $user->purchasing_role === PurchasingRole::Finance, 403); + } + + private function invalid(string $message): never + { + throw ValidationException::withMessages(['payment' => $message]); + } + + public function outstanding(Invoice $invoice): int + { + return (int) $invoice->total_minor - (int) ExternalPayment::query()->where('invoice_id', $invoice->id)->where('status', 'confirmed')->sum('amount_minor'); + } + + private function bank(User $user, Invoice $invoice, int $bankId, int $amount): SupplierBankDetail + { + if ($invoice->status !== 'validated' || $amount <= 0 || $amount > $this->outstanding($invoice)) { + $this->invalid('Factura não validada ou montante superior ao saldo pendente.'); + } + $order = PurchaseOrder::query()->findOrFail($invoice->purchase_order_id); + $request = Requisition::query()->findOrFail($order->requisition_id); + app(ConflictGuard::class)->assertProcess($request, $order->supplier_id, $user); + $supplier = Supplier::query()->lockForUpdate()->findOrFail($order->supplier_id); + $bank = SupplierBankDetail::query()->where('supplier_id', $supplier->id)->latest('revision')->first(); + if ($bank) { + User::query()->where('tenant_id', $user->tenant_id)->whereIn('id', SupplierBankReview::query()->where('supplier_bank_detail_id', $bank->id)->select('reviewed_by'))->orderBy('id')->lockForUpdate()->get(); + } + if (! $bank || $bank->id !== $bankId || app(SupplierBankWorkflow::class)->state($supplier) !== 'approved') { + $this->invalid('A versão bancária actual deve ter duas validações elegíveis. Actualize a ficha; não reutilize uma versão substituída.'); + } + if ($bank->currency !== $order->currency || $order->currency !== 'MZN') { + $this->invalid('A moeda da conta deve coincidir com a factura. Câmbio não suportado.'); + } + + return $bank; + } + + /** @param array $data */ + public function register(User $user, Invoice $invoice, array $data): ExternalPayment + { + $this->actor($user, $invoice->tenant_id); + + return DB::transaction(function () use ($user, $invoice, $data) { + app(ConflictGuard::class)->lockCompany($user); + $user = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->findOrFail($user->id); + $this->actor($user, $invoice->tenant_id); + $invoice = Invoice::query()->lockForUpdate()->findOrFail($invoice->id); + $reference = Str::upper(Str::squish($data['external_reference'])); + $amount = BigDecimal::of((string) $data['amount'])->multipliedBy(100)->toScale(0)->toInt(); + $canonical = ['invoice_id' => $invoice->id, 'bank_id' => (int) $data['supplier_bank_detail_id'], 'amount_minor' => $amount, 'paid_on' => $data['paid_on'], 'evidence_reference' => trim($data['evidence_reference'])]; + $hash = hash_hmac('sha256', json_encode($canonical, JSON_THROW_ON_ERROR), config('app.key')); + $referenceHash = hash_hmac('sha256', $reference, config('app.key')); + $existing = ExternalPayment::query()->where('reference_hash', $referenceHash)->first(); + if ($existing) { + if ($existing->created_by !== $user->id || $existing->payload_hash !== $hash) { + $this->invalid('Referência externa já registada. Consulte a conferência existente.'); + } + + return $existing; + } + if ($reference === '' || trim($data['evidence_reference']) === '' || $data['paid_on'] < (string) $invoice->issued_on || $data['paid_on'] > today()->format('Y-m-d') || substr($data['paid_on'], 0, 4) !== now()->format('Y')) { + $this->invalid('Referência, evidência e data válida do exercício actual são obrigatórias.'); + } + $bank = $this->bank($user, $invoice, (int) $data['supplier_bank_detail_id'], $amount); + $payment = ExternalPayment::query()->create(['tenant_id' => $user->tenant_id, 'invoice_id' => $invoice->id, 'supplier_bank_detail_id' => $bank->id, 'created_by' => $user->id, 'amount_minor' => $amount, 'currency' => $bank->currency, 'paid_on' => $data['paid_on'], 'external_reference' => $reference, 'reference_hash' => $referenceHash, 'payload_hash' => $hash, 'evidence_reference' => trim($data['evidence_reference']), 'bank_snapshot' => ['bank_id' => $bank->id, 'revision' => $bank->revision, 'bank_name' => $bank->bank_name, 'country' => $bank->country, 'currency' => $bank->currency, 'beneficiary' => $bank->beneficiary, 'account_identifier' => $bank->account_identifier, 'reviewer_ids' => SupplierBankReview::query()->where('supplier_bank_detail_id', $bank->id)->pluck('reviewed_by')->all(), 'development_only' => true, 'bank_transfer_executed' => false]]); + $this->audit($user, $invoice, $payment, 'payment.registered'); + + return $payment; + }); + } + + public function review(User $user, ExternalPayment $payment, string $decision, string $reason): void + { + $this->actor($user, $payment->tenant_id); + abort_if($payment->created_by === $user->id, 403); + if (! in_array($decision, ['confirmed', 'rejected']) || trim($reason) === '') { + $this->invalid('Decisão e fundamento obrigatórios.'); + } + DB::transaction(function () use ($user, $payment, $decision, $reason) { + app(ConflictGuard::class)->lockCompany($user); + $user = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->findOrFail($user->id); + $this->actor($user, $payment->tenant_id); + $invoice = Invoice::query()->lockForUpdate()->findOrFail($payment->invoice_id); + $payment = ExternalPayment::query()->lockForUpdate()->findOrFail($payment->id); + if ($payment->status !== 'pending') { + if ($payment->status !== $decision) { + $this->invalid('Conferência já decidida; não pode ser reescrita.'); + } + + return; + } + $order = PurchaseOrder::query()->findOrFail($invoice->purchase_order_id); + app(ConflictGuard::class)->assertProcess(Requisition::query()->findOrFail($order->requisition_id), $order->supplier_id, $user); + if ($decision === 'confirmed') { + if (substr((string) $payment->paid_on, 0, 4) !== now()->format('Y')) { + $this->invalid('Conferência entre exercícios não suportada.'); + } + $this->bank($user, $invoice, $payment->supplier_bank_detail_id, $payment->amount_minor); + $author = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->find($payment->created_by); + if (! $author || $author->purchasing_role !== PurchasingRole::Finance) { + $this->invalid('O autor do registo deixou de ser elegível.'); + } + } + $payment->update(['status' => $decision, 'reviewed_by' => $user->id, 'review_reason' => $reason, 'reviewed_at' => now()]); + $this->audit($user, $invoice, $payment, 'payment.'.$decision); + }); + } + + private function audit(User $user, Invoice $invoice, ExternalPayment $payment, string $action): void + { + $order = PurchaseOrder::query()->findOrFail($invoice->purchase_order_id); + $request = Requisition::query()->findOrFail($order->requisition_id); + app(RequisitionWorkflow::class)->audit($user, $action, $request, null, ['payment_id' => $payment->id, 'invoice_id' => $invoice->id, 'bank_detail_id' => $payment->supplier_bank_detail_id, 'amount_minor' => $payment->amount_minor, 'status' => $payment->status, 'bank_transfer_executed' => false]); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index e6ddabf..2bfa639 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -24,7 +24,7 @@ return Application::configure(basePath: dirname(__DIR__)) ]); }) ->withExceptions(function (Exceptions $exceptions): void { - $exceptions->dontFlash(['account_identifier', 'beneficiary', 'evidence_reference', 'reason']); + $exceptions->dontFlash(['account_identifier', 'beneficiary', 'external_reference', 'evidence_reference', 'reason']); $exceptions->shouldRenderJsonWhen( fn (Request $request) => $request->is('api/*') || $request->expectsJson(), ); diff --git a/database/migrations/2026_10_28_000012_create_external_payments.php b/database/migrations/2026_10_28_000012_create_external_payments.php new file mode 100644 index 0000000..8766f0d --- /dev/null +++ b/database/migrations/2026_10_28_000012_create_external_payments.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('tenant_id')->constrained()->restrictOnDelete(); + $table->foreignId('invoice_id')->constrained()->restrictOnDelete(); + $table->foreignId('supplier_bank_detail_id')->constrained()->restrictOnDelete(); + $table->foreignId('created_by')->constrained('users')->restrictOnDelete(); + $table->foreignId('reviewed_by')->nullable()->constrained('users')->restrictOnDelete(); + $table->string('status')->default('pending'); + $table->bigInteger('amount_minor'); + $table->string('currency', 3); + $table->date('paid_on'); + $table->text('external_reference'); + $table->string('reference_hash', 64); + $table->string('payload_hash', 64); + $table->text('evidence_reference'); + $table->text('bank_snapshot'); + $table->text('review_reason')->nullable(); + $table->timestamp('reviewed_at')->nullable(); + $table->timestamps(); + $table->unique(['tenant_id', 'reference_hash']); + }); + } + + public function down(): void + { + throw new LogicException('Preservar registos e conferências de pagamentos externos.'); + } +}; diff --git a/docs/compatibilidade.md b/docs/compatibilidade.md index 2b354fe..90fa248 100644 --- a/docs/compatibilidade.md +++ b/docs/compatibilidade.md @@ -6,7 +6,7 @@ Estado actualizado em 28 de Outubro de 2026. Projecto activo: C:\laragon\www\com | ------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Acesso e organização | Fortify, conta, empresa, equipa, departamentos e centros de custo | MFA obrigatório e ciclo completo de acessos | | Requisições e aprovações | Rascunhos, cálculos exactos, etapas, segregação e snapshots | Validação empresarial das regras provisórias | -| Fornecedores | Cadastro, anexos privados e versões bancárias com duas validações | Integração futura com pagamentos | +| Fornecedores | Cadastro, anexos privados e versões bancárias com duas validações | Múltiplas contas e verificação bancária externa | | Cotações | Propostas, avaliações e matriz comparativa | Multimoeda e revisão das propostas | | RF-AF-01 | Declarações pessoais, histórico e ConflictGuard no servidor | Política de periodicidade, validade e levantamento | | Adjudicação | Escolha fundamentada, revalidação e aprovação do valor final | Excepções, revisão e cancelamento sem política ficam bloqueados | @@ -15,7 +15,7 @@ Estado actualizado em 28 de Outubro de 2026. Projecto activo: C:\laragon\www\com | Stock | Artigos, armazéns, entradas, saídas fundamentadas, transferências atómicas, contagens e ajustes auditados por empresa | Revisão visual, reposição e conversões | | Equipamentos e peças | Cadastro, compatibilidade, mínimos, alertas, validação técnica e rastreabilidade | Regra de investimento; adjudicação/emissão de equipamento bloqueadas | | Orçamento | Dotação por empresa/centro/exercício, aprovação independente, reservas e compromissos | Revisão visual, reforços e validação operacional integral | -| Facturas e pagamentos | Registo por empresa, conferência, validação independente e consumo orçamental | Pagamentos, notas de crédito e anexos | +| Facturas e pagamentos | Facturas, consumo e registo/conferência de pagamentos externos | Estornos, notas de crédito e anexos | | Início e navegação | Página inicial própria, menus por área/perfil e consulta pública por módulo | Revisão visual | | Auditoria | Histórico das operações implementadas | Exportações, retenção e protecção contra SQL administrativo | @@ -23,6 +23,16 @@ Preservar enums, políticas, modelos relacionais, Brick Math e isolamento por em ## Verificação actual +Registo e conferência de pagamentos externos entregues, com liquidação parcial, +independência, revalidação bancária e consumo orçamental sem duplicação. +Suite actual: **347 aprovados, 3 ignorados e 2.630 asserções**. +Migração 000012 aplicada no lote 13 após backup e restauro verificados de 44 +tabelas. Três cenários concorrentes PostgreSQL e 33 testes específicos aprovados. +Tabela de pagamentos externos vazia na base real. Não executa transferências. +Reforços, estornos, investimento e revisão visual continuam pendentes. + +### Histórico da entrega de dados bancários + Dados bancários entregues: versões por empresa/fornecedor, duas decisões independentes de Finanças, campos sensíveis cifrados e consultas restritas. Migração 000011 no lote 12 após backup e restauro verificados de 42 tabelas. diff --git a/docs/desenvolvimento.md b/docs/desenvolvimento.md index 841ad97..adbc17b 100644 --- a/docs/desenvolvimento.md +++ b/docs/desenvolvimento.md @@ -1,5 +1,77 @@ # Desenvolvimento do SGC +## Registo e conferência de pagamentos externos + +Implementado `/pagamentos`, por empresa. Finanças regista pagamentos já +efectuados fora do SGC, com factura validada, montante explícito, data efectiva, +referência externa e evidência. Outra pessoa de Finanças confirma ou rejeita a +conferência. O sistema **não executa transferências, remessas ou ordens bancárias**. + +- Apenas os registos confirmados abatem a dívida da factura. Liquidação parcial + ou total é calculada a partir desse histórico; não reescreve a factura validada. + Pendentes/rejeitados não abatem o saldo. A confirmação não lança novamente + consumo ou compromisso orçamental: o orçamento foi consumido na factura. +- Montante deve ser positivo e caber no saldo confirmado ainda pendente. Duas + confirmações concorrentes não podem exceder a factura. Referência normalizada + única por empresa; repetição idêntica pelo autor devolve o registo existente, + enquanto dados diferentes para a mesma referência são recusados. +- Registo e confirmação revalidam factura, fornecedor, conflitos, moeda e + versão bancária mais recente com duas validações elegíveis. Versão alterada, + fornecedor suspenso, perda de perfil ou conflito posterior bloqueiam confirmação. + Registos históricos já confirmados não são reescritos por mudanças posteriores. +- Uma operação externa corresponde a uma factura nesta etapa. Datas entre + exercícios, câmbio, adiantamentos, estornos, reembolsos, anulação de confirmados + e repartição da mesma operação por várias facturas não estão implementados. + Uma referência de evidência não é upload de comprovativo nem verificação bancária. +- Referência externa, evidência, fundamento e snapshot bancário são cifrados. + Snapshot preserva versão, conta e validadores do momento do registo; não é + exposto na listagem. Administração consulta metadados; Finanças consulta + referências/evidência com auditoria. Outros perfis não acedem ao módulo. + Auditoria contém IDs, montantes e estados, sem referências externas nem números + bancários. Histórico Inertia cifrado, resposta sem cache e dados sensíveis + excluídos de input antigo de sessão. Preservar a chave de aplicação no restauro. +- Modelos impedem edição do registo e eliminação. Decisão final é imutável; + falha na auditoria reverte registo ou confirmação. Protecção contra SQL + administrativo directo continua dependente das permissões da base de dados. +- `/consulta/pagamentos` contém apenas exemplos fictícios. Valores operacionais + vêm das facturas da própria empresa e do montante introduzido; não há valores + financeiros predefinidos para clientes. Moeda suportada nesta etapa: MZN, + coincidente com a conta; sem autorização implícita para conversões. + +### Verificação e migração + +Suite completa actual: **347 aprovados, 3 ignorados, 2.630 asserções**. +PHPStan, Pint, TypeScript, compilação e formatação/lint frontend aprovados. +O serviço recarrega o perfil do operador depois do bloqueio da empresa e +bloqueia os registos dos validadores bancários durante a conferência. + +33 testes específicos aprovados, 107 asserções: liquidação parcial/total, limites, idempotência, +versões bancárias, perfis, conflitos, moedas, isolamento, cifragem, privacidade, +input não guardado, ausência de consumo duplicado, auditoria e rollback. +Três cenários concorrentes PostgreSQL aprovados com duas ligações independentes +observadas simultaneamente à espera de bloqueios: excesso, mesma confirmação +e pagamentos complementares. Saldos e manutenção do consumo orçamental conferidos; +base temporária eliminada. São testes de serviço, não de carga ou HTTP. +[Evidência](pagamentos-concorrencia-postgresql.json). + +Migração 000012 aplicada no **lote 13**, após backup custom PostgreSQL 17 de +**239.843 bytes** e restauro verificado das **44 tabelas** anteriores numa base +temporária, depois eliminada. Snapshot consistente para dump e manifesto. +Registos de negócio, restrições e `.env` preservados. Tabela de pagamentos +externos vazia; sem seeds, contas novas, alterações de credenciais ou pagamentos +fictícios na base real. Backup e manifesto privados, fora do Git. +[Evidência sem dados privados](pagamentos-migracao-000012.json). + +### Pendências e próximo incremento + +O navegador continua indisponível (`The browser access host is unavailable`). +**Revisão visual pública, autenticada e de impressão permanece pendente.** +Este incremento não constitui integração bancária, confirmação bancária automática +ou validação operacional integral do orçamento. Documentos continuam de desenvolvimento. +Próximo incremento: reforços orçamentais auditados, propostos e aprovados por +pessoas distintas, com valores definidos por cada empresa. Regras de investimento +também continuam por definir por cliente; equipamento permanece bloqueado. + ## Dados bancários de fornecedores com dupla validação Implementado no mesmo projecto, com dados isolados por empresa/fornecedor. diff --git a/docs/orientacoes-continuidade.md b/docs/orientacoes-continuidade.md index 2f0d7f5..0667ae2 100644 --- a/docs/orientacoes-continuidade.md +++ b/docs/orientacoes-continuidade.md @@ -1,5 +1,15 @@ # Continuidade do desenvolvimento do SGC +## Próxima etapa após pagamentos externos + +Registo e conferência independente de pagamentos externos já implementados, +com migração 000012 aplicada. Não repetir as etapas históricas abaixo. +Prosseguir com reforços orçamentais por empresa/centro/exercício, sem reescrever +a dotação inicial, com proposta e aprovação por pessoas distintas e auditoria. +Valores definidos por cada empresa. Não implementar transferências bancárias +ou desbloquear investimento sem regra própria aprovada. Revisão visual e +validação operacional integral continuam pendentes; consultar o relatório actual. + ## Próxima etapa após dados bancários Versões bancárias por empresa/fornecedor, cifragem e duas validações distintas diff --git a/docs/pagamentos-concorrencia-postgresql.json b/docs/pagamentos-concorrencia-postgresql.json new file mode 100644 index 0000000..f156974 --- /dev/null +++ b/docs/pagamentos-concorrencia-postgresql.json @@ -0,0 +1,33 @@ +{ + "tested_at_utc": "2026-10-28T12:49:13+00:00", + "separate_database": true, + "transport": "Independent PHP service transactions and PostgreSQL connections", + "scenarios": [ + { + "name": "over_payment", + "passed": true, + "overlapping_lock_waits": 2, + "confirmed_minor": 4000, + "outstanding_minor": 2000, + "budget_consumption_unchanged": true + }, + { + "name": "same_confirmation", + "passed": true, + "overlapping_lock_waits": 2, + "confirmed_minor": 4000, + "outstanding_minor": 2000, + "budget_consumption_unchanged": true + }, + { + "name": "complementary_payments", + "passed": true, + "overlapping_lock_waits": 2, + "confirmed_minor": 6000, + "outstanding_minor": 0, + "budget_consumption_unchanged": true + } + ], + "passed": true, + "test_database_dropped": true +} diff --git a/docs/pagamentos-migracao-000012.json b/docs/pagamentos-migracao-000012.json new file mode 100644 index 0000000..795a222 --- /dev/null +++ b/docs/pagamentos-migracao-000012.json @@ -0,0 +1,15 @@ +{ + "verified_at_utc": "2026-10-28T12:46:38+00:00", + "migration": "2026_10_28_000012_create_external_payments", + "batch": 13, + "backup_created": true, + "backup_bytes": 239843, + "restore_verified": true, + "restored_tables": 44, + "restore_database_dropped": true, + "business_records_unchanged": true, + "existing_constraints_preserved": true, + "env_unchanged": true, + "external_payments_count": 0, + "operational_validation": false +} diff --git a/docs/proposta-regras-financeiras.md b/docs/proposta-regras-financeiras.md index 357ed90..32e76c2 100644 --- a/docs/proposta-regras-financeiras.md +++ b/docs/proposta-regras-financeiras.md @@ -7,7 +7,9 @@ Actualização: o utilizador autorizou avançar com a abordagem. Dotações, res compromissos e conferência de facturas/consumo foram implementados e migrados. A base de IVA é uma decisão explícita de cada empresa. As secções seguintes conservam a proposta original; não comprovam aprovação de limites monetários, -tolerâncias ou regras de investimento. Reforços e pagamentos continuam pendentes. +tolerâncias ou regras de investimento. Reforços continuam pendentes. Registo e +conferência de pagamentos externos foram acrescentados depois; não executam +transferências nem substituem a confirmação humana da evidência. Estado actual em `docs/desenvolvimento.md`. ## Abordagem recomendada diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 7db9c7f..6e206cb 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -141,6 +141,11 @@ export function AppSidebar() { href: '/facturas', icon: FileCheck2, }, + { + title: 'Pagamentos externos', + href: '/pagamentos', + icon: FileCheck2, + }, ]} /> )} diff --git a/resources/js/lib/preview-navigation.ts b/resources/js/lib/preview-navigation.ts index 61d71b8..81ac3cd 100644 --- a/resources/js/lib/preview-navigation.ts +++ b/resources/js/lib/preview-navigation.ts @@ -11,5 +11,6 @@ export const previewPaths: Record = { equipment: '/consulta/equipamentos', invoices: '/consulta/facturas', bank: '/consulta/dados-bancarios', + payments: '/consulta/pagamentos', appearance: '/consulta/aparencia', }; diff --git a/resources/js/pages/budgets/index.tsx b/resources/js/pages/budgets/index.tsx index 6c819e7..5c829b3 100644 --- a/resources/js/pages/budgets/index.tsx +++ b/resources/js/pages/budgets/index.tsx @@ -51,8 +51,9 @@ export default function Budgets({ Desenvolvimento. O orçamento continua sem validação operacional. Centros sem dotação continuam no circuito anterior, sem controlo orçamental. Facturas validadas - convertem compromisso em consumo. Pagamentos ainda não - implementados. + convertem compromisso em consumo. Pagamentos externos têm + registo e conferência próprios; o SGC não executa + transferências.

{ + e.preventDefault(); + form.post(`/pagamentos/${id}/conferir`); + }} + > + + +