Files
Investidor-V2/app/Services/PartnershipPublication.php
T

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()];
}
}