From f6c60f3d7928e88d37cc028123db9b46efb7250b Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Fri, 19 Dec 2025 09:53:44 +0100 Subject: [PATCH] BFI cart conversion product purchase, R cart dates --- .../create_blax_shop_tables.php.stub | 4 +- .../Controllers/StripeWebhookController.php | 118 +++++++++++++++--- src/Models/Cart.php | 79 ++++++++---- src/Models/CartItem.php | 8 +- tests/Feature/CartDateManagementTest.php | 36 +++--- tests/Feature/CartDateStringParsingTest.php | 26 ++-- tests/Unit/HtmlDateTimeCastTest.php | 48 +++---- 7 files changed, 219 insertions(+), 100 deletions(-) diff --git a/database/migrations/create_blax_shop_tables.php.stub b/database/migrations/create_blax_shop_tables.php.stub index 706608a..f77c7ea 100644 --- a/database/migrations/create_blax_shop_tables.php.stub +++ b/database/migrations/create_blax_shop_tables.php.stub @@ -263,8 +263,8 @@ return new class extends Migration $table->timestamp('last_activity_at')->nullable(); $table->timestamp('expires_at')->nullable(); $table->timestamp('converted_at')->nullable(); - $table->timestamp('from_date')->nullable(); // Default start date for booking items - $table->timestamp('until_date')->nullable(); // Default end date for booking items + $table->timestamp('from')->nullable(); // Default start date for booking items + $table->timestamp('until')->nullable(); // Default end date for booking items $table->json('meta')->nullable(); $table->timestamps(); $table->softDeletes(); diff --git a/src/Http/Controllers/StripeWebhookController.php b/src/Http/Controllers/StripeWebhookController.php index 38f27be..d47ba44 100644 --- a/src/Http/Controllers/StripeWebhookController.php +++ b/src/Http/Controllers/StripeWebhookController.php @@ -122,7 +122,7 @@ class StripeWebhookController 'converted_at' => now(), ]); - // Update associated purchases + // Update associated purchases and claim stocks $this->updatePurchasesForSession($cart, $session); Log::info('Cart converted via Stripe checkout', [ @@ -165,15 +165,20 @@ class StripeWebhookController // Update purchases with this charge ID if they exist $purchases = ProductPurchase::where('charge_id', $charge->id)->get(); foreach ($purchases as $purchase) { - $updateData = [ - 'status' => PurchaseStatus::COMPLETED, - ]; + if ($purchase->status !== PurchaseStatus::COMPLETED) { + $updateData = [ + 'status' => PurchaseStatus::COMPLETED, + ]; - if (in_array('amount_paid', $purchase->getFillable())) { - $updateData['amount_paid'] = $charge->amount / 100; + if (in_array('amount_paid', $purchase->getFillable())) { + $updateData['amount_paid'] = $charge->amount / 100; + } + + $purchase->update($updateData); + + // Claim stock if not already claimed + $this->claimStockForPurchase($purchase); } - - $purchase->update($updateData); } } @@ -209,15 +214,20 @@ class StripeWebhookController // Update purchases with this payment intent $purchases = ProductPurchase::where('charge_id', $paymentIntent->id)->get(); foreach ($purchases as $purchase) { - $updateData = [ - 'status' => PurchaseStatus::COMPLETED, - ]; + if ($purchase->status !== PurchaseStatus::COMPLETED) { + $updateData = [ + 'status' => PurchaseStatus::COMPLETED, + ]; - if (in_array('amount_paid', $purchase->getFillable())) { - $updateData['amount_paid'] = $paymentIntent->amount / 100; + if (in_array('amount_paid', $purchase->getFillable())) { + $updateData['amount_paid'] = $paymentIntent->amount / 100; + } + + $purchase->update($updateData); + + // Claim stock if not already claimed + $this->claimStockForPurchase($purchase); } - - $purchase->update($updateData); } } @@ -244,7 +254,8 @@ class StripeWebhookController */ protected function updatePurchasesForSession(Cart $cart, $session) { - $purchases = $cart->items()->with('purchase')->get()->pluck('purchase')->filter(); + // Get all purchases for this cart + $purchases = ProductPurchase::where('cart_id', $cart->id)->get(); foreach ($purchases as $purchase) { if (!$purchase) { @@ -255,16 +266,87 @@ class StripeWebhookController 'status' => PurchaseStatus::COMPLETED, ]; - // Only update columns that exist in the model + // Update charge_id if it exists in fillable if (in_array('charge_id', $purchase->getFillable())) { $updateData['charge_id'] = $session->payment_intent; } + // Update amount_paid if it exists in fillable if (in_array('amount_paid', $purchase->getFillable())) { - $updateData['amount_paid'] = $session->amount_total / 100; // Convert from cents + // Use the purchase's amount since it was already set correctly + $updateData['amount_paid'] = $purchase->amount; } $purchase->update($updateData); + + // Claim stock after successful payment + $this->claimStockForPurchase($purchase); + } + } + + /** + * Claim stock for a purchase (used after successful payment) + */ + protected function claimStockForPurchase(ProductPurchase $purchase) + { + $product = $purchase->purchasable; + if (!($product instanceof \Blax\Shop\Models\Product)) { + return; + } + + // Skip if product doesn't manage stock + if (!$product->manage_stock && !$product->isPool()) { + return; + } + + // Determine if we need to claim stock with timespan (from/until) + $hasTimespan = $purchase->from && $purchase->until; + + try { + if ($product->isPool()) { + // For pool products: claim from single items (they manage their own stock) + // Only claim if there's a timespan (booking dates) + if ($hasTimespan) { + $product->claimPoolStock( + $purchase->quantity, + $purchase, + $purchase->from, + $purchase->until, + "Purchase #{$purchase->id} completed" + ); + } + // If no timespan, pool products don't claim stock + // (single items would be simple products that don't need claiming) + } elseif ($product->isBooking()) { + // For booking products: claim stock for the timespan + if ($hasTimespan) { + $product->claimStock( + $purchase->quantity, + $purchase, + $purchase->from, + $purchase->until, + "Purchase #{$purchase->id} completed" + ); + } else { + Log::warning('Booking product without timespan', [ + 'purchase_id' => $purchase->id, + 'product_id' => $product->id, + ]); + } + } else { + // For simple/consumable products (like shampoo bottle): + // Decrease stock immediately (no timespan needed) + if ($product->manage_stock) { + $product->decreaseStock($purchase->quantity); + } + } + } catch (\Exception $e) { + Log::error('Failed to claim/decrease stock for purchase', [ + 'purchase_id' => $purchase->id, + 'product_id' => $product->id, + 'product_type' => $product->type->value ?? 'unknown', + 'error' => $e->getMessage(), + ]); } } } diff --git a/src/Models/Cart.php b/src/Models/Cart.php index 19fa712..88f6049 100644 --- a/src/Models/Cart.php +++ b/src/Models/Cart.php @@ -5,6 +5,7 @@ namespace Blax\Shop\Models; use Blax\Shop\Contracts\Cartable; use Blax\Shop\Enums\CartStatus; use Blax\Shop\Enums\ProductType; +use Blax\Shop\Enums\PurchaseStatus; use Blax\Shop\Exceptions\InvalidDateRangeException; use Blax\Shop\Exceptions\NotEnoughAvailableInTimespanException; use Blax\Shop\Services\CartService; @@ -17,6 +18,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Support\Facades\DB; class Cart extends Model { @@ -32,8 +34,8 @@ class Cart extends Model 'expires_at', 'converted_at', 'meta', - 'from_date', - 'until_date', + 'from', + 'until', ]; protected $casts = [ @@ -42,8 +44,8 @@ class Cart extends Model 'converted_at' => 'datetime', 'last_activity_at' => 'datetime', 'meta' => 'object', - 'from_date' => 'datetime', - 'until_date' => 'datetime', + 'from' => 'datetime', + 'until' => 'datetime', ]; protected $appends = [ @@ -236,8 +238,8 @@ class Cart extends Model // Update cart with from/until $this->update([ - 'from_date' => $from, - 'until_date' => $until, + 'from' => $from, + 'until' => $until, ]); // Update cart items with from/until @@ -264,15 +266,15 @@ class Cart extends Model $from = Carbon::parse($from); } - if ($this->until_date && $from >= $this->until_date) { + if ($this->until && $from >= $this->until) { throw new InvalidDateRangeException(); } - if ($validateAvailability && $this->until_date) { - $this->validateDateAvailability($from, $this->until_date); + if ($validateAvailability && $this->until) { + $this->validateDateAvailability($from, $this->until); } - $this->update(['from_date' => $from]); + $this->update(['from' => $from]); return $this->fresh(); } @@ -293,15 +295,15 @@ class Cart extends Model $until = Carbon::parse($until); } - if ($this->from_date && $this->from_date >= $until) { + if ($this->from && $this->from >= $until) { throw new InvalidDateRangeException(); } - if ($validateAvailability && $this->from_date) { - $this->validateDateAvailability($this->from_date, $until); + if ($validateAvailability && $this->from) { + $this->validateDateAvailability($this->from, $until); } - $this->update(['until_date' => $until]); + $this->update(['until' => $until]); return $this->fresh(); } @@ -315,27 +317,27 @@ class Cart extends Model */ public function applyDatesToItems(bool $validateAvailability = true): self { - if (!$this->from_date || !$this->until_date) { + if (!$this->from || !$this->until) { return $this; } foreach ($this->items as $item) { - // Only apply to items without dates that are booking products + // Only apply to booking items that don't already have dates set if ($item->is_booking && (!$item->from || !$item->until)) { if ($validateAvailability) { $product = $item->purchasable; - if ($product && !$product->isAvailableForBooking($this->from_date, $this->until_date, $item->quantity)) { + if ($product && !$product->isAvailableForBooking($this->from, $this->until, $item->quantity)) { throw new NotEnoughAvailableInTimespanException( productName: $product->name ?? 'Product', requested: $item->quantity, available: 0, // Could calculate actual available amount - from: $this->from_date, - until: $this->until_date + from: $this->from, + until: $this->until ); } } - $item->updateDates($this->from_date, $this->until_date); + $item->updateDates($this->from, $this->until); } } @@ -844,7 +846,7 @@ class Cart extends Model public function checkout(): static { - return \DB::transaction(function () { + return DB::transaction(function () { // Lock the cart to prevent concurrent checkouts $this->lockForUpdate(); @@ -949,6 +951,7 @@ class Cart extends Model * * This method: * - Validates the cart (doesn't convert it) + * - Creates ProductPurchase records for each cart item (with PENDING status) * - Uses dynamic price_data for each cart item (no pre-created Stripe prices needed) * - Creates line items with descriptions including booking dates * - Returns the Stripe checkout session @@ -971,6 +974,40 @@ class Cart extends Model // Validate cart before proceeding (doesn't convert it) $this->validateForCheckout(); + // Create ProductPurchase records for each cart item + DB::transaction(function () { + foreach ($this->items as $item) { + // Skip if purchase already exists + if ($item->purchase_id) { + continue; + } + + $product = $item->purchasable; + $from = $item->from; + $until = $item->until; + + // Create purchase record with PENDING status + $purchase = ProductPurchase::create([ + 'cart_id' => $this->id, + 'price_id' => $item->price_id, + 'purchasable_id' => $product->id, + 'purchasable_type' => get_class($product), + 'purchaser_id' => $this->customer_id, + 'purchaser_type' => $this->customer_type, + 'quantity' => $item->quantity, + 'amount' => $item->subtotal, + 'amount_paid' => 0, + 'status' => PurchaseStatus::PENDING, + 'from' => $from, + 'until' => $until, + 'meta' => $item->meta, + ]); + + // Link purchase to cart item + $item->update(['purchase_id' => $purchase->id]); + } + }); + $lineItems = []; foreach ($this->items as $item) { diff --git a/src/Models/CartItem.php b/src/Models/CartItem.php index ab5e7c4..caa6a64 100644 --- a/src/Models/CartItem.php +++ b/src/Models/CartItem.php @@ -342,7 +342,7 @@ class CartItem extends Model /** * Get the effective 'from' date for this cart item. - * Returns the item's specific date if set, otherwise falls back to the cart's from_date. + * Returns the item's specific date if set, otherwise falls back to the cart's from. * * @return \Carbon\Carbon|null */ @@ -352,12 +352,12 @@ class CartItem extends Model return $this->from; } - return $this->cart?->from_date; + return $this->cart?->from; } /** * Get the effective 'until' date for this cart item. - * Returns the item's specific date if set, otherwise falls back to the cart's until_date. + * Returns the item's specific date if set, otherwise falls back to the cart's until. * * @return \Carbon\Carbon|null */ @@ -367,7 +367,7 @@ class CartItem extends Model return $this->until; } - return $this->cart?->until_date; + return $this->cart?->until; } /** diff --git a/tests/Feature/CartDateManagementTest.php b/tests/Feature/CartDateManagementTest.php index cf55494..76c4695 100644 --- a/tests/Feature/CartDateManagementTest.php +++ b/tests/Feature/CartDateManagementTest.php @@ -24,8 +24,8 @@ class CartDateManagementTest extends TestCase $cart->setDates($from, $until, validateAvailability: false); $cart->refresh(); - $this->assertEquals($from->toDateTimeString(), $cart->from_date->toDateTimeString()); - $this->assertEquals($until->toDateTimeString(), $cart->until_date->toDateTimeString()); + $this->assertEquals($from->toDateTimeString(), $cart->from->toDateTimeString()); + $this->assertEquals($until->toDateTimeString(), $cart->until->toDateTimeString()); } /** @test */ @@ -48,7 +48,7 @@ class CartDateManagementTest extends TestCase $cart->setFromDate($from, validateAvailability: false); $cart->refresh(); - $this->assertEquals($from->toDateTimeString(), $cart->from_date->toDateTimeString()); + $this->assertEquals($from->toDateTimeString(), $cart->from->toDateTimeString()); } /** @test */ @@ -60,14 +60,14 @@ class CartDateManagementTest extends TestCase $cart->setUntilDate($until, validateAvailability: false); $cart->refresh(); - $this->assertEquals($until->toDateTimeString(), $cart->until_date->toDateTimeString()); + $this->assertEquals($until->toDateTimeString(), $cart->until->toDateTimeString()); } /** @test */ public function it_throws_exception_when_setting_from_date_after_existing_until_date() { $cart = Cart::factory()->create([ - 'until_date' => Carbon::now()->addDays(2), + 'until' => Carbon::now()->addDays(2), ]); $this->expectException(InvalidDateRangeException::class); @@ -78,7 +78,7 @@ class CartDateManagementTest extends TestCase public function it_throws_exception_when_setting_until_date_before_existing_from_date() { $cart = Cart::factory()->create([ - 'from_date' => Carbon::now()->addDays(3), + 'from' => Carbon::now()->addDays(3), ]); $this->expectException(InvalidDateRangeException::class); @@ -102,8 +102,8 @@ class CartDateManagementTest extends TestCase ]); $cart = Cart::factory()->create([ - 'from_date' => Carbon::now()->addDays(1), - 'until_date' => Carbon::now()->addDays(3), + 'from' => Carbon::now()->addDays(1), + 'until' => Carbon::now()->addDays(3), ]); $itemFromDate = Carbon::now()->addDays(5); @@ -136,8 +136,8 @@ class CartDateManagementTest extends TestCase $cartUntilDate = Carbon::now()->addDays(3); $cart = Cart::factory()->create([ - 'from_date' => $cartFromDate, - 'until_date' => $cartUntilDate, + 'from' => $cartFromDate, + 'until' => $cartUntilDate, ]); $item = $cart->addToCart($product, 1); @@ -187,8 +187,8 @@ class CartDateManagementTest extends TestCase ]); $cart = Cart::factory()->create([ - 'from_date' => Carbon::now()->addDays(1), - 'until_date' => Carbon::now()->addDays(3), + 'from' => Carbon::now()->addDays(1), + 'until' => Carbon::now()->addDays(3), ]); $item = $cart->addToCart($product, 1); @@ -283,8 +283,8 @@ class CartDateManagementTest extends TestCase ]); $cart = Cart::factory()->create([ - 'from_date' => Carbon::now()->addDays(1), - 'until_date' => Carbon::now()->addDays(3), + 'from' => Carbon::now()->addDays(1), + 'until' => Carbon::now()->addDays(3), ]); $item = $cart->addToCart($product, 1); @@ -389,8 +389,8 @@ class CartDateManagementTest extends TestCase ]); $cart = Cart::factory()->create([ - 'from_date' => Carbon::now()->addDays(1), - 'until_date' => Carbon::now()->addDays(3), + 'from' => Carbon::now()->addDays(1), + 'until' => Carbon::now()->addDays(3), ]); // Add item that would exceed available stock @@ -428,7 +428,7 @@ class CartDateManagementTest extends TestCase validateAvailability: false ); - $this->assertNotNull($cart->from_date); - $this->assertNotNull($cart->until_date); + $this->assertNotNull($cart->from); + $this->assertNotNull($cart->until); } } diff --git a/tests/Feature/CartDateStringParsingTest.php b/tests/Feature/CartDateStringParsingTest.php index cb34af5..a1a6c6b 100644 --- a/tests/Feature/CartDateStringParsingTest.php +++ b/tests/Feature/CartDateStringParsingTest.php @@ -51,10 +51,10 @@ class CartDateStringParsingTest extends TestCase { $cart = $this->cart->setDates('2025-12-20', '2025-12-25', false); - $this->assertNotNull($cart->from_date); - $this->assertNotNull($cart->until_date); - $this->assertEquals('2025-12-20', $cart->from_date->format('Y-m-d')); - $this->assertEquals('2025-12-25', $cart->until_date->format('Y-m-d')); + $this->assertNotNull($cart->from); + $this->assertNotNull($cart->until); + $this->assertEquals('2025-12-20', $cart->from->format('Y-m-d')); + $this->assertEquals('2025-12-25', $cart->until->format('Y-m-d')); } /** @test */ @@ -65,8 +65,8 @@ class CartDateStringParsingTest extends TestCase $cart = $this->cart->setDates($from, $until, false); - $this->assertEquals('2025-12-20', $cart->from_date->format('Y-m-d')); - $this->assertEquals('2025-12-25', $cart->until_date->format('Y-m-d')); + $this->assertEquals('2025-12-20', $cart->from->format('Y-m-d')); + $this->assertEquals('2025-12-25', $cart->until->format('Y-m-d')); } /** @test */ @@ -74,18 +74,18 @@ class CartDateStringParsingTest extends TestCase { $cart = $this->cart->setFromDate('2025-12-20', false); - $this->assertNotNull($cart->from_date); - $this->assertEquals('2025-12-20', $cart->from_date->format('Y-m-d')); + $this->assertNotNull($cart->from); + $this->assertEquals('2025-12-20', $cart->from->format('Y-m-d')); } /** @test */ public function cart_set_until_date_accepts_string() { - $this->cart->update(['from_date' => Carbon::parse('2025-12-20')]); + $this->cart->update(['from' => Carbon::parse('2025-12-20')]); $cart = $this->cart->setUntilDate('2025-12-25', false); - $this->assertNotNull($cart->until_date); - $this->assertEquals('2025-12-25', $cart->until_date->format('Y-m-d')); + $this->assertNotNull($cart->until); + $this->assertEquals('2025-12-25', $cart->until->format('Y-m-d')); } /** @test */ @@ -107,8 +107,8 @@ class CartDateStringParsingTest extends TestCase $cart = $cart->setDates($from, $until, false); - $this->assertNotNull($cart->from_date, "Failed to parse: $from"); - $this->assertNotNull($cart->until_date, "Failed to parse: $until"); + $this->assertNotNull($cart->from, "Failed to parse: $from"); + $this->assertNotNull($cart->until, "Failed to parse: $until"); } } diff --git a/tests/Unit/HtmlDateTimeCastTest.php b/tests/Unit/HtmlDateTimeCastTest.php index 6e83dc6..b442693 100644 --- a/tests/Unit/HtmlDateTimeCastTest.php +++ b/tests/Unit/HtmlDateTimeCastTest.php @@ -16,15 +16,15 @@ class HtmlDateTimeCastTest extends TestCase $cart = Cart::factory()->create(); $date = Carbon::parse('2025-12-25 14:30:00'); - $cart->from_date = $date; + $cart->from = $date; $cart->save(); // Reload from database $cart->refresh(); // Should return Carbon instance - $this->assertInstanceOf(Carbon::class, $cart->from_date); - $this->assertEquals('2025-12-25 14:30:00', $cart->from_date->format('Y-m-d H:i:s')); + $this->assertInstanceOf(Carbon::class, $cart->from); + $this->assertEquals('2025-12-25 14:30:00', $cart->from->format('Y-m-d H:i:s')); } /** @test */ @@ -33,13 +33,13 @@ class HtmlDateTimeCastTest extends TestCase $cart = Cart::factory()->create(); $date = new \DateTime('2025-12-25 14:30:00'); - $cart->from_date = $date; + $cart->from = $date; $cart->save(); $cart->refresh(); - $this->assertInstanceOf(Carbon::class, $cart->from_date); - $this->assertEquals('2025-12-25 14:30:00', $cart->from_date->format('Y-m-d H:i:s')); + $this->assertInstanceOf(Carbon::class, $cart->from); + $this->assertEquals('2025-12-25 14:30:00', $cart->from->format('Y-m-d H:i:s')); } /** @test */ @@ -48,13 +48,13 @@ class HtmlDateTimeCastTest extends TestCase $cart = Cart::factory()->create(); // Standard datetime string - $cart->from_date = '2025-12-25 14:30:00'; + $cart->from = '2025-12-25 14:30:00'; $cart->save(); $cart->refresh(); - $this->assertInstanceOf(Carbon::class, $cart->from_date); - $this->assertEquals('2025-12-25 14:30:00', $cart->from_date->format('Y-m-d H:i:s')); + $this->assertInstanceOf(Carbon::class, $cart->from); + $this->assertEquals('2025-12-25 14:30:00', $cart->from->format('Y-m-d H:i:s')); } /** @test */ @@ -63,26 +63,26 @@ class HtmlDateTimeCastTest extends TestCase $cart = Cart::factory()->create(); // HTML5 datetime-local format (YYYY-MM-DDTHH:MM) - $cart->from_date = '2025-12-25T14:30'; + $cart->from = '2025-12-25T14:30'; $cart->save(); $cart->refresh(); - $this->assertInstanceOf(Carbon::class, $cart->from_date); - $this->assertEquals('2025-12-25 14:30:00', $cart->from_date->format('Y-m-d H:i:s')); + $this->assertInstanceOf(Carbon::class, $cart->from); + $this->assertEquals('2025-12-25 14:30:00', $cart->from->format('Y-m-d H:i:s')); } /** @test */ public function it_can_format_for_html5_input() { $cart = Cart::factory()->create(); - $cart->from_date = Carbon::parse('2025-12-25 14:30:00'); + $cart->from = Carbon::parse('2025-12-25 14:30:00'); $cart->save(); $cart->refresh(); // Can format for HTML5 datetime-local input - $htmlFormat = $cart->from_date->format('Y-m-d\TH:i'); + $htmlFormat = $cart->from->format('Y-m-d\TH:i'); $this->assertEquals('2025-12-25T14:30', $htmlFormat); } @@ -90,12 +90,12 @@ class HtmlDateTimeCastTest extends TestCase public function it_handles_null_values() { $cart = Cart::factory()->create(); - $cart->from_date = null; + $cart->from = null; $cart->save(); $cart->refresh(); - $this->assertNull($cart->from_date); + $this->assertNull($cart->from); } /** @test */ @@ -128,28 +128,28 @@ class HtmlDateTimeCastTest extends TestCase $cart = Cart::factory()->create(); $timestamp = Carbon::parse('2025-12-25 14:30:00')->timestamp; - $cart->from_date = $timestamp; + $cart->from = $timestamp; $cart->save(); $cart->refresh(); - $this->assertInstanceOf(Carbon::class, $cart->from_date); - $this->assertEquals('2025-12-25 14:30:00', $cart->from_date->format('Y-m-d H:i:s')); + $this->assertInstanceOf(Carbon::class, $cart->from); + $this->assertEquals('2025-12-25 14:30:00', $cart->from->format('Y-m-d H:i:s')); } /** @test */ public function it_maintains_carbon_methods() { $cart = Cart::factory()->create(); - $cart->from_date = Carbon::parse('2025-12-25 14:30:00'); + $cart->from = Carbon::parse('2025-12-25 14:30:00'); $cart->save(); $cart->refresh(); // All Carbon methods should be available - $this->assertTrue($cart->from_date->isAfter(Carbon::parse('2025-12-24'))); - $this->assertTrue($cart->from_date->isBefore(Carbon::parse('2025-12-26'))); - $this->assertEquals('December', $cart->from_date->format('F')); - $this->assertEquals('2025-12-25', $cart->from_date->toDateString()); + $this->assertTrue($cart->from->isAfter(Carbon::parse('2025-12-24'))); + $this->assertTrue($cart->from->isBefore(Carbon::parse('2025-12-26'))); + $this->assertEquals('December', $cart->from->format('F')); + $this->assertEquals('2025-12-25', $cart->from->toDateString()); } }