From 153ba2c1af08ec3907c2106b8037b1de0c4941b1 Mon Sep 17 00:00:00 2001 From: nala Date: Wed, 28 Oct 2026 15:03:49 +0200 Subject: [PATCH] Implementa reforcos orcamentais com aprovacao independente --- README.md | 10 +- app/Http/Controllers/BudgetController.php | 7 +- .../BudgetReinforcementController.php | 30 ++++ app/Models/Budget.php | 8 + app/Models/BudgetReinforcement.php | 35 ++++ app/Services/BudgetReinforcementWorkflow.php | 101 +++++++++++ app/Services/BudgetWorkflow.php | 2 +- ...28_000013_create_budget_reinforcements.php | 33 ++++ docs/compatibilidade.md | 41 +++-- docs/desenvolvimento.md | 63 +++++++ docs/orientacoes-continuidade.md | 10 ++ docs/proposta-regras-financeiras.md | 2 +- docs/reforcos-concorrencia-postgresql.json | 33 ++++ docs/reforcos-migracao-000013.json | 15 ++ .../js/components/budget-reinforcements.tsx | 97 ++++++++++ resources/js/lib/preview-navigation.ts | 1 + resources/js/pages/budgets/index.tsx | 74 +++++++- resources/js/pages/preview/index.tsx | 34 +++- routes/preview.php | 3 +- routes/web.php | 3 + tests/Concurrency/reinforcement-worker.php | 42 +++++ tests/Concurrency/run-reinforcements.php | 132 ++++++++++++++ tests/Feature/BudgetReinforcementTest.php | 167 ++++++++++++++++++ .../Support/apply-reinforcement-migration.php | 105 +++++++++++ 24 files changed, 1023 insertions(+), 25 deletions(-) create mode 100644 app/Http/Controllers/BudgetReinforcementController.php create mode 100644 app/Models/BudgetReinforcement.php create mode 100644 app/Services/BudgetReinforcementWorkflow.php create mode 100644 database/migrations/2026_10_28_000013_create_budget_reinforcements.php create mode 100644 docs/reforcos-concorrencia-postgresql.json create mode 100644 docs/reforcos-migracao-000013.json create mode 100644 resources/js/components/budget-reinforcements.tsx create mode 100644 tests/Concurrency/reinforcement-worker.php create mode 100644 tests/Concurrency/run-reinforcements.php create mode 100644 tests/Feature/BudgetReinforcementTest.php create mode 100644 tests/Support/apply-reinforcement-migration.php diff --git a/README.md b/README.md index 51649ad..8782bc8 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 +- Reforços de orçamento por empresa/centro/exercício, com proposta, aprovação + independente e histórico imutável. A dotação inicial e a base de IVA são + preservadas; apenas reforços aprovados aumentam o saldo disponível. Migração + 000013 aplicada após backup/restauro verificados; três cenários concorrentes + PostgreSQL aprovados. Exemplo público em `/consulta/reforcos`. + - 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 @@ -46,7 +52,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 estornos continuam pendentes; não constitui validação + 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`. @@ -81,7 +87,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; 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. +Revisão visual dos módulos entregues; reposição de stock; conciliação de pagamentos externos; regras de investimento, 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/BudgetController.php b/app/Http/Controllers/BudgetController.php index 6843494..7400735 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -6,6 +6,7 @@ use App\Enums\PurchasingRole; use App\Models\Budget; use App\Models\BudgetConsumption; use App\Models\BudgetEntry; +use App\Models\BudgetReinforcement; use App\Models\CostCenter; use App\Services\ConflictGuard; use App\Services\RequisitionWorkflow; @@ -34,8 +35,10 @@ class BudgetController extends Controller $committed = (int) $entries->sum('committed_minor'); $consumed = (int) BudgetConsumption::query()->where('budget_id', $budget->id)->sum('amount_minor'); - return [...$budget->toArray(), 'reserved_minor' => $reserved, 'committed_minor' => $committed, 'consumed_minor' => $consumed, 'available_minor' => $budget->amount_minor - $reserved - $committed - $consumed]; - })]); + $total = $budget->approvedTotalMinor(); + + return [...$budget->toArray(), 'reinforced_minor' => $total - $budget->amount_minor, 'approved_total_minor' => $total, 'reserved_minor' => $reserved, 'committed_minor' => $committed, 'consumed_minor' => $consumed, 'available_minor' => $total - $reserved - $committed - $consumed]; + }), 'reinforcements' => BudgetReinforcement::query()->latest('id')->paginate(20)]); } public function store(Request $request): RedirectResponse diff --git a/app/Http/Controllers/BudgetReinforcementController.php b/app/Http/Controllers/BudgetReinforcementController.php new file mode 100644 index 0000000..cef6e39 --- /dev/null +++ b/app/Http/Controllers/BudgetReinforcementController.php @@ -0,0 +1,30 @@ +user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403); + $data = $request->validate(['reference' => 'required|string|max:100', 'amount' => ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/'], 'reason' => 'required|string|max:4000']); + $workflow->propose($request->user(), $budget, $data); + + return back(); + } + + public function review(Request $request, BudgetReinforcement $reinforcement, BudgetReinforcementWorkflow $workflow): RedirectResponse + { + $data = $request->validate(['decision' => 'required|in:approved,rejected', 'reason' => 'required|string|max:4000']); + $workflow->review($request->user(), $reinforcement, $data['decision'], $data['reason']); + + return back(); + } +} diff --git a/app/Models/Budget.php b/app/Models/Budget.php index 9039fcf..80101ea 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Models\Concerns\BelongsToTenant; +use Brick\Math\BigDecimal; use Illuminate\Database\Eloquent\Model; /** @property int $id @@ -16,6 +17,13 @@ class Budget extends Model protected $guarded = ['id']; + public function approvedTotalMinor(): int + { + $sum = BudgetReinforcement::query()->where('budget_id', $this->id)->where('status', 'approved')->sum('amount_minor'); + + return BigDecimal::of((string) $sum)->plus($this->amount_minor)->toInt(); + } + protected static function booted(): void { static::updating(function (Budget $budget) { diff --git a/app/Models/BudgetReinforcement.php b/app/Models/BudgetReinforcement.php new file mode 100644 index 0000000..86a23d4 --- /dev/null +++ b/app/Models/BudgetReinforcement.php @@ -0,0 +1,35 @@ + 'pending']; + + protected $hidden = ['payload_hash']; + + protected static function booted(): void + { + static::updating(function (BudgetReinforcement $reinforcement) { + if ($reinforcement->getRawOriginal('status') !== 'pending' || array_diff(array_keys($reinforcement->getDirty()), ['status', 'reviewed_by', 'review_reason', 'reviewed_at', 'updated_at']) !== []) { + throw new \LogicException('Preservar o reforço e a decisão.'); + } + }); + static::deleting(fn () => throw new \LogicException('Não eliminar reforços orçamentais.')); + } +} diff --git a/app/Services/BudgetReinforcementWorkflow.php b/app/Services/BudgetReinforcementWorkflow.php new file mode 100644 index 0000000..8c21689 --- /dev/null +++ b/app/Services/BudgetReinforcementWorkflow.php @@ -0,0 +1,101 @@ +id() === $user->id && $user->tenant_id === $tenantId && in_array($user->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403); + } + + private function valid(Budget $budget, int $amount): void + { + if (! $budget->approved_by || (int) $budget->year !== now()->year || $amount <= 0) { + throw ValidationException::withMessages(['reinforcement' => 'Exige dotação aprovada do exercício actual e montante positivo.']); + } + // Same technical monetary ceiling as requisitions; not a company approval threshold. + if (BigDecimal::of($budget->approvedTotalMinor())->plus($amount)->isGreaterThan('100000000000000')) { + throw ValidationException::withMessages(['reinforcement' => 'A dotação acumulada ultrapassa o limite técnico de valores suportados.']); + } + } + + /** @param array $data */ + public function propose(User $user, Budget $budget, array $data): BudgetReinforcement + { + $this->actor($user, $budget->tenant_id); + + return DB::transaction(function () use ($user, $budget, $data) { + app(ConflictGuard::class)->lockCompany($user); + $user = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->findOrFail($user->id); + $this->actor($user, $budget->tenant_id); + $budget = Budget::query()->lockForUpdate()->findOrFail($budget->id); + $amount = BigDecimal::of((string) $data['amount'])->multipliedBy(100)->toScale(0)->toInt(); + $reference = Str::upper(Str::squish($data['reference'])); + $reason = trim($data['reason']); + $hash = hash('sha256', json_encode(['budget_id' => $budget->id, 'amount_minor' => $amount, 'reason' => $reason], JSON_THROW_ON_ERROR)); + $existing = BudgetReinforcement::query()->where('reference', $reference)->first(); + if ($existing) { + if ($existing->created_by !== $user->id || $existing->payload_hash !== $hash) { + throw ValidationException::withMessages(['reference' => 'Referência já registada com outros dados.']); + } + + return $existing; + } + $this->valid($budget, $amount); + if ($reference === '' || $reason === '') { + throw ValidationException::withMessages(['reinforcement' => 'Referência e fundamento obrigatórios.']); + } + $reinforcement = BudgetReinforcement::query()->create(['tenant_id' => $user->tenant_id, 'budget_id' => $budget->id, 'created_by' => $user->id, 'reference' => $reference, 'payload_hash' => $hash, 'amount_minor' => $amount, 'reason' => $reason]); + $this->audit($user, $reinforcement, 'budget.reinforcement_proposed'); + + return $reinforcement; + }); + } + + public function review(User $user, BudgetReinforcement $reinforcement, string $decision, string $reason): void + { + $this->actor($user, $reinforcement->tenant_id); + abort_if($user->id === $reinforcement->created_by, 403); + if (! in_array($decision, ['approved', 'rejected']) || trim($reason) === '') { + throw ValidationException::withMessages(['reinforcement' => 'Decisão e fundamento obrigatórios.']); + } + DB::transaction(function () use ($user, $reinforcement, $decision, $reason) { + app(ConflictGuard::class)->lockCompany($user); + $user = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->findOrFail($user->id); + $this->actor($user, $reinforcement->tenant_id); + $budget = Budget::query()->lockForUpdate()->findOrFail($reinforcement->budget_id); + $reinforcement = BudgetReinforcement::query()->lockForUpdate()->findOrFail($reinforcement->id); + if ($reinforcement->status !== 'pending') { + if ($reinforcement->status !== $decision) { + throw ValidationException::withMessages(['reinforcement' => 'Decisão final imutável.']); + } + + return; + } + if ($decision === 'approved') { + $this->valid($budget, $reinforcement->amount_minor); + $author = User::query()->where('tenant_id', $user->tenant_id)->lockForUpdate()->find($reinforcement->created_by); + if (! $author || ! in_array($author->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance])) { + throw ValidationException::withMessages(['reinforcement' => 'Autor da proposta deixou de ser elegível.']); + } + } + $reinforcement->update(['status' => $decision, 'reviewed_by' => $user->id, 'review_reason' => trim($reason), 'reviewed_at' => now()]); + $this->audit($user, $reinforcement, 'budget.reinforcement_'.$decision); + }); + } + + private function audit(User $user, BudgetReinforcement $reinforcement, string $action): void + { + app(RequisitionWorkflow::class)->audit($user, $action, null, null, ['reinforcement_id' => $reinforcement->id, 'budget_id' => $reinforcement->budget_id, 'amount_minor' => $reinforcement->amount_minor, 'status' => $reinforcement->status]); + } +} diff --git a/app/Services/BudgetWorkflow.php b/app/Services/BudgetWorkflow.php index ea7bf3d..5f8e38c 100644 --- a/app/Services/BudgetWorkflow.php +++ b/app/Services/BudgetWorkflow.php @@ -54,7 +54,7 @@ class BudgetWorkflow $ledger = BudgetEntry::query()->where('budget_id', $budget->id); $used = (int) (clone $ledger)->sum('reserved_minor') + (int) $ledger->sum('committed_minor'); $used += (int) BudgetConsumption::query()->where('budget_id', $budget->id)->sum('amount_minor'); - if ($used + $reserveDelta + $commitDelta > $budget->amount_minor) { + if ($used + $reserveDelta + $commitDelta > $budget->approvedTotalMinor()) { throw ValidationException::withMessages(['budget' => 'Saldo orçamental insuficiente. Não são permitidos excessos.']); } BudgetEntry::query()->create(['tenant_id' => $user->tenant_id, 'budget_id' => $budget->id, 'requisition_id' => $request->id, 'created_by' => $user->id, 'kind' => $kind, 'reserved_minor' => $reserveDelta, 'committed_minor' => $commitDelta, 'created_at' => now()]); diff --git a/database/migrations/2026_10_28_000013_create_budget_reinforcements.php b/database/migrations/2026_10_28_000013_create_budget_reinforcements.php new file mode 100644 index 0000000..c2cfb89 --- /dev/null +++ b/database/migrations/2026_10_28_000013_create_budget_reinforcements.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('tenant_id')->constrained()->restrictOnDelete(); + $table->foreignId('budget_id')->constrained()->restrictOnDelete(); + $table->foreignId('created_by')->constrained('users')->restrictOnDelete(); + $table->foreignId('reviewed_by')->nullable()->constrained('users')->restrictOnDelete(); + $table->string('reference', 100); + $table->string('payload_hash', 64); + $table->bigInteger('amount_minor'); + $table->text('reason'); + $table->string('status')->default('pending'); + $table->text('review_reason')->nullable(); + $table->timestamp('reviewed_at')->nullable(); + $table->timestamps(); + $table->unique(['tenant_id', 'reference']); + }); + } + + public function down(): void + { + throw new LogicException('Preservar dotações, reforços e decisões.'); + } +}; diff --git a/docs/compatibilidade.md b/docs/compatibilidade.md index 90fa248..bdc0670 100644 --- a/docs/compatibilidade.md +++ b/docs/compatibilidade.md @@ -2,27 +2,36 @@ Estado actualizado em 28 de Outubro de 2026. Projecto activo: C:\laragon\www\compras, Laravel + React + PostgreSQL, http://127.0.0.1:8015. Preservar conta, dados, configuração, cookie e porta. SGF Tempo e Investmoz são independentes. A implementação Python separada fica apenas como referência; não importar contas, sessões ou bases experimentais. -| Área | Implementado no Laravel | Pendente | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| 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 | 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 | -| Ordens | Emissão única/idempotente, snapshots, consulta e impressão | Orçamento não validado; documentos de desenvolvimento | -| Recepção | Parcial/total, qualidade, divergências, pendentes, segregação do emitente e auditoria | Substituições e anexos; rejeições não autorizam receber acima do total | -| 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 | 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 | +| Área | Implementado no Laravel | Pendente | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| 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 | 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 | +| Ordens | Emissão única/idempotente, snapshots, consulta e impressão | Orçamento não validado; documentos de desenvolvimento | +| Recepção | Parcial/total, qualidade, divergências, pendentes, segregação do emitente e auditoria | Substituições e anexos; rejeições não autorizam receber acima do total | +| 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, reforços aprovados, reservas e compromissos por empresa/centro/exercício | Revisão visual, reduções/transferências e validação operacional integral | +| 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 | Preservar enums, políticas, modelos relacionais, Brick Math e isolamento por empresa. As regras guardadas nos snapshots não são substituídas pela configuração actual. Ausência de conflito não anula conflito registado. Exemplos públicos fictícios não dão acesso aos dados de negócio. ## Verificação actual +Reforços entregues por empresa/centro/exercício, com aprovação independente e +histórico, sem reescrever a dotação inicial. Migração 000013 no lote 14 após +backup e restauro verificados de 45 tabelas. Três cenários concorrentes PostgreSQL +aprovados. Suite actual: **370 aprovados, 3 ignorados, 2.704 asserções**. +Tabela de reforços vazia na base real. Regras de investimento, reduções, +transferências, estornos e revisão visual continuam pendentes. + +### Histórico da entrega de pagamentos externos + 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**. diff --git a/docs/desenvolvimento.md b/docs/desenvolvimento.md index adbc17b..ca5ba85 100644 --- a/docs/desenvolvimento.md +++ b/docs/desenvolvimento.md @@ -1,5 +1,68 @@ # Desenvolvimento do SGC +## Reforços orçamentais por empresa + +Implementadas propostas de reforço em `/orcamentos`, por dotação existente de +empresa/centro de custo/exercício. Administração ou Finanças propõe referência +interna, montante positivo e fundamento; outra pessoa destes perfis aprova ou +rejeita, fundamentando. Perfis do operador e do proponente são recarregados em +transacção. O autor não aprova o próprio reforço. + +A dotação inicial aprovada não é alterada. Dotação acumulada = valor inicial +mais reforços aprovados. Disponível = acumulada menos reservas, compromissos +remanescentes e consumos. Pendentes e rejeitados não aumentam o saldo; a +aprovação permite novas reservas apenas dentro do disponível actualizado. +Reforçar não altera consumo ou compromissos anteriores, base de IVA, moeda, +centro, exercício ou regras de aprovação de compras. Montantes são definidos +pela própria empresa, sem valores financeiros predefinidos para clientes. + +Referência normalizada única por empresa, permitindo referências iguais noutras +empresas. Repetição idêntica da proposta ou da aprovação não duplica a verba. +Decisões finais e dados do reforço não admitem reescrita ou eliminação pelos +modelos. Falha na auditoria reverte integralmente proposta ou decisão. +Só dotações aprovadas do exercício actual recebem reforços. Há o mesmo limite +técnico de representação monetária das requisições, não um limite empresarial +ou autorização comercial. Aprovações históricas não são revogadas automaticamente +por mudanças posteriores de perfil; correcções exigem fluxo próprio, ainda ausente. + +### Verificação e aplicação + +**370 testes aprovados, 3 ignorados, 2.704 asserções** na suite completa. +Inclui **23 testes de reforços, 74 asserções**: perfis, independência, exactidão, +idempotência, bases preservadas, isolamento, valores por empresa, ano encerrado, +auditoria e rollback, inputs manipulados e disponibilidade para reservas. +PHPStan, Pint, TypeScript e compilação aprovados. + +Três cenários concorrentes PostgreSQL aprovados: mesma aprovação repetida, +reforços distintos aprovados em simultâneo e aprovação concorrente com reserva. +Duas ligações independentes em processos PHP observadas simultaneamente à +espera de bloqueios por cenário. Conferidos dotação acumulada, saldo reservado, +ausência de excesso e preservação do valor inicial. Base temporária eliminada. +São transacções de serviço, não teste HTTP ou de carga. +[Evidência](reforcos-concorrencia-postgresql.json). + +Migração 000013 aplicada no **lote 14**, após backup custom PostgreSQL 17 de +**245.514 bytes** e restauro verificado das **45 tabelas** anteriores numa base +temporária, depois eliminada. Dump e manifesto partilharam snapshot consistente. +Registos de negócio, restrições e `.env` preservados. Tabela de reforços vazia: +sem seeds, verbas fictícias, contas novas ou alterações de credenciais na base +real. Backup/manifesto privados, fora do Git. +[Evidência sem dados privados](reforcos-migracao-000013.json). + +`/consulta/reforcos` contém somente exemplos fictícios; não consulta dotações ou +utilizadores e não grava dados. O navegador voltou a reportar +`The browser access host is unavailable`: **revisão visual pública, autenticada +e de impressão continua pendente**. Não constitui validação operacional integral. + +### Próximo passo e limitações + +Próximo incremento: regras de investimento configuráveis por empresa e aprovadas +independentemente. Não inventar montantes comuns aos clientes, semântica de +limites ou dispensa de aprovações. Enquanto não houver regra própria aprovada, +equipamentos continuam bloqueados para adjudicação/emissão. Reduções, transferências +de dotações, estornos de reforços e passagem de saldos entre exercícios não +foram implementados; exigem fluxos e decisões próprios. + ## Registo e conferência de pagamentos externos Implementado `/pagamentos`, por empresa. Finanças regista pagamentos já diff --git a/docs/orientacoes-continuidade.md b/docs/orientacoes-continuidade.md index 0667ae2..8471777 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 reforços + +Reforços por empresa/centro/exercício já estão implementados e migrados +(000013, lote 14). Não repetir as orientações históricas abaixo. Prosseguir +com regras de investimento configuráveis por empresa, com decisões e valores +explícitos, versionamento e aprovação independente. Não presumir um valor ou +interpretar limite ausente como autorização. Até existir regra própria aprovada, +preservar o bloqueio de adjudicação/emissão de equipamento. Revisão visual e +validação operacional integral continuam pendentes. Consultar o relatório actual. + ## Próxima etapa após pagamentos externos Registo e conferência independente de pagamentos externos já implementados, diff --git a/docs/proposta-regras-financeiras.md b/docs/proposta-regras-financeiras.md index 32e76c2..25156d6 100644 --- a/docs/proposta-regras-financeiras.md +++ b/docs/proposta-regras-financeiras.md @@ -7,7 +7,7 @@ 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 continuam pendentes. Registo e +tolerâncias ou regras de investimento. Reforços positivos, com aprovação independente, foram implementados posteriormente. 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`. diff --git a/docs/reforcos-concorrencia-postgresql.json b/docs/reforcos-concorrencia-postgresql.json new file mode 100644 index 0000000..b937cf8 --- /dev/null +++ b/docs/reforcos-concorrencia-postgresql.json @@ -0,0 +1,33 @@ +{ + "tested_at_utc": "2026-10-28T12:59:44+00:00", + "separate_database": true, + "transport": "Independent PHP service transactions and PostgreSQL connections", + "scenarios": [ + { + "name": "same_approval", + "passed": true, + "overlapping_lock_waits": 2, + "approved_total_minor": 12000, + "reserved_minor": 0, + "original_allocation_unchanged": true + }, + { + "name": "distinct_reinforcements", + "passed": true, + "overlapping_lock_waits": 2, + "approved_total_minor": 19000, + "reserved_minor": 0, + "original_allocation_unchanged": true + }, + { + "name": "approval_vs_reservation", + "passed": true, + "overlapping_lock_waits": 2, + "approved_total_minor": 12000, + "reserved_minor": 10000, + "original_allocation_unchanged": true + } + ], + "passed": true, + "test_database_dropped": true +} diff --git a/docs/reforcos-migracao-000013.json b/docs/reforcos-migracao-000013.json new file mode 100644 index 0000000..ebc2efe --- /dev/null +++ b/docs/reforcos-migracao-000013.json @@ -0,0 +1,15 @@ +{ + "verified_at_utc": "2026-10-28T13:00:53+00:00", + "migration": "2026_10_28_000013_create_budget_reinforcements", + "batch": 14, + "backup_created": true, + "backup_bytes": 245514, + "restore_verified": true, + "restored_tables": 45, + "restore_database_dropped": true, + "business_records_unchanged": true, + "existing_constraints_preserved": true, + "env_unchanged": true, + "budget_reinforcements_count": 0, + "operational_validation": false +} diff --git a/resources/js/components/budget-reinforcements.tsx b/resources/js/components/budget-reinforcements.tsx new file mode 100644 index 0000000..506340b --- /dev/null +++ b/resources/js/components/budget-reinforcements.tsx @@ -0,0 +1,97 @@ +import { useForm } from '@inertiajs/react'; +import { FormErrors, controlClass } from '@/components/procurement'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; + +export function ReinforcementProposal({ budgetId }: { budgetId: number }) { + const form = useForm({ reference: '', amount: '', reason: '' }); + return ( +
+ + Propor reforço desta dotação + +
{ + e.preventDefault(); + form.post(`/orcamentos/${budgetId}/reforcos`, { + onSuccess: () => form.reset(), + }); + }} + > + +

+ Aumenta o saldo apenas depois da aprovação por outra pessoa. + Mantém a moeda e a base de IVA da dotação. +

+ + +