shopit-back/tests/Feature/Cart/CartControllerTest.php

384 lines
13 KiB
PHP

<?php
namespace Tests\Feature\Cart;
use App\Domains\Catalog\Models\Product;
use App\Domains\Catalog\Models\Attribute;
use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Catalog\Models\ProductAttribute;
use App\Domains\Catalog\Models\ProductVariantDefinition;
use App\Domains\Tenant\Models\Tenant;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class CartControllerTest extends TestCase
{
use RefreshDatabase;
public function test_it_returns_an_empty_guest_cart_when_cart_does_not_exist(): void
{
$this->createTenant('acme', 'Acme', 'acme.com');
$this->getJson('/api/tenants/acme/cart')
->assertOk()
->assertJson([
'data' => [
'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 = Attribute::query()->create([
'tenant_codigo' => 'acme',
'codigo' => 'color',
'nombre' => 'Color',
'type' => 'string',
]);
$productAttribute = ProductAttribute::query()->create([
'product_id' => $variant->producto_id,
'attribute_id' => $attribute->id,
]);
ProductVariantDefinition::query()->create([
'producto_variante_id' => $variant->id,
'products_attribute_id' => $productAttribute->id,
'value' => 'Red',
]);
$response = $this->postJson('/api/tenants/acme/cart/items', [
'product_variant_id' => $variant->id,
'cantidad' => 2,
]);
$response
->assertOk()
->assertCookie('guest_token')
->assertJsonPath('data.tenant_codigo', 'acme')
->assertJsonPath('data.items.0.cantidad', 2)
->assertJsonPath('data.items.0.precio_unitario', '49.90')
->assertJsonPath('data.items.0.product_id', $variant->product->id)
->assertJsonPath('data.items.0.product.nombre', 'Shirt acme (Color: Red)')
->assertJsonPath('data.items.0.product.imagen', null)
->assertJsonPath('data.subtotal', '99.80');
$this->assertDatabaseHas('carritos', [
'tenant_codigo' => 'acme',
'guest_token' => $response->getCookie('guest_token', false)?->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', false)?->getValue();
$response = $this->call(
'POST',
'/api/tenants/acme/cart/items',
[],
['guest_token' => $guestToken],
[],
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'],
json_encode([
'product_variant_id' => $variant->id,
'cantidad' => 3,
])
);
$response
->assertOk()
->assertJsonPath('data.items.0.cantidad', 5)
->assertJsonPath('data.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', false)?->getValue();
$response = $this->call(
'PATCH',
"/api/tenants/acme/cart/items/{$variant->id}",
[],
['guest_token' => $guestToken],
[],
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'],
json_encode([
'cantidad' => 5,
])
);
$response
->assertOk()
->assertJsonPath('data.items.0.cantidad', 5)
->assertJsonPath('data.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', false)?->getValue();
$response = $this->call(
'DELETE',
"/api/tenants/acme/cart/items/{$variant->id}",
[],
['guest_token' => $guestToken],
[],
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json']
);
$response
->assertOk()
->assertJsonPath('data.items', [])
->assertJsonPath('data.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('data.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', false)?->getValue();
$response1 = $this->call(
'POST',
'/api/tenants/acme/cart/items',
[],
['guest_token' => $guestToken],
[],
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'],
json_encode([
'product_variant_id' => $variant->id,
'cantidad' => 1,
])
);
$response1
->assertUnprocessable()
->assertJsonValidationErrors(['cantidad']);
$response2 = $this->call(
'PATCH',
"/api/tenants/acme/cart/items/{$variant->id}",
[],
['guest_token' => $guestToken],
[],
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'],
json_encode([
'cantidad' => 3,
])
);
$response2
->assertUnprocessable()
->assertJsonValidationErrors(['cantidad']);
}
protected function createVariantForTenant(
string $tenantCode,
int $stock,
string $price,
string $slugPrefix = 'shirt',
): ProductVariant {
$tenant = Tenant::query()->where('codigo', $tenantCode)->first();
if (! $tenant) {
$this->createTenant($tenantCode, ucfirst($tenantCode), "{$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');
}
protected function createTenant(string $codigo, string $nombre, string $dominio): Tenant
{
$hdrKey = (string) \Illuminate\Support\Str::uuid();
$ftrKey = (string) \Illuminate\Support\Str::uuid();
$headerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $hdrKey,
'path' => 'tenants/' . $hdrKey . '.png',
'filename' => 'logo_header.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
$footerAttachment = \App\Domains\Attachable\Models\Attachment::create([
'key' => $ftrKey,
'path' => 'tenants/' . $ftrKey . '.png',
'filename' => 'logo_footer.png',
'type' => \App\Domains\Attachable\Enums\AttachmentType::Image,
'mime_type' => 'image/png',
]);
return Tenant::create([
'codigo' => $codigo,
'nombre' => $nombre,
'dominio' => $dominio,
'primary_color' => '#111111',
'secondary_color' => '#222222',
'danger_color' => '#333333',
'success_color' => '#28a745',
'header_bg_color' => '#444444',
'footer_bg_color' => '#444444',
'header_logo_id' => $headerAttachment->id,
'footer_logo_id' => $footerAttachment->id,
]);
}
}