449 lines
15 KiB
PHP
449 lines
15 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Cart;
|
|
|
|
use App\Domains\Attachable\Enums\AttachmentType;
|
|
use App\Domains\Attachable\Models\Attachment;
|
|
use App\Domains\Auth\Models\User;
|
|
use App\Domains\Catalog\Enums\InventoryPolicy;
|
|
use App\Domains\Catalog\Models\Attribute;
|
|
use App\Domains\Catalog\Models\Category;
|
|
use App\Domains\Catalog\Models\Product;
|
|
use App\Domains\Catalog\Models\ProductAttribute;
|
|
use App\Domains\Catalog\Models\ProductVariant;
|
|
use App\Domains\Catalog\Models\ProductVariantDefinition;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Str;
|
|
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_real' => 10,
|
|
'stock_reservado' => 2,
|
|
]);
|
|
}
|
|
|
|
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_real' => 12,
|
|
'stock_reservado' => 5,
|
|
]);
|
|
}
|
|
|
|
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_real' => 10,
|
|
'stock_reservado' => 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_real' => 10,
|
|
'stock_reservado' => 0,
|
|
]);
|
|
}
|
|
|
|
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' => 'El máximo que se puede agregar es 2.']);
|
|
}
|
|
|
|
public function test_unlimited_inventory_can_be_reserved_updated_and_released_without_real_stock(): void
|
|
{
|
|
$variant = $this->createVariantForTenant(
|
|
'acme',
|
|
0,
|
|
'10.00',
|
|
'unlimited',
|
|
InventoryPolicy::Unlimited,
|
|
);
|
|
|
|
$response = $this->postJson('/api/tenants/acme/cart/items', [
|
|
'product_variant_id' => $variant->id,
|
|
'cantidad' => 100,
|
|
])->assertOk();
|
|
|
|
$guestToken = $response->getCookie('guest_token', false)?->getValue();
|
|
$this->assertDatabaseHas('productos_variantes', [
|
|
'id' => $variant->id,
|
|
'stock_real' => 0,
|
|
'stock_reservado' => 100,
|
|
]);
|
|
|
|
$this->call(
|
|
'PATCH',
|
|
"/api/tenants/acme/cart/items/{$variant->id}",
|
|
[],
|
|
['guest_token' => $guestToken],
|
|
[],
|
|
['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'],
|
|
json_encode(['cantidad' => 150]),
|
|
)->assertOk();
|
|
|
|
$this->assertDatabaseHas('productos_variantes', [
|
|
'id' => $variant->id,
|
|
'stock_real' => 0,
|
|
'stock_reservado' => 150,
|
|
]);
|
|
|
|
$this->call(
|
|
'DELETE',
|
|
"/api/tenants/acme/cart/items/{$variant->id}",
|
|
[],
|
|
['guest_token' => $guestToken],
|
|
[],
|
|
['HTTP_Accept' => 'application/json'],
|
|
)->assertOk();
|
|
|
|
$this->assertDatabaseHas('productos_variantes', [
|
|
'id' => $variant->id,
|
|
'stock_real' => 0,
|
|
'stock_reservado' => 0,
|
|
]);
|
|
}
|
|
|
|
protected function createVariantForTenant(
|
|
string $tenantCode,
|
|
int $stock,
|
|
string $price,
|
|
string $slugPrefix = 'shirt',
|
|
InventoryPolicy $inventoryPolicy = InventoryPolicy::Tracked,
|
|
): ProductVariant {
|
|
$tenant = Tenant::query()->where('codigo', $tenantCode)->first();
|
|
if (! $tenant) {
|
|
$this->createTenant($tenantCode, ucfirst($tenantCode), "{$tenantCode}.com");
|
|
}
|
|
|
|
$category = 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,
|
|
'inventory_policy' => $inventoryPolicy->value,
|
|
'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) Str::uuid();
|
|
$ftrKey = (string) Str::uuid();
|
|
|
|
$headerAttachment = Attachment::create([
|
|
'key' => $hdrKey,
|
|
'path' => 'tenants/'.$hdrKey.'.png',
|
|
'filename' => 'logo_header.png',
|
|
'type' => AttachmentType::Image,
|
|
'mime_type' => 'image/png',
|
|
]);
|
|
$footerAttachment = Attachment::create([
|
|
'key' => $ftrKey,
|
|
'path' => 'tenants/'.$ftrKey.'.png',
|
|
'filename' => 'logo_footer.png',
|
|
'type' => 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,
|
|
]);
|
|
}
|
|
}
|