refactor(checkout): materialize purchase item snapshots at start

This commit is contained in:
2026-08-21 11:03:39 -03:00
parent 3a2dc8b7e0
commit 4e7e42d16e
9 changed files with 74 additions and 325 deletions

View File

@@ -154,12 +154,7 @@ class Purchase extends Model
return (float) $this->getRelation('items')->sum('total'); return (float) $this->getRelation('items')->sum('total');
} }
$itemsTotal = (float) $this->items()->sum('total'); return (float) $this->items()->sum('total');
if ($itemsTotal > 0 || $this->items()->exists()) {
return $itemsTotal;
}
return (float) ($this->cart?->getTotalAmount() ?? $this->total ?? 0);
} }
protected function valueChangeTenantCode(): string protected function valueChangeTenantCode(): string

View File

@@ -2,16 +2,12 @@
namespace App\Domains\Purchase\Resources; namespace App\Domains\Purchase\Resources;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Purchase\Models\PurchaseItem; use App\Domains\Purchase\Models\PurchaseItem;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin PurchaseItem|CartItem */ /** @mixin PurchaseItem */
class PurchaseItemResource extends JsonResource class PurchaseItemResource extends JsonResource
{ {
/** @return array<string, mixed> */ /** @return array<string, mixed> */
@@ -20,158 +16,29 @@ class PurchaseItemResource extends JsonResource
$tenant = $request->route('tenant'); $tenant = $request->route('tenant');
$displayImage = ! $tenant instanceof Tenant || $tenant->display_cart_item_images; $displayImage = ! $tenant instanceof Tenant || $tenant->display_cart_item_images;
if ($this->resource instanceof PurchaseItem) {
$imageUrl = $displayImage
? $this->imageAttachment?->getTemporaryUrl(1440)
: null;
$attributes = $this->variant_attributes ?? [];
$catalogItem = $this->relationLoaded('sourceCatalogItem')
? $this->sourceCatalogItem
: null;
$includeVariants = $tenant instanceof Tenant
&& $tenant->checkout_editing_policy->allowsVariantChanges()
&& $catalogItem !== null
&& $catalogItem->relationLoaded('variants');
return [
'id' => $this->id,
'quantity' => (int) $this->cantidad,
'unit_price' => $this->formatMoney($this->precio_unitario),
'line_total' => $this->formatMoney($this->total),
'source_catalog_item_id' => $this->source_catalog_item_id,
'source_variant_id' => $this->source_variant_id,
'item_details' => [
'nombre' => $this->item_nombre,
'descripcion' => $this->descripcion,
'slug' => $this->slug,
'imagen' => $imageUrl,
'attributes' => $attributes,
],
'variants' => $this->when(
$includeVariants,
fn () => $catalogItem
->visibleVariants($this->source_variant_id)
->map(fn (Variant $variant): array => $this->variantData($catalogItem, $variant))
->values(),
),
];
}
$selectedItem = $this->selectedItem();
$catalogItem = $this->catalogItem;
$variant = $this->variant;
$quantity = (int) ($this->cantidad ?? 0);
$unitPrice = $this->resolveUnitPrice($selectedItem);
$lineTotal = $unitPrice * $quantity;
$imageUrl = $displayImage $imageUrl = $displayImage
? $this->resolveImageUrl($selectedItem, $catalogItem) ? $this->imageAttachment?->getTemporaryUrl(1440)
: null; : null;
$includeVariants = $tenant instanceof Tenant
&& $tenant->checkout_editing_policy->allowsVariantChanges()
&& $catalogItem?->relationLoaded('variants');
return [ return [
'id' => $this->id, 'id' => $this->id,
'quantity' => $quantity, 'quantity' => (int) $this->cantidad,
'unit_price' => $this->formatMoney($unitPrice), 'unit_price' => $this->formatMoney($this->precio_unitario),
'line_total' => $this->formatMoney($lineTotal), 'line_total' => $this->formatMoney($this->total),
'source_catalog_item_id' => $this->catalog_item_id, 'source_catalog_item_id' => $this->source_catalog_item_id,
'source_variant_id' => $this->variant_id, 'source_variant_id' => $this->source_variant_id,
'item_details' => $selectedItem === null ? null : [ 'item_details' => [
'nombre' => $selectedItem->getName(), 'nombre' => $this->item_nombre,
'descripcion' => $selectedItem->getDescription(), 'descripcion' => $this->descripcion,
'slug' => $catalogItem?->slug, 'slug' => $this->slug,
'imagen' => $imageUrl, 'imagen' => $imageUrl,
'attributes' => $variant === null ? [] : $this->resolveAttributes($variant), 'attributes' => $this->variant_attributes ?? [],
], ],
'variants' => $this->when(
$includeVariants,
fn () => $catalogItem
->visibleVariants($this->variant_id)
->map(fn (Variant $availableVariant): array => $this->variantData(
$catalogItem,
$availableVariant,
))
->values(),
),
]; ];
} }
private function resolveUnitPrice(CatalogItem|Variant|null $selectedItem): float
{
return (float) ($selectedItem?->getPrice() ?? 0);
}
private function resolveImageUrl(
CatalogItem|Variant|null $selectedItem,
?CatalogItem $catalogItem,
): ?string {
$attachment = $selectedItem?->relationLoaded('attachments')
? $selectedItem->attachments->first()
: null;
if ($attachment === null && $catalogItem?->relationLoaded('attachments')) {
$attachment = $catalogItem->attachments->first();
}
return $attachment?->getTemporaryUrl(1440);
}
/** @return array<int, array{name: string, value: mixed}> */
private function resolveAttributes(Variant $variant): array
{
if (! $variant->relationLoaded('definitions')) {
return [];
}
$attributes = $variant->definitions
->groupBy('item_attribute_id')
->map(function ($definitions): array {
$itemAttribute = $definitions->first()?->itemAttribute;
$values = $definitions->pluck('value')->values();
return [
'name' => (string) ($itemAttribute?->attribute?->nombre ?? ''),
'value' => $itemAttribute?->allow_multi_select
? $values->all()
: $values->first(),
];
})
->filter(fn (array $attribute): bool => $attribute['name'] !== '' || $attribute['value'] !== null)
->values();
$eventDates = $variant->relationLoaded('eventDates')
? $variant->selectedEventDates()
: collect();
if ($eventDates->isNotEmpty()) {
$attributes->prepend([
'name' => 'Fecha',
'value' => $eventDates
->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))
->values()
->all(),
]);
}
return $attributes->all();
}
private function formatMoney(float|int|string|null $amount): string private function formatMoney(float|int|string|null $amount): string
{ {
return number_format((float) ($amount ?? 0), 2, '.', ''); return number_format((float) ($amount ?? 0), 2, '.', '');
} }
/** @return array<string, mixed> */
private function variantData(CatalogItem $catalogItem, Variant $variant): array
{
return [
'id' => $variant->id,
'precio' => $this->formatMoney($variant->precio ?? $catalogItem->precio),
'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited
? null
: $variant->inventory->availableStock(),
'values' => $variant->selectorOptions($catalogItem->itemAttributes),
];
}
} }

View File

@@ -2,7 +2,6 @@
namespace App\Domains\Purchase\Resources; namespace App\Domains\Purchase\Resources;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Models\PurchaseItem; use App\Domains\Purchase\Models\PurchaseItem;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -18,28 +17,16 @@ class PurchaseResource extends JsonResource
*/ */
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
$purchaseItems = $this->resource->relationLoaded('items') $items = $this->resource->relationLoaded('items')
? $this->resource->getRelation('items') ? $this->resource->getRelation('items')
: collect(); : collect();
$cartItems = $this->resource->relationLoaded('cart')
&& $this->resource->getRelation('cart')?->relationLoaded('items')
? $this->resource->getRelation('cart')->getRelation('items')
: null;
$usesCartItems = in_array($this->status, [
Purchase::STATUS_CREATED,
Purchase::STATUS_PENDING_PAYMENT,
], true) && $cartItems !== null;
$items = $usesCartItems ? $cartItems : $purchaseItems;
$itemsSource = $usesCartItems
? 'cart'
: ($purchaseItems->isNotEmpty() ? 'purchase' : null);
$ticketsCount = array_key_exists('tickets_count', $this->resource->getAttributes()) $ticketsCount = array_key_exists('tickets_count', $this->resource->getAttributes())
? (int) $this->resource->getAttribute('tickets_count') ? (int) $this->resource->getAttribute('tickets_count')
: null; : null;
$subtotal = $items->isNotEmpty() $subtotal = $items->isNotEmpty()
? $items->reduce( ? $items->reduce(
fn (float $carry, PurchaseItem|CartItem $item): float => $carry + $this->resolveItemSubtotal($item), fn (float $carry, PurchaseItem $item): float => $carry + $this->resolveItemSubtotal($item),
0.0, 0.0,
) )
: (float) ($this->total ?? 0); : (float) ($this->total ?? 0);
@@ -48,7 +35,7 @@ class PurchaseResource extends JsonResource
? (float) $this->total ? (float) $this->total
: ($items->isNotEmpty() : ($items->isNotEmpty()
? $items->reduce( ? $items->reduce(
fn (float $carry, PurchaseItem|CartItem $item): float => $carry + $this->resolveItemTotal($item), fn (float $carry, PurchaseItem $item): float => $carry + $this->resolveItemTotal($item),
0.0, 0.0,
) )
: (float) ($this->total ?? 0)); : (float) ($this->total ?? 0));
@@ -67,7 +54,7 @@ class PurchaseResource extends JsonResource
'telefono' => $this->telefono, 'telefono' => $this->telefono,
'nombre_apellido' => $this->nombre_apellido, 'nombre_apellido' => $this->nombre_apellido,
'email' => $this->email, 'email' => $this->email,
'items_source' => $itemsSource, 'items_source' => $items->isNotEmpty() ? 'purchase' : null,
'items' => PurchaseItemResource::collection($items), 'items' => PurchaseItemResource::collection($items),
'tickets_count' => $this->when($ticketsCount !== null, $ticketsCount), 'tickets_count' => $this->when($ticketsCount !== null, $ticketsCount),
'has_generated_tickets' => $this->when($ticketsCount !== null, $ticketsCount > 0), 'has_generated_tickets' => $this->when($ticketsCount !== null, $ticketsCount > 0),
@@ -76,21 +63,13 @@ class PurchaseResource extends JsonResource
]; ];
} }
protected function resolveItemSubtotal(PurchaseItem|CartItem $item): float protected function resolveItemSubtotal(PurchaseItem $item): float
{ {
if ($item instanceof CartItem) {
return (float) ($item->selectedItem()?->getPrice() ?? 0) * $item->cantidad;
}
return (float) $item->precio_unitario * $item->cantidad; return (float) $item->precio_unitario * $item->cantidad;
} }
protected function resolveItemTotal(PurchaseItem|CartItem $item): float protected function resolveItemTotal(PurchaseItem $item): float
{ {
if ($item instanceof CartItem) {
return $this->resolveItemSubtotal($item);
}
return (float) ($item->total ?? 0); return (float) ($item->total ?? 0);
} }

View File

@@ -15,7 +15,6 @@ class CompleteCheckoutService
public function __construct( public function __construct(
private readonly StockReservationService $reservations, private readonly StockReservationService $reservations,
private readonly SourceCartService $sourceCart, private readonly SourceCartService $sourceCart,
private readonly PurchaseItemSnapshotFactory $snapshots,
private readonly PurchaseStateGuard $purchaseState, private readonly PurchaseStateGuard $purchaseState,
) {} ) {}
@@ -90,18 +89,17 @@ class CompleteCheckoutService
]); ]);
} }
if ($purchase->items()->exists()) { if (! $purchase->items()->exists()) {
$cart = $purchase->cart()->withTrashed()->lockForUpdate()->first();
if ($cart?->status === 'converted' && $cart->trashed()) {
return;
}
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'items' => __('api.purchase.inconsistent_reservation'), 'items' => __('api.purchase.inconsistent_reservation'),
]); ]);
} }
$cart = $purchase->cart()->lockForUpdate()->first(); $cart = $purchase->cart()->withTrashed()->lockForUpdate()->first();
if ($cart?->status === 'converted' && $cart->trashed()) {
return;
}
if ($cart === null || $cart->status !== 'checkout') { if ($cart === null || $cart->status !== 'checkout') {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'items' => __('api.purchase.inconsistent_reservation'), 'items' => __('api.purchase.inconsistent_reservation'),
@@ -115,10 +113,32 @@ class CompleteCheckoutService
]); ]);
} }
$snapshotQuantities = $purchase->items()
->lockForUpdate()
->get(['source_catalog_item_id', 'source_variant_id', 'cantidad'])
->groupBy(fn ($item): string => $this->itemKey(
(int) $item->source_catalog_item_id,
$item->source_variant_id === null ? null : (int) $item->source_variant_id,
))
->map(fn (Collection $items): int => (int) $items->sum('cantidad'))
->sortKeys()
->all();
$cartQuantities = $cartItems
->groupBy(fn (CartItem $item): string => $this->itemKey(
(int) $item->catalog_item_id,
$item->variant_id === null ? null : (int) $item->variant_id,
))
->map(fn (Collection $items): int => (int) $items->sum('cantidad'))
->sortKeys()
->all();
if ($snapshotQuantities !== $cartQuantities) {
throw ValidationException::withMessages([
'items' => __('api.purchase.inconsistent_reservation'),
]);
}
$this->loadCartItems($cartItems); $this->loadCartItems($cartItems);
$purchase->items()->createMany(
$this->snapshots->fromCartItems($cartItems),
);
foreach ($cartItems as $cartItem) { foreach ($cartItems as $cartItem) {
$selection = $cartItem->selectedItem(); $selection = $cartItem->selectedItem();
@@ -159,16 +179,12 @@ class CompleteCheckoutService
private function loadPurchase(Purchase $purchase): Purchase private function loadPurchase(Purchase $purchase): Purchase
{ {
return $purchase->load([ return $purchase->load(['items.imageAttachment']);
'items.imageAttachment', }
'cart.items.catalogItem.inventory',
'cart.items.catalogItem.attachments', private function itemKey(int $catalogItemId, ?int $variantId): string
'cart.items.variant.inventory', {
'cart.items.variant.attachments', return $catalogItemId.':'.($variantId ?? 'none');
'cart.items.variant.definitions.itemAttribute.attribute',
'cart.items.variant.eventDates',
'cart.items.variant.eventDate',
]);
} }
/** @param Collection<int, CartItem> $cartItems */ /** @param Collection<int, CartItem> $cartItems */

View File

@@ -8,51 +8,6 @@ class PurchaseResponseLoader
{ {
public function load(Purchase $purchase): Purchase public function load(Purchase $purchase): Purchase
{ {
$purchase->load(['tenant', 'items.imageAttachment']); return $purchase->load(['tenant', 'items.imageAttachment']);
if (
$purchase->cart_id !== null
&& (
in_array($purchase->status, [
Purchase::STATUS_CREATED,
Purchase::STATUS_PENDING_PAYMENT,
], true)
|| $purchase->items->isEmpty()
)
) {
$purchase->load([
'cart.items.catalogItem.inventory',
'cart.items.catalogItem.attachments',
'cart.items.variant.inventory',
'cart.items.variant.attachments',
'cart.items.variant.catalogItem',
'cart.items.variant.definitions.itemAttribute.attribute',
'cart.items.variant.eventDates',
'cart.items.variant.eventDate',
]);
}
if (! $purchase->tenant->checkout_editing_policy->allowsVariantChanges()) {
return $purchase;
}
$purchase->load([
'items.sourceCatalogItem.itemAttributes.attribute',
'items.sourceCatalogItem.variants' => fn ($query) => $query->orderBy('id'),
'items.sourceCatalogItem.variants.inventory',
'items.sourceCatalogItem.variants.definitions' => fn ($query) => $query->orderBy('id'),
'items.sourceCatalogItem.variants.definitions.itemAttribute.attribute.options',
'items.sourceCatalogItem.variants.eventDates',
'items.sourceCatalogItem.variants.eventDate',
'cart.items.catalogItem.itemAttributes.attribute',
'cart.items.catalogItem.variants' => fn ($query) => $query->orderBy('id'),
'cart.items.catalogItem.variants.inventory',
'cart.items.catalogItem.variants.definitions' => fn ($query) => $query->orderBy('id'),
'cart.items.catalogItem.variants.definitions.itemAttribute.attribute.options',
'cart.items.catalogItem.variants.eventDates',
'cart.items.catalogItem.variants.eventDate',
]);
return $purchase;
} }
} }

View File

@@ -96,12 +96,6 @@ class ReleaseCheckoutService
return $this->loadPurchase($purchase); return $this->loadPurchase($purchase);
} }
if ($purchase->items()->exists()) {
throw ValidationException::withMessages([
'items' => __('api.purchase.inconsistent_reservation'),
]);
}
$reservationReturnedToCart = $restoreCart && $this->sourceCart->restore($purchase); $reservationReturnedToCart = $restoreCart && $this->sourceCart->restore($purchase);
$this->releaseCartReservations($purchase, $reservationReturnedToCart, $targetStatus); $this->releaseCartReservations($purchase, $reservationReturnedToCart, $targetStatus);
@@ -181,15 +175,6 @@ class ReleaseCheckoutService
private function loadPurchase(Purchase $purchase): Purchase private function loadPurchase(Purchase $purchase): Purchase
{ {
return $purchase->load([ return $purchase->load(['items.imageAttachment']);
'items.imageAttachment',
'cart.items.catalogItem.inventory',
'cart.items.catalogItem.attachments',
'cart.items.variant.inventory',
'cart.items.variant.attachments',
'cart.items.variant.definitions.itemAttribute.attribute',
'cart.items.variant.eventDates',
'cart.items.variant.eventDate',
]);
} }
} }

View File

@@ -26,6 +26,7 @@ class StartCheckoutService
private readonly CatalogSelectionResolver $selections, private readonly CatalogSelectionResolver $selections,
private readonly InsufficientStockMessageBuilder $stockMessages, private readonly InsufficientStockMessageBuilder $stockMessages,
private readonly PurchaseResponseLoader $responses, private readonly PurchaseResponseLoader $responses,
private readonly PurchaseItemSnapshotFactory $snapshots,
) {} ) {}
/** @param array<string, mixed> $purchaseData */ /** @param array<string, mixed> $purchaseData */
@@ -193,6 +194,10 @@ class StartCheckoutService
$cart->getKey(), $cart->getKey(),
); );
$cartItems = $cart->items()->orderBy('id')->lockForUpdate()->get();
$this->loadCartItems($cartItems);
$purchase->items()->createMany($this->snapshots->fromCartItems($cartItems));
foreach ($cartItems as $index => $cartItem) { foreach ($cartItems as $index => $cartItem) {
$this->reservations->attachToPurchase( $this->reservations->attachToPurchase(
$cartItem, $cartItem,
@@ -259,6 +264,8 @@ class StartCheckoutService
$cart->getTotalAmount(), $cart->getTotalAmount(),
$cart->getKey(), $cart->getKey(),
); );
$purchase->items()->createMany($this->snapshots->fromCartItems($cartItems));
foreach ($cartItems as $cartItem) { foreach ($cartItems as $cartItem) {
$this->reservations->attachToPurchase( $this->reservations->attachToPurchase(
$cartItem, $cartItem,

View File

@@ -2,12 +2,10 @@
namespace App\Domains\Sale\Resources\AdminApp; namespace App\Domains\Sale\Resources\AdminApp;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Models\PurchaseItem; use App\Domains\Purchase\Models\PurchaseItem;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Collection;
/** @mixin Purchase */ /** @mixin Purchase */
class SaleDetailResource extends JsonResource class SaleDetailResource extends JsonResource
@@ -15,44 +13,23 @@ class SaleDetailResource extends JsonResource
/** @return array<string, mixed> */ /** @return array<string, mixed> */
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
$items = $this->saleItems();
return [ return [
'id' => $this->id, 'id' => $this->id,
'items' => $items->map(fn (PurchaseItem|CartItem $item): array => [ 'items' => $this->items->map(fn (PurchaseItem $item): array => [
'id' => $item->id, 'id' => $item->id,
'product' => $item instanceof PurchaseItem 'product' => $item->item_nombre,
? $item->item_nombre
: $item->selectedItem()?->getName(),
'event_dates' => $this->eventDates($item), 'event_dates' => $this->eventDates($item),
'quantity' => (int) $item->cantidad, 'quantity' => (int) $item->cantidad,
'unit_price' => $this->formatMoney($this->unitPrice($item)), 'unit_price' => $this->formatMoney($item->precio_unitario),
'total' => $this->formatMoney($this->lineTotal($item)), 'total' => $this->formatMoney($item->total),
])->values(), ])->values(),
'total' => $this->formatMoney($this->total), 'total' => $this->formatMoney($this->total),
]; ];
} }
/** @return Collection<int, PurchaseItem|CartItem> */
private function saleItems(): Collection
{
if ($this->items->isNotEmpty()) {
return $this->items;
}
return $this->cart?->items ?? collect();
}
/** @return list<string> */ /** @return list<string> */
private function eventDates(PurchaseItem|CartItem $item): array private function eventDates(PurchaseItem $item): array
{ {
if ($item instanceof CartItem) {
return $item->variant?->selectedEventDates()
->map(fn ($eventDate): string => $eventDate->date->format('Y-m-d'))
->values()
->all() ?? [];
}
return collect($item->variant_attributes ?? []) return collect($item->variant_attributes ?? [])
->filter(fn (mixed $attribute): bool => is_array($attribute) ->filter(fn (mixed $attribute): bool => is_array($attribute)
&& mb_strtolower(trim((string) ($attribute['name'] ?? ''))) === 'fecha') && mb_strtolower(trim((string) ($attribute['name'] ?? ''))) === 'fecha')
@@ -66,20 +43,6 @@ class SaleDetailResource extends JsonResource
->all(); ->all();
} }
private function unitPrice(PurchaseItem|CartItem $item): float|int|string|null
{
return $item instanceof PurchaseItem
? $item->precio_unitario
: $item->selectedItem()?->getPrice();
}
private function lineTotal(PurchaseItem|CartItem $item): float|int|string|null
{
return $item instanceof PurchaseItem
? $item->total
: ($item->selectedItem()?->getPrice() ?? 0) * $item->cantidad;
}
private function formatMoney(float|int|string|null $amount): string private function formatMoney(float|int|string|null $amount): string
{ {
return number_format((float) ($amount ?? 0), 2, '.', ''); return number_format((float) ($amount ?? 0), 2, '.', '');

View File

@@ -51,14 +51,7 @@ class AdminAppSaleService
{ {
return Purchase::query() return Purchase::query()
->where('tenant_codigo', $tenant->codigo) ->where('tenant_codigo', $tenant->codigo)
->with([ ->with('items')
'items',
'cart' => fn ($query) => $query->withTrashed(),
'cart.items.catalogItem',
'cart.items.variant.catalogItem',
'cart.items.variant.eventDates',
'cart.items.variant.eventDate',
])
->findOrFail($saleId); ->findOrFail($saleId);
} }
@@ -154,15 +147,9 @@ class AdminAppSaleService
) )
->select('compras.*') ->select('compras.*')
->selectRaw( ->selectRaw(
'CASE WHEN compras.status IN (?, ?) ' '(SELECT COALESCE(SUM(purchase_items.cantidad), 0) '
.'THEN (SELECT COALESCE(SUM(cart_items.cantidad), 0) FROM carrito_items AS cart_items ' .'FROM compra_items AS purchase_items '
.'WHERE cart_items.cart_id = compras.cart_id) ' .'WHERE purchase_items.compra_id = compras.id) AS quantity',
.'ELSE (SELECT COALESCE(SUM(purchase_items.cantidad), 0) FROM compra_items AS purchase_items '
.'WHERE purchase_items.compra_id = compras.id) END AS quantity',
[
Purchase::STATUS_CREATED,
Purchase::STATUS_PENDING_PAYMENT,
],
) )
->withCount('tickets') ->withCount('tickets')
->orderBy($sortColumns[$sortBy], $sortDirection) ->orderBy($sortColumns[$sortBy], $sortDirection)
@@ -199,16 +186,11 @@ class AdminAppSaleService
protected function quantityExpression(): string protected function quantityExpression(): string
{ {
// El fallback se resuelve en SQL para poder ordenar por cantidad antes de paginar;
// los ítems consolidados de la compra tienen prioridad sobre los del carrito de origen.
return <<<'SQL' return <<<'SQL'
COALESCE( COALESCE(
(SELECT SUM(compra_items.cantidad) (SELECT SUM(compra_items.cantidad)
FROM compra_items FROM compra_items
WHERE compra_items.compra_id = compras.id), WHERE compra_items.compra_id = compras.id),
(SELECT SUM(carrito_items.cantidad)
FROM carrito_items
WHERE carrito_items.cart_id = compras.cart_id),
0 0
) AS quantity ) AS quantity
SQL; SQL;