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

196 lines
12 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use stdClass;
class PartnershipPublicationHistory
{
/** @var array<int,string|null> */
private array $people = [];
/** @var array<string,string|null> */
private array $organizations = [];
/** @return array<string,mixed> */
public function page(User $actor, int $id, ?string $before = null): array
{
['record' => $record, 'moderator' => $moderator] = $this->context($actor, $id);
$boundary = $before === null ? null : $this->decode($before, $actor, $id);
$events = $this->events((int) $record->id, $boundary, 11);
$page = $events->take(10);
$last = $page->last();
$base = '/parcerias/'.$id.'/historico';
$next = $events->count() > 10 && $last ? $base.'?before='.rawurlencode(Crypt::encryptString(json_encode(['purpose' => 'publication', 'actor' => $actor->id, 'partnership' => $id, 'before' => (int) $last->aggregate_version], JSON_THROW_ON_ERROR))) : null;
return ['partnership_id' => $id, 'entries' => $page->map(fn ($event) => $this->entry($event, $actor, $id, $moderator))->all(), 'first_url' => $base, 'next_url' => $next, 'older_page' => $before !== null, 'partnership_href' => '/parcerias/'.$id, 'dashboard_href' => '/parcerias/painel'];
}
/** @return array{limit:int,has_more:bool,href:string,entries:list<array<string,mixed>>} */
public function summary(User $actor, int $id): array
{
['record' => $record, 'moderator' => $moderator] = $this->context($actor, $id);
$limit = 5;
$events = $this->events((int) $record->id, null, $limit + 1);
$entries = array_values($events->take($limit)->map(function ($event) use ($actor, $id, $moderator) {
$entry = $this->entry($event, $actor, $id, $moderator);
return ['version' => $entry['version'], 'restricted' => $entry['restricted'], 'action' => $entry['action'], 'actor_name' => $entry['actor_name'], 'occurred_at' => $entry['occurred_at'], 'reason' => $entry['reason'],
'status_before' => ($entry['previous']['availability'] ?? null) === 'available' ? ($entry['previous']['status'] ?? null) : null,
'status_after' => ($entry['after']['availability'] ?? null) === 'available' ? ($entry['after']['status'] ?? null) : null];
})->all());
return ['limit' => $limit, 'has_more' => $events->count() > $limit, 'href' => '/parcerias/'.$id.'/historico', 'entries' => $entries];
}
/** @return array{record:stdClass,moderator:bool} */
private function context(User $actor, int $id): array
{
$record = app(PartnershipExchange::class)->find($id);
$moderator = Gate::forUser($actor)->allows('manage-portal');
abort_unless($moderator || app(PartnershipExchange::class)->owns($actor, $record, false), 403, 'Histórico reservado aos responsáveis autorizados pela proposta e à moderação.');
return ['record' => $record, 'moderator' => $moderator];
}
/** @return Collection<int,stdClass> */
private function events(int $id, ?int $before, int $limit): Collection
{
return DB::table('partnership_events')->where('partnership_id', $id)->where('event_type', 'like', 'partnership_opportunity.%')->when($before, fn ($q) => $q->where('aggregate_version', '<', $before))->orderByDesc('aggregate_version')->limit($limit)->get(['aggregate_version', 'event_type', 'actor_id', 'occurred_at', 'payload']);
}
private function decode(string $token, User $actor, int $id): 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) !== 'publication' || ($value['actor'] ?? null) !== $actor->id || ($value['partnership'] ?? null) !== $id || ! is_int($value['before'] ?? null) || $value['before'] < 1) {
throw ValidationException::withMessages(['before' => 'A referência da página não pertence a esta conta e publicação. Volte ao início do histórico.']);
}
return $value['before'];
}
/** @return array<string,mixed> */
private function entry(stdClass $event, User $actor, int $id, bool $moderator): array
{
$envelope = json_decode($event->payload, true, 512, JSON_THROW_ON_ERROR);
$data = is_array($envelope) && is_array($envelope['payload'] ?? null) ? $envelope['payload'] : [];
$previous = is_array($data['previous'] ?? null) ? $data['previous'] : null;
$applied = is_array($data['applied'] ?? null) ? $data['applied'] : null;
$version = is_int($data['result_version'] ?? null) ? $data['result_version'] : (is_array($envelope) && is_int($envelope['publication_version'] ?? null) ? $envelope['publication_version'] : null);
if ($applied === null) {
$scope = $data['_notification_context']['partnership'] ?? null;
$applied = is_array($scope) ? $scope : [];
$applied['version'] = $version;
if (is_string($data['to'] ?? null)) {
$applied['status'] = $data['to'];
}
if (is_array($data['origins'] ?? null)) {
$applied['origins'] = $data['origins'];
}if (is_array($data['locations'] ?? null)) {
$applied['locations'] = $data['locations'];
}
}
if ($previous === null && (is_string($data['from'] ?? null) || is_string($data['previous_status'] ?? null))) {
$previous = ['status' => $data['from'] ?? $data['previous_status']];
}
// A synthetic insertion placeholder is not a prior version of a newly created proposal.
if (($data['action'] ?? null) === 'create' && array_key_exists('previous', $data) && $data['previous'] === null) {
$previous = null;
}
$beforeView = $this->snapshot($previous, $actor, $id, $moderator);
$afterView = $this->snapshot($applied, $actor, $id, $moderator);
$visible = $moderator || $beforeView['availability'] === 'available' || $afterView['availability'] === 'available';
$action = is_string($data['action'] ?? null) && in_array($data['action'], ['create', 'edit', 'submit', 'publish', 'return', 'reject', 'suspend', 'archive', 'renew'], true) ? $data['action'] : null;
return ['version' => $version, 'restricted' => ! $visible, 'action' => $visible ? $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, 'previous' => $beforeView, 'after' => $afterView];
}
/** @param array<string,mixed>|null $snapshot
* @return array<string,mixed>
*/
private function snapshot(?array $snapshot, User $actor, int $id, bool $moderator): array
{
if ($snapshot === null) {
return ['availability' => 'not_recorded'];
}
if (array_key_exists('id', $snapshot) && (! is_int($snapshot['id']) || $snapshot['id'] !== $id)) {
return ['availability' => 'scope_unknown'];
}
$known = is_int($snapshot['id'] ?? null) && $snapshot['id'] === $id && is_int($snapshot['user_id'] ?? null) && $snapshot['user_id'] > 0 && array_key_exists('organization_id', $snapshot) && ($snapshot['organization_id'] === null || is_string($snapshot['organization_id']) && Str::isUuid($snapshot['organization_id']));
if (! $moderator && ! $known) {
return ['availability' => 'scope_unknown'];
}
if (! $moderator && ! app(PartnershipExchange::class)->owns($actor, (object) $snapshot, false)) {
return ['availability' => 'restricted'];
}
$fields = [];
foreach (['title', 'offer', 'seeking', 'kind', 'partnership_type', 'deadline', 'published_at'] as $key) {
if (array_key_exists($key, $snapshot) && ($snapshot[$key] === null || is_string($snapshot[$key]))) {
$fields[$key] = $snapshot[$key];
}
}
$payload = is_array($snapshot['payload'] ?? null) ? $snapshot['payload'] : null;
foreach (['summary', 'requirements', 'confidentiality', 'availability'] as $key) {
if ($payload !== null && array_key_exists($key, $payload) && ($payload[$key] === null || is_string($payload[$key]))) {
$fields[$key] = $payload[$key];
}
}
$consent = $payload['consent'] ?? null;
$consentKnown = in_array($consent, [true, false, 1, 0, '1', '0', 'yes', 'on'], true);
$mandatory = $payload['mandatory'] ?? null;
$mandatoryKnown = is_array($mandatory) && array_is_list($mandatory) && count(array_filter($mandatory, 'is_string')) === count($mandatory);
$origins = [];
foreach (is_array($snapshot['origins'] ?? null) ? $snapshot['origins'] : [] as $origin) {
if (is_array($origin) && is_string($origin['type'] ?? null) && $origin['type'] !== '' && is_string($origin['target_id'] ?? null) && $origin['target_id'] !== '') {
$origins[] = ['type' => $origin['type'], 'target_id' => $origin['target_id']];
}
}
$locations = [];
foreach (is_array($snapshot['locations'] ?? null) ? $snapshot['locations'] : [] as $location) {
if (is_array($location) && is_string($location['geo_location_id'] ?? null) && Str::isUuid($location['geo_location_id'])) {
$locations[] = ['geo_location_id' => $location['geo_location_id'], 'role' => is_string($location['role'] ?? null) ? $location['role'] : null, 'source_reference' => is_string($location['source_reference'] ?? null) ? $location['source_reference'] : null];
}
}
$organization = $known && is_string($snapshot['organization_id']) ? $snapshot['organization_id'] : null;
$originsIncomplete = is_array($snapshot['origins'] ?? null) && count($origins) !== count($snapshot['origins']);
$locationsIncomplete = is_array($snapshot['locations'] ?? null) && (count($locations) !== count($snapshot['locations']) || count(array_filter($locations, fn ($location) => $location['role'] === null || $location['source_reference'] === null)) > 0);
return ['availability' => 'available', 'version' => is_int($snapshot['version'] ?? null) ? $snapshot['version'] : null, 'status' => is_string($snapshot['status'] ?? null) ? $snapshot['status'] : null, 'scope_known' => $known, 'organization_id' => $organization, 'organization_name' => $organization ? $this->organization($organization) : null,
'fields' => $fields, 'sector_recorded' => array_key_exists('sector_id', $snapshot) && ($snapshot['sector_id'] === null || is_int($snapshot['sector_id']) || is_string($snapshot['sector_id'])), 'sector_id' => is_int($snapshot['sector_id'] ?? null) || is_string($snapshot['sector_id'] ?? null) ? (string) $snapshot['sector_id'] : null,
'consent_known' => $consentKnown, 'consent' => $consentKnown ? in_array($consent, [true, 1, '1', 'yes', 'on'], true) : null, 'mandatory_known' => $mandatoryKnown, 'mandatory' => $mandatoryKnown ? $mandatory : [],
'origins_recorded' => is_array($snapshot['origins'] ?? null), 'origins' => $origins, 'origins_incomplete' => $originsIncomplete,
'locations_recorded' => is_array($snapshot['locations'] ?? null), 'locations' => $locations, 'locations_incomplete' => $locationsIncomplete,
'complete' => $known && is_int($snapshot['version'] ?? null) && is_string($snapshot['status'] ?? null) && array_key_exists('title', $fields) && $payload !== null && is_array($snapshot['origins'] ?? null) && ! $originsIncomplete && is_array($snapshot['locations'] ?? null) && ! $locationsIncomplete];
}
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];
}
private function organization(string $id): ?string
{
if (! array_key_exists($id, $this->organizations)) {
$this->organizations[$id] = DB::table('organizations')->where('id', $id)->value('name');
}
return $this->organizations[$id];
}
}