refactor(cart): invalidate payment on actual changes

This commit is contained in:
2026-08-19 12:38:21 -03:00
parent 3206e293eb
commit 8d6bcdcc43
7 changed files with 69 additions and 129 deletions

View File

@@ -108,16 +108,6 @@ class CartController extends Controller
]);
}
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(

View File

@@ -4,6 +4,7 @@ namespace App\Domains\Cart\Services;
use App\Domains\Auth\Models\User;
use App\Domains\Cart\Models\Cart;
use App\Domains\Cart\Models\CartItem;
use App\Domains\Catalog\Services\StockReservationService;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Tenant\Models\Tenant;
@@ -98,64 +99,6 @@ class CartService
$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())
@@ -192,11 +135,37 @@ class CartService
throw new NotFoundHttpException('Checkout cart not found.');
}
/** @var CartItem|null $cartItem */
$cartItem = $checkoutCart->items()
->whereKey($cartItemId)
->lockForUpdate()
->first();
if ($cartItem === null) {
throw new NotFoundHttpException('Checkout item not found.');
}
$hasChanges = (int) $cartItem->cantidad !== $quantity
|| ($updateVariant && $cartItem->variant_id !== $variantId);
if (! $hasChanges) {
return $this->loadCart($checkoutCart);
}
$checkoutCart->updateItem(
$cartItemId,
$quantity,
$variantId,
$updateVariant,
$purchase->getKey(),
);
$purchase->telepagosQr()->delete();
$purchase->update([
'status' => Purchase::STATUS_CREATED,
'payment_method' => null,
'transfer_payer_dni' => null,
'total' => $checkoutCart->getTotalAmount(),
'expires_at' => now()->addMinutes(
max(1, (int) config('purchase.checkout_expiration_minutes', 30)),
),

View File

@@ -14,6 +14,5 @@ Route::prefix('tenants/{tenant:codigo}')
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']);
});

View File

@@ -24,7 +24,7 @@ Implementa el ciclo de compra y checkout: crea la cabecera de compra desde un ca
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.
Las cantidades y variantes se editan mediante el dominio Cart. El endpoint autenticado `PATCH /checkout-carts/{cart}/items/{cartItem}` valida que el carrito pertenezca al usuario y a una compra editable. Cuando existe un cambio real, invalida atómicamente el intento de pago anterior, recalcula el total y renueva la reserva; 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.