feat(events): associate purchases and integrations with events

This commit is contained in:
2026-09-28 19:13:36 +00:00
parent 7f754037bb
commit 7996d53e12
13 changed files with 312 additions and 7 deletions

View File

@@ -2,13 +2,14 @@
namespace App\Domains\Commerce\Purchase\Models; namespace App\Domains\Commerce\Purchase\Models;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Commerce\Cart\Models\Cart; use App\Domains\Commerce\Cart\Models\Cart;
use App\Domains\Commerce\Catalog\Models\StockReservation; use App\Domains\Commerce\Catalog\Models\StockReservation;
use App\Shared\Logging\Models\Concerns\LogsValueChanges;
use App\Domains\Commerce\Purchase\Events\PurchasePaid; use App\Domains\Commerce\Purchase\Events\PurchasePaid;
use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use App\Domains\Ticketing\Ticket\Models\Ticket; use App\Domains\Ticketing\Ticket\Models\Ticket;
use App\Shared\Logging\Models\Concerns\LogsValueChanges;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@@ -21,6 +22,7 @@ use Illuminate\Support\Facades\DB;
#[Fillable([ #[Fillable([
'cart_id', 'cart_id',
'stock_reservation_id', 'stock_reservation_id',
'event_id',
'tenant_codigo', 'tenant_codigo',
'user_id', 'user_id',
'status', 'status',
@@ -149,6 +151,7 @@ class Purchase extends Model
return [ return [
'cart_id' => 'integer', 'cart_id' => 'integer',
'stock_reservation_id' => 'integer', 'stock_reservation_id' => 'integer',
'event_id' => 'integer',
'user_id' => 'integer', 'user_id' => 'integer',
'total' => 'decimal:2', 'total' => 'decimal:2',
]; ];
@@ -162,6 +165,12 @@ class Purchase extends Model
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo'); return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
} }
/** @return BelongsTo<Event, $this> */
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
/** /**
* @return BelongsTo<User, $this> * @return BelongsTo<User, $this>
*/ */

View File

@@ -36,11 +36,11 @@ class PurchaseResource extends JsonResource
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT; === StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
/** @var Event|null $checkoutEvent */ /** @var Event|null $checkoutEvent */
$checkoutEvent = $usesEventSummary $checkoutEvent = $usesEventSummary
? $items ? ($this->event ?? $items
->map(fn (PurchaseItem $item) => $item->sourceCatalogItem?->event) ->map(fn (PurchaseItem $item) => $item->sourceCatalogItem?->event)
->filter() ->filter()
->unique('id') ->unique('id')
->first() ->first())
: null; : null;
$subtotal = $items->isNotEmpty() $subtotal = $items->isNotEmpty()
@@ -62,6 +62,7 @@ class PurchaseResource extends JsonResource
return [ return [
'id' => $this->id, 'id' => $this->id,
'cart_id' => $this->cart_id, 'cart_id' => $this->cart_id,
'event_id' => $this->event_id,
'tenant_codigo' => $this->tenant_codigo, 'tenant_codigo' => $this->tenant_codigo,
'user_id' => $this->user_id, 'user_id' => $this->user_id,
'created_at' => $this->created_at, 'created_at' => $this->created_at,

View File

@@ -14,6 +14,7 @@ class PurchaseResponseLoader
=== StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT; === StorefrontWebsiteType::CHECKOUT_SUMMARY_EVENT;
$relations = [ $relations = [
'tenant', 'tenant',
'event.attachment',
'items.sourceCatalogItem.event.attachment', 'items.sourceCatalogItem.event.attachment',
'stockReservation', 'stockReservation',
]; ];

View File

@@ -206,6 +206,7 @@ class StartCheckoutService
fn (array $line): float => $line['selection']->getPrice() * $line['quantity'], fn (array $line): float => $line['selection']->getPrice() * $line['quantity'],
), ),
$cart->getKey(), $cart->getKey(),
$cartItems->first()?->catalogItem?->event_id,
); );
$cart->update(['current_purchase_id' => $purchase->getKey()]); $cart->update(['current_purchase_id' => $purchase->getKey()]);
$this->reservations->attachToPurchase($cart, $purchase, $this->checkoutExpiration()); $this->reservations->attachToPurchase($cart, $purchase, $this->checkoutExpiration());
@@ -273,6 +274,7 @@ class StartCheckoutService
$purchaseData, $purchaseData,
$cart->getTotalAmount(), $cart->getTotalAmount(),
$cart->getKey(), $cart->getKey(),
$cartItems->first()?->catalogItem?->event_id,
); );
$cart->update(['current_purchase_id' => $purchase->getKey()]); $cart->update(['current_purchase_id' => $purchase->getKey()]);
$this->reservations->attachToPurchase($cart, $purchase, $this->checkoutExpiration()); $this->reservations->attachToPurchase($cart, $purchase, $this->checkoutExpiration());
@@ -410,10 +412,12 @@ class StartCheckoutService
array $purchaseData, array $purchaseData,
float $total, float $total,
?int $cartId, ?int $cartId,
?int $eventId,
): Purchase { ): Purchase {
return Purchase::query()->create([ return Purchase::query()->create([
...$purchaseData, ...$purchaseData,
'cart_id' => $cartId, 'cart_id' => $cartId,
'event_id' => $eventId,
'tenant_codigo' => $tenant->codigo, 'tenant_codigo' => $tenant->codigo,
'user_id' => $userId, 'user_id' => $userId,
'status' => Purchase::STATUS_CREATED, 'status' => Purchase::STATUS_CREATED,

View File

@@ -2,8 +2,9 @@
namespace App\Domains\Core\Client\Models; namespace App\Domains\Core\Client\Models;
use App\Shared\Integration\Models\ClientIntegration;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event;
use App\Shared\Integration\Models\ClientIntegration;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -22,6 +23,12 @@ class Client extends Model
return $this->hasMany(Tenant::class); return $this->hasMany(Tenant::class);
} }
/** @return HasMany<Event, $this> */
public function events(): HasMany
{
return $this->hasMany(Event::class);
}
/** @return HasMany<ClientIntegration, $this> */ /** @return HasMany<ClientIntegration, $this> */
public function integrations(): HasMany public function integrations(): HasMany
{ {

View File

@@ -3,6 +3,7 @@
namespace App\Domains\Ticketing\Event\Models; namespace App\Domains\Ticketing\Event\Models;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Core\Client\Models\Client;
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\Ticket\Models\Ticket; use App\Domains\Ticketing\Ticket\Models\Ticket;
@@ -14,14 +15,14 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable(['tenant_code', 'slug', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])] #[Fillable(['client_id', 'tenant_code', 'slug', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])]
class Event extends Model class Event extends Model
{ {
use HasFactory; use HasFactory;
protected function casts(): array protected function casts(): array
{ {
return ['published_at' => 'datetime', 'exact_location' => 'array']; return ['client_id' => 'integer', 'published_at' => 'datetime', 'exact_location' => 'array'];
} }
/** @return BelongsTo<Tenant, $this> */ /** @return BelongsTo<Tenant, $this> */
@@ -30,6 +31,17 @@ class Event extends Model
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
} }
/** @return BelongsTo<Client, $this> */
public function client(): BelongsTo
{
return $this->belongsTo(Client::class);
}
public function effectiveClient(): Client
{
return $this->client ?? $this->tenant->client;
}
/** @return BelongsTo<EventCategory, $this> */ /** @return BelongsTo<EventCategory, $this> */
public function eventCategory(): BelongsTo public function eventCategory(): BelongsTo
{ {

View File

@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('events', function (Blueprint $table): void {
$table->foreignId('client_id')
->nullable()
->after('id')
->constrained('clients')
->restrictOnDelete();
});
}
public function down(): void
{
Schema::table('events', function (Blueprint $table): void {
$table->dropConstrainedForeignId('client_id');
});
}
};

View File

@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('compras', function (Blueprint $table): void {
$table->foreignId('event_id')
->nullable()
->after('tenant_codigo')
->constrained('events')
->nullOnDelete();
});
DB::table('compras')
->orderBy('id')
->each(function (object $purchase): void {
$eventIds = DB::table('compra_items')
->join('catalog_items', 'catalog_items.id', '=', 'compra_items.source_catalog_item_id')
->where('compra_items.compra_id', $purchase->id)
->whereNotNull('catalog_items.event_id')
->distinct()
->pluck('catalog_items.event_id');
if ($eventIds->count() === 1) {
DB::table('compras')
->where('id', $purchase->id)
->update(['event_id' => $eventIds->first()]);
}
});
}
public function down(): void
{
Schema::table('compras', function (Blueprint $table): void {
$table->dropConstrainedForeignId('event_id');
});
}
};

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
private const EVENT_TITLE = '26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas';
public function up(): void
{
$clientId = DB::table('clients')
->where('code', 'pyme_rural')
->value('id');
if ($clientId === null) {
return;
}
DB::table('events')
->where('tenant_code', 'onticket')
->where('title', self::EVENT_TITLE)
->update(['client_id' => $clientId]);
}
public function down(): void
{
$clientId = DB::table('clients')
->where('code', 'pyme_rural')
->value('id');
if ($clientId === null) {
return;
}
DB::table('events')
->where('tenant_code', 'onticket')
->where('title', self::EVENT_TITLE)
->where('client_id', $clientId)
->update(['client_id' => null]);
}
};

View File

@@ -6,6 +6,7 @@ use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
use App\Domains\Commerce\Catalog\Models\Attribute; use App\Domains\Commerce\Catalog\Models\Attribute;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
use App\Domains\Commerce\Catalog\Services\CatalogService; use App\Domains\Commerce\Catalog\Services\CatalogService;
use App\Domains\Core\Client\Models\Client;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\Event; use App\Domains\Ticketing\Event\Models\Event;
use App\Shared\Attachable\Services\AttachmentService; use App\Shared\Attachable\Services\AttachmentService;
@@ -35,6 +36,12 @@ class FiestaTradicionArrufoSeeder extends Seeder
throw new RuntimeException("Tenant 'onticket' no encontrado."); throw new RuntimeException("Tenant 'onticket' no encontrado.");
} }
$client = Client::query()->where('code', 'pyme_rural')->first();
if ($client === null) {
throw new RuntimeException("Client 'pyme_rural' no encontrado.");
}
Attribute::query()->firstOrCreate( Attribute::query()->firstOrCreate(
['tenant_codigo' => $tenant->codigo, 'codigo' => 'event_date'], ['tenant_codigo' => $tenant->codigo, 'codigo' => 'event_date'],
['nombre' => 'Fecha', 'type' => FieldType::EventDate, 'is_required' => true], ['nombre' => 'Fecha', 'type' => FieldType::EventDate, 'is_required' => true],
@@ -45,6 +52,7 @@ class FiestaTradicionArrufoSeeder extends Seeder
['published_at' => now()], ['published_at' => now()],
); );
$event->update([ $event->update([
'client_id' => $client->id,
'subtitle' => 'Una noche para celebrar nuestras raíces y mantener viva la tradición gaucha.', 'subtitle' => 'Una noche para celebrar nuestras raíces y mantener viva la tradición gaucha.',
'description' => 'La 26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas reunirá a agrupaciones, artesanos, pilcheros y público en general para compartir una jornada dedicada a nuestras costumbres y cultura.' 'description' => 'La 26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas reunirá a agrupaciones, artesanos, pilcheros y público en general para compartir una jornada dedicada a nuestras costumbres y cultura.'
."\n\n".'Un encuentro para disfrutar de la tradición, la identidad gaucha y el espíritu de camaradería, en el Predio de Doma del Club Unión Deportiva Arrufó.' ."\n\n".'Un encuentro para disfrutar de la tradición, la identidad gaucha y el espíritu de camaradería, en el Predio de Doma del Club Unión Deportiva Arrufó.'

View File

@@ -0,0 +1,57 @@
<?php
namespace Tests\Feature\Migrations;
use App\Domains\Core\Client\Models\Client;
use App\Domains\Ticketing\Event\Models\Event;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class AddClientIdToEventsTest extends TestCase
{
public function test_event_client_overrides_the_tenant_client_when_configured(): void
{
Schema::create('clients', function (Blueprint $table): void {
$table->id();
$table->string('code');
$table->string('name');
});
Schema::create('tenants', function (Blueprint $table): void {
$table->id();
$table->foreignId('client_id');
$table->string('codigo');
});
Schema::create('events', function (Blueprint $table): void {
$table->id();
$table->string('tenant_code');
$table->string('title');
$table->timestamps();
});
DB::table('clients')->insert([
['id' => 10, 'code' => 'tenant-client', 'name' => 'Tenant Client'],
['id' => 11, 'code' => 'event-client', 'name' => 'Event Client'],
]);
DB::table('tenants')->insert(['id' => 20, 'client_id' => 10, 'codigo' => 'tenant']);
DB::table('events')->insert([
'id' => 30,
'tenant_code' => 'tenant',
'title' => 'Event',
'created_at' => now(),
'updated_at' => now(),
]);
$migration = require database_path('migrations/2026_09_28_000000_add_client_id_to_events.php');
$migration->up();
$event = Event::query()->findOrFail(30);
$this->assertSame(10, $event->effectiveClient()->id);
$event->update(['client_id' => 11]);
$this->assertSame(11, $event->fresh()->effectiveClient()->id);
$this->assertTrue(Client::query()->findOrFail(11)->events()->whereKey(30)->exists());
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Tests\Feature\Migrations;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class AddEventIdToPurchasesTest extends TestCase
{
public function test_it_backfills_the_event_from_purchase_items(): void
{
Schema::create('events', fn (Blueprint $table) => $table->id());
Schema::create('compras', fn (Blueprint $table) => $table->id());
Schema::create('catalog_items', function (Blueprint $table): void {
$table->id();
$table->foreignId('event_id')->nullable();
});
Schema::create('compra_items', function (Blueprint $table): void {
$table->id();
$table->foreignId('compra_id');
$table->foreignId('source_catalog_item_id');
});
DB::table('events')->insert([['id' => 10], ['id' => 11]]);
DB::table('compras')->insert([['id' => 20], ['id' => 21]]);
DB::table('catalog_items')->insert([
['id' => 30, 'event_id' => 10],
['id' => 31, 'event_id' => 11],
]);
DB::table('compra_items')->insert([
['compra_id' => 20, 'source_catalog_item_id' => 30],
['compra_id' => 21, 'source_catalog_item_id' => 30],
['compra_id' => 21, 'source_catalog_item_id' => 31],
]);
$migration = require database_path('migrations/2026_09_28_000100_add_event_id_to_compras.php');
$migration->up();
$this->assertDatabaseHas('compras', ['id' => 20, 'event_id' => 10]);
$this->assertDatabaseHas('compras', ['id' => 21, 'event_id' => null]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Tests\Feature\Migrations;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class AssignArrufoEventToPymeRuralClientTest extends TestCase
{
public function test_it_assigns_only_the_arrufo_event_to_pyme_rural(): void
{
Schema::create('clients', function (Blueprint $table): void {
$table->id();
$table->string('code');
});
Schema::create('events', function (Blueprint $table): void {
$table->id();
$table->foreignId('client_id')->nullable();
$table->string('tenant_code');
$table->string('title');
});
DB::table('clients')->insert([
['id' => 10, 'code' => 'pyme_rural'],
['id' => 11, 'code' => 'onticket'],
]);
DB::table('events')->insert([
[
'id' => 20,
'client_id' => null,
'tenant_code' => 'onticket',
'title' => '26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas',
],
[
'id' => 21,
'client_id' => 11,
'tenant_code' => 'onticket',
'title' => 'Otro evento',
],
]);
$migration = require database_path('migrations/2026_09_28_000200_assign_arrufo_event_to_pyme_rural_client.php');
$migration->up();
$this->assertDatabaseHas('events', ['id' => 20, 'client_id' => 10]);
$this->assertDatabaseHas('events', ['id' => 21, 'client_id' => 11]);
}
}