Compare commits
4 Commits
c1bfab471c
...
1c2f6c4127
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c2f6c4127 | |||
| 9e74e21fb5 | |||
| 7c2880646f | |||
| 4817cb28fe |
@@ -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(),
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user