id(); $table->string('tenant_codigo'); $table->string('status'); $table->unsignedBigInteger('current_purchase_id')->nullable(); $table->unsignedBigInteger('current_stock_reservation_id')->nullable(); $table->timestamps(); $table->softDeletes(); }); Schema::create('carrito_items', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('cart_id'); $table->unsignedBigInteger('variant_id'); }); Schema::create('variantes', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('event_date_id')->nullable(); $table->softDeletes(); }); Schema::create('event_dates', function (Blueprint $table): void { $table->id(); $table->date('date')->nullable(); $table->time('time_start')->nullable(); }); Schema::create('variant_event_dates', function (Blueprint $table): void { $table->unsignedBigInteger('variant_id'); $table->unsignedBigInteger('event_date_id'); }); Schema::create('compras', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('stock_reservation_id'); }); Schema::create('inventories', function (Blueprint $table): void { $table->id(); $table->integer('real_stock')->default(10); $table->integer('reserved_stock')->default(2); $table->integer('sold_units')->default(0); $table->integer('refunded_units')->default(0); }); Schema::create('stock_reservations', function (Blueprint $table): void { $table->id(); $table->string('status'); $table->timestamp('expires_at')->nullable(); $table->timestamp('released_at')->nullable(); $table->timestamp('expired_at')->nullable(); $table->string('release_reason')->nullable(); $table->timestamps(); }); Schema::create('stock_reservation_lines', function (Blueprint $table): void { $table->id(); $table->unsignedBigInteger('stock_reservation_id'); $table->unsignedBigInteger('inventory_id'); $table->integer('quantity'); }); DB::table('event_dates')->insert([['id' => 1], ['id' => 2]]); DB::table('variantes')->insert([ ['id' => 1, 'event_date_id' => 1], ['id' => 2, 'event_date_id' => 2], ['id' => 3, 'event_date_id' => null], ]); DB::table('variant_event_dates')->insert(['variant_id' => 3, 'event_date_id' => 1]); } public static function releaseReasons(): array { return [ [StockReservationService::REASON_EVENT_DATE_RESCHEDULED], [StockReservationService::REASON_EVENT_DATE_SUSPENDED], ]; } #[DataProvider('releaseReasons')] public function test_invalidates_whole_cart_and_releases_all_its_stock_only_once(string $reason): void { $cart = $this->cart(1); $otherInventory = DB::table('inventories')->insertGetId([]); DB::table('carrito_items')->insert(['cart_id' => $cart->id, 'variant_id' => 2]); DB::table('stock_reservation_lines')->insert([ 'stock_reservation_id' => $cart->current_stock_reservation_id, 'inventory_id' => $otherInventory, 'quantity' => 2, ]); $reservationId = $cart->current_stock_reservation_id; $this->invalidate($reason); $this->invalidate($reason); $this->assertSame(Cart::STATUS_EXPIRED, $cart->fresh()->status); $this->assertNull($cart->fresh()->current_stock_reservation_id); $this->assertSame(0, (int) Inventory::query()->sum('reserved_stock')); $this->assertSame(20, (int) Inventory::query()->sum('real_stock')); $this->assertDatabaseHas('stock_reservations', [ 'id' => $reservationId, 'status' => StockReservation::STATUS_RELEASED, 'release_reason' => $reason, ]); } public function test_preserves_other_dates_tenants_and_purchase_reservations(): void { $otherDate = $this->cart(2); $otherTenant = $this->cart(1, 'other'); $purchaseCart = $this->cart(1); DB::table('compras')->insert(['stock_reservation_id' => $purchaseCart->current_stock_reservation_id]); $currentPurchaseCart = $this->cart(1); $currentPurchaseCart->update(['current_purchase_id' => 99]); $affected = $this->cart(3); $this->invalidate(); foreach ([$otherDate, $otherTenant, $purchaseCart, $currentPurchaseCart] as $cart) { $this->assertSame(Cart::STATUS_ACTIVE, $cart->fresh()->status); $this->assertSame(StockReservation::STATUS_ACTIVE, $cart->fresh()->currentStockReservation->status); } $this->assertSame(Cart::STATUS_EXPIRED, $affected->fresh()->status); $this->assertSame(8, (int) Inventory::query()->sum('reserved_stock')); } public function test_rolls_back_invalidation_when_the_date_change_fails(): void { $cart = $this->cart(1); DB::beginTransaction(); $this->invalidate(); DB::rollBack(); $this->assertSame(Cart::STATUS_ACTIVE, $cart->fresh()->status); $this->assertSame(StockReservation::STATUS_ACTIVE, $cart->fresh()->currentStockReservation->status); $this->assertSame(2, (int) Inventory::query()->sum('reserved_stock')); } private function invalidate(string $reason = StockReservationService::REASON_EVENT_DATE_RESCHEDULED): void { app(InvalidateEventDateCartsService::class)->invalidate(new Tenant(['codigo' => 'acme']), collect([1]), $reason); } private function cart(int $variantId, string $tenantCode = 'acme'): Cart { $reservation = StockReservation::query()->create([ 'status' => StockReservation::STATUS_ACTIVE, 'expires_at' => now()->addHour(), ]); $cart = Cart::query()->create([ 'tenant_codigo' => $tenantCode, 'status' => Cart::STATUS_ACTIVE, 'current_stock_reservation_id' => $reservation->id, ]); DB::table('carrito_items')->insert(['cart_id' => $cart->id, 'variant_id' => $variantId]); DB::table('stock_reservation_lines')->insert([ 'stock_reservation_id' => $reservation->id, 'inventory_id' => DB::table('inventories')->insertGetId([]), 'quantity' => 2, ]); return $cart; } }