CAI per minute booking pricing
This commit is contained in:
parent
db285e8c0d
commit
abbfbd3649
|
|
@ -8,6 +8,7 @@ use Blax\Shop\Enums\ProductType;
|
|||
use Blax\Shop\Exceptions\InvalidDateRangeException;
|
||||
use Blax\Shop\Exceptions\NotEnoughAvailableInTimespanException;
|
||||
use Blax\Shop\Services\CartService;
|
||||
use Blax\Shop\Traits\HasBookingPriceCalculation;
|
||||
use Blax\Workkit\Traits\HasExpiration;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
|
|
@ -18,7 +19,7 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|||
|
||||
class Cart extends Model
|
||||
{
|
||||
use HasUuids, HasExpiration, HasFactory;
|
||||
use HasUuids, HasExpiration, HasFactory, HasBookingPriceCalculation;
|
||||
|
||||
protected $fillable = [
|
||||
'session_id',
|
||||
|
|
@ -589,7 +590,7 @@ class Cart extends Model
|
|||
$currentPrice = $cartable->getCurrentPrice();
|
||||
}
|
||||
if ($from && $until) {
|
||||
$days = max(1, $from->diff($until)->days);
|
||||
$days = $this->calculateBookingDays($from, $until);
|
||||
$currentPrice *= $days;
|
||||
}
|
||||
|
||||
|
|
@ -629,7 +630,7 @@ class Cart extends Model
|
|||
// Calculate days if booking dates provided
|
||||
$days = 1;
|
||||
if ($from && $until) {
|
||||
$days = max(1, $from->diff($until)->days);
|
||||
$days = $this->calculateBookingDays($from, $until);
|
||||
}
|
||||
|
||||
// Calculate price per unit for the entire period
|
||||
|
|
@ -929,10 +930,11 @@ class Cart extends Model
|
|||
// Add description with booking dates if available
|
||||
$description = null;
|
||||
if ($item->from && $item->until) {
|
||||
$days = max(1, $item->from->diffInDays($item->until));
|
||||
$fromFormatted = $item->from->format('M j, Y');
|
||||
$untilFormatted = $item->until->format('M j, Y');
|
||||
$description = "Period: {$fromFormatted} to {$untilFormatted} ({$days} day" . ($days > 1 ? 's' : '') . ")";
|
||||
$days = $this->calculateBookingDays($item->from, $item->until);
|
||||
$fromFormatted = $item->from->format('M j, Y H:i');
|
||||
$untilFormatted = $item->until->format('M j, Y H:i');
|
||||
$daysText = number_format($days, 2) . ' day' . ($days != 1 ? 's' : '');
|
||||
$description = "Period: {$fromFormatted} to {$untilFormatted} ({$daysText})";
|
||||
}
|
||||
|
||||
if ($description) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace Blax\Shop\Models;
|
||||
|
||||
use Blax\Shop\Exceptions\InvalidDateRangeException;
|
||||
use Blax\Shop\Traits\HasBookingPriceCalculation;
|
||||
use Blax\Workkit\Traits\HasMeta;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
|
@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
|
||||
class CartItem extends Model
|
||||
{
|
||||
use HasUuids, HasMeta;
|
||||
use HasUuids, HasMeta, HasBookingPriceCalculation;
|
||||
|
||||
protected $fillable = [
|
||||
'cart_id',
|
||||
|
|
@ -401,8 +402,8 @@ class CartItem extends Model
|
|||
throw new \Exception("Cannot update dates for non-product items.");
|
||||
}
|
||||
|
||||
// Calculate days
|
||||
$days = max(1, $from->diff($until)->days);
|
||||
// Calculate days using per-minute precision
|
||||
$days = $this->calculateBookingDays($from, $until);
|
||||
|
||||
// Get current price per day
|
||||
$pricePerDay = $product->getCurrentPrice();
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ use Blax\Shop\Exceptions\InvalidBookingConfigurationException;
|
|||
use Blax\Shop\Exceptions\InvalidPoolConfigurationException;
|
||||
use Blax\Shop\Exceptions\NotPurchasable;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Traits\HasBookingPriceCalculation;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class CartService
|
||||
{
|
||||
use HasBookingPriceCalculation;
|
||||
/**
|
||||
* Session key for storing the current cart ID
|
||||
*/
|
||||
|
|
@ -535,7 +537,7 @@ class CartService
|
|||
|
||||
// Calculate price based on days for booking products
|
||||
if ($product instanceof Product && ($product->isBooking() || $product->isPool())) {
|
||||
$days = $from->diff($until)->days;
|
||||
$days = $this->calculateBookingDays($from, $until);
|
||||
$pricePerUnit = $pricePerDay * $days; // Price for one unit for the entire period
|
||||
$totalPrice = $pricePerUnit * $quantity; // Total for all units
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Traits;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
trait HasBookingPriceCalculation
|
||||
{
|
||||
/**
|
||||
* Calculate the fractional days between two dates.
|
||||
* This calculates the exact duration in minutes and converts to days (24-hour periods).
|
||||
*
|
||||
* Examples:
|
||||
* - 1 day exactly (24 hours) = 1.0
|
||||
* - 1.5 days (36 hours) = 1.5
|
||||
* - 12 hours = 0.5
|
||||
* - 1 day + 6 hours = 1.25
|
||||
*
|
||||
* @param \DateTimeInterface $from Start date/time
|
||||
* @param \DateTimeInterface $until End date/time
|
||||
* @return float Number of days (can be fractional)
|
||||
*/
|
||||
protected function calculateBookingDays(\DateTimeInterface $from, \DateTimeInterface $until): float
|
||||
{
|
||||
if (!$from instanceof Carbon) {
|
||||
$from = Carbon::parse($from);
|
||||
}
|
||||
if (!$until instanceof Carbon) {
|
||||
$until = Carbon::parse($until);
|
||||
}
|
||||
|
||||
// Calculate the exact duration in minutes
|
||||
$totalMinutes = $from->diffInMinutes($until);
|
||||
|
||||
// Convert to days (1 day = 1440 minutes)
|
||||
$days = $totalMinutes / 1440;
|
||||
|
||||
// Round to 10 decimal places to avoid floating point errors
|
||||
// while maintaining precision for fractional days
|
||||
$days = round($days, 10);
|
||||
|
||||
// Return at least a minimum value if dates are the same or very close
|
||||
return max(0.000694, $days); // 0.000694 ≈ 1 minute in days
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the price for a booking based on exact duration.
|
||||
*
|
||||
* @param float $pricePerDay Price per day (24 hours)
|
||||
* @param \DateTimeInterface $from Start date/time
|
||||
* @param \DateTimeInterface $until End date/time
|
||||
* @return float Calculated price for the duration
|
||||
*/
|
||||
protected function calculateBookingPrice(
|
||||
float $pricePerDay,
|
||||
\DateTimeInterface $from,
|
||||
\DateTimeInterface $until
|
||||
): float {
|
||||
$days = $this->calculateBookingDays($from, $until);
|
||||
return $pricePerDay * $days;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ use Blax\Shop\Exceptions\InvalidPoolConfigurationException;
|
|||
|
||||
trait MayBePoolProduct
|
||||
{
|
||||
use HasBookingPriceCalculation;
|
||||
/**
|
||||
* Check if this is a pool product
|
||||
*/
|
||||
|
|
@ -702,7 +703,7 @@ trait MayBePoolProduct
|
|||
// Calculate days for price normalization
|
||||
$days = 1;
|
||||
if ($from && $until) {
|
||||
$days = max(1, $from->diff($until)->days);
|
||||
$days = $this->calculateBookingDays($from, $until);
|
||||
}
|
||||
|
||||
// Build usage map: price => quantity used
|
||||
|
|
|
|||
|
|
@ -0,0 +1,497 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Shop\Facades\Cart;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductPrice;
|
||||
use Blax\Shop\Tests\TestCase;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Workbench\App\Models\User;
|
||||
|
||||
class BookingPerMinutePricingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
protected Product $bookingProduct;
|
||||
protected ProductPrice $price;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->actingAs($this->user);
|
||||
|
||||
// Create a booking product
|
||||
$this->bookingProduct = Product::factory()->create([
|
||||
'name' => 'Conference Room',
|
||||
'slug' => 'conference-room',
|
||||
'type' => ProductType::BOOKING,
|
||||
'manage_stock' => true,
|
||||
'stock_quantity' => 0,
|
||||
]);
|
||||
|
||||
// Initialize stock
|
||||
$this->bookingProduct->increaseStock(10);
|
||||
|
||||
// Create a price: $100.00 per day (24 hours)
|
||||
$this->price = ProductPrice::factory()->create([
|
||||
'purchasable_id' => $this->bookingProduct->id,
|
||||
'purchasable_type' => Product::class,
|
||||
'unit_amount' => 100, // $100.00 per day
|
||||
'currency' => 'USD',
|
||||
'is_default' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_exact_24_hours()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(9, 0, 0); // Exactly 24 hours = 1 day
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// Expecting exactly 100.00 for 1 day
|
||||
$this->assertEquals('100.00', $cartItem->price);
|
||||
$this->assertEquals(100.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_12_hours_as_half_day()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(21, 0, 0); // 12 hours = 0.5 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// Expecting 50.00 for 0.5 days (12 hours)
|
||||
$this->assertEquals('50.00', $cartItem->price);
|
||||
$this->assertEquals(50.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_36_hours()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(21, 0, 0); // 36 hours = 1.5 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// Expecting 150.00 for 1.5 days (36 hours)
|
||||
$this->assertEquals('150.00', $cartItem->price);
|
||||
$this->assertEquals(150.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_6_hours()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(16, 0, 0); // 6 hours = 0.25 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// Expecting 25.00 for 0.25 days (6 hours)
|
||||
$this->assertEquals('25.00', $cartItem->price);
|
||||
$this->assertEquals(25.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_90_minutes()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(14, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(15, 30, 0); // 90 minutes = 1.5 hours = 0.0625 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// 90 minutes = 1.5 hours = 0.0625 days
|
||||
// Price: 100.00 * 0.0625 = 6.25
|
||||
$this->assertEquals('6.25', $cartItem->price);
|
||||
$this->assertEquals(6.25, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_2_days_and_3_hours()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(7)->setTime(12, 0, 0); // 2 days + 3 hours = 51 hours = 2.125 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// 51 hours = 2.125 days
|
||||
// Price: 100.00 * 2.125 = 212.50
|
||||
$this->assertEquals('212.50', $cartItem->price);
|
||||
$this->assertEquals(212.50, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_with_quantity_for_fractional_days()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(22, 0, 0); // 12 hours = 0.5 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 3, $from, $until);
|
||||
|
||||
// 0.5 days * 100.00 = 50.00 per unit
|
||||
// 3 units * 50.00 = 150.00 total
|
||||
$this->assertEquals('50.00', $cartItem->price); // price per unit
|
||||
$this->assertEquals(150.00, $cartItem->subtotal); // total for 3 units
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_recalculates_price_when_dates_are_updated()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(9, 0, 0); // 24 hours = 1 day
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// Initial price for 1 day
|
||||
$this->assertEquals('100.00', $cartItem->price);
|
||||
|
||||
// Update to 18 hours (0.75 days)
|
||||
$newUntil = Carbon::now()->addDays(6)->setTime(3, 0, 0);
|
||||
$cartItem->updateDates($from, $newUntil);
|
||||
|
||||
// Price should now be 75.00 for 0.75 days
|
||||
$this->assertEquals(75.00, $cartItem->fresh()->price);
|
||||
$this->assertEquals(75.00, $cartItem->fresh()->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_45_minutes()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(14, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(14, 45, 0); // 45 minutes = 0.03125 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// 45 minutes = 0.75 hours = 0.03125 days
|
||||
// Price: 100.00 * 0.03125 = 3.125
|
||||
$this->assertEquals(3.13, round($cartItem->price, 2)); // Rounded due to decimal precision
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_purchases_booking_with_per_minute_pricing()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(14, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(2, 0, 0); // 12 hours = 0.5 days
|
||||
|
||||
$purchase = $this->user->purchase(
|
||||
$this->price,
|
||||
2,
|
||||
null,
|
||||
$from,
|
||||
$until
|
||||
);
|
||||
|
||||
$this->assertNotNull($purchase);
|
||||
$this->assertTrue($purchase->isBooking());
|
||||
|
||||
// Stock should be decreased
|
||||
$this->bookingProduct->refresh();
|
||||
$this->assertEquals(8, $this->bookingProduct->getAvailableStock());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_updates_cart_item_from_date_recalculates_per_minute_price()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(12, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(12, 0, 0); // 24 hours
|
||||
|
||||
$cart = \Blax\Shop\Models\Cart::factory()->create([
|
||||
'customer_id' => $this->user->id,
|
||||
'customer_type' => get_class($this->user),
|
||||
]);
|
||||
$cartItem = $cart->addToCart($this->bookingProduct, 1, [], $from, $until);
|
||||
|
||||
// Initial: 1 day = 100.00
|
||||
$this->assertEquals('100.00', $cartItem->price);
|
||||
|
||||
// Update from date to make it 30 hours (1.25 days)
|
||||
$newFrom = Carbon::now()->addDays(5)->setTime(6, 0, 0);
|
||||
$cartItem->setFromDate($newFrom);
|
||||
|
||||
// Price should be 125.00 for 1.25 days
|
||||
$this->assertEquals(125.00, $cartItem->fresh()->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_updates_cart_item_until_date_recalculates_per_minute_price()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(22, 0, 0); // 12 hours
|
||||
|
||||
$cart = \Blax\Shop\Models\Cart::factory()->create([
|
||||
'customer_id' => $this->user->id,
|
||||
'customer_type' => get_class($this->user),
|
||||
]);
|
||||
$cartItem = $cart->addToCart($this->bookingProduct, 1, [], $from, $until);
|
||||
|
||||
// Initial: 0.5 days = 50.00
|
||||
$this->assertEquals('50.00', $cartItem->price);
|
||||
|
||||
// Update until date to make it 18 hours (0.75 days)
|
||||
$newUntil = Carbon::now()->addDays(6)->setTime(4, 0, 0);
|
||||
$cartItem->setUntilDate($newUntil);
|
||||
|
||||
// Price should be 75.00 for 0.75 days
|
||||
$this->assertEquals(75.00, $cartItem->fresh()->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_weekend_booking_friday_to_monday()
|
||||
{
|
||||
// Friday 6pm to Monday 10am = 64 hours = 2.666... days
|
||||
$from = Carbon::now()->addDays(5)->setTime(18, 0, 0); // Friday 6pm
|
||||
$until = Carbon::now()->addDays(8)->setTime(10, 0, 0); // Monday 10am
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// 64 hours = 2.6667 days (rounded)
|
||||
// Price: 100.00 * 2.6667 = 266.67
|
||||
$expectedPrice = round(100.00 * (64 / 24), 2);
|
||||
$this->assertEquals($expectedPrice, $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_multiple_bookings_with_different_durations()
|
||||
{
|
||||
$cart = \Blax\Shop\Models\Cart::factory()->create([
|
||||
'customer_id' => $this->user->id,
|
||||
'customer_type' => get_class($this->user),
|
||||
]);
|
||||
|
||||
// Booking 1: 12 hours
|
||||
$from1 = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until1 = Carbon::now()->addDays(5)->setTime(21, 0, 0);
|
||||
$item1 = $cart->addToCart($this->bookingProduct, 1, [], $from1, $until1);
|
||||
|
||||
// Booking 2: 6 hours
|
||||
$from2 = Carbon::now()->addDays(10)->setTime(10, 0, 0);
|
||||
$until2 = Carbon::now()->addDays(10)->setTime(16, 0, 0);
|
||||
$item2 = $cart->addToCart($this->bookingProduct, 1, [], $from2, $until2);
|
||||
|
||||
$this->assertEquals('50.00', $item1->price); // 12 hours = 0.5 days
|
||||
$this->assertEquals('25.00', $item2->price); // 6 hours = 0.25 days
|
||||
|
||||
// Total cart should be 75.00
|
||||
$this->assertEquals(75.00, $cart->getTotal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_precise_price_for_irregular_time_spans()
|
||||
{
|
||||
// Test various odd time spans
|
||||
$testCases = [
|
||||
['hours' => 1, 'expectedDays' => 1 / 24, 'expectedPrice' => 4.17], // 1 hour
|
||||
['hours' => 3, 'expectedDays' => 3 / 24, 'expectedPrice' => 12.50], // 3 hours
|
||||
['hours' => 7, 'expectedDays' => 7 / 24, 'expectedPrice' => 29.17], // 7 hours
|
||||
['hours' => 13, 'expectedDays' => 13 / 24, 'expectedPrice' => 54.17], // 13 hours
|
||||
['hours' => 25, 'expectedDays' => 25 / 24, 'expectedPrice' => 104.17], // 25 hours
|
||||
];
|
||||
|
||||
foreach ($testCases as $testCase) {
|
||||
$from = Carbon::now()->addDays(5)->setTime(12, 0, 0);
|
||||
$until = $from->copy()->addHours($testCase['hours']);
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
$this->assertEquals(
|
||||
$testCase['expectedPrice'],
|
||||
$cartItem->price,
|
||||
"Failed for {$testCase['hours']} hours"
|
||||
);
|
||||
|
||||
// Clean up for next test
|
||||
$cartItem->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_booking_removal_and_readdition()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(16, 0, 0); // 6 hours
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 2, $from, $until);
|
||||
|
||||
// Verify per-minute pricing is correct
|
||||
$this->assertEquals('25.00', $cartItem->price);
|
||||
$this->assertEquals(2, $cartItem->quantity);
|
||||
$this->assertEquals(50.00, $cartItem->subtotal);
|
||||
|
||||
// Remove item
|
||||
$cartItemId = $cartItem->id;
|
||||
$cartItem->delete();
|
||||
|
||||
// Verify deletion
|
||||
$this->assertNull(\Blax\Shop\Models\CartItem::find($cartItemId));
|
||||
|
||||
// Re-add with different quantity - price calculation should be consistent
|
||||
$cartItem2 = Cart::addBooking($this->bookingProduct, 3, $from, $until);
|
||||
$this->assertEquals('25.00', $cartItem2->price);
|
||||
$this->assertEquals(3, $cartItem2->quantity);
|
||||
$this->assertEquals(75.00, $cartItem2->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_half_hour_increments()
|
||||
{
|
||||
$testCases = [
|
||||
['minutes' => 30, 'expectedPrice' => '2.08'], // 0.5 hours
|
||||
['minutes' => 90, 'expectedPrice' => '6.25'], // 1.5 hours
|
||||
['minutes' => 150, 'expectedPrice' => '10.42'], // 2.5 hours
|
||||
['minutes' => 210, 'expectedPrice' => '14.58'], // 3.5 hours
|
||||
['minutes' => 270, 'expectedPrice' => '18.75'], // 4.5 hours
|
||||
];
|
||||
|
||||
foreach ($testCases as $testCase) {
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = $from->copy()->addMinutes($testCase['minutes']);
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
$this->assertEquals(
|
||||
$testCase['expectedPrice'],
|
||||
$cartItem->price,
|
||||
"Failed for {$testCase['minutes']} minutes"
|
||||
);
|
||||
|
||||
$cartItem->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_exact_hour_boundaries()
|
||||
{
|
||||
// Test exact hour boundaries from 1-10 hours
|
||||
for ($hours = 1; $hours <= 10; $hours++) {
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = $from->copy()->addHours($hours);
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
$expectedDays = $hours / 24;
|
||||
$expectedPrice = number_format(100 * $expectedDays, 2, '.', '');
|
||||
|
||||
$this->assertEquals(
|
||||
$expectedPrice,
|
||||
$cartItem->price,
|
||||
"Failed for {$hours} hours"
|
||||
);
|
||||
|
||||
$cartItem->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_maintains_price_consistency_across_cart_operations()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(14, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(20, 0, 0); // 6 hours
|
||||
|
||||
// Add to cart
|
||||
$cart = $this->user->currentCart();
|
||||
$cartItem = $cart->addToCart($this->bookingProduct, 1, [], $from, $until);
|
||||
$initialPrice = $cartItem->price;
|
||||
|
||||
// Refresh and check price hasn't changed
|
||||
$cartItem->refresh();
|
||||
$this->assertEquals($initialPrice, $cartItem->price);
|
||||
|
||||
// Get cart total
|
||||
$total = $cart->getTotal();
|
||||
$this->assertEquals(25.00, $total);
|
||||
|
||||
// Add more quantity
|
||||
$cart->addToCart($this->bookingProduct, 1, [], $from, $until);
|
||||
$cart->refresh();
|
||||
|
||||
// Total should double
|
||||
$this->assertEquals(50.00, $cart->getTotal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_very_long_booking_periods()
|
||||
{
|
||||
// 7 days = 168 hours
|
||||
$from = Carbon::now()->addDays(5)->setTime(12, 0, 0);
|
||||
$until = Carbon::now()->addDays(12)->setTime(12, 0, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// 7 days * $100 = $700.00
|
||||
$this->assertEquals('700.00', $cartItem->price);
|
||||
$this->assertEquals(700.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_7_hour_30_minute_booking()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(16, 30, 0); // 7.5 hours
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// 7.5 hours = 0.3125 days, $100 * 0.3125 = $31.25
|
||||
$this->assertEquals('31.25', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_booking_crossing_midnight()
|
||||
{
|
||||
// 11 PM to 3 AM = 4 hours
|
||||
$from = Carbon::now()->addDays(5)->setTime(23, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(3, 0, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// 4 hours = 0.166667 days, $100 * 0.166667 = $16.67
|
||||
$this->assertEquals('16.67', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_validates_minimum_price_for_very_short_bookings()
|
||||
{
|
||||
// 2 minutes
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(10, 2, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
// 2 minutes = 0.001389 days, $100 * 0.001389 = $0.1389, rounds to $0.14
|
||||
$this->assertEquals('0.14', $cartItem->price);
|
||||
|
||||
// Should still be less than 1 dollar
|
||||
$this->assertLessThan(1.0, (float)$cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_15_minute_interval_bookings()
|
||||
{
|
||||
$intervals = [15, 30, 45, 60, 75, 90, 105, 120];
|
||||
|
||||
foreach ($intervals as $minutes) {
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = $from->copy()->addMinutes($minutes);
|
||||
|
||||
$cartItem = Cart::addBooking($this->bookingProduct, 1, $from, $until);
|
||||
|
||||
$expectedDays = $minutes / 1440;
|
||||
$expectedPrice = number_format(100 * $expectedDays, 2, '.', '');
|
||||
|
||||
$this->assertEquals(
|
||||
$expectedPrice,
|
||||
$cartItem->price,
|
||||
"Failed for {$minutes} minutes"
|
||||
);
|
||||
|
||||
$cartItem->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -473,19 +473,19 @@ class CartAddToCartPoolPricingTest extends TestCase
|
|||
ProductPrice::factory()->create([
|
||||
'purchasable_id' => $this->poolProduct->id,
|
||||
'purchasable_type' => Product::class,
|
||||
'unit_amount' => 3000,
|
||||
'unit_amount' => 30,
|
||||
'currency' => 'USD',
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
// Same day booking (0 days diff)
|
||||
// Same day booking (4 hours)
|
||||
$from = Carbon::now()->addDays(1)->setTime(10, 0);
|
||||
$until = Carbon::now()->addDays(1)->setTime(14, 0);
|
||||
|
||||
$cartItem = $this->cart->addToCart($this->poolProduct, 1, [], $from, $until);
|
||||
|
||||
// Should treat as minimum 1 day
|
||||
$this->assertEquals(3000, $cartItem->price); // 30.00 × 1 day
|
||||
// 4 hours = 0.1667 days, 30 * 0.1667 = 5.00 (rounded to 2 decimals)
|
||||
$this->assertEquals('5.00', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
|
@ -1199,9 +1199,10 @@ class CartAddToCartPoolPricingTest extends TestCase
|
|||
$until
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(2000 * 2 * 5) + (5000 * 1 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01 // Allow 1 cent tolerance for floating point errors
|
||||
);
|
||||
$this->assertEquals(
|
||||
5000,
|
||||
|
|
@ -1216,9 +1217,10 @@ class CartAddToCartPoolPricingTest extends TestCase
|
|||
$until
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(2000 * 2 * 5) + (5000 * 2 * 5) + (8000 * 2 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01
|
||||
);
|
||||
$this->assertNull($pool->getCurrentPrice());
|
||||
|
||||
|
|
@ -1257,24 +1259,27 @@ class CartAddToCartPoolPricingTest extends TestCase
|
|||
$until
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(2000 * 2 * 5) + (5000 * 1 * 5) + (8000 * 2 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01
|
||||
);
|
||||
|
||||
$cart->removeFromCart($pool, 1);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(2000 * 2 * 5) + (5000 * 1 * 5) + (8000 * 1 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01
|
||||
);
|
||||
$this->assertEquals(8000, $pool->getCurrentPrice());
|
||||
|
||||
$cart->removeFromCart($pool, 1);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(2000 * 2 * 5) + (5000 * 1 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01
|
||||
);
|
||||
|
||||
// Get cart item with price 2000
|
||||
|
|
@ -1284,9 +1289,10 @@ class CartAddToCartPoolPricingTest extends TestCase
|
|||
|
||||
$cart->removeFromCart($cartItem, 1);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(2000 * 1 * 5) + (5000 * 1 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01
|
||||
);
|
||||
$this->assertEquals(
|
||||
2000,
|
||||
|
|
@ -1301,9 +1307,10 @@ class CartAddToCartPoolPricingTest extends TestCase
|
|||
$until
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(2000 * 2 * 5) + (5000 * 1 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01
|
||||
);
|
||||
|
||||
// Get cart item with price 2000
|
||||
|
|
@ -1313,9 +1320,10 @@ class CartAddToCartPoolPricingTest extends TestCase
|
|||
|
||||
$cart->removeFromCart($cartItem, 2);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(5000 * 1 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01
|
||||
);
|
||||
|
||||
$this->assertEquals(2000, $pool->getCurrentPrice());
|
||||
|
|
@ -1328,9 +1336,10 @@ class CartAddToCartPoolPricingTest extends TestCase
|
|||
$until
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
$this->assertEqualsWithDelta(
|
||||
(2000 * 2 * 5) + (5000 * 1 * 5) + (8000 * 2 * 5),
|
||||
$cart->getTotal()
|
||||
$cart->getTotal(),
|
||||
0.01
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,522 @@
|
|||
<?php
|
||||
|
||||
namespace Blax\Shop\Tests\Feature;
|
||||
|
||||
use Blax\Shop\Enums\ProductRelationType;
|
||||
use Blax\Shop\Enums\ProductType;
|
||||
use Blax\Shop\Enums\PricingStrategy;
|
||||
use Blax\Shop\Facades\Cart;
|
||||
use Blax\Shop\Models\Product;
|
||||
use Blax\Shop\Models\ProductPrice;
|
||||
use Blax\Shop\Tests\TestCase;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Workbench\App\Models\User;
|
||||
|
||||
class PoolPerMinutePricingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
protected Product $poolProduct;
|
||||
protected Product $singleItem1;
|
||||
protected Product $singleItem2;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->actingAs($this->user);
|
||||
|
||||
// Create pool product
|
||||
$this->poolProduct = Product::factory()->create([
|
||||
'name' => 'Parking Pool',
|
||||
'slug' => 'parking-pool',
|
||||
'type' => ProductType::POOL,
|
||||
'manage_stock' => false,
|
||||
]);
|
||||
|
||||
// Create single items (parking spots)
|
||||
$this->singleItem1 = Product::factory()->create([
|
||||
'name' => 'Parking Spot A',
|
||||
'type' => ProductType::BOOKING,
|
||||
'manage_stock' => true,
|
||||
]);
|
||||
$this->singleItem1->increaseStock(1);
|
||||
|
||||
$this->singleItem2 = Product::factory()->create([
|
||||
'name' => 'Parking Spot B',
|
||||
'type' => ProductType::BOOKING,
|
||||
'manage_stock' => true,
|
||||
]);
|
||||
$this->singleItem2->increaseStock(1);
|
||||
|
||||
// Link single items to pool
|
||||
$this->poolProduct->productRelations()->attach($this->singleItem1->id, [
|
||||
'type' => ProductRelationType::SINGLE->value,
|
||||
]);
|
||||
$this->poolProduct->productRelations()->attach($this->singleItem2->id, [
|
||||
'type' => ProductRelationType::SINGLE->value,
|
||||
]);
|
||||
|
||||
// Set prices on single items: $50 and $30 per day
|
||||
ProductPrice::factory()->create([
|
||||
'purchasable_id' => $this->singleItem1->id,
|
||||
'purchasable_type' => Product::class,
|
||||
'unit_amount' => 50, // $50.00 per day
|
||||
'currency' => 'USD',
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
ProductPrice::factory()->create([
|
||||
'purchasable_id' => $this->singleItem2->id,
|
||||
'purchasable_type' => Product::class,
|
||||
'unit_amount' => 30, // $30.00 per day
|
||||
'currency' => 'USD',
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
// Set pool to use LOWEST pricing strategy (default)
|
||||
$this->poolProduct->setPricingStrategy(PricingStrategy::LOWEST);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_for_12_hours()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(8, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(20, 0, 0); // 12 hours = 0.5 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is $30, for 0.5 days = $15.00
|
||||
$this->assertEquals(15.00, $cartItem->price);
|
||||
$this->assertEquals(15.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_for_36_hours()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(21, 0, 0); // 36 hours = 1.5 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is $30, for 1.5 days = $45.00
|
||||
$this->assertEquals(45.00, $cartItem->price);
|
||||
$this->assertEquals(45.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_for_6_hours()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(16, 0, 0); // 6 hours = 0.25 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is $30, for 0.25 days = $7.50
|
||||
$this->assertEquals(7.50, $cartItem->price);
|
||||
$this->assertEquals(7.50, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_for_90_minutes()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(14, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(15, 30, 0); // 90 minutes = 1.5 hours = 0.0625 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is $30, for 0.0625 days = $1.875 (rounds to 1.88)
|
||||
$this->assertEquals(1.88, round($cartItem->price, 2));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_uses_direct_pool_price_for_fractional_days()
|
||||
{
|
||||
// Set direct price on pool instead of using inherited pricing
|
||||
ProductPrice::factory()->create([
|
||||
'purchasable_id' => $this->poolProduct->id,
|
||||
'purchasable_type' => Product::class,
|
||||
'unit_amount' => 20, // $20.00 per day
|
||||
'currency' => 'USD',
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(22, 0, 0); // 12 hours = 0.5 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Direct pool price is $20, for 0.5 days = $10.00
|
||||
$this->assertEquals(10.00, $cartItem->price);
|
||||
$this->assertEquals(10.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_with_quantity_for_fractional_days()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(15, 0, 0); // 6 hours = 0.25 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 2, $from, $until);
|
||||
|
||||
// Lowest price is $30, for 0.25 days = $7.50 per unit
|
||||
// 2 units * $7.50 = $15.00 total
|
||||
$this->assertEquals(7.50, $cartItem->price); // price per unit
|
||||
$this->assertEquals(15.00, $cartItem->subtotal); // total for 2 units
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_uses_highest_pricing_strategy_for_fractional_days()
|
||||
{
|
||||
// Change to HIGHEST pricing strategy
|
||||
$this->poolProduct->setPricingStrategy(PricingStrategy::HIGHEST);
|
||||
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(22, 0, 0); // 12 hours = 0.5 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Highest price is $50, for 0.5 days = $25.00
|
||||
$this->assertEquals(25.00, $cartItem->price);
|
||||
$this->assertEquals(25.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_uses_average_pricing_strategy_for_fractional_days()
|
||||
{
|
||||
// Change to AVERAGE pricing strategy
|
||||
$this->poolProduct->setPricingStrategy(PricingStrategy::AVERAGE);
|
||||
|
||||
$from = Carbon::now()->addDays(5)->setTime(8, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(20, 0, 0); // 12 hours = 0.5 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Average price is ($50 + $30) / 2 = $40, for 0.5 days = $20.00
|
||||
$this->assertEquals(20.00, $cartItem->price);
|
||||
$this->assertEquals(20.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_for_multiple_fractional_bookings_in_cart()
|
||||
{
|
||||
$from1 = Carbon::now()->addDays(10)->setTime(10, 0, 0);
|
||||
$until1 = Carbon::now()->addDays(10)->setTime(16, 0, 0); // 6 hours = 0.25 days
|
||||
|
||||
$from2 = Carbon::now()->addDays(12)->setTime(14, 0, 0);
|
||||
$until2 = Carbon::now()->addDays(12)->setTime(20, 0, 0); // 6 hours = 0.25 days
|
||||
|
||||
$cart = \Blax\Shop\Models\Cart::factory()->create([
|
||||
'customer_id' => $this->user->id,
|
||||
'customer_type' => get_class($this->user),
|
||||
]);
|
||||
|
||||
// Add two different fractional bookings
|
||||
$cartItem1 = $cart->addToCart($this->poolProduct, 1, [], $from1, $until1);
|
||||
$cartItem2 = $cart->addToCart($this->poolProduct, 1, [], $from2, $until2);
|
||||
|
||||
// First booking uses lowest pricing: $30 * 0.25 = $7.50
|
||||
$this->assertEquals('7.50', $cartItem1->price);
|
||||
// Second booking may use next available pricing tier
|
||||
$this->assertGreaterThanOrEqual(7.50, (float)$cartItem2->price);
|
||||
|
||||
// Total should be reasonable for two 6-hour bookings
|
||||
$this->assertGreaterThan(15.00, $cart->getTotal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_for_very_short_durations()
|
||||
{
|
||||
// 30 minutes
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(10, 30, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// 30 minutes = 0.020833 days, $30 * 0.020833 = $0.62499, rounds to $0.62
|
||||
$this->assertEquals('0.62', $cartItem->price);
|
||||
|
||||
// 15 minutes
|
||||
$from2 = Carbon::now()->addDays(6)->setTime(14, 0, 0);
|
||||
$until2 = Carbon::now()->addDays(6)->setTime(14, 15, 0);
|
||||
|
||||
$cartItem2 = Cart::addBooking($this->poolProduct, 1, $from2, $until2);
|
||||
|
||||
// 15 minutes = 0.010417 days, $30 * 0.010417 = $0.3125, rounds to $0.31
|
||||
$this->assertEquals('0.31', $cartItem2->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_multiple_pool_bookings_with_different_durations()
|
||||
{
|
||||
$cart = \Blax\Shop\Models\Cart::factory()->create([
|
||||
'customer_id' => $this->user->id,
|
||||
'customer_type' => get_class($this->user),
|
||||
]);
|
||||
|
||||
// Booking 1: 12 hours
|
||||
$from1 = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until1 = Carbon::now()->addDays(5)->setTime(21, 0, 0);
|
||||
$item1 = $cart->addToCart($this->poolProduct, 1, [], $from1, $until1);
|
||||
|
||||
// Booking 2: 6 hours (different dates, so spots available)
|
||||
$from2 = Carbon::now()->addDays(10)->setTime(10, 0, 0);
|
||||
$until2 = Carbon::now()->addDays(10)->setTime(16, 0, 0);
|
||||
$item2 = $cart->addToCart($this->poolProduct, 1, [], $from2, $until2);
|
||||
|
||||
// First booking: $30 * 0.5 = $15.00
|
||||
$this->assertEquals(15.00, $item1->price);
|
||||
|
||||
// Second booking: $30 * 0.25 = $7.50 (different dates, so spots available)
|
||||
$this->assertEquals(7.50, $item2->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_for_3_hours()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(14, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(17, 0, 0); // 3 hours = 0.125 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is $30, for 0.125 days = $3.75
|
||||
$this->assertEquals(3.75, $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_pool_price_for_odd_duration_5_hours_30_minutes()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 30, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(15, 0, 0); // 5.5 hours = 0.229167 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is $30, for 5.5 hours (0.229167 days) = $6.875 (rounds to 6.88)
|
||||
$expectedPrice = round(30.00 * (5.5 / 24), 2);
|
||||
$this->assertEquals($expectedPrice, round($cartItem->price, 2));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_pool_booking_over_multiple_days_with_hours()
|
||||
{
|
||||
// Monday 2pm to Wednesday 5pm = 51 hours = 2.125 days
|
||||
$from = Carbon::now()->addDays(10)->setTime(14, 0, 0);
|
||||
$until = Carbon::now()->addDays(12)->setTime(17, 0, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is $30, for 2.125 days = $63.75
|
||||
$expectedPrice = 30.00 * 2.125;
|
||||
$this->assertEquals($expectedPrice, $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_prices_pool_correctly_when_both_spots_have_different_prices_for_fractional_time()
|
||||
{
|
||||
// Create a third spot with an even different price
|
||||
$singleItem3 = Product::factory()->create([
|
||||
'name' => 'Parking Spot C',
|
||||
'type' => ProductType::BOOKING,
|
||||
'manage_stock' => true,
|
||||
]);
|
||||
$singleItem3->increaseStock(1);
|
||||
|
||||
ProductPrice::factory()->create([
|
||||
'purchasable_id' => $singleItem3->id,
|
||||
'purchasable_type' => Product::class,
|
||||
'unit_amount' => 40, // $40.00 per day
|
||||
'currency' => 'USD',
|
||||
'is_default' => true,
|
||||
]);
|
||||
|
||||
$this->poolProduct->productRelations()->attach($singleItem3->id, [
|
||||
'type' => ProductRelationType::SINGLE->value,
|
||||
]);
|
||||
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(16, 0, 0); // 6 hours = 0.25 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is still $30, for 0.25 days = $7.50
|
||||
$this->assertEquals(7.50, $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_exact_24_hours_pool()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(9, 0, 0); // Exactly 24 hours = 1 day
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// Lowest price is $30, for exactly 1 day = $30.00
|
||||
$this->assertEquals(30.00, $cartItem->price);
|
||||
$this->assertEquals(30.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_updates_pool_cart_item_from_date_recalculates_per_minute_price()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(12, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(12, 0, 0); // 24 hours
|
||||
|
||||
$cart = \Blax\Shop\Models\Cart::factory()->create([
|
||||
'customer_id' => $this->user->id,
|
||||
'customer_type' => get_class($this->user),
|
||||
]);
|
||||
$cartItem = $cart->addToCart($this->poolProduct, 1, [], $from, $until);
|
||||
|
||||
// Initial: 1 day = $30.00
|
||||
$this->assertEquals(30.00, $cartItem->price);
|
||||
|
||||
// Update from date to make it 30 hours (1.25 days)
|
||||
$newFrom = Carbon::now()->addDays(5)->setTime(6, 0, 0);
|
||||
$cartItem->setFromDate($newFrom);
|
||||
|
||||
// Price should be $30 * 1.25 = $37.50
|
||||
$this->assertEquals(37.50, $cartItem->fresh()->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_booking_spanning_exactly_two_days()
|
||||
{
|
||||
$from = Carbon::now()->addDays(5)->setTime(0, 0, 0);
|
||||
$until = Carbon::now()->addDays(7)->setTime(0, 0, 0); // Exactly 48 hours
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// 48 hours = 2 days, $30 * 2 = $60.00
|
||||
$this->assertEquals('60.00', $cartItem->price);
|
||||
$this->assertEquals(60.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_for_business_hours_booking()
|
||||
{
|
||||
// 9 AM to 5 PM = 8 hours
|
||||
$from = Carbon::now()->addDays(5)->setTime(9, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(17, 0, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// 8 hours = 0.333333 days, $30 * 0.333333 = $10.00
|
||||
$this->assertEquals('10.00', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_overnight_booking()
|
||||
{
|
||||
// 10 PM to 6 AM next day = 8 hours
|
||||
$from = Carbon::now()->addDays(5)->setTime(22, 0, 0);
|
||||
$until = Carbon::now()->addDays(6)->setTime(6, 0, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// 8 hours = 0.333333 days, $30 * 0.333333 = $10.00
|
||||
$this->assertEquals('10.00', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_price_with_minutes_precision()
|
||||
{
|
||||
// 2 hours and 45 minutes
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(12, 45, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// 165 minutes = 0.114583 days, $30 * 0.114583 = $3.4375, rounds to $3.44
|
||||
$this->assertEquals('3.44', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_maintains_precision_for_multiple_quantity()
|
||||
{
|
||||
// 6 hours
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(16, 0, 0);
|
||||
|
||||
// Pool has 2 items, so max quantity is 2
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 2, $from, $until);
|
||||
|
||||
// 6 hours = 0.25 days, price per unit varies by pool pricing
|
||||
$this->assertEquals(2, $cartItem->quantity);
|
||||
// Subtotal should be reasonable for 6 hours with 2 items
|
||||
$this->assertGreaterThan(10.00, $cartItem->subtotal);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_weekend_hourly_booking()
|
||||
{
|
||||
// Friday 6 PM to Sunday 6 PM = 48 hours exactly
|
||||
$from = Carbon::now()->next(Carbon::FRIDAY)->setTime(18, 0, 0);
|
||||
$until = Carbon::now()->next(Carbon::FRIDAY)->addDays(2)->setTime(18, 0, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// 48 hours = 2 days, $30 * 2 = $60.00
|
||||
$this->assertEquals('60.00', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_different_pricing_strategies_for_fractional_time()
|
||||
{
|
||||
// Test LOWEST (already set in setUp)
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(13, 0, 0); // 3 hours = 0.125 days
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
// Lowest: $30 * 0.125 = $3.75
|
||||
$this->assertEquals('3.75', $cartItem->price);
|
||||
|
||||
// Clear cart
|
||||
$cartItem->delete();
|
||||
|
||||
// Test HIGHEST
|
||||
$this->poolProduct->setPricingStrategy(PricingStrategy::HIGHEST);
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
// Highest: $50 * 0.125 = $6.25
|
||||
$this->assertEquals('6.25', $cartItem->price);
|
||||
|
||||
// Clear cart
|
||||
$cartItem->delete();
|
||||
|
||||
// Test AVERAGE
|
||||
$this->poolProduct->setPricingStrategy(PricingStrategy::AVERAGE);
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
// Average: ($50 + $30) / 2 = $40, $40 * 0.125 = $5.00
|
||||
$this->assertEquals('5.00', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_single_minute_booking()
|
||||
{
|
||||
// Just 1 minute
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(10, 1, 0);
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// 1 minute = 0.000694 days (minimum), $30 * 0.000694 = $0.02082, rounds to $0.02
|
||||
$this->assertEquals('0.02', $cartItem->price);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_booking_with_seconds_precision()
|
||||
{
|
||||
// 2 hours, 30 minutes, 30 seconds (Carbon truncates seconds to minutes)
|
||||
$from = Carbon::now()->addDays(5)->setTime(10, 0, 0);
|
||||
$until = Carbon::now()->addDays(5)->setTime(12, 30, 30);
|
||||
|
||||
$cartItem = Cart::addBooking($this->poolProduct, 1, $from, $until);
|
||||
|
||||
// 150 minutes (seconds truncated), $30 * (150/1440) = $3.125, rounds to $3.13 or $3.14
|
||||
$price = (float)$cartItem->price;
|
||||
$this->assertGreaterThanOrEqual(3.12, $price);
|
||||
$this->assertLessThanOrEqual(3.14, $price);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue