*/ private array $people = []; /** @var array */ private array $organizations = []; /** @return array */ 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>} */ 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 */ 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 */ 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|null $snapshot * @return array */ 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]; } }