refactor(backend): reorganize domains into Core, Commerce, Ticketing and Shared
This commit is contained in:
350
app/Domains/Ticketing/Event/Services/EventService.php
Normal file
350
app/Domains/Ticketing/Event/Services/EventService.php
Normal file
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Event\Services;
|
||||
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Cart\Services\InvalidateEventDateCartsService;
|
||||
use App\Domains\Catalog\Models\Variant;
|
||||
use App\Domains\Catalog\Services\StockReservationService;
|
||||
use App\Domains\Catalog\Services\VariantReplacementService;
|
||||
use App\Domains\Event\Enums\EventDateChangeType;
|
||||
use App\Domains\Event\Events\EventDateRescheduled;
|
||||
use App\Domains\Event\Events\EventDateSuspended;
|
||||
use App\Domains\Event\Models\EventDate;
|
||||
use App\Domains\Event\Models\EventDateChange;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EventService
|
||||
{
|
||||
private const CONTACT_CODES = [
|
||||
'whatsapp_url' => 'whatsapp',
|
||||
'instagram_url' => 'instagram',
|
||||
'facebook_url' => 'facebook',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly EffectiveEventDateResolver $effectiveEventDateResolver,
|
||||
private readonly AffectedEventDatePurchaseResolver $affectedPurchaseResolver,
|
||||
private readonly VariantReplacementService $variantReplacementService,
|
||||
private readonly InvalidateEventDateCartsService $invalidateEventDateCarts,
|
||||
) {}
|
||||
|
||||
public function forTenant(Tenant $tenant): Tenant
|
||||
{
|
||||
return $tenant->load(['eventDates.validityTime', 'socialMedia']);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $data */
|
||||
public function updateForTenant(Tenant $tenant, array $data): Tenant
|
||||
{
|
||||
return DB::transaction(function () use ($tenant, $data): Tenant {
|
||||
$tenant = Tenant::query()->whereKey($tenant->getKey())->lockForUpdate()->firstOrFail();
|
||||
$tenant->update([
|
||||
'event_title' => $data['title'],
|
||||
'event_location' => $data['location'],
|
||||
...array_intersect_key($data, array_flip([
|
||||
'allow_ticket_refund',
|
||||
'allow_ticket_total_refund',
|
||||
'allow_ticket_partial_refund',
|
||||
'ticket_partial_refund_percentage',
|
||||
])),
|
||||
]);
|
||||
|
||||
if (array_key_exists('social_media', $data)) {
|
||||
$this->syncSocialMedia($tenant, $data['social_media']);
|
||||
} else {
|
||||
$this->syncLegacyContact($tenant, $data['contact']);
|
||||
}
|
||||
|
||||
return $tenant->load(['eventDates.validityTime', 'socialMedia']);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array{date: string, start_time: string, end_time: string} $data */
|
||||
public function createDateForTenant(Tenant $tenant, array $data): EventDate
|
||||
{
|
||||
return DB::transaction(function () use ($tenant, $data): EventDate {
|
||||
$attributes = $this->dateAttributes($data);
|
||||
|
||||
if ($tenant->eventDates()->where($attributes)->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'date' => ['La fecha y el horario ya existen.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $tenant->eventDates()->create($attributes)->load('validityTime');
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array{date: string} $data */
|
||||
public function rescheduleDateForTenant(
|
||||
Tenant $tenant,
|
||||
EventDate $eventDate,
|
||||
array $data,
|
||||
?User $createdBy = null,
|
||||
): EventDate {
|
||||
return DB::transaction(function () use ($tenant, $eventDate, $data, $createdBy): EventDate {
|
||||
$source = $this->lockedDateForTenant($tenant, $eventDate);
|
||||
|
||||
if ($source->suspended_at !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'event_date' => ['No se puede reprogramar una fecha suspendida.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($source->rescheduled_to_event_date_id !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'event_date' => ['La fecha ya fue reprogramada.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$destination = $tenant->eventDates()
|
||||
->whereDate('date', $data['date'])
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($destination === null) {
|
||||
$destination = $tenant->eventDates()->create([
|
||||
'date' => $data['date'],
|
||||
'time_start' => $source->time_start,
|
||||
'time_end' => $source->time_end,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($destination->is($source) || $this->chainContains($destination, $source)) {
|
||||
throw ValidationException::withMessages([
|
||||
'date' => ['La reprogramación generaría una referencia circular.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$effectiveDestination = $this->effectiveEventDateResolver->resolve($destination);
|
||||
if ($effectiveDestination === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'date' => ['La fecha de destino no es utilizable.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$affectedDateIds = $this->affectedDateIds($tenant, $source);
|
||||
$this->invalidateEventDateCarts->invalidate($tenant, $affectedDateIds);
|
||||
$purchaseTickets = $this->affectedPurchaseResolver->resolve(
|
||||
$tenant,
|
||||
$affectedDateIds,
|
||||
);
|
||||
$source->update(['rescheduled_to_event_date_id' => $destination->getKey()]);
|
||||
$this->variantReplacementService->replaceEventDate($source, $effectiveDestination);
|
||||
|
||||
EventDateChange::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'change_type' => EventDateChangeType::Rescheduled,
|
||||
'source_event_date_id' => $source->getKey(),
|
||||
'destination_event_date_id' => $destination->getKey(),
|
||||
'created_by_user_id' => $createdBy?->getKey(),
|
||||
'previous_date' => $source->date->format('Y-m-d'),
|
||||
'new_date' => $destination->date->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
EventDateRescheduled::dispatch(
|
||||
$tenant->codigo,
|
||||
$source->getKey(),
|
||||
$destination->getKey(),
|
||||
$source->date->format('d/m/Y'),
|
||||
$destination->date->format('d/m/Y'),
|
||||
$purchaseTickets,
|
||||
);
|
||||
|
||||
return $source->fresh(['validityTime', 'rescheduledTo.validityTime']);
|
||||
});
|
||||
}
|
||||
|
||||
public function suspendDateForTenant(
|
||||
Tenant $tenant,
|
||||
EventDate $eventDate,
|
||||
?User $createdBy = null,
|
||||
): EventDate {
|
||||
return DB::transaction(function () use ($tenant, $eventDate, $createdBy): EventDate {
|
||||
$date = $this->lockedDateForTenant($tenant, $eventDate);
|
||||
|
||||
if ($date->rescheduled_to_event_date_id !== null) {
|
||||
throw ValidationException::withMessages([
|
||||
'event_date' => ['No se puede suspender una fecha que ya fue reprogramada.'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($date->suspended_at !== null) {
|
||||
return $date->load('validityTime');
|
||||
}
|
||||
|
||||
$affectedDateIds = $this->affectedDateIds($tenant, $date);
|
||||
$this->invalidateEventDateCarts->invalidate(
|
||||
$tenant,
|
||||
$affectedDateIds,
|
||||
StockReservationService::REASON_EVENT_DATE_SUSPENDED,
|
||||
);
|
||||
$purchaseTickets = $this->affectedPurchaseResolver->resolve($tenant, $affectedDateIds);
|
||||
$date->update(['suspended_at' => now()]);
|
||||
$this->variantReplacementService->disableForSuspension($date);
|
||||
$this->disableTicketsWithoutUsableDates($tenant, $date);
|
||||
|
||||
EventDateChange::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'change_type' => EventDateChangeType::Suspended,
|
||||
'source_event_date_id' => $date->getKey(),
|
||||
'destination_event_date_id' => null,
|
||||
'created_by_user_id' => $createdBy?->getKey(),
|
||||
'previous_date' => $date->date->format('Y-m-d'),
|
||||
'new_date' => null,
|
||||
]);
|
||||
|
||||
EventDateSuspended::dispatch(
|
||||
$tenant->codigo,
|
||||
$date->getKey(),
|
||||
$date->date->format('d/m/Y'),
|
||||
$purchaseTickets,
|
||||
);
|
||||
|
||||
return $date->fresh('validityTime');
|
||||
});
|
||||
}
|
||||
|
||||
private function lockedDateForTenant(Tenant $tenant, EventDate $eventDate): EventDate
|
||||
{
|
||||
return $tenant->eventDates()
|
||||
->whereKey($eventDate->getKey())
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
private function chainContains(EventDate $start, EventDate $expected): bool
|
||||
{
|
||||
$current = $start;
|
||||
$visited = [];
|
||||
|
||||
while ($current->rescheduled_to_event_date_id !== null) {
|
||||
if ($current->is($expected)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isset($visited[$current->getKey()])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$visited[$current->getKey()] = true;
|
||||
$current = $current->rescheduledTo()->lockForUpdate()->first();
|
||||
|
||||
if ($current === null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $current->is($expected);
|
||||
}
|
||||
|
||||
/** @return Collection<int, int> */
|
||||
private function affectedDateIds(Tenant $tenant, EventDate $eventDate): Collection
|
||||
{
|
||||
$affectedDateIds = collect([$eventDate->getKey()]);
|
||||
$frontier = $affectedDateIds;
|
||||
|
||||
while ($frontier->isNotEmpty()) {
|
||||
$predecessors = $tenant->eventDates()
|
||||
->whereIn('rescheduled_to_event_date_id', $frontier)
|
||||
->pluck('id')
|
||||
->diff($affectedDateIds)
|
||||
->values();
|
||||
$affectedDateIds = $affectedDateIds->merge($predecessors)->unique()->values();
|
||||
$frontier = $predecessors;
|
||||
}
|
||||
|
||||
return $affectedDateIds;
|
||||
}
|
||||
|
||||
private function disableTicketsWithoutUsableDates(Tenant $tenant, EventDate $suspendedDate): void
|
||||
{
|
||||
$affectedDateIds = $this->affectedDateIds($tenant, $suspendedDate);
|
||||
|
||||
$variants = Variant::withTrashed()
|
||||
->where(function ($query) use ($affectedDateIds): void {
|
||||
$query->whereIn('event_date_id', $affectedDateIds)
|
||||
->orWhereHas('eventDates', fn ($eventDates) => $eventDates
|
||||
->whereIn('event_dates.id', $affectedDateIds));
|
||||
})
|
||||
->with(['eventDates', 'eventDate'])
|
||||
->get();
|
||||
|
||||
foreach ($variants as $variant) {
|
||||
$hasUsableDate = $variant->selectedEventDates()->contains(
|
||||
fn (EventDate $candidate): bool => $this->effectiveEventDateResolver->resolve($candidate) !== null
|
||||
);
|
||||
|
||||
if ($hasUsableDate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Ticket::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->where('source_variant_id', $variant->getKey())
|
||||
->whereNull('disabled_at')
|
||||
->whereNull('cancelled_at')
|
||||
->whereNull('refunded_at')
|
||||
->lockForUpdate()
|
||||
->get()
|
||||
->each(function (Ticket $ticket): void {
|
||||
$ticket->markAsDisabled();
|
||||
$ticket->save();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{date: string, start_time: string, end_time: string} $data
|
||||
* @return array{date: string, time_start: string, time_end: string}
|
||||
*/
|
||||
private function dateAttributes(array $data): array
|
||||
{
|
||||
return [
|
||||
'date' => $data['date'],
|
||||
'time_start' => $data['start_time'].':00',
|
||||
'time_end' => $data['end_time'].':00',
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, string|null> $contact */
|
||||
private function syncLegacyContact(Tenant $tenant, array $contact): void
|
||||
{
|
||||
foreach (self::CONTACT_CODES as $field => $code) {
|
||||
$url = $contact[$field] ?? null;
|
||||
|
||||
if ($url === null || $url === '') {
|
||||
$tenant->socialMedia()->detach($code);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$tenant->socialMedia()->syncWithoutDetaching([
|
||||
$code => ['url' => $url],
|
||||
]);
|
||||
}
|
||||
|
||||
$tenant->unsetRelation('socialMedia');
|
||||
}
|
||||
|
||||
/** @param array<int, array{code: string, url: string, orden?: int}> $socialMedia */
|
||||
private function syncSocialMedia(Tenant $tenant, array $socialMedia): void
|
||||
{
|
||||
$associations = [];
|
||||
|
||||
foreach (array_values($socialMedia) as $index => $item) {
|
||||
$associations[$item['code']] = [
|
||||
'url' => $item['url'],
|
||||
'orden' => $item['orden'] ?? $index,
|
||||
];
|
||||
}
|
||||
|
||||
$tenant->socialMedia()->sync($associations);
|
||||
$tenant->unsetRelation('socialMedia');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user