A stock methods calendarAvailability & dayAvailability
This commit is contained in:
parent
38e841a986
commit
5ac0229555
|
|
@ -6,6 +6,7 @@ use Blax\Shop\Enums\StockStatus;
|
||||||
use Blax\Shop\Enums\StockType;
|
use Blax\Shop\Enums\StockType;
|
||||||
use Blax\Shop\Exceptions\NotEnoughStockException;
|
use Blax\Shop\Exceptions\NotEnoughStockException;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use DateTimeInterface;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
@ -510,4 +511,170 @@ trait HasStocks
|
||||||
|
|
||||||
return $this->getAvailableStock($date);
|
return $this->getAvailableStock($date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the available amounts per date range, with $from and $until specified
|
||||||
|
* Returns associative array with keys
|
||||||
|
* - 'max_available' => Shows the peak available stock in the date range
|
||||||
|
* - 'min_available' => Shows the lowest available stock in the date range
|
||||||
|
* - 'dates' => An array of dates with their respective available stock
|
||||||
|
*
|
||||||
|
* @param \DateTimeInterface $from Start date of the range (optional, defaults to today)
|
||||||
|
* @param \DateTimeInterface $until End date of the range (optional, defaults to 30 days)
|
||||||
|
* @return array Associative array with 'max_available', 'min_available', and 'dates'
|
||||||
|
*/
|
||||||
|
public function calendarAvailability(
|
||||||
|
?DateTimeInterface $from = null,
|
||||||
|
?DateTimeInterface $until = null
|
||||||
|
): array {
|
||||||
|
if ($this->manage_stock === false) {
|
||||||
|
return [
|
||||||
|
'max_available' => PHP_INT_MAX,
|
||||||
|
'min_available' => PHP_INT_MAX,
|
||||||
|
'dates' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fromDate = Carbon::parse($from ?? now())->startOfDay();
|
||||||
|
$untilDate = Carbon::parse($until ?? $fromDate->copy()->addDays(30))->endOfDay();
|
||||||
|
|
||||||
|
// Fetch all relevant stocks once for performance
|
||||||
|
$allStocks = $this->stocks()
|
||||||
|
->withoutGlobalScope('willExpire')
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->where('status', StockStatus::COMPLETED->value)
|
||||||
|
->where('type', '!=', StockType::CLAIMED->value);
|
||||||
|
})
|
||||||
|
->orWhere(function ($query) {
|
||||||
|
$query->where('status', StockStatus::PENDING->value)
|
||||||
|
->where('type', StockType::CLAIMED->value);
|
||||||
|
})
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$dates = [];
|
||||||
|
$globalMax = PHP_INT_MIN;
|
||||||
|
$globalMin = PHP_INT_MAX;
|
||||||
|
|
||||||
|
$currentDate = $fromDate->copy();
|
||||||
|
while ($currentDate->lte($untilDate)) {
|
||||||
|
$dayStart = $currentDate->copy()->startOfDay();
|
||||||
|
$dayEnd = $currentDate->copy()->endOfDay();
|
||||||
|
|
||||||
|
// Find all "event" timestamps for this day where availability might change
|
||||||
|
$events = [$dayStart, $dayEnd];
|
||||||
|
foreach ($allStocks as $stock) {
|
||||||
|
if ($stock->claimed_from && $stock->claimed_from->between($dayStart, $dayEnd)) {
|
||||||
|
$events[] = Carbon::parse($stock->claimed_from);
|
||||||
|
}
|
||||||
|
if ($stock->expires_at && $stock->expires_at->between($dayStart, $dayEnd)) {
|
||||||
|
$events[] = Carbon::parse($stock->expires_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dayMin = PHP_INT_MAX;
|
||||||
|
$dayMax = PHP_INT_MIN;
|
||||||
|
|
||||||
|
// Check availability at each event timestamp to find min/max for the day
|
||||||
|
foreach ($events as $eventTime) {
|
||||||
|
$available = 0;
|
||||||
|
foreach ($allStocks as $stock) {
|
||||||
|
if ($stock->status === StockStatus::COMPLETED && $stock->type !== StockType::CLAIMED) {
|
||||||
|
if (is_null($stock->expires_at) || $stock->expires_at > $eventTime) {
|
||||||
|
$available += $stock->quantity;
|
||||||
|
}
|
||||||
|
} elseif ($stock->status === StockStatus::PENDING && $stock->type === StockType::CLAIMED) {
|
||||||
|
// Add back if NOT active at this timestamp
|
||||||
|
$isNotStarted = $stock->claimed_from && $stock->claimed_from > $eventTime;
|
||||||
|
$isExpired = $stock->expires_at && $stock->expires_at <= $eventTime;
|
||||||
|
if ($isNotStarted || $isExpired) {
|
||||||
|
$available += $stock->quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = max(0, $available);
|
||||||
|
$dayMin = min($dayMin, $available);
|
||||||
|
$dayMax = max($dayMax, $available);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dates[$currentDate->toDateString()] = [
|
||||||
|
'min' => $dayMin,
|
||||||
|
'max' => $dayMax,
|
||||||
|
];
|
||||||
|
|
||||||
|
$globalMin = min($globalMin, $dayMin);
|
||||||
|
$globalMax = max($globalMax, $dayMax);
|
||||||
|
|
||||||
|
$currentDate->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'max_available' => $globalMax === PHP_INT_MIN ? 0 : $globalMax,
|
||||||
|
'min_available' => $globalMin === PHP_INT_MAX ? 0 : $globalMin,
|
||||||
|
'dates' => $dates,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function calendarAvailabilityDates(
|
||||||
|
?DateTimeInterface $from = null,
|
||||||
|
?DateTimeInterface $until = null
|
||||||
|
): array {
|
||||||
|
$availability = $this->calendarAvailability($from, $until);
|
||||||
|
return $availability['dates'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the availability on the day by time. 00:00 shows the availables at the start of the day.
|
||||||
|
* Every other timestamp shows what total current availability is at that time.
|
||||||
|
*
|
||||||
|
* @param null|DateTimeInterface $date
|
||||||
|
* @return array|int
|
||||||
|
*/
|
||||||
|
public function dayAvailability(?DateTimeInterface $date = null)
|
||||||
|
{
|
||||||
|
if ($this->manage_stock === false) {
|
||||||
|
return PHP_INT_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
$date = Carbon::parse($date ?? now());
|
||||||
|
$startOfDay = $date->copy()->startOfDay();
|
||||||
|
$endOfDay = $date->copy()->endOfDay();
|
||||||
|
|
||||||
|
$availability = [
|
||||||
|
'00:00' => $this->availableOnDate($startOfDay),
|
||||||
|
];
|
||||||
|
|
||||||
|
$stocks = $this->stocks()
|
||||||
|
->withoutGlobalScope('willExpire')
|
||||||
|
->where(function ($query) use ($startOfDay, $endOfDay) {
|
||||||
|
$query->where(function ($q) use ($startOfDay, $endOfDay) {
|
||||||
|
$q->whereNotNull('claimed_from')
|
||||||
|
->whereBetween('claimed_from', [$startOfDay, $endOfDay]);
|
||||||
|
})->orWhere(function ($q) use ($startOfDay, $endOfDay) {
|
||||||
|
$q->whereNotNull('expires_at')
|
||||||
|
->whereBetween('expires_at', [$startOfDay, $endOfDay]);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($stocks as $stock) {
|
||||||
|
if ($stock->claimed_from && $stock->claimed_from->isSameDay($startOfDay)) {
|
||||||
|
$timeKey = $stock->claimed_from->format('H:i');
|
||||||
|
if (!isset($availability[$timeKey])) {
|
||||||
|
$availability[$timeKey] = $this->availableOnDate($stock->claimed_from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stock->expires_at && $stock->expires_at->isSameDay($startOfDay)) {
|
||||||
|
$timeKey = $stock->expires_at->format('H:i');
|
||||||
|
if (!isset($availability[$timeKey])) {
|
||||||
|
$availability[$timeKey] = $this->availableOnDate($stock->expires_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($availability);
|
||||||
|
|
||||||
|
return $availability;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -431,4 +431,78 @@ class StockManagementTest extends TestCase
|
||||||
$this->assertFalse($result);
|
$this->assertFalse($result);
|
||||||
$this->assertCount(0, $product->stocks);
|
$this->assertCount(0, $product->stocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function it_shows_calendar_availability_correctly_with_claimed_stock()
|
||||||
|
{
|
||||||
|
$product = Product::factory()->withStocks(50)->create();
|
||||||
|
|
||||||
|
// Claim stock from day 3 to day 7
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 20,
|
||||||
|
from: now()->endOfDay()->addDays(3),
|
||||||
|
until: now()->endOfDay()->subHours(6)->addDays(7)
|
||||||
|
);
|
||||||
|
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 2,
|
||||||
|
from: now()->endOfDay()->addDays(1),
|
||||||
|
until: now()->endOfDay()->addDays(2)
|
||||||
|
);
|
||||||
|
|
||||||
|
$product->claimStock(
|
||||||
|
quantity: 5,
|
||||||
|
from: now()->endOfDay()->addDays(10),
|
||||||
|
until: now()->endOfDay()->addDays(22)
|
||||||
|
);
|
||||||
|
|
||||||
|
$availability = $product->calendarAvailability();
|
||||||
|
|
||||||
|
$this->assertEquals(50, $availability['max_available']);
|
||||||
|
$this->assertEquals(30, $availability['min_available']);
|
||||||
|
$this->assertCount(31, $availability['dates']);
|
||||||
|
|
||||||
|
// Check specific dates
|
||||||
|
$this->assertEquals(['min' => 50, 'max' => 50], $availability['dates'][now()->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 48, 'max' => 50], $availability['dates'][now()->addDays(1)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 48, 'max' => 50], $availability['dates'][now()->addDays(2)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 30, 'max' => 50], $availability['dates'][now()->addDays(3)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 30, 'max' => 30], $availability['dates'][now()->addDays(4)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 30, 'max' => 50], $availability['dates'][now()->addDays(7)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 50, 'max' => 50], $availability['dates'][now()->addDays(8)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 45, 'max' => 45], $availability['dates'][now()->addDays(11)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 45, 'max' => 50], $availability['dates'][now()->addDays(22)->toDateString()]);
|
||||||
|
$this->assertEquals(['min' => 50, 'max' => 50], $availability['dates'][now()->addDays(23)->toDateString()]);
|
||||||
|
|
||||||
|
$minValues = array_column($availability['dates'], 'min');
|
||||||
|
$valueCounts = array_count_values($minValues);
|
||||||
|
|
||||||
|
$this->assertEquals(11, $valueCounts['50']);
|
||||||
|
$this->assertEquals(2, $valueCounts['48']);
|
||||||
|
$this->assertEquals(13, $valueCounts['45']);
|
||||||
|
$this->assertEquals(5, $valueCounts['30']);
|
||||||
|
|
||||||
|
// Test custom range
|
||||||
|
$customAvailability = $product->calendarAvailability(
|
||||||
|
from: now()->addDays(3),
|
||||||
|
until: now()->addDays(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertCount(8, $customAvailability['dates']); // Day 3 to Day 10 inclusive
|
||||||
|
$this->assertEquals(50, $customAvailability['max_available']);
|
||||||
|
$this->assertEquals(30, $customAvailability['min_available']);
|
||||||
|
|
||||||
|
$customMinValues = array_column($customAvailability['dates'], 'min');
|
||||||
|
$customValueCounts = array_count_values($customMinValues);
|
||||||
|
$this->assertEquals(5, $customValueCounts['30']); // Days 3, 4, 5, 6, 7
|
||||||
|
$this->assertEquals(2, $customValueCounts['50']); // Days 8, 9
|
||||||
|
$this->assertEquals(1, $customValueCounts['45']); // Day 10
|
||||||
|
|
||||||
|
// dayAvailability
|
||||||
|
$dayAvailability = $product->dayAvailability(now()->addDays(7));
|
||||||
|
|
||||||
|
$this->assertEquals(30, $dayAvailability['00:00']);
|
||||||
|
$this->assertArrayHasKey(now()->endOfDay()->subHours(6)->addDays(7)->format('H:i'), $dayAvailability);
|
||||||
|
$this->assertEquals(50, @$dayAvailability[now()->endOfDay()->subHours(6)->addDays(7)->format('H:i')]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue