418 lines
15 KiB
PHP
418 lines
15 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Desfile\Services;
|
|
|
|
use DateTimeInterface;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Str;
|
|
use RuntimeException;
|
|
|
|
class InvitationPurchaseProvisioner
|
|
{
|
|
public const TENANT_CODE = 'desfile_pura_tendencia';
|
|
|
|
public const USER_EMAIL = 'invitados@puratendencia.com';
|
|
|
|
private const PAYMENT_METHOD = 'invitation';
|
|
|
|
/**
|
|
* @var list<array{sector: string, row: int, seats: int, type: string}>
|
|
*/
|
|
private const ALLOCATIONS = [
|
|
['sector' => 'A', 'row' => 1, 'seats' => 16, 'type' => 'NORMAL'],
|
|
['sector' => 'A', 'row' => 3, 'seats' => 14, 'type' => 'NORMAL'],
|
|
['sector' => 'C', 'row' => 1, 'seats' => 16, 'type' => 'VIP + LUNCH'],
|
|
];
|
|
|
|
public function provision(): void
|
|
{
|
|
if (! $this->prerequisitesExist()) {
|
|
return;
|
|
}
|
|
|
|
DB::transaction(function (): void {
|
|
$now = now();
|
|
$userId = $this->userId($now);
|
|
$purchaseId = $this->purchaseId($userId, $now);
|
|
$catalogItem = DB::table('catalog_items')
|
|
->where('tenant_code', self::TENANT_CODE)
|
|
->where('slug', 'entrada')
|
|
->first();
|
|
|
|
if ($catalogItem === null) {
|
|
throw new RuntimeException('No se encontró el catálogo de entradas del desfile.');
|
|
}
|
|
|
|
foreach (self::ALLOCATIONS as $allocation) {
|
|
foreach (range(1, $allocation['seats']) as $seat) {
|
|
$variant = $this->variant(
|
|
(int) $catalogItem->id,
|
|
$allocation['sector'],
|
|
$allocation['row'],
|
|
$seat,
|
|
$allocation['type'],
|
|
);
|
|
|
|
$this->createPurchaseItem(
|
|
$purchaseId,
|
|
$catalogItem,
|
|
$variant,
|
|
$allocation['sector'],
|
|
$allocation['row'],
|
|
$seat,
|
|
$allocation['type'],
|
|
$now,
|
|
);
|
|
$this->createTicketAndCommitStock(
|
|
$purchaseId,
|
|
$userId,
|
|
(int) $catalogItem->id,
|
|
$variant,
|
|
$now,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private function prerequisitesExist(): bool
|
|
{
|
|
if (! DB::table('tenants')->where('codigo', self::TENANT_CODE)->exists()) {
|
|
return false;
|
|
}
|
|
|
|
if (! DB::table('roles')->where('codigo', 'user')->exists()) {
|
|
throw new RuntimeException('No se encontró el rol de usuario común.');
|
|
}
|
|
|
|
if (! DB::table('catalog_items')
|
|
->where('tenant_code', self::TENANT_CODE)
|
|
->where('slug', 'entrada')
|
|
->exists()) {
|
|
throw new RuntimeException('No se encontró el catálogo de entradas del desfile.');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private function userId(DateTimeInterface $now): int
|
|
{
|
|
$user = DB::table('users')->where('email', self::USER_EMAIL)->first();
|
|
|
|
if ($user !== null) {
|
|
if ($user->tenant_codigo !== self::TENANT_CODE) {
|
|
throw new RuntimeException('El email de invitados ya pertenece a otro tenant.');
|
|
}
|
|
|
|
DB::table('users')->where('id', $user->id)->update([
|
|
'rol_codigo' => 'user',
|
|
'nombre_apellido' => 'Invitados Pura Tendencia',
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
return (int) $user->id;
|
|
}
|
|
|
|
return DB::table('users')->insertGetId([
|
|
'rol_codigo' => 'user',
|
|
'tenant_codigo' => self::TENANT_CODE,
|
|
'nombre_apellido' => 'Invitados Pura Tendencia',
|
|
'email' => self::USER_EMAIL,
|
|
'email_verified_at' => $now,
|
|
'password' => Hash::make(Str::random(64)),
|
|
'dni' => null,
|
|
'telefono' => null,
|
|
'remember_token' => null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
private function purchaseId(int $userId, DateTimeInterface $now): int
|
|
{
|
|
$purchaseId = DB::table('compras')
|
|
->where('tenant_codigo', self::TENANT_CODE)
|
|
->where('user_id', $userId)
|
|
->where('payment_method', self::PAYMENT_METHOD)
|
|
->value('id');
|
|
|
|
if ($purchaseId !== null) {
|
|
DB::table('compras')->where('id', $purchaseId)->update([
|
|
'status' => 'paid',
|
|
'expires_at' => null,
|
|
'total' => 0,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
return (int) $purchaseId;
|
|
}
|
|
|
|
return DB::table('compras')->insertGetId([
|
|
'tenant_codigo' => self::TENANT_CODE,
|
|
'user_id' => $userId,
|
|
'cart_id' => null,
|
|
'status' => 'paid',
|
|
'payment_method' => self::PAYMENT_METHOD,
|
|
'expires_at' => null,
|
|
'total' => 0,
|
|
'dni' => null,
|
|
'transfer_payer_dni' => null,
|
|
'telefono' => null,
|
|
'nombre_apellido' => 'Invitados Pura Tendencia',
|
|
'email' => self::USER_EMAIL,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
private function variant(
|
|
int $catalogItemId,
|
|
string $sector,
|
|
int $row,
|
|
int $seat,
|
|
string $type,
|
|
): object {
|
|
$variant = $this->findVariant($catalogItemId, $sector, $row, $seat);
|
|
|
|
if ($variant === null) {
|
|
$variant = $this->createVariant($catalogItemId, $sector, $row, $seat, $type);
|
|
}
|
|
|
|
$this->ensureAttributeOptions($catalogItemId, [
|
|
'tipo' => $type,
|
|
'sector' => $sector,
|
|
'fila' => (string) $row,
|
|
'asiento' => (string) $seat,
|
|
]);
|
|
|
|
$typeValueId = DB::table('variant_values as variant_value')
|
|
->join('item_attributes as item_attribute', 'item_attribute.id', '=', 'variant_value.item_attribute_id')
|
|
->join('attribute', 'attribute.id', '=', 'item_attribute.attribute_id')
|
|
->where('variant_value.variant_id', $variant->id)
|
|
->where('attribute.codigo', 'tipo')
|
|
->value('variant_value.id');
|
|
|
|
if ($typeValueId === null) {
|
|
throw new RuntimeException("La variante {$variant->id} no tiene definido el tipo de entrada.");
|
|
}
|
|
|
|
DB::table('variant_values')->where('id', $typeValueId)->update(['value' => $type]);
|
|
DB::table('variantes')->where('id', $variant->id)->update([
|
|
'descripcion' => "Sector {$sector} - Fila {$row} - Asiento {$seat} - {$type}",
|
|
]);
|
|
|
|
return DB::table('variantes')->where('id', $variant->id)->first();
|
|
}
|
|
|
|
/** @param array<string, string> $values */
|
|
private function ensureAttributeOptions(int $catalogItemId, array $values): void
|
|
{
|
|
$attributes = DB::table('item_attributes')
|
|
->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id')
|
|
->where('item_attributes.catalog_item_id', $catalogItemId)
|
|
->pluck('attribute.id', 'attribute.codigo');
|
|
|
|
foreach ($values as $code => $value) {
|
|
$attributeId = $attributes[$code] ?? null;
|
|
|
|
if ($attributeId === null) {
|
|
throw new RuntimeException("Falta el atributo {$code} en el catálogo de entradas.");
|
|
}
|
|
|
|
if (DB::table('attribute_options')
|
|
->where('attribute_id', $attributeId)
|
|
->where('value', $value)
|
|
->exists()) {
|
|
continue;
|
|
}
|
|
|
|
$lastSortOrder = (int) DB::table('attribute_options')
|
|
->where('attribute_id', $attributeId)
|
|
->max('sort_order');
|
|
|
|
DB::table('attribute_options')->insert([
|
|
'attribute_id' => $attributeId,
|
|
'validity_time_id' => null,
|
|
'value' => $value,
|
|
'label' => $value,
|
|
'sort_order' => $lastSortOrder + 1,
|
|
'metadata' => null,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function findVariant(int $catalogItemId, string $sector, int $row, int $seat): ?object
|
|
{
|
|
$query = DB::table('variantes')->where('catalog_item_id', $catalogItemId);
|
|
|
|
foreach (['sector' => $sector, 'fila' => (string) $row, 'asiento' => (string) $seat] as $code => $value) {
|
|
$query->whereExists(fn ($subquery) => $subquery
|
|
->selectRaw('1')
|
|
->from('variant_values as selected_value')
|
|
->join('item_attributes as selected_item_attribute', 'selected_item_attribute.id', '=', 'selected_value.item_attribute_id')
|
|
->join('attribute as selected_attribute', 'selected_attribute.id', '=', 'selected_item_attribute.attribute_id')
|
|
->whereColumn('selected_value.variant_id', 'variantes.id')
|
|
->where('selected_attribute.codigo', $code)
|
|
->where('selected_value.value', $value));
|
|
}
|
|
|
|
return $query->first();
|
|
}
|
|
|
|
private function createVariant(
|
|
int $catalogItemId,
|
|
string $sector,
|
|
int $row,
|
|
int $seat,
|
|
string $type,
|
|
): object {
|
|
$itemAttributes = DB::table('item_attributes')
|
|
->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id')
|
|
->where('item_attributes.catalog_item_id', $catalogItemId)
|
|
->pluck('item_attributes.id', 'attribute.codigo');
|
|
|
|
foreach (['tipo', 'sector', 'fila', 'asiento'] as $requiredCode) {
|
|
if (! isset($itemAttributes[$requiredCode])) {
|
|
throw new RuntimeException("Falta el atributo {$requiredCode} en el catálogo de entradas.");
|
|
}
|
|
}
|
|
|
|
$inventoryId = DB::table('inventories')->insertGetId([
|
|
'sold_units' => 0,
|
|
'reserved_stock' => 0,
|
|
'real_stock' => 1,
|
|
]);
|
|
$variantId = DB::table('variantes')->insertGetId([
|
|
'catalog_item_id' => $catalogItemId,
|
|
'event_date_id' => null,
|
|
'inventory_id' => $inventoryId,
|
|
'descripcion' => "Sector {$sector} - Fila {$row} - Asiento {$seat} - {$type}",
|
|
'precio' => $this->catalogPrice($sector, $row),
|
|
]);
|
|
|
|
foreach (['tipo' => $type, 'sector' => $sector, 'fila' => (string) $row, 'asiento' => (string) $seat] as $code => $value) {
|
|
DB::table('variant_values')->insert([
|
|
'variant_id' => $variantId,
|
|
'item_attribute_id' => $itemAttributes[$code],
|
|
'value' => $value,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
}
|
|
|
|
return DB::table('variantes')->where('id', $variantId)->first();
|
|
}
|
|
|
|
private function catalogPrice(string $sector, int $row): int
|
|
{
|
|
$prices = in_array($sector, ['A', 'C'], true)
|
|
? [1 => 250000, 2 => 200000, 3 => 100000, 4 => 75000, 5 => 50000]
|
|
: [1 => 240000, 2 => 190000, 3 => 90000, 4 => 65000, 5 => 40000];
|
|
|
|
return $prices[$row];
|
|
}
|
|
|
|
private function createPurchaseItem(
|
|
int $purchaseId,
|
|
object $catalogItem,
|
|
object $variant,
|
|
string $sector,
|
|
int $row,
|
|
int $seat,
|
|
string $type,
|
|
DateTimeInterface $now,
|
|
): void {
|
|
if (DB::table('compra_items')
|
|
->where('compra_id', $purchaseId)
|
|
->where('source_variant_id', $variant->id)
|
|
->exists()) {
|
|
return;
|
|
}
|
|
|
|
$attributes = [
|
|
['name' => 'Tipo', 'value' => $type],
|
|
['name' => 'Sector', 'value' => $sector],
|
|
['name' => 'Fila', 'value' => (string) $row],
|
|
['name' => 'Asiento', 'value' => (string) $seat],
|
|
];
|
|
|
|
DB::table('compra_items')->insert([
|
|
'compra_id' => $purchaseId,
|
|
'source_catalog_item_id' => $catalogItem->id,
|
|
'source_variant_id' => $variant->id,
|
|
'image_attachment_id' => DB::table('catalog_items_attachments')
|
|
->where('catalog_item_id', $catalogItem->id)
|
|
->whereNull('variant_id')
|
|
->orderBy('orden')
|
|
->value('attachment_id'),
|
|
'nombre' => $catalogItem->nombre,
|
|
'descripcion' => $variant->descripcion,
|
|
'slug' => $catalogItem->slug,
|
|
'item_nombre' => "Entrada (Sector {$sector}, Fila {$row}, Asiento {$seat})",
|
|
'variant_attributes' => json_encode($attributes, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
|
|
'cantidad' => 1,
|
|
'precio_unitario' => 0,
|
|
'discount_total' => 0,
|
|
'tax_total' => 0,
|
|
'total' => 0,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
private function createTicketAndCommitStock(
|
|
int $purchaseId,
|
|
int $userId,
|
|
int $catalogItemId,
|
|
object $variant,
|
|
DateTimeInterface $now,
|
|
): void {
|
|
if (DB::table('tickets')
|
|
->where('source_purchase_id', $purchaseId)
|
|
->where('source_variant_id', $variant->id)
|
|
->exists()) {
|
|
return;
|
|
}
|
|
|
|
$inventory = DB::table('inventories')->where('id', $variant->inventory_id)->lockForUpdate()->first();
|
|
|
|
if ($inventory === null || $inventory->real_stock < 1 || $inventory->reserved_stock > 0) {
|
|
throw new RuntimeException("El asiento {$variant->descripcion} ya no está disponible.");
|
|
}
|
|
|
|
DB::table('inventories')->where('id', $inventory->id)->update([
|
|
'real_stock' => $inventory->real_stock - 1,
|
|
'sold_units' => $inventory->sold_units + 1,
|
|
]);
|
|
|
|
DB::table('stock_reservations')->insert([
|
|
'inventory_id' => $inventory->id,
|
|
'cart_item_id' => null,
|
|
'purchase_id' => $purchaseId,
|
|
'quantity' => 1,
|
|
'status' => 'committed',
|
|
'expires_at' => null,
|
|
'committed_at' => $now,
|
|
'released_at' => null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
DB::table('tickets')->insert([
|
|
'tenant_code' => self::TENANT_CODE,
|
|
'ticket' => (string) Str::uuid(),
|
|
'name' => null,
|
|
'description' => null,
|
|
'source_purchase_id' => $purchaseId,
|
|
'source_catalog_item_id' => $catalogItemId,
|
|
'source_variant_id' => $variant->id,
|
|
'used_at' => null,
|
|
'scanner_user_id' => null,
|
|
'user_id' => $userId,
|
|
]);
|
|
}
|
|
}
|