From 4e7e42d16ea3ed0d44a53f169745a76ff5ecf193 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 21 Aug 2026 11:03:39 -0300 Subject: [PATCH] refactor(checkout): materialize purchase item snapshots at start --- app/Domains/Purchase/Models/Purchase.php | 7 +- .../Resources/PurchaseItemResource.php | 157 ++---------------- .../Purchase/Resources/PurchaseResource.php | 33 +--- .../Checkout/CompleteCheckoutService.php | 58 ++++--- .../Checkout/PurchaseResponseLoader.php | 47 +----- .../Checkout/ReleaseCheckoutService.php | 17 +- .../Checkout/StartCheckoutService.php | 7 + .../Resources/AdminApp/SaleDetailResource.php | 47 +----- .../Sale/Services/AdminAppSaleService.php | 26 +-- 9 files changed, 74 insertions(+), 325 deletions(-) diff --git a/app/Domains/Purchase/Models/Purchase.php b/app/Domains/Purchase/Models/Purchase.php index 7bb3d94..592290c 100644 --- a/app/Domains/Purchase/Models/Purchase.php +++ b/app/Domains/Purchase/Models/Purchase.php @@ -154,12 +154,7 @@ class Purchase extends Model return (float) $this->getRelation('items')->sum('total'); } - $itemsTotal = (float) $this->items()->sum('total'); - if ($itemsTotal > 0 || $this->items()->exists()) { - return $itemsTotal; - } - - return (float) ($this->cart?->getTotalAmount() ?? $this->total ?? 0); + return (float) $this->items()->sum('total'); } protected function valueChangeTenantCode(): string diff --git a/app/Domains/Purchase/Resources/PurchaseItemResource.php b/app/Domains/Purchase/Resources/PurchaseItemResource.php index d96a4b3..1bd67d5 100644 --- a/app/Domains/Purchase/Resources/PurchaseItemResource.php +++ b/app/Domains/Purchase/Resources/PurchaseItemResource.php @@ -2,16 +2,12 @@ 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\Tenant\Models\Tenant; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; -/** @mixin PurchaseItem|CartItem */ +/** @mixin PurchaseItem */ class PurchaseItemResource extends JsonResource { /** @return array */ @@ -20,158 +16,29 @@ class PurchaseItemResource extends JsonResource $tenant = $request->route('tenant'); $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 - ? $this->resolveImageUrl($selectedItem, $catalogItem) + ? $this->imageAttachment?->getTemporaryUrl(1440) : null; - $includeVariants = $tenant instanceof Tenant - && $tenant->checkout_editing_policy->allowsVariantChanges() - && $catalogItem?->relationLoaded('variants'); return [ 'id' => $this->id, - 'quantity' => $quantity, - 'unit_price' => $this->formatMoney($unitPrice), - 'line_total' => $this->formatMoney($lineTotal), - 'source_catalog_item_id' => $this->catalog_item_id, - 'source_variant_id' => $this->variant_id, - 'item_details' => $selectedItem === null ? null : [ - 'nombre' => $selectedItem->getName(), - 'descripcion' => $selectedItem->getDescription(), - 'slug' => $catalogItem?->slug, + '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' => $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 */ - 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 { return number_format((float) ($amount ?? 0), 2, '.', ''); } - - /** @return array */ - 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), - ]; - } } diff --git a/app/Domains/Purchase/Resources/PurchaseResource.php b/app/Domains/Purchase/Resources/PurchaseResource.php index 5ccfa9b..1c69313 100644 --- a/app/Domains/Purchase/Resources/PurchaseResource.php +++ b/app/Domains/Purchase/Resources/PurchaseResource.php @@ -2,7 +2,6 @@ namespace App\Domains\Purchase\Resources; -use App\Domains\Cart\Models\CartItem; use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\PurchaseItem; use Illuminate\Http\Request; @@ -18,28 +17,16 @@ class PurchaseResource extends JsonResource */ public function toArray(Request $request): array { - $purchaseItems = $this->resource->relationLoaded('items') + $items = $this->resource->relationLoaded('items') ? $this->resource->getRelation('items') : 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()) ? (int) $this->resource->getAttribute('tickets_count') : null; $subtotal = $items->isNotEmpty() ? $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, ) : (float) ($this->total ?? 0); @@ -48,7 +35,7 @@ class PurchaseResource extends JsonResource ? (float) $this->total : ($items->isNotEmpty() ? $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, ) : (float) ($this->total ?? 0)); @@ -67,7 +54,7 @@ class PurchaseResource extends JsonResource 'telefono' => $this->telefono, 'nombre_apellido' => $this->nombre_apellido, 'email' => $this->email, - 'items_source' => $itemsSource, + 'items_source' => $items->isNotEmpty() ? 'purchase' : null, 'items' => PurchaseItemResource::collection($items), 'tickets_count' => $this->when($ticketsCount !== null, $ticketsCount), '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; } - 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); } diff --git a/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php b/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php index efafde2..09f7de0 100644 --- a/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php @@ -15,7 +15,6 @@ class CompleteCheckoutService public function __construct( private readonly StockReservationService $reservations, private readonly SourceCartService $sourceCart, - private readonly PurchaseItemSnapshotFactory $snapshots, private readonly PurchaseStateGuard $purchaseState, ) {} @@ -90,18 +89,17 @@ class CompleteCheckoutService ]); } - if ($purchase->items()->exists()) { - $cart = $purchase->cart()->withTrashed()->lockForUpdate()->first(); - if ($cart?->status === 'converted' && $cart->trashed()) { - return; - } - + if (! $purchase->items()->exists()) { throw ValidationException::withMessages([ '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') { throw ValidationException::withMessages([ '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); - $purchase->items()->createMany( - $this->snapshots->fromCartItems($cartItems), - ); foreach ($cartItems as $cartItem) { $selection = $cartItem->selectedItem(); @@ -159,16 +179,12 @@ class CompleteCheckoutService private function loadPurchase(Purchase $purchase): Purchase { - return $purchase->load([ - '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', - ]); + return $purchase->load(['items.imageAttachment']); + } + + private function itemKey(int $catalogItemId, ?int $variantId): string + { + return $catalogItemId.':'.($variantId ?? 'none'); } /** @param Collection $cartItems */ diff --git a/app/Domains/Purchase/Services/Checkout/PurchaseResponseLoader.php b/app/Domains/Purchase/Services/Checkout/PurchaseResponseLoader.php index 4e549a5..223a8ba 100644 --- a/app/Domains/Purchase/Services/Checkout/PurchaseResponseLoader.php +++ b/app/Domains/Purchase/Services/Checkout/PurchaseResponseLoader.php @@ -8,51 +8,6 @@ class PurchaseResponseLoader { public function load(Purchase $purchase): Purchase { - $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; + return $purchase->load(['tenant', 'items.imageAttachment']); } } diff --git a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php b/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php index a4266fb..39f9524 100644 --- a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php @@ -96,12 +96,6 @@ class ReleaseCheckoutService return $this->loadPurchase($purchase); } - if ($purchase->items()->exists()) { - throw ValidationException::withMessages([ - 'items' => __('api.purchase.inconsistent_reservation'), - ]); - } - $reservationReturnedToCart = $restoreCart && $this->sourceCart->restore($purchase); $this->releaseCartReservations($purchase, $reservationReturnedToCart, $targetStatus); @@ -181,15 +175,6 @@ class ReleaseCheckoutService private function loadPurchase(Purchase $purchase): Purchase { - return $purchase->load([ - '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', - ]); + return $purchase->load(['items.imageAttachment']); } } diff --git a/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php b/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php index fa906d6..c2f723d 100644 --- a/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php @@ -26,6 +26,7 @@ class StartCheckoutService private readonly CatalogSelectionResolver $selections, private readonly InsufficientStockMessageBuilder $stockMessages, private readonly PurchaseResponseLoader $responses, + private readonly PurchaseItemSnapshotFactory $snapshots, ) {} /** @param array $purchaseData */ @@ -193,6 +194,10 @@ class StartCheckoutService $cart->getKey(), ); + $cartItems = $cart->items()->orderBy('id')->lockForUpdate()->get(); + $this->loadCartItems($cartItems); + $purchase->items()->createMany($this->snapshots->fromCartItems($cartItems)); + foreach ($cartItems as $index => $cartItem) { $this->reservations->attachToPurchase( $cartItem, @@ -259,6 +264,8 @@ class StartCheckoutService $cart->getTotalAmount(), $cart->getKey(), ); + $purchase->items()->createMany($this->snapshots->fromCartItems($cartItems)); + foreach ($cartItems as $cartItem) { $this->reservations->attachToPurchase( $cartItem, diff --git a/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php b/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php index f96235b..d389d1a 100644 --- a/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php +++ b/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php @@ -2,12 +2,10 @@ namespace App\Domains\Sale\Resources\AdminApp; -use App\Domains\Cart\Models\CartItem; use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\PurchaseItem; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; -use Illuminate\Support\Collection; /** @mixin Purchase */ class SaleDetailResource extends JsonResource @@ -15,44 +13,23 @@ class SaleDetailResource extends JsonResource /** @return array */ public function toArray(Request $request): array { - $items = $this->saleItems(); - return [ 'id' => $this->id, - 'items' => $items->map(fn (PurchaseItem|CartItem $item): array => [ + 'items' => $this->items->map(fn (PurchaseItem $item): array => [ 'id' => $item->id, - 'product' => $item instanceof PurchaseItem - ? $item->item_nombre - : $item->selectedItem()?->getName(), + 'product' => $item->item_nombre, 'event_dates' => $this->eventDates($item), 'quantity' => (int) $item->cantidad, - 'unit_price' => $this->formatMoney($this->unitPrice($item)), - 'total' => $this->formatMoney($this->lineTotal($item)), + 'unit_price' => $this->formatMoney($item->precio_unitario), + 'total' => $this->formatMoney($item->total), ])->values(), 'total' => $this->formatMoney($this->total), ]; } - /** @return Collection */ - private function saleItems(): Collection - { - if ($this->items->isNotEmpty()) { - return $this->items; - } - - return $this->cart?->items ?? collect(); - } - /** @return list */ - 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 ?? []) ->filter(fn (mixed $attribute): bool => is_array($attribute) && mb_strtolower(trim((string) ($attribute['name'] ?? ''))) === 'fecha') @@ -66,20 +43,6 @@ class SaleDetailResource extends JsonResource ->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 { return number_format((float) ($amount ?? 0), 2, '.', ''); diff --git a/app/Domains/Sale/Services/AdminAppSaleService.php b/app/Domains/Sale/Services/AdminAppSaleService.php index 6f0bac4..75ae593 100644 --- a/app/Domains/Sale/Services/AdminAppSaleService.php +++ b/app/Domains/Sale/Services/AdminAppSaleService.php @@ -51,14 +51,7 @@ class AdminAppSaleService { return Purchase::query() ->where('tenant_codigo', $tenant->codigo) - ->with([ - 'items', - 'cart' => fn ($query) => $query->withTrashed(), - 'cart.items.catalogItem', - 'cart.items.variant.catalogItem', - 'cart.items.variant.eventDates', - 'cart.items.variant.eventDate', - ]) + ->with('items') ->findOrFail($saleId); } @@ -154,15 +147,9 @@ class AdminAppSaleService ) ->select('compras.*') ->selectRaw( - 'CASE WHEN compras.status IN (?, ?) ' - .'THEN (SELECT COALESCE(SUM(cart_items.cantidad), 0) FROM carrito_items AS cart_items ' - .'WHERE cart_items.cart_id = compras.cart_id) ' - .'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, - ], + '(SELECT COALESCE(SUM(purchase_items.cantidad), 0) ' + .'FROM compra_items AS purchase_items ' + .'WHERE purchase_items.compra_id = compras.id) AS quantity', ) ->withCount('tickets') ->orderBy($sortColumns[$sortBy], $sortDirection) @@ -199,16 +186,11 @@ class AdminAppSaleService 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' COALESCE( (SELECT SUM(compra_items.cantidad) FROM compra_items WHERE compra_items.compra_id = compras.id), - (SELECT SUM(carrito_items.cantidad) - FROM carrito_items - WHERE carrito_items.cart_id = compras.cart_id), 0 ) AS quantity SQL;