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

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;
}
}