diff --git a/app/Domains/Cart/Controllers/CartController.php b/app/Domains/Cart/Controllers/CartController.php index ee914d1..610f67a 100644 --- a/app/Domains/Cart/Controllers/CartController.php +++ b/app/Domains/Cart/Controllers/CartController.php @@ -2,11 +2,11 @@ namespace App\Domains\Cart\Controllers; +use App\Domains\Cart\Models\CartItem; use App\Domains\Cart\Requests\AddCartItemRequest; use App\Domains\Cart\Requests\UpdateCartItemQuantityRequest; use App\Domains\Cart\Resources\CartResource; use App\Domains\Cart\Services\CartService; -use App\Domains\Catalog\Models\ProductVariant; use App\Domains\Tenant\Models\Tenant; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; @@ -16,8 +16,7 @@ class CartController extends Controller { public function __construct( protected CartService $cartService, - ) { - } + ) {} public function show(Request $request, Tenant $tenant): CartResource { @@ -48,7 +47,7 @@ class CartController extends Controller public function updateItemQuantity( UpdateCartItemQuantityRequest $request, Tenant $tenant, - \App\Domains\Cart\Models\CartItem $cartItem, + CartItem $cartItem, ): CartResource { return CartResource::make( $this->cartService->updateItemQuantity( @@ -60,7 +59,7 @@ class CartController extends Controller )->additional(['message' => 'Cantidad de producto actualizada.']); } - public function removeItem(Request $request, Tenant $tenant, \App\Domains\Cart\Models\CartItem $cartItem): CartResource + public function removeItem(Request $request, Tenant $tenant, CartItem $cartItem): CartResource { return CartResource::make( $this->cartService->removeItem($tenant, $request, $cartItem->getKey()) diff --git a/app/Domains/Cart/Requests/AddCartItemRequest.php b/app/Domains/Cart/Requests/AddCartItemRequest.php index 1e3ec0b..2b44551 100644 --- a/app/Domains/Cart/Requests/AddCartItemRequest.php +++ b/app/Domains/Cart/Requests/AddCartItemRequest.php @@ -2,7 +2,10 @@ namespace App\Domains\Cart\Requests; +use App\Domains\Bundle\Models\Bundle; +use App\Domains\Catalog\Models\ProductVariant; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class AddCartItemRequest extends FormRequest { @@ -17,7 +20,7 @@ class AddCartItemRequest extends FormRequest public function rules(): array { return [ - 'buyable_type' => ['required', 'string', \Illuminate\Validation\Rule::in(['variant', 'bundle'])], + 'buyable_type' => ['required', 'string', Rule::in(['variant', 'bundle'])], 'buyable_id' => ['required', 'integer'], 'cantidad' => ['required', 'integer', 'min:1'], ]; @@ -26,8 +29,8 @@ class AddCartItemRequest extends FormRequest public function mappedBuyableType(): string { return match ($this->input('buyable_type')) { - 'variant' => \App\Domains\Catalog\Models\ProductVariant::class, - 'bundle' => \App\Domains\Bundle\Models\Bundle::class, + 'variant' => ProductVariant::class, + 'bundle' => Bundle::class, default => throw new \InvalidArgumentException('Invalid buyable type'), }; } diff --git a/app/Domains/Cart/Requests/UpdateCartItemQuantityRequest.php b/app/Domains/Cart/Requests/UpdateCartItemQuantityRequest.php index 2a98754..c3db1ca 100644 --- a/app/Domains/Cart/Requests/UpdateCartItemQuantityRequest.php +++ b/app/Domains/Cart/Requests/UpdateCartItemQuantityRequest.php @@ -18,6 +18,8 @@ class UpdateCartItemQuantityRequest extends FormRequest { return [ 'cantidad' => ['required', 'integer', 'min:1'], + 'buyable_type' => ['prohibited'], + 'buyable_id' => ['prohibited'], ]; } } diff --git a/app/Domains/Cart/Resources/CartResource.php b/app/Domains/Cart/Resources/CartResource.php index 36defee..ff02f81 100644 --- a/app/Domains/Cart/Resources/CartResource.php +++ b/app/Domains/Cart/Resources/CartResource.php @@ -2,11 +2,12 @@ namespace App\Domains\Cart\Resources; +use App\Domains\Cart\Models\Cart; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; /** - * @mixin \App\Domains\Cart\Models\Cart + * @mixin Cart */ class CartResource extends JsonResource { @@ -20,7 +21,7 @@ class CartResource extends JsonResource : collect(); $subtotal = $items->reduce( - fn (float $carry, $item): float => $carry + ((float) ($item->variant?->product?->precio ?? 0) * $item->cantidad), + fn (float $carry, $item): float => $carry + ((float) ($item->buyable?->getPrice() ?? 0) * $item->cantidad), 0.0, ); diff --git a/app/Domains/Cart/Services/CartService.php b/app/Domains/Cart/Services/CartService.php index c790f6e..17d309b 100644 --- a/app/Domains/Cart/Services/CartService.php +++ b/app/Domains/Cart/Services/CartService.php @@ -2,10 +2,14 @@ namespace App\Domains\Cart\Services; -use App\Domains\Cart\Models\Cart; use App\Domains\Auth\Models\User; +use App\Domains\Bundle\Models\Bundle; +use App\Domains\Cart\Models\Cart; +use App\Domains\Catalog\Models\ProductVariant; use App\Domains\Tenant\Models\Tenant; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -93,9 +97,16 @@ class CartService protected function loadCart(Cart $cart): Cart { return $cart->fresh()->load([ - 'items.variant.product', - 'items.variant.definitions.productAttribute.attribute', - 'items.variant.attachments', + 'items.buyable' => function (MorphTo $morphTo): void { + $morphTo->morphWith([ + ProductVariant::class => [ + 'product', + 'definitions.productAttribute.attribute', + 'attachments', + ], + Bundle::class => ['items.variant'], + ]); + }, ]); } @@ -104,7 +115,7 @@ class CartService */ protected function resolveIdentity(Request $request, bool $generateGuestToken = false): ?array { - $user = $request->user() ?? \Illuminate\Support\Facades\Auth::guard('sanctum')->user(); + $user = $request->user() ?? Auth::guard('sanctum')->user(); if ($user instanceof User) { return [ @@ -201,5 +212,4 @@ class CartService return Cart::query()->firstOrCreate($attributes, ['status' => 'active']); } - } diff --git a/app/Domains/Catalog/Models/ProductVariant.php b/app/Domains/Catalog/Models/ProductVariant.php index d46d62c..79a9b14 100644 --- a/app/Domains/Catalog/Models/ProductVariant.php +++ b/app/Domains/Catalog/Models/ProductVariant.php @@ -99,7 +99,10 @@ class ProductVariant extends Model implements Buyable if ($this->relationLoaded('definitions') && $this->definitions->isNotEmpty()) { $definitions = $this->definitions->map(function ($def) { - return $def->value ?? $def->productAttribute?->attribute?->nombre; + $attributeName = $def->productAttribute?->attribute?->nombre; + $value = $def->value; + + return $attributeName ? "{$attributeName}: {$value}" : $value; })->filter()->implode(', '); if ($definitions !== '') { diff --git a/app/Domains/Purchase/Controllers/PurchaseController.php b/app/Domains/Purchase/Controllers/PurchaseController.php index e976891..499536e 100644 --- a/app/Domains/Purchase/Controllers/PurchaseController.php +++ b/app/Domains/Purchase/Controllers/PurchaseController.php @@ -2,14 +2,20 @@ namespace App\Domains\Purchase\Controllers; +use App\Domains\Bundle\Models\Bundle; +use App\Domains\Catalog\Models\ProductVariant; +use App\Domains\Integration\Services\TelepagosIntegrationService; use App\Domains\Purchase\Models\Purchase; +use App\Domains\Purchase\Requests\PaymentIntentRequest; use App\Domains\Purchase\Requests\StorePurchaseRequest; use App\Domains\Purchase\Resources\PurchaseResource; use App\Domains\Purchase\Services\CheckoutService; use App\Domains\Tenant\Models\Tenant; use App\Http\Controllers\Controller; +use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -46,12 +52,17 @@ class PurchaseController extends Controller { $compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra); - return PurchaseResource::make( - $compra->loadMissing($this->purchaseDetailRelations()) - ); + $compra->loadMissing(['items', 'cart.items']); + $this->loadBuyables($compra->items); + + if ($compra->cart !== null) { + $this->loadBuyables($compra->cart->items); + } + + return PurchaseResource::make($compra); } - public function paymentIntent(\App\Domains\Purchase\Requests\PaymentIntentRequest $request, Tenant $tenant, Purchase $compra): JsonResponse + public function paymentIntent(PaymentIntentRequest $request, Tenant $tenant, Purchase $compra): JsonResponse { $compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra); $method = $request->validated('method'); @@ -63,7 +74,7 @@ class PurchaseController extends Controller ]); if ($method === 'transfer') { - $telepagosService = new \App\Domains\Integration\Services\TelepagosIntegrationService(); + $telepagosService = new TelepagosIntegrationService; $telepagosService->forTenant($tenant->codigo); try { @@ -81,13 +92,13 @@ class PurchaseController extends Controller ]); } catch (\Exception $e) { return response()->json([ - 'message' => 'Error getting account info: ' . $e->getMessage() + 'message' => 'Error getting account info: '.$e->getMessage(), ], 500); } } if ($method === 'qr') { - $telepagosService = new \App\Domains\Integration\Services\TelepagosIntegrationService(); + $telepagosService = new TelepagosIntegrationService; $telepagosService->forTenant($tenant->codigo); try { @@ -100,11 +111,11 @@ class PurchaseController extends Controller $telepagosQr = $compra->telepagosQr()->create([ 'qr_order_id' => (string) ($qrResponse['qr_order_id'] ?? ''), - 'qr_code' => $qrResponse['qr_code'] ?? '', + 'qr_code' => $qrResponse['qr_code'] ?? '', ]); } catch (\Exception $e) { return response()->json([ - 'message' => 'Error generating QR: ' . $e->getMessage() + 'message' => 'Error generating QR: '.$e->getMessage(), ], 500); } @@ -143,15 +154,19 @@ class PurchaseController extends Controller /** * @return list */ - protected function purchaseDetailRelations(): array + protected function loadBuyables(Collection $items): void { - return [ - 'items.variant.product', - 'items.variant.definitions.productAttribute.attribute', - 'items.variant.attachments', - 'cart.items.variant.product', - 'cart.items.variant.definitions.productAttribute.attribute', - 'cart.items.variant.attachments', - ]; + $items->load([ + 'buyable' => function (MorphTo $morphTo): void { + $morphTo->morphWith([ + ProductVariant::class => [ + 'product', + 'definitions.productAttribute.attribute', + 'attachments', + ], + Bundle::class => ['items.variant'], + ]); + }, + ]); } } diff --git a/app/Domains/Purchase/Models/Purchase.php b/app/Domains/Purchase/Models/Purchase.php index 7a18740..2d39e7a 100644 --- a/app/Domains/Purchase/Models/Purchase.php +++ b/app/Domains/Purchase/Models/Purchase.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; #[Fillable([ 'cart_id', @@ -28,9 +29,13 @@ class Purchase extends Model use HasFactory; public const STATUS_CREATED = 'created'; + public const STATUS_PENDING_PAYMENT = 'pending_payment'; + public const STATUS_PAID = 'paid'; + public const STATUS_CANCELLED = 'cancelled'; + public const STATUS_REJECTED = 'rejected'; protected $table = 'compras'; @@ -77,7 +82,7 @@ class Purchase extends Model } /** - * @return \Illuminate\Database\Eloquent\Relations\HasOne + * @return HasOne */ public function telepagosQr() { @@ -113,7 +118,7 @@ class Purchase extends Model $cart = $this->relationLoaded('cart') ? $this->getRelation('cart') - : $this->cart()->with('items.variant.product')->first(); + : $this->cart()->with('items.buyable')->first(); if (! $cart) { return 0.0; diff --git a/app/Domains/Purchase/Resources/PurchaseItemResource.php b/app/Domains/Purchase/Resources/PurchaseItemResource.php index 875d770..e1524b5 100644 --- a/app/Domains/Purchase/Resources/PurchaseItemResource.php +++ b/app/Domains/Purchase/Resources/PurchaseItemResource.php @@ -2,13 +2,16 @@ namespace App\Domains\Purchase\Resources; +use App\Domains\Bundle\Models\Bundle; use App\Domains\Cart\Models\CartItem; +use App\Domains\Catalog\Models\ProductVariant; use App\Domains\Purchase\Models\PurchaseItem; +use App\Domains\Shared\Contracts\Buyable; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; /** - * @mixin \App\Domains\Purchase\Models\PurchaseItem|\App\Domains\Cart\Models\CartItem + * @mixin PurchaseItem|CartItem */ class PurchaseItemResource extends JsonResource { @@ -17,15 +20,16 @@ class PurchaseItemResource extends JsonResource */ public function toArray(Request $request): array { - /** @var \App\Domains\Shared\Contracts\Buyable|null $buyable */ + /** @var Buyable|null $buyable */ $buyable = $this->buyable; $quantity = (int) ($this->cantidad ?? 0); $unitPrice = $this->resolveUnitPrice(); $lineTotal = $this->resolveLineTotal($unitPrice, $quantity); + $variant = $buyable instanceof ProductVariant ? $buyable : null; $imageUrl = null; $attributes = []; - if ($this->buyable_type === \App\Domains\Catalog\Models\ProductVariant::class && $buyable) { + if ($this->buyable_type === ProductVariant::class && $buyable) { $imageUrl = $this->resolveImageUrl($buyable); $attributes = $this->resolveAttributes($buyable); } @@ -37,6 +41,16 @@ class PurchaseItemResource extends JsonResource 'line_total' => $this->formatMoney($lineTotal), 'buyable_type' => $this->mapBuyableTypeToAlias($this->buyable_type), 'buyable_id' => $this->buyable_id, + 'product' => $variant === null ? null : [ + 'id' => $variant->product?->id, + 'nombre' => $variant->product?->nombre, + 'slug' => $variant->product?->slug, + 'imagen' => $imageUrl, + ], + 'variant' => $variant === null ? null : [ + 'id' => $variant->id, + 'attributes' => $attributes, + ], 'item_details' => $buyable === null ? null : [ 'nombre' => $buyable->getName(), 'imagen' => $imageUrl, @@ -48,8 +62,8 @@ class PurchaseItemResource extends JsonResource protected function mapBuyableTypeToAlias(?string $type): string { return match ($type) { - \App\Domains\Catalog\Models\ProductVariant::class => 'variant', - \App\Domains\Bundle\Models\Bundle::class => 'bundle', + ProductVariant::class => 'variant', + Bundle::class => 'bundle', default => 'unknown', }; } diff --git a/app/Domains/Purchase/Resources/PurchaseResource.php b/app/Domains/Purchase/Resources/PurchaseResource.php index d1a8537..b955849 100644 --- a/app/Domains/Purchase/Resources/PurchaseResource.php +++ b/app/Domains/Purchase/Resources/PurchaseResource.php @@ -3,13 +3,14 @@ namespace App\Domains\Purchase\Resources; use App\Domains\Cart\Models\CartItem; +use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Models\PurchaseItem; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Collection; /** - * @mixin \App\Domains\Purchase\Models\Purchase + * @mixin Purchase */ class PurchaseResource extends JsonResource { @@ -87,7 +88,7 @@ class PurchaseResource extends JsonResource return (float) $item->precio_unitario * $item->cantidad; } - return (float) ($item->variant?->product?->precio ?? 0) * $item->cantidad; + return (float) ($item->buyable?->getPrice() ?? 0) * $item->cantidad; } protected function resolveItemTotal(PurchaseItem|CartItem $item): float diff --git a/database/migrations/2026_07_14_184802_make_cart_items_polymorphic.php b/database/migrations/2026_07_14_184802_make_cart_items_polymorphic.php index fe1a0d1..b55c3b5 100644 --- a/database/migrations/2026_07_14_184802_make_cart_items_polymorphic.php +++ b/database/migrations/2026_07_14_184802_make_cart_items_polymorphic.php @@ -2,6 +2,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -14,17 +15,23 @@ return new class extends Migration Schema::table('carrito_items', function (Blueprint $table) { $table->string('buyable_type')->nullable(); $table->unsignedBigInteger('buyable_id')->nullable(); + $table->index('cart_id'); }); // Copy existing data - \Illuminate\Support\Facades\DB::table('carrito_items')->update([ + DB::table('carrito_items')->update([ 'buyable_type' => 'App\Domains\Catalog\Models\ProductVariant', - 'buyable_id' => \Illuminate\Support\Facades\DB::raw('producto_variante_id'), + 'buyable_id' => DB::raw('producto_variante_id'), ]); Schema::table('carrito_items', function (Blueprint $table) { $table->dropForeign(['producto_variante_id']); + }); + + Schema::table('carrito_items', function (Blueprint $table) { + $table->dropUnique(['cart_id', 'producto_variante_id']); $table->dropColumn('producto_variante_id'); + $table->unique(['cart_id', 'buyable_type', 'buyable_id']); }); } @@ -34,15 +41,18 @@ return new class extends Migration public function down(): void { Schema::table('carrito_items', function (Blueprint $table) { + $table->dropUnique(['cart_id', 'buyable_type', 'buyable_id']); $table->foreignId('producto_variante_id')->nullable()->constrained('productos_variantes')->onDelete('cascade'); }); - \Illuminate\Support\Facades\DB::table('carrito_items')->update([ - 'producto_variante_id' => \Illuminate\Support\Facades\DB::raw('buyable_id'), + DB::table('carrito_items')->update([ + 'producto_variante_id' => DB::raw('buyable_id'), ]); Schema::table('carrito_items', function (Blueprint $table) { $table->dropColumn(['buyable_type', 'buyable_id']); + $table->dropIndex(['cart_id']); + $table->unique(['cart_id', 'producto_variante_id']); }); } }; diff --git a/tests/Feature/Cart/CartControllerTest.php b/tests/Feature/Cart/CartControllerTest.php index 6f68ca7..313f25e 100644 --- a/tests/Feature/Cart/CartControllerTest.php +++ b/tests/Feature/Cart/CartControllerTest.php @@ -60,7 +60,8 @@ class CartControllerTest extends TestCase ]); $response = $this->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 2, ]); @@ -70,7 +71,8 @@ class CartControllerTest extends TestCase ->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.buyable_type', 'variant') + ->assertJsonPath('data.items.0.buyable_id', $variant->id) ->assertJsonPath('data.items.0.product.nombre', 'Shirt acme (Color: Red)') ->assertJsonPath('data.items.0.product.imagen', null) ->assertJsonPath('data.subtotal', '99.80'); @@ -82,7 +84,8 @@ class CartControllerTest extends TestCase ]); $this->assertDatabaseHas('carrito_items', [ - 'producto_variante_id' => $variant->id, + 'buyable_type' => ProductVariant::class, + 'buyable_id' => $variant->id, 'cantidad' => 2, ]); @@ -98,7 +101,8 @@ class CartControllerTest extends TestCase $variant = $this->createVariantForTenant('acme', 12, '25.00'); $firstResponse = $this->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 2, ]); @@ -112,7 +116,8 @@ class CartControllerTest extends TestCase [], ['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'], json_encode([ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 3, ]) ); @@ -125,7 +130,8 @@ class CartControllerTest extends TestCase $this->assertDatabaseCount('carritos', 1); $this->assertDatabaseCount('carrito_items', 1); $this->assertDatabaseHas('carrito_items', [ - 'producto_variante_id' => $variant->id, + 'buyable_type' => ProductVariant::class, + 'buyable_id' => $variant->id, 'cantidad' => 5, ]); $this->assertDatabaseHas('productos_variantes', [ @@ -140,15 +146,17 @@ class CartControllerTest extends TestCase $variant = $this->createVariantForTenant('acme', 10, '15.00'); $createResponse = $this->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 2, ]); $guestToken = $createResponse->getCookie('guest_token', false)?->getValue(); + $cartItemId = $createResponse->json('data.items.0.id'); $response = $this->call( 'PATCH', - "/api/tenants/acme/cart/items/{$variant->id}", + "/api/tenants/acme/cart/items/{$cartItemId}", [], ['guest_token' => $guestToken], [], @@ -164,7 +172,8 @@ class CartControllerTest extends TestCase ->assertJsonPath('data.subtotal', '75.00'); $this->assertDatabaseHas('carrito_items', [ - 'producto_variante_id' => $variant->id, + 'buyable_type' => ProductVariant::class, + 'buyable_id' => $variant->id, 'cantidad' => 5, ]); $this->assertDatabaseHas('productos_variantes', [ @@ -179,15 +188,17 @@ class CartControllerTest extends TestCase $variant = $this->createVariantForTenant('acme', 10, '15.00'); $createResponse = $this->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 4, ]); $guestToken = $createResponse->getCookie('guest_token', false)?->getValue(); + $cartItemId = $createResponse->json('data.items.0.id'); $response = $this->call( 'DELETE', - "/api/tenants/acme/cart/items/{$variant->id}", + "/api/tenants/acme/cart/items/{$cartItemId}", [], ['guest_token' => $guestToken], [], @@ -216,14 +227,16 @@ class CartControllerTest extends TestCase $this->actingAs($user) ->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $acmeVariantA->id, + 'buyable_type' => 'variant', + 'buyable_id' => $acmeVariantA->id, 'cantidad' => 1, ]) ->assertOk(); $this->actingAs($user) ->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $acmeVariantB->id, + 'buyable_type' => 'variant', + 'buyable_id' => $acmeVariantB->id, 'cantidad' => 2, ]) ->assertOk() @@ -231,7 +244,8 @@ class CartControllerTest extends TestCase $this->actingAs($user) ->postJson('/api/tenants/globex/cart/items', [ - 'product_variant_id' => $globexVariant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $globexVariant->id, 'cantidad' => 1, ]) ->assertOk(); @@ -253,7 +267,8 @@ class CartControllerTest extends TestCase $otherVariant = $this->createVariantForTenant('globex', 10, '20.00'); $this->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $otherVariant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $otherVariant->id, 'cantidad' => 1, ])->assertNotFound(); } @@ -275,16 +290,19 @@ class CartControllerTest extends TestCase $variant = $this->createVariantForTenant('acme', 2, '10.00'); $this->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 0, ])->assertUnprocessable()->assertJsonValidationErrors(['cantidad']); $response = $this->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 2, ]); $guestToken = $response->getCookie('guest_token', false)?->getValue(); + $cartItemId = $response->json('data.items.0.id'); $response1 = $this->call( 'POST', @@ -294,7 +312,8 @@ class CartControllerTest extends TestCase [], ['HTTP_Accept' => 'application/json', 'CONTENT_TYPE' => 'application/json'], json_encode([ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 1, ]) ); @@ -305,7 +324,7 @@ class CartControllerTest extends TestCase $response2 = $this->call( 'PATCH', - "/api/tenants/acme/cart/items/{$variant->id}", + "/api/tenants/acme/cart/items/{$cartItemId}", [], ['guest_token' => $guestToken], [], @@ -320,6 +339,32 @@ class CartControllerTest extends TestCase ->assertJsonValidationErrors(['cantidad' => 'El máximo que se puede agregar es 2.']); } + public function test_it_only_accepts_buyable_identity_when_adding_an_item(): void + { + $variant = $this->createVariantForTenant('acme', 10, '10.00'); + + $this->postJson('/api/tenants/acme/cart/items', [ + 'product_variant_id' => $variant->id, + 'cantidad' => 1, + ])->assertUnprocessable() + ->assertJsonValidationErrors(['buyable_type', 'buyable_id']); + + $createResponse = $this->postJson('/api/tenants/acme/cart/items', [ + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, + 'cantidad' => 1, + ])->assertOk(); + + $cartItemId = $createResponse->json('data.items.0.id'); + + $this->patchJson("/api/tenants/acme/cart/items/{$cartItemId}", [ + 'cantidad' => 2, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, + ])->assertUnprocessable() + ->assertJsonValidationErrors(['buyable_type', 'buyable_id']); + } + public function test_unlimited_inventory_can_be_reserved_updated_and_released_without_real_stock(): void { $variant = $this->createVariantForTenant( @@ -331,11 +376,13 @@ class CartControllerTest extends TestCase ); $response = $this->postJson('/api/tenants/acme/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 100, ])->assertOk(); $guestToken = $response->getCookie('guest_token', false)?->getValue(); + $cartItemId = $response->json('data.items.0.id'); $this->assertDatabaseHas('productos_variantes', [ 'id' => $variant->id, 'stock_real' => 0, @@ -344,7 +391,7 @@ class CartControllerTest extends TestCase $this->call( 'PATCH', - "/api/tenants/acme/cart/items/{$variant->id}", + "/api/tenants/acme/cart/items/{$cartItemId}", [], ['guest_token' => $guestToken], [], @@ -360,7 +407,7 @@ class CartControllerTest extends TestCase $this->call( 'DELETE', - "/api/tenants/acme/cart/items/{$variant->id}", + "/api/tenants/acme/cart/items/{$cartItemId}", [], ['guest_token' => $guestToken], [], diff --git a/tests/Feature/Integration/TelepagosWebhookTest.php b/tests/Feature/Integration/TelepagosWebhookTest.php index af75602..db8ec16 100644 --- a/tests/Feature/Integration/TelepagosWebhookTest.php +++ b/tests/Feature/Integration/TelepagosWebhookTest.php @@ -113,7 +113,8 @@ class TelepagosWebhookTest extends TestCase $this->assertDatabaseHas('compra_items', [ 'compra_id' => $matchingPurchase->id, - 'producto_variante_id' => $variant->id, + 'buyable_type' => ProductVariant::class, + 'buyable_id' => $variant->id, 'cantidad' => 1, 'total' => 50, ]); @@ -201,7 +202,7 @@ class TelepagosWebhookTest extends TestCase 'status' => 'active', ]); - $cart->addItem($variantId, $quantity); + $cart->addItem(ProductVariant::class, $variantId, $quantity); /** @var CheckoutService $checkoutService */ $checkoutService = app(CheckoutService::class); diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index bd9549a..b822797 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -51,7 +51,8 @@ class StorePurchaseTest extends TestCase $cartResponse = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 2, ]) ->assertOk(); @@ -111,7 +112,8 @@ class StorePurchaseTest extends TestCase ]); $this->assertDatabaseHas('carrito_items', [ 'cart_id' => $cartId, - 'producto_variante_id' => $variant->id, + 'buyable_type' => ProductVariant::class, + 'buyable_id' => $variant->id, 'cantidad' => 2, ]); $this->assertDatabaseHas('productos_variantes', [ @@ -131,7 +133,8 @@ class StorePurchaseTest extends TestCase $cartId = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 2, ]) ->assertOk() @@ -280,7 +283,8 @@ class StorePurchaseTest extends TestCase $purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); $purchase->items()->create([ - 'producto_variante_id' => $variant->id, + 'buyable_type' => ProductVariant::class, + 'buyable_id' => $variant->id, 'cantidad' => 1, 'precio_unitario' => '50.00', 'discount_total' => null, @@ -326,7 +330,8 @@ class StorePurchaseTest extends TestCase $cartId = $this->actingAs($owner, 'sanctum') ->postJson('/api/tenants/sonder/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 1, ]) ->assertOk() @@ -352,7 +357,8 @@ class StorePurchaseTest extends TestCase $cartId = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/globex/cart/items', [ - 'product_variant_id' => $variant->id, + 'buyable_type' => 'variant', + 'buyable_id' => $variant->id, 'cantidad' => 1, ]) ->assertOk() @@ -492,7 +498,7 @@ class StorePurchaseTest extends TestCase 'status' => 'active', ]); - $cart->addItem($variant->id, $quantity); + $cart->addItem(ProductVariant::class, $variant->id, $quantity); return app(CheckoutService::class)->startCheckout($tenant, $user->id, [ 'cart_id' => $cart->id,