createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create([ 'email' => 'buyer@example.com', ]); $category = Category::query()->create([ 'tenant_code' => 'sonder', 'nombre' => 'Test Category', ]); $inventory = Inventory::query()->create(['real_stock' => 10]); $catalogItem = CatalogItem::query()->create([ 'tenant_code' => 'sonder', 'category_id' => $category->id, 'slug' => 'test-product', 'nombre' => 'Test Product', 'descripcion' => 'Test', 'precio' => '50.00', ]); $variant = Variant::query()->create([ 'catalog_item_id' => $catalogItem->id, 'inventory_id' => $inventory->id, ]); $cartResponse = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/cart/items', [ 'catalog_item_id' => $catalogItem->id, 'variant_id' => $variant->id, 'cantidad' => 2, ]) ->assertOk(); $cartId = $cartResponse->json('data.id'); $this->assertDatabaseHas('carritos', [ 'id' => $cartId, 'tenant_codigo' => 'sonder', 'user_id' => $user->id, 'status' => 'active', ]); $response = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'cart_id' => $cartId, ]); $response->assertCreated(); $response->assertJsonPath('data.cart_id', $cartId); $response->assertJsonPath('data.dni', null); $response->assertJsonPath('data.telefono', null); $response->assertJsonPath('data.nombre_apellido', null); $response->assertJsonPath('data.email', null); $response->assertJsonPath('data.tenant_codigo', 'sonder'); $response->assertJsonPath('data.status', Purchase::STATUS_CREATED); $response->assertJsonPath('data.items_source', 'purchase'); $response->assertJsonCount(1, 'data.items'); $response->assertJsonPath('data.subtotal', '100.00'); $response->assertJsonPath('data.total', '100.00'); $purchaseId = $response->json('data.id'); $this->assertDatabaseHas('compras', [ 'id' => $purchaseId, 'cart_id' => $cartId, 'tenant_codigo' => 'sonder', 'user_id' => $user->id, 'dni' => null, 'telefono' => null, 'nombre_apellido' => null, 'email' => null, 'status' => Purchase::STATUS_CREATED, 'total' => 100, ]); $this->assertDatabaseHas('compra_items', [ 'compra_id' => $purchaseId, 'source_catalog_item_id' => $catalogItem->id, 'source_variant_id' => $variant->id, 'cantidad' => 2, 'reservation_status' => 'active', ]); $this->assertDatabaseHas('carritos', [ 'id' => $cartId, 'user_id' => $user->id, 'status' => 'checkout', 'deleted_at' => null, ]); $this->assertDatabaseHas('carrito_items', [ 'cart_id' => $cartId, 'catalog_item_id' => $catalogItem->id, 'variant_id' => $variant->id, 'cantidad' => 2, ]); $this->assertDatabaseHas('inventories', [ 'id' => $inventory->id, 'real_stock' => 10, 'reserved_stock' => 2, ]); } public function test_it_creates_a_direct_purchase_without_creating_or_changing_a_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $response = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'direct_item' => [ 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 3, ], ]) ->assertCreated() ->assertJsonPath('data.cart_id', null) ->assertJsonPath('data.items_source', 'purchase') ->assertJsonPath('data.items.0.quantity', 3) ->assertJsonPath('data.total', '150.00'); $this->assertDatabaseCount('carritos', 0); $this->assertDatabaseHas('compra_items', [ 'compra_id' => $response->json('data.id'), 'source_catalog_item_id' => $variant->catalog_item_id, 'source_variant_id' => $variant->id, 'cantidad' => 3, 'reservation_status' => 'active', ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'real_stock' => 10, 'reserved_stock' => 3, ]); $this->actingAs($user, 'sanctum') ->postJson("/api/tenants/sonder/compras/{$response->json('data.id')}/cancel") ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_CANCELLED); $this->assertDatabaseHas('compra_items', [ 'compra_id' => $response->json('data.id'), 'reservation_status' => 'released', ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'real_stock' => 10, 'reserved_stock' => 0, ]); } public function test_it_restores_the_source_cart_when_checkout_is_cancelled(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 3); $this->assertDatabaseHas('carritos', [ 'id' => $purchase->cart_id, 'status' => 'checkout', 'deleted_at' => null, ]); $this->actingAs($user, 'sanctum') ->postJson("/api/tenants/sonder/compras/{$purchase->id}/cancel") ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_CANCELLED); $this->assertDatabaseHas('carritos', [ 'id' => $purchase->cart_id, 'user_id' => $user->id, 'status' => 'active', 'deleted_at' => null, ]); $this->assertDatabaseHas('carrito_items', [ 'cart_id' => $purchase->cart_id, 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 3, ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'reserved_stock' => 3, ]); } public function test_it_merges_the_checkout_cart_when_the_user_created_another_active_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $activeCartId = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/cart/items', [ 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 1, ]) ->assertOk() ->json('data.id'); $this->assertNotSame($purchase->cart_id, $activeCartId); $this->actingAs($user, 'sanctum') ->postJson("/api/tenants/sonder/compras/{$purchase->id}/cancel") ->assertOk(); $this->assertDatabaseHas('carritos', [ 'id' => $activeCartId, 'user_id' => $user->id, 'status' => 'active', 'deleted_at' => null, ]); $this->assertDatabaseHas('carrito_items', [ 'cart_id' => $activeCartId, 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 3, ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'reserved_stock' => 3, ]); } public function test_it_creates_purchase_items_before_checkout_and_updates_customer_data(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchaseResponse = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'direct_item' => [ 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 2, ], ]) ->assertCreated() ->assertJsonPath('data.items_source', 'purchase') ->assertJsonCount(1, 'data.items') ->assertJsonPath('data.items.0.quantity', 2) ->assertJsonPath('data.dni', null) ->assertJsonPath('data.telefono', null); $purchaseId = $purchaseResponse->json('data.id'); $this->actingAs($user, 'sanctum') ->patchJson("/api/tenants/sonder/compras/{$purchaseId}/customer-data", [ 'dni' => '987654321', 'telefono' => '+54 9 341 555-4321', 'nombre_apellido' => 'Juan Perez', 'email' => 'juan.perez@example.com', ]) ->assertOk() ->assertJsonPath('data.id', $purchaseId) ->assertJsonPath('data.dni', '987654321') ->assertJsonPath('data.telefono', '+54 9 341 555-4321') ->assertJsonPath('data.nombre_apellido', 'Juan Perez') ->assertJsonPath('data.email', 'juan.perez@example.com') ->assertJsonCount(1, 'data.items'); $this->assertDatabaseHas('compras', [ 'id' => $purchaseId, 'dni' => '987654321', 'telefono' => '+54 9 341 555-4321', 'nombre_apellido' => 'Juan Perez', 'email' => 'juan.perez@example.com', ]); } public function test_it_updates_customer_data_for_a_pending_payment_purchase(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 1); $purchase->update(['status' => Purchase::STATUS_PENDING_PAYMENT]); $this->actingAs($user, 'sanctum') ->patchJson("/api/tenants/sonder/compras/{$purchase->id}/customer-data", [ 'dni' => '987654321', 'telefono' => '+54 9 341 555-4321', 'nombre_apellido' => 'Juan Perez', 'email' => 'juan.perez@example.com', ]) ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_PENDING_PAYMENT) ->assertJsonPath('data.dni', '987654321'); $this->assertDatabaseHas('compras', [ 'id' => $purchase->id, 'status' => Purchase::STATUS_PENDING_PAYMENT, 'dni' => '987654321', ]); } public function test_it_updates_a_created_purchase_item_quantity_and_its_stock_reservation(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $itemId = $purchase->items->firstOrFail()->id; $this->actingAs($user, 'sanctum') ->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [ 'quantity' => 4, ]) ->assertOk() ->assertJsonPath('data.items.0.quantity', 4) ->assertJsonPath('data.items.0.line_total', '200.00') ->assertJsonPath('data.subtotal', '200.00') ->assertJsonPath('data.total', '200.00'); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'reserved_stock' => 4, ]); $this->assertDatabaseHas('carrito_items', [ 'cart_id' => $purchase->cart_id, 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 4, ]); $this->actingAs($user, 'sanctum') ->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [ 'quantity' => 1, ]) ->assertOk() ->assertJsonPath('data.items.0.quantity', 1) ->assertJsonPath('data.total', '50.00'); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'reserved_stock' => 1, ]); } public function test_it_reopens_a_pending_purchase_before_editing_items(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $purchase->update([ 'status' => Purchase::STATUS_PENDING_PAYMENT, 'payment_method' => 'qr', ]); $purchase->telepagosQr()->create([ 'qr_order_id' => 'stale-order', 'qr_code' => 'stale-qr', ]); $this->actingAs($user, 'sanctum') ->postJson("/api/tenants/sonder/compras/{$purchase->id}/edit-items") ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_CREATED) ->assertJsonPath('data.payment_method', null); $this->assertDatabaseHas('compras', [ 'id' => $purchase->id, 'status' => Purchase::STATUS_CREATED, 'payment_method' => null, ]); $this->assertDatabaseMissing('telepagos_qr', [ 'compra_id' => $purchase->id, 'qr_order_id' => 'stale-order', ]); } public function test_start_checkout_rejects_customer_data(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'direct_item' => [ 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 1, ], 'dni' => '987654321', 'telefono' => '+54 9 341 555-4321', 'nombre_apellido' => 'Juan Perez', 'email' => 'juan.perez@example.com', ]) ->assertUnprocessable() ->assertJsonValidationErrors([ 'dni', 'telefono', 'nombre_apellido', 'email', ]); } public function test_it_moves_a_created_purchase_to_pending_payment_when_finalized(): void { $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create([ 'email' => 'buyer@example.com', ]); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $cartId = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/cart/items', [ 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 2, ]) ->assertOk() ->json('data.id'); $purchaseId = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'cart_id' => $cartId, ]) ->assertCreated() ->json('data.id'); Purchase::query() ->whereKey($purchaseId) ->update([ 'payment_method' => 'transfer', ]); $this->actingAs($user, 'sanctum') ->postJson("/api/tenants/sonder/compras/{$purchaseId}/complete") ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_PENDING_PAYMENT); $this->assertDatabaseHas('compras', [ 'id' => $purchaseId, 'status' => Purchase::STATUS_PENDING_PAYMENT, 'payment_method' => 'transfer', ]); } public function test_it_expires_an_abandoned_purchase_and_restores_its_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 3); $this->assertNotNull($purchase->expires_at); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'reserved_stock' => 3, ]); $this->travel(31)->minutes(); $this->artisan('purchases:expire') ->expectsOutput('Expired purchases: 1') ->assertSuccessful(); $this->assertDatabaseHas('compras', [ 'id' => $purchase->id, 'status' => Purchase::STATUS_EXPIRED, ]); $this->assertDatabaseHas('compra_items', [ 'compra_id' => $purchase->id, 'reservation_status' => 'released', ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'real_stock' => 10, 'reserved_stock' => 3, 'sold_units' => 0, ]); $this->assertDatabaseHas('carritos', [ 'id' => $purchase->cart_id, 'user_id' => $user->id, 'status' => 'active', 'deleted_at' => null, ]); $this->artisan('purchases:expire') ->expectsOutput('Expired purchases: 0') ->assertSuccessful(); } public function test_purchase_detail_uses_purchase_items_for_created_purchase(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create([ 'email' => 'buyer@example.com', ]); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $this->actingAs($user, 'sanctum') ->getJson("/api/tenants/sonder/compras/{$purchase->id}") ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_CREATED) ->assertJsonPath('data.items_source', 'purchase') ->assertJsonCount(1, 'data.items') ->assertJsonPath('data.items.0.quantity', 2) ->assertJsonPath('data.items.0.unit_price', '50.00') ->assertJsonPath('data.items.0.line_total', '100.00') ->assertJsonPath('data.items.0.source_catalog_item_id', $variant->catalogItem->id) ->assertJsonPath('data.items.0.source_variant_id', $variant->id) ->assertJsonPath('data.items.0.item_details.nombre', $variant->catalogItem->nombre) ->assertJsonPath('data.items.0.item_details.slug', $variant->catalogItem->slug) ->assertJsonPath('data.items.0.item_details.imagen', null) ->assertJsonPath('data.items.0.item_details.attributes', []) ->assertJsonPath('data.subtotal', '100.00') ->assertJsonPath('data.total', '100.00'); } public function test_purchase_detail_uses_purchase_items_for_pending_payment_purchase(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create([ 'email' => 'buyer@example.com', ]); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $purchase->update([ 'payment_method' => 'transfer', ]); app(CheckoutService::class)->completePurchase($purchase); $this->actingAs($user, 'sanctum') ->getJson("/api/tenants/sonder/compras/{$purchase->id}") ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_PENDING_PAYMENT) ->assertJsonPath('data.items_source', 'purchase') ->assertJsonCount(1, 'data.items') ->assertJsonPath('data.items.0.quantity', 2) ->assertJsonPath('data.items.0.unit_price', '50.00') ->assertJsonPath('data.items.0.line_total', '100.00') ->assertJsonPath('data.items.0.item_details.imagen', null) ->assertJsonPath('data.items.0.item_details.attributes', []) ->assertJsonPath('data.subtotal', '100.00') ->assertJsonPath('data.total', '100.00'); } public function test_purchase_detail_uses_purchase_items_for_paid_purchase_even_without_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create([ 'email' => 'buyer@example.com', ]); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $purchase->update([ 'payment_method' => 'transfer', ]); $checkoutService = app(CheckoutService::class); $purchase = $checkoutService->completePurchase($purchase); $checkoutService->confirmPurchase($purchase); $purchase->refresh()->markAsPaid(); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'real_stock' => 8, 'reserved_stock' => 0, 'sold_units' => 2, ]); $this->assertSoftDeleted('carritos', [ 'id' => $purchase->cart_id, ]); $this->actingAs($user, 'sanctum') ->getJson("/api/tenants/sonder/compras/{$purchase->id}") ->assertOk() ->assertJsonPath('data.status', Purchase::STATUS_PAID) ->assertJsonPath('data.items_source', 'purchase') ->assertJsonCount(1, 'data.items') ->assertJsonPath('data.items.0.quantity', 2) ->assertJsonPath('data.items.0.unit_price', '50.00') ->assertJsonPath('data.items.0.line_total', '100.00') ->assertJsonPath('data.items.0.source_catalog_item_id', $variant->catalog_item_id) ->assertJsonPath('data.items.0.source_variant_id', $variant->id) ->assertJsonPath('data.items.0.item_details.imagen', null) ->assertJsonPath('data.items.0.item_details.attributes', []) ->assertJsonPath('data.subtotal', '100.00') ->assertJsonPath('data.total', '100.00'); } public function test_purchase_detail_does_not_require_its_source_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create([ 'email' => 'buyer@example.com', ]); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $purchase->update(['cart_id' => null]); $this->actingAs($user, 'sanctum') ->getJson("/api/tenants/sonder/compras/{$purchase->id}") ->assertOk() ->assertJsonPath('data.items_source', 'purchase') ->assertJsonCount(1, 'data.items') ->assertJsonPath('data.items.0.quantity', 2) ->assertJsonPath('data.items.0.line_total', '100.00') ->assertJsonPath('data.subtotal', '100.00') ->assertJsonPath('data.total', '100.00'); } public function test_purchase_index_returns_empty_items_without_loaded_relations(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create([ 'email' => 'buyer@example.com', ]); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $this->actingAs($user, 'sanctum') ->getJson('/api/tenants/sonder/compras?status=created') ->assertOk() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.items_source', null) ->assertJsonPath('data.0.items', []) ->assertJsonPath('data.0.total', '100.00'); } public function test_it_rejects_a_cart_from_another_user(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $owner = User::factory()->create(); $attacker = User::factory()->create(); $variant = $this->createVariantForTenant('sonder', 10, '50.00'); $cartId = $this->actingAs($owner, 'sanctum') ->postJson('/api/tenants/sonder/cart/items', [ 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 1, ]) ->assertOk() ->json('data.id'); $this->actingAs($attacker, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'cart_id' => $cartId, ]) ->assertNotFound(); } public function test_it_rejects_a_cart_from_another_tenant(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $this->createTenant('globex', 'Globex', 'globex.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant('globex', 10, '50.00'); $cartId = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/globex/cart/items', [ 'catalog_item_id' => $variant->catalog_item_id, 'variant_id' => $variant->id, 'cantidad' => 1, ]) ->assertOk() ->json('data.id'); $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'cart_id' => $cartId, ]) ->assertNotFound(); } public function test_it_rejects_an_empty_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $cart = Cart::query()->create([ 'tenant_codigo' => 'sonder', 'user_id' => $user->id, 'status' => 'active', ]); $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'cart_id' => $cart->id, ]) ->assertUnprocessable() ->assertJsonValidationErrors(['cart_id']); } public function test_it_rejects_a_converted_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $cart = Cart::query()->create([ 'tenant_codigo' => 'sonder', 'user_id' => $user->id, 'status' => 'converted', ]); $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'cart_id' => $cart->id, ]) ->assertUnprocessable() ->assertJsonValidationErrors(['cart_id']); } public function test_it_confirms_unlimited_inventory_without_reducing_real_stock_and_is_idempotent(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $user = User::factory()->create(); $variant = $this->createVariantForTenant( 'sonder', 0, '50.00', 'unlimited', InventoryPolicy::Unlimited, ); $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 25); $purchase->update(['payment_method' => 'transfer']); $checkoutService = app(CheckoutService::class); $purchase = $checkoutService->completePurchase($purchase); $checkoutService->confirmPurchase($purchase); $checkoutService->confirmPurchase($purchase); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, 'real_stock' => 0, 'reserved_stock' => 0, 'sold_units' => 25, ]); $this->assertDatabaseCount('compra_items', 1); } protected function createVariantForTenant( string $tenantCode, int $stock, string $price, string $slugPrefix = 'shirt', InventoryPolicy $inventoryPolicy = InventoryPolicy::Tracked, ): Variant { $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}", ]); $inventory = Inventory::query()->create(['real_stock' => $stock]); $catalogItem = CatalogItem::query()->create([ 'tenant_code' => $tenantCode, 'category_id' => $category->id, 'slug' => "{$slugPrefix}-{$tenantCode}-".CatalogItem::query()->count(), 'nombre' => ucfirst($slugPrefix)." {$tenantCode}", 'descripcion' => 'Test product', 'precio' => $price, 'inventory_policy' => $inventoryPolicy, ]); return Variant::query()->create([ 'catalog_item_id' => $catalogItem->id, 'inventory_id' => $inventory->id, ])->load(['catalogItem', 'inventory']); } protected function createCheckoutPurchase( User $user, string $tenantCode, Variant $variant, int $quantity, ): Purchase { $tenant = Tenant::query()->where('codigo', $tenantCode)->firstOrFail(); $cart = Cart::query()->create([ 'tenant_codigo' => $tenantCode, 'user_id' => $user->id, 'status' => 'active', ]); $cart->addItem($variant->catalog_item_id, $variant->id, $quantity); return app(CheckoutService::class)->startCheckout($tenant, $user->id, [ 'cart_id' => $cart->id, ]); } 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, ]); } }