Compare commits

...

4 Commits

7 changed files with 88 additions and 33 deletions

View File

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

View File

@@ -108,10 +108,6 @@ class CartService
'items.catalogItem.attachments', 'items.catalogItem.attachments',
'items.catalogItem.inventory', 'items.catalogItem.inventory',
'items.catalogItem.itemAttributes.attribute', '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.attachments',
'items.variant.inventory', 'items.variant.inventory',
'items.variant.definitions.itemAttribute.attribute.options', 'items.variant.definitions.itemAttribute.attribute.options',

View File

@@ -25,7 +25,7 @@ Bajo `/tenants/{tenant:codigo}`:
## Contratos ## Contratos
`AddCartItemRequest` y `UpdateCartItemQuantityRequest` validan selección y cantidad. `CartResource` y `CartItemResource` estabilizan la respuesta pública. `AddCartItemRequest` y `UpdateCartItemQuantityRequest` validan selección y cantidad. `CartResource` y `CartItemResource` estabilizan la respuesta pública. Cada ítem expone `nombre`, `imagen` y `precio_unitario` en la raíz, priorizando la variante seleccionada y usando el catálogo base como respaldo. La variante seleccionada se serializa en `variant`; las variantes alternativas no forman parte de la respuesta del carrito.
## Dependencias y reglas ## Dependencias y reglas

View File

@@ -125,6 +125,7 @@ class CatalogController extends Controller
$includedVariantId = null; $includedVariantId = null;
$cartItemId = $request->validated('cart_item_id'); $cartItemId = $request->validated('cart_item_id');
$selectedValues = $request->validated('selected_values', []);
if ($cartItemId !== null) { if ($cartItemId !== null) {
$cartItem = $cartService->show($tenant, $request) $cartItem = $cartService->show($tenant, $request)
@@ -132,12 +133,18 @@ class CatalogController extends Controller
->firstWhere('id', (int) $cartItemId); ->firstWhere('id', (int) $cartItemId);
abort_unless($cartItem?->catalog_item_id === $catalogItem->id, 404); abort_unless($cartItem?->catalog_item_id === $catalogItem->id, 404);
$includedVariantId = $cartItem->variant_id; $includedVariantId = $cartItem->variant_id;
if ($selectedValues === [] && $cartItem->variant !== null) {
$selectedValues = $cartItem->variant
->selectorOptions($catalogItem->itemAttributes)
->all();
}
} }
return CatalogVariantOptionsResource::make( return CatalogVariantOptionsResource::make(
$variantSelectionService->options( $variantSelectionService->options(
$catalogItem, $catalogItem,
$request->validated('selected_values', []), $selectedValues,
$includedVariantId, $includedVariantId,
) )
); );

View File

@@ -38,7 +38,7 @@ class VariantSelectionService
$matchingVariants = $variants $matchingVariants = $variants
->filter(fn (Variant $variant): bool => $this->matches($variant, $normalizedSelections)) ->filter(fn (Variant $variant): bool => $this->matches($variant, $normalizedSelections))
->values(); ->values();
$attributeKeys = $this->attributeKeys($catalogItem, $variants); $attributeKeys = $this->attributeKeys($catalogItem, $catalogItem->variants->values());
$isComplete = $attributeKeys->isNotEmpty() $isComplete = $attributeKeys->isNotEmpty()
&& $attributeKeys->every(fn (string $key): bool => array_key_exists($key, $normalizedSelections)); && $attributeKeys->every(fn (string $key): bool => array_key_exists($key, $normalizedSelections));
$resolvedVariant = $isComplete && $matchingVariants->count() === 1 $resolvedVariant = $isComplete && $matchingVariants->count() === 1
@@ -46,6 +46,10 @@ class VariantSelectionService
: null; : null;
return [ return [
'variants' => $variants
->map(fn (Variant $variant): array => $this->variantData($catalogItem, $variant))
->values()
->all(),
'selectors' => $this->selectors( 'selectors' => $this->selectors(
$catalogItem, $catalogItem,
$variants, $variants,

View File

@@ -49,9 +49,12 @@ class CartControllerTest extends TestCase
->assertCookie('guest_token') ->assertCookie('guest_token')
->assertJsonPath('data.items.0.catalog_item_id', $item->id) ->assertJsonPath('data.items.0.catalog_item_id', $item->id)
->assertJsonPath('data.items.0.variant_id', null) ->assertJsonPath('data.items.0.variant_id', null)
->assertJsonPath('data.items.0.variant', null)
->assertJsonPath('data.items.0.cantidad', 2) ->assertJsonPath('data.items.0.cantidad', 2)
->assertJsonPath('data.items.0.precio_unitario', '49.90') ->assertJsonPath('data.items.0.precio_unitario', '49.90')
->assertJsonPath('data.items.0.product.nombre', 'Item acme') ->assertJsonPath('data.items.0.nombre', 'Item acme')
->assertJsonPath('data.items.0.imagen', null)
->assertJsonMissingPath('data.items.0.product')
->assertJsonPath('data.subtotal', '99.80'); ->assertJsonPath('data.subtotal', '99.80');
$guestTokenCookie = $response->getCookie('guest_token', false); $guestTokenCookie = $response->getCookie('guest_token', false);
@@ -276,11 +279,6 @@ class CartControllerTest extends TestCase
'inventory_id' => $secondInventory->id, 'inventory_id' => $secondInventory->id,
'precio' => '20.00', 'precio' => '20.00',
]); ]);
$unavailableInventory = Inventory::query()->create(['real_stock' => 0]);
$unavailableVariant = $item->variants()->create([
'inventory_id' => $unavailableInventory->id,
'precio' => '25.00',
]);
$createResponse = $this->postJson('/api/tenants/acme/cart/items', [ $createResponse = $this->postJson('/api/tenants/acme/cart/items', [
'catalog_item_id' => $item->id, 'catalog_item_id' => $item->id,
'variant_id' => $firstVariant->id, 'variant_id' => $firstVariant->id,
@@ -301,11 +299,10 @@ class CartControllerTest extends TestCase
->assertOk() ->assertOk()
->assertJsonPath('data.items.0.variant_id', $secondVariant->id) ->assertJsonPath('data.items.0.variant_id', $secondVariant->id)
->assertJsonPath('data.items.0.precio_unitario', '20.00') ->assertJsonPath('data.items.0.precio_unitario', '20.00')
->assertJsonCount(2, 'data.items.0.product.variants') ->assertJsonPath('data.items.0.variant.id', $secondVariant->id)
->assertJsonPath('data.items.0.product.variants.0.id', $firstVariant->id) ->assertJsonPath('data.items.0.variant.precio', '20.00')
->assertJsonPath('data.items.0.product.variants.1.id', $secondVariant->id) ->assertJsonPath('data.items.0.variant.stock_tecnico', 0)
->assertJsonPath('data.items.0.product.variants.1.stock_tecnico', 0) ->assertJsonMissingPath('data.items.0.product');
->assertJsonMissing(['id' => $unavailableVariant->id, 'stock_tecnico' => 0]);
$this->assertDatabaseHas('inventories', [ $this->assertDatabaseHas('inventories', [
'id' => $firstVariant->inventory_id, 'id' => $firstVariant->inventory_id,

View File

@@ -4,6 +4,8 @@ namespace Tests\Feature\Catalog;
use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment; use App\Domains\Attachable\Models\Attachment;
use App\Domains\Auth\Models\User;
use App\Domains\Cart\Models\Cart;
use App\Domains\Catalog\Enums\InventoryPolicy; use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\Attribute; use App\Domains\Catalog\Models\Attribute;
use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\CatalogItem;
@@ -340,7 +342,10 @@ class CatalogItemDetailControllerTest extends TestCase
->assertOk() ->assertOk()
->assertJsonPath('data.valid', true) ->assertJsonPath('data.valid', true)
->assertJsonPath('data.resolved_variant', null) ->assertJsonPath('data.resolved_variant', null)
->assertJsonMissingPath('data.variants') ->assertJsonCount(3, 'data.variants')
->assertJsonPath('data.variants.0.id', $first->id)
->assertJsonPath('data.variants.0.values.sector.value', 'A')
->assertJsonPath('data.variants.0.values.seat.value', '1')
->assertJsonCount(2, 'data.selectors') ->assertJsonCount(2, 'data.selectors')
->assertJsonPath('data.selectors.0.key', 'sector') ->assertJsonPath('data.selectors.0.key', 'sector')
->assertJsonCount(2, 'data.selectors.0.options') ->assertJsonCount(2, 'data.selectors.0.options')
@@ -358,6 +363,57 @@ class CatalogItemDetailControllerTest extends TestCase
->assertJsonPath('data.resolved_variant.values.sector.value', 'A'); ->assertJsonPath('data.resolved_variant.values.sector.value', 'A');
} }
public function test_variant_options_restore_canonical_values_from_the_reserved_cart_item(): void
{
$tenant = $this->createTenant('reserved-variant-options');
$item = $this->createItem($tenant, 'Reserved numbered entry');
$sector = Attribute::query()->create([
'tenant_codigo' => $tenant->codigo,
'codigo' => 'sector',
'nombre' => 'Sector',
'type' => FieldType::Select,
]);
$seat = Attribute::query()->create([
'tenant_codigo' => $tenant->codigo,
'codigo' => 'seat',
'nombre' => 'Seat',
'type' => FieldType::Select,
]);
$itemSector = $item->itemAttributes()->create([
'attribute_id' => $sector->id,
'sort_order' => 0,
]);
$itemSeat = $item->itemAttributes()->create([
'attribute_id' => $seat->id,
'sort_order' => 1,
]);
$variant = $this->createVariant($item, 1, 0);
$variant->definitions()->createMany([
['item_attribute_id' => $itemSector->id, 'value' => 'VIP'],
['item_attribute_id' => $itemSeat->id, 'value' => 'A-12'],
]);
$user = User::factory()->create();
$cart = Cart::query()->create([
'tenant_codigo' => $tenant->codigo,
'user_id' => $user->id,
'status' => 'active',
]);
$cartItem = $cart->addItem($item->id, $variant->id, 1);
$this->actingAs($user, 'sanctum')
->postJson(
"/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}/variant-options",
[
'selected_values' => [],
'cart_item_id' => $cartItem->id,
],
)
->assertOk()
->assertJsonPath('data.selected_values.sector', 'VIP')
->assertJsonPath('data.selected_values.seat', 'A-12')
->assertJsonPath('data.resolved_variant.id', $variant->id);
}
private function createItem( private function createItem(
Tenant $tenant, Tenant $tenant,
string $name, string $name,