146 lines
6.9 KiB
PHP
146 lines
6.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Database\Query\Builder;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use LogicException;
|
|
use stdClass;
|
|
|
|
class PartnershipNotifications
|
|
{
|
|
/** @param array<string,mixed> $payload
|
|
* @return array<string,mixed>
|
|
*/
|
|
public function context(stdClass $record, array $payload): array
|
|
{
|
|
$interest = null;
|
|
$room = null;
|
|
if (isset($payload['room_id'])) {
|
|
$room = DB::table('partnership_rooms')->where('id', $payload['room_id'])->first();
|
|
if (! $room) {
|
|
throw new LogicException('Notification room does not exist.');
|
|
}
|
|
$interest = DB::table('partnership_interests')->where('id', $room->interest_id)->first();
|
|
}
|
|
if (isset($payload['interest_id'])) {
|
|
$selected = DB::table('partnership_interests')->where('id', $payload['interest_id'])->first();
|
|
if (! $selected || $interest && (int) $selected->id !== (int) $interest->id) {
|
|
throw new LogicException('Notification candidacy is inconsistent.');
|
|
}
|
|
$interest = $selected;
|
|
}
|
|
if ($interest && (int) $interest->partnership_id !== (int) $record->id) {
|
|
throw new LogicException('Notification belongs to another partnership.');
|
|
}
|
|
|
|
return ['version' => 1, 'partnership' => $this->scope($record), 'interest' => $interest ? $this->scope($interest) : null, 'room_id' => $room ? (int) $room->id : null];
|
|
}
|
|
|
|
/** @return array{id:int,user_id:int,organization_id:string|null} */
|
|
private function scope(stdClass $record): array
|
|
{
|
|
return ['id' => (int) $record->id, 'user_id' => (int) $record->user_id, 'organization_id' => $record->organization_id];
|
|
}
|
|
|
|
/** @return list<array{id:int,title:string,href:string,read_at:string|null}> */
|
|
public function forUser(User $actor): array
|
|
{
|
|
// Filter before the visible limit. An old, inaccessible notice never conceals a newer eligible page.
|
|
return array_values($this->query($actor)->lazyByIdDesc(100, 'n.id', 'id')->map(fn ($notice) => $this->project($actor, $notice))->filter()->take(50)->all());
|
|
}
|
|
|
|
public function markRead(User $actor, int $id): void
|
|
{
|
|
DB::transaction(function () use ($actor, $id): void {
|
|
$notice = $this->query($actor)->where('n.id', $id)->lockForUpdate()->first();
|
|
abort_unless($notice && $this->project($actor, $notice) !== null, 404);
|
|
if ($notice->read_at === null) {
|
|
DB::table('partnership_notifications')->where('id', $id)->where('user_id', $actor->id)->whereNull('read_at')->update(['read_at' => now()]);
|
|
}
|
|
});
|
|
}
|
|
|
|
private function query(User $actor): Builder
|
|
{
|
|
return DB::table('partnership_notifications as n')->join('partnership_events as e', 'e.id', '=', 'n.event_id')->where('n.user_id', $actor->id)
|
|
->select(['n.id', 'n.title', 'n.read_at', 'e.partnership_id', 'e.event_type', 'e.payload']);
|
|
}
|
|
|
|
/** @return array{id:int,title:string,href:string,read_at:string|null}|null */
|
|
private function project(User $actor, stdClass $notice): ?array
|
|
{
|
|
$envelope = json_decode($notice->payload, true, 512, JSON_THROW_ON_ERROR);
|
|
$payload = is_array($envelope) && is_array($envelope['payload'] ?? null) ? $envelope['payload'] : [];
|
|
$record = DB::table('portal_partnerships')->where('id', $notice->partnership_id)->first();
|
|
if (! $record) {
|
|
return null;
|
|
}
|
|
$context = $payload['_notification_context'] ?? null;
|
|
if (array_key_exists('_notification_context', $payload)) {
|
|
if (! is_array($context) || ($context['version'] ?? null) !== 1 || ! $this->knownScope($context['partnership'] ?? null) || (int) $context['partnership']['id'] !== (int) $record->id) {
|
|
return null;
|
|
}
|
|
if (($context['interest'] ?? null) !== null && ! $this->knownScope($context['interest'])) {
|
|
return null;
|
|
}
|
|
} else {
|
|
$context = $this->legacyContext($notice, $payload);
|
|
if ($context === null) {
|
|
return null;
|
|
}
|
|
}
|
|
$interest = null;
|
|
if (is_array($context['interest'] ?? null)) {
|
|
$interest = DB::table('partnership_interests')->where('id', $context['interest']['id'])->where('partnership_id', $record->id)->first();
|
|
if (! $interest) {
|
|
return null;
|
|
}
|
|
}
|
|
$exchange = app(PartnershipExchange::class);
|
|
$proponent = is_array($context['partnership'] ?? null) && $exchange->owns($actor, $record, false) && $exchange->owns($actor, (object) $context['partnership'], false);
|
|
$candidate = $interest && $exchange->owns($actor, $interest, false) && $exchange->owns($actor, (object) $context['interest'], false);
|
|
if (! $proponent && ! $candidate) {
|
|
return null;
|
|
}
|
|
$href = '/parcerias/'.$record->id;
|
|
if ($interest) {
|
|
$href = '/parcerias/candidaturas/'.$interest->id.'/historico';
|
|
if (is_int($context['room_id'] ?? null) && DB::table('partnership_rooms')->where('id', $context['room_id'])->where('interest_id', $interest->id)->exists()) {
|
|
$href = '/parcerias/salas/'.$context['room_id'];
|
|
}
|
|
}
|
|
|
|
return ['id' => (int) $notice->id, 'title' => $notice->title, 'href' => $href, 'read_at' => $notice->read_at];
|
|
}
|
|
|
|
private function knownScope(mixed $scope): bool
|
|
{
|
|
return is_array($scope) && is_int($scope['id'] ?? null) && $scope['id'] > 0 && is_int($scope['user_id'] ?? null) && $scope['user_id'] > 0
|
|
&& array_key_exists('organization_id', $scope) && ($scope['organization_id'] === null || is_string($scope['organization_id']) && Str::isUuid($scope['organization_id']));
|
|
}
|
|
|
|
/** @param array<string,mixed> $payload
|
|
* @return array<string,mixed>|null
|
|
*/
|
|
private function legacyContext(stdClass $notice, array $payload): ?array
|
|
{
|
|
if (! in_array($notice->event_type, ['partnership_interest.submitted.v1', 'partnership_interest.changed.v1'], true) || ! is_int($payload['interest_id'] ?? null)) {
|
|
return null;
|
|
}
|
|
$snapshot = $notice->event_type === 'partnership_interest.submitted.v1' ? ($payload['submitted'] ?? null) : ($payload['previous'] ?? null);
|
|
if (! is_array($snapshot) || ! array_key_exists('organization_id', $snapshot)) {
|
|
return null;
|
|
}
|
|
$interest = DB::table('partnership_interests')->where('id', $payload['interest_id'])->where('partnership_id', $notice->partnership_id)->first();
|
|
if (! $interest) {
|
|
return null;
|
|
}
|
|
$scope = ['id' => (int) $interest->id, 'user_id' => (int) $interest->user_id, 'organization_id' => $snapshot['organization_id']];
|
|
|
|
return $this->knownScope($scope) ? ['partnership' => null, 'interest' => $scope, 'room_id' => null] : null;
|
|
}
|
|
}
|