BF pool priority claiming, IA date cast
This commit is contained in:
parent
1b2559b824
commit
7360391581
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Casts;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast for datetime fields that:
|
||||||
|
* - Accepts string, DateTimeInterface, or Carbon as input
|
||||||
|
* - Stores as Unix timestamp in database (integer)
|
||||||
|
* - Returns Carbon instance on get
|
||||||
|
*
|
||||||
|
* Usage for HTML5 datetime-local inputs:
|
||||||
|
* $model->created_at->format('Y-m-d\TH:i')
|
||||||
|
*/
|
||||||
|
class HtmlDateTimeCast implements CastsAttributes
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Cast the given value.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attributes
|
||||||
|
*/
|
||||||
|
public function get(Model $model, string $key, mixed $value, array $attributes): ?Carbon
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert timestamp to Carbon
|
||||||
|
return Carbon::createFromTimestamp($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the given value for storage.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attributes
|
||||||
|
*/
|
||||||
|
public function set(Model $model, string $key, mixed $value, array $attributes): ?int
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Carbon instances
|
||||||
|
if ($value instanceof Carbon) {
|
||||||
|
return $value->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle DateTimeInterface
|
||||||
|
if ($value instanceof \DateTimeInterface) {
|
||||||
|
return Carbon::instance($value)->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle string input (including HTML5 datetime-local format)
|
||||||
|
if (is_string($value)) {
|
||||||
|
return Carbon::parse($value)->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle numeric timestamp
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return (int) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"Invalid datetime value for {$key}. Expected string, DateTimeInterface, Carbon, or timestamp."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Models;
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
|
use Blax\Shop\Casts\HtmlDateTimeCast;
|
||||||
use Blax\Shop\Contracts\Cartable;
|
use Blax\Shop\Contracts\Cartable;
|
||||||
use Blax\Shop\Enums\CartStatus;
|
use Blax\Shop\Enums\CartStatus;
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
|
@ -40,8 +41,8 @@ class Cart extends Model
|
||||||
'converted_at' => 'datetime',
|
'converted_at' => 'datetime',
|
||||||
'last_activity_at' => 'datetime',
|
'last_activity_at' => 'datetime',
|
||||||
'meta' => 'object',
|
'meta' => 'object',
|
||||||
'from_date' => 'datetime',
|
'from_date' => HtmlDateTimeCast::class,
|
||||||
'until_date' => 'datetime',
|
'until_date' => HtmlDateTimeCast::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $appends = [
|
protected $appends = [
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Blax\Shop\Models;
|
namespace Blax\Shop\Models;
|
||||||
|
|
||||||
|
use Blax\Shop\Casts\HtmlDateTimeCast;
|
||||||
use Blax\Shop\Exceptions\InvalidDateRangeException;
|
use Blax\Shop\Exceptions\InvalidDateRangeException;
|
||||||
use Blax\Workkit\Traits\HasMeta;
|
use Blax\Workkit\Traits\HasMeta;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
|
@ -35,8 +36,8 @@ class CartItem extends Model
|
||||||
'subtotal' => 'decimal:2',
|
'subtotal' => 'decimal:2',
|
||||||
'parameters' => 'array',
|
'parameters' => 'array',
|
||||||
'meta' => 'array',
|
'meta' => 'array',
|
||||||
'from' => 'datetime',
|
'from' => HtmlDateTimeCast::class,
|
||||||
'until' => 'datetime',
|
'until' => HtmlDateTimeCast::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $appends = [
|
protected $appends = [
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ trait MayBePoolProduct
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claim stock for a pool product
|
* Claim stock for a pool product
|
||||||
* This will claim stock from the available single items
|
* This will claim stock from the available single items, respecting the pricing strategy
|
||||||
*
|
*
|
||||||
* @param int $quantity Number of pool items to claim
|
* @param int $quantity Number of pool items to claim
|
||||||
* @param mixed $reference Reference model
|
* @param mixed $reference Reference model
|
||||||
|
|
@ -134,13 +134,28 @@ trait MayBePoolProduct
|
||||||
throw new \Exception('Pool product has no single items to claim');
|
throw new \Exception('Pool product has no single items to claim');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get available single items for the period
|
// Get pricing strategy
|
||||||
|
$strategy = $this->getPricingStrategy();
|
||||||
|
|
||||||
|
// Build list of available single items with their prices
|
||||||
$availableItems = [];
|
$availableItems = [];
|
||||||
foreach ($singleItems as $item) {
|
foreach ($singleItems as $item) {
|
||||||
if ($item->isAvailableForBooking($from, $until, 1)) {
|
if ($item->isAvailableForBooking($from, $until, 1)) {
|
||||||
$availableItems[] = $item;
|
// Get the price for this item
|
||||||
|
$price = $item->defaultPrice()->first()?->getCurrentPrice($item->isOnSale());
|
||||||
|
|
||||||
|
// If item has no price, use pool's fallback price
|
||||||
|
if ($price === null && $this->hasPrice()) {
|
||||||
|
$price = $this->defaultPrice()->first()?->getCurrentPrice($this->isOnSale());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$availableItems[] = [
|
||||||
|
'item' => $item,
|
||||||
|
'price' => $price ?? PHP_FLOAT_MAX, // Items without prices go last
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early exit if we have enough
|
||||||
if (count($availableItems) >= $quantity) {
|
if (count($availableItems) >= $quantity) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -150,9 +165,19 @@ trait MayBePoolProduct
|
||||||
throw new \Exception("Only " . count($availableItems) . " items available, but {$quantity} requested");
|
throw new \Exception("Only " . count($availableItems) . " items available, but {$quantity} requested");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Claim stock from each selected single item
|
// Sort by pricing strategy
|
||||||
|
usort($availableItems, function ($a, $b) use ($strategy) {
|
||||||
|
return match ($strategy) {
|
||||||
|
\Blax\Shop\Enums\PricingStrategy::LOWEST => $a['price'] <=> $b['price'],
|
||||||
|
\Blax\Shop\Enums\PricingStrategy::HIGHEST => $b['price'] <=> $a['price'],
|
||||||
|
\Blax\Shop\Enums\PricingStrategy::AVERAGE => 0, // Keep original order for average
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Claim stock from selected items in order
|
||||||
$claimedItems = [];
|
$claimedItems = [];
|
||||||
foreach (array_slice($availableItems, 0, $quantity) as $item) {
|
foreach (array_slice($availableItems, 0, $quantity) as $itemData) {
|
||||||
|
$item = $itemData['item'];
|
||||||
$item->claimStock(1, $reference, $from, $until, $note);
|
$item->claimStock(1, $reference, $from, $until, $note);
|
||||||
$claimedItems[] = $item;
|
$claimedItems[] = $item;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
|
use Blax\Shop\Enums\PriceType;
|
||||||
|
use Blax\Shop\Enums\PricingStrategy;
|
||||||
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
use Blax\Shop\Models\Cart;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Models\ProductPrice;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class PoolClaimingPriorityTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @test */
|
||||||
|
public function it_claims_lowest_priced_items_first_with_lowest_strategy()
|
||||||
|
{
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set pool pricing strategy to LOWEST
|
||||||
|
$pool->setPoolPricingStrategy(PricingStrategy::LOWEST);
|
||||||
|
|
||||||
|
// Create fallback price on pool
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $pool->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'type' => PriceType::RECURRING,
|
||||||
|
'unit_amount' => 500, // $5.00 fallback
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create single parking spots with different prices
|
||||||
|
$spot1 = Product::factory()->create([
|
||||||
|
'name' => 'Parking Spot 1',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot1->increaseStock(1);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $spot1->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'type' => PriceType::RECURRING,
|
||||||
|
'unit_amount' => 500, // $5.00
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot2 = Product::factory()->create([
|
||||||
|
'name' => 'Parking Spot 2',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot2->increaseStock(1);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $spot2->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'type' => PriceType::RECURRING,
|
||||||
|
'unit_amount' => 1000, // $10.00 (most expensive)
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$spot3 = Product::factory()->create([
|
||||||
|
'name' => 'Parking Spot 3',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$spot3->increaseStock(1);
|
||||||
|
|
||||||
|
// Spot 3 has NO price - should use pool fallback (500)
|
||||||
|
|
||||||
|
// Attach spots to pool
|
||||||
|
$pool->attachSingleItems([$spot1->id, $spot2->id, $spot3->id]);
|
||||||
|
|
||||||
|
// Create cart and claim 3 spots
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
$from = Carbon::now()->addDays(1);
|
||||||
|
$until = Carbon::now()->addDays(3);
|
||||||
|
|
||||||
|
$claimedItems = $pool->claimPoolStock(3, $cart, $from, $until);
|
||||||
|
|
||||||
|
// Should claim in order: spot1 (500), spot3 (500 fallback), spot2 (1000)
|
||||||
|
$this->assertCount(3, $claimedItems);
|
||||||
|
|
||||||
|
// Extract IDs for easier comparison
|
||||||
|
$claimedIds = array_map(fn($item) => $item->id, $claimedItems);
|
||||||
|
|
||||||
|
// First two should be spot1 and spot3 (both $5.00) in some order
|
||||||
|
$this->assertContains($spot1->id, [$claimedIds[0], $claimedIds[1]]);
|
||||||
|
$this->assertContains($spot3->id, [$claimedIds[0], $claimedIds[1]]);
|
||||||
|
|
||||||
|
// Last should be spot2 (most expensive)
|
||||||
|
$this->assertEquals($spot2->id, $claimedIds[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_claims_highest_priced_items_first_with_highest_strategy()
|
||||||
|
{
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Premium Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set pool pricing strategy to HIGHEST
|
||||||
|
$pool->setPoolPricingStrategy(PricingStrategy::HIGHEST);
|
||||||
|
|
||||||
|
// Create fallback price on pool
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $pool->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'type' => PriceType::RECURRING,
|
||||||
|
'unit_amount' => 500,
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create spots with different prices
|
||||||
|
$cheapSpot = Product::factory()->create([
|
||||||
|
'name' => 'Cheap Spot',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$cheapSpot->increaseStock(1);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $cheapSpot->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'type' => PriceType::RECURRING,
|
||||||
|
'unit_amount' => 300, // Cheapest
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$expensiveSpot = Product::factory()->create([
|
||||||
|
'name' => 'Expensive Spot',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$expensiveSpot->increaseStock(1);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $expensiveSpot->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'type' => PriceType::RECURRING,
|
||||||
|
'unit_amount' => 1500, // Most expensive
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Attach to pool
|
||||||
|
$pool->attachSingleItems([$cheapSpot->id, $expensiveSpot->id]);
|
||||||
|
|
||||||
|
// Claim 2 spots
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
$from = Carbon::now()->addDays(1);
|
||||||
|
$until = Carbon::now()->addDays(3);
|
||||||
|
|
||||||
|
$claimedItems = $pool->claimPoolStock(2, $cart, $from, $until);
|
||||||
|
|
||||||
|
// Should claim expensive first, then cheap
|
||||||
|
$this->assertEquals($expensiveSpot->id, $claimedItems[0]->id);
|
||||||
|
$this->assertEquals($cheapSpot->id, $claimedItems[1]->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_uses_fallback_price_for_items_without_price()
|
||||||
|
{
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Parking Pool',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pool->setPoolPricingStrategy(PricingStrategy::LOWEST);
|
||||||
|
|
||||||
|
// Pool fallback price
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $pool->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'type' => PriceType::RECURRING,
|
||||||
|
'unit_amount' => 500,
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Spot with specific price (higher than fallback)
|
||||||
|
$pricedSpot = Product::factory()->create([
|
||||||
|
'name' => 'Priced Spot',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$pricedSpot->increaseStock(1);
|
||||||
|
|
||||||
|
ProductPrice::factory()->create([
|
||||||
|
'purchasable_id' => $pricedSpot->id,
|
||||||
|
'purchasable_type' => Product::class,
|
||||||
|
'type' => PriceType::RECURRING,
|
||||||
|
'unit_amount' => 1000,
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Spot without price (uses fallback)
|
||||||
|
$unpricedSpot = Product::factory()->create([
|
||||||
|
'name' => 'Unpriced Spot',
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$unpricedSpot->increaseStock(1);
|
||||||
|
// No price created for this spot
|
||||||
|
|
||||||
|
$pool->attachSingleItems([$pricedSpot->id, $unpricedSpot->id]);
|
||||||
|
|
||||||
|
// Claim both
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
$from = Carbon::now()->addDays(1);
|
||||||
|
$until = Carbon::now()->addDays(3);
|
||||||
|
|
||||||
|
$claimedItems = $pool->claimPoolStock(2, $cart, $from, $until);
|
||||||
|
|
||||||
|
// Unpriced spot (using fallback 500) should be claimed first
|
||||||
|
$this->assertEquals($unpricedSpot->id, $claimedItems[0]->id);
|
||||||
|
$this->assertEquals($pricedSpot->id, $claimedItems[1]->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Blax\Shop\Tests\Unit;
|
||||||
|
|
||||||
|
use Blax\Shop\Models\Cart;
|
||||||
|
use Blax\Shop\Models\CartItem;
|
||||||
|
use Blax\Shop\Models\Product;
|
||||||
|
use Blax\Shop\Tests\TestCase;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class HtmlDateTimeCastTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @test */
|
||||||
|
public function it_accepts_carbon_instance_and_stores_as_timestamp()
|
||||||
|
{
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
$date = Carbon::parse('2025-12-25 14:30:00');
|
||||||
|
|
||||||
|
$cart->from_date = $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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_accepts_datetime_interface_and_stores_as_timestamp()
|
||||||
|
{
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
$date = new \DateTime('2025-12-25 14:30:00');
|
||||||
|
|
||||||
|
$cart->from_date = $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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_accepts_string_and_stores_as_timestamp()
|
||||||
|
{
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
|
||||||
|
// Standard datetime string
|
||||||
|
$cart->from_date = '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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_accepts_html5_datetime_local_format()
|
||||||
|
{
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
|
||||||
|
// HTML5 datetime-local format (YYYY-MM-DDTHH:MM)
|
||||||
|
$cart->from_date = '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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @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->save();
|
||||||
|
|
||||||
|
$cart->refresh();
|
||||||
|
|
||||||
|
// Can format for HTML5 datetime-local input
|
||||||
|
$htmlFormat = $cart->from_date->format('Y-m-d\TH:i');
|
||||||
|
$this->assertEquals('2025-12-25T14:30', $htmlFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_handles_null_values()
|
||||||
|
{
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
$cart->from_date = null;
|
||||||
|
$cart->save();
|
||||||
|
|
||||||
|
$cart->refresh();
|
||||||
|
|
||||||
|
$this->assertNull($cart->from_date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_works_with_cart_items()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->create();
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
|
||||||
|
$item = $cart->items()->create([
|
||||||
|
'purchasable_id' => $product->id,
|
||||||
|
'purchasable_type' => get_class($product),
|
||||||
|
'quantity' => 1,
|
||||||
|
'price' => 100,
|
||||||
|
'subtotal' => 100,
|
||||||
|
'from' => '2025-12-25T14:30',
|
||||||
|
'until' => '2025-12-27T10:00',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$item->refresh();
|
||||||
|
|
||||||
|
$this->assertInstanceOf(Carbon::class, $item->from);
|
||||||
|
$this->assertInstanceOf(Carbon::class, $item->until);
|
||||||
|
$this->assertEquals('2025-12-25T14:30', $item->from->format('Y-m-d\TH:i'));
|
||||||
|
$this->assertEquals('2025-12-27T10:00', $item->until->format('Y-m-d\TH:i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_accepts_unix_timestamp()
|
||||||
|
{
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
$timestamp = Carbon::parse('2025-12-25 14:30:00')->timestamp;
|
||||||
|
|
||||||
|
$cart->from_date = $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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function it_maintains_carbon_methods()
|
||||||
|
{
|
||||||
|
$cart = Cart::factory()->create();
|
||||||
|
$cart->from_date = 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue