commit13
This commit is contained in:
@@ -5,22 +5,60 @@ namespace App\Services;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\CostCenter;
|
||||
use App\Models\Requisition;
|
||||
use App\Models\SupplyRecord;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Brick\Math\BigDecimal;
|
||||
use Brick\Math\RoundingMode;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Tempo\Purchasing\Internal\InternalRequisitions;
|
||||
use Tempo\Purchasing\Internal\RequisitionFailure;
|
||||
|
||||
class RequisitionWorkflow
|
||||
{
|
||||
/** @param array<string, mixed> $data */
|
||||
private function process(User $user): InternalRequisitions
|
||||
{
|
||||
return new InternalRequisitions(new InternalRequisitionPort($user, fn (array $data, ?Requisition $existing) => $this->persistDraft($user, $data, $existing)));
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $data */
|
||||
public function save(User $user, array $data, ?Requisition $existing = null): Requisition
|
||||
{
|
||||
return DB::transaction(function () use ($user, $data, $existing) {
|
||||
$key = $data['operation_key'] ?? null;
|
||||
$version = isset($data['process_version']) ? (int) $data['process_version'] : null;
|
||||
unset($data['operation_key'], $data['process_version']);
|
||||
$result = $this->execute($user, 'save', $existing?->id, $version, $key, $data);
|
||||
|
||||
return Requisition::query()->whereKey($result['id'])->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $input
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function execute(User $user, string $action, ?int $id, ?int $version, ?string $key, array $input = []): array
|
||||
{
|
||||
try {
|
||||
return $this->process($user)->command($action, $id, $version, $key, $input);
|
||||
} catch (RequisitionFailure $error) {
|
||||
abort_if($error->status !== 422, $error->status, $error->getMessage());
|
||||
throw ValidationException::withMessages(['requisition' => $error->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
private function persistDraft(User $user, array $data, ?Requisition $existing = null, ?SupplyRecord $automaticRule = null): Requisition
|
||||
{
|
||||
return DB::transaction(function () use ($user, $data, $existing, $automaticRule) {
|
||||
app(ConflictGuard::class)->lockCompany($user);
|
||||
$user = app(CurrentActor::class)->lock($user);
|
||||
abort_unless(app(OrganizationAccess::class)->canRequest($user), 403);
|
||||
$executor = $user;
|
||||
if ($automaticRule !== null) {
|
||||
abort_unless($existing === null, 403);
|
||||
$user = app(AutomaticPurchases::class)->requester($executor, $automaticRule);
|
||||
} else {
|
||||
abort_unless(app(OrganizationAccess::class)->canRequest($user), 403);
|
||||
}
|
||||
if (! $user->department_id || ! CostCenter::query()->whereKey($data['cost_center_id'])->where('department_id', $user->department_id)->exists()) {
|
||||
throw ValidationException::withMessages(['cost_center_id' => 'O departamento ou centro de custo mudou. Actualize o pedido antes de guardar.']);
|
||||
}
|
||||
@@ -39,9 +77,10 @@ class RequisitionWorkflow
|
||||
foreach ($lines as $line) {
|
||||
$line = app(RequisitionCatalogue::class)->line($user, $line, $oldLines);
|
||||
app(EquipmentWorkflow::class)->line($line, (int) $user->department_id, $data['priority']);
|
||||
$price = BigDecimal::of($line['unit_price'])->multipliedBy(100)->toScale(0)->toInt();
|
||||
$lineSubtotal = BigDecimal::of($line['quantity'])->multipliedBy($price)->toScale(0, RoundingMode::HalfUp)->toInt();
|
||||
$lineTax = BigDecimal::of($lineSubtotal)->multipliedBy($line['tax_rate'])->dividedBy(100, 0, RoundingMode::HalfUp)->toInt();
|
||||
$calculation = InternalRequisitions::line($line['quantity'], $line['unit_price'], $line['tax_rate']);
|
||||
$price = $calculation['unit_price_minor'];
|
||||
$lineSubtotal = $calculation['subtotal_minor'];
|
||||
$lineTax = $calculation['tax_minor'];
|
||||
$items[] = [
|
||||
'tenant_id' => $user->tenant_id,
|
||||
...(! empty($line['catalogue_entry_id']) ? ['catalogue_entry_id' => $line['catalogue_entry_id'], 'catalogue_snapshot' => $line['catalogue_snapshot']] : []),
|
||||
@@ -63,6 +102,7 @@ class RequisitionWorkflow
|
||||
'needed_by' => $data['needed_by'], 'justification' => $data['justification'],
|
||||
'subtotal_minor' => $subtotal, 'tax_minor' => $tax, 'total_minor' => $subtotal + $tax,
|
||||
...($existing ? [] : ['comparison_policy' => app(ComparisonPolicy::class)->company(Tenant::query()->findOrFail($user->tenant_id))]),
|
||||
...(Schema::hasColumn('requisitions', 'process_version') ? ['process_version' => $existing ? (int) $requisition->getAttribute('process_version') + 1 : 1] : []),
|
||||
])->save();
|
||||
if (! $existing) {
|
||||
$requisition->update(['number' => sprintf('REQ-%s-%06d', now()->format('Y'), $requisition->id)]);
|
||||
@@ -70,71 +110,29 @@ class RequisitionWorkflow
|
||||
$requisition->items()->delete();
|
||||
$requisition->items()->createMany($items);
|
||||
$requisition->load('items');
|
||||
$this->audit($user, $existing ? 'requisition.updated' : 'requisition.created', $requisition, $before, $requisition->toArray());
|
||||
$this->audit($executor, $automaticRule !== null ? 'requisition.automatically_created' : ($existing ? 'requisition.updated' : 'requisition.created'), $requisition, $before, $requisition->toArray() + ($automaticRule === null ? [] : ['automatic_rule_id' => $automaticRule->id, 'designated_requester_id' => $user->id]));
|
||||
|
||||
return $requisition;
|
||||
});
|
||||
}
|
||||
|
||||
public function submit(User $user, Requisition $requisition): void
|
||||
/** Only an independently activated rule may create a draft on behalf of its designated requester. */
|
||||
public function automaticDraft(User $executor, SupplyRecord $rule, array $data): Requisition
|
||||
{
|
||||
DB::transaction(function () use ($user, $requisition) {
|
||||
app(ConflictGuard::class)->lockCompany($user);
|
||||
$user = app(CurrentActor::class)->lock($user);
|
||||
abort_unless(app(OrganizationAccess::class)->canRequest($user), 403);
|
||||
$requisition = Requisition::query()->lockForUpdate()->findOrFail($requisition->id);
|
||||
abort_unless($requisition->requester_id === $user->id && $requisition->status === 'draft', 403);
|
||||
if ($user->department_id !== $requisition->department_id) {
|
||||
throw ValidationException::withMessages(['submission' => 'O departamento do solicitante mudou. Reveja o rascunho e o centro de custo antes de submeter.']);
|
||||
}
|
||||
app(ConflictGuard::class)->assertParticipation($user, $requisition);
|
||||
foreach ($requisition->items()->get() as $item) {
|
||||
app(EquipmentWorkflow::class)->line($item->toArray(), $requisition->department_id, $requisition->priority);
|
||||
}
|
||||
app(EquipmentWorkflow::class)->assertTechnical($requisition);
|
||||
if ($requisition->needed_by < today()) {
|
||||
throw ValidationException::withMessages(['submission' => 'Actualize a data de entrega antes de submeter.']);
|
||||
}
|
||||
$tenant = Tenant::query()->findOrFail($user->tenant_id);
|
||||
app(BudgetWorkflow::class)->record($user, $requisition, 'reserve', $requisition->total_minor, $requisition->subtotal_minor);
|
||||
$amount = $tenant->approval_includes_tax ? $requisition->total_minor : $requisition->subtotal_minor;
|
||||
$roles = ['department_head'];
|
||||
if ($amount > $tenant->first_limit_minor) {
|
||||
$roles[] = 'procurement';
|
||||
}
|
||||
if ($amount > $tenant->second_limit_minor || $requisition->items()->where('purchase_type', 'equipment')->exists()) {
|
||||
$roles[] = 'finance';
|
||||
}
|
||||
foreach ($roles as $position => $role) {
|
||||
$requisition->steps()->create(['tenant_id' => $user->tenant_id, 'position' => $position + 1, 'role' => $role]);
|
||||
}
|
||||
$snapshot = $tenant->only(['first_limit_minor', 'second_limit_minor', 'approval_includes_tax', 'rules_provisional']);
|
||||
$requisition->update(['status' => 'pending', 'submitted_at' => now(), 'approval_snapshot' => $snapshot]);
|
||||
$this->audit($user, 'requisition.submitted', $requisition, ['status' => 'draft'], ['status' => 'pending', 'rules' => $snapshot, 'roles' => $roles]);
|
||||
});
|
||||
$process = new InternalRequisitions(new InternalRequisitionPort($executor, fn (array $input, ?Requisition $existing) => $this->persistDraft($executor, $input, $existing, $rule), $rule));
|
||||
$result = $process->command('save', null, null, null, $data);
|
||||
|
||||
return Requisition::query()->findOrFail($result['id']);
|
||||
}
|
||||
|
||||
public function decide(User $user, Requisition $requisition, string $decision, ?string $comment): void
|
||||
public function submit(User $user, Requisition $requisition, ?string $operationKey = null, ?int $version = null): void
|
||||
{
|
||||
DB::transaction(function () use ($user, $requisition, $decision, $comment) {
|
||||
app(ConflictGuard::class)->lockCompany($user);
|
||||
$user = app(CurrentActor::class)->lock($user);
|
||||
$requisition = Requisition::query()->lockForUpdate()->findOrFail($requisition->id);
|
||||
$requisition->load('steps');
|
||||
abort_unless($requisition->canDecide($user), 403);
|
||||
if ($decision === 'approved') {
|
||||
app(EquipmentWorkflow::class)->assertTechnical($requisition);
|
||||
}
|
||||
$step = $requisition->steps->firstWhere('decision', 'pending');
|
||||
abort_if($step === null, 409);
|
||||
$step->update(['decision' => $decision, 'approver_id' => $user->id, 'comment' => $comment, 'decided_at' => now()]);
|
||||
$status = $decision === 'rejected' ? 'rejected' : ($requisition->steps()->where('decision', 'pending')->exists() ? 'pending' : 'approved');
|
||||
$requisition->update(['status' => $status]);
|
||||
if ($decision === 'rejected') {
|
||||
app(BudgetWorkflow::class)->record($user, $requisition, 'release');
|
||||
}
|
||||
$this->audit($user, 'approval.'.$decision, $requisition, ['status' => 'pending'], ['status' => $status, 'step_id' => $step->id, 'role' => $step->role, 'comment' => $comment]);
|
||||
});
|
||||
$this->execute($user, 'submit', $requisition->id, $version, $operationKey);
|
||||
}
|
||||
|
||||
public function decide(User $user, Requisition $requisition, string $decision, ?string $comment, ?string $operationKey = null, ?int $version = null): void
|
||||
{
|
||||
$this->execute($user, 'decide', $requisition->id, $version, $operationKey, ['decision' => $decision, 'comment' => $comment]);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed>|null $before
|
||||
|
||||
Reference in New Issue
Block a user