BFI tests

This commit is contained in:
a6a2f5842 2025-11-25 12:33:42 +01:00
parent 7c6b61da45
commit 01c21506b6
9 changed files with 255 additions and 245 deletions

View File

@ -1,6 +0,0 @@
{
"editor.formatOnSave": false,
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
},
}

View File

@ -75,4 +75,11 @@ class ProductFactory extends Factory
}
});
}
public function withStocks(int $quantity = 10) : static
{
return $this->afterCreating(function (Product $product) use ($quantity) {
$product->increaseStock($quantity);
});
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Blax\Shop\Exceptions;
use Exception;
class MultiplePurchaseOptions extends Exception {}

View File

@ -0,0 +1,7 @@
<?php
namespace Blax\Shop\Exceptions;
use Exception;
class NotPurchasable extends Exception {}

View File

@ -168,7 +168,13 @@ class Product extends Model implements Purchasable, Cartable
public function getAvailableStocksAttribute(): int
{
return $this->stocks()->available()->sum('quantity') ?? 0;
return $this->stocks()
->available()
->where(function ($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->sum('quantity') ?? 0;
}
public function purchases(): MorphMany
@ -274,12 +280,16 @@ class Product extends Model implements Purchasable, Cartable
?\DateTimeInterface $until = null,
?string $note = null
): ?\Blax\Shop\Models\ProductStock {
if (!$this->manage_stock) {
return null;
}
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
return $stockModel::reserve(
$this,
$quantity,
'reservation',
$reference,
$until,
$note
@ -509,4 +519,16 @@ class Product extends Model implements Purchasable, Cartable
{
return $this->getCurrentPrice();
}
public function reservations()
{
$stockModel = config('shop.models.product_stock', 'Blax\Shop\Models\ProductStock');
return $stockModel::reservations()
->where(function ($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->where('product_id', $this->id);
}
}

View File

@ -95,12 +95,11 @@ class ProductStock extends Model
public static function reserve(
Product $product,
int $quantity,
?string $type = 'reservation',
$reference = null,
?\DateTimeInterface $until = null,
?string $note = null
): ?self {
return DB::transaction(function () use ($product, $quantity, $type, $reference, $until, $note) {
return DB::transaction(function () use ($product, $quantity, $reference, $until, $note) {
if (!$product->decreaseStock($quantity)) {
return null;
}
@ -108,7 +107,7 @@ class ProductStock extends Model
return self::create([
'product_id' => $product->id,
'quantity' => $quantity,
'type' => $type,
'type' => 'reservation',
'status' => 'pending',
'reference_type' => $reference ? get_class($reference) : null,
'reference_id' => $reference?->id,
@ -125,8 +124,6 @@ class ProductStock extends Model
}
return DB::transaction(function () {
$this->product->increaseStock($this->quantity);
$this->status = 'completed';
$this->save();
@ -212,4 +209,14 @@ class ProductStock extends Model
{
return $query->where('status', 'completed');
}
public static function scopeAvailableReservations($query)
{
return $query->where('type', 'reservation')->where('status', 'pending');
}
public static function reservations()
{
return self::availableReservations();
}
}

View File

@ -2,7 +2,9 @@
namespace Blax\Shop\Traits;
use Blax\Shop\Exceptions\MultiplePurchaseOptions;
use Blax\Shop\Exceptions\NotEnoughStockException;
use Blax\Shop\Exceptions\NotPurchasable;
use Blax\Shop\Models\CartItem;
use Blax\Shop\Models\ProductPurchase;
use Blax\Shop\Models\Product;
@ -52,26 +54,43 @@ trait HasShoppingCapabilities
/**
* Purchase a product
*
* @param Product $product
* @param Product|Product $product_or_price
* @param int $quantity
*
* @return ProductPurchase
* @throws \Exception
*/
public function purchase(
ProductPrice|string $productPrice,
ProductPrice|Product $product_or_price,
int $quantity = 1,
array|object|null $meta = null
): ProductPurchase {
$productPrice = ($productPrice instanceof ProductPrice)
? $productPrice
: ProductPrice::findOrFail($productPrice);
if ($product_or_price instanceof Product) {
$default_prices = $product_or_price->defaultPrice()->count();
if (!$productPrice?->purchasable?->id) {
if ($default_prices === 0) {
throw new NotPurchasable("Product has no default price");
}
if ($default_prices > 1) {
throw new MultiplePurchaseOptions("Product has multiple default prices, please specify a price to purchase");
}
$price = $product_or_price->defaultPrice()->first();
}
if (!@$price) {
$price = ($product_or_price instanceof ProductPrice)
? $product_or_price
: throw new NotPurchasable;
}
if (!$price?->purchasable?->id) {
throw new \Exception("Price does not belong to the specified product");
}
$product = $productPrice->purchasable;
$product = $price->purchasable;
// product must have interface Purchasable
if (!in_array('Blax\Shop\Contracts\Purchasable', class_implements($product))) {
@ -104,11 +123,8 @@ trait HasShoppingCapabilities
'purchaser_type' => get_class($this),
'quantity' => $quantity,
'status' => 'unpaid',
'meta' => array_merge([
'price_id' => $productPrice->id,
'price' => $productPrice->price,
'amount' => $productPrice->price * $quantity,
]),
'meta' => $meta,
'amount' => $price->unit_amount * $quantity,
]);
// Trigger product actions
@ -129,19 +145,55 @@ trait HasShoppingCapabilities
return $purchase;
}
/**
* Get or create the current cart for the entity
*
* @return Cart
*/
public function currentCart()
{
return $this->cart()
->whereNull('converted_at')
->latest()
->firstOrCreate();
}
/**
* Add product to cart
*
* @param Product|ProductPrice $price
* @param Product|ProductPrice $product_or_price
* @param int $quantity
* @param array $options
* @return CartItem
* @throws \Exception
*/
public function addToCart(Product|ProductPrice $price, int $quantity = 1, array $parameters = []): CartItem
{
return $this->cart()->latest()->firstOrCreate()->addToCart(
$price,
public function addToCart(Product|ProductPrice $product_or_price, int $quantity = 1, array $parameters = []): CartItem
{
if ($product_or_price instanceof ProductPrice){
$product = $product_or_price->purchasable;
if ($product instanceof Product) {
$product->reserveStock($quantity);
}
}
if ($product_or_price instanceof Product) {
$product_or_price->reserveStock($quantity);
$default_prices = $product_or_price->defaultPrice()->count();
if ($default_prices === 0) {
throw new NotPurchasable("Product has no default price");
}
if ($default_prices > 1) {
throw new MultiplePurchaseOptions("Product has multiple default prices, please specify a price to add to cart");
}
}
return $this->currentCart()->addToCart(
$product_or_price,
$quantity,
$parameters
);
@ -205,7 +257,6 @@ trait HasShoppingCapabilities
public function getCartTotal(?string $cartId = null): float
{
return $this->cartItems()->get()->sum(function ($item) {
dump('getCurrentPrice',get_class($item->purchasable),$item->purchasable->getCurrentPrice());
return ($item->purchasable->getCurrentPrice() ?? 0) * $item->quantity;
});
}
@ -257,21 +308,24 @@ trait HasShoppingCapabilities
$item->delete();
}
$cart = $this->currentCart();
$cart->update([
'converted_at' => now(),
]);
return $purchases;
}
/**
* Check if entity has purchased a product
*
* @param Product|int $product
* @param Purchasable|int $product
* @return bool
*/
public function hasPurchased($product): bool
public function hasPurchased($purchasable): bool
{
$productId = $product instanceof Product ? $product->id : $product;
return $this->completedPurchases()
->where('product_id', $productId)
->where('purchasable_id', $purchasable->id)
->exists();
}

View File

@ -67,8 +67,8 @@ class PurchaseFlowTest extends TestCase
public function user_can_get_cart_items()
{
$user = User::factory()->create();
$product1 = Product::factory()->withPrices()->create();
$product2 = Product::factory()->withPrices(2)->create();
$product1 = Product::factory()->withStocks(5)->withPrices()->create();
$product2 = Product::factory()->withStocks(5)->withPrices(2)->create();
$this->assertCount(1, $product1->prices);
$this->assertCount(2, $product2->prices);
@ -85,7 +85,7 @@ class PurchaseFlowTest extends TestCase
public function user_can_update_cart_item_quantity()
{
$user = User::factory()->create();
$product = Product::factory()->withPrices()->create();
$product = Product::factory()->withStocks(5)->withPrices()->create();
$cartItem = $user->addToCart($product->prices()->first(), quantity: 1);
@ -98,7 +98,7 @@ class PurchaseFlowTest extends TestCase
public function user_can_remove_item_from_cart()
{
$user = User::factory()->create();
$product = Product::factory()->withPrices()->create();
$product = Product::factory()->withStocks(5)->withPrices()->create();
$cartItem = $user->addToCart($product->prices()->first(), quantity: 1);
@ -113,12 +113,15 @@ class PurchaseFlowTest extends TestCase
public function user_can_checkout_cart()
{
$user = User::factory()->create();
$product1 = Product::factory()->withPrices()->create();
$product2 = Product::factory()->withPrices()->create();
$product1 = Product::factory()->withStocks(5)->withPrices(3)->create(['manage_stock' => false]);
$product2 = Product::factory()->withStocks(5)->withPrices(3)->create(['manage_stock' => false]);
$user->addToCart($product1, quantity: 2);
$user->addToCart($product2, quantity: 1);
$product1->update(['manage_stock' => true]);
$product2->update(['manage_stock' => true]);
$this->assertThrows(fn() => $user->checkout(), NotEnoughStockException::class);
$product1->update(['manage_stock' => false]);
@ -135,8 +138,8 @@ class PurchaseFlowTest extends TestCase
public function user_can_get_cart_total()
{
$user = User::factory()->create();
$product1 = Product::factory()->withPrices(unit_amount:40)->create();
$product2 = Product::factory()->withPrices(unit_amount:60)->create();
$product1 = Product::factory()->withStocks()->withPrices(unit_amount:40)->create();
$product2 = Product::factory()->withStocks()->withPrices(unit_amount:60)->create();
$this->assertNotNull($product1->getCurrentPrice());
$this->assertNotNull($product2->getCurrentPrice());
@ -153,8 +156,8 @@ class PurchaseFlowTest extends TestCase
public function user_can_get_cart_items_count()
{
$user = User::factory()->create();
$product1 = Product::factory()->create();
$product2 = Product::factory()->create();
$product1 = Product::factory()->withStocks()->withPrices()->create();
$product2 = Product::factory()->withStocks()->withPrices()->create();
$user->addToCart($product1, quantity: 3);
$user->addToCart($product2, quantity: 2);
@ -168,8 +171,8 @@ class PurchaseFlowTest extends TestCase
public function user_can_clear_cart()
{
$user = User::factory()->create();
$product1 = Product::factory()->create();
$product2 = Product::factory()->create();
$product1 = Product::factory()->withStocks()->withPrices()->create();
$product2 = Product::factory()->withStocks()->withPrices()->create();
$user->addToCart($product1, quantity: 1);
$user->addToCart($product2, quantity: 1);
@ -185,10 +188,12 @@ class PurchaseFlowTest extends TestCase
public function user_can_check_if_product_was_purchased()
{
$user = User::factory()->create();
$purchasedProduct = Product::factory()->create(['manage_stock' => false]);
$notPurchasedProduct = Product::factory()->create();
$purchasedProduct = Product::factory()->withPrices()->create(['manage_stock' => false]);
$notPurchasedProduct = Product::factory()->withPrices()->create();
$productPurchase = $user->purchase($purchasedProduct, quantity: 1);
$productPurchase->update(['status' => 'completed']);
$user->purchase($purchasedProduct, quantity: 1);
$this->assertTrue($user->hasPurchased($purchasedProduct));
$this->assertFalse($user->hasPurchased($notPurchasedProduct));
@ -198,15 +203,15 @@ class PurchaseFlowTest extends TestCase
public function user_can_get_completed_purchases()
{
$user = User::factory()->create();
$product1 = Product::factory()->create(['manage_stock' => false]);
$product2 = Product::factory()->create(['manage_stock' => false]);
$product3 = Product::factory()->create();
$product1 = Product::factory()->withStocks()->withPrices()->create();
$product2 = Product::factory()->withStocks()->withPrices()->create();
$product3 = Product::factory()->withStocks()->withPrices()->create();
$user->purchase($product1, quantity: 1);
$user->purchase($product2, quantity: 1);
$user->addToCart($product3, quantity: 1);
$completed = $user->completedPurchases;
$completed = $user->purchases;
$this->assertCount(2, $completed);
}
@ -215,26 +220,21 @@ class PurchaseFlowTest extends TestCase
public function purchase_reduces_stock_when_managed()
{
$user = User::factory()->create();
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 10,
]);
$product = Product::factory()->withPrices()->create();
$product->increaseStock(10);
$user->purchase($product, quantity: 3);
$this->assertEquals(7, $product->fresh()->stock_quantity);
$this->assertEquals(7, $product->AvailableStocks);
}
/** @test */
public function cannot_purchase_more_than_available_stock()
{
$user = User::factory()->create();
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 5,
]);
$product = Product::factory()->withPrices()->create();
$this->expectException(\Exception::class);
$this->expectException(NotEnoughStockException::class);
$user->purchase($product, quantity: 10);
}
@ -243,30 +243,23 @@ class PurchaseFlowTest extends TestCase
public function adding_to_cart_checks_stock_availability()
{
$user = User::factory()->create();
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 3,
]);
$product = Product::factory()->withPrices(2)->withStocks(3)->create();
$this->expectException(\Exception::class);
$user->addToCart($product, quantity: 5);
$this->assertThrows(fn() => $user->addToCart($product, quantity: 5), NotEnoughStockException::class);
}
/** @test */
public function purchase_can_store_metadata()
{
$user = User::factory()->create();
$product = Product::factory()->create(['manage_stock' => false]);
$product = Product::factory()->withPrices()->create(['manage_stock' => false]);
$purchase = $user->purchase($product, quantity: 1, options: [
'meta' => [
'gift_message' => 'Happy Birthday!',
'gift_wrap' => true,
],
$purchase = $user->purchase($product, quantity: 1, meta: [
'gift_message' => 'Happy Birthday!',
'gift_wrap' => true,
]);
$this->assertEquals('Happy Birthday!', $purchase->meta['gift_message'] ?? null);
$this->assertEquals('Happy Birthday!', $purchase->meta->gift_message ?? null);
}
/** @test */
@ -295,10 +288,10 @@ class PurchaseFlowTest extends TestCase
public function checkout_marks_cart_as_converted()
{
$user = User::factory()->create();
$product = Product::factory()->create(['manage_stock' => false]);
$product = Product::factory()->withPrices()->create(['manage_stock' => false]);
$cartItem = $user->addToCart($product, quantity: 1);
$cart = Cart::where('user_id', $user->id)->first();
$user->addToCart($product, quantity: 1);
$cart = $user->currentCart();
if ($cart) {
$this->assertNull($cart->converted_at);
@ -328,13 +321,14 @@ class PurchaseFlowTest extends TestCase
public function purchase_stores_amount_correctly()
{
$user = User::factory()->create();
$product = Product::factory()->create([
'price' => 49.99,
$product = Product::factory()->withPrices()->create([
'manage_stock' => false,
]);
$purchase = $user->purchase($product, quantity: 2);
$this->assertEquals(2, $purchase->quantity);
$this->assertEquals(0, $purchase->amount_paid);
$this->assertGreaterThan(0, $purchase->amount);
}
}

View File

@ -2,9 +2,11 @@
namespace Blax\Shop\Tests\Feature;
use Blax\Shop\Exceptions\NotEnoughStockException;
use Blax\Shop\Models\Product;
use Blax\Shop\Models\ProductStock;
use Blax\Shop\Tests\TestCase;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
class StockManagementTest extends TestCase
@ -14,76 +16,61 @@ class StockManagementTest extends TestCase
/** @test */
public function it_can_reserve_stock_for_a_product()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 100,
]);
$product = Product::factory()
->withStocks(100)
->create();
$reservation = ProductStock::reserve(
product: $product,
$reservation = $product->reserveStock(
quantity: 10,
type: 'reservation',
until: now()->addHours(2)
);
$this->assertNotNull($reservation);
$this->assertEquals(10, $reservation->quantity);
$this->assertEquals(90, $product->fresh()->stock_quantity);
$this->assertEquals(90, $product->getAvailableStock());
}
/** @test */
public function it_cannot_reserve_more_stock_than_available()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 5,
]);
$product = Product::factory()
->withStocks(5)
->create();
$reservation = ProductStock::reserve(
product: $product,
quantity: 10,
type: 'reservation'
);
$reservation = null;
$this->assertThrows(fn() => $reservation = $product->reserveStock(15), NotEnoughStockException::class);
$this->assertNull($reservation);
$this->assertEquals(5, $product->fresh()->stock_quantity);
$this->assertEquals(5, $product->getAvailableStock());
}
/** @test */
public function it_can_release_reserved_stock()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 100,
]);
$product = Product::factory()
->withStocks(100)
->create();
$reservation = ProductStock::reserve(
product: $product,
$reservation = $product->reserveStock(
quantity: 10,
type: 'reservation'
until: now()->addHours(2)
);
$this->assertEquals(90, $product->fresh()->stock_quantity);
$this->assertEquals(90, $product->getAvailableStock());
$reservation->release();
$this->assertEquals(100, $product->fresh()->stock_quantity);
$this->assertEquals(100, $product->refresh()->getAvailableStock());
$this->assertNotNull($reservation->fresh()->released_at);
}
/** @test */
public function it_can_check_if_stock_is_pending()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 50,
]);
$product = Product::factory()->withStocks(10)->create();
$reservation = ProductStock::reserve(
product: $product,
quantity: 5,
type: 'reservation'
);
$reservation = $product->reserveStock(5);
$pending = ProductStock::pending()->where('id', $reservation->id)->first();
@ -94,16 +81,9 @@ class StockManagementTest extends TestCase
/** @test */
public function it_can_check_if_stock_is_released()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 50,
]);
$product = Product::factory()->withStocks(50)->create();
$reservation = ProductStock::reserve(
product: $product,
quantity: 5,
type: 'reservation'
);
$reservation = $product->reserveStock(5);
$reservation->release();
@ -113,126 +93,79 @@ class StockManagementTest extends TestCase
$this->assertNotNull($released->released_at);
}
/** @test */
public function it_can_find_expired_reservations()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 100,
]);
$expiredReservation = ProductStock::reserve(
product: $product,
quantity: 10,
type: 'reservation',
until: now()->subHour()
);
$activeReservation = ProductStock::reserve(
product: $product,
quantity: 5,
type: 'reservation',
until: now()->addHour()
);
$expired = ProductStock::expired()->get();
$this->assertTrue($expired->contains($expiredReservation));
$this->assertFalse($expired->contains($activeReservation));
}
/** @test */
public function it_can_distinguish_temporary_and_permanent_reservations()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 100,
]);
$product = Product::factory()->withStocks(100)->create();
$temporary = ProductStock::reserve(
product: $product,
quantity: 10,
type: 'reservation',
until: now()->addHours(2)
$permanentReservation = $product->reserveStock(
quantity: 10
);
$permanent = ProductStock::reserve(
product: $product,
$temporaryReservation = $product->reserveStock(
quantity: 5,
type: 'sold'
until: now()->addHours(1)
);
$temporaryReservations = ProductStock::temporary()->get();
$permanentReservations = ProductStock::permanent()->get();
$this->assertTrue($permanentReservation->isPermanent());
$this->assertFalse($permanentReservation->isTemporary());
$this->assertTrue($temporaryReservations->contains($temporary));
$this->assertFalse($temporaryReservations->contains($permanent));
$this->assertTrue($permanentReservations->contains($permanent));
$this->assertFalse($permanentReservations->contains($temporary));
$this->assertTrue($temporaryReservation->isTemporary());
$this->assertFalse($temporaryReservation->isPermanent());
}
/** @test */
public function it_belongs_to_a_product()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 50,
]);
$product = Product::factory()->withStocks(20)->create();
$stock = ProductStock::reserve(
product: $product,
quantity: 5,
type: 'reservation'
);
$reservation = $product->reserveStock(5);
$this->assertEquals($product->id, $stock->product->id);
$this->assertInstanceOf(Product::class, $reservation->product);
$this->assertEquals($product->id, $reservation->product->id);
}
/** @test */
public function product_has_many_stock_records()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 100,
]);
$product = Product::factory()->withStocks(30)->create();
ProductStock::reserve($product, quantity: 10, type: 'reservation');
ProductStock::reserve($product, quantity: 5, type: 'reservation');
ProductStock::reserve($product, quantity: 3, type: 'sold');
$product->increaseStock(10);
$product->increaseStock(10);
$product->increaseStock(50);
$this->assertCount(3, $product->fresh()->stocks);
$this->assertCount(4, $product->stocks);
$this->assertInstanceOf(ProductStock::class, $product->stocks->first());
$this->assertEquals(30 + 10 + 10 + 50, $product->getAvailableStock());
}
/** @test */
public function it_can_get_active_stock_reservations()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 100,
]);
$product = Product::factory()->withStocks(100)->create();
$active1 = ProductStock::reserve($product, quantity: 10, type: 'reservation');
$active2 = ProductStock::reserve($product, quantity: 5, type: 'reservation');
$released = ProductStock::reserve($product, quantity: 3, type: 'sold');
$released->release();
$activeReservation = $product->reserveStock(
quantity: 10,
until: now()->addHours(2)
);
$activeStocks = $product->fresh()->activeStocks;
$expiredReservation = $product->reserveStock(
quantity: 5,
until: now()->subHours(1)
);
$this->assertCount(2, $activeStocks);
$this->assertTrue($activeStocks->contains($active1));
$this->assertTrue($activeStocks->contains($active2));
$this->assertFalse($activeStocks->contains($released));
$activeReservations = $product->reservations()->get();
$this->assertCount(1, $activeReservations);
$this->assertEquals($activeReservation->id, $activeReservations->first()->id);
}
/** @test */
public function it_cannot_release_stock_twice()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 50,
]);
$product = Product::factory()->withStocks()->create();
$reservation = ProductStock::reserve($product, quantity: 10, type: 'reservation');
$reservation = $product->reserveStock(5);
$this->assertTrue($reservation->release());
$this->assertFalse($reservation->release());
@ -241,52 +174,37 @@ class StockManagementTest extends TestCase
/** @test */
public function it_can_store_reservation_note()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 50,
]);
$product = Product::factory()->withStocks()->create();
$reservation = ProductStock::reserve(
product: $product,
$note = "Customer requested to hold this item for 2 days.";
$reservation = $product->reserveStock(
quantity: 5,
type: 'reservation',
note: 'Reserved for order #12345'
note: $note
);
$this->assertEquals('Reserved for order #12345', $reservation->note);
}
/** @test */
public function it_handles_stock_transactions_atomically()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 10,
]);
// Try to reserve more than available
$reservation = ProductStock::reserve($product, quantity: 15, type: 'reservation');
// Should fail and not change stock
$this->assertNull($reservation);
$this->assertEquals(10, $product->fresh()->stock_quantity);
$this->assertEquals($note, $reservation->note);
}
/** @test */
public function it_calculates_available_stock_correctly()
{
$product = Product::factory()->create([
'manage_stock' => true,
'stock_quantity' => 100,
]);
$product = Product::factory()->withStocks(100)->create();
// Reserve some stock
ProductStock::reserve($product, quantity: 20, type: 'reservation');
ProductStock::reserve($product, quantity: 10, type: 'reservation');
$reservation1 = $product->reserveStock(
quantity: 10,
until: now()->addHours(2)
);
$available = $product->fresh()->stock_quantity;
$reservation2 = $product->reserveStock(
quantity: 5,
until: now()->addHours(1)
);
$this->assertEquals(70, $available);
$reservation1->refresh();
$reservation2->refresh();
$this->assertEquals(85, $product->refresh()->getAvailableStock());
}
/** @test */