From 3206e293eb42bb70f70186128595f87730f0e989 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 19 Aug 2026 12:24:48 -0300 Subject: [PATCH] refactor(cart): own checkout item editing --- ShopIt_API_Postman_Collection.json | 200 ++++++++++-------- .../Cart/Controllers/CartController.php | 41 ++++ app/Domains/Cart/Models/Cart.php | 6 + app/Domains/Cart/Services/CartService.php | 141 ++++++++++++ app/Domains/Cart/routes/api.php | 7 + .../Controllers/PurchaseController.php | 32 --- .../UpdatePurchaseItemQuantityRequest.php | 23 -- .../Services/Checkout/EditCheckoutService.php | 117 ---------- .../Purchase/Services/CheckoutService.php | 13 -- app/Domains/Purchase/documentacion/README.md | 6 +- app/Domains/Purchase/routes/api.php | 2 - postman/generate-shopit-collection.php | 10 +- tests/Feature/Purchase/StorePurchaseTest.php | 34 +-- 13 files changed, 330 insertions(+), 302 deletions(-) delete mode 100644 app/Domains/Purchase/Requests/UpdatePurchaseItemQuantityRequest.php diff --git a/ShopIt_API_Postman_Collection.json b/ShopIt_API_Postman_Collection.json index 7bb4ba5..e829712 100644 --- a/ShopIt_API_Postman_Collection.json +++ b/ShopIt_API_Postman_Collection.json @@ -572,6 +572,113 @@ } }, "response": [] + }, + { + "name": "Prepare Checkout Cart Editing Cart", + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "description": "Ruta Laravel: `POST /api/tenants/{tenant:codigo}/checkout-carts/{cart}/edit`\n\nControlador: `App\\Domains\\Cart\\Controllers\\CartController@prepareCheckoutEditing`\n\nRequiere autenticación Sanctum.", + "url": { + "raw": "{{base_url}}/api/tenants/{{tenant_code}}/checkout-carts/{{cart}}/edit", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "tenants", + "{{tenant_code}}", + "checkout-carts", + "{{cart}}", + "edit" + ] + }, + "body": { + "mode": "raw", + "raw": "[]", + "options": { + "raw": { + "language": "json" + } + } + }, + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + } + }, + "response": [] + }, + { + "name": "Update Checkout Cart Item Cart", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Accept", + "value": "application/json", + "type": "text" + }, + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "description": "Ruta Laravel: `PATCH /api/tenants/{tenant:codigo}/checkout-carts/{cart}/items/{cartItem}`\n\nControlador: `App\\Domains\\Cart\\Controllers\\CartController@updateCheckoutItem`\n\nRequiere autenticación Sanctum.", + "url": { + "raw": "{{base_url}}/api/tenants/{{tenant_code}}/checkout-carts/{{cart}}/items/{{cart_item_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "tenants", + "{{tenant_code}}", + "checkout-carts", + "{{cart}}", + "items", + "{{cart_item_id}}" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"cantidad\": 2\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{token}}", + "type": "string" + } + ] + } + }, + "response": [] } ] }, @@ -1115,99 +1222,6 @@ }, "response": [] }, - { - "name": "Prepare Item Editing Purchase", - "request": { - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `POST /api/tenants/{tenant:codigo}/compras/{compra}/edit-items`\n\nControlador: `App\\Domains\\Purchase\\Controllers\\PurchaseController@prepareItemEditing`\n\nRequiere autenticación Sanctum.", - "url": { - "raw": "{{base_url}}/api/tenants/{{tenant_code}}/compras/{{purchase_id}}/edit-items", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants", - "{{tenant_code}}", - "compras", - "{{purchase_id}}", - "edit-items" - ] - }, - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - } - }, - "response": [] - }, - { - "name": "Update Item Quantity Purchase", - "request": { - "method": "PATCH", - "header": [ - { - "key": "Accept", - "value": "application/json", - "type": "text" - }, - { - "key": "Content-Type", - "value": "application/json", - "type": "text" - } - ], - "description": "Ruta Laravel: `PATCH /api/tenants/{tenant:codigo}/compras/{compra}/items/{item}`\n\nControlador: `App\\Domains\\Purchase\\Controllers\\PurchaseController@updateItemQuantity`\n\nRequiere autenticación Sanctum.", - "url": { - "raw": "{{base_url}}/api/tenants/{{tenant_code}}/compras/{{purchase_id}}/items/{{purchase_item_id}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "tenants", - "{{tenant_code}}", - "compras", - "{{purchase_id}}", - "items", - "{{purchase_item_id}}" - ] - }, - "body": { - "mode": "raw", - "raw": "{\n \"quantity\": 2\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{token}}", - "type": "string" - } - ] - } - }, - "response": [] - }, { "name": "Create Payment Intent Purchase", "request": { diff --git a/app/Domains/Cart/Controllers/CartController.php b/app/Domains/Cart/Controllers/CartController.php index 368a77c..6910224 100644 --- a/app/Domains/Cart/Controllers/CartController.php +++ b/app/Domains/Cart/Controllers/CartController.php @@ -2,6 +2,7 @@ namespace App\Domains\Cart\Controllers; +use App\Domains\Cart\Models\Cart; use App\Domains\Cart\Models\CartItem; use App\Domains\Cart\Requests\AddCartItemRequest; use App\Domains\Cart\Requests\UpdateCartItemQuantityRequest; @@ -77,6 +78,46 @@ class CartController extends Controller ]); } + public function updateCheckoutItem( + UpdateCartItemQuantityRequest $request, + Tenant $tenant, + Cart $cart, + CartItem $cartItem, + ): CartResource { + $updatesVariant = $request->exists('variant_id'); + + return CartResource::make( + $this->cartService->updateCheckoutItem( + $tenant, + $request, + $cart, + $cartItem->getKey(), + (int) $request->validated('cantidad'), + $updatesVariant + ? ($request->validated('variant_id') !== null + ? (int) $request->validated('variant_id') + : null) + : $cartItem->variant_id, + $updatesVariant, + ), + )->additional([ + 'code' => $updatesVariant ? 'cart.item_updated' : 'cart.quantity_updated', + 'message' => $updatesVariant + ? __('api.cart.item_updated') + : __('api.cart.quantity_updated'), + ]); + } + + public function prepareCheckoutEditing( + Request $request, + Tenant $tenant, + Cart $cart, + ): CartResource { + return CartResource::make( + $this->cartService->prepareCheckoutEditing($tenant, $request, $cart), + ); + } + public function removeItem(Request $request, Tenant $tenant, CartItem $cartItem): CartResource { return CartResource::make( diff --git a/app/Domains/Cart/Models/Cart.php b/app/Domains/Cart/Models/Cart.php index 57edbfb..6032b94 100644 --- a/app/Domains/Cart/Models/Cart.php +++ b/app/Domains/Cart/Models/Cart.php @@ -143,6 +143,7 @@ class Cart extends Model int $quantity, ?int $variantId = null, bool $updateVariant = false, + ?int $excludedPurchaseId = null, ): CartItem { if ($quantity <= 0) { throw ValidationException::withMessages([ @@ -155,6 +156,7 @@ class Cart extends Model $quantity, $variantId, $updateVariant, + $excludedPurchaseId, ): CartItem { /** @var CartItem $item */ $item = $this->items() @@ -182,6 +184,7 @@ class Cart extends Model $this->assertUserPurchaseLimit( $nextSelection, $otherVariantsQuantity + $quantity, + $excludedPurchaseId, ); app(StockReservationService::class)->release($item, $currentSelection, $item->cantidad); @@ -228,6 +231,7 @@ class Cart extends Model $this->assertUserPurchaseLimit( $currentSelection, $otherVariantsQuantity + $quantity, + $excludedPurchaseId, ); } @@ -350,6 +354,7 @@ class Cart extends Model private function assertUserPurchaseLimit( CatalogItem|Variant $selectedItem, int $cartQuantity, + ?int $excludedPurchaseId = null, ): void { if ($this->user_id === null) { return; @@ -363,6 +368,7 @@ class Cart extends Model $catalogItem, $this->user_id, $cartQuantity, + $excludedPurchaseId, field: 'cantidad', ); } diff --git a/app/Domains/Cart/Services/CartService.php b/app/Domains/Cart/Services/CartService.php index 954f784..bbdd2be 100644 --- a/app/Domains/Cart/Services/CartService.php +++ b/app/Domains/Cart/Services/CartService.php @@ -4,15 +4,23 @@ namespace App\Domains\Cart\Services; use App\Domains\Auth\Models\User; use App\Domains\Cart\Models\Cart; +use App\Domains\Catalog\Services\StockReservationService; +use App\Domains\Purchase\Models\Purchase; use App\Domains\Tenant\Models\Tenant; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class CartService { + public function __construct( + private readonly StockReservationService $reservations, + ) {} + public function show(Tenant $tenant, Request $request): Cart { $resolvedIdentity = $this->resolveIdentity($request); @@ -66,6 +74,139 @@ class CartService return $this->loadCart($cart); } + public function updateCheckoutItem( + Tenant $tenant, + Request $request, + Cart $cart, + int $cartItemId, + int $quantity, + ?int $variantId, + bool $updateVariant, + ): Cart { + $user = $request->user() ?? Auth::guard('sanctum')->user(); + + if (! $user instanceof User) { + throw new NotFoundHttpException('Checkout cart not found.'); + } + + return DB::transaction(function () use ( + $tenant, + $user, + $cart, + $cartItemId, + $quantity, + $variantId, + $updateVariant, + ): Cart { + /** @var Purchase|null $purchase */ + $purchase = Purchase::query() + ->where('cart_id', $cart->getKey()) + ->where('tenant_codigo', $tenant->codigo) + ->where('user_id', $user->getKey()) + ->where('status', Purchase::STATUS_CREATED) + ->whereDoesntHave('items') + ->lockForUpdate() + ->first(); + + if ($purchase === null) { + throw new NotFoundHttpException('Checkout cart not found.'); + } + + if ($purchase->expires_at !== null && $purchase->expires_at->isPast()) { + throw ValidationException::withMessages([ + 'cart' => __('api.purchase.not_editable'), + ]); + } + + /** @var Cart|null $checkoutCart */ + $checkoutCart = Cart::query() + ->whereKey($cart->getKey()) + ->where('tenant_codigo', $tenant->codigo) + ->where('user_id', $user->getKey()) + ->where('status', 'checkout') + ->lockForUpdate() + ->first(); + + if ($checkoutCart === null) { + throw new NotFoundHttpException('Checkout cart not found.'); + } + + $checkoutCart->updateItem( + $cartItemId, + $quantity, + $variantId, + $updateVariant, + $purchase->getKey(), + ); + $purchase->update(['total' => $checkoutCart->getTotalAmount()]); + + return $this->loadCart($checkoutCart); + }); + } + + public function prepareCheckoutEditing( + Tenant $tenant, + Request $request, + Cart $cart, + ): Cart { + $user = $request->user() ?? Auth::guard('sanctum')->user(); + + if (! $user instanceof User) { + throw new NotFoundHttpException('Checkout cart not found.'); + } + + return DB::transaction(function () use ($tenant, $user, $cart): Cart { + /** @var Purchase|null $purchase */ + $purchase = Purchase::query() + ->where('cart_id', $cart->getKey()) + ->where('tenant_codigo', $tenant->codigo) + ->where('user_id', $user->getKey()) + ->whereIn('status', [ + Purchase::STATUS_CREATED, + Purchase::STATUS_PENDING_PAYMENT, + ]) + ->whereDoesntHave('items') + ->lockForUpdate() + ->first(); + + if ($purchase === null) { + throw new NotFoundHttpException('Checkout cart not found.'); + } + + if ($purchase->expires_at !== null && $purchase->expires_at->isPast()) { + throw ValidationException::withMessages([ + 'cart' => __('api.purchase.not_editable'), + ]); + } + + /** @var Cart|null $checkoutCart */ + $checkoutCart = Cart::query() + ->whereKey($cart->getKey()) + ->where('tenant_codigo', $tenant->codigo) + ->where('user_id', $user->getKey()) + ->where('status', 'checkout') + ->lockForUpdate() + ->first(); + + if ($checkoutCart === null) { + throw new NotFoundHttpException('Checkout cart not found.'); + } + + $purchase->telepagosQr()->delete(); + $purchase->update([ + 'status' => Purchase::STATUS_CREATED, + 'payment_method' => null, + 'transfer_payer_dni' => null, + 'expires_at' => now()->addMinutes( + max(1, (int) config('purchase.checkout_expiration_minutes', 30)), + ), + ]); + $this->reservations->syncPurchaseExpiration($purchase); + + return $this->loadCart($checkoutCart); + }); + } + public function removeItem(Tenant $tenant, Request $request, int $cartItemId): Cart { $identity = $this->requireIdentity($request); diff --git a/app/Domains/Cart/routes/api.php b/app/Domains/Cart/routes/api.php index 28488a7..df24c14 100644 --- a/app/Domains/Cart/routes/api.php +++ b/app/Domains/Cart/routes/api.php @@ -10,3 +10,10 @@ Route::prefix('tenants/{tenant:codigo}') Route::patch('cart/items/{cartItem}', [CartController::class, 'updateItemQuantity']); Route::delete('cart/items/{cartItem}', [CartController::class, 'removeItem']); }); + +Route::prefix('tenants/{tenant:codigo}') + ->middleware('auth:sanctum') + ->group(function (): void { + Route::post('checkout-carts/{cart}/edit', [CartController::class, 'prepareCheckoutEditing']); + Route::patch('checkout-carts/{cart}/items/{cartItem}', [CartController::class, 'updateCheckoutItem']); + }); diff --git a/app/Domains/Purchase/Controllers/PurchaseController.php b/app/Domains/Purchase/Controllers/PurchaseController.php index 440d6a5..fcda49d 100644 --- a/app/Domains/Purchase/Controllers/PurchaseController.php +++ b/app/Domains/Purchase/Controllers/PurchaseController.php @@ -7,7 +7,6 @@ use App\Domains\Purchase\Models\Purchase; use App\Domains\Purchase\Requests\PaymentIntentRequest; use App\Domains\Purchase\Requests\StartCheckoutRequest; use App\Domains\Purchase\Requests\UpdatePurchaseCustomerRequest; -use App\Domains\Purchase\Requests\UpdatePurchaseItemQuantityRequest; use App\Domains\Purchase\Resources\PurchaseResource; use App\Domains\Purchase\Services\CheckoutService; use App\Domains\Tenant\Models\Tenant; @@ -83,37 +82,6 @@ class PurchaseController extends Controller ); } - public function updateItemQuantity( - UpdatePurchaseItemQuantityRequest $request, - Tenant $tenant, - Purchase $compra, - int $item, - CheckoutService $checkoutService, - ): PurchaseResource { - $compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra); - - return PurchaseResource::make( - $checkoutService->updateItemQuantity( - $compra, - $item, - (int) $request->validated('quantity'), - ), - ); - } - - public function prepareItemEditing( - Request $request, - Tenant $tenant, - Purchase $compra, - CheckoutService $checkoutService, - ): PurchaseResource { - $compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra); - - return PurchaseResource::make( - $checkoutService->prepareItemEditing($compra), - ); - } - public function paymentIntent( PaymentIntentRequest $request, Tenant $tenant, diff --git a/app/Domains/Purchase/Requests/UpdatePurchaseItemQuantityRequest.php b/app/Domains/Purchase/Requests/UpdatePurchaseItemQuantityRequest.php deleted file mode 100644 index 9810352..0000000 --- a/app/Domains/Purchase/Requests/UpdatePurchaseItemQuantityRequest.php +++ /dev/null @@ -1,23 +0,0 @@ -user() !== null; - } - - /** - * @return array - */ - public function rules(): array - { - return [ - 'quantity' => ['required', 'integer', 'min:1', 'max:100'], - ]; - } -} diff --git a/app/Domains/Purchase/Services/Checkout/EditCheckoutService.php b/app/Domains/Purchase/Services/Checkout/EditCheckoutService.php index 0151a2b..f6f6d0d 100644 --- a/app/Domains/Purchase/Services/Checkout/EditCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/EditCheckoutService.php @@ -2,23 +2,12 @@ namespace App\Domains\Purchase\Services\Checkout; -use App\Domains\Cart\Models\CartItem; -use App\Domains\Catalog\Models\Variant; -use App\Domains\Catalog\Services\StockReservationService; use App\Domains\Purchase\Models\Purchase; -use App\Domains\Purchase\Services\UserPurchaseLimitService; use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class EditCheckoutService { - public function __construct( - private readonly StockReservationService $reservations, - private readonly UserPurchaseLimitService $purchaseLimits, - private readonly CatalogSelectionResolver $selections, - ) {} - /** @param array $customerData */ public function updateCustomer(Purchase $purchase, array $customerData): Purchase { @@ -32,112 +21,6 @@ class EditCheckoutService }); } - public function updateItemQuantity( - Purchase $purchase, - int $itemId, - int $quantity, - ): Purchase { - return DB::transaction(function () use ($purchase, $itemId, $quantity): Purchase { - $purchase = $this->lockPurchase($purchase); - - if ($purchase->status !== Purchase::STATUS_CREATED || $this->hasExpired($purchase)) { - throw ValidationException::withMessages([ - 'purchase' => __('api.purchase.not_editable'), - ]); - } - - return $this->updateCartItemQuantity($purchase, $itemId, $quantity); - }); - } - - public function prepareItemEditing(Purchase $purchase): Purchase - { - return DB::transaction(function () use ($purchase): Purchase { - $purchase = $this->lockPurchase($purchase); - $this->assertEditable($purchase); - - $purchase->telepagosQr()->delete(); - $purchase->update([ - 'status' => Purchase::STATUS_CREATED, - 'payment_method' => null, - 'transfer_payer_dni' => null, - 'expires_at' => now()->addMinutes( - max(1, (int) config('purchase.checkout_expiration_minutes', 30)), - ), - ]); - - $this->attachCartReservations($purchase); - - return $this->loadPurchase($purchase); - }); - } - - private function updateCartItemQuantity(Purchase $purchase, int $itemId, int $quantity): Purchase - { - $cart = $purchase->cart()->lockForUpdate()->first(); - if ($cart === null || $cart->status !== 'checkout') { - throw new NotFoundHttpException('Checkout cart not found.'); - } - - /** @var CartItem|null $cartItem */ - $cartItem = $cart->items()->whereKey($itemId)->lockForUpdate()->first(); - if ($cartItem === null) { - throw new NotFoundHttpException('Checkout item not found.'); - } - - $selection = $this->selections->resolve( - $purchase->tenant, - (int) $cartItem->catalog_item_id, - $cartItem->variant_id === null ? null : (int) $cartItem->variant_id, - 'item', - ); - $difference = $quantity - (int) $cartItem->cantidad; - - try { - if ($difference > 0) { - $otherItemQuantity = (int) $cart->items() - ->where('catalog_item_id', $cartItem->catalog_item_id) - ->whereKeyNot($cartItem->getKey()) - ->sum('cantidad'); - $catalogItem = $selection instanceof Variant ? $selection->catalogItem : $selection; - $this->purchaseLimits->assertCanPurchase( - $catalogItem, - (int) $purchase->user_id, - $otherItemQuantity + $quantity, - $purchase->getKey(), - ); - $this->reservations->reserve($cartItem, $selection, $difference); - } elseif ($difference < 0) { - $this->reservations->release($cartItem, $selection, abs($difference)); - } - } catch (\InvalidArgumentException) { - throw ValidationException::withMessages([ - 'quantity' => __('api.purchase.insufficient_stock'), - ]); - } - - $cartItem->update(['cantidad' => $quantity]); - $cart->unsetRelation('items'); - $purchase->setRelation('cart', $cart); - $purchase->update(['total' => $cart->getTotalAmount()]); - - return $this->loadPurchase($purchase); - } - - private function attachCartReservations(Purchase $purchase): void - { - $cartItems = $purchase->cart?->items()->lockForUpdate()->get() ?? collect(); - foreach ($cartItems as $cartItem) { - $selection = $this->selections->resolve( - $purchase->tenant, - (int) $cartItem->catalog_item_id, - $cartItem->variant_id === null ? null : (int) $cartItem->variant_id, - 'item', - ); - $this->reservations->attachToPurchase($cartItem, $selection, $purchase); - } - } - private function assertEditable(Purchase $purchase): void { if ( diff --git a/app/Domains/Purchase/Services/CheckoutService.php b/app/Domains/Purchase/Services/CheckoutService.php index 47e5c82..411f1b2 100644 --- a/app/Domains/Purchase/Services/CheckoutService.php +++ b/app/Domains/Purchase/Services/CheckoutService.php @@ -48,19 +48,6 @@ class CheckoutService return $this->editor->updateCustomer($purchase, $customerData); } - public function updateItemQuantity( - Purchase $purchase, - int $itemId, - int $quantity, - ): Purchase { - return $this->editor->updateItemQuantity($purchase, $itemId, $quantity); - } - - public function prepareItemEditing(Purchase $purchase): Purchase - { - return $this->editor->prepareItemEditing($purchase); - } - public function confirmPurchase(Purchase $purchase): void { $this->completer->confirm($purchase); diff --git a/app/Domains/Purchase/documentacion/README.md b/app/Domains/Purchase/documentacion/README.md index 117ffbe..420e3e7 100644 --- a/app/Domains/Purchase/documentacion/README.md +++ b/app/Domains/Purchase/documentacion/README.md @@ -16,14 +16,16 @@ Implementa el ciclo de compra y checkout: crea la cabecera de compra desde un ca `CheckoutService` es la fachada estable. Delega en: - `StartCheckoutService`: inicia la compra desde el carrito o crea un carrito técnico para compra directa, sin crear todavía `PurchaseItem`. -- `EditCheckoutService`: modifica cliente o cantidades del carrito de checkout antes del cierre. +- `EditCheckoutService`: modifica los datos del comprador antes del cierre. - `CompleteCheckoutService`: completa, envía a revisión o materializa los `PurchaseItem` al confirmar el pago. - `ReleaseCheckoutService`: cancela, vence y procesa vencimientos pendientes. -- `SourceCartService`: sincroniza, restaura o finaliza el carrito fuente. +- `SourceCartService`: restaura o finaliza el carrito fuente. - `CatalogSelectionResolver` y `PurchaseItemSnapshotFactory`: resuelven selecciones y generan snapshots. Durante `created` y `pending_payment`, `PurchaseResource` publica las líneas del carrito con `items_source=cart`; una compra materializada publica `items_source=purchase`. Los datos descriptivos y económicos del checkout se resuelven siempre desde el catálogo vigente. +Las cantidades y variantes se editan mediante el dominio Cart. Los endpoints autenticados `POST /checkout-carts/{cart}/edit` y `PATCH /checkout-carts/{cart}/items/{cartItem}` validan que el carrito pertenezca al usuario y a una compra editable; Purchase no expone operaciones sobre líneas antes de la confirmación. + `UserPurchaseLimitService` controla límites de compra y `CheckoutService` conserva el punto de entrada para controladores e integraciones. ## Endpoints diff --git a/app/Domains/Purchase/routes/api.php b/app/Domains/Purchase/routes/api.php index 41cc919..f8e8d5d 100644 --- a/app/Domains/Purchase/routes/api.php +++ b/app/Domains/Purchase/routes/api.php @@ -7,8 +7,6 @@ Route::prefix('tenants/{tenant:codigo}')->middleware('auth:sanctum')->group(func Route::get('compras', [PurchaseController::class, 'index']); Route::post('compras/start-checkout', [PurchaseController::class, 'startCheckout']); Route::get('compras/{compra}', [PurchaseController::class, 'show']); - Route::post('compras/{compra}/edit-items', [PurchaseController::class, 'prepareItemEditing']); - Route::patch('compras/{compra}/items/{item}', [PurchaseController::class, 'updateItemQuantity']); Route::patch('compras/{compra}/customer-data', [PurchaseController::class, 'updateCustomerData']); Route::post('compras/{compra}/payment-intent', [PurchaseController::class, 'paymentIntent']); Route::post('compras/{compra}/complete', [PurchaseController::class, 'complete']); diff --git a/postman/generate-shopit-collection.php b/postman/generate-shopit-collection.php index bee6476..2ca60a6 100644 --- a/postman/generate-shopit-collection.php +++ b/postman/generate-shopit-collection.php @@ -8,7 +8,6 @@ declare(strict_types=1); * Run from the backend root with: * php postman/generate-shopit-collection.php */ - $root = dirname(__DIR__); chdir($root); @@ -38,7 +37,7 @@ function jsonBody(array $payload): array } /** @param array $fields - * @return array + * @return array */ function formDataBody(array $fields, array $fileFields = []): array { @@ -85,7 +84,8 @@ function bodyFor(string $method, string $uri): ?array 'POST api/tenants/{tenant:codigo}/catalog-items/{catalogItem}/variant-options' => ['selected_values' => ['color' => 'azul'], 'cart_item_id' => '{{cart_item_id}}'], 'POST api/tenants/{tenant:codigo}/compras/start-checkout' => ['cart_id' => '{{cart_id}}'], 'PATCH api/tenants/{tenant:codigo}/compras/{compra}/customer-data' => ['dni' => '30123456', 'telefono' => '+5491112345678', 'nombre_apellido' => 'Usuario Demo', 'email' => '{{user_email}}'], - 'PATCH api/tenants/{tenant:codigo}/compras/{compra}/items/{item}' => ['quantity' => 2], + 'PATCH api/tenants/{tenant:codigo}/checkout-carts/{cart}/items/{cartItem}' => ['cantidad' => 2], + 'POST api/tenants/{tenant:codigo}/checkout-carts/{cart}/edit' => [], 'POST api/tenants/{tenant:codigo}/compras/{compra}/payment-intent' => ['method' => 'transfer', 'transfer_payer_dni' => '30123456'], 'POST api/tenants/{tenant:codigo}/tickets/pdf' => ['ticket_ids' => [1]], 'POST api/v1/adminapp/login' => ['email' => '{{admin_email}}', 'password' => '{{admin_password}}'], @@ -245,10 +245,12 @@ function requestName(string $method, string $action, bool $multiMethod): string $verbs = [ 'index' => 'List', 'store' => 'Create', 'show' => 'Get', 'update' => 'Update', 'destroy' => 'Delete', 'addItem' => 'Add Item', 'updateItemQuantity' => 'Update Item Quantity', + 'updateCheckoutItem' => 'Update Checkout Cart Item', + 'prepareCheckoutEditing' => 'Prepare Checkout Cart Editing', 'removeItem' => 'Remove Item', 'search' => 'Search', 'category' => 'Get Category', 'featuredGroupItems' => 'List Featured Group Items', 'variantOptions' => 'Get Variant Options', 'startCheckout' => 'Start Checkout', 'updateCustomerData' => 'Update Customer Data', - 'prepareItemEditing' => 'Prepare Item Editing', 'paymentIntent' => 'Create Payment Intent', + 'paymentIntent' => 'Create Payment Intent', 'submitForReview' => 'Submit for Review', 'cancel' => 'Cancel', 'complete' => 'Complete', 'downloadPdf' => 'Download PDF', 'downloadModificationsPdf' => 'Download Modifications PDF', 'modifications' => 'List Modifications', 'tickets' => 'List Tickets', 'confirm' => 'Confirm', diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index 7cee351..6b6f337 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -568,14 +568,12 @@ class StorePurchaseTest extends TestCase $itemId = $purchase->cart->items->firstOrFail()->id; $this->actingAs($user, 'sanctum') - ->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [ - 'quantity' => 4, + ->patchJson("/api/tenants/sonder/checkout-carts/{$purchase->cart_id}/items/{$itemId}", [ + 'cantidad' => 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'); + ->assertJsonPath('data.items.0.cantidad', 4) + ->assertJsonPath('data.subtotal', '200.00'); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, @@ -589,12 +587,17 @@ class StorePurchaseTest extends TestCase ]); $this->actingAs($user, 'sanctum') - ->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [ - 'quantity' => 1, + ->patchJson("/api/tenants/sonder/checkout-carts/{$purchase->cart_id}/items/{$itemId}", [ + 'cantidad' => 1, ]) ->assertOk() - ->assertJsonPath('data.items.0.quantity', 1) - ->assertJsonPath('data.total', '50.00'); + ->assertJsonPath('data.items.0.cantidad', 1) + ->assertJsonPath('data.subtotal', '50.00'); + + $this->assertDatabaseHas('compras', [ + 'id' => $purchase->id, + 'total' => '50.00', + ]); $this->assertDatabaseHas('inventories', [ 'id' => $variant->inventory_id, @@ -612,11 +615,11 @@ class StorePurchaseTest extends TestCase $itemId = $purchase->cart->items->firstOrFail()->id; $this->actingAs($user, 'sanctum') - ->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [ - 'quantity' => 4, + ->patchJson("/api/tenants/sonder/checkout-carts/{$purchase->cart_id}/items/{$itemId}", [ + 'cantidad' => 4, ]) ->assertUnprocessable() - ->assertJsonValidationErrors('quantity'); + ->assertJsonValidationErrors('cantidad'); $this->assertDatabaseHas('carrito_items', [ 'id' => $itemId, @@ -644,10 +647,9 @@ class StorePurchaseTest extends TestCase ]); $this->actingAs($user, 'sanctum') - ->postJson("/api/tenants/sonder/compras/{$purchase->id}/edit-items") + ->postJson("/api/tenants/sonder/checkout-carts/{$purchase->cart_id}/edit") ->assertOk() - ->assertJsonPath('data.status', Purchase::STATUS_CREATED) - ->assertJsonPath('data.payment_method', null); + ->assertJsonPath('data.status', 'checkout'); $this->assertDatabaseHas('compras', [ 'id' => $purchase->id,