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
4 changes: 2 additions & 2 deletions backend/app/Console/Commands/BootstrapDevDataCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public function handle(
type: EventType::RECURRING,
));

$occurrences = $generateOccurrencesHandler->handle(new GenerateOccurrencesDTO(
$generateOccurrencesHandler->handle(new GenerateOccurrencesDTO(
event_id: $recurringEvent->getId(),
recurrence_rule: [
'range' => ['type' => 'count', 'count' => 4, 'start' => now()->addDays(7)->toDateString()],
Expand Down Expand Up @@ -161,7 +161,7 @@ public function handle(
['free_product_id / price_id', $freeProduct['product_id'].' / '.$freeProduct['price_id']],
['paid_product_id / price_id (waitlist on)', $paidProduct['product_id'].' / '.$paidProduct['price_id']],
['recurring_event_id (LIVE)', $recurringEvent->getId()],
['recurring_occurrence_ids', $occurrences->map(fn ($o) => $o->getId())->implode(', ')],
['recurring_occurrence_ids', DB::table('event_occurrences')->where('event_id', $recurringEvent->getId())->pluck('id')->implode(', ')],
['recurring_product_id / price_id', $recurringProduct['product_id'].' / '.$recurringProduct['price_id']],
['promo_code', $promoCode->getCode()],
['affiliate_code', $affiliate->getCode()],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@
use HiEvents\Exceptions\InvalidRecurrenceRuleException;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Http\Request\EventOccurrence\GenerateOccurrencesRequest;
use HiEvents\Resources\EventOccurrence\EventOccurrenceResource;
use HiEvents\Http\ResponseCodes;
use HiEvents\Services\Application\Handlers\EventOccurrence\DTO\GenerateOccurrencesDTO;
use HiEvents\Services\Application\Handlers\EventOccurrence\GenerateOccurrencesFromRuleHandler;
use HiEvents\Services\Application\Handlers\EventOccurrence\StartOccurrenceGenerationHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Validation\ValidationException;

class GenerateOccurrencesAction extends BaseAction
{
public function __construct(
private readonly GenerateOccurrencesFromRuleHandler $handler,
private readonly StartOccurrenceGenerationHandler $handler,
) {}

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

try {
$occurrences = $this->handler->handle(
$jobStatus = $this->handler->handle(
new GenerateOccurrencesDTO(
event_id: $eventId,
recurrence_rule: $request->validated('recurrence_rule'),
Expand All @@ -35,9 +35,10 @@ public function __invoke(int $eventId, GenerateOccurrencesRequest $request): Jso
]);
}

return $this->resourceResponse(
resource: EventOccurrenceResource::class,
data: $occurrences,
);
return $this->jsonResponse([
'message' => $jobStatus->message,
'status' => $jobStatus->status->name,
'job_uuid' => $jobStatus->jobUuid,
], ResponseCodes::HTTP_ACCEPTED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace HiEvents\Http\Actions\EventOccurrences;

use HiEvents\DomainObjects\EventDomainObject;
use HiEvents\Http\Actions\BaseAction;
use HiEvents\Jobs\Occurrence\GenerateOccurrencesJob;
use HiEvents\Services\Infrastructure\Jobs\JobPollingService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class GetOccurrenceGenerationStatusAction extends BaseAction
{
public function __construct(
private readonly JobPollingService $jobPollingService,
) {}

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

$jobStatus = $this->jobPollingService->checkJobStatus(
jobUuid: (string) $request->query('job_uuid'),
expectedName: GenerateOccurrencesJob::batchName($eventId),
);

return $this->jsonResponse([
'message' => $jobStatus->message,
'status' => $jobStatus->status->name,
'job_uuid' => $jobStatus->jobUuid,
]);
}
}
71 changes: 71 additions & 0 deletions backend/app/Jobs/Occurrence/GenerateOccurrencesJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace HiEvents\Jobs\Occurrence;

use HiEvents\Exceptions\InvalidRecurrenceRuleException;
use HiEvents\Services\Application\Handlers\EventOccurrence\DTO\GenerateOccurrencesDTO;
use HiEvents\Services\Application\Handlers\EventOccurrence\GenerateOccurrencesFromRuleHandler;
use Illuminate\Bus\Batchable;
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 GenerateOccurrencesJob implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public int $tries = 3;

public int $backoff = 30;

public int $timeout = 120;

public function __construct(
public readonly int $eventId,
public readonly array $recurrenceRule,
) {
if (config('queue.occurrences_queue_name') !== null) {
$this->onQueue(config('queue.occurrences_queue_name'));
}
}

public static function batchName(int $eventId): string
{
return "Generate occurrences for Event #$eventId";
}

/**
* @throws Throwable
*/
public function handle(GenerateOccurrencesFromRuleHandler $handler): void
{
if ($this->batch()?->cancelled()) {
return;
}

try {
$handler->handle(
new GenerateOccurrencesDTO(
event_id: $this->eventId,
recurrence_rule: $this->recurrenceRule,
)
);
} catch (InvalidRecurrenceRuleException $e) {
$this->fail($e);
}
}

public function failed(Throwable $exception): void
{
Log::critical('GenerateOccurrencesJob permanently failed after retries', [
'event_id' => $this->eventId,
'error' => $exception->getMessage(),
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,51 +9,56 @@
use HiEvents\Repository\Interfaces\EventRepositoryInterface;
use HiEvents\Services\Application\Handlers\EventOccurrence\DTO\GenerateOccurrencesDTO;
use HiEvents\Services\Domain\Event\EventOccurrenceGeneratorService;
use HiEvents\Services\Domain\Event\RecurrenceRuleParserService;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;
use Throwable;

class GenerateOccurrencesFromRuleHandler
{
public function __construct(
private readonly EventOccurrenceGeneratorService $generatorService,
private readonly EventRepositoryInterface $eventRepository,
private readonly RecurrenceRuleParserService $ruleParserService,
private readonly DatabaseManager $databaseManager,
) {}

/**
* @throws Throwable
*/
public function handle(GenerateOccurrencesDTO $dto): Collection
public function handle(GenerateOccurrencesDTO $dto): void
{
$event = $this->eventRepository->findById($dto->event_id);
$timezone = $event->getTimezone() ?? 'UTC';
$this->databaseManager->transaction(function () use ($dto) {
$this->databaseManager->statement('SELECT pg_advisory_xact_lock(?)', [$dto->event_id]);

$previewCount = $this->ruleParserService->parse($dto->recurrence_rule, $timezone)->count();
$event = $this->eventRepository->findByIdLocked($dto->event_id);

if ($previewCount > RecurrenceRuleParserService::MAX_OCCURRENCES) {
throw ValidationException::withMessages([
'recurrence_rule' => [
__('This rule would generate too many occurrences. Please reduce the date range or frequency, or contact support.'),
],
]);
}
$rule = $this->mergeLiveExclusions($dto->recurrence_rule, $event->getRecurrenceRule() ?? []);

return $this->databaseManager->transaction(function () use ($dto, $event) {
$this->eventRepository->updateFromArray(
id: $event->getId(),
attributes: [
EventDomainObjectAbstract::RECURRENCE_RULE => $dto->recurrence_rule,
EventDomainObjectAbstract::RECURRENCE_RULE => $rule,
EventDomainObjectAbstract::TYPE => EventType::RECURRING->name,
],
);

$event->setRecurrenceRule($dto->recurrence_rule);
$event->setRecurrenceRule($rule);

return $this->generatorService->generate($event, $dto->recurrence_rule);
$this->generatorService->generate($event, $rule);
});
}

private function mergeLiveExclusions(array $submittedRule, array $liveRule): array
{
foreach (['excluded_occurrences', 'excluded_dates', 'additional_dates'] as $key) {
$merged = array_values(array_unique(array_merge(
$liveRule[$key] ?? [],
$submittedRule[$key] ?? [],
), SORT_REGULAR));

if ($merged !== []) {
$submittedRule[$key] = $merged;
}
}

return $submittedRule;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace HiEvents\Services\Application\Handlers\EventOccurrence;

use HiEvents\Jobs\Occurrence\GenerateOccurrencesJob;
use HiEvents\Repository\Interfaces\EventRepositoryInterface;
use HiEvents\Services\Application\Handlers\EventOccurrence\DTO\GenerateOccurrencesDTO;
use HiEvents\Services\Domain\Event\RecurrenceRuleParserService;
use HiEvents\Services\Infrastructure\Jobs\DTO\JobPollingResultDTO;
use HiEvents\Services\Infrastructure\Jobs\JobPollingService;
use Illuminate\Validation\ValidationException;

class StartOccurrenceGenerationHandler
{
public function __construct(
private readonly EventRepositoryInterface $eventRepository,
private readonly RecurrenceRuleParserService $ruleParserService,
private readonly JobPollingService $jobPollingService,
) {}

public function handle(GenerateOccurrencesDTO $dto): JobPollingResultDTO
{
$event = $this->eventRepository->findById($dto->event_id);
$timezone = $event->getTimezone() ?? 'UTC';

$previewCount = $this->ruleParserService->parse($dto->recurrence_rule, $timezone)->count();

if ($previewCount > RecurrenceRuleParserService::MAX_OCCURRENCES) {
throw ValidationException::withMessages([
'recurrence_rule' => [
__('This rule would generate too many occurrences. Please reduce the date range or frequency, or contact support.'),
],
]);
}

$startResult = $this->jobPollingService->startJob(
jobName: GenerateOccurrencesJob::batchName($dto->event_id),
jobs: [new GenerateOccurrencesJob($dto->event_id, $dto->recurrence_rule)],
);

return $this->jobPollingService->checkJobStatus($startResult->jobUuid);
}
}
Loading
Loading