196 lines
12 KiB
PHP
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];
|
|
}
|
|
}
|