From afd69b83934a66f5a19bf889ffa56aee750e5c44 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 21 Aug 2026 14:05:33 -0300 Subject: [PATCH] feat(checkout): track current cart purchase --- app/Domains/Cart/Models/Cart.php | 8 ++ .../Services/StockReservationService.php | 16 +++- .../Forms/Services/SaleFormService.php | 1 + .../Controllers/PurchaseController.php | 2 + app/Domains/Purchase/Models/Purchase.php | 3 + .../Checkout/CompleteCheckoutService.php | 15 +++- .../Services/Checkout/EditCheckoutService.php | 2 + .../Checkout/ReleaseCheckoutService.php | 6 ++ .../Checkout/StartCheckoutService.php | 55 +++++++++--- .../Purchase/Services/PurchaseStateGuard.php | 16 ++++ .../Sale/Services/AdminAppSaleService.php | 1 + ...add_current_purchase_id_to_carts_table.php | 46 ++++++++++ lang/en/api.php | 3 +- lang/es/api.php | 3 +- tests/Feature/Purchase/StorePurchaseTest.php | 85 ++++++++++++++++++- .../Sale/AdminAppSaleControllerTest.php | 18 ++++ tests/Unit/Forms/SaleFormServiceTest.php | 1 + 17 files changed, 258 insertions(+), 23 deletions(-) create mode 100644 database/migrations/2026_08_21_010000_add_current_purchase_id_to_carts_table.php diff --git a/app/Domains/Cart/Models/Cart.php b/app/Domains/Cart/Models/Cart.php index 4653370..5dd2216 100644 --- a/app/Domains/Cart/Models/Cart.php +++ b/app/Domains/Cart/Models/Cart.php @@ -27,6 +27,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; 'guest_token', 'status', 'origin', + 'current_purchase_id', ])] class Cart extends Model { @@ -43,6 +44,7 @@ class Cart extends Model { return [ 'user_id' => 'integer', + 'current_purchase_id' => 'integer', ]; } @@ -76,6 +78,12 @@ class Cart extends Model return $this->hasMany(Purchase::class, 'cart_id'); } + /** @return BelongsTo */ + public function currentPurchase(): BelongsTo + { + return $this->belongsTo(Purchase::class, 'current_purchase_id'); + } + public function getTotalAmount(): float { $items = $this->relationLoaded('items') diff --git a/app/Domains/Catalog/Services/StockReservationService.php b/app/Domains/Catalog/Services/StockReservationService.php index 43137f0..1b3f9d1 100644 --- a/app/Domains/Catalog/Services/StockReservationService.php +++ b/app/Domains/Catalog/Services/StockReservationService.php @@ -37,16 +37,24 @@ class StockReservationService }); } - public function commit(CartItem $cartItem, CatalogItem|Variant $selection): void - { - DB::transaction(function () use ($cartItem, $selection): void { + public function commit( + CartItem $cartItem, + CatalogItem|Variant $selection, + Purchase $purchase, + ): void { + DB::transaction(function () use ($cartItem, $selection, $purchase): void { $this->ensure($cartItem, $selection); $this->inventory->commit($selection, (int) $cartItem->cantidad); $requirements = $this->inventory->requirementsFor($selection, (int) $cartItem->cantidad); foreach ($requirements as $inventoryId => $quantity) { $reservation = $this->lockReservation($cartItem, $inventoryId); - if ($reservation === null || $reservation->status !== StockReservation::STATUS_ACTIVE || $reservation->quantity !== $quantity) { + if ( + $reservation === null + || $reservation->status !== StockReservation::STATUS_ACTIVE + || $reservation->purchase_id !== $purchase->getKey() + || $reservation->quantity !== $quantity + ) { throw new \InvalidArgumentException('La reserva de stock no coincide con el item del carrito.'); } diff --git a/app/Domains/Forms/Services/SaleFormService.php b/app/Domains/Forms/Services/SaleFormService.php index 4066e34..66b241f 100644 --- a/app/Domains/Forms/Services/SaleFormService.php +++ b/app/Domains/Forms/Services/SaleFormService.php @@ -16,6 +16,7 @@ class SaleFormService Purchase::STATUS_CANCELLED => 'Cancelada', Purchase::STATUS_REJECTED => 'Rechazada', Purchase::STATUS_EXPIRED => 'Vencida', + Purchase::STATUS_SUPERSEDED => 'Reemplazada', ]; return [ diff --git a/app/Domains/Purchase/Controllers/PurchaseController.php b/app/Domains/Purchase/Controllers/PurchaseController.php index 7d6a1b3..483b73f 100644 --- a/app/Domains/Purchase/Controllers/PurchaseController.php +++ b/app/Domains/Purchase/Controllers/PurchaseController.php @@ -114,6 +114,8 @@ class PurchaseController extends Controller return false; } + $purchaseState->lockCurrentCart($purchase); + $purchaseUpdate = [ 'payment_method' => $method, 'status' => Purchase::STATUS_PENDING_PAYMENT, diff --git a/app/Domains/Purchase/Models/Purchase.php b/app/Domains/Purchase/Models/Purchase.php index 592290c..3acf53c 100644 --- a/app/Domains/Purchase/Models/Purchase.php +++ b/app/Domains/Purchase/Models/Purchase.php @@ -47,6 +47,8 @@ class Purchase extends Model public const STATUS_EXPIRED = 'expired'; + public const STATUS_SUPERSEDED = 'superseded'; + /** @return list */ public static function statuses(): array { @@ -57,6 +59,7 @@ class Purchase extends Model self::STATUS_CANCELLED, self::STATUS_REJECTED, self::STATUS_EXPIRED, + self::STATUS_SUPERSEDED, ]; } diff --git a/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php b/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php index 1aae879..cc85bcf 100644 --- a/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/CompleteCheckoutService.php @@ -34,6 +34,8 @@ class CompleteCheckoutService return $this->loadPurchase($purchase); } + $this->purchaseState->lockCurrentCart($purchase); + $purchase->update([ 'status' => Purchase::STATUS_PENDING_PAYMENT, 'total' => $purchase->calculateCurrentTotalAmount(), @@ -53,6 +55,8 @@ class CompleteCheckoutService return $this->loadPurchase($purchase); } + $this->purchaseState->lockCurrentCart($purchase); + if ( $purchase->status !== Purchase::STATUS_PENDING_PAYMENT || ($purchase->expires_at !== null && $purchase->expires_at->isPast()) @@ -83,6 +87,7 @@ class CompleteCheckoutService Purchase::STATUS_CANCELLED, Purchase::STATUS_REJECTED, Purchase::STATUS_EXPIRED, + Purchase::STATUS_SUPERSEDED, ], true)) { throw ValidationException::withMessages([ 'purchase' => __('api.purchase.cannot_confirm'), @@ -95,12 +100,13 @@ class CompleteCheckoutService ]); } - $cart = $purchase->cart()->withTrashed()->lockForUpdate()->first(); - if ($cart?->status === 'converted' && $cart->trashed()) { + $cart = $this->purchaseState->lockCurrentCart($purchase); + + if ($cart->status === 'converted' && $cart->trashed()) { return; } - if ($cart === null || ! in_array($cart->status, ['active', 'checkout'], true)) { + if (! in_array($cart->status, ['active', 'checkout'], true)) { throw ValidationException::withMessages([ 'items' => __('api.purchase.inconsistent_reservation'), ]); @@ -149,7 +155,7 @@ class CompleteCheckoutService } try { - $this->reservations->commit($cartItem, $selection); + $this->reservations->commit($cartItem, $selection, $purchase); } catch (\InvalidArgumentException) { throw ValidationException::withMessages([ 'items' => __('api.purchase.inconsistent_reservation'), @@ -168,6 +174,7 @@ class CompleteCheckoutService Purchase::STATUS_CANCELLED, Purchase::STATUS_REJECTED, Purchase::STATUS_EXPIRED, + Purchase::STATUS_SUPERSEDED, ], true); } diff --git a/app/Domains/Purchase/Services/Checkout/EditCheckoutService.php b/app/Domains/Purchase/Services/Checkout/EditCheckoutService.php index b1bb011..cf6018c 100644 --- a/app/Domains/Purchase/Services/Checkout/EditCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/EditCheckoutService.php @@ -31,6 +31,8 @@ class EditCheckoutService ]); } + $this->purchaseState->lockCurrentCart($purchase); + $purchase->update($customerData); return $this->responses->load($purchase); diff --git a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php b/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php index 84ab282..fb723fa 100644 --- a/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/ReleaseCheckoutService.php @@ -2,6 +2,7 @@ namespace App\Domains\Purchase\Services\Checkout; +use App\Domains\Cart\Models\Cart; use App\Domains\Catalog\Models\StockReservation; use App\Domains\Catalog\Services\StockReservationService; use App\Domains\Purchase\Models\Purchase; @@ -103,6 +104,10 @@ class ReleaseCheckoutService if ($cart->status === 'active') { $this->reservations->detachFromPurchase($purchase); + Cart::query() + ->whereKey($cart->getKey()) + ->where('current_purchase_id', $purchase->getKey()) + ->update(['current_purchase_id' => null]); return; } @@ -154,6 +159,7 @@ class ReleaseCheckoutService Purchase::STATUS_CANCELLED, Purchase::STATUS_REJECTED, Purchase::STATUS_EXPIRED, + Purchase::STATUS_SUPERSEDED, ], true); } diff --git a/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php b/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php index 3e20932..b29d157 100644 --- a/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php +++ b/app/Domains/Purchase/Services/Checkout/StartCheckoutService.php @@ -193,6 +193,7 @@ class StartCheckoutService ), $cart->getKey(), ); + $cart->update(['current_purchase_id' => $purchase->getKey()]); $cartItems = $cart->items()->orderBy('id')->lockForUpdate()->get(); $this->loadCartItems($cartItems); @@ -264,6 +265,7 @@ class StartCheckoutService $cart->getTotalAmount(), $cart->getKey(), ); + $cart->update(['current_purchase_id' => $purchase->getKey()]); $purchase->items()->createMany($this->snapshots->fromCartItems($cartItems)); foreach ($cartItems as $cartItem) { @@ -279,9 +281,52 @@ class StartCheckoutService private function resolveCart(Tenant $tenant, int $userId, int $cartId): Cart { + /** @var Cart|null $candidate */ + $candidate = Cart::query()->find($cartId); + + $this->assertCartCanCheckout($candidate, $tenant, $userId); + + $candidatePurchaseId = $candidate->current_purchase_id; + $currentPurchase = $candidatePurchaseId === null + ? null + : Purchase::query()->lockForUpdate()->find($candidatePurchaseId); + /** @var Cart|null $cart */ $cart = Cart::query()->lockForUpdate()->find($cartId); + $this->assertCartCanCheckout($cart, $tenant, $userId); + + if ($cart->current_purchase_id !== $candidatePurchaseId) { + if ($cart->current_purchase_id !== null) { + throw ValidationException::withMessages([ + 'cart_id' => __('api.purchase.checkout_in_progress'), + ]); + } + + $currentPurchase = null; + } + + if ($currentPurchase?->status === Purchase::STATUS_PAID) { + throw ValidationException::withMessages([ + 'cart_id' => __('api.purchase.checkout_in_progress'), + ]); + } + + if ($currentPurchase !== null && in_array($currentPurchase->status, [ + Purchase::STATUS_CREATED, + Purchase::STATUS_PENDING_PAYMENT, + ], true)) { + $currentPurchase->update([ + 'status' => Purchase::STATUS_SUPERSEDED, + 'expires_at' => null, + ]); + } + + return $cart; + } + + private function assertCartCanCheckout(?Cart $cart, Tenant $tenant, int $userId): void + { if ($cart === null || $cart->tenant_codigo !== $tenant->codigo || $cart->user_id !== $userId) { throw new NotFoundHttpException('Cart not found for tenant.'); } @@ -291,16 +336,6 @@ class StartCheckoutService 'cart_id' => __('api.purchase.inactive_cart'), ]); } - - if ($cart->purchases() - ->whereIn('status', [Purchase::STATUS_CREATED, Purchase::STATUS_PENDING_PAYMENT]) - ->exists()) { - throw ValidationException::withMessages([ - 'cart_id' => __('api.purchase.checkout_in_progress'), - ]); - } - - return $cart; } /** @param Collection $cartItems */ diff --git a/app/Domains/Purchase/Services/PurchaseStateGuard.php b/app/Domains/Purchase/Services/PurchaseStateGuard.php index 45e8b92..2c9d4f9 100644 --- a/app/Domains/Purchase/Services/PurchaseStateGuard.php +++ b/app/Domains/Purchase/Services/PurchaseStateGuard.php @@ -2,8 +2,10 @@ namespace App\Domains\Purchase\Services; +use App\Domains\Cart\Models\Cart; use App\Domains\Purchase\Exceptions\PurchaseExpiredException; use App\Domains\Purchase\Models\Purchase; +use Illuminate\Validation\ValidationException; class PurchaseStateGuard { @@ -21,4 +23,18 @@ class PurchaseStateGuard throw new PurchaseExpiredException; } } + + public function lockCurrentCart(Purchase $purchase): Cart + { + /** @var Cart|null $cart */ + $cart = $purchase->cart()->withTrashed()->lockForUpdate()->first(); + + if ($cart === null || $cart->current_purchase_id !== $purchase->getKey()) { + throw ValidationException::withMessages([ + 'purchase' => __('api.purchase.not_current'), + ]); + } + + return $cart; + } } diff --git a/app/Domains/Sale/Services/AdminAppSaleService.php b/app/Domains/Sale/Services/AdminAppSaleService.php index 5a008d3..36afe58 100644 --- a/app/Domains/Sale/Services/AdminAppSaleService.php +++ b/app/Domains/Sale/Services/AdminAppSaleService.php @@ -126,6 +126,7 @@ class AdminAppSaleService return Purchase::query() ->where('tenant_codigo', $tenant->codigo) + ->where('status', '!=', Purchase::STATUS_SUPERSEDED) ->when($filters['q'] ?? null, function (Builder $query, string $search): void { $term = trim($search); diff --git a/database/migrations/2026_08_21_010000_add_current_purchase_id_to_carts_table.php b/database/migrations/2026_08_21_010000_add_current_purchase_id_to_carts_table.php new file mode 100644 index 0000000..7c08717 --- /dev/null +++ b/database/migrations/2026_08_21_010000_add_current_purchase_id_to_carts_table.php @@ -0,0 +1,46 @@ +foreignId('current_purchase_id') + ->nullable() + ->after('origin') + ->constrained('compras') + ->nullOnDelete(); + $table->unique('current_purchase_id', 'carts_current_purchase_unique'); + }); + + DB::table('carritos') + ->select('id') + ->orderBy('id') + ->each(function (object $cart): void { + $purchaseId = DB::table('compras') + ->where('cart_id', $cart->id) + ->whereIn('status', ['created', 'pending_payment']) + ->latest('id') + ->value('id'); + + if ($purchaseId !== null) { + DB::table('carritos') + ->where('id', $cart->id) + ->update(['current_purchase_id' => $purchaseId]); + } + }); + } + + public function down(): void + { + Schema::table('carritos', function (Blueprint $table): void { + $table->dropUnique('carts_current_purchase_unique'); + $table->dropConstrainedForeignId('current_purchase_id'); + }); + } +}; diff --git a/lang/en/api.php b/lang/en/api.php index e4780e6..1ba97e4 100644 --- a/lang/en/api.php +++ b/lang/en/api.php @@ -42,8 +42,9 @@ return [ 'payment_method_required' => 'The purchase payment method must be selected before finalizing.', 'not_editable' => 'The purchase is no longer editable.', 'insufficient_stock' => 'There is not enough stock available.', - 'cannot_confirm' => 'A cancelled, rejected, or expired purchase cannot be confirmed.', + 'cannot_confirm' => 'A cancelled, rejected, expired, or superseded purchase cannot be confirmed.', 'inconsistent_reservation' => 'The purchase has an inconsistent stock reservation.', + 'not_current' => 'The purchase is no longer the cart\'s current checkout.', 'paid_cannot_cancel' => 'A paid purchase cannot be cancelled.', 'stock' => [ 'seat_unavailable' => 'Seat :selection is no longer available.', diff --git a/lang/es/api.php b/lang/es/api.php index bbf2cd9..b1ee881 100644 --- a/lang/es/api.php +++ b/lang/es/api.php @@ -42,8 +42,9 @@ return [ 'payment_method_required' => 'Debes seleccionar el método de pago antes de finalizar la compra.', 'not_editable' => 'La compra ya no se puede modificar.', 'insufficient_stock' => 'No hay suficiente stock disponible.', - 'cannot_confirm' => 'Una compra cancelada, rechazada o vencida no se puede confirmar.', + 'cannot_confirm' => 'Una compra cancelada, rechazada, vencida o reemplazada no se puede confirmar.', 'inconsistent_reservation' => 'La compra tiene una reserva de stock inconsistente.', + 'not_current' => 'La compra ya no es el checkout actual del carrito.', 'paid_cannot_cancel' => 'Una compra pagada no se puede cancelar.', 'stock' => [ 'seat_unavailable' => 'El asiento :selection ya no está disponible.', diff --git a/tests/Feature/Purchase/StorePurchaseTest.php b/tests/Feature/Purchase/StorePurchaseTest.php index 5048121..787ae17 100644 --- a/tests/Feature/Purchase/StorePurchaseTest.php +++ b/tests/Feature/Purchase/StorePurchaseTest.php @@ -17,6 +17,7 @@ use App\Domains\Tenant\Models\Tenant; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Str; +use Illuminate\Validation\ValidationException; use Tests\TestCase; class StorePurchaseTest extends TestCase @@ -128,6 +129,7 @@ class StorePurchaseTest extends TestCase 'user_id' => $user->id, 'status' => 'active', 'origin' => Cart::ORIGIN_USER, + 'current_purchase_id' => $purchaseId, 'deleted_at' => null, ]); $this->assertDatabaseHas('carrito_items', [ @@ -150,12 +152,31 @@ class StorePurchaseTest extends TestCase ->assertJsonCount(1, 'data.items') ->assertJsonPath('data.items.0.cantidad', 2); - $this->actingAs($user, 'sanctum') + $replacementResponse = $this->actingAs($user, 'sanctum') ->postJson('/api/tenants/sonder/compras/start-checkout', [ 'cart_id' => $cartId, ]) - ->assertUnprocessable() - ->assertJsonValidationErrors('cart_id'); + ->assertCreated(); + $replacementPurchaseId = $replacementResponse->json('data.id'); + + $this->assertDatabaseHas('compras', [ + 'id' => $purchaseId, + 'status' => Purchase::STATUS_SUPERSEDED, + ]); + $this->assertDatabaseHas('carritos', [ + 'id' => $cartId, + 'current_purchase_id' => $replacementPurchaseId, + ]); + $this->assertDatabaseHas('stock_reservations', [ + 'inventory_id' => $inventory->id, + 'purchase_id' => $replacementPurchaseId, + 'quantity' => 2, + 'status' => 'active', + ]); + $this->assertDatabaseHas('inventories', [ + 'id' => $inventory->id, + 'reserved_stock' => 2, + ]); $catalogItem->update([ 'nombre' => 'Updated Product', @@ -196,6 +217,7 @@ class StorePurchaseTest extends TestCase 'id' => $response->json('data.cart_id'), 'status' => 'checkout', 'origin' => Cart::ORIGIN_DIRECT_CHECKOUT, + 'current_purchase_id' => $response->json('data.id'), ]); $this->assertDatabaseHas('compra_items', [ 'compra_id' => $response->json('data.id'), @@ -455,6 +477,7 @@ class StorePurchaseTest extends TestCase 'id' => $purchase->cart_id, 'status' => 'active', 'origin' => Cart::ORIGIN_USER, + 'current_purchase_id' => $purchase->id, 'deleted_at' => null, ]); $this->assertDatabaseCount('carrito_items', 1); @@ -469,6 +492,7 @@ class StorePurchaseTest extends TestCase 'id' => $activeCart->id, 'user_id' => $user->id, 'status' => 'active', + 'current_purchase_id' => null, 'deleted_at' => null, ]); $this->assertDatabaseHas('inventories', [ @@ -484,6 +508,60 @@ class StorePurchaseTest extends TestCase $this->assertSame(1, $activeCart->items()->count()); } + public function test_it_reassigns_a_terminal_purchase_reservation_and_rejects_a_late_confirmation(): void + { + $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); + $user = User::factory()->create(); + $variant = $this->createVariantForTenant('sonder', 10, '50.00'); + $previousPurchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2); + $cart = $previousPurchase->cart; + + $previousPurchase->update([ + 'status' => Purchase::STATUS_PENDING_PAYMENT, + 'payment_method' => 'transfer', + 'expires_at' => now()->addMinutes(30), + ]); + + $currentPurchase = app(CheckoutService::class)->startCheckout( + $previousPurchase->tenant, + $user->id, + ['cart_id' => $cart->id], + ); + + $this->assertDatabaseHas('compras', [ + 'id' => $previousPurchase->id, + 'status' => Purchase::STATUS_SUPERSEDED, + 'expires_at' => null, + ]); + $this->assertDatabaseHas('carritos', [ + 'id' => $cart->id, + 'current_purchase_id' => $currentPurchase->id, + ]); + $this->assertDatabaseHas('stock_reservations', [ + 'purchase_id' => $currentPurchase->id, + 'quantity' => 2, + 'status' => 'active', + ]); + + try { + app(CheckoutService::class)->confirmPaidPurchase($previousPurchase->fresh()); + $this->fail('A superseded purchase was allowed to commit the current reservation.'); + } catch (ValidationException $exception) { + $this->assertArrayHasKey('purchase', $exception->errors()); + } + + $this->assertDatabaseHas('inventories', [ + 'id' => $variant->inventory_id, + 'reserved_stock' => 2, + 'sold_units' => 0, + ]); + $this->assertDatabaseHas('stock_reservations', [ + 'purchase_id' => $currentPurchase->id, + 'quantity' => 2, + 'status' => 'active', + ]); + } + public function test_it_expires_the_purchase_without_mutating_the_active_cart(): void { $this->createTenant('sonder', 'Sonder', 'sonder.com.ar'); @@ -511,6 +589,7 @@ class StorePurchaseTest extends TestCase 'id' => $activeCart->id, 'user_id' => $user->id, 'status' => 'active', + 'current_purchase_id' => null, 'deleted_at' => null, ]); $this->assertDatabaseHas('stock_reservations', [ diff --git a/tests/Feature/Sale/AdminAppSaleControllerTest.php b/tests/Feature/Sale/AdminAppSaleControllerTest.php index 246c989..4d1e99a 100644 --- a/tests/Feature/Sale/AdminAppSaleControllerTest.php +++ b/tests/Feature/Sale/AdminAppSaleControllerTest.php @@ -108,6 +108,20 @@ class AdminAppSaleControllerTest extends TestCase 'total' => '20000.00', ]); + $supersededPurchase = Purchase::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'status' => Purchase::STATUS_SUPERSEDED, + 'total' => '50000.00', + ]); + PurchaseItem::query()->create([ + 'compra_id' => $supersededPurchase->id, + 'source_catalog_item_id' => $catalogItem->id, + 'item_nombre' => $catalogItem->nombre, + 'cantidad' => 5, + 'precio_unitario' => '10000.00', + 'total' => '50000.00', + ]); + $this->getJson('/api/v1/adminapp/tenant/sales?sort_by=id&sort_direction=asc') ->assertOk() ->assertJsonCount(3, 'data') @@ -117,6 +131,10 @@ class AdminAppSaleControllerTest extends TestCase ->assertJsonPath('data.1.quantity', 4) ->assertJsonPath('data.2.id', $paidPurchase->id) ->assertJsonPath('data.2.quantity', 2); + + $this->getJson('/api/v1/adminapp/tenant/sales?status='.Purchase::STATUS_SUPERSEDED) + ->assertOk() + ->assertJsonCount(0, 'data'); } public function test_authentication_is_required_to_read_a_sale_detail(): void diff --git a/tests/Unit/Forms/SaleFormServiceTest.php b/tests/Unit/Forms/SaleFormServiceTest.php index 41bdab2..c6b1e4a 100644 --- a/tests/Unit/Forms/SaleFormServiceTest.php +++ b/tests/Unit/Forms/SaleFormServiceTest.php @@ -20,6 +20,7 @@ class SaleFormServiceTest extends TestCase 'Cancelada', 'Rechazada', 'Vencida', + 'Reemplazada', ], array_column($form['statuses'], 'name')); } }