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

142 lines
8.4 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use stdClass;
class PartnershipAgreementHistory
{
/** @var array<int,string|null> */
private array $people = [];
public function __construct(private PartnershipAgreementProjection $projection) {}
/** @return array<string,mixed> */
public function page(User $actor, int $id, ?string $before = null): array
{
$context = app(PartnershipExchange::class)->room($actor, $id);
$boundary = $before === null ? null : $this->decode($before, $actor, $context);
$events = DB::table('partnership_events')->where('partnership_id', $context['partnership']->id)->where('event_type', 'partnership_agreement.changed.v1')
->whereRaw("payload->'payload'->>'room_id' = ?", [(string) $id])
->when($boundary, fn ($q) => $q->where('aggregate_version', '<', $boundary))->orderByDesc('aggregate_version')->limit(11)->get(['aggregate_version', 'actor_id', 'occurred_at', 'payload']);
$page = $events->take(10);
$base = '/parcerias/salas/'.$id.'/historico';
$last = $page->last();
$next = $events->count() > 10 && $last ? $base.'?before='.rawurlencode(Crypt::encryptString(json_encode(['purpose' => 'agreement', 'actor' => $actor->id, 'room' => $id, 'interest' => (int) $context['interest']->id, 'partnership' => (int) $context['partnership']->id, 'before' => (int) $last->aggregate_version], JSON_THROW_ON_ERROR))) : null;
return ['room_id' => $id, 'entries' => $page->map(fn ($event) => $this->entry($event, $actor, $context))->all(), 'first_url' => $base, 'next_url' => $next, 'older_page' => $before !== null, 'room_href' => '/parcerias/salas/'.$id, 'dashboard_href' => '/parcerias/painel'];
}
/** @param array{partnership:stdClass,interest:stdClass,room:stdClass} $context */
private function decode(string $token, User $actor, array $context): int
{
try {
$value = json_decode(Crypt::decryptString($token), true, 512, JSON_THROW_ON_ERROR);
} catch (DecryptException|\JsonException) {
throw ValidationException::withMessages(['before' => 'A referência da página não é válida. Volte ao início do histórico.']);
}
if (! is_array($value) || ($value['purpose'] ?? null) !== 'agreement' || ($value['actor'] ?? null) !== $actor->id || ($value['room'] ?? null) !== (int) $context['room']->id || ($value['interest'] ?? null) !== (int) $context['interest']->id || ($value['partnership'] ?? null) !== (int) $context['partnership']->id || ! $this->positive($value['before'] ?? null)) {
throw ValidationException::withMessages(['before' => 'A referência da página não pertence a esta sala e conta. Volte ao início do histórico.']);
}
return $value['before'];
}
/** @param array{partnership:stdClass,interest:stdClass,room:stdClass} $context
* @return array<string,mixed>
*/
private function entry(stdClass $event, User $actor, array $context): array
{
$envelope = json_decode($event->payload, true, 512, JSON_THROW_ON_ERROR);
$data = is_array($envelope) && is_array($envelope['payload'] ?? null) ? $envelope['payload'] : [];
$scope = $data['_notification_context'] ?? null;
$proponent = is_array($scope) ? $this->scope($scope['partnership'] ?? null) : null;
$candidate = is_array($scope) ? $this->scope($scope['interest'] ?? null) : null;
$known = is_array($scope) && ($scope['version'] ?? null) === 1 && ($scope['room_id'] ?? null) === (int) $context['room']->id && $proponent !== null && $candidate !== null && ($proponent['id'] ?? null) === (int) $context['partnership']->id && ($candidate['id'] ?? null) === (int) $context['interest']->id;
$exchange = app(PartnershipExchange::class);
$visible = $known && ($exchange->owns($actor, (object) $proponent, false) || $exchange->owns($actor, (object) $candidate, false));
$version = $this->positive($data['version'] ?? null) ? $data['version'] : null;
$unavailable = ['availability' => $known ? 'restricted' : 'scope_unknown'];
return ['version' => $version, 'restricted' => ! $visible,
'action' => $visible && in_array($data['action'] ?? null, ['record', 'confirm', 'start', 'complete', 'cancel', 'renew'], true) ? $data['action'] : null,
'actor_name' => $visible ? $this->person((int) $event->actor_id) : null, 'occurred_at' => $visible ? $event->occurred_at : null,
'reason' => $visible && is_string($data['reason'] ?? null) ? $data['reason'] : null,
'confirmation_party' => $visible && in_array($data['confirmation_party'] ?? null, ['proponent', 'candidate'], true) ? $data['confirmation_party'] : null,
'partnership_version' => $visible && $this->positive($data['partnership_version'] ?? null) ? $data['partnership_version'] : null,
'interest_version' => $visible && $this->positive($data['interest_version'] ?? null) ? $data['interest_version'] : null,
'previous' => $visible ? $this->snapshot($data['previous'] ?? null, $actor, (int) $context['room']->id) : $unavailable,
'after' => $visible ? $this->snapshot($data['applied'] ?? null, $actor, (int) $context['room']->id) : $unavailable];
}
/** @return array<string,mixed> */
private function snapshot(mixed $value, User $actor, int $id): array
{
if ($value === null) {
return ['availability' => 'not_recorded'];
}
if (! is_array($value) || ($value['id'] ?? null) !== $id || ! $this->positive($value['version'] ?? null) || ! is_string($value['status'] ?? null)) {
return ['availability' => 'scope_unknown'];
}
$view = ['availability' => 'available', 'version' => $value['version'], 'status' => $value['status'], 'instrument_availability' => 'not_recorded'];
if (! array_key_exists('agreement', $value)) {
return $view;
}
$instrument = $value['agreement'];
if ($instrument === null) {
return $view;
}
if (! is_array($instrument)) {
return [...$view, 'instrument_availability' => 'malformed'];
}
$scope = $instrument['recorded_context'] ?? null;
$proponent = is_array($scope) ? $this->scope($scope['proponent'] ?? null) : null;
$candidate = is_array($scope) ? $this->scope($scope['candidate'] ?? null) : null;
if ($proponent === null || $candidate === null || ! $this->positive($scope['partnership_version'] ?? null) || ! $this->positive($scope['interest_version'] ?? null)) {
return [...$view, 'instrument_availability' => 'scope_unknown'];
}
$exchange = app(PartnershipExchange::class);
if (! $exchange->owns($actor, (object) $proponent, false) && ! $exchange->owns($actor, (object) $candidate, false)) {
return [...$view, 'instrument_availability' => 'restricted'];
}
$fields = $this->projection->fields($instrument, true);
return [...$view, 'instrument_availability' => 'available', 'instrument' => $fields, 'instrument_partnership_version' => $scope['partnership_version'], 'instrument_interest_version' => $scope['interest_version']];
}
/** @return array{id?:int,user_id:int,organization_id:string|null}|null */
private function scope(mixed $value): ?array
{
if (! is_array($value) || ! $this->positive($value['user_id'] ?? null) || ! array_key_exists('organization_id', $value) || ! ($value['organization_id'] === null || is_string($value['organization_id']) && Str::isUuid($value['organization_id']))) {
return null;
}
$scope = ['user_id' => $value['user_id'], 'organization_id' => $value['organization_id']];
if ($this->positive($value['id'] ?? null)) {
$scope['id'] = $value['id'];
}
return $scope;
}
/** @phpstan-assert-if-true positive-int $value */
private function positive(mixed $value): bool
{
return is_int($value) && $value > 0;
}
private function person(int $id): ?string
{
if (! array_key_exists($id, $this->people)) {
$this->people[$id] = DB::table('users')->where('id', $id)->value('name');
}
return $this->people[$id];
}
}