R reserve to claim

This commit is contained in:
Fabian @ Blax Software 2025-12-04 11:06:09 +01:00
parent 5cc7b1f3f0
commit c711afb570
10 changed files with 360 additions and 126 deletions

View File

@ -150,22 +150,24 @@ return new class extends Migration
});
}
// Product stocks table (reservations)
// Product stocks table (claims)
if (!Schema::hasTable(config('shop.tables.product_stocks', 'product_stocks'))) {
Schema::create(config('shop.tables.product_stocks', 'product_stocks'), function (Blueprint $table) {
$table->uuid('id')->primary();
$table->uuid('product_id');
$table->integer('quantity');
$table->string('type')->default('reservation'); // reservation, adjustment, sale, return
$table->string('type')->default('claimed'); // claimed, adjustment, sale, return
$table->string('status')->default('pending'); // pending, completed, cancelled, expired
$table->string('reference_type')->nullable();
$table->string('reference_id')->nullable();
$table->timestamp('claimed_from')->nullable();
$table->timestamp('expires_at')->nullable();
$table->text('note')->nullable();
$table->timestamps();
$table->index(['product_id', 'status']);
$table->index(['reference_type', 'reference_id']);
$table->index(['claimed_from', 'expires_at']);
$table->foreign('product_id')->references('id')->on(config('shop.tables.products', 'products'))->onDelete('cascade');
});
}

View File

@ -9,7 +9,7 @@ class ReleaseExpiredStocks extends Command
{
protected $signature = 'shop:release-expired-stocks';
protected $description = 'Release expired stock reservations back to inventory';
protected $description = 'Release expired stock claims back to inventory';
public function handle(): int
{
@ -18,11 +18,11 @@ class ReleaseExpiredStocks extends Command
return self::SUCCESS;
}
$this->info('Checking for expired stock reservations...');
$this->info('Checking for expired stock claims...');
$count = ProductStock::releaseExpired();
$this->info("Released {$count} expired stock reservation(s).");
$this->info("Released {$count} expired stock claim(s).");
return self::SUCCESS;
}

View File

@ -4,7 +4,7 @@ namespace Blax\Shop\Enums;
enum StockType: string
{
case RESERVATION = 'reservation';
case CLAIMED = 'claimed';
case RETURN = 'return';
case INCREASE = 'increase';
case DECREASE = 'decrease';
@ -12,7 +12,7 @@ enum StockType: string
public function label(): string
{
return match ($this) {
self::RESERVATION => 'Reservation',
self::CLAIMED => 'Claimed',
self::RETURN => 'Return',
self::INCREASE => 'Increase',
self::DECREASE => 'Decrease',

View File

@ -355,26 +355,33 @@ class Product extends Model implements Purchasable, Cartable
return true;
}
// Get stock reservations that overlap with the requested period
$overlappingReservations = $this->stocks()
->where('type', StockType::RESERVATION->value)
// Get stock claims that overlap with the requested period
$overlappingClaims = $this->stocks()
->where('type', StockType::CLAIMED->value)
->where('status', StockStatus::PENDING->value)
->where(function ($query) use ($from, $until) {
$query->where(function ($q) use ($from, $until) {
// Reservation starts during the requested period
$q->whereBetween('created_at', [$from, $until]);
// Claim starts during the requested period
$q->whereBetween('claimed_from', [$from, $until]);
})->orWhere(function ($q) use ($from, $until) {
// Reservation ends during the requested period
// Claim ends during the requested period
$q->whereBetween('expires_at', [$from, $until]);
})->orWhere(function ($q) use ($from, $until) {
// Reservation encompasses the entire requested period
$q->where('created_at', '<=', $from)
->where('expires_at', '>=', $until);
// Claim encompasses the entire requested period
$q->where('claimed_from', '<=', $from)
->where('expires_at', '>=', $until);
})->orWhere(function ($q) use ($from, $until) {
// Claim without claimed_from (immediately claimed)
$q->whereNull('claimed_from')
->where(function ($subQ) use ($from, $until) {
$subQ->whereNull('expires_at')
->orWhere('expires_at', '>=', $from);
});
});
})
->sum('quantity');
$availableStock = $this->getAvailableStock() - abs($overlappingReservations);
$availableStock = $this->getAvailableStock() - abs($overlappingClaims);
return $availableStock >= $quantity;
}

View File

@ -23,6 +23,7 @@ class ProductStock extends Model
'status',
'reference_type',
'reference_id',
'claimed_from',
'expires_at',
'note',
];
@ -31,6 +32,7 @@ class ProductStock extends Model
'quantity' => 'integer',
'type' => StockType::class,
'status' => StockStatus::class,
'claimed_from' => 'datetime',
'expires_at' => 'datetime',
];
@ -88,14 +90,15 @@ class ProductStock extends Model
return $this->expires_at;
}
public static function reserve(
public static function claim(
Product $product,
int $quantity,
$reference = null,
?\DateTimeInterface $from = null,
?\DateTimeInterface $until = null,
?string $note = null
): ?self {
return DB::transaction(function () use ($product, $quantity, $reference, $until, $note) {
return DB::transaction(function () use ($product, $quantity, $reference, $from, $until, $note) {
if (!$product->decreaseStock($quantity)) {
return null;
}
@ -103,10 +106,11 @@ class ProductStock extends Model
return self::create([
'product_id' => $product->id,
'quantity' => $quantity,
'type' => StockType::RESERVATION,
'type' => StockType::CLAIMED,
'status' => StockStatus::PENDING,
'reference_type' => $reference ? get_class($reference) : null,
'reference_id' => $reference?->id,
'claimed_from' => $from,
'expires_at' => $until,
'note' => $note,
]);
@ -187,13 +191,37 @@ class ProductStock extends Model
return $query->where('status', StockStatus::COMPLETED->value);
}
public static function scopeAvailableReservations($query)
public static function scopeAvailableClaims($query)
{
return $query->where('type', StockType::RESERVATION->value)->where('status', StockStatus::PENDING->value);
return $query->where('type', StockType::CLAIMED->value)->where('status', StockStatus::PENDING->value);
}
public static function reservations()
public static function claims()
{
return self::availableReservations();
return self::availableClaims();
}
public static function scopeAvailableOnDate($query, \DateTimeInterface $date)
{
return $query->where('type', StockType::CLAIMED->value)
->where('status', StockStatus::PENDING->value)
->where(function ($q) use ($date) {
$q->where(function ($subQuery) use ($date) {
// Claimed items with claimed_from set
$subQuery->whereNotNull('claimed_from')
->where('claimed_from', '<=', $date)
->where(function ($dateQuery) use ($date) {
$dateQuery->whereNull('expires_at')
->orWhere('expires_at', '>=', $date);
});
})->orWhere(function ($subQuery) use ($date) {
// Claimed items without claimed_from (always claimed)
$subQuery->whereNull('claimed_from')
->where(function ($dateQuery) use ($date) {
$dateQuery->whereNull('expires_at')
->orWhere('expires_at', '>=', $date);
});
});
});
}
}

View File

@ -58,12 +58,12 @@ trait HasCart
$product = $product_or_price->purchasable;
if ($product instanceof Product) {
$product->reserveStock($quantity);
$product->claimStock($quantity);
}
}
if ($product_or_price instanceof Product) {
$product_or_price->reserveStock($quantity);
$product_or_price->claimStock($quantity);
$default_prices = $product_or_price->defaultPrice()->count();

View File

@ -100,9 +100,10 @@ trait HasStocks
return true;
}
public function reserveStock(
public function claimStock(
int $quantity,
$reference = null,
?\DateTimeInterface $from = null,
?\DateTimeInterface $until = null,
?string $note = null
): ?\Blax\Shop\Models\ProductStock {
@ -113,10 +114,11 @@ trait HasStocks
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
return $stockModel::reserve(
return $stockModel::claim(
$this,
$quantity,
$reference,
$from,
$until,
$note
);
@ -131,7 +133,7 @@ trait HasStocks
return max(0, $this->AvailableStocks);
}
public function getReservedStock(): int
public function getClaimedStock(): int
{
return $this->activeStocks()->sum('quantity');
}
@ -180,12 +182,38 @@ trait HasStocks
return $this->getAvailableStock() <= $this->low_stock_threshold;
}
public function reservations()
public function claims()
{
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
return $stockModel::reservations()
return $stockModel::claims()
->willExpire()
->where('product_id', $this->id);
}
public function availableOnDate(\DateTimeInterface $date): int
{
if (!$this->manage_stock) {
return PHP_INT_MAX;
}
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
// Get current available stock (includes all completed stocks minus all currently pending claims)
$currentAvailable = $this->getAvailableStock();
// Get all currently pending claimed stocks (not date-filtered)
$allClaimedStocks = $this->stocks()
->where('type', StockType::CLAIMED->value)
->where('status', StockStatus::PENDING->value)
->sum('quantity');
// Get stocks claimed on this specific date
$claimedOnDate = $stockModel::availableOnDate($date)
->where('product_id', $this->id)
->sum('quantity');
// Available on date = current available + all claims - claims active on date
return max(0, $currentAvailable + abs($allClaimedStocks) - abs($claimedOnDate));
}
}

View File

@ -124,14 +124,14 @@ class BookingFeatureTest extends TestCase
$until
);
// Find the stock reservation
$reservation = $this->bookingProduct->stocks()
// Find the stock claim
$claim = $this->bookingProduct->stocks()
->where('type', 'decrease')
->where('expires_at', $until)
->first();
$this->assertNotNull($reservation);
$this->assertEquals($until->format('Y-m-d H:i:s'), $reservation->expires_at->format('Y-m-d H:i:s'));
$this->assertNotNull($claim);
$this->assertEquals($until->format('Y-m-d H:i:s'), $claim->expires_at->format('Y-m-d H:i:s'));
}
/** @test */

View File

@ -79,120 +79,120 @@ class ProductStockTest extends TestCase
}
/** @test */
public function reservation_reduces_available_stock()
public function claim_reduces_available_stock()
{
$product = Product::factory()->withStocks(100)->create();
$reservation = $product->reserveStock(25);
$claim = $product->claimStock(25);
$this->assertEquals(75, $product->getAvailableStock());
$this->assertNotNull($reservation);
$this->assertNotNull($claim);
}
/** @test */
public function releasing_reservation_increases_available_stock()
public function releasing_claim_increases_available_stock()
{
$product = Product::factory()->withStocks(100)->create();
$reservation = $product->reserveStock(25);
$claim = $product->claimStock(25);
$this->assertEquals(75, $product->getAvailableStock());
$reservation->release();
$claim->release();
$this->assertEquals(100, $product->refresh()->getAvailableStock());
}
/** @test */
public function permanent_reservation_has_no_expiry()
public function permanent_claim_has_no_expiry()
{
$product = Product::factory()->withStocks(50)->create();
$reservation = $product->reserveStock(10);
$claim = $product->claimStock(10);
$this->assertNull($reservation->expires_at);
$this->assertTrue($reservation->isPermanent());
$this->assertNull($claim->expires_at);
$this->assertTrue($claim->isPermanent());
}
/** @test */
public function temporary_reservation_has_expiry()
public function temporary_claim_has_expiry()
{
$product = Product::factory()->withStocks(50)->create();
$reservation = $product->reserveStock(
$claim = $product->claimStock(
quantity: 10,
until: now()->addHours(2)
);
$this->assertNotNull($reservation->expires_at);
$this->assertTrue($reservation->isTemporary());
$this->assertNotNull($claim->expires_at);
$this->assertTrue($claim->isTemporary());
}
/** @test */
public function reservation_can_have_note()
public function claim_can_have_note()
{
$product = Product::factory()->withStocks(50)->create();
$note = 'Reserved for VIP customer';
$reservation = $product->reserveStock(
$note = 'Claimed for VIP customer';
$claim = $product->claimStock(
quantity: 10,
note: $note
);
$this->assertEquals($note, $reservation->note);
$this->assertEquals($note, $claim->note);
}
/** @test */
public function cannot_reserve_more_than_available()
public function cannot_claim_more_than_available()
{
$product = Product::factory()->withStocks(10)->create();
$this->expectException(NotEnoughStockException::class);
$product->reserveStock(15);
$product->claimStock(15);
}
/** @test */
public function pending_scope_returns_unreleased_reservations()
public function pending_scope_returns_unreleased_claims()
{
$product = Product::factory()->withStocks(100)->create();
$pending = $product->reserveStock(10);
$released = $product->reserveStock(5);
$pending = $product->claimStock(10);
$released = $product->claimStock(5);
$released->release();
$pendingReservations = ProductStock::pending()->get();
$pendingClaims = ProductStock::pending()->get();
$this->assertTrue($pendingReservations->contains($pending));
$this->assertFalse($pendingReservations->contains($released));
$this->assertTrue($pendingClaims->contains($pending));
$this->assertFalse($pendingClaims->contains($released));
}
/** @test */
public function released_scope_returns_released_reservations()
public function released_scope_returns_released_claims()
{
$product = Product::factory()->withStocks(100)->create();
$pending = $product->reserveStock(10);
$released = $product->reserveStock(5);
$pending = $product->claimStock(10);
$released = $product->claimStock(5);
$released->release();
$releasedReservations = ProductStock::released()->get();
$releasedClaims = ProductStock::released()->get();
$this->assertFalse($releasedReservations->contains($pending));
$this->assertTrue($releasedReservations->contains($released));
$this->assertFalse($releasedClaims->contains($pending));
$this->assertTrue($releasedClaims->contains($released));
}
/** @test */
public function expired_reservations_dont_affect_available_stock()
public function expired_claims_dont_affect_available_stock()
{
$product = Product::factory()->withStocks(100)->create();
$product->reserveStock(
$product->claimStock(
quantity: 20,
until: now()->subHour()
);
// Expired reservations should be counted in available stock
$available = $product->reservations()->get();
// Expired claims should be counted in available stock
$available = $product->claims()->get();
$this->assertEquals(0, $available->count());
}
@ -202,10 +202,10 @@ class ProductStockTest extends TestCase
{
$product = Product::factory()->withStocks(50)->create();
$reservation = $product->reserveStock(10);
$claim = $product->claimStock(10);
$this->assertTrue($reservation->release());
$this->assertFalse($reservation->release());
$this->assertTrue($claim->release());
$this->assertFalse($claim->release());
}
/** @test */
@ -242,13 +242,13 @@ class ProductStockTest extends TestCase
}
/** @test */
public function reservation_without_stock_management_returns_null()
public function claim_without_stock_management_returns_null()
{
$product = Product::factory()->create(['manage_stock' => false]);
$reservation = $product->reserveStock(10);
$claim = $product->claimStock(10);
$this->assertNull($reservation);
$this->assertNull($claim);
}
/** @test */
@ -263,17 +263,17 @@ class ProductStockTest extends TestCase
}
/** @test */
public function reservations_method_filters_active_only()
public function claims_method_filters_active_only()
{
$product = Product::factory()->withStocks(100)->create();
$active = $product->reserveStock(10, until: now()->addDay());
$expired = $product->reserveStock(5, until: now()->subDay());
$active = $product->claimStock(10, until: now()->addDay());
$expired = $product->claimStock(5, until: now()->subDay());
$reservations = $product->reservations()->get();
$claims = $product->claims()->get();
$this->assertCount(1, $reservations);
$this->assertEquals($active->id, $reservations->first()->id);
$this->assertCount(1, $claims);
$this->assertEquals($active->id, $claims->first()->id);
}
/** @test */
@ -312,4 +312,173 @@ class ProductStockTest extends TestCase
$this->assertEquals(25, $product->getAvailableStock());
}
/** @test */
public function it_can_claim_stock_with_claimed_from_date()
{
$product = Product::factory()->withStocks(100)->create();
$claimedFrom = now()->addDays(5);
$until = now()->addDays(10);
$claim = $product->claimStock(
quantity: 20,
from: $claimedFrom,
until: $until
);
$this->assertNotNull($claim);
$this->assertEquals($claimedFrom->format('Y-m-d H:i:s'), $claim->claimed_from->format('Y-m-d H:i:s'));
$this->assertEquals($until->format('Y-m-d H:i:s'), $claim->expires_at->format('Y-m-d H:i:s'));
}
/** @test */
public function it_can_check_available_stock_on_a_date()
{
$product = Product::factory()->withStocks(100)->create();
// Claim stock from day 5 to day 10
$product->claimStock(
quantity: 30,
from: now()->addDays(5),
until: now()->addDays(10)
);
// Should have full stock available before claimed_from date
$availableOnDay3 = $product->availableOnDate(now()->addDays(3));
$this->assertEquals(100, $availableOnDay3);
// Should have reduced stock during claimed period
$availableOnDay7 = $product->availableOnDate(now()->addDays(7));
$this->assertEquals(70, $availableOnDay7);
// Should have full stock available after expires_at date
$availableOnDay12 = $product->availableOnDate(now()->addDays(12));
$this->assertEquals(100, $availableOnDay12);
}
/** @test */
public function it_can_handle_multiple_overlapping_claims_on_date()
{
$product = Product::factory()->withStocks(100)->create();
// First claim: days 5-10
$product->claimStock(
quantity: 20,
from: now()->addDays(5),
until: now()->addDays(10)
);
// Second claim: days 8-15
$product->claimStock(
quantity: 30,
from: now()->addDays(8),
until: now()->addDays(15)
);
// Day 6: only first claim is active
$availableOnDay6 = $product->availableOnDate(now()->addDays(6));
$this->assertEquals(80, $availableOnDay6);
// Day 9: both claims are active
$availableOnDay9 = $product->availableOnDate(now()->addDays(9));
$this->assertEquals(50, $availableOnDay9);
// Day 13: only second claim is active
$availableOnDay13 = $product->availableOnDate(now()->addDays(13));
$this->assertEquals(70, $availableOnDay13);
// Day 20: no claims active
$availableOnDay20 = $product->availableOnDate(now()->addDays(20));
$this->assertEquals(100, $availableOnDay20);
}
/** @test */
public function it_handles_claims_without_claimed_from_as_immediately_claimed()
{
$product = Product::factory()->withStocks(100)->create();
// Claim without claimed_from (immediately claimed)
$product->claimStock(
quantity: 25,
until: now()->addDays(10)
);
// Should be claimed immediately
$availableNow = $product->availableOnDate(now());
$this->assertEquals(75, $availableNow);
// Should still be claimed on day 7
$availableOnDay7 = $product->availableOnDate(now()->addDays(7));
$this->assertEquals(75, $availableOnDay7);
// Should be released after expiry
$availableOnDay12 = $product->availableOnDate(now()->addDays(12));
$this->assertEquals(100, $availableOnDay12);
}
/** @test */
public function it_handles_permanent_claims_without_expires_at()
{
$product = Product::factory()->withStocks(100)->create();
// Permanent claim from day 5 onwards
$product->claimStock(
quantity: 40,
from: now()->addDays(5)
);
// Before claimed_from: full stock available
$availableOnDay3 = $product->availableOnDate(now()->addDays(3));
$this->assertEquals(100, $availableOnDay3);
// After claimed_from: reduced stock
$availableOnDay10 = $product->availableOnDate(now()->addDays(10));
$this->assertEquals(60, $availableOnDay10);
// Far future: still reduced (permanent claim)
$availableOnDay100 = $product->availableOnDate(now()->addDays(100));
$this->assertEquals(60, $availableOnDay100);
}
/** @test */
public function available_on_date_scope_filters_correctly()
{
$product = Product::factory()->withStocks(100)->create();
// Create various claims
$claim1 = $product->claimStock(
quantity: 10,
from: now()->addDays(5),
until: now()->addDays(10)
);
$claim2 = $product->claimStock(
quantity: 15,
from: now()->addDays(8),
until: now()->addDays(15)
);
$claim3 = $product->claimStock(
quantity: 20,
from: now()->addDays(20),
until: now()->addDays(25)
);
// Test scope on day 7 - should only include claim1
$claimsOnDay7 = \Blax\Shop\Models\ProductStock::availableOnDate(now()->addDays(7))
->where('product_id', $product->id)
->get();
$this->assertCount(1, $claimsOnDay7);
$this->assertEquals($claim1->id, $claimsOnDay7->first()->id);
// Test scope on day 12 - should only include claim2
$claimsOnDay12 = \Blax\Shop\Models\ProductStock::availableOnDate(now()->addDays(12))
->where('product_id', $product->id)
->get();
$this->assertCount(1, $claimsOnDay12);
$this->assertEquals($claim2->id, $claimsOnDay12->first()->id);
}
}

View File

@ -14,55 +14,55 @@ class StockManagementTest extends TestCase
use RefreshDatabase;
/** @test */
public function it_can_reserve_stock_for_a_product()
public function it_can_claim_stock_for_a_product()
{
$product = Product::factory()
->withStocks(100)
->create();
$reservation = $product->reserveStock(
$claim = $product->claimStock(
quantity: 10,
until: now()->addHours(2)
);
$this->assertNotNull($reservation);
$this->assertEquals(10, $reservation->quantity);
$this->assertNotNull($claim);
$this->assertEquals(10, $claim->quantity);
$this->assertEquals(90, $product->getAvailableStock());
}
/** @test */
public function it_cannot_reserve_more_stock_than_available()
public function it_cannot_claim_more_stock_than_available()
{
$product = Product::factory()
->withStocks(5)
->create();
$reservation = null;
$claim = null;
$this->assertThrows(fn() => $reservation = $product->reserveStock(15), NotEnoughStockException::class);
$this->assertThrows(fn() => $claim = $product->claimStock(15), NotEnoughStockException::class);
$this->assertNull($reservation);
$this->assertNull($claim);
$this->assertEquals(5, $product->getAvailableStock());
}
/** @test */
public function it_can_release_reserved_stock()
public function it_can_release_claimed_stock()
{
$product = Product::factory()
->withStocks(100)
->create();
$reservation = $product->reserveStock(
$claim = $product->claimStock(
quantity: 10,
until: now()->addHours(2)
);
$this->assertEquals(90, $product->getAvailableStock());
$reservation->release();
$claim->release();
$this->assertEquals(100, $product->refresh()->getAvailableStock());
$this->assertNotNull($reservation->fresh()->released_at);
$this->assertNotNull($claim->fresh()->released_at);
}
/** @test */
@ -70,9 +70,9 @@ class StockManagementTest extends TestCase
{
$product = Product::factory()->withStocks(10)->create();
$reservation = $product->reserveStock(5);
$claim = $product->claimStock(5);
$pending = ProductStock::pending()->where('id', $reservation->id)->first();
$pending = ProductStock::pending()->where('id', $claim->id)->first();
$this->assertNotNull($pending);
$this->assertNull($pending->released_at);
@ -83,35 +83,35 @@ class StockManagementTest extends TestCase
{
$product = Product::factory()->withStocks(50)->create();
$reservation = $product->reserveStock(5);
$claim = $product->claimStock(5);
$reservation->release();
$claim->release();
$released = ProductStock::released()->where('id', $reservation->id)->first();
$released = ProductStock::released()->where('id', $claim->id)->first();
$this->assertNotNull($released);
$this->assertNotNull($released->released_at);
}
/** @test */
public function it_can_distinguish_temporary_and_permanent_reservations()
public function it_can_distinguish_temporary_and_permanent_claims()
{
$product = Product::factory()->withStocks(100)->create();
$permanentReservation = $product->reserveStock(
$permanentClaim = $product->claimStock(
quantity: 10
);
$temporaryReservation = $product->reserveStock(
$temporaryClaim = $product->claimStock(
quantity: 5,
until: now()->addHours(1)
);
$this->assertTrue($permanentReservation->isPermanent());
$this->assertFalse($permanentReservation->isTemporary());
$this->assertTrue($permanentClaim->isPermanent());
$this->assertFalse($permanentClaim->isTemporary());
$this->assertTrue($temporaryReservation->isTemporary());
$this->assertFalse($temporaryReservation->isPermanent());
$this->assertTrue($temporaryClaim->isTemporary());
$this->assertFalse($temporaryClaim->isPermanent());
}
/** @test */
@ -119,10 +119,10 @@ class StockManagementTest extends TestCase
{
$product = Product::factory()->withStocks(20)->create();
$reservation = $product->reserveStock(5);
$claim = $product->claimStock(5);
$this->assertInstanceOf(Product::class, $reservation->product);
$this->assertEquals($product->id, $reservation->product->id);
$this->assertInstanceOf(Product::class, $claim->product);
$this->assertEquals($product->id, $claim->product->id);
}
/** @test */
@ -140,24 +140,24 @@ class StockManagementTest extends TestCase
}
/** @test */
public function it_can_get_active_stock_reservations()
public function it_can_get_active_stock_claims()
{
$product = Product::factory()->withStocks(100)->create();
$activeReservation = $product->reserveStock(
$activeClaim = $product->claimStock(
quantity: 10,
until: now()->addHours(2)
);
$expiredReservation = $product->reserveStock(
$expiredClaim = $product->claimStock(
quantity: 5,
until: now()->subHours(1)
);
$activeReservations = $product->reservations()->get();
$activeClaims = $product->claims()->get();
$this->assertCount(1, $activeReservations);
$this->assertEquals($activeReservation->id, $activeReservations->first()->id);
$this->assertCount(1, $activeClaims);
$this->assertEquals($activeClaim->id, $activeClaims->first()->id);
}
/** @test */
@ -165,25 +165,25 @@ class StockManagementTest extends TestCase
{
$product = Product::factory()->withStocks()->create();
$reservation = $product->reserveStock(5);
$claim = $product->claimStock(5);
$this->assertTrue($reservation->release());
$this->assertFalse($reservation->release());
$this->assertTrue($claim->release());
$this->assertFalse($claim->release());
}
/** @test */
public function it_can_store_reservation_note()
public function it_can_store_claim_note()
{
$product = Product::factory()->withStocks()->create();
$note = "Customer requested to hold this item for 2 days.";
$reservation = $product->reserveStock(
$claim = $product->claimStock(
quantity: 5,
note: $note
);
$this->assertEquals($note, $reservation->note);
$this->assertEquals($note, $claim->note);
}
/** @test */
@ -191,18 +191,18 @@ class StockManagementTest extends TestCase
{
$product = Product::factory()->withStocks(100)->create();
$reservation1 = $product->reserveStock(
$claim1 = $product->claimStock(
quantity: 10,
until: now()->addHours(2)
);
$reservation2 = $product->reserveStock(
$claim2 = $product->claimStock(
quantity: 5,
until: now()->addHours(1)
);
$reservation1->refresh();
$reservation2->refresh();
$claim1->refresh();
$claim2->refresh();
$this->assertEquals(85, $product->refresh()->getAvailableStock());
}