111 lines
6.7 KiB
PHP
111 lines
6.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use stdClass;
|
|
|
|
class PartnershipPublication
|
|
{
|
|
private const ALLOWED = ['submit' => ['draft', 'returned', 'rejected'], 'publish' => ['submitted', 'pending'], 'return' => ['submitted', 'pending'], 'reject' => ['submitted', 'pending'], 'suspend' => ['published'], 'archive' => ['draft', 'published', 'suspended', 'rejected'], 'renew' => ['published', 'suspended', 'archived']];
|
|
|
|
private const STATES = ['submit' => 'submitted', 'publish' => 'published', 'return' => 'returned', 'reject' => 'rejected', 'suspend' => 'suspended', 'archive' => 'archived', 'renew' => 'draft'];
|
|
|
|
private const MODERATION = ['publish', 'return', 'reject', 'suspend'];
|
|
|
|
private function permitted(User $actor, stdClass $record, string $action): bool
|
|
{
|
|
$owns = app(PartnershipExchange::class)->owns($actor, $record);
|
|
|
|
return in_array($action, self::MODERATION, true) ? Gate::forUser($actor)->allows('manage-portal') && ! $owns : $owns;
|
|
}
|
|
|
|
/** @return list<string> */
|
|
public function actions(?User $actor, stdClass $record): array
|
|
{
|
|
$actions = [];
|
|
foreach (self::ALLOWED as $action => $states) {
|
|
if ($actor && in_array($record->status, $states, true) && $this->permitted($actor, $record, $action)) {
|
|
$actions[] = $action;
|
|
}
|
|
}
|
|
|
|
return $actions;
|
|
}
|
|
|
|
/** @return list<array{key:string,action:string,version:int}> */
|
|
public function commands(?User $actor, stdClass $record): array
|
|
{
|
|
if (! $actor) {
|
|
return [];
|
|
}
|
|
$commands = [];
|
|
$events = DB::table('partnership_events')->where('partnership_id', $record->id)->where('actor_id', $actor->id)->where('event_type', 'like', 'partnership_opportunity.%')->whereRaw("payload->'payload'->>'client_key' IS NOT NULL")->orderBy('aggregate_version')->get(['payload']);
|
|
foreach ($events as $event) {
|
|
$data = json_decode($event->payload, true, 512, JSON_THROW_ON_ERROR)['payload'];
|
|
if (is_string($data['client_key'] ?? null) && is_string($data['action'] ?? null) && isset(self::ALLOWED[$data['action']]) && is_int($data['result_version'] ?? null) && $this->permitted($actor, $record, $data['action'])) {
|
|
$commands[] = ['key' => $data['client_key'], 'action' => $data['action'], 'version' => $data['result_version']];
|
|
}
|
|
}
|
|
|
|
return $commands;
|
|
}
|
|
|
|
public function transition(User $actor, int $id, int $version, string $action, string $reason, ?string $clientKey = null): stdClass
|
|
{
|
|
$reason = trim($reason);
|
|
$clientKey = $clientKey === null ? null : strtolower($clientKey);
|
|
Validator::make(['version' => $version, 'reason' => $reason, 'client_key' => $clientKey], ['version' => 'required|integer|min:1', 'reason' => 'required|string|max:2000', 'client_key' => 'nullable|uuid'])->validate();
|
|
$hash = hash('sha256', json_encode(Arr::sortRecursive(['version' => $version, 'action' => $action, 'reason' => $reason]), JSON_THROW_ON_ERROR));
|
|
|
|
return DB::transaction(function () use ($actor, $id, $version, $action, $reason, $clientKey, $hash) {
|
|
$record = app(PartnershipExchange::class)->find($id, true);
|
|
abort_unless($this->permitted($actor, $record, $action), 403, 'A competência actual não permite esta acção sobre a proposta.');
|
|
if ($clientKey !== null) {
|
|
$event = DB::table('partnership_events')->where('partnership_id', $id)->where('actor_id', $actor->id)->where('event_type', 'like', 'partnership_opportunity.%')->whereRaw("payload->'payload'->>'client_key' = ?", [$clientKey])->first(['payload']);
|
|
if ($event) {
|
|
$data = json_decode($event->payload, true, 512, JSON_THROW_ON_ERROR)['payload'];
|
|
abort_unless(($data['command_hash'] ?? null) === $hash, 409, 'Chave já utilizada com outro conteúdo.');
|
|
$record->command_replayed = true;
|
|
|
|
return $record;
|
|
}
|
|
}
|
|
abort_unless((int) $record->version === $version, 409, 'Proposta alterada; compare antes de decidir.');
|
|
abort_unless(isset(self::ALLOWED[$action]) && in_array($record->status, self::ALLOWED[$action], true), 409, 'Transição inválida.');
|
|
if ($action === 'submit') {
|
|
$payload = json_decode($record->payload, true);
|
|
Validator::make(['title' => $record->title, 'type' => $record->partnership_type, 'offer' => $record->offer, 'seeking' => $record->seeking, 'consent' => $payload['consent'] ?? false], ['title' => 'required', 'type' => 'required', 'offer' => 'required_without:seeking', 'seeking' => 'required_without:offer', 'consent' => 'accepted'])->validate();
|
|
}
|
|
$before = $this->snapshot($record);
|
|
$status = self::STATES[$action];
|
|
$moderation = in_array($action, self::MODERATION, true);
|
|
DB::table('portal_partnerships')->where('id', $id)->update(['status' => $status, 'version' => $version + 1, 'reviewed_by' => $moderation ? $actor->id : $record->reviewed_by, 'published_at' => $action === 'publish' ? now() : $record->published_at, 'updated_at' => now()]);
|
|
$saved = app(PartnershipExchange::class)->find($id);
|
|
$metadata = ['from' => $record->status, 'to' => $status, 'reason' => $reason, 'action' => $action, 'result_version' => $version + 1, 'previous' => $before, 'applied' => $this->snapshot($saved)];
|
|
if ($clientKey !== null) {
|
|
$metadata += ['client_key' => $clientKey, 'command_hash' => $hash];
|
|
}
|
|
app(PartnershipExchange::class)->event($saved, $actor, 'partnership_opportunity.'.$status.'.v1', $metadata, 'Proposta: '.$status, [$record->user_id]);
|
|
$saved->command_replayed = false;
|
|
|
|
return $saved;
|
|
});
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
public function snapshot(stdClass $record): array
|
|
{
|
|
$payload = json_decode($record->payload, true, 512, JSON_THROW_ON_ERROR);
|
|
unset($payload['_creation_hash']);
|
|
|
|
return [...Arr::only((array) $record, ['id', 'user_id', 'organization_id', 'title', 'kind', 'partnership_type', 'sector_id', 'deadline', 'offer', 'seeking', 'version', 'status', 'published_at']),
|
|
'payload' => $payload, 'origins' => DB::table('partnership_origins')->where('partnership_id', $record->id)->get(['type', 'target_id'])->map(fn ($row) => (array) $row)->all(),
|
|
'locations' => DB::table('partnership_locations')->where('partnership_id', $record->id)->get(['geo_location_id', 'role', 'source_reference'])->map(fn ($row) => (array) $row)->all()];
|
|
}
|
|
}
|