Skip to main content

Overview

The InventoryService manages inventory items and stock movements with automatic tracking, low stock notifications, and full audit trail capabilities. Namespace: App\Services\InventoryService

Methods

createItem

Creates a new inventory item with initial stock and records the initial movement.
public function createItem(
    Company $company,
    User $actor,
    array $payload
): InventoryItem

Parameters

company
Company
required
The company that owns the inventory item
actor
User
required
The user creating the inventory item
payload
array
required
Item configuration array with the following structure:
[
    'name' => string,                    // Product name
    'internal_code' => string,           // SKU or internal identifier
    'quantity' => int,                   // Initial stock quantity
    'low_stock_threshold' => int,        // Alert threshold (default: 5)
    'is_sale_enabled' => bool,           // Enable for billing (default: false)
    'sale_price' => float | null,        // Price when sale enabled
]

Returns

Returns the created InventoryItem model instance

Behavior

  • Converts internal_code to uppercase
  • Creates initial inventory movement with type 'addition'
  • Records stock change from 0 to initial quantity
  • Checks for low stock and sends notifications if needed
  • All operations wrapped in database transaction

Example Usage

use App\Services\InventoryService;

$inventoryService = app(InventoryService::class);

$item = $inventoryService->createItem(
    company: $company,
    actor: $user,
    payload: [
        'name' => 'Rodamiento SKF 6205',
        'internal_code' => 'ROD-SKF-6205',
        'quantity' => 50,
        'low_stock_threshold' => 10,
        'is_sale_enabled' => true,
        'sale_price' => 320.00,
    ]
);

echo "Created: {$item->name}";
echo "Stock: {$item->quantity}";
echo "Code: {$item->internal_code}"; // "ROD-SKF-6205" (uppercase)

adjustStock

Adjusts inventory stock levels (add or remove) with automatic movement tracking.
public function adjustStock(
    InventoryItem $item,
    User $actor,
    array $payload
): InventoryItem

Parameters

item
InventoryItem
required
The inventory item to adjust
actor
User
required
The user performing the adjustment
payload
array
required
Adjustment configuration:
[
    'movement_type' => 'addition' | 'removal',
    'quantity' => int,              // Amount to add or remove
    'notes' => string | null,       // Optional reason/notes
]

Returns

Returns the updated InventoryItem model instance (refreshed from database)

Behavior

Addition:
$stockAfter = $stockBefore + $quantity;
Removal:
$stockAfter = $stockBefore - $quantity;
Validation:
  • Cannot remove more than available stock (throws 422 error)
  • Stock cannot go negative
Tracking:
  • Creates InventoryMovement record with before/after stock levels
  • Records user, timestamp, and notes
  • Triggers low stock notifications if threshold crossed

Throws

422 Unprocessable Entity if:
  • Attempting to remove more stock than available
  • Result would be negative stock

Example Usage

Adding Stock:
$item = $inventoryService->adjustStock(
    item: $item,
    actor: $user,
    payload: [
        'movement_type' => 'addition',
        'quantity' => 25,
        'notes' => 'Recepción de pedido proveedor ABC',
    ]
);

echo "New stock: {$item->quantity}";
Removing Stock:
$item = $inventoryService->adjustStock(
    item: $item,
    actor: $user,
    payload: [
        'movement_type' => 'removal',
        'quantity' => 5,
        'notes' => 'Uso interno taller',
    ]
);
Error Handling:
try {
    $item = $inventoryService->adjustStock(
        item: $item,
        actor: $user,
        payload: [
            'movement_type' => 'removal',
            'quantity' => 999,
            'notes' => 'Invalid adjustment',
        ]
    );
} catch (\Illuminate\Http\Exceptions\HttpResponseException $e) {
    echo "Error: Cannot remove more than available stock";
}

lowStockItemsForCompany

Retrieves all inventory items that are at or below their low stock threshold.
public function lowStockItemsForCompany(int $companyId): Collection

Parameters

companyId
int
required
The company ID to check for low stock items

Returns

Returns an Eloquent Collection of InventoryItem models, ordered by quantity (lowest first)

Query Logic

InventoryItem::query()
    ->where('company_id', $companyId)
    ->whereColumn('quantity', '<=', 'low_stock_threshold')
    ->orderBy('quantity')
    ->get();

Example Usage

$lowStockItems = $inventoryService->lowStockItemsForCompany($company->id);

if ($lowStockItems->isNotEmpty()) {
    echo "Low stock alerts: {$lowStockItems->count()}";
    
    foreach ($lowStockItems as $item) {
        echo "{$item->name}: {$item->quantity} (threshold: {$item->low_stock_threshold})";
    }
}
Display in Dashboard:
$alerts = $inventoryService->lowStockItemsForCompany(auth()->user()->company_id);

return view('dashboard', [
    'low_stock_alerts' => $alerts,
]);

Low Stock Notifications

The service automatically sends notifications when items reach low stock levels.

notifyLowStockIfNeeded (Private)

Automatically called after stock adjustments to check and send notifications.
private function notifyLowStockIfNeeded(InventoryItem $item): void

Behavior

Recipients: Notifies users who are:
  • In the same company
  • Active (is_active = true)
  • Role is 'admin', OR
  • Role is 'worker' with can_access_inventory = true
Deduplication:
  • Checks for existing unread notifications for the same item
  • Only sends if no unread notification exists
  • Prevents notification spam
Notification Type:
  • Uses LowStockInventoryNotification
  • Contains item details and stock levels
  • Stored in notifications table

Trigger Condition

if ($item->quantity <= $item->low_stock_threshold) {
    // Send notifications
}

Inventory Movement Tracking

Every stock change creates an InventoryMovement record with:
[
    'inventory_item_id' => int,
    'company_id' => int,
    'user_id' => int,              // Who made the change
    'movement_type' => string,     // 'addition' or 'removal'
    'quantity' => int,             // Amount changed
    'stock_before' => int,         // Stock before change
    'stock_after' => int,          // Stock after change
    'notes' => string | null,      // Reason/context
    'created_at' => timestamp,     // When it happened
]

Audit Trail Example

$movements = InventoryMovement::query()
    ->where('inventory_item_id', $item->id)
    ->with('user')
    ->latest()
    ->get();

foreach ($movements as $movement) {
    echo "{$movement->created_at}: {$movement->user->name}";
    echo "{$movement->movement_type} {$movement->quantity}";
    echo "Stock: {$movement->stock_before} → {$movement->stock_after}";
    echo "Notes: {$movement->notes}";
}

Transaction Safety

All operations are wrapped in database transactions:
DB::transaction(function () {
    // Update inventory
    // Create movement record
    // Check notifications
});
If any step fails, all changes are rolled back.

Integration with Billing

The BillingService uses InventoryService to deduct stock:
// From BillingService.php:238
app(InventoryService::class)->adjustStock($inventory, $actor, [
    'movement_type' => 'removal',
    'quantity' => $qtyToDiscount,
    'notes' => 'Descuento por facturación emitida',
]);
This ensures:
  • Automatic stock deduction on invoice creation
  • Movement tracking for billing-related changes
  • Low stock alerts triggered if needed

Build docs developers (and LLMs) love