2025-12-03 12:21:23 +00:00
< ? php
namespace Blax\Shop\Traits ;
2025-12-03 12:59:01 +00:00
use Blax\Shop\Enums\StockStatus ;
use Blax\Shop\Enums\StockType ;
2025-12-03 12:21:23 +00:00
use Blax\Shop\Exceptions\NotEnoughStockException ;
use Carbon\Carbon ;
use Illuminate\Database\Eloquent\Relations\HasMany ;
use Illuminate\Support\Facades\DB ;
2025-12-04 10:16:38 +00:00
/**
* HasStocks Trait
*
* Provides stock management functionality to Product models .
*
* Key Features :
* - Basic stock operations ( increase , decrease , adjust )
* - Stock claims for bookings / reservations
* - Date - based availability checking
* - Low stock detection
* - Stock movement logging
*
* Usage :
* - Add 'manage_stock' boolean column to products table
* - Set manage_stock = true to enable stock tracking
* - Use increaseStock / decreaseStock for inventory changes
* - Use claimStock for reservations / bookings
* - Use availableOnDate for date - based availability
*
* Stock Calculation :
* - Physical Stock = Sum of all COMPLETED entries
* - Available Stock = Physical Stock ( accounts for pending claims via their DECREASE entries )
* - Claimed Stock = Sum of PENDING claims
* - Available on Date = Available Stock + All Claims - Claims Active on Date
*/
2025-12-03 12:21:23 +00:00
trait HasStocks
{
2025-12-04 10:16:38 +00:00
/**
* Get all stock entries for this product
*/
2025-12-03 12:21:23 +00:00
public function stocks () : HasMany
{
return $this -> hasMany ( config ( 'shop.models.product_stock' , 'Blax\Shop\Models\ProductStock' ));
}
2025-12-04 10:16:38 +00:00
/**
* Attribute accessor : Get available physical stock
*
* Sums all COMPLETED stock entries that haven ' t expired .
* This includes INCREASE ( + ), DECREASE ( - ), and released claims .
* Does NOT include PENDING claims ( they ' re tracked separately ) .
*/
2025-12-03 12:21:23 +00:00
public function getAvailableStocksAttribute () : int
{
return $this -> stocks ()
-> available ()
2025-12-04 09:51:45 +00:00
-> willExpire ()
2025-12-03 12:21:23 +00:00
-> sum ( 'quantity' ) ? ? 0 ;
}
2025-12-04 10:16:38 +00:00
/**
* Check if product is in stock
*
* @ return bool True if stock management is disabled OR available stock > 0
*/
2025-12-03 12:21:23 +00:00
public function isInStock () : bool
{
if ( ! $this -> manage_stock ) {
return true ;
}
return $this -> getAvailableStock () > 0 ;
}
2025-12-04 10:16:38 +00:00
/**
* Decrease physical stock ( inventory reduction )
*
* Creates a DECREASE entry with negative quantity and COMPLETED status .
* This represents actual stock leaving the inventory ( sold , damaged , etc . ) .
*
* @ param int $quantity Amount to decrease
* @ param Carbon | null $until Optional expiration ( for temporary decreases )
* @ return bool True if successful
* @ throws NotEnoughStockException If insufficient stock available
*/
2025-12-03 12:21:23 +00:00
public function decreaseStock ( int $quantity = 1 , Carbon | null $until = null ) : bool
{
if ( ! $this -> manage_stock ) {
return true ;
}
if ( $this -> AvailableStocks < $quantity ) {
return throw new NotEnoughStockException ( " Not enough stock available for product ID { $this -> id } " );
}
$this -> stocks () -> create ([
'quantity' => - $quantity ,
2025-12-03 12:59:01 +00:00
'type' => StockType :: DECREASE ,
'status' => StockStatus :: COMPLETED ,
2025-12-03 12:21:23 +00:00
'expires_at' => $until ,
]);
$this -> logStockChange ( - $quantity , 'decrease' );
$this -> save ();
return true ;
}
2025-12-04 10:16:38 +00:00
/**
* Increase physical stock ( inventory addition )
*
* Creates an INCREASE entry with positive quantity and COMPLETED status .
* This represents stock being added to inventory ( purchased , returned , etc . ) .
*
* @ param int $quantity Amount to increase
* @ return bool True if successful , false if stock management disabled
*/
2025-12-03 12:21:23 +00:00
public function increaseStock ( int $quantity = 1 ) : bool
{
if ( ! $this -> manage_stock ) {
return false ;
}
$this -> stocks () -> create ([
'quantity' => $quantity ,
2025-12-03 12:59:01 +00:00
'type' => StockType :: INCREASE ,
'status' => StockStatus :: COMPLETED ,
2025-12-03 12:21:23 +00:00
]);
$this -> logStockChange ( $quantity , 'increase' );
$this -> save ();
return true ;
}
2025-12-04 10:16:38 +00:00
/**
* Adjust stock with custom type and status
*
* More flexible than increaseStock / decreaseStock , allows :
* - Custom stock type ( INCREASE , DECREASE , RETURN )
* - Custom status ( defaults to COMPLETED )
* - Optional expiration date
*
* @ param StockType $type The type of adjustment
* @ param int $quantity Amount to adjust ( always positive , type determines direction )
* @ param \DateTimeInterface | null $until Optional expiration date
* @ param StockStatus | null $status Optional status ( defaults to COMPLETED )
* @ return bool True if successful , false if stock management disabled
*/
2025-12-03 14:45:11 +00:00
public function adjustStock (
StockType $type ,
int $quantity ,
\DateTimeInterface | null $until = null ,
? StockStatus $status = null ,
) {
if ( ! $this -> manage_stock ) {
return false ;
}
2025-12-04 10:16:38 +00:00
// INCREASE and RETURN add stock (positive), DECREASE and CLAIMED remove stock (negative)
$isPositive = in_array ( $type , [ StockType :: INCREASE , StockType :: RETURN ]);
$adjustedQuantity = $isPositive ? $quantity : - $quantity ;
2025-12-03 14:45:11 +00:00
$this -> stocks () -> create ([
2025-12-04 10:16:38 +00:00
'quantity' => $adjustedQuantity ,
2025-12-03 14:45:11 +00:00
'type' => $type ,
'status' => $status ? ? StockStatus :: COMPLETED ,
'expires_at' => $until ,
]);
2025-12-04 10:16:38 +00:00
$this -> logStockChange ( $adjustedQuantity , 'adjust' );
2025-12-03 14:45:11 +00:00
$this -> save ();
return true ;
}
2025-12-04 10:16:38 +00:00
/**
* Claim stock for temporary use ( reservation / booking )
*
* This is different from decreaseStock - it :
* 1. Removes stock from available inventory ( via DECREASE entry )
* 2. Tracks it as a claim ( via CLAIMED entry with PENDING status )
* 3. Can be released back later ( changes CLAIMED to COMPLETED )
* 4. Supports date ranges for bookings ( claimed_from to expires_at )
*
* Use cases :
* - Hotel room bookings ( claimed_from = check - in , expires_at = check - out )
* - Equipment rentals ( claimed_from = rental start , expires_at = return date )
* - Cart reservations ( no claimed_from , expires_at = cart expiry )
*
* @ param int $quantity Amount to claim
* @ param mixed $reference Optional reference model ( Order , Booking , Cart , etc . )
* @ param \DateTimeInterface | null $from When claim starts ( null = immediately )
* @ param \DateTimeInterface | null $until When claim expires ( null = permanent )
* @ param string | null $note Optional note about the claim
* @ return \Blax\Shop\Models\ProductStock | null The claim entry , or null if insufficient stock
*/
2025-12-04 10:06:09 +00:00
public function claimStock (
2025-12-03 12:21:23 +00:00
int $quantity ,
$reference = null ,
2025-12-04 10:06:09 +00:00
? \DateTimeInterface $from = null ,
2025-12-03 12:21:23 +00:00
? \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' );
2025-12-04 10:06:09 +00:00
return $stockModel :: claim (
2025-12-03 12:21:23 +00:00
$this ,
$quantity ,
$reference ,
2025-12-04 10:06:09 +00:00
$from ,
2025-12-03 12:21:23 +00:00
$until ,
$note
);
}
2025-12-04 10:16:38 +00:00
/**
* Get currently available stock
*
* This is the stock available for new orders / claims .
* Calculated as : Sum of all COMPLETED entries ( includes DECREASE from active claims )
*
* @ return int Available quantity ( PHP_INT_MAX if stock management disabled )
*/
2025-12-03 12:21:23 +00:00
public function getAvailableStock () : int
{
if ( ! $this -> manage_stock ) {
return PHP_INT_MAX ;
}
return max ( 0 , $this -> AvailableStocks );
}
2025-12-04 10:16:38 +00:00
/**
* Get total currently claimed stock
*
* Sum of all active ( PENDING ) claims .
* This stock is unavailable but tracked separately from physical inventory .
*
* @ return int Total claimed quantity
*/
2025-12-04 10:06:09 +00:00
public function getClaimedStock () : int
2025-12-03 12:21:23 +00:00
{
2025-12-04 10:16:38 +00:00
return $this -> stocks ()
-> where ( 'type' , StockType :: CLAIMED -> value )
-> where ( 'status' , StockStatus :: PENDING -> value )
-> sum ( 'quantity' );
2025-12-03 12:21:23 +00:00
}
2025-12-04 10:16:38 +00:00
/**
* Log a stock change to the audit log
*
* @ param int $quantityChange The change in quantity ( positive or negative )
* @ param string $type The type of change ( increase , decrease , adjust )
*/
2025-12-03 12:21:23 +00:00
protected function logStockChange ( int $quantityChange , string $type ) : void
{
DB :: table ( 'product_stock_logs' ) -> insert ([
'product_id' => $this -> id ,
'quantity_change' => $quantityChange ,
'quantity_after' => $this -> stock_quantity ,
'type' => $type ,
'created_at' => now (),
'updated_at' => now (),
]);
}
2025-12-04 10:16:38 +00:00
/**
* Query scope : Get products that are in stock
*
* Includes products with :
* - Stock management disabled ( always in stock ), OR
* - Stock management enabled AND available stock > 0
*/
2025-12-03 12:21:23 +00:00
public function scopeInStock ( $query )
{
return $query -> where ( function ( $q ) {
$q -> where ( 'manage_stock' , false )
-> orWhere ( function ( $q2 ) {
$q2 -> where ( 'manage_stock' , true )
-> whereRaw ( " (SELECT SUM(quantity) FROM " . config ( 'shop.tables.product_stocks' , 'product_stocks' ) . " WHERE product_id = " . config ( 'shop.tables.products' , 'products' ) . " .id) > 0 " );
});
});
}
2025-12-04 10:16:38 +00:00
/**
* Query scope : Get products with low stock
*
* Returns products where :
* - Stock management is enabled
* - low_stock_threshold is set
* - Available stock <= threshold
*/
2025-12-03 12:21:23 +00:00
public function scopeLowStock ( $query )
{
$stockTable = config ( 'shop.tables.product_stocks' , 'product_stocks' );
$productTable = config ( 'shop.tables.products' , 'products' );
return $query -> where ( 'manage_stock' , true )
-> whereNotNull ( 'low_stock_threshold' )
-> whereRaw ( " (SELECT COALESCE(SUM(quantity), 0) FROM { $stockTable } WHERE { $stockTable } .product_id = { $productTable } .id AND { $stockTable } .status IN ('completed', 'pending') AND ( { $stockTable } .expires_at IS NULL OR { $stockTable } .expires_at > ?)) <= { $productTable } .low_stock_threshold " , [
now ()
]);
}
2025-12-04 10:16:38 +00:00
/**
* Check if product stock is low
*
* @ return bool True if stock management enabled , threshold set , and stock <= threshold
*/
2025-12-03 12:21:23 +00:00
public function isLowStock () : bool
{
if ( ! $this -> manage_stock || ! $this -> low_stock_threshold ) {
return false ;
}
return $this -> getAvailableStock () <= $this -> low_stock_threshold ;
}
2025-12-04 10:16:38 +00:00
/**
* Get active claims for this product
*
* Returns query builder for PENDING claims that haven ' t expired yet .
* Useful for :
* - Viewing current reservations
* - Checking what ' s claimed but not released
* - Managing active bookings
*
* @ return \Illuminate\Database\Eloquent\Builder
*/
2025-12-04 10:06:09 +00:00
public function claims ()
2025-12-03 12:21:23 +00:00
{
$stockModel = config ( 'shop.models.product_stock' , 'Blax\Shop\Models\ProductStock' );
2025-12-04 10:06:09 +00:00
return $stockModel :: claims ()
2025-12-04 09:51:45 +00:00
-> willExpire ()
2025-12-03 12:21:23 +00:00
-> where ( 'product_id' , $this -> id );
}
2025-12-04 10:06:09 +00:00
2025-12-04 10:16:38 +00:00
/**
* Get available stock on a specific date
*
* This is crucial for booking / rental systems where you need to know :
* " How many units are available on date X? "
*
* Calculation :
* 1. Start with current available stock
* 2. Add back all currently pending claims ( they reduce available stock )
* 3. Subtract only the claims that are active on the specific date
*
* Example with 100 units :
* - Claim 1 : 20 units , days 5 - 10
* - Claim 2 : 30 units , days 8 - 15
* - Current available : 50 ( 100 - 20 - 30 )
* - Available on day 3 : 100 ( no claims active )
* - Available on day 6 : 80 ( only claim 1 active )
* - Available on day 9 : 50 ( both claims active )
* - Available on day 12 : 70 ( only claim 2 active )
* - Available on day 20 : 100 ( no claims active )
*
* @ param \DateTimeInterface $date The date to check availability for
* @ return int Available stock on that date ( PHP_INT_MAX if stock management disabled )
*/
2025-12-04 10:06:09 +00:00
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 ));
}
2025-12-03 12:21:23 +00:00
}