|null */ public function record(User $user, Requisition $request, string $kind, int $amount = 0, ?int $subtotal = null): ?array { abort_unless(auth()->id() === $user->id && $user->tenant_id !== null && $request->tenant_id === $user->tenant_id, 403); if (DB::connection()->transactionLevel() === 0) { throw new \LogicException('Movimentos de orçamento exigem transacção.'); } app(ConflictGuard::class)->lockCompany($user); $existing = BudgetEntry::query()->where('requisition_id', $request->id)->orderBy('id')->first(); $budget = $existing ? Budget::query()->lockForUpdate()->findOrFail($existing->budget_id) : Budget::query()->where('cost_center_id', $request->cost_center_id)->where('year', now()->year)->lockForUpdate()->first(); // Unconfigured legacy processes remain development documents, explicitly unchecked. if (! $budget) { return null; } if (! $budget->approved_by || ($kind !== 'release' && (int) $budget->year !== now()->year)) { throw ValidationException::withMessages(['budget' => 'Dotação pendente de aprovação ou exercício encerrado.']); } if (! $budget->includes_tax && in_array($kind, ['reserve', 'award', 'commit'])) { $amount = $subtotal ?? throw new \LogicException('Subtotal necessário para a base sem IVA.'); } if (BudgetEntry::query()->where('requisition_id', $request->id)->where('kind', $kind)->exists()) { return ['budget_id' => $budget->id, 'year' => (int) $budget->year]; } $own = BudgetEntry::query()->where('requisition_id', $request->id); $reserved = (int) (clone $own)->sum('reserved_minor'); $committed = (int) $own->sum('committed_minor'); if ($committed !== 0 || ($kind !== 'reserve' && ! $existing)) { throw ValidationException::withMessages(['budget' => 'Processo sem reserva válida ou já comprometido.']); } [$reserveDelta, $commitDelta] = match ($kind) { 'reserve', 'award' => [$amount - $reserved, 0], 'release' => [-$reserved, 0], 'commit' => [-$reserved, $amount], default => throw new \LogicException('Movimento orçamental inválido.'), }; $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) { 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()]); app(RequisitionWorkflow::class)->audit($user, 'budget.'.$kind, $request, null, ['budget_id' => $budget->id, 'reserved_minor' => $reserveDelta, 'committed_minor' => $commitDelta]); return ['budget_id' => $budget->id, 'year' => (int) $budget->year]; } }