Add tests for ticket validity and event date formatting

- Create TicketValiditySchemaTest to verify database schema for ticket validity.
- Update CatalogModelsTest to include tests for event date attributes and selection options.
- Introduce EventDateTextFormatterTest for formatting event dates in Spanish.
- Refactor EventModelsTest to include validity time relationships.
- Add SaleDetailResourceTest to ensure correct serialization of purchase items.
- Enhance TicketTest with validity time checks and status management.
- Implement ValidityTimeResourceTest to validate resource output for different validity types.
- Add ValidityTimeTest to verify casting and validity checks for validity time types.
This commit is contained in:
2026-08-11 12:41:35 -03:00
parent b294e5c46e
commit 02cf3f3773
166 changed files with 10203 additions and 1849 deletions

View File

@@ -54,16 +54,26 @@ class CartController extends Controller
Tenant $tenant,
CartItem $cartItem,
): CartResource {
$updatesVariant = $request->exists('variant_id');
return CartResource::make(
$this->cartService->updateItemQuantity(
$this->cartService->updateItem(
$tenant,
$request,
$cartItem->getKey(),
(int) $request->validated('cantidad'),
$updatesVariant
? ($request->validated('variant_id') !== null
? (int) $request->validated('variant_id')
: null)
: $cartItem->variant_id,
$updatesVariant,
)
)->additional([
'code' => 'cart.quantity_updated',
'message' => __('api.cart.quantity_updated'),
'code' => $updatesVariant ? 'cart.item_updated' : 'cart.quantity_updated',
'message' => $updatesVariant
? __('api.cart.item_updated')
: __('api.cart.quantity_updated'),
]);
}

View File

@@ -7,6 +7,7 @@ use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\Inventory;
use App\Domains\Catalog\Models\Variant;
use App\Domains\Catalog\Services\CatalogInventoryService;
use App\Domains\Purchase\Services\UserPurchaseLimitService;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -86,6 +87,10 @@ class Cart extends Model
return DB::transaction(function () use ($catalogItemId, $variantId, $quantity): CartItem {
self::query()->whereKey($this->getKey())->lockForUpdate()->firstOrFail();
$selectedItem = $this->resolveScopedItem($catalogItemId, $variantId, true);
$cartQuantity = (int) $this->items()
->where('catalog_item_id', $catalogItemId)
->sum('cantidad');
$this->assertUserPurchaseLimit($selectedItem, $cartQuantity + $quantity);
$inventoryService = app(CatalogInventoryService::class);
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
@@ -119,29 +124,99 @@ class Cart extends Model
});
}
public function updateItem(int $cartItemId, int $quantity): CartItem
{
public function updateItem(
int $cartItemId,
int $quantity,
?int $variantId = null,
bool $updateVariant = false,
): CartItem {
if ($quantity <= 0) {
throw ValidationException::withMessages([
'cantidad' => __('api.cart.positive_quantity'),
]);
}
return DB::transaction(function () use ($cartItemId, $quantity): CartItem {
return DB::transaction(function () use (
$cartItemId,
$quantity,
$variantId,
$updateVariant,
): CartItem {
/** @var CartItem $item */
$item = $this->items()
->where('id', $cartItemId)
->lockForUpdate()
->firstOrFail();
$selectedItem = $this->resolveScopedItem(
$currentSelection = $this->resolveScopedItem(
$item->catalog_item_id,
$item->variant_id,
true,
);
$inventoryService = app(CatalogInventoryService::class);
if ($updateVariant && $variantId !== $item->variant_id) {
$nextSelection = $this->resolveScopedItem(
$item->catalog_item_id,
$variantId,
true,
);
$otherVariantsQuantity = (int) $this->items()
->where('catalog_item_id', $item->catalog_item_id)
->whereKeyNot($item->getKey())
->sum('cantidad');
$this->assertUserPurchaseLimit(
$nextSelection,
$otherVariantsQuantity + $quantity,
);
$inventoryService->release($currentSelection, $item->cantidad);
$availableQuantity = $inventoryService->availableQuantity($nextSelection);
if ($availableQuantity !== null && $availableQuantity < $quantity) {
throw ValidationException::withMessages([
'variant_id' => __('api.cart.insufficient_stock', ['max' => $availableQuantity]),
]);
}
$targetItem = $this->items()
->where('catalog_item_id', $item->catalog_item_id)
->where('variant_id', $variantId)
->whereKeyNot($item->getKey())
->lockForUpdate()
->first();
$inventoryService->reserve($nextSelection, $quantity);
if ($targetItem !== null) {
$targetItem->cantidad += $quantity;
$targetItem->save();
$item->delete();
return $targetItem->fresh();
}
$item->variant_id = $variantId;
$item->cantidad = $quantity;
$item->save();
return $item->fresh();
}
$delta = $quantity - $item->cantidad;
$availableQuantity = $inventoryService->availableQuantity($selectedItem);
if ($delta > 0) {
$otherVariantsQuantity = (int) $this->items()
->where('catalog_item_id', $item->catalog_item_id)
->whereKeyNot($item->getKey())
->sum('cantidad');
$this->assertUserPurchaseLimit(
$currentSelection,
$otherVariantsQuantity + $quantity,
);
}
$availableQuantity = $inventoryService->availableQuantity($currentSelection);
if ($delta > 0 && $availableQuantity !== null && $availableQuantity < $delta) {
$maxAvailable = $availableQuantity + $item->cantidad;
@@ -154,11 +229,11 @@ class Cart extends Model
$item->save();
if ($delta > 0) {
$inventoryService->reserve($selectedItem, $delta);
$inventoryService->reserve($currentSelection, $delta);
}
if ($delta < 0) {
$inventoryService->release($selectedItem, abs($delta));
$inventoryService->release($currentSelection, abs($delta));
}
return $item->fresh();
@@ -256,6 +331,26 @@ class Cart extends Model
return $variant;
}
private function assertUserPurchaseLimit(
CatalogItem|Variant $selectedItem,
int $cartQuantity,
): void {
if ($this->user_id === null) {
return;
}
$catalogItem = $selectedItem instanceof Variant
? $selectedItem->catalogItem
: $selectedItem;
app(UserPurchaseLimitService::class)->assertCanPurchase(
$catalogItem,
$this->user_id,
$cartQuantity,
field: 'cantidad',
);
}
protected function resolveInventory(int $inventoryId, bool $lockForUpdate): Inventory
{
$query = Inventory::query()->whereKey($inventoryId);

View File

@@ -19,7 +19,7 @@ class UpdateCartItemQuantityRequest extends FormRequest
return [
'cantidad' => ['required', 'integer', 'min:1'],
'catalog_item_id' => ['prohibited'],
'variant_id' => ['prohibited'],
'variant_id' => ['sometimes', 'nullable', 'integer'],
];
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Domains\Cart\Resources;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\Variant;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -36,6 +38,16 @@ class CartItemResource extends JsonResource
'product' => $selectedItem === null ? null : [
'nombre' => $selectedItem->getName(),
'imagen' => $imageUrl,
'variants' => $this->catalogItem->visibleVariants($this->variant_id)
->map(fn (Variant $variant): array => [
'id' => $variant->id,
'precio' => $this->formatMoney($variant->getPrice()),
'stock_tecnico' => $this->catalogItem->inventory_policy === InventoryPolicy::Unlimited
? null
: $variant->inventory->availableStock(),
'values' => $variant->selectionOptions($this->catalogItem->itemAttributes),
])
->values(),
],
];
}

View File

@@ -51,11 +51,17 @@ class CartService
];
}
public function updateItemQuantity(Tenant $tenant, Request $request, int $cartItemId, int $quantity): Cart
{
public function updateItem(
Tenant $tenant,
Request $request,
int $cartItemId,
int $quantity,
?int $variantId,
bool $updateVariant,
): Cart {
$identity = $this->requireIdentity($request);
$cart = $this->findCartOrFail($tenant, $identity);
$cart->updateItem($cartItemId, $quantity);
$cart->updateItem($cartItemId, $quantity, $variantId, $updateVariant);
return $this->loadCart($cart);
}
@@ -101,9 +107,16 @@ class CartService
return $cart->fresh()->load([
'items.catalogItem.attachments',
'items.catalogItem.inventory',
'items.catalogItem.itemAttributes.attribute',
'items.catalogItem.variants.inventory',
'items.catalogItem.variants.definitions.itemAttribute.attribute.options',
'items.catalogItem.variants.eventDates',
'items.catalogItem.variants.eventDate',
'items.variant.attachments',
'items.variant.inventory',
'items.variant.definitions.itemAttribute.attribute',
'items.variant.definitions.itemAttribute.attribute.options',
'items.variant.eventDates',
'items.variant.eventDate',
]);
}

View File

@@ -0,0 +1,32 @@
# Dominio Cart
## Propósito
Gestiona el carrito activo de un tenant tanto para visitantes como para usuarios autenticados.
## Modelo
- `Cart`: pertenece a un tenant y opcionalmente a un usuario; calcula el total y permite agregar, actualizar o quitar ítems.
- `CartItem`: referencia un `CatalogItem` y, opcionalmente, una `Variant`; expone la selección efectiva.
## Servicios
- `CartService`: obtiene el carrito, modifica ítems y administra la cookie del token invitado.
- `GuestCartMergeService`: incorpora el carrito invitado al usuario cuando este se autentica.
## Endpoints
Bajo `/tenants/{tenant:codigo}`:
- `GET /cart`.
- `POST /cart/items`.
- `PATCH /cart/items/{cartItem}`.
- `DELETE /cart/items/{cartItem}`.
## Contratos
`AddCartItemRequest` y `UpdateCartItemQuantityRequest` validan selección y cantidad. `CartResource` y `CartItemResource` estabilizan la respuesta pública.
## Dependencias y reglas
Depende de `Catalog` para productos y variantes, de `Tenant` para aislar datos y de `Auth` cuando existe usuario. Toda operación debe comprobar que carrito e ítem pertenecen al tenant actual.