159 lines
9.0 KiB
PHP
159 lines
9.0 KiB
PHP
<?php
|
|
|
|
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 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
|
|
{
|
|
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
|
|
{
|
|
$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);
|
|
|
|
$record = Requisition::query()->whereKey($result['id'])->firstOrFail();
|
|
if ($existing !== null) {
|
|
app(AutomaticPurchases::class)->requisitionChanged($user, $record);
|
|
}
|
|
|
|
return $record;
|
|
}
|
|
|
|
/**
|
|
* @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 = $automaticRule === null ? app(CurrentActor::class)->lock($user) : app(AutomaticPurchases::class)->lockExecutor($user);
|
|
$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.']);
|
|
}
|
|
$requisition = $existing ? Requisition::query()->lockForUpdate()->findOrFail($existing->id) : new Requisition;
|
|
if ($existing) {
|
|
abort_unless($requisition->requester_id === $user->id && $requisition->status === 'draft', 403);
|
|
app(ConflictGuard::class)->assertParticipation($user, $requisition);
|
|
}
|
|
$before = $existing ? $requisition->load('items')->toArray() : null;
|
|
$items = [];
|
|
$subtotal = 0;
|
|
$tax = 0;
|
|
/** @var list<array{description: string, category: string, purchase_type: string, quantity: string, unit: string, unit_price: string, tax_rate: string, equipment_id?: int, spare_part_id?: int, production_area?: string, downtime_started_at?: string|null}> $lines */
|
|
$lines = $data['items'];
|
|
$oldLines = $existing ? $requisition->items()->get()->keyBy('id')->all() : [];
|
|
foreach ($lines as $line) {
|
|
$line = app(RequisitionCatalogue::class)->line($user, $line, $oldLines);
|
|
app(EquipmentWorkflow::class)->line($line, (int) $user->department_id, $data['priority']);
|
|
$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']] : []),
|
|
'description' => $line['description'], 'category' => $line['category'],
|
|
'purchase_type' => $line['purchase_type'], 'quantity' => $line['quantity'], 'unit' => $line['unit'],
|
|
'equipment_id' => $line['equipment_id'] ?? null, 'spare_part_id' => $line['spare_part_id'] ?? null, 'production_area' => $line['production_area'] ?? null, 'downtime_started_at' => $line['downtime_started_at'] ?? null,
|
|
'unit_price_minor' => $price, 'tax_rate' => $line['tax_rate'],
|
|
'subtotal_minor' => $lineSubtotal, 'tax_minor' => $lineTax, 'total_minor' => $lineSubtotal + $lineTax,
|
|
];
|
|
$subtotal += $lineSubtotal;
|
|
$tax += $lineTax;
|
|
if ($subtotal + $tax > 100000000000000) {
|
|
throw ValidationException::withMessages(['items' => 'O total da requisição ultrapassa o limite suportado de 1.000.000.000.000 MZN.']);
|
|
}
|
|
}
|
|
$requisition->fill([
|
|
'tenant_id' => $user->tenant_id, 'requester_id' => $user->id, 'department_id' => $user->department_id,
|
|
'cost_center_id' => $data['cost_center_id'], 'priority' => $data['priority'],
|
|
'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)]);
|
|
}
|
|
$requisition->items()->delete();
|
|
$requisition->items()->createMany($items);
|
|
$requisition->load('items');
|
|
$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;
|
|
});
|
|
}
|
|
|
|
/** Only an independently activated rule may create a draft on behalf of its designated requester.
|
|
* @param array<string,mixed> $data
|
|
*/
|
|
public function automaticDraft(User $executor, SupplyRecord $rule, array $data): Requisition
|
|
{
|
|
$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()->whereKey($result['id'])->firstOrFail();
|
|
}
|
|
|
|
public function submit(User $user, Requisition $requisition, ?string $operationKey = null, ?int $version = null): void
|
|
{
|
|
$this->execute($user, 'submit', $requisition->id, $version, $operationKey);
|
|
app(AutomaticPurchases::class)->requisitionChanged($user, $requisition);
|
|
}
|
|
|
|
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]);
|
|
app(AutomaticPurchases::class)->requisitionChanged($user, $requisition);
|
|
}
|
|
|
|
/** @param array<string, mixed>|null $before
|
|
* @param array<string, mixed> $after
|
|
*/
|
|
public function audit(User $user, string $action, ?Requisition $requisition, ?array $before, array $after): void
|
|
{
|
|
AuditLog::query()->create([
|
|
'tenant_id' => $user->tenant_id, 'user_id' => $user->id,
|
|
'requisition_id' => $requisition?->id, 'action' => $action,
|
|
'ip_address' => app(ReplenishmentRun::class)->tenantId() === null ? request()->ip() : null, 'before' => $before, 'after' => $after + (app(ReplenishmentRun::class)->tenantId() === null ? [] : ['execution' => app(ReplenishmentRun::class)->evidence()]),
|
|
]);
|
|
}
|
|
}
|