From 413c201d972b00a48ddaec0490faf1de2f3a1d6e Mon Sep 17 00:00:00 2001 From: nala Date: Thu, 29 Oct 2026 22:03:09 +0200 Subject: [PATCH] commit5 --- .env.example | 6 + .gitignore | 1 + README.md | 12 + app/Http/Controllers/InvoiceController.php | 3 +- .../Controllers/RequisitionController.php | 11 +- .../SupplierPayablesController.php | 43 ++ app/Http/Middleware/HandleInertiaRequests.php | 2 + app/Providers/AppServiceProvider.php | 5 + app/Services/ExternalPaymentWorkflow.php | 24 +- app/Services/SupplierPayables.php | 196 ++++++++++ config/common-payables.php | 3 + ..._10_29_000021_create_supplier_payables.php | 17 + ..._000022_create_payable_history_imports.php | 17 + ...00023_create_payable_payment_decisions.php | 17 + docs/conferencia-pendentes-importados.md | 29 ++ docs/desenvolvimento.md | 21 + docs/historico-activacao-local.json | 19 + docs/historico-migracao-000022.json | 22 ++ docs/integracao-factura-obrigacao.md | 67 ++++ docs/menus-benchmark-20261029.md | 48 +++ docs/obrigacoes-activacao-local.json | 22 ++ docs/obrigacoes-migracao-000021.json | 27 ++ docs/orientacoes-continuidade.md | 32 ++ docs/passagem-historico-pagamentos.md | 49 +++ docs/proofs/evaluation-access.json | 42 ++ docs/proofs/invoice-payable-link.json | 11 + docs/proofs/menu-links.json | 155 ++++++++ docs/proofs/payable-history-pg.json | 28 ++ docs/proofs/payable-payment-decisions-pg.json | 19 + docs/proofs/screenshots/menu-desktop.png | Bin 0 -> 73446 bytes docs/proofs/screenshots/menu-mobile.png | Bin 0 -> 26585 bytes docs/referencias/benchmark-menus-20261029.txt | 131 +++++++ phpunit.xml | 4 + resources/js/components/app-sidebar.tsx | 5 +- resources/js/components/nav-main.tsx | 15 +- resources/js/components/sgc-shell.tsx | 17 +- .../js/components/sgc-workspace-shell.tsx | 369 ++++++++++++++++++ resources/js/lib/procurement-navigation.ts | 267 +++++++++++++ resources/js/pages/invoices/index.tsx | 10 + resources/js/pages/requisitions/index.tsx | 50 ++- resources/js/types/navigation.ts | 1 + resources/views/app.blade.php | 3 + routes/evaluation.php | 80 ++++ routes/web.php | 7 + scripts/evaluation-access-check.mjs | 48 +++ scripts/invoice-payable-link-check.mjs | 39 ++ scripts/menu-links-check.mjs | 95 +++++ scripts/payable-history-pg.php | 195 +++++++++ scripts/payable-history-proof.mjs | 49 +++ tests/Feature/ExternalPaymentTest.php | 14 + tests/Feature/PayableHistoryImportTest.php | 200 ++++++++++ tests/Feature/PayablePaymentDecisionTest.php | 109 ++++++ tests/Feature/ProcurementTest.php | 30 ++ .../SupplierPayableIntegrationTest.php | 81 ++++ 54 files changed, 2753 insertions(+), 14 deletions(-) create mode 100644 app/Http/Controllers/SupplierPayablesController.php create mode 100644 app/Services/SupplierPayables.php create mode 100644 config/common-payables.php create mode 100644 database/migrations/2026_10_29_000021_create_supplier_payables.php create mode 100644 database/migrations/2026_10_29_000022_create_payable_history_imports.php create mode 100644 database/migrations/2026_10_29_000023_create_payable_payment_decisions.php create mode 100644 docs/conferencia-pendentes-importados.md create mode 100644 docs/historico-activacao-local.json create mode 100644 docs/historico-migracao-000022.json create mode 100644 docs/integracao-factura-obrigacao.md create mode 100644 docs/menus-benchmark-20261029.md create mode 100644 docs/obrigacoes-activacao-local.json create mode 100644 docs/obrigacoes-migracao-000021.json create mode 100644 docs/passagem-historico-pagamentos.md create mode 100644 docs/proofs/evaluation-access.json create mode 100644 docs/proofs/invoice-payable-link.json create mode 100644 docs/proofs/menu-links.json create mode 100644 docs/proofs/payable-history-pg.json create mode 100644 docs/proofs/payable-payment-decisions-pg.json create mode 100644 docs/proofs/screenshots/menu-desktop.png create mode 100644 docs/proofs/screenshots/menu-mobile.png create mode 100644 docs/referencias/benchmark-menus-20261029.txt create mode 100644 resources/js/components/sgc-workspace-shell.tsx create mode 100644 resources/js/lib/procurement-navigation.ts create mode 100644 routes/evaluation.php create mode 100644 scripts/evaluation-access-check.mjs create mode 100644 scripts/invoice-payable-link-check.mjs create mode 100644 scripts/menu-links-check.mjs create mode 100644 scripts/payable-history-pg.php create mode 100644 scripts/payable-history-proof.mjs create mode 100644 tests/Feature/PayableHistoryImportTest.php create mode 100644 tests/Feature/PayablePaymentDecisionTest.php create mode 100644 tests/Feature/SupplierPayableIntegrationTest.php diff --git a/.env.example b/.env.example index 1125a04..561ba5a 100644 --- a/.env.example +++ b/.env.example @@ -65,3 +65,9 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +# Activação deliberada, depois de aplicar/verificar o esquema e conceder acessos. +COMMON_PAYABLES_ENABLED=false +COMMON_PAYABLES_HISTORY_ENABLED=false +COMMON_PAYABLES_HISTORY_DECISIONS_ENABLED=false +PAYABLE_SOURCE_INSTANCE= diff --git a/.gitignore b/.gitignore index 77b2461..a37150b 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ yarn-error.log /boost.json /opencode.json /opencode.jsonc +/storage/framework/payables/ diff --git a/README.md b/README.md index 2720ad6..ecf1fd3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,16 @@ Este é o projecto activo em `C:\laragon\www\compras`. Preserve a conta existent ## Executar localmente +Conferência dos pagamentos pendentes após importação: [entrega e provas](docs/conferencia-pendentes-importados.md). Implementada em testes, com migração 000023 e activação ainda por aplicar; demonstração actual preservada. + +### Explorar a aplicação sem palavra-passe + +Na demonstração local isolada, abrir **http://127.0.0.1:8005/avaliar** e escolher **Entrar como proprietário**. A página «Explorar Compras», acessível também pela ligação acima da aplicação, reúne os 20 módulos/páginas principais. Disponibiliza ainda os seis perfis operacionais sintéticos para experimentar os respectivos passos sem procurar credenciais. Os dados e alterações desta demonstração persistem entre visitas. + +A entrada pertence exclusivamente à base QA local existente de obrigações, com empresa e exemplos sintéticos; não altera contas/dados da instalação normal em 8015. As regras de aprovação independente e de integridade permanecem. O proprietário tem o perfil administrativo e as três acções do ensaio de obrigações; os passos reservados a Procurement, Finanças ou Armazém usam os perfis de avaliação disponíveis. Não se declara que todos os percursos já tenham exemplos completos nesta base. + +Retomar o servidor existente, sem apagar dados: `powershell -NoProfile -ExecutionPolicy Bypass -File C:\laragon\www\sgf-tempo\scripts\payables-demo.ps1 start`. Se já estiver activo, basta abrir a ligação. Verificação HTTP: `scripts/evaluation-access-check.mjs`; resultados em `docs/proofs/evaluation-access.json` (20 páginas, seis perfis, CSRF, recusa de identificadores arbitrários e saída; sem operações de negócio). A revisão visual do utilizador é separada. + Endereço local deste projecto: **http://127.0.0.1:8015**. Use `SERVER_PORT=8015`, `APP_URL=http://127.0.0.1:8015` e `SESSION_COOKIE=procuraflow_session` no `.env` para evitar colisões com outros projectos. O comando `php artisan serve` respeita `SERVER_PORT`. As dependências e o `.env` devem estar configurados. O projecto suporta a ligação PostgreSQL existente; os testes usam SQLite em memória. @@ -23,6 +33,8 @@ Crie um pedido, guarde o rascunho, confira os valores e submeta. Entre com as co ## Entrega actual +- Passagem explícita de pagamentos históricos para a obrigação comum: principal integral, confirmados deduzidos uma vez, pendentes/rejeitados preservados e validação distinta. Migração 000022 aplicada e activação histórica concluída em `compras_dev`, após backup/restauro e preservação conferida; sem incorporação automática. A demonstração em 8005 mantém o seu cenário anterior. Não executa pagamentos nem confirma pendentes após a passagem. Ver [entrega, aplicação e provas](docs/passagem-historico-pagamentos.md). + - Regras de investimento próprias por empresa: tecto por processo, base de IVA e vigência explícitos, com duas aprovações administrativas distintas do autor. Equipamento exige também validação técnica, Finanças e reserva em dotação de diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 1e68396..2b49e53 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -6,6 +6,7 @@ use App\Enums\PurchasingRole; use App\Models\Invoice; use App\Models\PurchaseOrder; use App\Services\InvoiceWorkflow; +use App\Services\SupplierPayables; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Inertia\Inertia; @@ -17,7 +18,7 @@ class InvoiceController extends Controller { abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance]), 403); - return Inertia::render('invoices/index', ['invoices' => Invoice::query()->with('items')->latest('id')->paginate(15), 'orders' => PurchaseOrder::query()->with('items')->latest('id')->get()->map(fn ($order) => ['id' => $order->id, 'number' => $order->number, 'supplier' => $order->snapshot['supplier']['name'], 'items' => $order->items]), 'canManage' => $request->user()->purchasing_role === PurchasingRole::Finance]); + return Inertia::render('invoices/index', ['invoices' => Invoice::query()->with('items')->latest('id')->paginate(15), 'orders' => PurchaseOrder::query()->with('items')->latest('id')->get()->map(fn ($order) => ['id' => $order->id, 'number' => $order->number, 'supplier' => $order->snapshot['supplier']['name'], 'items' => $order->items]), 'payablesAvailable' => SupplierPayables::enabled() && app(SupplierPayables::class)->can($request->user(), 'view'), 'canManage' => $request->user()->purchasing_role === PurchasingRole::Finance]); } public function store(Request $request, PurchaseOrder $order, InvoiceWorkflow $workflow): RedirectResponse diff --git a/app/Http/Controllers/RequisitionController.php b/app/Http/Controllers/RequisitionController.php index 7dcb12f..dbe9133 100644 --- a/app/Http/Controllers/RequisitionController.php +++ b/app/Http/Controllers/RequisitionController.php @@ -25,8 +25,17 @@ class RequisitionController extends Controller { public function index(Request $request): Response { - $filters = $request->validate(['search' => 'nullable|string|max:100', 'status' => ['nullable', Rule::in(['draft', 'pending', 'approved', 'rejected', 'cancelled'])]]); + $filters = $request->validate(['search' => 'nullable|string|max:100', 'status' => ['nullable', Rule::in(['draft', 'pending', 'approved', 'rejected', 'cancelled'])], 'view' => ['nullable', Rule::in(['all', 'mine', 'approvals', 'decided'])]]); $query = Requisition::query()->visibleTo($request->user())->with(['requester:id,name', 'department:id,name', 'steps']); + if (($filters['view'] ?? 'all') === 'mine') { + $query->where('requester_id', $request->user()->id); + } elseif (($filters['view'] ?? 'all') === 'approvals') { + $eligible = (clone $query)->where('status', 'pending')->get() + ->filter(fn (Requisition $requisition) => $requisition->canDecide($request->user()))->modelKeys(); + $query->whereIn('id', $eligible); + } elseif (($filters['view'] ?? 'all') === 'decided') { + $query->whereHas('steps', fn ($steps) => $steps->where('approver_id', $request->user()->id)->whereIn('decision', ['approved', 'rejected'])); + } if (! empty($filters['search'])) { $query->where('number', 'like', '%'.$filters['search'].'%'); } diff --git a/app/Http/Controllers/SupplierPayablesController.php b/app/Http/Controllers/SupplierPayablesController.php new file mode 100644 index 0000000..5652ca6 --- /dev/null +++ b/app/Http/Controllers/SupplierPayablesController.php @@ -0,0 +1,43 @@ +validate(['invoice' => 'sometimes|required|integer|min:1']); + + return response()->json($s->listing($r->user(), isset($filter['invoice']) ? (int) $filter['invoice'] : null))->header('Cache-Control', 'no-store'); + } + + public function page(Request $r, SupplierPayables $s): Response + { + $filter = $r->validate(['invoice' => 'sometimes|required|integer|min:1']); + $data = $s->listing($r->user(), isset($filter['invoice']) ? (int) $filter['invoice'] : null); + $company = Tenant::query()->findOrFail($r->user()->tenant_id)->name; + + return response(Workspace::render($data, ['host' => 'Compras autónomo', 'actor' => $r->user()->name, 'organisation' => $company, 'api' => '/api/supplier-payables', 'csrf' => csrf_token(), 'return_url' => '/facturas', 'return_label' => 'Voltar às facturas']))->header('Cache-Control', 'no-store'); + } + + public function command(Request $r, string $action, SupplierPayables $s): JsonResponse + { + abort_if(str_ends_with($action, '_history') && ! SupplierPayables::historyEnabled(), 403, 'Passagem de histórico não activada nesta instalação.'); + $d = $r->validate(['id' => 'required|integer|min:1', 'reason' => 'required|string|min:3|max:2000']); + abort_if(array_diff(array_keys($r->all()), ['id', 'reason']) !== [], 422); + try { + return response()->json($s->engine($r->user())->command($action, $d['id'], $d['reason'], (string) $r->header('Idempotency-Key'))); + } catch (\DomainException $e) { + abort(409, $e->getMessage()); + } catch (\InvalidArgumentException $e) { + abort(422, $e->getMessage()); + } + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index f4cc770..618c5b6 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -2,6 +2,7 @@ namespace App\Http\Middleware; +use App\Services\SupplierPayables; use Illuminate\Http\Request; use Inertia\Middleware; @@ -41,6 +42,7 @@ class HandleInertiaRequests extends Middleware 'auth' => [ 'user' => $request->user(), ], + 'payablesAvailable' => $request->user() !== null && app(SupplierPayables::class)->can($request->user(), 'view'), 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', ]; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 2741457..f008a02 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -20,6 +20,11 @@ class AppServiceProvider extends ServiceProvider $package = base_path('../sgf-tempo/packages/budget-control'); } require_once $package.'/bootstrap.php'; + $payables = config('common-payables.path'); + if (! is_file($payables.'/bootstrap.php') && ! $this->app->environment('production')) { + $payables = base_path('../sgf-tempo/packages/payables'); + } + require_once $payables.'/bootstrap.php'; } /** diff --git a/app/Services/ExternalPaymentWorkflow.php b/app/Services/ExternalPaymentWorkflow.php index d96ca88..4fa99db 100644 --- a/app/Services/ExternalPaymentWorkflow.php +++ b/app/Services/ExternalPaymentWorkflow.php @@ -30,6 +30,11 @@ class ExternalPaymentWorkflow public function outstanding(Invoice $invoice): int { + $canonical = app(SupplierPayables::class)->balance($invoice); + if ($canonical !== null) { + return $canonical; + } + return (int) $invoice->total_minor - (int) ExternalPayment::query()->where('invoice_id', $invoice->id)->where('status', 'confirmed')->sum('amount_minor'); } @@ -59,6 +64,7 @@ class ExternalPaymentWorkflow /** @param array $data */ public function register(User $user, Invoice $invoice, array $data): ExternalPayment { + abort_if(app(SupplierPayables::class)->principal($invoice) !== null, 403, 'Pagamento de obrigação do novo ensaio fora do âmbito.'); $this->actor($user, $invoice->tenant_id); return DB::transaction(function () use ($user, $invoice, $data) { @@ -66,6 +72,7 @@ class ExternalPaymentWorkflow $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); + abort_if(app(SupplierPayables::class)->principal($invoice) !== null, 403, 'Pagamento de obrigação do novo ensaio fora do âmbito.'); $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'])]; @@ -102,7 +109,9 @@ class ExternalPaymentWorkflow $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); + $historical = app(SupplierPayables::class)->assertHistoricalReview($user, $invoice); $payment = ExternalPayment::query()->lockForUpdate()->findOrFail($payment->id); + abort_if($payment->created_by === $user->id, 403); if ($payment->status !== 'pending') { if ($payment->status !== $decision) { $this->invalid('Conferência já decidida; não pode ser reescrita.'); @@ -122,8 +131,19 @@ class ExternalPaymentWorkflow $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); + $write = function () use ($payment, $decision, $user, $reason, $invoice) { + $payment->update(['status' => $decision, 'reviewed_by' => $user->id, 'review_reason' => $reason, 'reviewed_at' => now()]); + $this->audit($user, $invoice, $payment, 'payment.'.$decision); + }; + if ($historical) { + try { + app(SupplierPayables::class)->engine($user)->decideImportedPayment($invoice->id, $payment->id, $write); + } catch (\DomainException $e) { + abort(409, $e->getMessage()); + } + } else { + $write(); + } }); } diff --git a/app/Services/SupplierPayables.php b/app/Services/SupplierPayables.php new file mode 100644 index 0000000..ac8b110 --- /dev/null +++ b/app/Services/SupplierPayables.php @@ -0,0 +1,196 @@ +environment(['testing', 'payable-testing']) && DB::connection()->getDriverName() === 'pgsql' + && (bool) preg_match('/^compras_receipts_test_[a-f0-9]{12}$/D', DB::connection()->getDatabaseName()); + + return ($isolated || config('common-payables.enabled') === true) + && is_string(config('common-payables.source_instance')) && trim(config('common-payables.source_instance')) !== '' + && Schema::hasTable('pa_obligations') && Schema::hasTable('pa_access'); + } + + /** @return array */ + private function scope(User $u): array + { + return ['tenant_id' => (int) $u->tenant_id, 'company_id' => 0, 'unit_id' => 0]; + } + + public function can(User $u, string $action): bool + { + return self::enabled() && DB::table('pa_access')->where($this->scope($u))->where(['user_id' => $u->id, 'action' => $action])->exists(); + } + + public static function historyEnabled(): bool + { + return self::enabled() && config('common-payables.history_enabled') === true + && Schema::hasTable('pa_history_imports') && Schema::hasTable('pa_imported_payments'); + } + + public function assertHistoricalReview(User $user, Invoice $invoice): bool + { + if ($this->principal($invoice) === null) { + return false; + } + abort_unless(self::historyDecisionsEnabled() && $this->can($user, 'validate'), 403, 'Conferência de pendentes importados não activada.'); + + return true; + } + + public static function historyDecisionsEnabled(): bool + { + return self::historyEnabled() && config('common-payables.history_decisions_enabled') === true && Schema::hasTable('pa_payment_decisions'); + } + + /** + * Original payment facts only; no encrypted references or bank data. + * + * @return list> + */ + private function paymentHistory(User $user, int $invoiceId): array + { + $result = []; + $payments = DB::table('external_payments')->where(['tenant_id' => $user->tenant_id, 'invoice_id' => $invoiceId])->orderBy('id')->get(); + foreach ($payments as $payment) { + $audit = fn (string $action, int $actor, string $status) => DB::table('audit_logs')->where('tenant_id', $user->tenant_id) + ->where('action', $action)->where('user_id', $actor)->where('after->payment_id', $payment->id) + ->where('after->invoice_id', $invoiceId)->where('after->amount_minor', $payment->amount_minor)->where('after->status', $status)->orderByDesc('id')->first(['id']); + $registration = $audit('payment.registered', (int) $payment->created_by, 'pending'); + $decision = $payment->status === 'pending' ? null : $audit('payment.'.$payment->status, (int) $payment->reviewed_by, $payment->status); + if (! $registration || ($payment->status !== 'pending' && ! $decision)) { + throw new \DomainException('Histórico sem auditoria compatível; preservar os originais para conferência.'); + } + $result[] = ['id' => (int) $payment->id, 'company' => (int) $payment->tenant_id, 'document' => (int) $payment->invoice_id, + 'amount_minor' => (int) $payment->amount_minor, 'currency' => $payment->currency, 'status' => $payment->status, + 'created_by' => (int) $payment->created_by, 'paid_on' => $payment->paid_on, 'registration_audit_id' => (int) $registration->id, + 'reviewed_by' => $payment->reviewed_by === null ? null : (int) $payment->reviewed_by, 'reviewed_at' => $payment->reviewed_at, + 'decision_audit_id' => $decision ? (int) $decision->id : null]; + } + + return $result; + } + + /** @return array */ + public function document(User $u, int $id): array + { + abort_unless(self::enabled() && auth()->id() === $u->id, 403); + $invoice = Invoice::query()->where('tenant_id', $u->tenant_id)->findOrFail($id); + $order = DB::table('purchase_orders')->where(['id' => $invoice->purchase_order_id, 'tenant_id' => $u->tenant_id])->first(); + $supplier = DB::table('suppliers')->where(['id' => $invoice->supplier_id, 'tenant_id' => $u->tenant_id])->first(); + $audit = DB::table('audit_logs')->where('tenant_id', $u->tenant_id)->where('action', 'invoice.validated')->where('after->invoice_id', $id)->orderByDesc('id')->first(); + + return ['local_id' => $id, 'instance' => config('common-payables.source_instance'), 'company' => (int) $u->tenant_id, 'document' => $id, 'number' => $invoice->number, + 'supplier_id' => $supplier?->id, 'supplier_name' => $invoice->snapshot['supplier']['name'] ?? null, 'order_id' => $order?->id, 'currency' => $invoice->snapshot['currency'] ?? null, + 'issued_on' => $invoice->issued_on, 'subtotal_minor' => (int) $invoice->subtotal_minor, 'tax_minor' => (int) $invoice->tax_minor, 'total_minor' => (int) $invoice->total_minor, + 'status' => $invoice->status, 'validated_by' => $invoice->validated_by, 'validated_at' => $invoice->validated_at, 'validation_audit_id' => $audit?->id, + 'lines' => $invoice->items()->orderBy('id')->get()->map(fn ($l) => ['id' => $l->id, 'order_line' => $l->purchase_order_item_id, 'quantity_milli' => (int) $l->quantity_milli, 'subtotal_minor' => (int) $l->subtotal_minor, 'tax_minor' => (int) $l->tax_minor, 'snapshot' => $l->snapshot])->all(), + 'payment_count' => DB::table('external_payments')->where(['tenant_id' => $u->tenant_id, 'invoice_id' => $id])->count()]; + } + + public function engine(User $u): Payables + { + abort_unless(self::enabled() && auth()->id() === $u->id && $u->tenant_id !== null, 403, 'Obrigações não activadas nesta instalação ou acesso indisponível.'); + $s = $this->scope($u); + $person = DB::table('pa_access')->where($s)->where(['user_id' => $u->id, 'action' => 'view'])->value('person'); + abort_unless($person, 403); + + return new Payables(DB::connection(), $u->id, $person, $s, function (string $action) use ($u, $s, $person) { + abort_unless(auth()->id() === $u->id, 403); + app(ConflictGuard::class)->lockCompany($u); + app(CurrentActor::class)->lock($u, [PurchasingRole::Finance, PurchasingRole::Admin]); + abort_unless(DB::table('pa_access')->where($s)->where(['user_id' => $u->id, 'action' => 'view'])->value('person') === $person, 409, 'Identidade de pessoa alterada; reabra a proposta.'); + abort_unless($this->can($u, 'view') && $this->can($u, $action), 403); + }, fn (int $id) => $this->document($u, $id), self::historyEnabled() ? fn (int $id) => $this->paymentHistory($u, $id) : null); + } + + /** @return array */ + public function listing(User $u, ?int $invoiceId = null): array + { + $engine = $this->engine($u); + $d = $engine->listing(Invoice::query()->where('tenant_id', $u->tenant_id)->pluck('id')->all()); + $d['permissions'] = ['prepare' => $this->can($u, 'prepare'), 'validate' => $this->can($u, 'validate')]; + $d['history_import_available'] = self::historyEnabled(); + $d['history_decisions_available'] = self::historyDecisionsEnabled(); + + if ($invoiceId !== null) { + $invoice = Invoice::query()->where('tenant_id', $u->tenant_id)->findOrFail($invoiceId); + $document = null; + foreach ($d['documents'] as $candidate) { + if ((int) $candidate['local_id'] === $invoice->id) { + $document = $candidate; + break; + } + } + abort_unless($document, 404); + $sourceIds = DB::table('pa_sources')->where($this->scope($u)) + ->where('source_key', Payables::identity($document))->pluck('id')->map(fn ($id) => (int) $id)->all(); + $d['documents'] = [$document]; + $d['proposals'] = $d['proposals']->filter(fn ($p) => in_array((int) $p->source_id, $sourceIds, true))->values(); + $d['obligations'] = array_values(array_filter($d['obligations'], fn ($o) => in_array((int) $o['source_id'], $sourceIds, true))); + $d['events'] = $d['events']->filter(fn ($event) => in_array((int) $event->source_id, $sourceIds, true))->values(); + } + + return $d; + } + + public function principal(Invoice $invoice): ?int + { + $position = $this->recognisedPosition($invoice); + + return $position ? (int) $position['principal_minor'] : null; + } + + public function balance(Invoice $invoice): ?int + { + $position = $this->recognisedPosition($invoice); + + return $position ? (int) $position['balance_minor'] : null; + } + + /** @return array|null */ + private function recognisedPosition(Invoice $invoice): ?array + { + if (! self::enabled()) { + if (Schema::hasTable('pa_obligations') && Schema::hasTable('pa_sources')) { + abort_if(DB::table('pa_sources as s')->join('pa_obligations as o', 'o.source_id', '=', 's.id') + ->where('s.tenant_id', $invoice->tenant_id)->where('s.company_id', 0)->where('s.unit_id', 0) + ->where('s.local_source_id', $invoice->id)->exists(), 409, 'Núcleo desactivado com obrigação incorporada; saldo exige transição explícita.'); + } + + return null; + } + $u = auth()->user(); + abort_unless($u instanceof User, 403); + abort_unless((int) $invoice->tenant_id === (int) $u->tenant_id, 403); + $s = $this->scope($u); + $identity = Payables::identity(['instance' => config('common-payables.source_instance'), 'company' => (int) $invoice->tenant_id, 'document' => $invoice->id]); + $row = DB::table('pa_sources as s')->join('pa_obligations as o', 'o.source_id', '=', 's.id')->where('s.tenant_id', $s['tenant_id'])->where('s.company_id', 0)->where('s.unit_id', 0)->where('s.source_key', $identity)->where('s.local_source_id', $invoice->id)->select('o.principal_minor', 'o.source_revision')->first(); + if (! $row) { + abort_if(DB::table('pa_sources as s')->join('pa_obligations as o', 'o.source_id', '=', 's.id') + ->where('s.tenant_id', $s['tenant_id'])->where('s.company_id', 0)->where('s.unit_id', 0) + ->where('s.local_source_id', $invoice->id)->exists(), 409, 'Identidade da instalação alterada; reconciliar origem antes de calcular saldo.'); + + return null; + } + abort_unless($this->can($u, 'view'), 403); + try { + $position = $this->engine($u)->positionForSource($invoice->id); + } catch (\DomainException $e) { + abort(409, $e->getMessage()); + } + abort_unless($position && ! $position['source_conflict'] && $position['balance_minor'] !== null, 409, 'Origem ou histórico alterado; principal preservado e saldo pendente de revisão.'); + + return $position; + } +} diff --git a/config/common-payables.php b/config/common-payables.php new file mode 100644 index 0000000..79fd4cd --- /dev/null +++ b/config/common-payables.php @@ -0,0 +1,3 @@ + env('COMMON_PAYABLES_PATH', base_path('packages/payables')), 'source_instance' => env('PAYABLE_SOURCE_INSTANCE', ''), 'enabled' => env('COMMON_PAYABLES_ENABLED', false), 'history_enabled' => env('COMMON_PAYABLES_HISTORY_ENABLED', false), 'history_decisions_enabled' => env('COMMON_PAYABLES_HISTORY_DECISIONS_ENABLED', false)]; diff --git a/database/migrations/2026_10_29_000021_create_supplier_payables.php b/database/migrations/2026_10_29_000021_create_supplier_payables.php new file mode 100644 index 0000000..259e79d --- /dev/null +++ b/database/migrations/2026_10_29_000021_create_supplier_payables.php @@ -0,0 +1,17 @@ +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 literal 0 HcmV?d00001 diff --git a/docs/proofs/screenshots/menu-mobile.png b/docs/proofs/screenshots/menu-mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..447e05c8a316e3317bfd9d4abebb0bb9a487cbfe GIT binary patch literal 26585 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 ZJ2#q+5%}!~y6|7xwv+#&LOuC|`d + + + + diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index bae551d..367a776 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -35,7 +35,7 @@ const generalNavItems: NavItem[] = [ ]; export function AppSidebar() { - const { auth } = usePage().props; + const { auth, payablesAvailable } = usePage().props; const canReadPurchases = [ 'admin', 'procurement', @@ -155,6 +155,9 @@ export function AppSidebar() { href: '/facturas', icon: FileCheck2, }, + ...(payablesAvailable === true + ? [{title: 'Obrigações a fornecedores', href: '/obrigacoes-fornecedores', icon: FileCheck2, fullPage: true}] + : []), { title: 'Pagamentos externos', href: '/pagamentos', diff --git a/resources/js/components/nav-main.tsx b/resources/js/components/nav-main.tsx index 80fbd29..a465aa4 100644 --- a/resources/js/components/nav-main.tsx +++ b/resources/js/components/nav-main.tsx @@ -39,10 +39,17 @@ export function NavMain({ } tooltip={{ children: item.title }} > - - {item.icon && } - {item.title} - + {item.fullPage ? ( + + {item.icon && } + {item.title} + + ) : ( + + {item.icon && } + {item.title} + + )} ))} diff --git a/resources/js/components/sgc-shell.tsx b/resources/js/components/sgc-shell.tsx index caf7494..caf87e7 100644 --- a/resources/js/components/sgc-shell.tsx +++ b/resources/js/components/sgc-shell.tsx @@ -9,6 +9,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { UserMenuContent } from '@/components/user-menu-content'; +import { SgcWorkspaceShell } from '@/components/sgc-workspace-shell'; type Entry = { label: string; path: string; demo: string; roles?: string[] }; const readers = ['admin', 'procurement', 'finance', 'department_head']; @@ -159,7 +160,7 @@ const groups: { label: string; entries: Entry[] }[] = [ }, ]; -export function SgcShell({ +function SgcPreviewShell({ children, demo = false, }: { @@ -320,3 +321,17 @@ export function SgcShell({ ); } + +export function SgcShell({ + children, + demo = false, +}: { + children: ReactNode; + demo?: boolean; +}) { + return demo ? ( + {children} + ) : ( + {children} + ); +} diff --git a/resources/js/components/sgc-workspace-shell.tsx b/resources/js/components/sgc-workspace-shell.tsx new file mode 100644 index 0000000..64b3e92 --- /dev/null +++ b/resources/js/components/sgc-workspace-shell.tsx @@ -0,0 +1,369 @@ +import { Link, usePage } from '@inertiajs/react'; +import { + ChevronDown, + LayoutDashboard, + Menu, + Plus, + Search, + X, +} from 'lucide-react'; +import { useEffect, useState } from 'react'; +import type { ReactNode } from 'react'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { UserMenuContent } from '@/components/user-menu-content'; +import { + activeProcurementEntry, + procurementNavigation, +} from '@/lib/procurement-navigation'; +import { roleLabels } from '@/types/procurement'; + +export function SgcWorkspaceShell({ children }: { children: ReactNode }) { + const page = usePage(); + const user = page.props.auth?.user; + const [mobileOpen, setMobileOpen] = useState(false); + const [search, setSearch] = useState(''); + const [expanded, setExpanded] = useState>({}); + const groups = procurementNavigation( + user?.purchasing_role ?? '', + page.props.payablesAvailable === true, + ); + const activeId = activeProcurementEntry(groups, page.url); + const activeGroup = groups.find((group) => + group.entries.some((entry) => entry.id === activeId), + ); + const activeGroupId = activeGroup?.id; + useEffect(() => { + if (activeGroupId) + setExpanded((previous) => + previous[activeGroupId] + ? previous + : { ...previous, [activeGroupId]: true }, + ); + }, [activeId, activeGroupId]); + const normalize = (text: string) => + text + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLocaleLowerCase('pt'); + const query = normalize(search.trim()); + const visibleGroups = groups + .map((group) => ({ + ...group, + entries: group.entries.filter( + (entry) => + !query || + normalize(group.label + ' ' + entry.label).includes(query), + ), + })) + .filter((group) => group.entries.length); + const navigate = () => { + setMobileOpen(false); + setSearch(''); + }; + const brand = ( + + + SGC + + + + Compras + + + Sistema de Gestão de Compras + + + + ); + const searchField = (mobile = false) => ( +
+
+ ); + const navigation = ( + + ); + const sidebarFooter = ( +
+

+ SGC · Espaço de trabalho +

+ + Aparência + + · + + Página inicial + +
+ ); + return ( +
+ + Saltar para o conteúdo + + + + + + Menu de Compras + + Escolhe uma área da aplicação ou procura pelo nome. + + + {brand} + {searchField(true)} + {navigation} + {sidebarFooter} + + +
+
+
+ +
+

+ COMPRAS +

+

+ {page.url.split('?')[0] === '/dashboard' + ? 'Painel inicial' + : (activeGroup?.label ?? + 'Espaço de trabalho')} +

+
+
+ {searchField()} +
+ + {user && ( + + + + + + + + + )} +
+
+
+ {children} +
+
+ SGC · Sistema de Gestão de Compras +
+
+
+ ); +} diff --git a/resources/js/lib/procurement-navigation.ts b/resources/js/lib/procurement-navigation.ts new file mode 100644 index 0000000..636b77d --- /dev/null +++ b/resources/js/lib/procurement-navigation.ts @@ -0,0 +1,267 @@ +import { + BadgeCheck, + ClipboardList, + Scale, + ShieldCheck, + Users, + Wallet, + Warehouse, +} from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; + +export type ProcurementEntry = { + id: string; + label: string; + path: string; + roles?: string[]; + fullPage?: boolean; +}; +export type ProcurementGroup = { + id: string; + label: string; + icon: LucideIcon; + entries: ProcurementEntry[]; +}; +const readers = ['admin', 'procurement', 'finance', 'department_head']; +const finance = ['admin', 'finance']; +const inventory = [...readers, 'warehouse']; + +export function procurementNavigation( + role: string, + payables: boolean, +): ProcurementGroup[] { + const groups: ProcurementGroup[] = [ + { + id: 'requests', + label: 'Requisições', + icon: ClipboardList, + entries: [ + { + id: 'new_request', + label: 'Nova requisição', + path: '/requisicoes/criar', + }, + { + id: 'my_requests', + label: 'Minhas requisições', + path: '/requisicoes?view=mine', + }, + { + id: 'my_drafts', + label: 'Os meus rascunhos', + path: '/requisicoes?view=mine&status=draft', + }, + { + id: 'all_requests', + label: 'Todas as autorizadas', + path: '/requisicoes?view=all', + }, + { id: 'planning', label: 'Planeamento', path: '/planeamento' }, + ], + }, + { + id: 'approvals', + label: 'Aprovações', + icon: BadgeCheck, + entries: [ + { + id: 'pending_approvals', + label: 'Pendentes para mim', + path: '/requisicoes?view=approvals', + roles: readers, + }, + { + id: 'decisions', + label: 'Minhas decisões', + path: '/requisicoes?view=decided', + roles: readers, + }, + ], + }, + { + id: 'sourcing', + label: 'Consultas e adjudicações', + icon: Scale, + entries: [ + { + id: 'quotations', + label: 'Cotações e comparação', + path: '/cotacoes', + roles: readers, + }, + { + id: 'orders', + label: 'Adjudicações e ordens', + path: '/ordens', + roles: readers, + }, + { + id: 'purchase_review', + label: 'Revisão e cancelamento', + path: '/revisao-compras', + }, + ], + }, + { + id: 'suppliers', + label: 'Fornecedores', + icon: Users, + entries: [ + { + id: 'suppliers', + label: 'Cadastro e documentos', + path: '/fornecedores', + roles: readers, + }, + ], + }, + { + id: 'receiving', + label: 'Recepções e armazém', + icon: Warehouse, + entries: [ + { + id: 'receipts', + label: 'Recepções e divergências', + path: '/recepcoes', + roles: inventory, + }, + { + id: 'stock', + label: 'Artigos e stock', + path: '/stock', + roles: ['admin', 'procurement', 'finance', 'warehouse'], + }, + { + id: 'stock_operations', + label: 'Saídas e inventários', + path: '/stock/operacoes', + roles: ['admin', 'procurement', 'finance', 'warehouse'], + }, + { + id: 'equipment', + label: 'Equipamentos e peças', + path: '/equipamentos', + roles: inventory, + }, + ], + }, + { + id: 'finance', + label: 'Finanças', + icon: Wallet, + entries: [ + { + id: 'invoices', + label: 'Facturas e conferência', + path: '/facturas', + roles: finance, + }, + ...(payables + ? [ + { + id: 'payables', + label: 'Obrigações a fornecedores', + path: '/obrigacoes-fornecedores', + roles: finance, + fullPage: true, + }, + ] + : []), + { + id: 'payments', + label: 'Pagamentos externos', + path: '/pagamentos', + roles: finance, + }, + { + id: 'budgets', + label: 'Orçamentos e reforços', + path: '/orcamentos', + roles: finance, + }, + { + id: 'investment', + label: 'Regras de investimento', + path: '/regras-investimento', + roles: ['admin', 'procurement', 'finance'], + }, + ], + }, + { + id: 'administration', + label: 'Administração e controlo', + icon: ShieldCheck, + entries: [ + { + id: 'company', + label: 'Empresa, equipa e centros', + path: '/empresa', + roles: ['admin'], + }, + { + id: 'admission_policy', + label: 'Política de admissão', + path: '/politica-admissao', + roles: ['admin'], + }, + { + id: 'recommendation_policy', + label: 'Política de recomendação', + path: '/politica-recomendacao', + roles: ['admin'], + }, + { + id: 'conflicts', + label: 'Conflitos de interesses', + path: '/conflitos', + }, + ], + }, + ]; + return groups + .map((group) => ({ + ...group, + entries: group.entries.filter( + (entry) => !entry.roles || entry.roles.includes(role), + ), + })) + .filter((group) => group.entries.length); +} + +export function activeProcurementEntry( + groups: ProcurementGroup[], + url: string, +): string | undefined { + const current = new URL(url, 'http://local'); + if (/^\/ordens\/\d+\/recepcoes/.test(current.pathname)) return 'receipts'; + if (/^\/requisicoes\/\d+\/adjudicacao(?:\/|$)/.test(current.pathname)) + return 'orders'; + if ( + /^\/requisicoes\/\d+\/(cotacoes|abertura|qualificacao|admissao|recomendacao|relatorio)(?:\/|$)/.test( + current.pathname, + ) + ) + return 'quotations'; + const matches = groups + .flatMap((group) => group.entries) + .filter((entry) => { + const target = new URL(entry.path, 'http://local'); + if ( + current.pathname !== target.pathname && + !current.pathname.startsWith(target.pathname + '/') + ) + return false; + return [...target.searchParams].every( + ([key, value]) => + (current.searchParams.get(key) ?? + (key === 'view' ? 'all' : null)) === value, + ); + }); + const specificity = (path: string) => { + const target = new URL(path, 'http://local'); + return target.pathname.length * 100 + target.searchParams.size; + }; + matches.sort((a, b) => specificity(b.path) - specificity(a.path)); + return matches[0]?.id; +} diff --git a/resources/js/pages/invoices/index.tsx b/resources/js/pages/invoices/index.tsx index 38f163c..5d61652 100644 --- a/resources/js/pages/invoices/index.tsx +++ b/resources/js/pages/invoices/index.tsx @@ -48,10 +48,12 @@ export default function Invoices({ invoices, orders, canManage, + payablesAvailable = false, }: { invoices: Page; orders: { id: number; number: string; supplier: string; items: Source[] }[]; canManage: boolean; + payablesAvailable?: boolean; }) { const { auth } = usePage().props; const [orderId, setOrderId] = useState(''); @@ -69,6 +71,7 @@ export default function Invoices({ return ( <> + {payablesAvailable && Obrigações a fornecedores — ensaio sintético}
+ {payablesAvailable && ( +

+ + Consultar ou preparar obrigação desta factura + +

+ )}
    {invoice.items.map((l) => (
  • diff --git a/resources/js/pages/requisitions/index.tsx b/resources/js/pages/requisitions/index.tsx index 667bcd8..9e39c4d 100644 --- a/resources/js/pages/requisitions/index.tsx +++ b/resources/js/pages/requisitions/index.tsx @@ -20,21 +20,48 @@ type Props = { next_page_url: string | null; total: number; }; - filters: { search?: string; status?: string }; + filters: { search?: string; status?: string; view?: string }; }; export default function Index({ requisitions, filters }: Props) { const form = useForm({ search: filters.search ?? '', status: filters.status ?? '', + view: filters.view ?? 'all', }); + const views: Record = { + all: { + title: 'Requisições autorizadas', + description: + 'Consulte os pedidos disponíveis para o seu perfil e acompanhe o estado de cada processo.', + }, + mine: { + title: + filters.status === 'draft' + ? 'Os meus rascunhos' + : 'Minhas requisições', + description: + 'Pedidos criados por si, desde o rascunho até à decisão.', + }, + approvals: { + title: 'Pendentes para mim', + description: + 'Pedidos em que a próxima etapa pode ser decidida por si. A elegibilidade é conferida pelo servidor.', + }, + decided: { + title: 'Minhas decisões', + description: + 'Histórico dos pedidos em que registou uma aprovação ou rejeição.', + }, + }; + const view = views[filters.view ?? 'all'] ?? views.all; return ( <> - +
    @@ -52,6 +79,21 @@ export default function Index({ requisitions, filters }: Props) { form.get('/requisicoes', { preserveState: true }); }} > + ; icon?: LucideIcon | null; isActive?: boolean; + fullPage?: boolean; }; diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 6987b35..df33cb3 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -43,6 +43,9 @@ + @if(app()->environment('payable-testing')) + + @endif diff --git a/routes/evaluation.php b/routes/evaluation.php new file mode 100644 index 0000000..d231712 --- /dev/null +++ b/routes/evaluation.php @@ -0,0 +1,80 @@ +environment('payable-testing') + && in_array($request->ip(), ['127.0.0.1', '::1'], true) + && in_array($request->getHost(), ['127.0.0.1', 'localhost', '::1'], true), 404); + $db = DB::connection(); + abort_unless($db->getDriverName() === 'pgsql' && $db->getConfig('host') === '127.0.0.1' + && preg_match('/^compras_receipts_test_[a-f0-9]{12}$/D', $db->getDatabaseName()), 404); + $identity = $db->selectOne('select current_database() as db, current_user as role'); + abort_unless($identity->db === $db->getDatabaseName() + && $identity->role === 'payable_qa_'.substr($identity->db, -12), 404); + return User::where('email', 'preparacao-obrigacoes@example.test')->firstOrFail(); +}; + +$evaluationPages = [ + '/dashboard' => 'Visão geral', '/empresa' => 'Empresa e equipa', + '/requisicoes' => 'Requisições', '/planeamento' => 'Planeamento', + '/fornecedores' => 'Fornecedores', '/cotacoes' => 'Cotações', '/ordens' => 'Ordens de compra', + '/recepcoes' => 'Recepções', '/stock' => 'Stock e movimentos', '/stock/operacoes' => 'Saídas e inventários', + '/equipamentos' => 'Equipamentos e peças', '/orcamentos' => 'Orçamentos', + '/regras-investimento' => 'Regras de investimento', '/facturas' => 'Facturas', + '/pagamentos' => 'Pagamentos externos', '/obrigacoes-fornecedores' => 'Obrigações a fornecedores', + '/conflitos' => 'Conflitos de interesses', '/revisao-compras' => 'Revisão e cancelamento', + '/politica-admissao' => 'Política de admissão', '/politica-recomendacao' => 'Política de recomendação', +]; + +Route::get('/avaliar', function (Request $request) use ($evaluationScope, $evaluationPages) { + $evaluationScope($request); + $token = htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8'); + $html = 'Explorar Compras

    Explorar Compras

    Entrada local sem palavra-passe. Os exemplos e alterações ficam guardados nesta instalação de testes.

    Acesso de proprietário

    Abre a administração e as páginas dos módulos abaixo. As regras dos processos e as funções operacionais continuam visíveis.

    '; + if ($request->user()) { + $html .= '

    Áreas da aplicação

      '; + foreach ($evaluationPages as $path => $label) { + $html .= '
    • '.$label.'
    • '; + } + $html .= '
    '; + } + $html .= '

    Experimentar funções operacionais

    Para preparar ou decidir passos específicos, podes entrar nestes perfis sintéticos sem pedir contas ou credenciais.

    '; + foreach (PurchasingRole::cases() as $role) { + if ($role === PurchasingRole::Admin) continue; + $html .= '
    '; + } + return response($html.'

    Ambiente sintético partilhado. Sem transferências bancárias. Não altera a instalação habitual na porta 8015. Aprovações próprias e restantes regras de integridade não são dispensadas.

    ')->header('Cache-Control', 'no-store')->header('X-Robots-Tag', 'noindex, nofollow'); +}); + +Route::post('/avaliar/entrar', function (Request $request) use ($evaluationScope) { + $source = $evaluationScope($request); + $data = $request->validate(['role' => ['required', \Illuminate\Validation\Rule::enum(PurchasingRole::class)]]); + abort_if(array_diff(array_keys($request->all()), ['role', '_token']) !== [], 422); + $role = PurchasingRole::from($data['role']); + $user = DB::transaction(function () use ($source, $role) { + DB::select('select pg_advisory_xact_lock(?)', [80502910]); + $user = User::firstOrCreate(['email' => 'avaliacao-'.$role->value.'@example.test'], + ['name' => $role === PurchasingRole::Admin ? 'Proprietário — avaliação local' : $role->label().' — avaliação local', 'password' => bin2hex(random_bytes(32))]); + abort_if($user->tenant_id !== null && (int) $user->tenant_id !== (int) $source->tenant_id, 403); + $user->forceFill(['tenant_id' => $source->tenant_id, 'department_id' => $source->department_id, + 'purchasing_role' => $role, 'email_verified_at' => $user->email_verified_at ?? now()])->save(); + // The isolated payables workspace has its own explicit action grants. + foreach ($role === PurchasingRole::Admin ? ['view', 'prepare', 'validate'] : ['view'] as $action) { + DB::table('pa_access')->insertOrIgnore(['user_id' => $user->id, 'tenant_id' => $source->tenant_id, 'company_id' => 0, 'unit_id' => 0, 'person' => 'evaluation-person-'.$user->id, 'action' => $action]); + } + return $user; + }); + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + Auth::login($user, false); + $request->session()->regenerate(); + app(\App\Services\RequisitionWorkflow::class)->audit($user, 'evaluation.entered', null, null, ['role' => $role->value, 'synthetic' => true]); + return redirect('/dashboard'); +})->middleware('throttle:30,1'); diff --git a/routes/web.php b/routes/web.php index 3cc9d13..7e6425f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -28,6 +28,10 @@ use App\Http\Controllers\SupplierDocumentController; use App\Http\Middleware\EnsureCompanyIsConfigured; use Illuminate\Support\Facades\Route; +if (app()->environment('payable-testing')) { + require __DIR__.'/evaluation.php'; +} + Route::inertia('/', 'home', ['auth' => ['user' => null]])->name('home'); require __DIR__.'/preview.php'; @@ -37,6 +41,9 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::post('empresa/iniciar', [CompanyController::class, 'store'])->name('company.store'); Route::middleware(EnsureCompanyIsConfigured::class)->group(function () { + Route::get('obrigacoes-fornecedores', [\App\Http\Controllers\SupplierPayablesController::class,'page']); + Route::get('api/supplier-payables', [\App\Http\Controllers\SupplierPayablesController::class,'index']); + Route::post('api/supplier-payables/{action}', [\App\Http\Controllers\SupplierPayablesController::class,'command'])->whereIn('action',['prepare','validate','prepare_history','validate_history']); Route::get('politica-recomendacao', [FormalRecommendationController::class, 'policy'])->name('recommendations.policy'); Route::post('politica-recomendacao', [FormalRecommendationController::class, 'configure']); Route::get('requisicoes/{requisition}/recomendacao', [FormalRecommendationController::class, 'show'])->name('recommendations.show'); diff --git a/scripts/evaluation-access-check.mjs b/scripts/evaluation-access-check.mjs new file mode 100644 index 0000000..607392a --- /dev/null +++ b/scripts/evaluation-access-check.mjs @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; + +const base = 'http://127.0.0.1:8005'; +const cookies = new Map(); +async function request(path, method = 'GET', body) { + const response = await fetch(base + path, {method, redirect: 'manual', headers: { + Accept: 'application/json', Cookie: [...cookies].map(([k,v]) => `${k}=${v}`).join('; '), + ...(body ? {'Content-Type': 'application/x-www-form-urlencoded'} : {}), + }, ...(body ? {body: new URLSearchParams(body)} : {})}); + for (const cookie of response.headers.getSetCookie()) { + const pair = cookie.split(';', 1)[0], index = pair.indexOf('='); + cookies.set(pair.slice(0,index),pair.slice(index+1)); + } + return {status: response.status, text: await response.text(), location: response.headers.get('location')}; +} +const token = text => text.match(/name="_token" value="([^"]+)"/)[1]; +async function enter(role) { + const page = await request('/avaliar'); + assert.equal(page.status, 200); + const entered = await request('/avaliar/entrar', 'POST', {role, _token: token(page.text)}); + assert.equal(entered.status, 302, role); + assert.equal(new URL(entered.location).pathname, '/dashboard'); + assert.equal((await request('/dashboard')).status, 200); +} +let page = await request('/avaliar'); +assert.equal((await request('/avaliar/entrar','POST',{role:'admin'})).status,419); +assert.equal((await request('/avaliar/entrar','POST',{role:'root',_token:token(page.text)})).status,422); +assert.equal((await request('/avaliar/entrar','POST',{role:'admin',user_id:'1',_token:token(page.text)})).status,422); +await enter('admin'); +const modules = ['/dashboard','/empresa','/requisicoes','/planeamento','/fornecedores','/cotacoes','/ordens', + '/recepcoes','/stock','/stock/operacoes','/equipamentos','/orcamentos','/regras-investimento','/facturas', + '/pagamentos','/obrigacoes-fornecedores','/conflitos','/revisao-compras','/politica-admissao','/politica-recomendacao']; +for (const path of modules) assert.equal((await request(path)).status,200,path); +const roles = ['requester','department_head','procurement','finance','warehouse','admin']; +for (const role of roles) await enter(role); +page = await request('/avaliar'); +assert.equal((await request('/logout','POST',{_token:token(page.text)})).status,204); +assert.equal((await request('/dashboard')).status,401); +assert.equal((await request('/empresa')).status,401); +const operational = await fetch('http://127.0.0.1:8015/avaliar', {redirect:'manual'}).catch(() => null); +if (operational) assert.equal(operational.status,404,'Evaluation entry must not exist on the normal installation'); +await fs.mkdir('docs/proofs',{recursive:true}); +await fs.writeFile('docs/proofs/evaluation-access.json',JSON.stringify({checked_at:new Date().toISOString(),base, + owner_modules:modules,roles,csrf_enforced:true,arbitrary_role_refused:true,arbitrary_user_refused:true,logout:true, + business_commands:0,real_accounts_changed:false,operational_entry_status:operational?.status??'not running', + visual_review:false},null,2)+'\n'); +console.log('Owner entry, 20 module pages, six roles, CSRF, rejected identifiers and logout passed.'); diff --git a/scripts/invoice-payable-link-check.mjs b/scripts/invoice-payable-link-check.mjs new file mode 100644 index 0000000..b95e808 --- /dev/null +++ b/scripts/invoice-payable-link-check.mjs @@ -0,0 +1,39 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +const cookies = new Map(), base = 'http://127.0.0.1:8005'; +async function request(path, method = 'GET', body) { + const r = await fetch(base + path, {method, redirect:'manual', headers:{Accept:'application/json', + Cookie:[...cookies].map(([k,v])=>`${k}=${v}`).join('; '), + ...(body?{'Content-Type':'application/x-www-form-urlencoded'}:{})}, + ...(body?{body:new URLSearchParams(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));} + return {status:r.status,text:await r.text()}; +} +const landing=await request('/avaliar'); +const csrf=landing.text.match(/name="_token" value="([^"]+)"/)[1]; +assert.equal((await request('/avaliar/entrar','POST',{role:'admin',_token:csrf})).status,302); +const before=JSON.parse((await request('/api/supplier-payables')).text); +assert(before.documents.length); +for(const doc of before.documents) { + const filtered=await request('/api/supplier-payables?invoice='+doc.local_id); + assert.equal(filtered.status,200); + const data=JSON.parse(filtered.text); + assert.deepEqual(data.documents,[doc]); + const page=await request('/obrigacoes-fornecedores?invoice='+doc.local_id); + assert.equal(page.status,200); + assert(page.text.includes('href="/facturas"')); + assert(page.text.includes('Voltar às facturas')); + assert(page.text.includes(doc.number)); +} +for(const query of ['0','-1','abc','1.5']) { + assert.equal((await request('/api/supplier-payables?invoice='+query)).status,422); + assert.equal((await request('/obrigacoes-fornecedores?invoice='+query)).status,422); +} +assert.equal((await request('/api/supplier-payables?invoice=2147483647')).status,404); +const after=JSON.parse((await request('/api/supplier-payables')).text); +assert.deepEqual(after,before,'Read-only navigation must preserve all documents and histories'); +await fs.writeFile('docs/proofs/invoice-payable-link.json',JSON.stringify({checked_at:new Date().toISOString(), + documents_checked:before.documents.length,per_invoice_entry:true,return_to_invoices:true, + invalid_identifiers_refused:true,unknown_invoice_refused:true,full_listing_unchanged:true,business_commands:0, + visual_review:false},null,2)+'\n'); +console.log('Per-invoice payable navigation, invalid/unknown identifiers and unchanged history passed.'); diff --git a/scripts/menu-links-check.mjs b/scripts/menu-links-check.mjs new file mode 100644 index 0000000..e7cd66a --- /dev/null +++ b/scripts/menu-links-check.mjs @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import {procurementNavigation, activeProcurementEntry} from '../resources/js/lib/procurement-navigation.ts'; + +const base = 'http://127.0.0.1:8005'; +const cookies = new Map(); +let version = ''; +async function request(path, {method = 'GET', body, inertia = false} = {}) { + const response = await fetch(base + path, {method, redirect: 'manual', headers: { + Accept: 'application/json', Cookie: [...cookies].map(([k,v]) => `${k}=${v}`).join('; '), + ...(body ? {'Content-Type': 'application/x-www-form-urlencoded'} : {}), + ...(inertia ? {'X-Inertia': 'true', 'X-Inertia-Version': version} : {}), + }, ...(body ? {body: new URLSearchParams(body)} : {})}); + for (const cookie of response.headers.getSetCookie()) { + const pair = cookie.split(';', 1)[0], index = pair.indexOf('='); + cookies.set(pair.slice(0,index),pair.slice(index+1)); + } + const text = await response.text(); + let data; + try {data = JSON.parse(text);} catch {} + if (!data) { + const page = text.match(/