feat: enhance checkout process with cart integration and stock confirmation

This commit is contained in:
2026-07-06 14:06:25 -03:00
parent 6bda3bf013
commit dc32eb9bee
11 changed files with 403 additions and 64 deletions

View File

@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -23,6 +24,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Cart extends Model class Cart extends Model
{ {
use HasFactory; use HasFactory;
use SoftDeletes;
protected $table = 'carritos'; protected $table = 'carritos';

View File

@@ -85,6 +85,25 @@ class ProductVariant extends Model
$this->save(); $this->save();
} }
public function confirmReservedStock(int $amount): void
{
if ($amount < 0) {
throw new \InvalidArgumentException('El monto a confirmar debe ser positivo.');
}
if ($this->stock_real < $amount) {
throw new \InvalidArgumentException('No hay suficiente stock real para confirmar la reserva.');
}
if ($this->stock_reservado < $amount) {
throw new \InvalidArgumentException('No hay suficiente stock reservado para confirmar la reserva.');
}
$this->stock_real -= $amount;
$this->stock_reservado -= $amount;
$this->save();
}
protected function stockTecnico(): Attribute protected function stockTecnico(): Attribute
{ {
return Attribute::get(fn () => $this->stock_real - $this->stock_reservado); return Attribute::get(fn () => $this->stock_real - $this->stock_reservado);

View File

@@ -2,6 +2,7 @@
namespace App\Domains\Integration\Services; namespace App\Domains\Integration\Services;
use App\Domains\Purchase\Services\CheckoutService;
use App\Domains\Purchase\Models\TelepagosPayment; use App\Domains\Purchase\Models\TelepagosPayment;
use App\Domains\Purchase\Models\TelepagosQr; use App\Domains\Purchase\Models\TelepagosQr;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
@@ -10,6 +11,10 @@ use Illuminate\Support\Facades\Log;
class TelepagosWebhookService class TelepagosWebhookService
{ {
public function __construct(
private readonly CheckoutService $checkoutService,
) {}
/** /**
* Handle the Telepagos webhook notification. * Handle the Telepagos webhook notification.
* *
@@ -79,6 +84,7 @@ class TelepagosWebhookService
\Illuminate\Support\Facades\DB::transaction(function () use ($compra, $paymentData) { \Illuminate\Support\Facades\DB::transaction(function () use ($compra, $paymentData) {
TelepagosPayment::create($paymentData); TelepagosPayment::create($paymentData);
$this->checkoutService->confirmPurchase($compra);
$compra->finalize(); $compra->finalize();
}); });

View File

@@ -32,7 +32,7 @@ class PurchaseController extends Controller
{ {
$data = $request->validated(); $data = $request->validated();
$purchase = $checkoutService->processCheckout( $purchase = $checkoutService->startCheckout(
$tenant, $tenant,
$request->user()->id, $request->user()->id,
$data $data

View File

@@ -3,14 +3,17 @@
namespace App\Domains\Purchase\Models; namespace App\Domains\Purchase\Models;
use App\Domains\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Cart\Models\Cart;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
#[Fillable([ #[Fillable([
'cart_id',
'tenant_codigo', 'tenant_codigo',
'user_id', 'user_id',
'status', 'status',
@@ -30,6 +33,7 @@ class Purchase extends Model
protected function casts(): array protected function casts(): array
{ {
return [ return [
'cart_id' => 'integer',
'user_id' => 'integer', 'user_id' => 'integer',
]; ];
} }
@@ -50,6 +54,14 @@ class Purchase extends Model
return $this->belongsTo(User::class); return $this->belongsTo(User::class);
} }
/**
* @return BelongsTo<Cart, $this>
*/
public function cart(): BelongsTo
{
return $this->belongsTo(Cart::class, 'cart_id')->withTrashed();
}
/** /**
* @return HasMany<PurchaseItem, $this> * @return HasMany<PurchaseItem, $this>
*/ */
@@ -76,7 +88,25 @@ class Purchase extends Model
public function getTotalAmount(): float public function getTotalAmount(): float
{ {
return (float) $this->items()->sum('total'); if ($this->items()->exists()) {
return (float) $this->items()->sum('total');
}
$cart = $this->relationLoaded('cart') ? $this->getRelation('cart') : $this->cart;
if ($cart === null) {
return 0.0;
}
/** @var Collection<int, mixed> $items */
$items = $cart->relationLoaded('items')
? $cart->getRelation('items')
: $cart->items()->with('variant.product')->get();
return $items->reduce(
static fn (float $carry, $item): float => $carry + ((float) ($item->variant?->product?->precio ?? 0) * (int) $item->cantidad),
0.0,
);
} }
public function finalize(): void public function finalize(): void

View File

@@ -19,13 +19,11 @@ class StorePurchaseRequest extends FormRequest
{ {
return [ return [
'status' => ['sometimes', 'string', Rule::in(['pending', 'paid', 'cancelled'])], 'status' => ['sometimes', 'string', Rule::in(['pending', 'paid', 'cancelled'])],
'cart_id' => ['required', 'integer', 'exists:carritos,id'],
'dni' => ['required', 'string'], 'dni' => ['required', 'string'],
'telefono' => ['required', 'string'], 'telefono' => ['required', 'string'],
'nombre_apellido' => ['required', 'string'], 'nombre_apellido' => ['required', 'string'],
'email' => ['required', 'string', 'email'], 'email' => ['required', 'string', 'email'],
'items' => ['required', 'array', 'min:1'],
'items.*.producto_variante_id' => ['required', 'integer', 'exists:productos_variantes,id'],
'items.*.cantidad' => ['required', 'integer', 'min:1'],
]; ];
} }
} }

View File

@@ -31,6 +31,7 @@ class PurchaseResource extends JsonResource
return [ return [
'id' => $this->id, 'id' => $this->id,
'cart_id' => $this->cart_id,
'tenant_codigo' => $this->tenant_codigo, 'tenant_codigo' => $this->tenant_codigo,
'user_id' => $this->user_id, 'user_id' => $this->user_id,
'status' => $this->status, 'status' => $this->status,

View File

@@ -2,28 +2,37 @@
namespace App\Domains\Purchase\Services; namespace App\Domains\Purchase\Services;
use App\Domains\Cart\Models\Cart;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Models\ProductVariant; use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\Purchase;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class CheckoutService class CheckoutService
{ {
public function processCheckout(Tenant $tenant, int $userId, array $purchaseData): Purchase public function startCheckout(Tenant $tenant, int $userId, array $purchaseData): Purchase
{ {
// 1. Preparar items de la compra $cartId = (int) $purchaseData['cart_id'];
$items = $purchaseData['items']; unset($purchaseData['cart_id']);
unset($purchaseData['items']);
$variants = $this->resolveTenantVariants($tenant, $items); return DB::transaction(function () use ($tenant, $userId, $purchaseData, $cartId): Purchase {
$purchaseItemsPayload = $this->buildPurchaseItemsPayload($items, $variants); $cart = $this->resolveCheckoutCart($tenant, $userId, $cartId);
$cartItems = $cart->items()->lockForUpdate()->get();
if ($cartItems->isEmpty()) {
throw ValidationException::withMessages([
'cart_id' => 'The selected cart does not contain items.',
]);
}
// 2. Crear la orden de compra y vaciar carrito
return DB::transaction(function () use ($tenant, $userId, $purchaseData, $purchaseItemsPayload): Purchase {
/** @var Purchase $purchase */ /** @var Purchase $purchase */
$purchase = Purchase::query()->create([ $purchase = Purchase::query()->create([
...$purchaseData, ...$purchaseData,
'cart_id' => $cart->getKey(),
'tenant_codigo' => $tenant->codigo, 'tenant_codigo' => $tenant->codigo,
'user_id' => $userId, 'user_id' => $userId,
'status' => 'pending', 'status' => 'pending',
@@ -31,45 +40,66 @@ class CheckoutService
'payment_method' => null, 'payment_method' => null,
]); ]);
$purchase->items()->createMany($purchaseItemsPayload);
// Vaciar y eliminar el carrito activo del usuario
$cart = \App\Domains\Cart\Models\Cart::query()
->where('tenant_codigo', $tenant->codigo)
->where('user_id', $userId)
->first();
if ($cart) {
$cart->items()->delete();
$cart->delete();
}
return $purchase->load(['items.variant.product', 'items.variant.definitions.productAttribute.attribute']); return $purchase->load(['items.variant.product', 'items.variant.definitions.productAttribute.attribute']);
}); });
} }
/** public function confirmPurchase(Purchase $purchase): void
* @param array<int, array<string, mixed>> $items
* @return \Illuminate\Support\Collection<int, ProductVariant>
*/
protected function resolveTenantVariants(Tenant $tenant, array $items)
{ {
$variantIds = collect($items) DB::transaction(function () use ($purchase): void {
/** @var Cart|null $cart */
$cart = $purchase->cart()->lockForUpdate()->first();
if ($cart === null) {
throw ValidationException::withMessages([
'cart_id' => 'The purchase cart is no longer available.',
]);
}
$cartItems = $cart->items()->lockForUpdate()->get();
if ($cartItems->isEmpty()) {
throw ValidationException::withMessages([
'cart_id' => 'The purchase cart does not contain items.',
]);
}
if ($purchase->items()->exists()) {
return;
}
$cartItems->load('variant.product');
$variants = $this->resolveTenantVariants($purchase->tenant, $cartItems);
$purchaseItemsPayload = $this->buildPurchaseItemsPayload($cartItems, $variants);
$purchase->items()->createMany($purchaseItemsPayload);
$this->completeCartConversion($cart, $cartItems, $variants);
});
}
/**
* @return Collection<int, ProductVariant>
*/
protected function resolveTenantVariants(Tenant $tenant, Collection $cartItems): Collection
{
$variantIds = $cartItems
->pluck('producto_variante_id') ->pluck('producto_variante_id')
->filter()
->map(static fn (mixed $id): int => (int) $id) ->map(static fn (mixed $id): int => (int) $id)
->unique() ->unique()
->values(); ->values();
/** @var Collection<int, ProductVariant> $variants */
$variants = ProductVariant::query() $variants = ProductVariant::query()
->with('product') ->with('product')
->whereIn('id', $variantIds) ->whereIn('id', $variantIds)
->whereHas('product', fn ($query) => $query->where('tenant_codigo', $tenant->codigo)) ->whereHas('product', fn ($query) => $query->where('tenant_codigo', $tenant->codigo))
->lockForUpdate()
->get() ->get()
->keyBy('id'); ->keyBy('id');
if ($variants->count() !== $variantIds->count()) { if ($variants->count() !== $variantIds->count()) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'items' => 'One or more product variants do not belong to the tenant.', 'cart_id' => 'One or more product variants do not belong to the tenant.',
]); ]);
} }
@@ -77,14 +107,38 @@ class CheckoutService
} }
/** /**
* @param array<int, array<string, mixed>> $items * @param Collection<int, CartItem> $cartItems
* @param \Illuminate\Support\Collection<int, ProductVariant> $variants * @return \Illuminate\Support\Collection<int, ProductVariant>
*/
protected function resolveCheckoutCart(Tenant $tenant, int $userId, int $cartId): Cart
{
/** @var Cart|null $cart */
$cart = Cart::query()
->lockForUpdate()
->find($cartId);
if ($cart === null || $cart->tenant_codigo !== $tenant->codigo || $cart->user_id !== $userId) {
throw new NotFoundHttpException('Cart not found for tenant.');
}
if ($cart->status !== 'active') {
throw ValidationException::withMessages([
'cart_id' => 'The selected cart is no longer active.',
]);
}
return $cart;
}
/**
* @param Collection<int, CartItem> $cartItems
* @param Collection<int, ProductVariant> $variants
* @return array<int, array<string, mixed>> * @return array<int, array<string, mixed>>
*/ */
protected function buildPurchaseItemsPayload(array $items, $variants): array protected function buildPurchaseItemsPayload(Collection $cartItems, Collection $variants): array
{ {
return collect($items) return $cartItems
->map(function (array $item) use ($variants): array { ->map(function (CartItem $item) use ($variants): array {
/** @var ProductVariant $variant */ /** @var ProductVariant $variant */
$variant = $variants->get((int) $item['producto_variante_id']); $variant = $variants->get((int) $item['producto_variante_id']);
$quantity = (int) $item['cantidad']; $quantity = (int) $item['cantidad'];
@@ -101,4 +155,31 @@ class CheckoutService
}) })
->all(); ->all();
} }
/**
* @param Collection<int, CartItem> $cartItems
* @param Collection<int, ProductVariant> $variants
*/
protected function completeCartConversion(Cart $cart, Collection $cartItems, Collection $variants): void
{
foreach ($cartItems as $item) {
/** @var ProductVariant $variant */
$variant = $variants->get((int) $item->producto_variante_id);
$quantity = (int) $item->cantidad;
try {
$variant->confirmReservedStock($quantity);
} catch (\InvalidArgumentException $exception) {
throw ValidationException::withMessages([
'cart_id' => 'The selected cart has inconsistent stock state.',
]);
}
}
$cart->status = 'converted';
$cart->user_id = null;
$cart->guest_token = null;
$cart->save();
$cart->delete();
}
} }

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('compras', function (Blueprint $table) {
$table->foreignId('cart_id')
->nullable()
->after('user_id')
->constrained('carritos')
->cascadeOnUpdate()
->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('compras', function (Blueprint $table) {
$table->dropConstrainedForeignId('cart_id');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('carritos', function (Blueprint $table) {
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('carritos', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@@ -3,6 +3,7 @@
namespace Tests\Feature\Purchase; namespace Tests\Feature\Purchase;
use App\Domains\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Cart\Models\Cart;
use App\Domains\Catalog\Models\Product; use App\Domains\Catalog\Models\Product;
use App\Domains\Catalog\Models\ProductVariant; use App\Domains\Catalog\Models\ProductVariant;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
@@ -13,17 +14,12 @@ class StorePurchaseTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
public function test_it_creates_a_purchase_with_customer_details_and_clears_the_cart(): void public function test_it_creates_a_purchase_from_cart_id_without_persisting_items_yet(): void
{ {
// 1. Setup Tenant
$tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); $tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
// 2. Setup User
$user = User::factory()->create([ $user = User::factory()->create([
'email' => 'buyer@example.com' 'email' => 'buyer@example.com',
]); ]);
// 3. Setup Product & Variant
$category = \App\Domains\Catalog\Models\Category::query()->create([ $category = \App\Domains\Catalog\Models\Category::query()->create([
'tenant_code' => 'sonder', 'tenant_code' => 'sonder',
'nombre' => 'Test Category', 'nombre' => 'Test Category',
@@ -46,61 +42,206 @@ class StorePurchaseTest extends TestCase
'precio' => '50.00', 'precio' => '50.00',
]); ]);
// 4. Add item to user's cart $cartResponse = $this->actingAs($user, 'sanctum')
$this->actingAs($user, 'sanctum')
->postJson('/api/tenants/sonder/cart/items', [ ->postJson('/api/tenants/sonder/cart/items', [
'product_variant_id' => $variant->id, 'product_variant_id' => $variant->id,
'cantidad' => 2, 'cantidad' => 2,
]) ])
->assertOk(); ->assertOk();
// Check cart exists in DB $cartId = $cartResponse->json('data.id');
$this->assertDatabaseHas('carritos', [ $this->assertDatabaseHas('carritos', [
'id' => $cartId,
'tenant_codigo' => 'sonder', 'tenant_codigo' => 'sonder',
'user_id' => $user->id, 'user_id' => $user->id,
'status' => 'active',
]); ]);
// 5. Submit Purchase with payment_method at root
$response = $this->actingAs($user, 'sanctum') $response = $this->actingAs($user, 'sanctum')
->postJson('/api/tenants/sonder/compras', [ ->postJson('/api/tenants/sonder/compras', [
'cart_id' => $cartId,
'dni' => '987654321', 'dni' => '987654321',
'telefono' => '+54 9 341 555-4321', 'telefono' => '+54 9 341 555-4321',
'nombre_apellido' => 'Juan Perez', 'nombre_apellido' => 'Juan Perez',
'email' => 'juan.perez@example.com', 'email' => 'juan.perez@example.com',
'payment_method' => 'transferencia',
'items' => [
[
'producto_variante_id' => $variant->id,
'cantidad' => 2,
]
]
]); ]);
// 6. Assertions
$response->assertCreated(); $response->assertCreated();
$response->assertJsonPath('data.cart_id', $cartId);
$response->assertJsonPath('data.dni', '987654321'); $response->assertJsonPath('data.dni', '987654321');
$response->assertJsonPath('data.telefono', '+54 9 341 555-4321'); $response->assertJsonPath('data.telefono', '+54 9 341 555-4321');
$response->assertJsonPath('data.nombre_apellido', 'Juan Perez'); $response->assertJsonPath('data.nombre_apellido', 'Juan Perez');
$response->assertJsonPath('data.email', 'juan.perez@example.com'); $response->assertJsonPath('data.email', 'juan.perez@example.com');
$response->assertJsonPath('data.payment_method', 'transferencia');
$response->assertJsonPath('data.tenant_codigo', 'sonder'); $response->assertJsonPath('data.tenant_codigo', 'sonder');
$response->assertJsonPath('data.items', []);
$response->assertJsonPath('data.total', '0.00');
$purchaseId = $response->json('data.id');
// Assert Purchase saved in DB
$this->assertDatabaseHas('compras', [ $this->assertDatabaseHas('compras', [
'id' => $purchaseId,
'cart_id' => $cartId,
'tenant_codigo' => 'sonder', 'tenant_codigo' => 'sonder',
'user_id' => $user->id, 'user_id' => $user->id,
'dni' => '987654321', 'dni' => '987654321',
'telefono' => '+54 9 341 555-4321', 'telefono' => '+54 9 341 555-4321',
'nombre_apellido' => 'Juan Perez', 'nombre_apellido' => 'Juan Perez',
'email' => 'juan.perez@example.com', 'email' => 'juan.perez@example.com',
'payment_method' => 'transferencia',
]); ]);
$this->assertDatabaseMissing('compra_items', [
// Assert Cart was cleared (deleted) 'compra_id' => $purchaseId,
$this->assertDatabaseMissing('carritos', [ ]);
'tenant_codigo' => 'sonder', $this->assertDatabaseHas('carritos', [
'id' => $cartId,
'status' => 'active',
'user_id' => $user->id, 'user_id' => $user->id,
]); ]);
$this->assertDatabaseMissing('carritos', [
'id' => $cartId,
'deleted_at' => null,
]);
$this->assertDatabaseHas('carrito_items', [
'cart_id' => $cartId,
'producto_variante_id' => $variant->id,
'cantidad' => 2,
]);
$this->assertDatabaseHas('productos_variantes', [
'id' => $variant->id,
'stock_real' => 10,
'stock_reservado' => 2,
]);
}
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', [
'product_variant_id' => $variant->id,
'cantidad' => 1,
])
->assertOk()
->json('data.id');
$this->actingAs($attacker, 'sanctum')
->postJson('/api/tenants/sonder/compras', [
'cart_id' => $cartId,
'dni' => '12345678',
'telefono' => '+54 9 341 555-1111',
'nombre_apellido' => 'Intruso',
'email' => 'intruso@example.com',
])
->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', [
'product_variant_id' => $variant->id,
'cantidad' => 1,
])
->assertOk()
->json('data.id');
$this->actingAs($user, 'sanctum')
->postJson('/api/tenants/sonder/compras', [
'cart_id' => $cartId,
'dni' => '12345678',
'telefono' => '+54 9 341 555-1111',
'nombre_apellido' => 'Juan Perez',
'email' => 'juan.perez@example.com',
])
->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', [
'cart_id' => $cart->id,
'dni' => '12345678',
'telefono' => '+54 9 341 555-1111',
'nombre_apellido' => 'Juan Perez',
'email' => 'juan.perez@example.com',
])
->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', [
'cart_id' => $cart->id,
'dni' => '12345678',
'telefono' => '+54 9 341 555-1111',
'nombre_apellido' => 'Juan Perez',
'email' => 'juan.perez@example.com',
])
->assertUnprocessable()
->assertJsonValidationErrors(['cart_id']);
}
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 protected function createTenant(string $codigo, string $nombre, string $dominio): Tenant