143 lines
8.5 KiB
PHP
143 lines
8.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\LabourDemand;
|
|
use App\Models\TrainingCapacity;
|
|
use App\Models\User;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Support\Str;
|
|
|
|
final class EducationDeclarationDrafts
|
|
{
|
|
public function installed(): bool
|
|
{
|
|
return Schema::hasTable('education_declaration_drafts');
|
|
}
|
|
|
|
public function guard(User $actor, string $kind): void
|
|
{
|
|
abort_unless(config('education.enabled') && $this->installed() && in_array($kind, ['demands', 'capacities'], true), 404);
|
|
app(EducationAccess::class)->authorize($actor);
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function show(User $actor, string $kind, string $id): array
|
|
{
|
|
$this->guard($actor, $kind);
|
|
$row = DB::table('education_declaration_drafts')->where('id', $id)->where('owner_user_id', $actor->id)->where('kind', $kind)->firstOrFail();
|
|
|
|
return ['id' => $id, 'kind' => $kind, 'version_no' => (int) $row->version_no, 'state' => $row->state,
|
|
'payload' => $this->decode($row->payload),
|
|
'references' => $row->reference_snapshot ? json_decode(Crypt::decryptString($row->reference_snapshot), true, 512, JSON_THROW_ON_ERROR) : null,
|
|
'record_id' => $row->demand_id ?? $row->capacity_id, 'updated_at' => CarbonImmutable::parse($row->updated_at)->toIso8601String()];
|
|
}
|
|
|
|
/** @param array<string,mixed> $input
|
|
* @return array<string,mixed> */
|
|
public function save(User $actor, string $kind, ?string $id, array $input): array
|
|
{
|
|
$this->guard($actor, $kind);
|
|
$data = Validator::make($input, ['version_no' => 'required|integer|min:0', 'client_key' => $id ? 'sometimes|uuid' : 'required|uuid', 'payload' => 'present|array'])->validate();
|
|
$payload = $this->partial($kind, $data['payload']);
|
|
|
|
return DB::transaction(function () use ($actor, $kind, $id, $data, $payload): array {
|
|
DB::table('users')->where('id', $actor->id)->lock('FOR NO KEY UPDATE')->firstOrFail();
|
|
$query = DB::table('education_declaration_drafts')->where('owner_user_id', $actor->id);
|
|
$row = $id ? $query->where('id', $id)->where('kind', $kind)->lockForUpdate()->firstOrFail()
|
|
: $query->where('client_key', $data['client_key'])->lockForUpdate()->first();
|
|
abort_if($row && ($row->kind !== $kind || $row->state !== 'draft'), 409, 'Este rascunho já foi submetido ou pertence a outro tipo de declaração.');
|
|
if ($row && Arr::sortRecursive($this->decode($row->payload)) === Arr::sortRecursive($payload)
|
|
&& in_array((int) $data['version_no'], [(int) $row->version_no, (int) $row->version_no - 1], true)) {
|
|
return $this->show($actor, $kind, $row->id);
|
|
}
|
|
abort_unless((int) ($row->version_no ?? 0) === (int) $data['version_no'], 409, 'O rascunho mudou. Recupere a versão actual antes de guardar.');
|
|
$id = $row->id ?? (string) Str::uuid();
|
|
DB::table('education_declaration_drafts')->updateOrInsert(['id' => $id], ['owner_user_id' => $actor->id, 'client_key' => $row->client_key ?? $data['client_key'],
|
|
'kind' => $kind, 'version_no' => (int) ($row->version_no ?? 0) + 1, 'payload' => Crypt::encryptString(json_encode($payload, JSON_THROW_ON_ERROR)),
|
|
'state' => 'draft', 'created_at' => $row->created_at ?? now(), 'updated_at' => now()]);
|
|
$this->audit($actor, $id, 'draft_saved', (int) ($row->version_no ?? 0) + 1, $kind);
|
|
|
|
return $this->show($actor, $kind, $id);
|
|
});
|
|
}
|
|
|
|
/** @param array<string,mixed> $input
|
|
* @return array<string,mixed> */
|
|
public function submit(User $actor, string $kind, string $id, array $input): array
|
|
{
|
|
$this->guard($actor, $kind);
|
|
$data = Validator::make($input, ['version_no' => 'required|integer|min:1', 'payload' => 'present|array'])->validate();
|
|
$payload = $this->partial($kind, $data['payload']);
|
|
|
|
return DB::transaction(function () use ($actor, $kind, $id, $data, $payload): array {
|
|
DB::table('users')->where('id', $actor->id)->lock('FOR NO KEY UPDATE')->firstOrFail();
|
|
$row = DB::table('education_declaration_drafts')->where('id', $id)->where('owner_user_id', $actor->id)->where('kind', $kind)->lockForUpdate()->firstOrFail();
|
|
if ($row->state === 'submitted') {
|
|
abort_unless(in_array((int) $data['version_no'], [(int) $row->version_no, (int) $row->version_no - 1], true)
|
|
&& Arr::sortRecursive($this->decode($row->payload)) === Arr::sortRecursive($payload), 409);
|
|
|
|
return $this->show($actor, $kind, $id);
|
|
}
|
|
abort_unless((int) $row->version_no === (int) $data['version_no'], 409, 'O rascunho mudou. Recupere a versão actual antes de submeter.');
|
|
$validated = Validator::make($payload, EducationDeclarationRules::forKind($kind))->validate();
|
|
$references = [];
|
|
foreach (['occupation_id' => 'occupations', 'skill_id' => 'skills', $kind === 'demands' ? 'project_id' : 'course_id' => $kind === 'demands' ? 'projects' : 'courses'] as $field => $table) {
|
|
$source = DB::table($table)->where('id', $validated[$field])->firstOrFail();
|
|
$references[$field] = array_intersect_key((array) $source, array_flip(['id', 'name', 'title', 'code', 'provider_id', 'updated_at']));
|
|
}
|
|
$references['territory_path'] = array_map(fn ($node) => ['id' => $node->id, 'name' => $node->name, 'type' => $node->location_type], app(InvestmentGeography::class)->path($validated['geo_location_id'], true));
|
|
$record = $kind === 'demands' ? new LabourDemand : new TrainingCapacity;
|
|
PortalAudit::save($record, [...$validated, 'submitted_by' => $actor->id, 'status' => 'submitted'], $actor->id);
|
|
DB::table('education_declaration_drafts')->where('id', $id)->update(['payload' => Crypt::encryptString(json_encode($payload, JSON_THROW_ON_ERROR)),
|
|
'reference_snapshot' => Crypt::encryptString(json_encode($references, JSON_THROW_ON_ERROR)),
|
|
'state' => 'submitted', 'version_no' => (int) $row->version_no + 1, $kind === 'demands' ? 'demand_id' : 'capacity_id' => $record->id, 'updated_at' => now()]);
|
|
$this->audit($actor, $id, 'draft_submitted', (int) $row->version_no + 1, $kind);
|
|
|
|
return $this->show($actor, $kind, $id);
|
|
});
|
|
}
|
|
|
|
/** @param array<string,mixed> $payload
|
|
* @return array<string,mixed> */
|
|
private function partial(string $kind, array $payload): array
|
|
{
|
|
unset($payload[$kind === 'demands' ? 'course_id' : 'project_id']);
|
|
if ($kind === 'capacities') {
|
|
unset($payload['employer']);
|
|
}
|
|
$rules = ['occupation_id' => 'nullable|uuid', 'skill_id' => 'nullable|uuid', 'geo_location_id' => 'nullable|uuid',
|
|
$kind === 'demands' ? 'project_id' : 'course_id' => 'nullable|uuid', 'qualification' => 'nullable|string|max:150',
|
|
'quantity' => 'nullable|integer|min:0|max:10000000', 'period_start' => 'nullable|date_format:Y-m-d', 'period_end' => 'nullable|date_format:Y-m-d',
|
|
'source' => 'nullable|string|max:5000', 'reference_date' => 'nullable|date_format:Y-m-d', 'valid_until' => 'nullable|date_format:Y-m-d'];
|
|
if ($kind === 'demands') {
|
|
$rules['employer'] = 'nullable|string|max:200';
|
|
}
|
|
Validator::make(['payload' => $payload], ['payload' => 'array:'.implode(',', array_keys($rules))])->validate();
|
|
|
|
return Validator::make($payload, $rules)->validate();
|
|
}
|
|
|
|
private function audit(User $actor, string $id, string $action, int $version, string $kind): void
|
|
{
|
|
DB::table('audit_events')->insert(['source_system' => 'education-drafts', 'entity_type' => 'education_declaration_drafts', 'entity_id' => $id,
|
|
'actor_user_id' => $actor->id, 'action' => $action, 'data_classification' => 'restricted', 'after_data' => json_encode(['version_no' => $version, 'kind' => $kind], JSON_THROW_ON_ERROR)]);
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function decode(string $encrypted): array
|
|
{
|
|
$payload = json_decode(Crypt::decryptString($encrypted), true, 512, JSON_THROW_ON_ERROR);
|
|
if (! is_array($payload)) {
|
|
throw new \UnexpectedValueException('Invalid education draft payload.');
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
}
|