This commit is contained in:
2026-11-02 20:27:11 +02:00
parent b44378b899
commit badaada043
300 changed files with 17458 additions and 1056 deletions
+29 -3
View File
@@ -19,7 +19,28 @@ class InvoiceWorkflow
{
private function access(User $user): void
{
abort_unless(auth()->id() === $user->id && $user->tenant_id !== null && $user->hasFunctions([PurchasingRole::Finance]), 403);
abort_unless(auth()->id() === $user->id && $user->tenant_id !== null && $user->hasFunctions([PurchasingRole::Finance]) && ! app(OrganizationAccess::class)->maintenanceOnly($user), 403);
}
private function reviewerBaseEligible(User $user, Invoice $invoice): bool
{
return $user->tenant_id === $invoice->tenant_id && $invoice->created_by !== $user->id
&& $user->hasFunctions([PurchasingRole::Finance]) && ! app(OrganizationAccess::class)->maintenanceOnly($user);
}
/** Pending intervention, not a promise of a matching invoice or budget availability. */
public function reviewerEligible(User $user, Invoice $invoice): bool
{
if ($invoice->status !== 'pending' || ! $this->reviewerBaseEligible($user, $invoice)) {
return false;
}
$order = PurchaseOrder::query()->whereKey($invoice->purchase_order_id)->first();
if ($order === null) {
return false;
}
$request = Requisition::query()->visibleTo($user)->whereKey($order->requisition_id)->first();
return $request !== null && ! app(ConflictGuard::class)->processBlocked($request, $order->supplier_id, $user);
}
private function invalid(string $message): never
@@ -49,7 +70,10 @@ class InvoiceWorkflow
if ($quantity <= 0 || (int) $previous + $quantity > (int) $accepted || (int) $previous + $quantity > ReceiptWorkflow::milli($source->quantity)) {
$this->invalid('Quantidade superior à recepção aceite ainda não facturada. Serviços também exigem aceitação.');
}
if (BigDecimal::of((string) $input['unit_price'])->multipliedBy(100)->compareTo($source->unit_price_minor) !== 0 || BigDecimal::of((string) $input['tax_rate'])->compareTo((string) $source->tax_rate) !== 0 || trim((string) $input['unit']) !== $source->unit) {
$unit = array_key_exists('unit', $input) ? $input['unit'] : $source->unit;
$unitPrice = array_key_exists('unit_price', $input) ? $input['unit_price'] : (string) BigDecimal::of($source->unit_price_minor)->dividedBy(100, 2);
$taxRate = array_key_exists('tax_rate', $input) ? $input['tax_rate'] : $source->tax_rate;
if (BigDecimal::of((string) $unitPrice)->multipliedBy(100)->compareTo($source->unit_price_minor) !== 0 || BigDecimal::of((string) $taxRate)->compareTo((string) $source->tax_rate) !== 0 || trim((string) $unit) !== $source->unit) {
$this->invalid('Preço, unidade e IVA devem coincidir exactamente com a ordem. Não há tolerância ou excepção configurada.');
}
// Cumulative rounding ensures partial invoices sum exactly to the frozen order.
@@ -75,6 +99,7 @@ class InvoiceWorkflow
return DB::transaction(function () use ($user, $order, $data) {
app(ConflictGuard::class)->lockCompany($user);
$user = app(CurrentActor::class)->lock($user, [PurchasingRole::Finance]);
$this->access($user);
$order = PurchaseOrder::query()->lockForUpdate()->findOrFail($order->id);
$request = Requisition::query()->findOrFail($order->requisition_id);
app(ConflictGuard::class)->assertProcess($request, $order->supplier_id, $user);
@@ -104,10 +129,11 @@ class InvoiceWorkflow
public function validate(User $user, Invoice $invoice): void
{
$this->access($user);
abort_unless($invoice->tenant_id === $user->tenant_id && $invoice->created_by !== $user->id, 403);
abort_unless($this->reviewerBaseEligible($user, $invoice), 403);
DB::transaction(function () use ($user, $invoice) {
app(ConflictGuard::class)->lockCompany($user);
$user = app(CurrentActor::class)->lock($user, [PurchasingRole::Finance]);
$this->access($user);
$invoice = Invoice::query()->lockForUpdate()->findOrFail($invoice->id);
if ($invoice->status === 'validated') {
return;