This commit is contained in:
2026-11-03 11:51:44 +02:00
parent badaada043
commit e753f7a0a4
309 changed files with 14537 additions and 529 deletions
+66 -10
View File
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Enums\PurchasingRole;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
@@ -75,16 +76,26 @@ final class SupplierPayables
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];
$result[] = self::paymentFacts($payment, $registration, $decision);
}
return $result;
}
/** @return array<string,mixed> */
public static function paymentFacts(\stdClass $payment, ?\stdClass $registration, ?\stdClass $decision): array
{
if (! $registration || ($payment->status !== 'pending' && ! $decision)) {
throw new \DomainException('Histórico sem auditoria compatível; preservar os originais para conferência.');
}
return ['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 array<string,mixed> */
public function document(User $u, int $id): array
{
@@ -94,12 +105,49 @@ final class SupplierPayables
$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,
return self::sourceSnapshot($invoice, $order, $supplier, $audit, array_values($invoice->items()->orderBy('id')->get()->map(fn ($l) => self::lineSnapshot($l))->all()),
DB::table('external_payments')->where(['tenant_id' => $u->tenant_id, 'invoice_id' => $id])->count());
}
/** @return array<string,mixed> */
public static function lineSnapshot(InvoiceItem $line): array
{
return ['id' => $line->id, 'order_line' => $line->purchase_order_item_id, 'quantity_milli' => (int) $line->quantity_milli,
'subtotal_minor' => (int) $line->subtotal_minor, 'tax_minor' => (int) $line->tax_minor, 'snapshot' => $line->snapshot];
}
/** @param list<array<string,mixed>> $lines
* @return array<string,mixed>
*/
public static function sourceSnapshot(Invoice $invoice, ?\stdClass $order, ?\stdClass $supplier, ?\stdClass $audit, array $lines, int $paymentCount): array
{
return ['local_id' => $invoice->id, 'instance' => config('common-payables.source_instance'), 'company' => (int) $invoice->tenant_id, 'document' => $invoice->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()];
'lines' => $lines, 'payment_count' => $paymentCount];
}
/** Fresh read authorization; unlike commands, it never takes writer locks.
* @return array{actor:User,scope:array<string,int>,person:string}
*/
public function readContext(User $u): array
{
abort_unless(self::enabled() && auth()->id() === $u->id && $u->tenant_id !== null, 403);
$fresh = User::query()->where('tenant_id', $u->tenant_id)->findOrFail($u->id);
$access = app(OrganizationAccess::class);
$contexts = app(WorkContext::class);
abort_if($access->maintenanceOnly($fresh), 403);
if ($contexts->enforced($fresh)) {
abort_unless(app()->bound('request') && request()->hasSession() && $contexts->active(request()) === 'procurement.internal', 403, 'Seleccione Compras internas para consultar obrigações.');
abort_unless(array_intersect($access->permissions($fresh, 'procurement.internal'), ['finance', 'admin']) !== [], 403);
}
abort_unless($fresh->hasFunctions([PurchasingRole::Finance, PurchasingRole::Admin]) && $this->can($fresh, 'view'), 403);
$scope = $this->scope($fresh);
$person = DB::table('pa_access')->where($scope)->where(['user_id' => $fresh->id, 'action' => 'view'])->value('person');
abort_unless(is_string($person) && $person !== '', 403);
return ['actor' => $fresh, 'scope' => $scope, 'person' => $person];
}
public function engine(User $u): Payables
@@ -118,9 +166,17 @@ final class SupplierPayables
}, fn (int $id) => $this->document($u, $id), self::historyEnabled() ? fn (int $id) => $this->paymentHistory($u, $id) : null);
}
/** @return array<string,mixed> */
public function listing(User $u, ?int $invoiceId = null): array
/** @param array<string,mixed> $options
* @return array<string,mixed>
*/
public function listing(User $u, ?int $invoiceId = null, array $options = []): array
{
if (config('common-payables.paged_read_enabled') === true) {
abort_unless(in_array('listingPage', get_class_methods(Payables::class), true), 503, 'Pacote financeiro sem leitura paginada; composição explícita pendente.');
return app(SupplierPayablePage::class)->listing($u, $invoiceId, $options);
}
abort_if($options !== [], 503, 'Consulta paginada ainda não activada nesta composição.');
$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')];