BF has_more, I hasprices->fromPrice
This commit is contained in:
parent
37b3e6bdc0
commit
6beecf597c
|
|
@ -70,4 +70,11 @@ trait HasPrices
|
||||||
{
|
{
|
||||||
return $this->prices()->exists();
|
return $this->prices()->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function fromPrice($price_id)
|
||||||
|
{
|
||||||
|
return static::whereHas('prices', function ($q) use ($price_id) {
|
||||||
|
$q->where('id', $price_id);
|
||||||
|
})->first();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -851,41 +851,146 @@ trait HasStocks
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accounts the current cart, from/until and also for pool products
|
* Get remaining available stock, accounting for cart items and date range
|
||||||
* @return int
|
*
|
||||||
|
* This method calculates how many more units can be added to a cart:
|
||||||
|
* - For pool products: aggregates availability from all single items minus cart items
|
||||||
|
* - For booking products: considers the date range for availability
|
||||||
|
* - Subtracts items already in the provided cart
|
||||||
|
*
|
||||||
|
* @param \Blax\Shop\Models\Cart|null $cart Optional cart to subtract items from
|
||||||
|
* @param \DateTimeInterface|null $from Optional start date for booking availability
|
||||||
|
* @param \DateTimeInterface|null $until Optional end date for booking availability
|
||||||
|
* @return int Available quantity (PHP_INT_MAX if unlimited)
|
||||||
*/
|
*/
|
||||||
public function getHasMoreAttribute(): int
|
public function getHasMore(
|
||||||
{
|
$cart = null,
|
||||||
|
?\DateTimeInterface $from = null,
|
||||||
|
?\DateTimeInterface $until = null
|
||||||
|
): int {
|
||||||
|
// Try to get current cart from facade if not provided
|
||||||
|
if ($cart === null) {
|
||||||
|
try {
|
||||||
|
$cart = \Blax\Shop\Facades\Cart::current();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// No cart available, that's fine
|
||||||
|
$cart = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get from/until from cart if not provided
|
||||||
|
if ($cart && $from === null && $until === null) {
|
||||||
|
$from = $cart->from;
|
||||||
|
$until = $cart->until;
|
||||||
|
}
|
||||||
|
|
||||||
if (method_exists($this, 'isPool') && $this->isPool()) {
|
if (method_exists($this, 'isPool') && $this->isPool()) {
|
||||||
// For pool products, check availability across all single items
|
return $this->getPoolHasMore($cart, $from, $until);
|
||||||
if (!$this->relationLoaded('singleProducts')) {
|
|
||||||
$this->load('singleProducts');
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalAvailable = 0;
|
|
||||||
foreach ($this->singleProducts as $single) {
|
|
||||||
$singleAvailable = $single->getHasMoreAttribute();
|
|
||||||
|
|
||||||
// If any single has unlimited availability, the pool effectively has unlimited
|
|
||||||
if ($singleAvailable === PHP_INT_MAX) {
|
|
||||||
return PHP_INT_MAX;
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalAvailable += $singleAvailable;
|
|
||||||
|
|
||||||
// Prevent overflow - cap at PHP_INT_MAX
|
|
||||||
if ($totalAvailable >= PHP_INT_MAX || $totalAvailable < 0) {
|
|
||||||
return PHP_INT_MAX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $totalAvailable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->manage_stock === false) {
|
if ($this->manage_stock === false) {
|
||||||
return PHP_INT_MAX;
|
return PHP_INT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->getAvailableStock();
|
// Get base available stock (considering date range for bookings)
|
||||||
|
$baseAvailable = ($from && $until && method_exists($this, 'isBooking') && $this->isBooking())
|
||||||
|
? $this->getMinAvailableInRange($from, $until)
|
||||||
|
: $this->getAvailableStock();
|
||||||
|
|
||||||
|
// Subtract items already in cart for this product
|
||||||
|
if ($cart) {
|
||||||
|
$inCart = $cart->items()
|
||||||
|
->where('purchasable_id', $this->getKey())
|
||||||
|
->where('purchasable_type', get_class($this))
|
||||||
|
->sum('quantity');
|
||||||
|
|
||||||
|
$baseAvailable = max(0, $baseAvailable - $inCart);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $baseAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get remaining availability for pool products, accounting for cart and dates
|
||||||
|
*
|
||||||
|
* @param \Blax\Shop\Models\Cart|null $cart
|
||||||
|
* @param \DateTimeInterface|null $from
|
||||||
|
* @param \DateTimeInterface|null $until
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getPoolHasMore(
|
||||||
|
$cart = null,
|
||||||
|
?\DateTimeInterface $from = null,
|
||||||
|
?\DateTimeInterface $until = null
|
||||||
|
): int {
|
||||||
|
if (!$this->relationLoaded('singleProducts')) {
|
||||||
|
$this->load('singleProducts');
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalAvailable = 0;
|
||||||
|
|
||||||
|
foreach ($this->singleProducts as $single) {
|
||||||
|
$singleAvailable = $single->getHasMore(null, $from, $until);
|
||||||
|
|
||||||
|
if ($singleAvailable === PHP_INT_MAX) {
|
||||||
|
return PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalAvailable += $singleAvailable;
|
||||||
|
|
||||||
|
if ($totalAvailable >= PHP_INT_MAX || $totalAvailable < 0) {
|
||||||
|
return PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract pool items already in cart
|
||||||
|
if ($cart) {
|
||||||
|
$inCart = $cart->items()
|
||||||
|
->where('purchasable_id', $this->getKey())
|
||||||
|
->where('purchasable_type', get_class($this))
|
||||||
|
->sum('quantity');
|
||||||
|
|
||||||
|
$totalAvailable = max(0, $totalAvailable - $inCart);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $totalAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get minimum available stock across a date range
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface $from
|
||||||
|
* @param \DateTimeInterface $until
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getMinAvailableInRange(\DateTimeInterface $from, \DateTimeInterface $until): int
|
||||||
|
{
|
||||||
|
$availability = $this->calendarAvailability($from, $until);
|
||||||
|
|
||||||
|
if (empty($availability['dates'])) {
|
||||||
|
return $availability['min_available'] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$minAvailable = PHP_INT_MAX;
|
||||||
|
foreach ($availability['dates'] as $dayData) {
|
||||||
|
$minAvailable = min($minAvailable, $dayData['min'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $minAvailable === PHP_INT_MAX ? 0 : $minAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute accessor for has_more
|
||||||
|
*
|
||||||
|
* Returns available stock accounting for:
|
||||||
|
* - Current cart (from Cart facade)
|
||||||
|
* - Cart's from/until dates for bookings
|
||||||
|
* - Pool product aggregation
|
||||||
|
*
|
||||||
|
* @return int Available quantity (PHP_INT_MAX if unlimited)
|
||||||
|
*/
|
||||||
|
public function getHasMoreAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->getHasMore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,33 @@
|
||||||
namespace Blax\Shop\Tests\Feature;
|
namespace Blax\Shop\Tests\Feature;
|
||||||
|
|
||||||
use Blax\Shop\Enums\ProductType;
|
use Blax\Shop\Enums\ProductType;
|
||||||
|
use Blax\Shop\Models\Cart;
|
||||||
use Blax\Shop\Models\Product;
|
use Blax\Shop\Models\Product;
|
||||||
use Blax\Shop\Tests\TestCase;
|
use Blax\Shop\Tests\TestCase;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
|
use Workbench\App\Models\User;
|
||||||
|
|
||||||
class GetHasMoreAttributeTest extends TestCase
|
class GetHasMoreAttributeTest extends TestCase
|
||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function createUserWithCart(): array
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
'id' => Str::uuid(),
|
||||||
|
'name' => 'Test User',
|
||||||
|
'email' => 'test@example.com',
|
||||||
|
'password' => bcrypt('password'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$cart = Cart::factory()->forCustomer($user)->create();
|
||||||
|
|
||||||
|
return [$user, $cart];
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function it_returns_php_int_max_when_stock_management_is_disabled()
|
public function it_returns_php_int_max_when_stock_management_is_disabled()
|
||||||
{
|
{
|
||||||
|
|
@ -311,4 +328,221 @@ class GetHasMoreAttributeTest extends TestCase
|
||||||
// 100 - 20 + 50 - 30 = 100
|
// 100 - 20 + 50 - 30 = 100
|
||||||
$this->assertEquals(100, $product->has_more);
|
$this->assertEquals(100, $product->has_more);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_subtracts_cart_items_from_available_stock()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->withStocks(10)->withPrices(1, 1000)->create();
|
||||||
|
|
||||||
|
// Add 3 to cart
|
||||||
|
$cart->addToCart($product, 3);
|
||||||
|
|
||||||
|
// Use getHasMore with explicit cart - should show 7 remaining
|
||||||
|
$this->assertEquals(7, $product->getHasMore($cart));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_subtracts_cart_items_from_pool_availability()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Hotel Rooms',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a price for the pool
|
||||||
|
$pool->prices()->create([
|
||||||
|
'unit_amount' => 10000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 3 single items with stock
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Room 10{$i}",
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single->increaseStock(1);
|
||||||
|
|
||||||
|
$single->prices()->create([
|
||||||
|
'unit_amount' => 10000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$singles[] = $single;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($singles as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool should have 3 available initially
|
||||||
|
$this->assertEquals(3, $pool->getHasMore($cart));
|
||||||
|
|
||||||
|
// Add 2 rooms to cart
|
||||||
|
$cart->addToCart($pool, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
$cart->addToCart($pool, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
|
||||||
|
// Now pool should show 1 remaining
|
||||||
|
$this->assertEquals(1, $pool->getHasMore($cart));
|
||||||
|
|
||||||
|
// Add the last room
|
||||||
|
$cart->addToCart($pool, 1, [], now()->addDays(5), now()->addDays(10));
|
||||||
|
|
||||||
|
// Now pool should show 0 remaining
|
||||||
|
$this->assertEquals(0, $pool->getHasMore($cart));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_considers_date_range_for_booking_products()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$product->increaseStock(10);
|
||||||
|
|
||||||
|
$product->prices()->create([
|
||||||
|
'unit_amount' => 5000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Claim 5 units for days 5-10
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 5,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Without date range: should show current available (10)
|
||||||
|
$this->assertEquals(10, $product->getHasMore($cart));
|
||||||
|
|
||||||
|
// With date range during claim: should show 5 available
|
||||||
|
$this->assertEquals(5, $product->getHasMore($cart, now()->addDays(6), now()->addDays(8)));
|
||||||
|
|
||||||
|
// With date range outside claim: should show 10 available
|
||||||
|
$this->assertEquals(10, $product->getHasMore($cart, now()->addDays(15), now()->addDays(20)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_uses_cart_dates_when_from_until_not_provided()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
$product = Product::factory()->create([
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$product->increaseStock(10);
|
||||||
|
|
||||||
|
$product->prices()->create([
|
||||||
|
'unit_amount' => 5000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Claim 4 units for days 5-10
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 4,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set cart dates to be during the claim period
|
||||||
|
$cart->update([
|
||||||
|
'from' => now()->addDays(6),
|
||||||
|
'until' => now()->addDays(8),
|
||||||
|
]);
|
||||||
|
$cart->refresh();
|
||||||
|
|
||||||
|
// Should use cart's from/until to determine availability (6 available during claim)
|
||||||
|
$this->assertEquals(6, $product->getHasMore($cart));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_combines_cart_items_and_date_range_for_pool_products()
|
||||||
|
{
|
||||||
|
[$user, $cart] = $this->createUserWithCart();
|
||||||
|
|
||||||
|
// Create pool product
|
||||||
|
$pool = Product::factory()->create([
|
||||||
|
'name' => 'Rental Cars',
|
||||||
|
'type' => ProductType::POOL,
|
||||||
|
'manage_stock' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$pool->prices()->create([
|
||||||
|
'unit_amount' => 15000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create 5 single items with stock
|
||||||
|
$singles = [];
|
||||||
|
for ($i = 1; $i <= 5; $i++) {
|
||||||
|
$single = Product::factory()->create([
|
||||||
|
'name' => "Car {$i}",
|
||||||
|
'type' => ProductType::BOOKING,
|
||||||
|
'manage_stock' => true,
|
||||||
|
]);
|
||||||
|
$single->increaseStock(1);
|
||||||
|
|
||||||
|
$single->prices()->create([
|
||||||
|
'unit_amount' => 15000,
|
||||||
|
'currency' => 'usd',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$singles[] = $single;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($singles as $single) {
|
||||||
|
$pool->productRelations()->attach($single->id, [
|
||||||
|
'type' => \Blax\Shop\Enums\ProductRelationType::SINGLE->value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claim 2 cars for days 5-10
|
||||||
|
$singles[0]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
$singles[1]->claimStock(
|
||||||
|
quantity: 1,
|
||||||
|
from: now()->startOfDay()->addDays(5),
|
||||||
|
until: now()->endOfDay()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set cart dates during claim period
|
||||||
|
$cart->update([
|
||||||
|
'from' => now()->addDays(6),
|
||||||
|
'until' => now()->addDays(8),
|
||||||
|
]);
|
||||||
|
$cart->refresh();
|
||||||
|
|
||||||
|
// During claim: 3 cars available (5 - 2 claimed)
|
||||||
|
$this->assertEquals(3, $pool->getHasMore($cart));
|
||||||
|
|
||||||
|
// Add 2 cars to cart
|
||||||
|
$cart->addToCart($pool, 1, [], now()->addDays(6), now()->addDays(8));
|
||||||
|
$cart->addToCart($pool, 1, [], now()->addDays(6), now()->addDays(8));
|
||||||
|
|
||||||
|
// Now should show 1 remaining (3 - 2 in cart)
|
||||||
|
$this->assertEquals(1, $pool->getHasMore($cart));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue