'acme', 'nombre' => 'Acme', 'dominio' => 'acme.com', ]); $this->getJson('/api/tenants/acme/cart') ->assertOk() ->assertJson([ 'id' => null, 'tenant_codigo' => 'acme', 'status' => 'active', 'items' => [], 'subtotal' => '0.00', ]); } public function test_it_creates_a_guest_cart_and_returns_the_cart_snapshot(): void { $variant = $this->createVariantForTenant('acme', 10, '49.90'); $attribute = ProductAttribute::query()->create([ 'tenant_codigo' => 'acme', 'codigo' => 'color', 'nombre' => 'Color', 'type' => 'text', ]); ProductVariantDefinition::query()->create([ 'producto_variante_id' => $variant->id, 'attribute_id' => $attribute->id, 'value' => 'Red', ]); $response = $this->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $variant->id, 'cantidad' => 2, ]); $response ->assertOk() ->assertCookie('guest_token') ->assertJsonPath('tenant_codigo', 'acme') ->assertJsonPath('items.0.cantidad', 2) ->assertJsonPath('items.0.precio_unitario', '49.90') ->assertJsonPath('items.0.subtotal', '99.80') ->assertJsonPath('items.0.product.id', $variant->product->id) ->assertJsonPath('items.0.variant.id', $variant->id) ->assertJsonPath('items.0.variant.definitions.0.attribute.codigo', 'color') ->assertJsonPath('subtotal', '99.80'); $this->assertDatabaseHas('carritos', [ 'tenant_codigo' => 'acme', 'guest_token' => $response->getCookie('guest_token')?->getValue(), 'status' => 'active', ]); $this->assertDatabaseHas('carrito_items', [ 'producto_variante_id' => $variant->id, 'cantidad' => 2, ]); $this->assertDatabaseHas('productos_variantes', [ 'id' => $variant->id, 'stock' => 8, ]); } public function test_it_merges_quantities_when_the_same_guest_adds_the_same_variant_twice(): void { $variant = $this->createVariantForTenant('acme', 12, '25.00'); $firstResponse = $this->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $variant->id, 'cantidad' => 2, ]); $guestToken = $firstResponse->getCookie('guest_token')?->getValue(); $this->withCookie('guest_token', $guestToken) ->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $variant->id, 'cantidad' => 3, ]) ->assertOk() ->assertJsonPath('items.0.cantidad', 5) ->assertJsonPath('items.0.subtotal', '125.00') ->assertJsonPath('subtotal', '125.00'); $this->assertDatabaseCount('carritos', 1); $this->assertDatabaseCount('carrito_items', 1); $this->assertDatabaseHas('carrito_items', [ 'producto_variante_id' => $variant->id, 'cantidad' => 5, ]); $this->assertDatabaseHas('productos_variantes', [ 'id' => $variant->id, 'stock' => 7, ]); } public function test_it_updates_item_quantity_and_adjusts_stock(): void { $variant = $this->createVariantForTenant('acme', 10, '15.00'); $createResponse = $this->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $variant->id, 'cantidad' => 2, ]); $guestToken = $createResponse->getCookie('guest_token')?->getValue(); $this->withCookie('guest_token', $guestToken) ->patchJson("/api/tenants/acme/cart/items/{$variant->id}", [ 'cantidad' => 5, ]) ->assertOk() ->assertJsonPath('items.0.cantidad', 5) ->assertJsonPath('items.0.subtotal', '75.00') ->assertJsonPath('subtotal', '75.00'); $this->assertDatabaseHas('carrito_items', [ 'producto_variante_id' => $variant->id, 'cantidad' => 5, ]); $this->assertDatabaseHas('productos_variantes', [ 'id' => $variant->id, 'stock' => 5, ]); } public function test_it_removes_an_item_and_restores_stock(): void { $variant = $this->createVariantForTenant('acme', 10, '15.00'); $createResponse = $this->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $variant->id, 'cantidad' => 4, ]); $guestToken = $createResponse->getCookie('guest_token')?->getValue(); $this->withCookie('guest_token', $guestToken) ->deleteJson("/api/tenants/acme/cart/items/{$variant->id}") ->assertOk() ->assertJsonPath('items', []) ->assertJsonPath('subtotal', '0.00'); $this->assertDatabaseCount('carrito_items', 0); $this->assertDatabaseHas('productos_variantes', [ 'id' => $variant->id, 'stock' => 10, ]); } public function test_authenticated_users_reuse_the_same_cart_per_tenant_and_get_a_new_one_for_another_tenant(): void { $user = User::factory()->create(); $acmeVariantA = $this->createVariantForTenant('acme', 10, '10.00'); $acmeVariantB = $this->createVariantForTenant('acme', 8, '20.00', 'hoodie'); $globexVariant = $this->createVariantForTenant('globex', 6, '30.00'); $this->actingAs($user) ->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $acmeVariantA->id, 'cantidad' => 1, ]) ->assertOk(); $this->actingAs($user) ->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $acmeVariantB->id, 'cantidad' => 2, ]) ->assertOk() ->assertJsonPath('subtotal', '50.00'); $this->actingAs($user) ->postJson('/api/tenants/globex/cart/items', [ 'product_variant_id' => $globexVariant->id, 'cantidad' => 1, ]) ->assertOk(); $this->assertDatabaseCount('carritos', 2); $this->assertDatabaseHas('carritos', [ 'tenant_codigo' => 'acme', 'user_id' => $user->id, ]); $this->assertDatabaseHas('carritos', [ 'tenant_codigo' => 'globex', 'user_id' => $user->id, ]); } public function test_it_rejects_variants_from_another_tenant(): void { $this->createVariantForTenant('acme', 10, '10.00'); $otherVariant = $this->createVariantForTenant('globex', 10, '20.00'); $this->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $otherVariant->id, 'cantidad' => 1, ])->assertNotFound(); } public function test_it_returns_not_found_when_the_cart_item_does_not_exist_for_update_or_delete(): void { $variant = $this->createVariantForTenant('acme', 10, '10.00'); $this->patchJson("/api/tenants/acme/cart/items/{$variant->id}", [ 'cantidad' => 2, ])->assertNotFound(); $this->deleteJson("/api/tenants/acme/cart/items/{$variant->id}") ->assertNotFound(); } public function test_it_validates_quantity_and_stock_constraints(): void { $variant = $this->createVariantForTenant('acme', 2, '10.00'); $this->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $variant->id, 'cantidad' => 0, ])->assertUnprocessable()->assertJsonValidationErrors(['cantidad']); $response = $this->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $variant->id, 'cantidad' => 2, ]); $guestToken = $response->getCookie('guest_token')?->getValue(); $this->withCookie('guest_token', $guestToken) ->postJson('/api/tenants/acme/cart/items', [ 'product_variant_id' => $variant->id, 'cantidad' => 1, ]) ->assertUnprocessable() ->assertJsonValidationErrors(['cantidad']); $this->withCookie('guest_token', $guestToken) ->patchJson("/api/tenants/acme/cart/items/{$variant->id}", [ 'cantidad' => 3, ]) ->assertUnprocessable() ->assertJsonValidationErrors(['cantidad']); } protected function createVariantForTenant( string $tenantCode, int $stock, string $price, string $slugPrefix = 'shirt', ): ProductVariant { Tenant::query()->firstOrCreate( ['codigo' => $tenantCode], ['nombre' => ucfirst($tenantCode), 'dominio' => "{$tenantCode}.com"], ); $category = \App\Domains\Catalog\Models\Category::query()->create([ 'tenant_code' => $tenantCode, 'nombre' => "{$slugPrefix} category {$tenantCode}", ]); $product = Product::query()->create([ 'tenant_codigo' => $tenantCode, 'categoria_id' => $category->id, 'slug' => "{$slugPrefix}-{$tenantCode}-".Product::query()->count(), 'nombre' => ucfirst($slugPrefix)." {$tenantCode}", 'descripcion' => 'Test product', 'precio' => $price, ]); return ProductVariant::query()->create([ 'producto_id' => $product->id, 'slug' => "{$slugPrefix}-variant-".ProductVariant::query()->count(), 'nombre' => ucfirst($slugPrefix).' Variant', 'stock' => $stock, 'descripcion' => 'Test variant', 'precio' => $price, ])->load('product'); } }