79 lines
5.3 KiB
PHP
79 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class PartnershipMilestones
|
|
{
|
|
/** @return array<int,string> Creation keys are private to the actor who submitted them. */
|
|
public function creationKeys(User $actor, int $partnership, int $room): array
|
|
{
|
|
$keys = [];
|
|
$events = DB::table('partnership_events')->where('partnership_id', $partnership)->where('actor_id', $actor->id)
|
|
->where('event_type', 'partnership_milestone.changed.v1')->whereRaw("payload->'payload'->>'room_id' = ?", [(string) $room])
|
|
->whereRaw("payload->'payload'->>'client_key' IS NOT NULL")->get(['payload']);
|
|
foreach ($events as $event) {
|
|
$payload = json_decode($event->payload, true, 512, JSON_THROW_ON_ERROR)['payload'];
|
|
$keys[(int) $payload['milestone_id']] = $payload['client_key'];
|
|
}
|
|
|
|
return $keys;
|
|
}
|
|
|
|
/** @param array<string,mixed> $data
|
|
* @return array{saved:bool,milestone_id:int,version:int,replayed:bool}
|
|
*/
|
|
public function save(User $actor, int $roomId, array $data, ?int $milestone = null): array
|
|
{
|
|
$command = ['title' => $data['title'], 'responsible_id' => (int) $data['responsible_id'], 'due_date' => $data['due_date'] ?? null,
|
|
'status' => $data['status'], 'evidence' => $data['evidence'] ?? null];
|
|
$creationHash = hash('sha256', json_encode($command, JSON_THROW_ON_ERROR));
|
|
|
|
return DB::transaction(function () use ($actor, $roomId, $data, $milestone, $command, $creationHash) {
|
|
$exchange = app(PartnershipExchange::class);
|
|
$context = $exchange->room($actor, $roomId);
|
|
abort_unless($exchange->owns($actor, $context['partnership']) || $exchange->owns($actor, $context['interest']), 403);
|
|
$room = DB::table('partnership_rooms')->where('id', $roomId)->lockForUpdate()->first();
|
|
abort_unless($room !== null, 404);
|
|
if ($milestone === null && ($data['client_key'] ?? null)) {
|
|
// The room lock serializes this actor/room key check and its event, including legacy clients.
|
|
$previous = DB::table('partnership_events')->where('partnership_id', $context['partnership']->id)->where('actor_id', $actor->id)
|
|
->where('event_type', 'partnership_milestone.changed.v1')->whereRaw("payload->'payload'->>'room_id' = ?", [(string) $roomId])
|
|
->whereRaw("payload->'payload'->>'client_key' = ?", [$data['client_key']])->first(['payload']);
|
|
if ($previous) {
|
|
$payload = json_decode($previous->payload, true, 512, JSON_THROW_ON_ERROR)['payload'];
|
|
abort_unless($payload['creation_hash'] === $creationHash, 409, 'Chave de criação de marco já utilizada com outro conteúdo.');
|
|
$saved = DB::table('partnership_milestones')->where('room_id', $roomId)->where('id', $payload['milestone_id'])->first();
|
|
abort_unless($saved !== null, 409, 'O marco original não está disponível para reconciliar a criação.');
|
|
|
|
return ['saved' => true, 'milestone_id' => (int) $saved->id, 'version' => (int) $saved->version, 'replayed' => true];
|
|
}
|
|
}
|
|
abort_unless(in_array($room->status, ['agreement_recorded', 'agreed', 'execution'], true), 409, 'O estado da sala não permite alterar marcos.');
|
|
$responsible = User::query()->whereKey($command['responsible_id'])->first();
|
|
abort_unless($responsible !== null && ($exchange->owns($responsible, $context['partnership']) || $exchange->owns($responsible, $context['interest'])), 422, 'Responsável deve ser participante identificado.');
|
|
$previousResponsible = null;
|
|
$version = 1;
|
|
if ($milestone !== null) {
|
|
$existing = DB::table('partnership_milestones')->where('room_id', $roomId)->where('id', $milestone)->lockForUpdate()->first();
|
|
abort_unless($existing !== null, 404);
|
|
abort_unless((int) $existing->version === (int) $data['version'], 409, 'O marco foi alterado. Compare com os dados actuais.');
|
|
$previousResponsible = (int) $existing->responsible_id;
|
|
$version = (int) $existing->version + 1;
|
|
DB::table('partnership_milestones')->where('id', $milestone)->update([...$command, 'version' => $version, 'updated_at' => now()]);
|
|
} else {
|
|
$milestone = (int) DB::table('partnership_milestones')->insertGetId([...$command, 'room_id' => $roomId, 'created_at' => now(), 'updated_at' => now()]);
|
|
}
|
|
$metadata = ['room_id' => $roomId, 'milestone_id' => $milestone, 'status' => $command['status'], 'previous_responsible_id' => $previousResponsible, 'responsible_id' => $command['responsible_id']];
|
|
if ($version === 1 && ($data['client_key'] ?? null)) {
|
|
$metadata += ['client_key' => $data['client_key'], 'creation_hash' => $creationHash];
|
|
}
|
|
$exchange->event($context['partnership'], $actor, 'partnership_milestone.changed.v1', $metadata, 'Marco atualizado', [$context['partnership']->user_id, $context['interest']->user_id]);
|
|
|
|
return ['saved' => true, 'milestone_id' => $milestone, 'version' => $version, 'replayed' => false];
|
|
});
|
|
}
|
|
}
|