Implementa regras de investimento por empresa com dupla aprovacao

This commit is contained in:
2026-10-28 16:57:45 +02:00
parent 153ba2c1af
commit 95dd2cc1ed
37 changed files with 1418 additions and 28 deletions
+14 -7
View File
@@ -36,6 +36,7 @@ class PurchaseWorkflow
$this->sourceApproved($requisition, $rules);
$quote = Quotation::query()->where('requisition_id', $requisition->id)->lockForUpdate()->findOrFail($data['quotation_id']);
$result = $this->checkComparison($user, $requisition, $quote);
$investment = app(InvestmentWorkflow::class)->check($requisition, $quote);
app(BudgetWorkflow::class)->record($user, $requisition, 'award', $quote->total_minor, $quote->subtotal_minor);
$award = PurchaseAward::query()->create([
'tenant_id' => $user->tenant_id, 'requisition_id' => $requisition->id, 'quotation_id' => $quote->id,
@@ -43,8 +44,9 @@ class PurchaseWorkflow
'delivery_location' => $data['delivery_location'], 'status' => 'pending',
'subtotal_minor' => $quote->subtotal_minor, 'tax_minor' => $quote->tax_minor, 'total_minor' => $quote->total_minor,
'approval_snapshot' => $rules, 'decision_matrix' => $result['rows'], 'comparison_hash' => $result['fingerprint'],
'investment_snapshot' => $investment,
]);
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $quote->total_minor : $quote->subtotal_minor);
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $quote->total_minor : $quote->subtotal_minor, $requisition->items()->where('purchase_type', 'equipment')->exists());
foreach ($roles as $position => $role) {
$award->approvals()->create(['tenant_id' => $user->tenant_id, 'position' => $position + 1, 'role' => $role]);
}
@@ -130,6 +132,8 @@ class PurchaseWorkflow
$snapshot = [
'development_only' => true, 'budget_checked' => false,
'budget_control' => app(BudgetWorkflow::class)->record($user, $requisition, 'commit', $quote->total_minor, $quote->subtotal_minor),
'investment_rules' => $award->investment_snapshot,
'equipment_requests' => $requisition->items()->where('purchase_type', 'equipment')->get()->map(fn ($line) => $line->only(['id', 'description', 'production_area']))->all(),
'company' => Tenant::query()->findOrFail($user->tenant_id)->only(['id', 'name']),
'requisition' => $requisition->only(['id', 'number', 'justification']),
'department' => $requisition->department->only(['id', 'name']),
@@ -166,6 +170,9 @@ class PurchaseWorkflow
$this->supportedItems($requisition);
$this->sourceApproved($requisition, $rules);
$quote = Quotation::query()->where('requisition_id', $requisition->id)->lockForUpdate()->findOrFail($award->quotation_id);
if (app(InvestmentWorkflow::class)->check($requisition, $quote) !== $award->investment_snapshot) {
throw ValidationException::withMessages(['investment' => 'A regra de investimento ou a base técnica/orçamental mudou. As aprovações anteriores não autorizam uma emissão com outra base.']);
}
$result = $this->checkComparison($user, $requisition, $quote);
if ($result['fingerprint'] !== $award->comparison_hash || $award->approval_snapshot !== $rules ||
$quote->subtotal_minor !== $award->subtotal_minor || $quote->tax_minor !== $award->tax_minor || $quote->total_minor !== $award->total_minor) {
@@ -214,13 +221,13 @@ class PurchaseWorkflow
/** @param array{first_limit_minor: int, second_limit_minor: int, approval_includes_tax: bool, rules_provisional: bool} $rules
* @return list<string>
*/
private function roles(array $rules, int $amount): array
private function roles(array $rules, int $amount, bool $equipment = false): array
{
$roles = ['department_head'];
if ($amount > $rules['first_limit_minor']) {
$roles[] = 'procurement';
}
if ($amount > $rules['second_limit_minor']) {
if ($amount > $rules['second_limit_minor'] || $equipment) {
$roles[] = 'finance';
}
@@ -230,7 +237,7 @@ class PurchaseWorkflow
/** @param array{first_limit_minor: int, second_limit_minor: int, approval_includes_tax: bool, rules_provisional: bool} $rules */
private function sourceApproved(Requisition $requisition, array $rules): void
{
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $requisition->total_minor : $requisition->subtotal_minor);
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $requisition->total_minor : $requisition->subtotal_minor, $requisition->items()->where('purchase_type', 'equipment')->exists());
$steps = $requisition->steps()->get();
$seen = [$requisition->requester_id];
if ($requisition->status !== 'approved' || $steps->count() !== count($roles)) {
@@ -248,7 +255,7 @@ class PurchaseWorkflow
private function validateFinalApprovals(PurchaseAward $award, Requisition $requisition): void
{
$rules = $this->rules($requisition);
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $award->total_minor : $award->subtotal_minor);
$roles = $this->roles($rules, $rules['approval_includes_tax'] ? $award->total_minor : $award->subtotal_minor, $requisition->items()->where('purchase_type', 'equipment')->exists());
$steps = $award->approvals()->get();
$seen = [$requisition->requester_id, $award->created_by];
if ($steps->count() !== count($roles)) {
@@ -268,10 +275,10 @@ class PurchaseWorkflow
private function supportedItems(Requisition $requisition): void
{
if ($requisition->items()->where('purchase_type', 'equipment')->exists()) {
throw ValidationException::withMessages(['purchase_type' => 'O limite próprio de investimento e a sua regra de aplicação ainda não foram definidos pela empresa. A validação técnica e Finanças não dispensam essa decisão; adjudicação e emissão de equipamento bloqueadas.']);
app(InvestmentWorkflow::class)->requireCurrent();
}
foreach ($requisition->items()->get() as $line) {
if (! in_array($line->purchase_type, ['consumable', 'service', 'other', 'spare_part']) || ($line->purchase_type === 'spare_part' && (! $line->equipment_id || ! $line->spare_part_id))) {
if (! in_array($line->purchase_type, ['consumable', 'service', 'other', 'spare_part', 'equipment']) || ($line->purchase_type === 'spare_part' && (! $line->equipment_id || ! $line->spare_part_id))) {
throw ValidationException::withMessages(['purchase_type' => 'Tipo não suportado ou peça sem equipamento/artigo associado. Complete o cadastro antes da compra.']);
}
app(EquipmentWorkflow::class)->line($line->toArray(), $requisition->department_id, $requisition->priority);