Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

namespace HiEvents\Console;

use HiEvents\Jobs\Message\SendScheduledMessagesJob;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule): void
{
$schedule->job(new SendScheduledMessagesJob)->everyMinute()->withoutOverlapping();
}

protected function commands(): void
{
$this->load(__DIR__ . '/Commands');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ abstract class MessageDomainObjectAbstract extends \HiEvents\DomainObjects\Abstr
final public const UPDATED_AT = 'updated_at';
final public const DELETED_AT = 'deleted_at';
final public const ELIGIBILITY_FAILURES = 'eligibility_failures';
final public const SCHEDULED_AT = 'scheduled_at';

protected int $id;
protected int $event_id;
Expand All @@ -45,6 +46,7 @@ abstract class MessageDomainObjectAbstract extends \HiEvents\DomainObjects\Abstr
protected ?string $updated_at = null;
protected ?string $deleted_at = null;
protected array|string|null $eligibility_failures = null;
protected ?string $scheduled_at = null;

public function toArray(): array
{
Expand All @@ -66,6 +68,7 @@ public function toArray(): array
'updated_at' => $this->updated_at ?? null,
'deleted_at' => $this->deleted_at ?? null,
'eligibility_failures' => $this->eligibility_failures ?? null,
'scheduled_at' => $this->scheduled_at ?? null,
];
}

Expand Down Expand Up @@ -255,4 +258,15 @@ public function getEligibilityFailures(): array|string|null
{
return $this->eligibility_failures;
}

public function setScheduledAt(?string $scheduled_at): self
{
$this->scheduled_at = $scheduled_at;
return $this;
}

public function getScheduledAt(): ?string
{
return $this->scheduled_at;
}
}
10 changes: 9 additions & 1 deletion backend/app/DomainObjects/MessageDomainObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

namespace HiEvents\DomainObjects;

use HiEvents\DomainObjects\Interfaces\IsFilterable;
use HiEvents\DomainObjects\Interfaces\IsSortable;
use HiEvents\DomainObjects\SortingAndFiltering\AllowedSorts;
use HiEvents\Helper\StringHelper;

class MessageDomainObject extends Generated\MessageDomainObjectAbstract implements IsSortable
class MessageDomainObject extends Generated\MessageDomainObjectAbstract implements IsSortable, IsFilterable
{
private ?UserDomainObject $sentByUser = null;

Expand Down Expand Up @@ -37,6 +38,13 @@ public static function getAllowedSorts(): AllowedSorts

}

public static function getAllowedFilterFields(): array
{
return [
self::STATUS,
];
}

public function getSentByUser(): ?UserDomainObject
{
return $this->sentByUser;
Expand Down
2 changes: 2 additions & 0 deletions backend/app/DomainObjects/Status/MessageStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ enum MessageStatus
case PROCESSING;
case SENT;
case FAILED;
case SCHEDULED;
case CANCELLED;
}
28 changes: 28 additions & 0 deletions backend/app/Http/Actions/Messages/CancelMessageAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace HiEvents\Http\Actions\Messages;

use HiEvents\DomainObjects\EventDomainObject;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Resources\Message\MessageResource;
use HiEvents\Services\Application\Handlers\Message\CancelMessageHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class CancelMessageAction extends BaseAction
{
public function __construct(
private readonly CancelMessageHandler $cancelMessageHandler,
)
{
}

public function __invoke(Request $request, int $eventId, int $messageId): JsonResponse
{
$this->isActionAuthorized($eventId, EventDomainObject::class);

$message = $this->cancelMessageHandler->handle($messageId, $eventId);

return $this->resourceResponse(MessageResource::class, $message);
}
}
30 changes: 30 additions & 0 deletions backend/app/Http/Actions/Messages/GetMessageRecipientsAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace HiEvents\Http\Actions\Messages;

use HiEvents\DomainObjects\EventDomainObject;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Resources\Message\OutgoingMessageResource;
use HiEvents\Services\Application\Handlers\Message\GetMessageRecipientsHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class GetMessageRecipientsAction extends BaseAction
{
public function __construct(
private readonly GetMessageRecipientsHandler $handler,
)
{
}

public function __invoke(Request $request, int $eventId, int $messageId): JsonResponse
{
$this->isActionAuthorized($eventId, EventDomainObject::class);

$params = $this->getPaginationQueryParams($request);

$recipients = $this->handler->handle($eventId, $messageId, $params);

return $this->resourceResponse(OutgoingMessageResource::class, $recipients);
}
}
1 change: 1 addition & 0 deletions backend/app/Http/Actions/Messages/SendMessageAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function __invoke(SendMessageRequest $request, int $eventId): JsonRespons
'send_copy_to_current_user' => $request->boolean('send_copy_to_current_user'),
'sent_by_user_id' => $user->getId(),
'account_id' => $this->getAuthenticatedAccountId(),
'scheduled_at' => $request->input('scheduled_at'),
]));
} catch (AccountNotVerifiedException $e) {
return $this->errorResponse($e->getMessage(), Response::HTTP_UNAUTHORIZED);
Expand Down
3 changes: 2 additions & 1 deletion backend/app/Http/Request/Message/SendMessageRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function rules(): array
{
return [
'subject' => 'required|string|max:100',
'message' => 'required|string|max:5000',
'message' => 'required|string|max:8000',
'message_type' => [new In(MessageTypeEnum::valuesArray()), 'required'],
'is_test' => 'boolean',
'attendee_ids' => 'max:50,array|required_if:message_type,' . MessageTypeEnum::INDIVIDUAL_ATTENDEES->name,
Expand All @@ -25,6 +25,7 @@ public function rules(): array
'required_if:message_type,' . MessageTypeEnum::ORDER_OWNERS_WITH_PRODUCT->name,
new In([OrderStatus::COMPLETED->name, OrderStatus::AWAITING_OFFLINE_PAYMENT->name]),
],
'scheduled_at' => 'nullable|date|after:now',
];
}

Expand Down
44 changes: 44 additions & 0 deletions backend/app/Jobs/Message/SendScheduledMessagesJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace HiEvents\Jobs\Message;

use Carbon\Carbon;
use HiEvents\DomainObjects\Status\MessageStatus;
use HiEvents\Repository\Interfaces\MessageRepositoryInterface;
use HiEvents\Services\Domain\Message\MessageDispatchService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Throwable;

class SendScheduledMessagesJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public function handle(
MessageRepositoryInterface $messageRepository,
MessageDispatchService $messageDispatchService,
): void
{
$messages = $messageRepository->findWhere([
'status' => MessageStatus::SCHEDULED->name,
['scheduled_at', '<=', Carbon::now()->toDateTimeString()],
]);

foreach ($messages as $message) {
try {
$messageDispatchService->dispatchMessage($message);
} catch (Throwable $e) {
Log::error('Failed to dispatch scheduled message', [
'message_id' => $message->getId(),
'error' => $e->getMessage(),
]);
}
}
}
}
4 changes: 4 additions & 0 deletions backend/app/Repository/Eloquent/MessageRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public function findByEventId(int $eventId, QueryParamsDTO $params): LengthAware
};
}

if ($params->filter_fields && $params->filter_fields->isNotEmpty()) {
$this->applyFilterFields($params, MessageDomainObject::getAllowedFilterFields());
}

$this->model = $this->model->orderBy(
$params->sort_by ?? MessageDomainObject::getDefaultSort(),
$params->sort_direction ?? 'desc',
Expand Down
3 changes: 2 additions & 1 deletion backend/app/Resources/Message/MessageResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ public function toArray(Request $request): array
'attendee_ids' => $this->getAttendeeIds(),
'order_id' => $this->getOrderId(),
'product_ids' => $this->getProductIds(),
'sent_at' => $this->getCreatedAt(),
'sent_at' => $this->getSentAt(),
'status' => $this->getStatus(),
'scheduled_at' => $this->getScheduledAt(),
'message_preview' => $this->getMessagePreview(),
$this->mergeWhen(!is_null($this->getSentByUser()), fn() => [
'sent_by_user' => new UserResource($this->getSentByUser()),
Expand Down
25 changes: 25 additions & 0 deletions backend/app/Resources/Message/OutgoingMessageResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace HiEvents\Resources\Message;

use HiEvents\DomainObjects\OutgoingMessageDomainObject;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

/**
* @mixin OutgoingMessageDomainObject
*/
class OutgoingMessageResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->getId(),
'message_id' => $this->getMessageId(),
'recipient' => $this->getRecipient(),
'status' => $this->getStatus(),
'subject' => $this->getSubject(),
'created_at' => $this->getCreatedAt(),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@

namespace HiEvents\Services\Application\Handlers\Admin;

use HiEvents\DomainObjects\Enums\MessageTypeEnum;
use Carbon\Carbon;
use HiEvents\DomainObjects\MessageDomainObject;
use HiEvents\DomainObjects\Status\MessageStatus;
use HiEvents\Exceptions\ResourceNotFoundException;
use HiEvents\Jobs\Event\SendMessagesJob;
use HiEvents\Repository\Interfaces\MessageRepositoryInterface;
use HiEvents\Services\Application\Handlers\Message\DTO\SendMessageDTO;
use HiEvents\Services\Domain\Message\MessageDispatchService;
use Illuminate\Database\DatabaseManager;
use Illuminate\Validation\ValidationException;

Expand All @@ -19,6 +18,7 @@ class ApproveMessageHandler
public function __construct(
private readonly MessageRepositoryInterface $messageRepository,
private readonly DatabaseManager $databaseManager,
private readonly MessageDispatchService $messageDispatchService,
)
{
}
Expand All @@ -44,29 +44,17 @@ private function approveMessage(int $messageId): MessageDomainObject
]);
}

$updatedMessage = $this->messageRepository->updateFromArray($messageId, [
'status' => MessageStatus::PROCESSING->name,
]);
$scheduledAt = $message->getScheduledAt();
$isFutureScheduled = $scheduledAt !== null && Carbon::parse($scheduledAt)->isFuture();

$sendData = $message->getSendData();
$sendDataArray = is_string($sendData) ? json_decode($sendData, true) : $sendData;
if ($isFutureScheduled) {
return $this->messageRepository->updateFromArray($messageId, [
'status' => MessageStatus::SCHEDULED->name,
]);
}

SendMessagesJob::dispatch(new SendMessageDTO(
account_id: $sendDataArray['account_id'],
event_id: $message->getEventId(),
subject: $message->getSubject(),
message: $message->getMessage(),
type: MessageTypeEnum::fromName($message->getType()),
is_test: false,
send_copy_to_current_user: $sendDataArray['send_copy_to_current_user'] ?? false,
sent_by_user_id: $message->getSentByUserId(),
order_id: $message->getOrderId(),
order_statuses: $sendDataArray['order_statuses'] ?? [],
id: $message->getId(),
attendee_ids: $message->getAttendeeIds() ?? [],
product_ids: $message->getProductIds() ?? [],
));
$this->messageDispatchService->dispatchMessage($message, MessageStatus::PENDING_REVIEW);

return $updatedMessage;
return $this->messageRepository->findFirst($messageId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace HiEvents\Services\Application\Handlers\Message;

use HiEvents\DomainObjects\MessageDomainObject;
use HiEvents\DomainObjects\Status\MessageStatus;
use HiEvents\Exceptions\ResourceNotFoundException;
use HiEvents\Repository\Interfaces\MessageRepositoryInterface;
use Illuminate\Validation\ValidationException;

class CancelMessageHandler
{
public function __construct(
private readonly MessageRepositoryInterface $messageRepository,
)
{
}

public function handle(int $messageId, int $eventId): MessageDomainObject
{
$message = $this->messageRepository->findFirstWhere([
'id' => $messageId,
'event_id' => $eventId,
]);

if ($message === null) {
throw new ResourceNotFoundException(__('Message not found'));
}

if ($message->getStatus() !== MessageStatus::SCHEDULED->name) {
throw ValidationException::withMessages([
'status' => [__('Only scheduled messages can be cancelled')],
]);
}

$updated = $this->messageRepository->updateWhere(
['status' => MessageStatus::CANCELLED->name],
['id' => $messageId, 'status' => MessageStatus::SCHEDULED->name],
);

if ($updated === 0) {
throw ValidationException::withMessages([
'status' => [__('This message can no longer be cancelled')],
]);
}

return $this->messageRepository->findFirst($messageId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function __construct(
public readonly ?int $id = null,
public readonly ?array $attendee_ids = [],
public readonly ?array $product_ids = [],
public readonly ?string $scheduled_at = null,
)
{
}
Expand Down
Loading
Loading