feat(events): authorize event editing and date changes by assigned event

This commit is contained in:
2026-09-30 15:31:54 -03:00
parent 5a0ce08adb
commit 55c28f33ef
7 changed files with 53 additions and 34 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Domains\Ticketing\Event\Controllers\AdminApp; namespace App\Domains\Ticketing\Event\Controllers\AdminApp;
use App\Domains\Core\Auth\Services\EventScopeService;
use App\Domains\Ticketing\Event\Models\EventDate; use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Event\Requests\RescheduleEventDateRequest; use App\Domains\Ticketing\Event\Requests\RescheduleEventDateRequest;
use App\Domains\Ticketing\Event\Requests\StoreEventDateRequest; use App\Domains\Ticketing\Event\Requests\StoreEventDateRequest;
@@ -19,7 +20,8 @@ class EventController extends Controller
public function show(Request $request): EventResource public function show(Request $request): EventResource
{ {
return EventResource::make( return EventResource::make(
$this->eventService->forTenant($request->user()->tenant()->firstOrFail()) $this->eventService->forTenant($request->user()->tenant()->firstOrFail(),
app(EventScopeService::class)->eventId($request->user()))
); );
} }
@@ -28,7 +30,8 @@ class EventController extends Controller
return EventResource::make( return EventResource::make(
$this->eventService->updateForTenant( $this->eventService->updateForTenant(
$request->user()->tenant()->firstOrFail(), $request->user()->tenant()->firstOrFail(),
$request->validated() $request->validated(),
app(EventScopeService::class)->eventId($request->user())
) )
); );
} }
@@ -39,6 +42,7 @@ class EventController extends Controller
$this->eventService->createDateForTenant( $this->eventService->createDateForTenant(
$request->user()->tenant()->firstOrFail(), $request->user()->tenant()->firstOrFail(),
$request->validated(), $request->validated(),
app(EventScopeService::class)->eventId($request->user()),
) )
); );
} }

View File

@@ -2,18 +2,19 @@
namespace App\Domains\Ticketing\Event\Services; namespace App\Domains\Ticketing\Event\Services;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Commerce\Cart\Services\InvalidateEventDateCartsService; use App\Domains\Commerce\Cart\Services\InvalidateEventDateCartsService;
use App\Domains\Commerce\Catalog\Models\Variant; use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Commerce\Catalog\Services\StockReservationService; use App\Domains\Commerce\Catalog\Services\StockReservationService;
use App\Domains\Commerce\Catalog\Services\VariantReplacementService; use App\Domains\Commerce\Catalog\Services\VariantReplacementService;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Auth\Services\EventScopeService;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Enums\EventDateChangeType; use App\Domains\Ticketing\Event\Enums\EventDateChangeType;
use App\Domains\Ticketing\Event\Events\EventDateRescheduled; use App\Domains\Ticketing\Event\Events\EventDateRescheduled;
use App\Domains\Ticketing\Event\Events\EventDateSuspended; use App\Domains\Ticketing\Event\Events\EventDateSuspended;
use App\Domains\Ticketing\Event\Models\Event;
use App\Domains\Ticketing\Event\Models\EventDate; use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Ticketing\Event\Models\EventDateChange; use App\Domains\Ticketing\Event\Models\EventDateChange;
use App\Domains\Ticketing\Event\Models\Event;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Ticket\Models\Ticket; use App\Domains\Ticketing\Ticket\Models\Ticket;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -34,16 +35,16 @@ class EventService
private readonly InvalidateEventDateCartsService $invalidateEventDateCarts, private readonly InvalidateEventDateCartsService $invalidateEventDateCarts,
) {} ) {}
public function forTenant(Tenant $tenant): Event public function forTenant(Tenant $tenant, ?int $eventId = null): Event
{ {
return $tenant->activeEvent->load(['dates.validityTime', 'socialMedia']); return $this->resolveEvent($tenant, $eventId)->load(['dates.validityTime', 'socialMedia']);
} }
/** @param array<string, mixed> $data */ /** @param array<string, mixed> $data */
public function updateForTenant(Tenant $tenant, array $data): Event public function updateForTenant(Tenant $tenant, array $data, ?int $eventId = null): Event
{ {
return DB::transaction(function () use ($tenant, $data): Event { return DB::transaction(function () use ($tenant, $data, $eventId): Event {
$event = $tenant->activeEvent; $event = $this->resolveEvent($tenant, $eventId);
$event->update([ $event->update([
'title' => $data['title'], 'title' => $data['title'],
'location' => $data['location'], 'location' => $data['location'],
@@ -69,10 +70,10 @@ class EventService
} }
/** @param array{date: string, start_time: string, end_time: string} $data */ /** @param array{date: string, start_time: string, end_time: string} $data */
public function createDateForTenant(Tenant $tenant, array $data): EventDate public function createDateForTenant(Tenant $tenant, array $data, ?int $eventId = null): EventDate
{ {
return DB::transaction(function () use ($tenant, $data): EventDate { return DB::transaction(function () use ($tenant, $data, $eventId): EventDate {
$event = $tenant->activeEvent; $event = $this->resolveEvent($tenant, $eventId);
$attributes = $this->dateAttributes($data); $attributes = $this->dateAttributes($data);
if ($event->dates()->where($attributes)->exists()) { if ($event->dates()->where($attributes)->exists()) {
@@ -93,7 +94,7 @@ class EventService
?User $createdBy = null, ?User $createdBy = null,
): EventDate { ): EventDate {
return DB::transaction(function () use ($tenant, $eventDate, $data, $createdBy): EventDate { return DB::transaction(function () use ($tenant, $eventDate, $data, $createdBy): EventDate {
$source = $this->lockedDateForTenant($tenant, $eventDate); $source = $this->lockedDateForTenant($tenant, $eventDate, $createdBy);
if ($source->suspended_at !== null) { if ($source->suspended_at !== null) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@@ -172,7 +173,7 @@ class EventService
?User $createdBy = null, ?User $createdBy = null,
): EventDate { ): EventDate {
return DB::transaction(function () use ($tenant, $eventDate, $createdBy): EventDate { return DB::transaction(function () use ($tenant, $eventDate, $createdBy): EventDate {
$date = $this->lockedDateForTenant($tenant, $eventDate); $date = $this->lockedDateForTenant($tenant, $eventDate, $createdBy);
if ($date->rescheduled_to_event_date_id !== null) { if ($date->rescheduled_to_event_date_id !== null) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@@ -216,10 +217,18 @@ class EventService
}); });
} }
private function lockedDateForTenant(Tenant $tenant, EventDate $eventDate): EventDate private function resolveEvent(Tenant $tenant, ?int $eventId): Event
{
return Event::query()->where('tenant_code', $tenant->codigo)
->findOrFail($eventId ?? $tenant->active_event_id);
}
private function lockedDateForTenant(Tenant $tenant, EventDate $eventDate, ?User $actor = null): EventDate
{ {
return $tenant->eventDates() return $tenant->eventDates()
->whereKey($eventDate->getKey()) ->whereKey($eventDate->getKey())
->when($actor !== null && ! $actor->isTenantAdministrator(),
fn ($query) => $query->where('event_id', app(EventScopeService::class)->eventId($actor)))
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
} }

View File

@@ -37,10 +37,10 @@ se guardan en el evento. Sin evento activo se conservan las redes propias del te
## API ## API
Los endpoints de AdminApp usan `auth:sanctum` y `adminapp.tenant`: Los endpoints de AdminApp usan `auth:sanctum` y `adminapp.tenant:event`:
- `GET/PUT /v1/adminapp/tenant/event`: acceso al evento seleccionado para el - `GET/PUT /v1/adminapp/tenant/event`: acceso al evento asociado al usuario con scope de evento;
storefront de evento único. para admins de tenant se conserva el evento activo.
- `POST /v1/adminapp/tenant/event-dates`: crea una fecha para ese evento. - `POST /v1/adminapp/tenant/event-dates`: crea una fecha para ese evento.
- `POST /v1/adminapp/tenant/event-dates/{eventDate}/reschedule` y - `POST /v1/adminapp/tenant/event-dates/{eventDate}/reschedule` y
`POST /v1/adminapp/tenant/event-dates/{eventDate}/suspend`: cambios de fecha. `POST /v1/adminapp/tenant/event-dates/{eventDate}/suspend`: cambios de fecha.
@@ -56,4 +56,5 @@ después de excluir los cambios que el usuario ya vio tres veces.
La configuración de devoluciones se persiste en `events`, se entrega en La configuración de devoluciones se persiste en `events`, se entrega en
`EventResource` y se aplica al evento de cada ticket. La migración inicial copia `EventResource` y se aplica al evento de cada ticket. La migración inicial copia
los valores anteriores del tenant a sus eventos. los valores anteriores del tenant a sus eventos. Las fechas se autorizan por
tenant y por evento antes de cualquier suspensión o reprogramación.

View File

@@ -4,7 +4,7 @@ use App\Domains\Ticketing\Event\Controllers\AdminApp\EventController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::prefix('v1/adminapp/tenant') Route::prefix('v1/adminapp/tenant')
->middleware(['auth:sanctum', 'adminapp.tenant']) ->middleware(['auth:sanctum', 'adminapp.tenant:event'])
->group(function (): void { ->group(function (): void {
Route::get('event', [EventController::class, 'show']); Route::get('event', [EventController::class, 'show']);
Route::put('event', [EventController::class, 'update']); Route::put('event', [EventController::class, 'update']);

View File

@@ -2,9 +2,10 @@
namespace App\Shared\Forms\Controllers\AdminApp; namespace App\Shared\Forms\Controllers\AdminApp;
use App\Domains\Core\Auth\Services\EventScopeService;
use App\Http\Controllers\Controller;
use App\Shared\Forms\Resources\EventFormResource; use App\Shared\Forms\Resources\EventFormResource;
use App\Shared\Forms\Services\EventFormService; use App\Shared\Forms\Services\EventFormService;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class EventFormController extends Controller class EventFormController extends Controller
@@ -15,7 +16,8 @@ class EventFormController extends Controller
{ {
return EventFormResource::make( return EventFormResource::make(
$this->eventFormService->get( $this->eventFormService->get(
$request->user('sanctum')->tenant()->firstOrFail() $request->user('sanctum')->tenant()->firstOrFail(),
app(EventScopeService::class)->eventId($request->user())
) )
); );
} }

View File

@@ -4,14 +4,17 @@ namespace App\Shared\Forms\Services;
use App\Domains\Core\Tenant\Models\SocialMedia; use App\Domains\Core\Tenant\Models\SocialMedia;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
class EventFormService class EventFormService
{ {
/** @return array{social_media: Collection<int, SocialMedia>} */ /** @return array{social_media: Collection<int, SocialMedia>} */
public function get(Tenant $tenant): array public function get(Tenant $tenant, ?int $eventId = null): array
{ {
$event = $tenant->activeEvent; $event = $eventId === null ? $tenant->activeEvent
: Event::query()
->where('tenant_code', $tenant->codigo)->findOrFail($eventId);
$urls = $event?->socialMedia() $urls = $event?->socialMedia()
->pluck('event_social_media.url', 'social_media.code') ?? collect(); ->pluck('event_social_media.url', 'social_media.code') ?? collect();

View File

@@ -12,33 +12,33 @@ use App\Shared\Forms\Controllers\AdminApp\TicketFormController;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::prefix('v1/adminapp/forms') Route::prefix('v1/adminapp/forms')
->middleware(['auth:sanctum', 'adminapp.tenant']) ->middleware(['auth:sanctum', 'adminapp.tenant:event'])
->group(function (): void { ->group(function (): void {
Route::get('event', EventFormController::class); Route::get('event', EventFormController::class);
Route::get( Route::get(
'desfile/entry-reservation', 'desfile/entry-reservation',
DesfileEntryReservationFormController::class DesfileEntryReservationFormController::class
)->middleware('tenant.menu:adminapp.desfile.reservas') )->middleware('adminapp.tenant')->middleware('tenant.menu:adminapp.desfile.reservas')
->name('adminapp.forms.desfile.entry-reservation'); ->name('adminapp.forms.desfile.entry-reservation');
Route::get('sale', SaleFormController::class); Route::get('sale', SaleFormController::class)->middleware('adminapp.tenant');
Route::get('staff', StaffFormController::class); Route::get('staff', StaffFormController::class)->middleware('adminapp.tenant');
Route::get('tickets-filter', TicketFilterFormController::class) Route::get('tickets-filter', TicketFilterFormController::class)->middleware('adminapp.tenant')
->middleware('tenant.menu:adminapp.tickets') ->middleware('tenant.menu:adminapp.tickets')
->name('adminapp.forms.tickets-filter'); ->name('adminapp.forms.tickets-filter');
Route::get( Route::get(
'fiesta-futbol-infantil/ticket', 'fiesta-futbol-infantil/ticket',
TicketFormController::class TicketFormController::class
); )->middleware('adminapp.tenant');
Route::get( Route::get(
'fiesta-futbol-infantil/entry', 'fiesta-futbol-infantil/entry',
EntryFormController::class EntryFormController::class
); )->middleware('adminapp.tenant');
Route::get( Route::get(
'fiesta-futbol-infantil/merchandise', 'fiesta-futbol-infantil/merchandise',
MerchandiseFormController::class MerchandiseFormController::class
); )->middleware('adminapp.tenant');
Route::get( Route::get(
'fiesta-futbol-infantil/food', 'fiesta-futbol-infantil/food',
FoodFormController::class FoodFormController::class
); )->middleware('adminapp.tenant');
}); });