From a8161eef761ff7b5e672e1b1e2ccd697c19cd36f Mon Sep 17 00:00:00 2001 From: Dave Earley Date: Sun, 23 Mar 2025 08:10:11 -0700 Subject: [PATCH 1/6] Fix disabled ComboBox style (#456) --- backend/app/Resources/Image/ImageResource.php | 1 + frontend/src/components/common/CustomSelect/index.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/app/Resources/Image/ImageResource.php b/backend/app/Resources/Image/ImageResource.php index f7d896e3..26c4cbaf 100644 --- a/backend/app/Resources/Image/ImageResource.php +++ b/backend/app/Resources/Image/ImageResource.php @@ -16,6 +16,7 @@ public function toArray($request): array return [ 'id' => $this->getId(), 'url' => Url::getCdnUrl($this->getPath()), + 'path' => $this->getPath(), 'size' => $this->getSize(), 'file_name' => $this->getFileName(), 'mime_type' => $this->getMimeType(), diff --git a/frontend/src/components/common/CustomSelect/index.tsx b/frontend/src/components/common/CustomSelect/index.tsx index d4fb4d61..944c4a81 100644 --- a/frontend/src/components/common/CustomSelect/index.tsx +++ b/frontend/src/components/common/CustomSelect/index.tsx @@ -112,7 +112,9 @@ export const CustomSelect = ({ }; return ( - + {label && ( {label} From afcb4fe391292983520132d1f47fa856b9dea555 Mon Sep 17 00:00:00 2001 From: Dave Earley Date: Sun, 23 Mar 2025 20:12:12 -0700 Subject: [PATCH 2/6] Add domain events (Preparation for integrations) (#457) * Add domain events (Preparation for integrations) * Update event type --- .../Request/Webhook/UpsertWebhookRequest.php | 4 +- .../Webhook/DispatchAttendeeWebhookJob.php | 6 +- .../Webhook/DispatchCheckInWebhookJob.php | 6 +- .../Order/Webhook/DispatchOrderWebhookJob.php | 6 +- .../Webhook/DispatchProductWebhookJob.php | 6 +- .../Webhook/WebhookEventListener.php | 55 ++++++++++++++++ .../app/Providers/EventServiceProvider.php | 35 ++++++++--- .../Repository/Eloquent/BaseRepository.php | 4 +- .../Interfaces/RepositoryInterface.php | 4 +- .../Attendee/CreateAttendeeHandler.php | 16 ++--- .../Handlers/Attendee/EditAttendeeHandler.php | 15 +++-- .../Attendee/PartialEditAttendeeHandler.php | 15 +++-- .../CreateAttendeeCheckInPublicHandler.php | 15 +++-- .../DeleteAttendeeCheckInPublicHandler.php | 41 +++++++----- .../Handlers/Order/CancelOrderHandler.php | 2 - .../Handlers/Order/CompleteOrderHandler.php | 15 +++-- ...TransitionOrderToOfflinePaymentHandler.php | 15 +++-- .../Handlers/Product/EditProductHandler.php | 17 ++--- .../Domain/Order/EditOrderService.php | 19 +++--- .../Domain/Order/MarkOrderAsPaidService.php | 15 +++-- .../Domain/Order/OrderCancelService.php | 15 +++-- .../ChargeRefundUpdatedHandler.php | 21 ++++--- .../PaymentIntentSucceededHandler.php | 15 +++-- .../Domain/Product/CreateProductService.php | 15 +++-- .../Domain/Product/DeleteProductService.php | 15 +++-- .../DomainEventDispatcherService.php | 18 ++++++ .../DomainEvents/Enums/DomainEventType.php} | 6 +- .../DomainEvents/Events/AttendeeEvent.php | 15 +++++ .../DomainEvents/Events/BaseDomainEvent.php | 10 +++ .../DomainEvents/Events/CheckinEvent.php | 15 +++++ .../DomainEvents/Events/OrderEvent.php | 15 +++++ .../DomainEvents/Events/ProductEvent.php | 15 +++++ .../Webhook/WebhookDispatchService.php | 62 ++++--------------- backend/config/queue.php | 2 +- .../Order/CompleteOrderHandlerTest.php | 23 ++++--- .../Domain/Order/OrderCancelServiceTest.php | 17 +++-- .../modals/WebhookLogsModal/index.tsx | 4 +- 37 files changed, 387 insertions(+), 207 deletions(-) create mode 100644 backend/app/Listeners/Webhook/WebhookEventListener.php create mode 100644 backend/app/Services/Infrastructure/DomainEvents/DomainEventDispatcherService.php rename backend/app/{DomainObjects/Enums/WebhookEventType.php => Services/Infrastructure/DomainEvents/Enums/DomainEventType.php} (82%) create mode 100644 backend/app/Services/Infrastructure/DomainEvents/Events/AttendeeEvent.php create mode 100644 backend/app/Services/Infrastructure/DomainEvents/Events/BaseDomainEvent.php create mode 100644 backend/app/Services/Infrastructure/DomainEvents/Events/CheckinEvent.php create mode 100644 backend/app/Services/Infrastructure/DomainEvents/Events/OrderEvent.php create mode 100644 backend/app/Services/Infrastructure/DomainEvents/Events/ProductEvent.php diff --git a/backend/app/Http/Request/Webhook/UpsertWebhookRequest.php b/backend/app/Http/Request/Webhook/UpsertWebhookRequest.php index b0eb746f..28aa337a 100644 --- a/backend/app/Http/Request/Webhook/UpsertWebhookRequest.php +++ b/backend/app/Http/Request/Webhook/UpsertWebhookRequest.php @@ -2,9 +2,9 @@ namespace HiEvents\Http\Request\Webhook; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Status\WebhookStatus; use HiEvents\Http\Request\BaseRequest; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use Illuminate\Validation\Rule; class UpsertWebhookRequest extends BaseRequest @@ -13,7 +13,7 @@ public function rules(): array { return [ 'url' => 'required|url', - 'event_types.*' => ['required', Rule::in(WebhookEventType::valuesArray())], + 'event_types.*' => ['required', Rule::in(DomainEventType::valuesArray())], 'status' => ['nullable', Rule::in(WebhookStatus::valuesArray())], ]; } diff --git a/backend/app/Jobs/Order/Webhook/DispatchAttendeeWebhookJob.php b/backend/app/Jobs/Order/Webhook/DispatchAttendeeWebhookJob.php index fc885634..e7c30804 100644 --- a/backend/app/Jobs/Order/Webhook/DispatchAttendeeWebhookJob.php +++ b/backend/app/Jobs/Order/Webhook/DispatchAttendeeWebhookJob.php @@ -2,7 +2,7 @@ namespace HiEvents\Jobs\Order\Webhook; -use HiEvents\DomainObjects\Enums\WebhookEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; use Illuminate\Bus\Queueable; use Illuminate\Foundation\Bus\Dispatchable; @@ -14,8 +14,8 @@ class DispatchAttendeeWebhookJob use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( - public int $attendeeId, - public WebhookEventType $eventType, + public int $attendeeId, + public DomainEventType $eventType, ) { } diff --git a/backend/app/Jobs/Order/Webhook/DispatchCheckInWebhookJob.php b/backend/app/Jobs/Order/Webhook/DispatchCheckInWebhookJob.php index db5ce766..02db3668 100644 --- a/backend/app/Jobs/Order/Webhook/DispatchCheckInWebhookJob.php +++ b/backend/app/Jobs/Order/Webhook/DispatchCheckInWebhookJob.php @@ -2,7 +2,7 @@ namespace HiEvents\Jobs\Order\Webhook; -use HiEvents\DomainObjects\Enums\WebhookEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; use Illuminate\Bus\Queueable; use Illuminate\Foundation\Bus\Dispatchable; @@ -14,8 +14,8 @@ class DispatchCheckInWebhookJob use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( - public int $attendeeCheckInId, - public WebhookEventType $eventType, + public int $attendeeCheckInId, + public DomainEventType $eventType, ) { } diff --git a/backend/app/Jobs/Order/Webhook/DispatchOrderWebhookJob.php b/backend/app/Jobs/Order/Webhook/DispatchOrderWebhookJob.php index fa9aa453..e0bca073 100644 --- a/backend/app/Jobs/Order/Webhook/DispatchOrderWebhookJob.php +++ b/backend/app/Jobs/Order/Webhook/DispatchOrderWebhookJob.php @@ -2,7 +2,7 @@ namespace HiEvents\Jobs\Order\Webhook; -use HiEvents\DomainObjects\Enums\WebhookEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; use Illuminate\Bus\Queueable; use Illuminate\Foundation\Bus\Dispatchable; @@ -14,8 +14,8 @@ class DispatchOrderWebhookJob use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( - public int $orderId, - public WebhookEventType $eventType, + public int $orderId, + public DomainEventType $eventType, ) { } diff --git a/backend/app/Jobs/Order/Webhook/DispatchProductWebhookJob.php b/backend/app/Jobs/Order/Webhook/DispatchProductWebhookJob.php index 12e70eb6..978b877a 100644 --- a/backend/app/Jobs/Order/Webhook/DispatchProductWebhookJob.php +++ b/backend/app/Jobs/Order/Webhook/DispatchProductWebhookJob.php @@ -2,7 +2,7 @@ namespace HiEvents\Jobs\Order\Webhook; -use HiEvents\DomainObjects\Enums\WebhookEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; use Illuminate\Bus\Queueable; use Illuminate\Foundation\Bus\Dispatchable; @@ -14,8 +14,8 @@ class DispatchProductWebhookJob use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct( - public int $productId, - public WebhookEventType $eventType, + public int $productId, + public DomainEventType $eventType, ) { } diff --git a/backend/app/Listeners/Webhook/WebhookEventListener.php b/backend/app/Listeners/Webhook/WebhookEventListener.php new file mode 100644 index 00000000..c3f93cfd --- /dev/null +++ b/backend/app/Listeners/Webhook/WebhookEventListener.php @@ -0,0 +1,55 @@ +config->get('queue.webhook_queue_name'); + + switch (get_class($event)) { + case AttendeeEvent::class: + DispatchAttendeeWebhookJob::dispatch( + attendeeId: $event->attendeeId, + eventType: $event->type, + )->onQueue($queueName); + break; + case OrderEvent::class: + DispatchOrderWebhookJob::dispatch( + orderId: $event->orderId, + eventType: $event->type, + )->onQueue($queueName); + break; + case ProductEvent::class: + DispatchProductWebhookJob::dispatch( + productId: $event->productId, + eventType: $event->type, + )->onQueue($queueName); + break; + case CheckinEvent::class: + DispatchCheckInWebhookJob::dispatch( + attendeeCheckInId: $event->attendeeCheckinId, + eventType: $event->type, + )->onQueue($queueName); + break; + } + } +} diff --git a/backend/app/Providers/EventServiceProvider.php b/backend/app/Providers/EventServiceProvider.php index 338991f4..9d94df60 100644 --- a/backend/app/Providers/EventServiceProvider.php +++ b/backend/app/Providers/EventServiceProvider.php @@ -2,20 +2,27 @@ namespace HiEvents\Providers; -use Illuminate\Auth\Events\Registered; -use Illuminate\Auth\Listeners\SendEmailVerificationNotification; +use HiEvents\Listeners\Webhook\WebhookEventListener; +use HiEvents\Services\Infrastructure\DomainEvents\Events\AttendeeEvent; +use HiEvents\Services\Infrastructure\DomainEvents\Events\CheckinEvent; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; +use HiEvents\Services\Infrastructure\DomainEvents\Events\ProductEvent; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use Illuminate\Support\Facades\Event; class EventServiceProvider extends ServiceProvider { /** - * The event to listener mappings for the application. + * Map of listeners to the events they should handle. * - * @var array> + * @var array> */ - protected $listen = [ - Registered::class => [ - SendEmailVerificationNotification::class, + private static array $domainEventMap = [ + WebhookEventListener::class => [ + ProductEvent::class, + OrderEvent::class, + AttendeeEvent::class, + CheckinEvent::class, ], ]; @@ -24,7 +31,19 @@ class EventServiceProvider extends ServiceProvider */ public function boot(): void { - // + $this->registerDomainEventListeners(); + } + + /** + * Dynamically register all domain event listeners. + */ + private function registerDomainEventListeners(): void + { + foreach (self::$domainEventMap as $listener => $events) { + foreach ($events as $event) { + Event::listen($event, [$listener, 'handle']); + } + } } /** diff --git a/backend/app/Repository/Eloquent/BaseRepository.php b/backend/app/Repository/Eloquent/BaseRepository.php index b5f0e1a2..0a3364b7 100644 --- a/backend/app/Repository/Eloquent/BaseRepository.php +++ b/backend/app/Repository/Eloquent/BaseRepository.php @@ -96,7 +96,7 @@ public function paginateWhere( public function simplePaginateWhere( array $where, - int $limit = null, + ?int $limit = null, array $columns = self::DEFAULT_COLUMNS, ): Paginator { @@ -126,7 +126,7 @@ public function findById(int $id, array $columns = self::DEFAULT_COLUMNS): Domai public function findFirstByField( string $field, - string $value = null, + ?string $value = null, array $columns = ['*'] ): ?DomainObjectInterface { diff --git a/backend/app/Repository/Interfaces/RepositoryInterface.php b/backend/app/Repository/Interfaces/RepositoryInterface.php index 0b5930bc..13783497 100644 --- a/backend/app/Repository/Interfaces/RepositoryInterface.php +++ b/backend/app/Repository/Interfaces/RepositoryInterface.php @@ -71,7 +71,7 @@ public function paginateWhere( */ public function simplePaginateWhere( array $where, - int $limit = null, + ?int $limit = null, array $columns = self::DEFAULT_COLUMNS, ): Paginator; @@ -129,7 +129,7 @@ public function findFirstWhere(array $where, array $columns = self::DEFAULT_COLU */ public function findFirstByField( string $field, - string $value = null, + ?string $value = null, array $columns = ['*'] ): ?DomainObjectInterface; diff --git a/backend/app/Services/Application/Handlers/Attendee/CreateAttendeeHandler.php b/backend/app/Services/Application/Handlers/Attendee/CreateAttendeeHandler.php index 8ccd56d5..4ca0d841 100644 --- a/backend/app/Services/Application/Handlers/Attendee/CreateAttendeeHandler.php +++ b/backend/app/Services/Application/Handlers/Attendee/CreateAttendeeHandler.php @@ -5,7 +5,6 @@ use Brick\Money\Money; use HiEvents\DomainObjects\AttendeeDomainObject; use HiEvents\DomainObjects\Enums\ProductType; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Generated\AttendeeDomainObjectAbstract; use HiEvents\DomainObjects\Generated\OrderDomainObjectAbstract; use HiEvents\DomainObjects\Generated\OrderItemDomainObjectAbstract; @@ -31,7 +30,9 @@ use HiEvents\Services\Domain\Order\OrderManagementService; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; use HiEvents\Services\Domain\Tax\TaxAndFeeRollupService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use Illuminate\Database\DatabaseManager; use Illuminate\Support\Collection; use RuntimeException; @@ -49,7 +50,7 @@ public function __construct( private readonly TaxAndFeeRepositoryInterface $taxAndFeeRepository, private readonly TaxAndFeeRollupService $taxAndFeeRollupService, private readonly OrderManagementService $orderManagementService, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -103,7 +104,7 @@ public function handle(CreateAttendeeDTO $attendeeDTO): AttendeeDomainObject $this->fireEventsAndUpdateQuantities($attendeeDTO, $order); - $this->queueWebhooks($attendee, $order); + $this->queueWebhooks($order); return $attendee; }); @@ -247,11 +248,10 @@ private function fireEventsAndUpdateQuantities(CreateAttendeeDTO $attendeeDTO, O )); } - private function queueWebhooks(AttendeeDomainObject $attendee, OrderDomainObject $order): void + private function queueWebhooks(OrderDomainObject $order): void { - $this->webhookDispatchService->queueOrderWebhook( - eventType: WebhookEventType::ORDER_CREATED, - orderId: $order->getId(), + $this->domainEventDispatcherService->dispatch( + new OrderEvent(DomainEventType::ORDER_CREATED, $order->getId()) ); } } diff --git a/backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php b/backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php index 08d197ad..9a714d2a 100644 --- a/backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php +++ b/backend/app/Services/Application/Handlers/Attendee/EditAttendeeHandler.php @@ -4,7 +4,6 @@ use HiEvents\DomainObjects\AttendeeDomainObject; use HiEvents\DomainObjects\Enums\ProductPriceType; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Generated\AttendeeDomainObjectAbstract; use HiEvents\DomainObjects\Generated\ProductDomainObjectAbstract; use HiEvents\DomainObjects\ProductDomainObject; @@ -14,7 +13,9 @@ use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Services\Application\Handlers\Attendee\DTO\EditAttendeeDTO; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\AttendeeEvent; use Illuminate\Database\DatabaseManager; use Illuminate\Validation\ValidationException; use Throwable; @@ -26,7 +27,7 @@ public function __construct( private readonly ProductRepositoryInterface $productRepository, private readonly ProductQuantityUpdateService $productQuantityService, private readonly DatabaseManager $databaseManager, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -46,9 +47,11 @@ public function handle(EditAttendeeDTO $editAttendeeDTO): AttendeeDomainObject $updatedAttendee = $this->updateAttendee($editAttendeeDTO); - $this->webhookDispatchService->queueAttendeeWebhook( - eventType: WebhookEventType::ATTENDEE_UPDATED, - attendeeId: $updatedAttendee->getId(), + $this->domainEventDispatcherService->dispatch( + new AttendeeEvent( + type: DomainEventType::ATTENDEE_UPDATED, + attendeeId: $updatedAttendee->getId(), + ) ); return $updatedAttendee; diff --git a/backend/app/Services/Application/Handlers/Attendee/PartialEditAttendeeHandler.php b/backend/app/Services/Application/Handlers/Attendee/PartialEditAttendeeHandler.php index 316899d8..e724b88d 100644 --- a/backend/app/Services/Application/Handlers/Attendee/PartialEditAttendeeHandler.php +++ b/backend/app/Services/Application/Handlers/Attendee/PartialEditAttendeeHandler.php @@ -3,12 +3,13 @@ namespace HiEvents\Services\Application\Handlers\Attendee; use HiEvents\DomainObjects\AttendeeDomainObject; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Status\AttendeeStatus; use HiEvents\Repository\Interfaces\AttendeeRepositoryInterface; use HiEvents\Services\Application\Handlers\Attendee\DTO\PartialEditAttendeeDTO; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\AttendeeEvent; use Illuminate\Database\DatabaseManager; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Throwable; @@ -19,7 +20,7 @@ public function __construct( private readonly AttendeeRepositoryInterface $attendeeRepository, private readonly ProductQuantityUpdateService $productQuantityService, private readonly DatabaseManager $databaseManager, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -52,9 +53,11 @@ private function updateAttendee(PartialEditAttendeeDTO $data): AttendeeDomainObj } if ($statusIsUpdated && $data->status === AttendeeStatus::CANCELLED->name) { - $this->webhookDispatchService->queueAttendeeWebhook( - eventType: WebhookEventType::ATTENDEE_CANCELLED, - attendeeId: $attendee->getId(), + $this->domainEventDispatcherService->dispatch( + new AttendeeEvent( + type: DomainEventType::ATTENDEE_CANCELLED, + attendeeId: $attendee->getId(), + ) ); } diff --git a/backend/app/Services/Application/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php b/backend/app/Services/Application/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php index 11b6e12f..74916036 100644 --- a/backend/app/Services/Application/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php +++ b/backend/app/Services/Application/Handlers/CheckInList/Public/CreateAttendeeCheckInPublicHandler.php @@ -3,12 +3,13 @@ namespace HiEvents\Services\Application\Handlers\CheckInList\Public; use HiEvents\DomainObjects\AttendeeCheckInDomainObject; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\Exceptions\CannotCheckInException; use HiEvents\Services\Application\Handlers\CheckInList\Public\DTO\CreateAttendeeCheckInPublicDTO; use HiEvents\Services\Domain\CheckInList\CreateAttendeeCheckInService; use HiEvents\Services\Domain\CheckInList\DTO\CreateAttendeeCheckInsResponseDTO; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\CheckinEvent; use Psr\Log\LoggerInterface; use Throwable; @@ -17,7 +18,7 @@ class CreateAttendeeCheckInPublicHandler public function __construct( private readonly CreateAttendeeCheckInService $createAttendeeCheckInService, private readonly LoggerInterface $logger, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -42,9 +43,11 @@ public function handle(CreateAttendeeCheckInPublicDTO $checkInData): CreateAtten /** @var AttendeeCheckInDomainObject $checkIn */ foreach ($checkIns->attendeeCheckIns as $checkIn) { - $this->webhookDispatchService->queueCheckInWebhook( - WebhookEventType::CHECKIN_CREATED, - $checkIn->getId(), + $this->domainEventDispatcherService->dispatch( + new CheckinEvent( + type: DomainEventType::CHECKIN_CREATED, + attendeeCheckinId: $checkIn->getId(), + ) ); } diff --git a/backend/app/Services/Application/Handlers/CheckInList/Public/DeleteAttendeeCheckInPublicHandler.php b/backend/app/Services/Application/Handlers/CheckInList/Public/DeleteAttendeeCheckInPublicHandler.php index 09efe080..569b4c34 100644 --- a/backend/app/Services/Application/Handlers/CheckInList/Public/DeleteAttendeeCheckInPublicHandler.php +++ b/backend/app/Services/Application/Handlers/CheckInList/Public/DeleteAttendeeCheckInPublicHandler.php @@ -2,42 +2,51 @@ namespace HiEvents\Services\Application\Handlers\CheckInList\Public; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\Exceptions\CannotCheckInException; use HiEvents\Services\Application\Handlers\CheckInList\Public\DTO\DeleteAttendeeCheckInPublicDTO; use HiEvents\Services\Domain\CheckInList\DeleteAttendeeCheckInService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\CheckinEvent; +use Illuminate\Database\DatabaseManager; use Psr\Log\LoggerInterface; +use Throwable; class DeleteAttendeeCheckInPublicHandler { public function __construct( private readonly DeleteAttendeeCheckInService $deleteAttendeeCheckInService, private readonly LoggerInterface $logger, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, + private readonly DatabaseManager $databaseManager ) { } /** * @throws CannotCheckInException + * @throws Throwable */ public function handle(DeleteAttendeeCheckInPublicDTO $checkInData): void { - $deletedCheckInId = $this->deleteAttendeeCheckInService->deleteAttendeeCheckIn( - $checkInData->checkInListShortId, - $checkInData->checkInShortId, - ); + $this->databaseManager->transaction(function () use ($checkInData) { + $deletedCheckInId = $this->deleteAttendeeCheckInService->deleteAttendeeCheckIn( + $checkInData->checkInListShortId, + $checkInData->checkInShortId, + ); - $this->logger->info('Attendee check-in deleted', [ - 'check_in_list_uuid' => $checkInData->checkInListShortId, - 'attendee_public_id' => $checkInData->checkInShortId, - 'check_in_user_ip_address' => $checkInData->checkInUserIpAddress, - ]); + $this->logger->info('Attendee check-in deleted', [ + 'check_in_list_uuid' => $checkInData->checkInListShortId, + 'attendee_public_id' => $checkInData->checkInShortId, + 'check_in_user_ip_address' => $checkInData->checkInUserIpAddress, + ]); - $this->webhookDispatchService->queueCheckInWebhook( - eventType: WebhookEventType::CHECKIN_DELETED, - attendeeCheckInId: $deletedCheckInId, - ); + $this->domainEventDispatcherService->dispatch( + new CheckinEvent( + type: DomainEventType::CHECKIN_DELETED, + attendeeCheckinId: $deletedCheckInId, + ) + ); + }); } } diff --git a/backend/app/Services/Application/Handlers/Order/CancelOrderHandler.php b/backend/app/Services/Application/Handlers/Order/CancelOrderHandler.php index 0b30fb8f..c44af907 100644 --- a/backend/app/Services/Application/Handlers/Order/CancelOrderHandler.php +++ b/backend/app/Services/Application/Handlers/Order/CancelOrderHandler.php @@ -2,14 +2,12 @@ namespace HiEvents\Services\Application\Handlers\Order; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Generated\OrderDomainObjectAbstract; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\Exceptions\ResourceConflictException; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Application\Handlers\Order\DTO\CancelOrderDTO; use HiEvents\Services\Domain\Order\OrderCancelService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; use Illuminate\Database\DatabaseManager; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Throwable; diff --git a/backend/app/Services/Application/Handlers/Order/CompleteOrderHandler.php b/backend/app/Services/Application/Handlers/Order/CompleteOrderHandler.php index b998d84d..1604f253 100644 --- a/backend/app/Services/Application/Handlers/Order/CompleteOrderHandler.php +++ b/backend/app/Services/Application/Handlers/Order/CompleteOrderHandler.php @@ -8,7 +8,6 @@ use Exception; use HiEvents\DomainObjects\AttendeeDomainObject; use HiEvents\DomainObjects\Enums\ProductType; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Generated\AttendeeDomainObjectAbstract; use HiEvents\DomainObjects\Generated\OrderDomainObjectAbstract; use HiEvents\DomainObjects\Generated\ProductPriceDomainObjectAbstract; @@ -34,7 +33,9 @@ use HiEvents\Services\Application\Handlers\Order\DTO\OrderQuestionsDTO; use HiEvents\Services\Domain\Payment\Stripe\EventHandlers\PaymentIntentSucceededHandler; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use RuntimeException; @@ -51,7 +52,7 @@ public function __construct( private readonly QuestionAnswerRepositoryInterface $questionAnswersRepository, private readonly ProductQuantityUpdateService $productQuantityUpdateService, private readonly ProductPriceRepositoryInterface $productPriceRepository, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -87,9 +88,11 @@ public function handle(string $orderShortId, CompleteOrderDTO $orderData): Order OrderStatusChangedEvent::dispatch($updatedOrder); if ($updatedOrder->isOrderCompleted()) { - $this->webhookDispatchService->queueOrderWebhook( - eventType: WebhookEventType::ORDER_CREATED, - orderId: $updatedOrder->getId(), + $this->domainEventDispatcherService->dispatch( + new OrderEvent( + type: DomainEventType::ORDER_CREATED, + orderId: $updatedOrder->getId(), + ) ); } diff --git a/backend/app/Services/Application/Handlers/Order/TransitionOrderToOfflinePaymentHandler.php b/backend/app/Services/Application/Handlers/Order/TransitionOrderToOfflinePaymentHandler.php index 1b41da01..b7d57773 100644 --- a/backend/app/Services/Application/Handlers/Order/TransitionOrderToOfflinePaymentHandler.php +++ b/backend/app/Services/Application/Handlers/Order/TransitionOrderToOfflinePaymentHandler.php @@ -3,7 +3,6 @@ namespace HiEvents\Services\Application\Handlers\Order; use HiEvents\DomainObjects\Enums\PaymentProviders; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\EventSettingDomainObject; use HiEvents\DomainObjects\Generated\OrderDomainObjectAbstract; use HiEvents\DomainObjects\OrderDomainObject; @@ -17,7 +16,9 @@ use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Application\Handlers\Order\DTO\TransitionOrderToOfflinePaymentPublicDTO; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use Illuminate\Database\DatabaseManager; class TransitionOrderToOfflinePaymentHandler @@ -27,7 +28,7 @@ public function __construct( private readonly OrderRepositoryInterface $orderRepository, private readonly DatabaseManager $databaseManager, private readonly EventSettingsRepositoryInterface $eventSettingsRepository, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { @@ -62,9 +63,11 @@ public function handle(TransitionOrderToOfflinePaymentPublicDTO $dto): OrderDoma createInvoice: $eventSettings->getEnableInvoicing(), )); - $this->webhookDispatchService->queueOrderWebhook( - eventType: WebhookEventType::ORDER_CREATED, - orderId: $order->getId(), + $this->domainEventDispatcherService->dispatch( + new OrderEvent( + type: DomainEventType::ORDER_CREATED, + orderId: $order->getId(), + ), ); return $order; diff --git a/backend/app/Services/Application/Handlers/Product/EditProductHandler.php b/backend/app/Services/Application/Handlers/Product/EditProductHandler.php index 5044685e..d45c9b71 100644 --- a/backend/app/Services/Application/Handlers/Product/EditProductHandler.php +++ b/backend/app/Services/Application/Handlers/Product/EditProductHandler.php @@ -5,7 +5,6 @@ namespace HiEvents\Services\Application\Handlers\Product; use Exception; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Interfaces\DomainObjectInterface; use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\DomainObjects\ProductPriceDomainObject; @@ -14,13 +13,14 @@ use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Services\Application\Handlers\Product\DTO\UpsertProductDTO; -use HiEvents\Services\Domain\Product\ProductOrderingService; use HiEvents\Services\Domain\Product\ProductPriceUpdateService; use HiEvents\Services\Domain\ProductCategory\GetProductCategoryService; use HiEvents\Services\Domain\Tax\DTO\TaxAndProductAssociateParams; use HiEvents\Services\Domain\Tax\TaxAndProductAssociationService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\ProductEvent; use HiEvents\Services\Infrastructure\HtmlPurifier\HtmlPurifierService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; use Illuminate\Database\DatabaseManager; use Throwable; @@ -36,9 +36,8 @@ public function __construct( private readonly ProductPriceUpdateService $priceUpdateService, private readonly HtmlPurifierService $purifier, private readonly EventRepositoryInterface $eventRepository, - private readonly ProductOrderingService $productOrderingService, private readonly GetProductCategoryService $getProductCategoryService, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -65,9 +64,11 @@ public function handle(UpsertProductDTO $productsData): DomainObjectInterface $this->eventRepository->findById($productsData->event_id) ); - $this->webhookDispatchService->queueProductWebhook( - eventType: WebhookEventType::PRODUCT_UPDATED, - productId: $product->getId(), + $this->domainEventDispatcherService->dispatch( + new ProductEvent( + type: DomainEventType::PRODUCT_UPDATED, + productId: $product->getId(), + ) ); return $this->productRepository diff --git a/backend/app/Services/Domain/Order/EditOrderService.php b/backend/app/Services/Domain/Order/EditOrderService.php index 5bfb4184..186b73db 100644 --- a/backend/app/Services/Domain/Order/EditOrderService.php +++ b/backend/app/Services/Domain/Order/EditOrderService.php @@ -2,19 +2,20 @@ namespace HiEvents\Services\Domain\Order; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use Illuminate\Database\DatabaseManager; use Throwable; class EditOrderService { public function __construct( - private readonly OrderRepositoryInterface $orderRepository, - private readonly WebhookDispatchService $webhookDispatchService, - private readonly DatabaseManager $databaseManager, + private readonly OrderRepositoryInterface $orderRepository, + private readonly DomainEventDispatcherService $domainEventDispatcherService, + private readonly DatabaseManager $databaseManager, ) { } @@ -43,9 +44,11 @@ public function editOrder( ] ); - $this->webhookDispatchService->queueOrderWebhook( - eventType: WebhookEventType::ORDER_UPDATED, - orderId: $id, + $this->domainEventDispatcherService->dispatch( + new OrderEvent( + type: DomainEventType::ORDER_UPDATED, + orderId: $id, + ), ); return $this->orderRepository->findById($id); diff --git a/backend/app/Services/Domain/Order/MarkOrderAsPaidService.php b/backend/app/Services/Domain/Order/MarkOrderAsPaidService.php index ea5a6bf9..4682d3f2 100644 --- a/backend/app/Services/Domain/Order/MarkOrderAsPaidService.php +++ b/backend/app/Services/Domain/Order/MarkOrderAsPaidService.php @@ -6,7 +6,6 @@ use HiEvents\DomainObjects\AccountDomainObject; use HiEvents\DomainObjects\AttendeeDomainObject; use HiEvents\DomainObjects\Enums\PaymentProviders; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; use HiEvents\DomainObjects\Generated\OrderDomainObjectAbstract; @@ -27,7 +26,9 @@ use HiEvents\Repository\Interfaces\InvoiceRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Domain\Mail\SendOrderDetailsService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use Illuminate\Database\DatabaseManager; use Throwable; @@ -38,7 +39,7 @@ public function __construct( private readonly DatabaseManager $databaseManager, private readonly InvoiceRepositoryInterface $invoiceRepository, private readonly AttendeeRepositoryInterface $attendeeRepository, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, private readonly OrderApplicationFeeCalculationService $orderApplicationFeeCalculationService, private readonly EventRepositoryInterface $eventRepository, private readonly OrderApplicationFeeService $orderApplicationFeeService, @@ -88,9 +89,11 @@ public function markOrderAsPaid( sendEmails: false )); - $this->webhookDispatchService->queueOrderWebhook( - eventType: WebhookEventType::ORDER_MARKED_AS_PAID, - orderId: $orderId, + $this->domainEventDispatcherService->dispatch( + new OrderEvent( + type: DomainEventType::ORDER_MARKED_AS_PAID, + orderId: $orderId, + ), ); $this->storeApplicationFeePayment($updatedOrder); diff --git a/backend/app/Services/Domain/Order/OrderCancelService.php b/backend/app/Services/Domain/Order/OrderCancelService.php index 31ffbc30..2c02a40d 100644 --- a/backend/app/Services/Domain/Order/OrderCancelService.php +++ b/backend/app/Services/Domain/Order/OrderCancelService.php @@ -3,7 +3,6 @@ namespace HiEvents\Services\Domain\Order; use HiEvents\DomainObjects\AttendeeDomainObject; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\EventSettingDomainObject; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\DomainObjects\Status\AttendeeStatus; @@ -13,7 +12,9 @@ use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use Illuminate\Contracts\Mail\Mailer; use Illuminate\Database\DatabaseManager; use Throwable; @@ -27,7 +28,7 @@ public function __construct( private readonly OrderRepositoryInterface $orderRepository, private readonly DatabaseManager $databaseManager, private readonly ProductQuantityUpdateService $productQuantityService, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -55,9 +56,11 @@ public function cancelOrder(OrderDomainObject $order): void eventSettings: $event->getEventSettings(), )); - $this->webhookDispatchService->queueOrderWebhook( - eventType: WebhookEventType::ORDER_CANCELLED, - orderId: $order->getId(), + $this->domainEventDispatcherService->dispatch( + new OrderEvent( + type: DomainEventType::ORDER_CANCELLED, + orderId: $order->getId(), + ), ); }); } diff --git a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/ChargeRefundUpdatedHandler.php b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/ChargeRefundUpdatedHandler.php index 5ba5122e..3aea1ba7 100644 --- a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/ChargeRefundUpdatedHandler.php +++ b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/ChargeRefundUpdatedHandler.php @@ -4,7 +4,6 @@ use Brick\Money\Money; use HiEvents\DomainObjects\Enums\PaymentProviders; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Generated\OrderDomainObjectAbstract; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\DomainObjects\Status\OrderRefundStatus; @@ -12,7 +11,9 @@ use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Repository\Interfaces\StripePaymentsRepositoryInterface; use HiEvents\Services\Domain\EventStatistics\EventStatisticsUpdateService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use HiEvents\Values\MoneyValue; use Illuminate\Database\DatabaseManager; use Illuminate\Log\Logger; @@ -28,7 +29,7 @@ public function __construct( private readonly DatabaseManager $databaseManager, private readonly EventStatisticsUpdateService $eventStatisticsUpdateService, private readonly OrderRefundRepositoryInterface $orderRefundRepository, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -82,9 +83,11 @@ public function handleEvent(Refund $refund): void 'refund_id' => $refund->id, ]); - $this->webhookDispatchService->queueOrderWebhook( - eventType: WebhookEventType::ORDER_REFUNDED, - orderId: $order->getId(), + $this->domainEventDispatcherService->dispatch( + new OrderEvent( + type: DomainEventType::ORDER_REFUNDED, + orderId: $order->getId() + ), ); }); } @@ -102,9 +105,9 @@ private function updateEventStatistics(OrderDomainObject $order, MoneyValue $amo private function updateOrderRefundedAmount(int $orderId, float $refundedAmount): void { $this->orderRepository->increment( - $orderId, - OrderDomainObjectAbstract::TOTAL_REFUNDED, - $refundedAmount + id: $orderId, + column: OrderDomainObjectAbstract::TOTAL_REFUNDED, + amount: $refundedAmount ); } diff --git a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php index 9a403f0f..1da220c1 100644 --- a/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php +++ b/backend/app/Services/Domain/Payment/Stripe/EventHandlers/PaymentIntentSucceededHandler.php @@ -8,7 +8,6 @@ use Brick\Money\Exception\UnknownCurrencyException; use Carbon\Carbon; use HiEvents\DomainObjects\Enums\PaymentProviders; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Generated\OrderDomainObjectAbstract; use HiEvents\DomainObjects\Generated\StripePaymentDomainObjectAbstract; use HiEvents\DomainObjects\OrderDomainObject; @@ -26,7 +25,9 @@ use HiEvents\Services\Domain\Order\OrderApplicationFeeService; use HiEvents\Services\Domain\Payment\Stripe\StripeRefundExpiredOrderService; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use Illuminate\Cache\Repository; use Illuminate\Database\DatabaseManager; use Psr\Log\LoggerInterface; @@ -45,7 +46,7 @@ public function __construct( private readonly DatabaseManager $databaseManager, private readonly LoggerInterface $logger, private readonly Repository $cache, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, private readonly OrderApplicationFeeService $orderApplicationFeeService, ) { @@ -92,9 +93,11 @@ public function handleEvent(PaymentIntent $paymentIntent): void OrderStatusChangedEvent::dispatch($updatedOrder); - $this->webhookDispatchService->queueOrderWebhook( - eventType: WebhookEventType::ORDER_CREATED, - orderId: $updatedOrder->getId(), + $this->domainEventDispatcherService->dispatch( + new OrderEvent( + type: DomainEventType::ORDER_CREATED, + orderId: $updatedOrder->getId() + ), ); $this->markPaymentIntentAsHandled($paymentIntent, $updatedOrder); diff --git a/backend/app/Services/Domain/Product/CreateProductService.php b/backend/app/Services/Domain/Product/CreateProductService.php index 6a79f73e..2f517961 100644 --- a/backend/app/Services/Domain/Product/CreateProductService.php +++ b/backend/app/Services/Domain/Product/CreateProductService.php @@ -3,15 +3,16 @@ namespace HiEvents\Services\Domain\Product; use Exception; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\ProductDomainObject; use HiEvents\Helper\DateHelper; use HiEvents\Repository\Interfaces\EventRepositoryInterface; use HiEvents\Repository\Interfaces\ProductRepositoryInterface; use HiEvents\Services\Domain\Tax\DTO\TaxAndProductAssociateParams; use HiEvents\Services\Domain\Tax\TaxAndProductAssociationService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\ProductEvent; use HiEvents\Services\Infrastructure\HtmlPurifier\HtmlPurifierService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; use Illuminate\Database\DatabaseManager; use Illuminate\Support\Collection; use Throwable; @@ -26,7 +27,7 @@ public function __construct( private readonly HtmlPurifierService $purifier, private readonly EventRepositoryInterface $eventRepository, private readonly ProductOrderingService $productOrderingService, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -49,9 +50,11 @@ public function createProduct( $product = $this->createProductPrices($persistedProduct, $product); - $this->webhookDispatchService->queueProductWebhook( - eventType: WebhookEventType::PRODUCT_CREATED, - productId: $product->getId(), + $this->domainEventDispatcherService->dispatch( + new ProductEvent( + type: DomainEventType::PRODUCT_CREATED, + productId: $product->getId(), + ) ); return $product; diff --git a/backend/app/Services/Domain/Product/DeleteProductService.php b/backend/app/Services/Domain/Product/DeleteProductService.php index ffdd285a..091b9411 100644 --- a/backend/app/Services/Domain/Product/DeleteProductService.php +++ b/backend/app/Services/Domain/Product/DeleteProductService.php @@ -2,13 +2,14 @@ namespace HiEvents\Services\Domain\Product; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\Generated\ProductDomainObjectAbstract; use HiEvents\DomainObjects\Generated\ProductPriceDomainObjectAbstract; use HiEvents\Exceptions\CannotDeleteEntityException; use HiEvents\Repository\Interfaces\ProductPriceRepositoryInterface; use HiEvents\Repository\Interfaces\ProductRepositoryInterface; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\ProductEvent; use Illuminate\Database\DatabaseManager; use Psr\Log\LoggerInterface; use Throwable; @@ -20,7 +21,7 @@ public function __construct( private readonly ProductPriceRepositoryInterface $productPriceRepository, private readonly LoggerInterface $logger, private readonly DatabaseManager $databaseManager, - private readonly WebhookDispatchService $webhookDispatchService, + private readonly DomainEventDispatcherService $domainEventDispatcherService, ) { } @@ -52,9 +53,11 @@ public function deleteProduct(int $productId, int $eventId): void ); }); - $this->webhookDispatchService->queueProductWebhook( - eventType: WebhookEventType::PRODUCT_DELETED, - productId: $productId, + $this->domainEventDispatcherService->dispatch( + new ProductEvent( + type: DomainEventType::PRODUCT_DELETED, + productId: $productId, + ) ); $this->logger->info( diff --git a/backend/app/Services/Infrastructure/DomainEvents/DomainEventDispatcherService.php b/backend/app/Services/Infrastructure/DomainEvents/DomainEventDispatcherService.php new file mode 100644 index 00000000..5c833d86 --- /dev/null +++ b/backend/app/Services/Infrastructure/DomainEvents/DomainEventDispatcherService.php @@ -0,0 +1,18 @@ +dispatcher->dispatch($event); + } +} diff --git a/backend/app/DomainObjects/Enums/WebhookEventType.php b/backend/app/Services/Infrastructure/DomainEvents/Enums/DomainEventType.php similarity index 82% rename from backend/app/DomainObjects/Enums/WebhookEventType.php rename to backend/app/Services/Infrastructure/DomainEvents/Enums/DomainEventType.php index 2526d356..dbaf38ae 100644 --- a/backend/app/DomainObjects/Enums/WebhookEventType.php +++ b/backend/app/Services/Infrastructure/DomainEvents/Enums/DomainEventType.php @@ -1,8 +1,10 @@ onQueue($this->config->get('queue.webhook_queue_name')); - } - - public function queueProductWebhook(WebhookEventType $eventType, int $productId): void - { - DispatchProductWebhookJob::dispatch( - productId: $productId, - eventType: $eventType, - )->onQueue($this->config->get('queue.webhook_queue_name')); - } - - public function queueCheckInWebhook(WebhookEventType $eventType, int $attendeeCheckInId): void - { - DispatchCheckInWebhookJob::dispatch( - attendeeCheckInId: $attendeeCheckInId, - eventType: $eventType, - )->onQueue($this->config->get('queue.webhook_queue_name')); - } - - public function queueAttendeeWebhook(WebhookEventType $eventType, int $attendeeId): void - { - DispatchAttendeeWebhookJob::dispatch( - attendeeId: $attendeeId, - eventType: $eventType, - )->onQueue($this->config->get('queue.webhook_queue_name')); - } - - public function dispatchAttendeeWebhook(WebhookEventType $eventType, int $attendeeId): void + public function dispatchAttendeeWebhook(DomainEventType $eventType, int $attendeeId): void { $attendee = $this->attendeeRepository ->loadRelation(new Relationship( @@ -92,7 +54,7 @@ public function dispatchAttendeeWebhook(WebhookEventType $eventType, int $attend ); } - public function dispatchCheckInWebhook(WebhookEventType $eventType, int $attendeeCheckInId): void + public function dispatchCheckInWebhook(DomainEventType $eventType, int $attendeeCheckInId): void { $attendeeCheckIn = $this->attendeeCheckInRepository ->loadRelation(new Relationship( @@ -109,7 +71,7 @@ public function dispatchCheckInWebhook(WebhookEventType $eventType, int $attende ); } - public function dispatchProductWebhook(WebhookEventType $eventType, int $productId): void + public function dispatchProductWebhook(DomainEventType $eventType, int $productId): void { $product = $this->productRepository ->loadRelation(ProductPriceDomainObject::class) @@ -124,7 +86,7 @@ public function dispatchProductWebhook(WebhookEventType $eventType, int $product ); } - public function dispatchOrderWebhook(WebhookEventType $eventType, int $orderId): void + public function dispatchOrderWebhook(DomainEventType $eventType, int $orderId): void { $order = $this->orderRepository ->loadRelation(OrderItemDomainObject::class) @@ -141,21 +103,21 @@ public function dispatchOrderWebhook(WebhookEventType $eventType, int $orderId): ->loadRelation(QuestionAndAnswerViewDomainObject::class) ->findById($orderId); - if ($eventType === WebhookEventType::ORDER_CREATED) { + if ($eventType === DomainEventType::ORDER_CREATED) { /** @var AttendeeDomainObject $attendee */ foreach ($order->getAttendees() as $attendee) { - $this->queueAttendeeWebhook( - eventType: WebhookEventType::ATTENDEE_CREATED, + $this->dispatchAttendeeWebhook( + eventType: DomainEventType::ATTENDEE_CREATED, attendeeId: $attendee->getId(), ); } } - if ($eventType === WebhookEventType::ORDER_CANCELLED) { + if ($eventType === DomainEventType::ORDER_CANCELLED) { /** @var AttendeeDomainObject $attendee */ foreach ($order->getAttendees() as $attendee) { - $this->queueAttendeeWebhook( - eventType: WebhookEventType::ATTENDEE_CANCELLED, + $this->dispatchAttendeeWebhook( + eventType: DomainEventType::ATTENDEE_CANCELLED, attendeeId: $attendee->getId(), ); } @@ -168,7 +130,7 @@ public function dispatchOrderWebhook(WebhookEventType $eventType, int $orderId): ); } - private function dispatchWebhook(WebhookEventType $eventType, JsonResource $payload, int $eventId): void + private function dispatchWebhook(DomainEventType $eventType, JsonResource $payload, int $eventId): void { /** @var Collection $webhooks */ $webhooks = $this->webhookRepository->findWhere([ diff --git a/backend/config/queue.php b/backend/config/queue.php index c4260ae9..52622994 100644 --- a/backend/config/queue.php +++ b/backend/config/queue.php @@ -65,7 +65,7 @@ 'queue' => env('SQS_QUEUE', 'default'), 'suffix' => env('SQS_SUFFIX'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), - 'after_commit' => false, + 'after_commit' => true, ], 'redis' => [ diff --git a/backend/tests/Unit/Services/Application/Handlers/Order/CompleteOrderHandlerTest.php b/backend/tests/Unit/Services/Application/Handlers/Order/CompleteOrderHandlerTest.php index d65f4225..9d003d89 100644 --- a/backend/tests/Unit/Services/Application/Handlers/Order/CompleteOrderHandlerTest.php +++ b/backend/tests/Unit/Services/Application/Handlers/Order/CompleteOrderHandlerTest.php @@ -5,7 +5,6 @@ use Carbon\Carbon; use Exception; use HiEvents\DomainObjects\AttendeeDomainObject; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\DomainObjects\OrderItemDomainObject; use HiEvents\DomainObjects\ProductPriceDomainObject; @@ -20,7 +19,10 @@ use HiEvents\Services\Application\Handlers\Order\DTO\CompleteOrderOrderDTO; use HiEvents\Services\Application\Handlers\Order\DTO\CompleteOrderProductDataDTO; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; -use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\BaseDomainEvent; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use Illuminate\Database\Connection; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Bus; @@ -40,7 +42,7 @@ class CompleteOrderHandlerTest extends TestCase private ProductQuantityUpdateService|MockInterface $productQuantityUpdateService; private ProductPriceRepositoryInterface|MockInterface $productPriceRepository; private CompleteOrderHandler $completeOrderHandler; - private WebhookDispatchService $webhookDispatchService; + private DomainEventDispatcherService $domainEventDispatcherService; protected function setUp(): void { @@ -55,7 +57,7 @@ protected function setUp(): void $this->questionAnswersRepository = Mockery::mock(QuestionAnswerRepositoryInterface::class); $this->productQuantityUpdateService = Mockery::mock(ProductQuantityUpdateService::class); $this->productPriceRepository = Mockery::mock(ProductPriceRepositoryInterface::class); - $this->webhookDispatchService = Mockery::mock(WebhookDispatchService::class); + $this->domainEventDispatcherService = Mockery::mock(DomainEventDispatcherService::class); $this->completeOrderHandler = new CompleteOrderHandler( $this->orderRepository, @@ -63,7 +65,7 @@ protected function setUp(): void $this->questionAnswersRepository, $this->productQuantityUpdateService, $this->productPriceRepository, - $this->webhookDispatchService + $this->domainEventDispatcherService ); } @@ -164,8 +166,11 @@ public function testHandleUpdatesProductQuantitiesForFreeOrder(): void $this->productQuantityUpdateService->shouldReceive('updateQuantitiesFromOrder')->once(); - $this->webhookDispatchService->shouldReceive('queueOrderWebhook') - ->with(WebhookEventType::ORDER_CREATED, $updatedOrder->getId()) + $this->domainEventDispatcherService->shouldReceive('dispatch') + ->withArgs(function (OrderEvent $event) use ($order) { + return $event->type === DomainEventType::ORDER_CREATED + && $event->orderId === $order->getId(); + }) ->once(); $order = $this->completeOrderHandler->handle($orderShortId, $orderData); @@ -252,10 +257,10 @@ private function createMockCompleteOrderDTO(): CompleteOrderDTO ); $attendeeDTO = new CompleteOrderProductDataDTO( + product_price_id: 1, first_name: 'John', last_name: 'Doe', - email: 'john@example.com', - product_price_id: 1 + email: 'john@example.com' ); return new CompleteOrderDTO( diff --git a/backend/tests/Unit/Services/Domain/Order/OrderCancelServiceTest.php b/backend/tests/Unit/Services/Domain/Order/OrderCancelServiceTest.php index 4f1f93bb..d40e0313 100644 --- a/backend/tests/Unit/Services/Domain/Order/OrderCancelServiceTest.php +++ b/backend/tests/Unit/Services/Domain/Order/OrderCancelServiceTest.php @@ -3,7 +3,6 @@ namespace Tests\Unit\Services\Domain\Order; use HiEvents\DomainObjects\AttendeeDomainObject; -use HiEvents\DomainObjects\Enums\WebhookEventType; use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; use HiEvents\DomainObjects\OrderDomainObject; @@ -14,6 +13,9 @@ use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Domain\Order\OrderCancelService; use HiEvents\Services\Domain\Product\ProductQuantityUpdateService; +use HiEvents\Services\Infrastructure\DomainEvents\DomainEventDispatcherService; +use HiEvents\Services\Infrastructure\DomainEvents\Enums\DomainEventType; +use HiEvents\Services\Infrastructure\DomainEvents\Events\OrderEvent; use HiEvents\Services\Infrastructure\Webhook\WebhookDispatchService; use Illuminate\Contracts\Mail\Mailer; use Illuminate\Database\DatabaseManager; @@ -31,7 +33,7 @@ class OrderCancelServiceTest extends TestCase private DatabaseManager $databaseManager; private ProductQuantityUpdateService $productQuantityService; private OrderCancelService $service; - private WebhookDispatchService $webhookDispatchService; + private DomainEventDispatcherService $domainEventDispatcherService; protected function setUp(): void { @@ -43,7 +45,7 @@ protected function setUp(): void $this->orderRepository = m::mock(OrderRepositoryInterface::class); $this->databaseManager = m::mock(DatabaseManager::class); $this->productQuantityService = m::mock(ProductQuantityUpdateService::class); - $this->webhookDispatchService = m::mock(WebhookDispatchService::class); + $this->domainEventDispatcherService = m::mock(DomainEventDispatcherService::class); $this->service = new OrderCancelService( mailer: $this->mailer, @@ -52,7 +54,7 @@ protected function setUp(): void orderRepository: $this->orderRepository, databaseManager: $this->databaseManager, productQuantityService: $this->productQuantityService, - webhookDispatchService: $this->webhookDispatchService, + domainEventDispatcherService: $this->domainEventDispatcherService, ); } @@ -105,8 +107,11 @@ public function testCancelOrder(): void return $mail instanceof OrderCancelled; }); - $this->webhookDispatchService->shouldReceive('queueOrderWebhook') - ->with(WebhookEventType::ORDER_CANCELLED, 1) + $this->domainEventDispatcherService->shouldReceive('dispatch') + ->withArgs(function (OrderEvent $event) use ($order) { + return $event->type === DomainEventType::ORDER_CANCELLED + && $event->orderId === $order->getId(); + }) ->once(); $this->databaseManager->shouldReceive('transaction')->once()->andReturnUsing(function ($callback) { diff --git a/frontend/src/components/modals/WebhookLogsModal/index.tsx b/frontend/src/components/modals/WebhookLogsModal/index.tsx index d43b9c6b..979a78f1 100644 --- a/frontend/src/components/modals/WebhookLogsModal/index.tsx +++ b/frontend/src/components/modals/WebhookLogsModal/index.tsx @@ -183,11 +183,11 @@ export const WebhookLogsModal = ({onClose, webhookId}: WebhookLogsModalProps) => )} {logs && logs.length > 0 && ( - + <> {logs.map((log) => ( ))} - + )} ); From ad50f8ce13c6a38b681403c8875b87276bb8f25b Mon Sep 17 00:00:00 2001 From: Dave Earley Date: Tue, 25 Mar 2025 06:58:12 -0700 Subject: [PATCH 3/6] Add outgoing message tracking (#459) --- .../OutgoingMessageDomainObjectAbstract.php | 146 ++++++++++++++++++ .../OutgoingMessageDomainObject.php | 7 + .../Status/OutgoingMessageStatus.php | 9 ++ backend/app/Jobs/Event/SendEventEmailJob.php | 72 +++++++++ backend/app/Models/Message.php | 7 +- backend/app/Models/OutgoingMessage.php | 10 ++ backend/app/Providers/AppServiceProvider.php | 78 +++++++--- .../Providers/RepositoryServiceProvider.php | 3 + .../Eloquent/OutgoingMessageRepository.php | 20 +++ .../OutgoingMessageRepositoryInterface.php | 14 ++ .../Mail/SendEventEmailMessagesService.php | 54 ++++--- backend/config/mail.php | 1 + ..._24_052900_add_outgoing_messages_table.php | 37 +++++ 13 files changed, 417 insertions(+), 41 deletions(-) create mode 100644 backend/app/DomainObjects/Generated/OutgoingMessageDomainObjectAbstract.php create mode 100644 backend/app/DomainObjects/OutgoingMessageDomainObject.php create mode 100644 backend/app/DomainObjects/Status/OutgoingMessageStatus.php create mode 100644 backend/app/Jobs/Event/SendEventEmailJob.php create mode 100644 backend/app/Models/OutgoingMessage.php create mode 100644 backend/app/Repository/Eloquent/OutgoingMessageRepository.php create mode 100644 backend/app/Repository/Interfaces/OutgoingMessageRepositoryInterface.php create mode 100644 backend/database/migrations/2025_03_24_052900_add_outgoing_messages_table.php diff --git a/backend/app/DomainObjects/Generated/OutgoingMessageDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/OutgoingMessageDomainObjectAbstract.php new file mode 100644 index 00000000..8375d808 --- /dev/null +++ b/backend/app/DomainObjects/Generated/OutgoingMessageDomainObjectAbstract.php @@ -0,0 +1,146 @@ + $this->id ?? null, + 'event_id' => $this->event_id ?? null, + 'message_id' => $this->message_id ?? null, + 'subject' => $this->subject ?? null, + 'recipient' => $this->recipient ?? null, + 'status' => $this->status ?? null, + 'created_at' => $this->created_at ?? null, + 'updated_at' => $this->updated_at ?? null, + 'deleted_at' => $this->deleted_at ?? null, + ]; + } + + public function setId(int $id): self + { + $this->id = $id; + return $this; + } + + public function getId(): int + { + return $this->id; + } + + public function setEventId(int $event_id): self + { + $this->event_id = $event_id; + return $this; + } + + public function getEventId(): int + { + return $this->event_id; + } + + public function setMessageId(int $message_id): self + { + $this->message_id = $message_id; + return $this; + } + + public function getMessageId(): int + { + return $this->message_id; + } + + public function setSubject(string $subject): self + { + $this->subject = $subject; + return $this; + } + + public function getSubject(): string + { + return $this->subject; + } + + public function setRecipient(string $recipient): self + { + $this->recipient = $recipient; + return $this; + } + + public function getRecipient(): string + { + return $this->recipient; + } + + public function setStatus(string $status): self + { + $this->status = $status; + return $this; + } + + public function getStatus(): string + { + return $this->status; + } + + public function setCreatedAt(?string $created_at): self + { + $this->created_at = $created_at; + return $this; + } + + public function getCreatedAt(): ?string + { + return $this->created_at; + } + + public function setUpdatedAt(?string $updated_at): self + { + $this->updated_at = $updated_at; + return $this; + } + + public function getUpdatedAt(): ?string + { + return $this->updated_at; + } + + public function setDeletedAt(?string $deleted_at): self + { + $this->deleted_at = $deleted_at; + return $this; + } + + public function getDeletedAt(): ?string + { + return $this->deleted_at; + } +} diff --git a/backend/app/DomainObjects/OutgoingMessageDomainObject.php b/backend/app/DomainObjects/OutgoingMessageDomainObject.php new file mode 100644 index 00000000..25d613fd --- /dev/null +++ b/backend/app/DomainObjects/OutgoingMessageDomainObject.php @@ -0,0 +1,7 @@ +to($this->email, $this->toName) + ->send($this->eventMessage); + } catch (Throwable $exception) { + $outgoingMessageRepository->create([ + OutgoingMessageDomainObjectAbstract::MESSAGE_ID => $this->messageData->id, + OutgoingMessageDomainObjectAbstract::EVENT_ID => $this->messageData->event_id, + OutgoingMessageDomainObjectAbstract::STATUS => OutgoingMessageStatus::FAILED->name, + OutgoingMessageDomainObjectAbstract::RECIPIENT => $this->email, + OutgoingMessageDomainObjectAbstract::SUBJECT => $this->messageData->subject, + ]); + + throw $exception; + } + + $outgoingMessageRepository->create([ + OutgoingMessageDomainObjectAbstract::MESSAGE_ID => $this->messageData->id, + OutgoingMessageDomainObjectAbstract::EVENT_ID => $this->messageData->event_id, + OutgoingMessageDomainObjectAbstract::STATUS => OutgoingMessageStatus::SENT->name, + OutgoingMessageDomainObjectAbstract::RECIPIENT => $this->email, + OutgoingMessageDomainObjectAbstract::SUBJECT => $this->messageData->subject, + ]); + } +} diff --git a/backend/app/Models/Message.php b/backend/app/Models/Message.php index c9e1e058..3680e2a1 100644 --- a/backend/app/Models/Message.php +++ b/backend/app/Models/Message.php @@ -2,6 +2,7 @@ namespace HiEvents\Models; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\SoftDeletes; @@ -14,6 +15,11 @@ public function sent_by_user(): HasOne return $this->hasOne(User::class, 'id', 'sent_by_user_id'); } + public function outgoing_messages(): HasMany + { + return $this->hasMany(OutgoingMessage::class); + } + protected function getCastMap(): array { return [ @@ -22,5 +28,4 @@ protected function getCastMap(): array 'send_data' => 'array', ]; } - } diff --git a/backend/app/Models/OutgoingMessage.php b/backend/app/Models/OutgoingMessage.php new file mode 100644 index 00000000..c857f7e9 --- /dev/null +++ b/backend/app/Models/OutgoingMessage.php @@ -0,0 +1,10 @@ +bindDoctrineConnection(); @@ -30,28 +35,25 @@ public function register(): void */ public function boot(): void { - if ($this->app->environment('local')) { - URL::forceScheme('https'); - URL::forceRootUrl(config('app.url')); - } + $this->handleHttpsEnforcing(); - if (env('APP_DEBUG') === true && env('APP_LOG_QUERIES') === true && !app()->isProduction()) { - DB::listen( - static function ($query) { - File::append( - storage_path('/logs/query.log'), - $query->sql . ' [' . implode(', ', $query->bindings) . ']' . PHP_EOL - ); - } - ); - } + $this->handleQueryLogging(); - Model::preventLazyLoading(!app()->isProduction()); + $this->disableLazyLoading(); - Relation::enforceMorphMap([ - EventDomainObject::class => Event::class, - OrganizerDomainObject::class => Organizer::class, - ]); + $this->registerMorphMaps(); + + $this->registerJobRateLimiters(); + } + + private function registerJobRateLimiters(): void + { + RateLimiter::for( + name: self::MAIL_RATE_LIMIT_PER_SECOND, + callback: static fn(ShouldQueue $job) => Limit::perMinute( + maxAttempts: config('mail.rate_limit_per_second') + ) + ); } private function bindDoctrineConnection(): void @@ -90,4 +92,42 @@ private function bindStripeClient(): void fn() => new StripeClient(config('services.stripe.secret_key')) ); } + + /** + * @return void + */ + private function handleQueryLogging(): void + { + if (env('APP_DEBUG') === true && env('APP_LOG_QUERIES') === true && !app()->isProduction()) { + DB::listen( + static function ($query) { + File::append( + storage_path('/logs/query.log'), + $query->sql . ' [' . implode(', ', $query->bindings) . ']' . PHP_EOL + ); + } + ); + } + } + + private function handleHttpsEnforcing(): void + { + if ($this->app->environment('local')) { + URL::forceScheme('https'); + URL::forceRootUrl(config('app.url')); + } + } + + private function registerMorphMaps(): void + { + Relation::enforceMorphMap([ + EventDomainObject::class => Event::class, + OrganizerDomainObject::class => Organizer::class, + ]); + } + + private function disableLazyLoading(): void + { + Model::preventLazyLoading(!app()->isProduction()); + } } diff --git a/backend/app/Providers/RepositoryServiceProvider.php b/backend/app/Providers/RepositoryServiceProvider.php index 18cb630d..573d64c6 100644 --- a/backend/app/Providers/RepositoryServiceProvider.php +++ b/backend/app/Providers/RepositoryServiceProvider.php @@ -23,6 +23,7 @@ use HiEvents\Repository\Eloquent\OrderRefundRepository; use HiEvents\Repository\Eloquent\OrderRepository; use HiEvents\Repository\Eloquent\OrganizerRepository; +use HiEvents\Repository\Eloquent\OutgoingMessageRepository; use HiEvents\Repository\Eloquent\PasswordResetRepository; use HiEvents\Repository\Eloquent\PasswordResetTokenRepository; use HiEvents\Repository\Eloquent\ProductCategoryRepository; @@ -57,6 +58,7 @@ use HiEvents\Repository\Interfaces\OrderRefundRepositoryInterface; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface; +use HiEvents\Repository\Interfaces\OutgoingMessageRepositoryInterface; use HiEvents\Repository\Interfaces\PasswordResetRepositoryInterface; use HiEvents\Repository\Interfaces\PasswordResetTokenRepositoryInterface; use HiEvents\Repository\Interfaces\ProductCategoryRepositoryInterface; @@ -114,6 +116,7 @@ class RepositoryServiceProvider extends ServiceProvider OrderApplicationFeeRepositoryInterface::class => OrderApplicationFeeRepository::class, AccountConfigurationRepositoryInterface::class => AccountConfigurationRepository::class, QuestionAndAnswerViewRepositoryInterface::class => QuestionAndAnswerViewRepository::class, + OutgoingMessageRepositoryInterface::class => OutgoingMessageRepository::class, ]; public function register(): void diff --git a/backend/app/Repository/Eloquent/OutgoingMessageRepository.php b/backend/app/Repository/Eloquent/OutgoingMessageRepository.php new file mode 100644 index 00000000..b977237a --- /dev/null +++ b/backend/app/Repository/Eloquent/OutgoingMessageRepository.php @@ -0,0 +1,20 @@ + + */ +interface OutgoingMessageRepositoryInterface extends RepositoryInterface +{ + +} diff --git a/backend/app/Services/Domain/Mail/SendEventEmailMessagesService.php b/backend/app/Services/Domain/Mail/SendEventEmailMessagesService.php index 03e997aa..f1937a52 100644 --- a/backend/app/Services/Domain/Mail/SendEventEmailMessagesService.php +++ b/backend/app/Services/Domain/Mail/SendEventEmailMessagesService.php @@ -11,6 +11,7 @@ use HiEvents\DomainObjects\Status\AttendeeStatus; use HiEvents\DomainObjects\Status\MessageStatus; use HiEvents\Exceptions\UnableToSendMessageException; +use HiEvents\Jobs\Event\SendEventEmailJob; use HiEvents\Mail\Event\EventMessage; use HiEvents\Repository\Eloquent\Value\Relationship; use HiEvents\Repository\Interfaces\AttendeeRepositoryInterface; @@ -19,20 +20,22 @@ use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Repository\Interfaces\UserRepositoryInterface; use HiEvents\Services\Application\Handlers\Message\DTO\SendMessageDTO; -use Illuminate\Mail\Mailer; +use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Support\Collection; use Symfony\Component\HttpKernel\Log\Logger; class SendEventEmailMessagesService { + private array $sentEmails = []; + public function __construct( private readonly OrderRepositoryInterface $orderRepository, private readonly AttendeeRepositoryInterface $attendeeRepository, private readonly EventRepositoryInterface $eventRepository, private readonly MessageRepositoryInterface $messageRepository, private readonly UserRepositoryInterface $userRepository, - private readonly Mailer $mailer, - private readonly Logger $logger + private readonly Logger $logger, + private readonly Dispatcher $dispatcher, ) { } @@ -202,24 +205,6 @@ private function sendEmailToMessageSender(SendMessageDTO $messageData, EventDoma ); } - private function sendMessage( - string $emailAddress, - string $fullName, - SendMessageDTO $messageData, - EventDomainObject $event, - ): void - { - $this->mailer->to( - $emailAddress, - $fullName - ) - ->queue(new EventMessage( - event: $event, - eventSettings: $event->getEventSettings(), - messageData: $messageData - )); - } - private function sendProductMessages(SendMessageDTO $messageData, EventDomainObject $event): void { $orders = $this->orderRepository->findOrdersAssociatedWithProducts( @@ -243,4 +228,31 @@ private function sendProductMessages(SendMessageDTO $messageData, EventDomainObj ); }); } + + private function sendMessage( + string $emailAddress, + string $fullName, + SendMessageDTO $messageData, + EventDomainObject $event, + ): void + { + if (in_array($emailAddress, $this->sentEmails, true)) { + return; + } + + $this->dispatcher->dispatch( + new SendEventEmailJob( + email: $emailAddress, + toName: $fullName, + eventMessage: new EventMessage( + event: $event, + eventSettings: $event->getEventSettings(), + messageData: $messageData + ), + messageData: $messageData, + ) + ); + + $this->sentEmails[] = $emailAddress; + } } diff --git a/backend/config/mail.php b/backend/config/mail.php index 3b6cca04..811c09f3 100644 --- a/backend/config/mail.php +++ b/backend/config/mail.php @@ -1,6 +1,7 @@ env('MAIL_RATE_LIMIT_PER_SECOND', 14), /* |-------------------------------------------------------------------------- diff --git a/backend/database/migrations/2025_03_24_052900_add_outgoing_messages_table.php b/backend/database/migrations/2025_03_24_052900_add_outgoing_messages_table.php new file mode 100644 index 00000000..0a3afe56 --- /dev/null +++ b/backend/database/migrations/2025_03_24_052900_add_outgoing_messages_table.php @@ -0,0 +1,37 @@ +id(); + + $table->foreignId('event_id')->constrained()->cascadeOnDelete(); + $table->foreignId('message_id')->constrained()->cascadeOnDelete(); + $table->string('subject'); + $table->string('recipient'); + $table->string('status'); + + $table->index('event_id'); + $table->index('message_id'); + + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('outgoing_messages'); + } +}; From 40c0b868635d3b4b2f5a0234ffedec55860d9bec Mon Sep 17 00:00:00 2001 From: Dave Earley Date: Tue, 25 Mar 2025 14:32:23 +0000 Subject: [PATCH 4/6] Update README --- README.md | 176 +++++++++++++++++++++--------------------------------- 1 file changed, 68 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 2ea6352a..1ef4daeb 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,39 @@
- 🌟 A star would be much appreciated! 🌟 -
+
-

- Hi.Events Logo -

+πŸ’– **Found Hi.Events helpful?** +⭐ Please consider giving us a star to support the project! ⭐ -

Hi.Events

+
-

-Demo Event 🌟 β€’ Website 🌎 β€’ Documentation πŸ“„ β€’ Installation βš™οΈ +

+ Hi.Events Logo

-

- Open-source event management and ticketing platform. -

- -
+

Hi.Events

+

Open-source event management and ticketing platform to sell tickets online for events of all sizes

[![Share on AddToAny](https://img.shields.io/badge/Share%20Hi.Events-blue)](https://www.addtoany.com/share?linkurl=https://github.com/HiEventsDev/hi.events) [![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/HiEventsTickets)](https://x.com/HiEventsTickets) -
[![Hi.Events docs](https://img.shields.io/badge/docs-hi.events-blue)](https://hi.events/docs) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://github.com/HiEventsDev/Hi.Events/blob/develop/LICENCE) [![GitHub Release](https://img.shields.io/github/v/release/HiEventsDev/hi.events?include_prereleases)](https://github.com/HiEventsDev/hi.events/releases) [![Run Unit Tests](https://github.com/HiEventsDev/hi.events/actions/workflows/unit-tests.yml/badge.svg?event=push)](https://github.com/HiEventsDev/hi.events/actions/workflows/unit-tests.yml) [![Docker Pulls](https://img.shields.io/docker/pulls/daveearley/hi.events-all-in-one)](https://hub.docker.com/r/daveearley/hi.events-all-in-one) -
+

+Try Cloud ☁️ β€’ +Demo Event 🌟 β€’ +Website 🌎 β€’ +Documentation πŸ“„ β€’ +Installation βš™οΈ +

-
- HiEventsDev%2Fhi.events | Trendshift +HiEventsDev%2Fhi.events | Trendshift

-

+

Deutsch | PortuguΓͺs | FranΓ§ais | @@ -44,85 +43,58 @@


-## Table of Contents - -- [Introduction](#-introduction) -- [Features](#-features) -- [Getting Started](#-getting-started) -- [Change Log](#-change-log) -- [Contributing](#-contributing) -- [FAQ](#-faq) - ## πŸ“š Introduction -Hi.Events is a feature-rich, self-hosted event management and ticketing platform. From conferences to club nights, -Hi.Events is designed to help you create, manage, and sell tickets for events of all sizes. +Hi.Events is a feature-rich, self-hosted event management and ticketing platform that helps you sell tickets online for all types of events. From conferences and workshops to club nights and concerts, Hi.Events provides everything you need to create, manage, and monetize your events with ease. Hi.Events self-hosted ticket selling dashboard +
Generated using Screenshot Rocks +
-## 🌟 Features - -Hi.Events is packed with features to streamline your event management and ticketing: - -### 🎟 Ticketing & Product Sales -- **Multiple Ticket Types:** Free, Paid, Donation, and Tiered tickets. -- **Capacity Management:** Set event-wide or ticket-specific limits. -- **Capacity Assignments:** Manage shared capacity across multiple ticket types. -- **Promo Codes:** Discount codes for pre-sale access and special offers. -- **Product Sales:** Sell event-related products (e.g., t-shirts, add-ons). -- **Taxes & Fees:** Apply custom taxes and fees per product or order. - -### πŸ† Event Management & Customization -- **Event Dashboard:** Real-time revenue, ticket sales, and attendee analytics. -- **Homepage Designer:** Customize event pages with a live preview editor. -- **Embeddable Ticket Widget:** Add a seamless ticketing experience to your website. -- **SEO Tools:** Customize event metadata for better search visibility. -- **Product Categories:** Organize products and tickets with category management. -- **Offline Event Support:** Provide instructions for physical events. - -### πŸ“§ Attendee & Order Management -- **Custom Checkout Forms:** Collect attendee details with tailored questions. -- **Attendee Management:** Search, edit, cancel, and message attendees. -- **Order Management:** Refund, cancel, and resend order details easily. -- **Bulk Messaging:** Email or message specific ticket holders. -- **Data Exports:** Export attendees and orders to CSV/XLSX. - -### πŸ“± Mobile-Friendly & Check-In Tools -- **QR Code Check-In:** Web-based and mobile-friendly check-in tool. -- **Check-In Lists:** Generate and share access-controlled check-in lists. -- **Multi-User Access:** Role-based access control for event staff. - -### πŸ”§ Integrations & Automation -- **Webhooks Support:** Automate tasks with Zapier, IFTTT, Make, or CRM integrations. -- **Stripe Connect Integration:** Organizers get instant payouts. - -### πŸ“Š Advanced Features -- **Multi-Language Support:** English, Deutsch, EspaΓ±ol, PortuguΓͺs, FranΓ§ais, δΈ­ζ–‡ (ZhōngwΓ©n), and more. -- **Partial & Full Refunds:** Manage refunds with detailed order tracking. -- **Role-Based Access Control:** Multiple user roles with permission management. -- **REST API:** Full API access for custom integrations. -- **Invoicing System:** Generate and send invoices with tax details, payment terms, and due dates. -- **Offline Payment Support:** Enable bank transfers, cash payments, or custom payment methods. -- **Event Archive:** Archive past events to keep the dashboard organized. -- **Advanced Ticket Locking:** Lock tickets behind promo codes or access restrictions. -- **Advanced Reporting:** Daily sales, tax breakdowns, product sales, and promo code usage reports. - -## πŸš€ Getting Started - -For detailed installation instructions, please refer to our [documentation](https://hi.events/docs/getting-started). For -a quick start, follow these steps: +## ⚑ Quick Deploy -### One-Click Deployments +Get started in minutes with our one-click deployment options: [![Deploy on DigitalOcean](https://www.deploytodo.com/do-btn-blue.svg)](https://github.com/HiEventsDev/hi.events-digitalocean) - [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://github.com/HiEventsDev/hi.events-render.com) - [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/8CGKmu?referralCode=KvSr11) - [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/8DIRY6) +## 🌟 Key Features + +Hi.Events offers comprehensive tools to streamline your event management: + +### 🎟 Ticketing & Sales +- **Multiple Ticket Types:** Create free, paid, donation-based, and tiered tickets +- **Capacity Management:** Set limits per event or ticket type +- **Promo Codes & Discounts:** Drive early sales with special offers +- **Product Upsells:** Sell merchandise and add-ons alongside tickets +- **Custom Pricing:** Apply taxes and fees per product or entire order + +### πŸ† Event Management +- **Real-time Dashboard:** Track sales, revenue, and attendee metrics +- **Visual Page Editor:** Design beautiful event pages with live preview +- **Website Integration:** Embed ticketing widgets on your existing site +- **SEO Optimization:** Customize metadata for better search visibility +- **Offline Event Support:** Provide location details and instructions + +### πŸ“± Attendee Experience +- **Custom Registration Forms:** Collect exactly the information you need +- **QR Code Check-In:** Fast, mobile-friendly entry verification +- **Multi-language Support:** Reach global audiences with localized interfaces +- **Bulk Communication:** Send targeted messages to specific ticket holders +- **Refund Management:** Process full or partial refunds when needed + +### πŸ”§ For Organizers +- **Team Collaboration:** Role-based access for staff members +- **Webhook Integration:** Connect with Zapier, IFTTT, Make, or your CRM +- **Stripe Connect:** Receive instant payouts for ticket sales +- **Comprehensive API:** Build custom integrations with full API access +- **Advanced Reporting:** Generate sales, tax, and usage reports + +## πŸš€ Getting Started + ### 🐳 Quick Start with Docker > [!IMPORTANT] @@ -148,7 +120,7 @@ a quick start, follow these steps: echo base64:$(openssl rand -base64 32) # For APP_KEY openssl rand -base64 32 # For JWT_SECRET ``` - + **Windows:** Check the instructions in *./docker/all-in-one/README.md* for generating the keys on Windows. @@ -159,48 +131,36 @@ a quick start, follow these steps: docker compose up -d ``` 5. **Create an account:** - ```bash - Open your browser and navigate to http://localhost:8123/auth/register. + ``` + Open your browser and navigate to http://localhost:8123/auth/register ``` -ℹ️ Please refer to the [getting started guide](https://hi.events/docs/getting-started) for other installation methods, and -for setting up a production or local development environment. +ℹ️ For detailed setup instructions including production deployment, please refer to our [getting started guide](https://hi.events/docs/getting-started). ## πŸ’œ Sponsors - -Stinking Badges - -### Making a Donation +### Support the Project -If you find Hi.Events useful, it would be massively appreciated if you made a small donation to help support the project. +If you find Hi.Events valuable for your organization, please consider supporting ongoing development: -We'll use your donation to fund ongoing development and maintenance of Hi.Events. +Buy Me A Coffee -Buy Me A Coffee -
-or - Sponsor on GitHub -or - Sponsor on Open Collective +Or support us on: GitHub Sponsors | Open Collective ## πŸ“ Change Log -Stay updated with our ongoing improvements and feature additions at -our [GitHub releases page](https://github.com/HiEventsDev/hi.events/releases). +Stay updated with our latest features and improvements on our [GitHub releases page](https://github.com/HiEventsDev/hi.events/releases). ## 🀝 Contributing -We welcome contributions, suggestions, and bug reports! Please see our [contributing guidelines](CONTRIBUTING.md) for more -information. +We welcome contributions from the community! Please see our [contributing guidelines](CONTRIBUTING.md) for details on how to get involved. ## ❓ FAQ -Have questions? Our [Docs](https://hi.events/docs) have answers. If you can't find what you're looking for, feel free to -reach out to us at [hello@hi.events](mailto:hello@hi.events). +Have questions? Our [documentation](https://hi.events/docs?utm_source=gh-readme&utm_content=faq-docs-link) has answers. For additional support, contact us at [hello@hi.events](mailto:hello@hi.events). ## πŸ“œ License -Hi.Events is licensed under the terms of the [AGPL-3.0](https://github.com/HiEventsDev/hi.events/blob/main/LICENCE) license. +Hi.Events is licensed under the [AGPL-3.0](https://github.com/HiEventsDev/hi.events/blob/main/LICENCE) license. For more licensing information, including commercial licencing options, please visit our licensing page [here](https://hi.events/licensing). From 1ff5527f0640511a4ca1bf8fad616e09c5e87d05 Mon Sep 17 00:00:00 2001 From: Dave Earley Date: Tue, 25 Mar 2025 07:33:30 -0700 Subject: [PATCH 5/6] Update README (#460) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ef4daeb..aadbea20 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/daveearley/hi.events-all-in-one)](https://hub.docker.com/r/daveearley/hi.events-all-in-one)

-Try Cloud ☁️ β€’ +Try Cloud ☁️ β€’ Demo Event 🌟 β€’ Website 🌎 β€’ Documentation πŸ“„ β€’ From 3ff8eeef043fde0e925cdbd92edd13acf9f16204 Mon Sep 17 00:00:00 2001 From: Dave Earley Date: Tue, 25 Mar 2025 20:44:02 -0700 Subject: [PATCH 6/6] Fix session ID not being passed --- .../components/routes/product-widget/SelectProducts/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/routes/product-widget/SelectProducts/index.tsx b/frontend/src/components/routes/product-widget/SelectProducts/index.tsx index 44d3c326..fee491fd 100644 --- a/frontend/src/components/routes/product-widget/SelectProducts/index.tsx +++ b/frontend/src/components/routes/product-widget/SelectProducts/index.tsx @@ -283,7 +283,7 @@ const SelectProducts = (props: SelectProductsProps) => { If a new tab did not open, please {' '} - {t`click here`}.