refactor(checkout): materialize purchase item snapshots at start
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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, '.', '');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user