Compare commits
6 Commits
3cee9c28f8
...
7a19b3a97a
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a19b3a97a | |||
| a94ac7a473 | |||
| 161693509f | |||
| 93c99afb13 | |||
| 32d2b182c6 | |||
| 2632a80722 |
22
app/Domains/Auth/Services/AdminCredentialVerifier.php
Normal file
22
app/Domains/Auth/Services/AdminCredentialVerifier.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Auth\Services;
|
||||||
|
|
||||||
|
use App\Domains\Auth\Models\User;
|
||||||
|
use App\Domains\Authorization\Enums\RoleCode;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class AdminCredentialVerifier
|
||||||
|
{
|
||||||
|
public function verify(string $email, string $password): bool
|
||||||
|
{
|
||||||
|
$admin = User::query()
|
||||||
|
->where('email', mb_strtolower(trim($email)))
|
||||||
|
->where('rol_codigo', RoleCode::Admin->value)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $admin !== null
|
||||||
|
&& ! $admin->locked_until?->isFuture()
|
||||||
|
&& Hash::check($password, $admin->getAuthPassword());
|
||||||
|
}
|
||||||
|
}
|
||||||
417
app/Domains/Desfile/Services/InvitationPurchaseProvisioner.php
Normal file
417
app/Domains/Desfile/Services/InvitationPurchaseProvisioner.php
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
<?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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,20 @@ class TelepagosWebhookService
|
|||||||
$amount = $this->normalizeAmount($details['data']['amount'] ?? $details['amount'] ?? 0);
|
$amount = $this->normalizeAmount($details['data']['amount'] ?? $details['amount'] ?? 0);
|
||||||
$operationId = $details['data']['operation_id'] ?? $details['operation_id'] ?? null;
|
$operationId = $details['data']['operation_id'] ?? $details['operation_id'] ?? null;
|
||||||
|
|
||||||
|
$paymentData = [
|
||||||
|
'compra_id' => null,
|
||||||
|
'matched_purchase_ids' => null,
|
||||||
|
'cuit_buyer' => $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null,
|
||||||
|
'cvu_buyer' => $details['data']['buyer']['cvu'] ?? $details['buyer']['cvu'] ?? null,
|
||||||
|
'amount' => $amount,
|
||||||
|
'concept' => $details['data']['concept'] ?? $details['concept'] ?? null,
|
||||||
|
'operation' => $details['data']['operation'] ?? $details['operation'] ?? null,
|
||||||
|
'operation_id' => $details['data']['operation_id'] ?? $details['operation_id'] ?? null,
|
||||||
|
'transaction_id' => $details['data']['transaction_id'] ?? $details['transaction_id'] ?? null,
|
||||||
|
'qr_order_id' => $qrOrderId,
|
||||||
|
'link_id' => $details['data']['link_id'] ?? $details['link_id'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
$transferenciaOperationIds = [1, 3, 11];
|
$transferenciaOperationIds = [1, 3, 11];
|
||||||
$qrOperationIds = [31, 37, 47];
|
$qrOperationIds = [31, 37, 47];
|
||||||
|
|
||||||
@@ -68,12 +82,16 @@ class TelepagosWebhookService
|
|||||||
->where('payment_method', 'transfer')
|
->where('payment_method', 'transfer')
|
||||||
->where('total', $amount)
|
->where('total', $amount)
|
||||||
->latest()
|
->latest()
|
||||||
->limit(2)
|
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$paymentData['matched_purchase_ids'] = $purchases->pluck('id')->all();
|
||||||
$compra = $purchases->count() === 1 ? $purchases->first() : null;
|
$compra = $purchases->count() === 1 ? $purchases->first() : null;
|
||||||
|
|
||||||
if (! $compra) {
|
if (! $compra) {
|
||||||
|
if ($purchases->count() > 1) {
|
||||||
|
TelepagosPayment::create($paymentData);
|
||||||
|
}
|
||||||
|
|
||||||
Log::channel('telepagos')->warning('Telepagos webhook: Expected exactly one matching purchase.', [
|
Log::channel('telepagos')->warning('Telepagos webhook: Expected exactly one matching purchase.', [
|
||||||
'client_code' => $client->code,
|
'client_code' => $client->code,
|
||||||
'cashin_id' => $cashinId,
|
'cashin_id' => $cashinId,
|
||||||
@@ -163,18 +181,7 @@ class TelepagosWebhookService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$paymentData = [
|
$paymentData['compra_id'] = $compra->id;
|
||||||
'compra_id' => $compra->id,
|
|
||||||
'cuit_buyer' => $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null,
|
|
||||||
'cvu_buyer' => $details['data']['buyer']['cvu'] ?? $details['buyer']['cvu'] ?? null,
|
|
||||||
'amount' => $amount,
|
|
||||||
'concept' => $details['data']['concept'] ?? $details['concept'] ?? null,
|
|
||||||
'operation' => $details['data']['operation'] ?? $details['operation'] ?? null,
|
|
||||||
'operation_id' => $details['data']['operation_id'] ?? $details['operation_id'] ?? null,
|
|
||||||
'transaction_id' => $details['data']['transaction_id'] ?? $details['transaction_id'] ?? null,
|
|
||||||
'qr_order_id' => $qrOrderId,
|
|
||||||
'link_id' => $details['data']['link_id'] ?? $details['link_id'] ?? null,
|
|
||||||
];
|
|
||||||
|
|
||||||
DB::transaction(function () use ($compra, $paymentData) {
|
DB::transaction(function () use ($compra, $paymentData) {
|
||||||
TelepagosPayment::create($paymentData);
|
TelepagosPayment::create($paymentData);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'compra_id',
|
'compra_id',
|
||||||
|
'matched_purchase_ids',
|
||||||
'cuit_buyer',
|
'cuit_buyer',
|
||||||
'cvu_buyer',
|
'cvu_buyer',
|
||||||
'amount',
|
'amount',
|
||||||
@@ -29,6 +30,7 @@ class TelepagosPayment extends Model
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'compra_id' => 'integer',
|
'compra_id' => 'integer',
|
||||||
|
'matched_purchase_ids' => 'array',
|
||||||
'amount' => 'decimal:2',
|
'amount' => 'decimal:2',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
145
app/Domains/Purchase/Services/TenantTransactionResetService.php
Normal file
145
app/Domains/Purchase/Services/TenantTransactionResetService.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Purchase\Services;
|
||||||
|
|
||||||
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
class TenantTransactionResetService
|
||||||
|
{
|
||||||
|
/** @return array<string, int> */
|
||||||
|
public function preview(string $tenantCode): array
|
||||||
|
{
|
||||||
|
$this->ensureTenantExists($tenantCode);
|
||||||
|
$scope = $this->scope($tenantCode);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'users_preserved' => DB::table('users')->where('tenant_codigo', $tenantCode)->count(),
|
||||||
|
'purchases' => $scope['purchase_ids']->count(),
|
||||||
|
'purchase_items' => DB::table('compra_items')->whereIn('compra_id', $scope['purchase_ids'])->count(),
|
||||||
|
'telepagos_payments' => DB::table('telepagos_payments')->whereIn('compra_id', $scope['purchase_ids'])->count(),
|
||||||
|
'telepagos_qr' => DB::table('telepagos_qr')->whereIn('compra_id', $scope['purchase_ids'])->count(),
|
||||||
|
'carts' => $scope['cart_ids']->count(),
|
||||||
|
'cart_items' => $scope['cart_item_ids']->count(),
|
||||||
|
'tickets' => DB::table('tickets')->where('tenant_code', $tenantCode)->count(),
|
||||||
|
'stock_reservations' => $this->reservationQuery($scope)->count(),
|
||||||
|
'purchase_changes' => DB::table('value_changes')
|
||||||
|
->where('tenant_code', $tenantCode)
|
||||||
|
->where('trackable_type', Purchase::class)
|
||||||
|
->count(),
|
||||||
|
'inventories' => $scope['inventory_ids']->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, int> */
|
||||||
|
public function reset(string $tenantCode): array
|
||||||
|
{
|
||||||
|
$this->ensureTenantExists($tenantCode);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($tenantCode): array {
|
||||||
|
$scope = $this->scope($tenantCode);
|
||||||
|
$purchaseItems = DB::table('compra_items')->whereIn('compra_id', $scope['purchase_ids'])->count();
|
||||||
|
$cartItems = $scope['cart_item_ids']->count();
|
||||||
|
$telepagosPayments = DB::table('telepagos_payments')->whereIn('compra_id', $scope['purchase_ids'])->count();
|
||||||
|
$telepagosQr = DB::table('telepagos_qr')->whereIn('compra_id', $scope['purchase_ids'])->count();
|
||||||
|
$summary = [
|
||||||
|
'stock_reservations_deleted' => $this->reservationQuery($scope)->delete(),
|
||||||
|
'tickets_deleted' => DB::table('tickets')->where('tenant_code', $tenantCode)->delete(),
|
||||||
|
'purchase_changes_deleted' => DB::table('value_changes')
|
||||||
|
->where('tenant_code', $tenantCode)
|
||||||
|
->where('trackable_type', Purchase::class)
|
||||||
|
->delete(),
|
||||||
|
'purchases_deleted' => DB::table('compras')->whereIn('id', $scope['purchase_ids'])->delete(),
|
||||||
|
'purchase_items_deleted' => $purchaseItems,
|
||||||
|
'telepagos_payments_deleted' => $telepagosPayments,
|
||||||
|
'telepagos_qr_deleted' => $telepagosQr,
|
||||||
|
'carts_deleted' => DB::table('carritos')->whereIn('id', $scope['cart_ids'])->delete(),
|
||||||
|
'cart_items_deleted' => $cartItems,
|
||||||
|
'inventories_reset' => $scope['inventory_ids']->count(),
|
||||||
|
'users_preserved' => DB::table('users')->where('tenant_codigo', $tenantCode)->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
DB::table('inventories')
|
||||||
|
->whereIn('id', $scope['inventory_ids'])
|
||||||
|
->update([
|
||||||
|
'real_stock' => DB::raw('real_stock + sold_units'),
|
||||||
|
'reserved_stock' => 0,
|
||||||
|
'sold_units' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $summary;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureTenantExists(string $tenantCode): void
|
||||||
|
{
|
||||||
|
if (! DB::table('tenants')->where('codigo', $tenantCode)->exists()) {
|
||||||
|
throw new InvalidArgumentException("El tenant {$tenantCode} no existe.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* purchase_ids: Collection<int, int>,
|
||||||
|
* cart_ids: Collection<int, int>,
|
||||||
|
* cart_item_ids: Collection<int, int>,
|
||||||
|
* inventory_ids: Collection<int, int>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function scope(string $tenantCode): array
|
||||||
|
{
|
||||||
|
$catalogItemIds = DB::table('catalog_items')
|
||||||
|
->where('tenant_code', $tenantCode)
|
||||||
|
->pluck('id');
|
||||||
|
$purchaseIds = DB::table('compras')
|
||||||
|
->where('tenant_codigo', $tenantCode)
|
||||||
|
->pluck('id');
|
||||||
|
$cartIds = DB::table('carritos')
|
||||||
|
->where('tenant_codigo', $tenantCode)
|
||||||
|
->pluck('id');
|
||||||
|
$cartItemIds = DB::table('carrito_items')
|
||||||
|
->whereIn('cart_id', $cartIds)
|
||||||
|
->pluck('id');
|
||||||
|
$inventoryIds = DB::table('variantes')
|
||||||
|
->whereIn('catalog_item_id', $catalogItemIds)
|
||||||
|
->whereNotNull('inventory_id')
|
||||||
|
->pluck('inventory_id')
|
||||||
|
->merge(
|
||||||
|
DB::table('catalog_items')
|
||||||
|
->whereIn('id', $catalogItemIds)
|
||||||
|
->whereNotNull('inventory_id')
|
||||||
|
->pluck('inventory_id'),
|
||||||
|
)
|
||||||
|
->map(fn ($id): int => (int) $id)
|
||||||
|
->unique()
|
||||||
|
->values();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'purchase_ids' => $purchaseIds->map(fn ($id): int => (int) $id),
|
||||||
|
'cart_ids' => $cartIds->map(fn ($id): int => (int) $id),
|
||||||
|
'cart_item_ids' => $cartItemIds->map(fn ($id): int => (int) $id),
|
||||||
|
'inventory_ids' => $inventoryIds,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{
|
||||||
|
* purchase_ids: Collection<int, int>,
|
||||||
|
* cart_ids: Collection<int, int>,
|
||||||
|
* cart_item_ids: Collection<int, int>,
|
||||||
|
* inventory_ids: Collection<int, int>
|
||||||
|
* } $scope
|
||||||
|
*/
|
||||||
|
private function reservationQuery(array $scope): Builder
|
||||||
|
{
|
||||||
|
return DB::table('stock_reservations')
|
||||||
|
->where(function (Builder $query) use ($scope): void {
|
||||||
|
$query->whereIn('inventory_id', $scope['inventory_ids'])
|
||||||
|
->orWhereIn('purchase_id', $scope['purchase_ids'])
|
||||||
|
->orWhereIn('cart_item_id', $scope['cart_item_ids']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Desfile\Services\InvitationPurchaseProvisioner;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
app(InvitationPurchaseProvisioner::class)->provision();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Intentionally irreversible: issued invitation tickets may already be used.
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?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('telepagos_payments', function (Blueprint $table) {
|
||||||
|
$table->foreignId('compra_id')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('telepagos_payments', function (Blueprint $table) {
|
||||||
|
$table->foreignId('compra_id')->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?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('telepagos_payments', function (Blueprint $table) {
|
||||||
|
$table->json('matched_purchase_ids')->nullable()->after('compra_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('telepagos_payments', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('matched_purchase_ids');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ use App\Domains\Catalog\Models\CatalogItem;
|
|||||||
use App\Domains\Catalog\Models\FeaturedGroup;
|
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||||
use App\Domains\Catalog\Services\CatalogService;
|
use App\Domains\Catalog\Services\CatalogService;
|
||||||
use App\Domains\Client\Models\Client;
|
use App\Domains\Client\Models\Client;
|
||||||
|
use App\Domains\Desfile\Services\InvitationPurchaseProvisioner;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Tenant\Services\TenantService;
|
use App\Domains\Tenant\Services\TenantService;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
@@ -28,6 +29,7 @@ class DesfilePuraTendenciaSeeder extends Seeder
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TenantService $tenantService,
|
private readonly TenantService $tenantService,
|
||||||
private readonly CatalogService $catalogService,
|
private readonly CatalogService $catalogService,
|
||||||
|
private readonly InvitationPurchaseProvisioner $invitationPurchaseProvisioner,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
@@ -41,15 +43,15 @@ class DesfilePuraTendenciaSeeder extends Seeder
|
|||||||
|
|
||||||
if ($tenant) {
|
if ($tenant) {
|
||||||
$tenant->update(['client_id' => $client->id]);
|
$tenant->update(['client_id' => $client->id]);
|
||||||
|
} else {
|
||||||
return;
|
DB::transaction(function () use ($client): void {
|
||||||
|
$this->createTenant($client);
|
||||||
|
$this->createEventDate();
|
||||||
|
$this->createEntryCatalog();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
DB::transaction(function () use ($client): void {
|
$this->invitationPurchaseProvisioner->provision();
|
||||||
$this->createTenant($client);
|
|
||||||
$this->createEventDate();
|
|
||||||
$this->createEntryCatalog();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createTenant(Client $client): void
|
private function createTenant(Client $client): void
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Auth\Services\AdminCredentialVerifier;
|
||||||
use App\Domains\Catalog\Services\ExpireStockReservationsService;
|
use App\Domains\Catalog\Services\ExpireStockReservationsService;
|
||||||
|
use App\Domains\Purchase\Services\TenantTransactionResetService;
|
||||||
use Illuminate\Foundation\Inspiring;
|
use Illuminate\Foundation\Inspiring;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Illuminate\Support\Facades\Schedule;
|
use Illuminate\Support\Facades\Schedule;
|
||||||
@@ -19,3 +21,64 @@ Artisan::command('reservations:expire', function (): void {
|
|||||||
Schedule::command('reservations:expire')
|
Schedule::command('reservations:expire')
|
||||||
->everyMinute()
|
->everyMinute()
|
||||||
->withoutOverlapping();
|
->withoutOverlapping();
|
||||||
|
|
||||||
|
Artisan::command(
|
||||||
|
'tenants:reset-transactions
|
||||||
|
{tenant : Código del tenant que se limpiará}
|
||||||
|
{--dry-run : Mostrar el alcance sin modificar datos}
|
||||||
|
{--force : Omitir la confirmación interactiva}',
|
||||||
|
function (
|
||||||
|
TenantTransactionResetService $resetService,
|
||||||
|
AdminCredentialVerifier $adminCredentialVerifier,
|
||||||
|
): int {
|
||||||
|
$tenantCode = (string) $this->argument('tenant');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$preview = $resetService->preview($tenantCode);
|
||||||
|
} catch (InvalidArgumentException $exception) {
|
||||||
|
$this->error($exception->getMessage());
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->warn("Se eliminarán los datos transaccionales de: {$tenantCode}");
|
||||||
|
$this->table(
|
||||||
|
['Dato', 'Cantidad'],
|
||||||
|
collect($preview)->map(fn (int $count, string $label): array => [$label, $count])->values(),
|
||||||
|
);
|
||||||
|
$this->info('Los usuarios, el catálogo, los eventos y la configuración se conservarán.');
|
||||||
|
|
||||||
|
if ($this->option('dry-run')) {
|
||||||
|
$this->comment('Vista previa finalizada; no se modificaron datos.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->option('force') && ! $this->confirm('¿Confirmás esta limpieza irreversible?')) {
|
||||||
|
$this->comment('Operación cancelada.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->warn('Autorización administrativa requerida.');
|
||||||
|
$adminEmail = (string) $this->ask('Email del administrador');
|
||||||
|
$adminPassword = (string) $this->secret('Contraseña del administrador');
|
||||||
|
|
||||||
|
if (! $adminCredentialVerifier->verify($adminEmail, $adminPassword)) {
|
||||||
|
$this->error('Las credenciales no son válidas o el usuario no posee el rol admin.');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = $resetService->reset($tenantCode);
|
||||||
|
|
||||||
|
$this->info('Limpieza completada correctamente.');
|
||||||
|
$this->table(
|
||||||
|
['Resultado', 'Cantidad'],
|
||||||
|
collect($summary)->map(fn (int $count, string $label): array => [$label, $count])->values(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
},
|
||||||
|
)->purpose('Delete tenant sales, carts, tickets and reservations while preserving users and catalog');
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use App\Domains\Catalog\Models\Variant;
|
|||||||
use App\Domains\Integration\Models\ClientIntegration;
|
use App\Domains\Integration\Models\ClientIntegration;
|
||||||
use App\Domains\Integration\Models\Integration;
|
use App\Domains\Integration\Models\Integration;
|
||||||
use App\Domains\Purchase\Models\Purchase;
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
|
use App\Domains\Purchase\Models\TelepagosPayment;
|
||||||
use App\Domains\Purchase\Services\CheckoutService;
|
use App\Domains\Purchase\Services\CheckoutService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@@ -261,6 +262,84 @@ class TelepagosWebhookTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_transfer_webhook_records_unmatched_payment_when_multiple_purchases_match(): void
|
||||||
|
{
|
||||||
|
$tenant = $this->createTenant('ambiguous', 'Ambiguous', 'ambiguous.com.ar');
|
||||||
|
$this->configureTelepagosIntegration($tenant);
|
||||||
|
|
||||||
|
$variant = $this->createVariantForTenant('ambiguous', 10, '50.00');
|
||||||
|
$firstPurchase = $this->createPendingTransferPurchase(
|
||||||
|
$tenant,
|
||||||
|
User::factory()->create()->id,
|
||||||
|
$variant->id,
|
||||||
|
1,
|
||||||
|
'12345678',
|
||||||
|
);
|
||||||
|
$secondPurchase = $this->createPendingTransferPurchase(
|
||||||
|
$tenant,
|
||||||
|
User::factory()->create()->id,
|
||||||
|
$variant->id,
|
||||||
|
1,
|
||||||
|
'12345678',
|
||||||
|
);
|
||||||
|
$thirdPurchase = $this->createPendingTransferPurchase(
|
||||||
|
$tenant,
|
||||||
|
User::factory()->create()->id,
|
||||||
|
$variant->id,
|
||||||
|
1,
|
||||||
|
'12345678',
|
||||||
|
);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
|
||||||
|
'status' => 'ok',
|
||||||
|
'token' => 'test-token',
|
||||||
|
'expires_at' => now()->addHour()->toIso8601String(),
|
||||||
|
]),
|
||||||
|
'https://api.telepagos.com.ar/v2/payment/cashin/ambiguous' => Http::response([
|
||||||
|
'status' => 'ok',
|
||||||
|
'data' => [
|
||||||
|
'amount' => 50,
|
||||||
|
'operation_id' => 1,
|
||||||
|
'transaction_id' => 'tx-ambiguous',
|
||||||
|
'buyer' => [
|
||||||
|
'cuit' => '20123456789',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->postJson('/api/webhooks/telepagos/ambiguous', [
|
||||||
|
'id' => 'ambiguous',
|
||||||
|
])->assertOk()->assertJsonPath('status', 'success');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('telepagos_payments', [
|
||||||
|
'compra_id' => null,
|
||||||
|
'amount' => 50,
|
||||||
|
'operation_id' => 1,
|
||||||
|
'transaction_id' => 'tx-ambiguous',
|
||||||
|
]);
|
||||||
|
$payment = TelepagosPayment::query()
|
||||||
|
->where('transaction_id', 'tx-ambiguous')
|
||||||
|
->firstOrFail();
|
||||||
|
$this->assertEqualsCanonicalizing(
|
||||||
|
[$firstPurchase->id, $secondPurchase->id, $thirdPurchase->id],
|
||||||
|
$payment->matched_purchase_ids,
|
||||||
|
);
|
||||||
|
$this->assertDatabaseHas('compras', [
|
||||||
|
'id' => $firstPurchase->id,
|
||||||
|
'status' => Purchase::STATUS_PENDING_PAYMENT,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('compras', [
|
||||||
|
'id' => $secondPurchase->id,
|
||||||
|
'status' => Purchase::STATUS_PENDING_PAYMENT,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('compras', [
|
||||||
|
'id' => $thirdPurchase->id,
|
||||||
|
'status' => Purchase::STATUS_PENDING_PAYMENT,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_webhook_confirms_a_purchase_with_tickets_enabled(): void
|
public function test_webhook_confirms_a_purchase_with_tickets_enabled(): void
|
||||||
{
|
{
|
||||||
$tenant = $this->createTenant('expired-ticket', 'Expired Ticket', 'expired-ticket.com.ar');
|
$tenant = $this->createTenant('expired-ticket', 'Expired Ticket', 'expired-ticket.com.ar');
|
||||||
|
|||||||
@@ -216,12 +216,18 @@ class DesfilePuraTendenciaSeederTest extends TestCase
|
|||||||
->whereIn('variant_id', (clone $variants)->pluck('id'))
|
->whereIn('variant_id', (clone $variants)->pluck('id'))
|
||||||
->count());
|
->count());
|
||||||
$this->assertSame(330, (clone $variants)->whereNull('event_date_id')->count());
|
$this->assertSame(330, (clone $variants)->whereNull('event_date_id')->count());
|
||||||
$this->assertSame(330, DB::table('inventories')
|
$this->assertSame(284, DB::table('inventories')
|
||||||
->whereIn('id', (clone $variants)->pluck('inventory_id'))
|
->whereIn('id', (clone $variants)->pluck('inventory_id'))
|
||||||
->where('real_stock', 1)
|
->where('real_stock', 1)
|
||||||
->where('reserved_stock', 0)
|
->where('reserved_stock', 0)
|
||||||
->where('sold_units', 0)
|
->where('sold_units', 0)
|
||||||
->count());
|
->count());
|
||||||
|
$this->assertSame(46, DB::table('inventories')
|
||||||
|
->whereIn('id', (clone $variants)->pluck('inventory_id'))
|
||||||
|
->where('real_stock', 0)
|
||||||
|
->where('reserved_stock', 0)
|
||||||
|
->where('sold_units', 1)
|
||||||
|
->count());
|
||||||
$this->assertDatabaseHas('variantes', [
|
$this->assertDatabaseHas('variantes', [
|
||||||
'catalog_item_id' => $catalogItem->id,
|
'catalog_item_id' => $catalogItem->id,
|
||||||
'descripcion' => 'Sector A - Fila 1 - Asiento 17 - VIP + LUNCH',
|
'descripcion' => 'Sector A - Fila 1 - Asiento 17 - VIP + LUNCH',
|
||||||
@@ -247,7 +253,7 @@ class DesfilePuraTendenciaSeederTest extends TestCase
|
|||||||
'sector' => ['B', 'D'],
|
'sector' => ['B', 'D'],
|
||||||
'asiento' => ['17'],
|
'asiento' => ['17'],
|
||||||
]));
|
]));
|
||||||
$this->assertSame(66, $this->variantCountForSelection($catalogItem->id, [
|
$this->assertSame(50, $this->variantCountForSelection($catalogItem->id, [
|
||||||
'tipo' => ['VIP + LUNCH'],
|
'tipo' => ['VIP + LUNCH'],
|
||||||
'fila' => ['1'],
|
'fila' => ['1'],
|
||||||
]));
|
]));
|
||||||
@@ -255,6 +261,71 @@ class DesfilePuraTendenciaSeederTest extends TestCase
|
|||||||
'tipo' => ['NORMAL'],
|
'tipo' => ['NORMAL'],
|
||||||
'fila' => ['5'],
|
'fila' => ['5'],
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
$this->assertInvitationPurchase($catalogItem->id);
|
||||||
|
|
||||||
|
$this->seed(DesfilePuraTendenciaSeeder::class);
|
||||||
|
|
||||||
|
$this->assertInvitationPurchase($catalogItem->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertInvitationPurchase(int $catalogItemId): void
|
||||||
|
{
|
||||||
|
$user = DB::table('users')->where('email', 'invitados@puratendencia.com')->sole();
|
||||||
|
|
||||||
|
$this->assertSame('user', $user->rol_codigo);
|
||||||
|
$this->assertSame('desfile_pura_tendencia', $user->tenant_codigo);
|
||||||
|
|
||||||
|
$purchase = DB::table('compras')
|
||||||
|
->where('tenant_codigo', 'desfile_pura_tendencia')
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->where('payment_method', 'invitation')
|
||||||
|
->sole();
|
||||||
|
|
||||||
|
$this->assertSame('paid', $purchase->status);
|
||||||
|
$this->assertEquals(0, $purchase->total);
|
||||||
|
|
||||||
|
$items = DB::table('compra_items')->where('compra_id', $purchase->id);
|
||||||
|
|
||||||
|
$this->assertSame(46, (clone $items)->count());
|
||||||
|
$this->assertSame(46, (clone $items)
|
||||||
|
->where('precio_unitario', 0)
|
||||||
|
->where('discount_total', 0)
|
||||||
|
->where('tax_total', 0)
|
||||||
|
->where('total', 0)
|
||||||
|
->count());
|
||||||
|
$this->assertSame(46, DB::table('tickets')
|
||||||
|
->where('source_purchase_id', $purchase->id)
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->where('source_catalog_item_id', $catalogItemId)
|
||||||
|
->count());
|
||||||
|
$this->assertSame(46, DB::table('stock_reservations')
|
||||||
|
->where('purchase_id', $purchase->id)
|
||||||
|
->where('status', 'committed')
|
||||||
|
->count());
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
['sector' => 'A', 'fila' => '1', 'tipo' => 'NORMAL', 'count' => 16],
|
||||||
|
['sector' => 'A', 'fila' => '3', 'tipo' => 'NORMAL', 'count' => 14],
|
||||||
|
['sector' => 'C', 'fila' => '1', 'tipo' => 'VIP + LUNCH', 'count' => 16],
|
||||||
|
] as $allocation) {
|
||||||
|
$allocatedVariants = DB::table('variantes')
|
||||||
|
->where('catalog_item_id', $catalogItemId)
|
||||||
|
->whereIn('id', (clone $items)->pluck('source_variant_id'));
|
||||||
|
|
||||||
|
foreach (['sector', 'fila', 'tipo'] as $code) {
|
||||||
|
$allocatedVariants->whereExists(fn ($query) => $query
|
||||||
|
->selectRaw('1')
|
||||||
|
->from('variant_values')
|
||||||
|
->join('item_attributes', 'item_attributes.id', '=', 'variant_values.item_attribute_id')
|
||||||
|
->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id')
|
||||||
|
->whereColumn('variant_values.variant_id', 'variantes.id')
|
||||||
|
->where('attribute.codigo', $code)
|
||||||
|
->where('variant_values.value', $allocation[$code]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame($allocation['count'], $allocatedVariants->count());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, list<string>> $selection */
|
/** @param array<string, list<string>> $selection */
|
||||||
|
|||||||
Reference in New Issue
Block a user