126 lines
7.0 KiB
PHP
126 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\RequisitionItem;
|
|
use App\Models\StockItem;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\ValidationException;
|
|
use stdClass;
|
|
|
|
class RequisitionCatalogue
|
|
{
|
|
public function canManage(User $actor): bool
|
|
{
|
|
return $actor->hasFunctions(['admin', 'warehouse']) && ! app(OrganizationAccess::class)->maintenanceOnly($actor);
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function source(StockItem $stock): array
|
|
{
|
|
return $stock->only(['id', 'code', 'name', 'unit']);
|
|
}
|
|
|
|
public function basis(StockItem $stock): string
|
|
{
|
|
return hash('sha256', json_encode($this->source($stock), JSON_THROW_ON_ERROR));
|
|
}
|
|
|
|
/** @param array<string,mixed> $input */
|
|
public function save(User $actor, int $stockId, array $input): void
|
|
{
|
|
DB::transaction(function () use ($actor, $stockId, $input) {
|
|
app(ConflictGuard::class)->lockCompany($actor);
|
|
$actor = app(CurrentActor::class)->lock($actor);
|
|
abort_unless($this->canManage($actor), 403);
|
|
$data = Validator::make($input, ['version' => 'required|integer|min:0', 'stock_basis' => 'required|string|size:64', 'category' => 'required|string|max:100',
|
|
'purchase_type' => 'required|in:consumable,other', 'published' => 'required|boolean', 'reason' => 'required|string|min:10|max:4000'])->validate();
|
|
$stock = StockItem::query()->where('tenant_id', $actor->tenant_id)->lockForUpdate()->findOrFail($stockId);
|
|
$entry = DB::table('requisition_catalogue')->where('tenant_id', $actor->tenant_id)->where('stock_item_id', $stock->id)->lockForUpdate()->first();
|
|
if ((int) $data['version'] !== (int) ($entry->version ?? 0) || $data['stock_basis'] !== $this->basis($stock)) {
|
|
throw ValidationException::withMessages(['version' => 'O artigo ou a publicação mudou. Actualize e confira antes de guardar.']);
|
|
}
|
|
$values = ['category' => $data['category'], 'purchase_type' => $data['purchase_type'], 'published' => $data['published'],
|
|
'source_snapshot' => json_encode($this->source($stock), JSON_THROW_ON_ERROR), 'version' => ($entry->version ?? 0) + 1, 'updated_at' => now()];
|
|
if ($entry === null) {
|
|
$id = DB::table('requisition_catalogue')->insertGetId([...$values, 'tenant_id' => $actor->tenant_id, 'stock_item_id' => $stock->id, 'created_at' => now()]);
|
|
} else {
|
|
$id = $entry->id;
|
|
DB::table('requisition_catalogue')->where('id', $id)->update($values);
|
|
}
|
|
app(RequisitionWorkflow::class)->audit($actor, 'stock.catalogue_configured', null, $entry === null ? null : (array) $entry,
|
|
['id' => $id, 'stock_item_id' => $stock->id, ...$values, 'reason' => $data['reason']]);
|
|
});
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function options(User $actor, string $search): array
|
|
{
|
|
$ready = Schema::hasTable('requisition_catalogue');
|
|
$items = [];
|
|
if ($ready && app(OrganizationAccess::class)->canRequest($actor)) {
|
|
$query = DB::table('requisition_catalogue')->where('tenant_id', $actor->tenant_id)->where('published', true);
|
|
$query->orderBy('id')->chunkById(100, function ($entries) use ($actor, $search, &$items) {
|
|
$stocks = StockItem::query()->where('tenant_id', $actor->tenant_id)->whereIn('id', $entries->pluck('stock_item_id'))->get()->keyBy('id');
|
|
foreach ($entries as $entry) {
|
|
$stock = $stocks->get($entry->stock_item_id);
|
|
if ($stock === null || json_decode($entry->source_snapshot, true, 16, JSON_THROW_ON_ERROR) !== $this->source($stock)) {
|
|
continue;
|
|
}
|
|
if ($search !== '' && ! str_contains(mb_strtolower($stock->code.' '.$stock->name), mb_strtolower($search))) {
|
|
continue;
|
|
}
|
|
$items[] = ['id' => $entry->id, 'version' => $entry->version, ...$this->sourceDescriptor($entry)];
|
|
if (count($items) === 21) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return ['ready' => $ready, 'search' => $search, 'items' => array_slice($items, 0, 20), 'more' => count($items) > 20];
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function sourceDescriptor(stdClass $entry): array
|
|
{
|
|
$source = json_decode($entry->source_snapshot, true, 16, JSON_THROW_ON_ERROR);
|
|
|
|
return ['stock_item_id' => $source['id'], 'code' => $source['code'], 'description' => $source['name'], 'unit' => $source['unit'],
|
|
'category' => $entry->category, 'purchase_type' => $entry->purchase_type];
|
|
}
|
|
|
|
/** @param array<string,mixed> $line
|
|
* @param array<int,RequisitionItem> $existing
|
|
* @return array<string,mixed>
|
|
*/
|
|
public function line(User $actor, array $line, array $existing): array
|
|
{
|
|
if (empty($line['catalogue_entry_id'])) {
|
|
return $line;
|
|
}
|
|
abort_unless(Schema::hasTable('requisition_catalogue'), 503, 'Instalação do catálogo pendente.');
|
|
$input = Validator::make($line, ['catalogue_entry_id' => 'required|integer|min:1', 'catalogue_version' => 'required|integer|min:1', 'source_line_id' => 'nullable|integer|min:1'])->validate();
|
|
$old = isset($input['source_line_id']) ? ($existing[$input['source_line_id']] ?? null) : null;
|
|
if ($old !== null && $old->catalogue_entry_id === (int) $input['catalogue_entry_id'] && ($old->catalogue_snapshot['version'] ?? null) === (int) $input['catalogue_version']) {
|
|
$snapshot = $old->catalogue_snapshot;
|
|
} else {
|
|
$entry = DB::table('requisition_catalogue')->where('tenant_id', $actor->tenant_id)->where('id', $input['catalogue_entry_id'])->lockForUpdate()->first();
|
|
abort_unless($entry !== null, 404);
|
|
$stock = StockItem::query()->where('tenant_id', $actor->tenant_id)->where('id', $entry->stock_item_id)->lockForUpdate()->firstOrFail();
|
|
if (! $entry->published || $entry->version !== (int) $input['catalogue_version'] || json_decode($entry->source_snapshot, true, 16, JSON_THROW_ON_ERROR) !== $this->source($stock)) {
|
|
throw ValidationException::withMessages(['items' => 'O artigo do catálogo mudou ou deixou de estar publicado. Pesquise e seleccione a versão actual.']);
|
|
}
|
|
$snapshot = ['entry_id' => $entry->id, 'version' => $entry->version, ...$this->sourceDescriptor($entry)];
|
|
}
|
|
|
|
return [...$line, 'catalogue_entry_id' => $snapshot['entry_id'], 'catalogue_snapshot' => $snapshot,
|
|
'description' => $snapshot['description'], 'unit' => $snapshot['unit'], 'category' => $snapshot['category'], 'purchase_type' => $snapshot['purchase_type']];
|
|
}
|
|
}
|