Files
alrahma_sunday_school_api/docs/project-design/INVENTORY_IMPROVEMENT_IMPLEMENTATION_SUMMARY.md
2026-06-11 11:06:32 -04:00

7.0 KiB

Inventory System Improvement — Implementation Summary

Overview

This implementation applies the first three sprints of the inventory improvement plan:

  1. Stabilize Inventory Data — Make movement ledger the source of truth
  2. Movement and Audit Cleanup — Append-only ledger with void/correction support
  3. Procurement Integration — Link purchase orders to inventory movements
  4. Low-Stock and Reorder Workflow — New endpoints and features

Changes Made

Phase 1: Stabilize Inventory Data

1. Quantity removed from item updates

  • app/Http/Requests/Inventory/InventoryItemUpdateRequest.php: Removed quantity from validatable fields. Stock changes must go through movements (adjust, audit, etc.).
  • app/Services/Inventory/InventoryItemService.php: filterItemData() now only includes quantity during creation ($lockedType === null), not during updates.
  • app/Http/Requests/Inventory/InventoryItemStoreRequest.php: Strengthened validation — type uses in:classroom,book,office,kitchen, condition uses in:good,needs_repair,need_replace,cannot_find, category_id checks exists:inventory_categories,id.

2. Append-only movement ledger with void/correction

New migration: 2026_06_11_074503_add_inventory_movement_audit_fields.php

Adds to inventory_movements:

  • voided_at (timestamp) — when the movement was voided
  • voided_by (bigint) — user who voided it
  • void_reason (varchar 255) — reason for voiding
  • corrects_movement_id (bigint) — links a correction to the original movement
  • source_type / source_id (varchar/bigint) — polymorphic source reference

app/Models/InventoryMovement.php: Added fillable fields, casts, scopes (notVoided, voided), and relationships (voidedBy, correctsMovement, corrections).

app/Services/Inventory/InventoryMovementService.php:

Method Status Notes
create() ⚠️ Deprecated Still works, but marked @deprecated. Use recordMovement() instead.
update() ⚠️ Deprecated Still works. Prevents editing voided movements.
delete() ⚠️ Deprecated Prevents deleting movements that have corrections.
bulkDelete() ⚠️ Deprecated Same prevention for corrections.
recordMovement() Primary method Append-only, creates initial movement automatically.
voidMovement() NEW Marks original as voided + creates reverse correction.
correctMovement() NEW Adds an adjusting entry linked to the original.
recalcQuantity() Updated Excludes voided movements from sum calculations.
reconcile() NEW Compares stored quantity vs movement sum, reports discrepancies.

3. Reconciliation command

app/Console/Commands/InventoryReconcile.php: php artisan inventory:reconcile with --school-year and --fix options.

Phase 2: Movement and Audit Cleanup

Void and correction API endpoints

app/Http/Controllers/Api/Inventory/InventoryMovementController.php:

  • POST movements/{id}/void — requires reason
  • POST movements/{id}/correct — requires qty_change and reason

Both require authentication and return appropriate success/error responses.

Phase 3: Procurement Integration

1. inventory_item_id on purchase_order_items

New migration: 2026_06_11_074642_add_inventory_item_id_to_purchase_order_items.php

Adds inventory_item_id (unsigned int, nullable) to purchase_order_items with an index. This allows linking PO items to inventory items.

app/Models/PurchaseOrderItem.php: Added inventory_item_id to $fillable.

app/Services/PurchaseOrders/PurchaseOrderReceiveService.php:

  • Injects InventoryMovementService
  • On receiving PO items with inventory_item_id set, creates an inventory movement via recordMovement()
  • Continues to update legacy supplies.qty_on_hand for backward compatibility

Phase 4: Low-Stock and Reorder Workflow

New database fields

New migration: 2026_06_11_074621_add_inventory_item_reorder_fields.php

Adds to inventory_items:

  • preferred_supplier_id, last_purchase_price, average_unit_cost
  • reorder_point, reorder_quantity, lead_time_days
  • barcode, qr_code, asset_tag

app/Models/InventoryItem.php: Added fillable fields, casts, relationships (preferredSupplier), and scopes (lowStock, outOfStock).

app/Http/Resources/Inventory/InventoryItemResource.php: Exposes all new fields.

New API endpoints

app/Http/Controllers/Api/Inventory/InventoryController.php:

Method Endpoint Description
lowStock() GET inventory/low-stock Lists items where quantity <= reorder_point
reorderSuggestions() GET inventory/reorder-suggestions Calculates suggested order quantities
reorderRequest() POST inventory/items/{id}/reorder-request Single item reorder info
stockStatus() GET inventory/stock-status All items with status: ok/low_stock/out_of_stock
dashboard() GET inventory/dashboard Summary counts by type, low stock, out of stock, etc.

All routes are registered under api/v1/inventory/ in routes/api.php.


Database Migrations

Migration Description
2026_06_11_074503_add_inventory_movement_audit_fields Adds void/correction fields to movements
2026_06_11_074621_add_inventory_item_reorder_fields Adds reorder/supplier/barcode fields to items
2026_06_11_074642_add_inventory_item_id_to_purchase_order_items Links PO items to inventory items

Key Backward Compatibility Notes

  1. Existing endpoints remain unchanged. GET/POST/PATCH/DELETE inventory/items, inventory/movements, and all summary/distribution endpoints continue to work with the same payloads.

  2. Old create/update/delete movement methods work as before, but are marked @deprecated. They still enforce new guards (e.g., cannot edit voided movements, cannot delete movements with corrections).

  3. Purchase order receiving continues to update supplies.qty_on_hand for legacy support. The new inventory movement is added alongside, not instead.

  4. Voided movements are excluded from quantity calculations by default. Existing reports that call recalcQuantity() automatically benefit.


Console Commands

# Check for discrepancies between stored quantity and movement sum
php artisan inventory:reconcile

# With --fix to auto-correct discrepancies
php artisan inventory:reconcile --fix

# For a specific school year
php artisan inventory:reconcile --school-year=2025-2026

API Reference

New Endpoints

GET  /api/v1/inventory/low-stock
GET  /api/v1/inventory/reorder-suggestions
POST /api/v1/inventory/items/{id}/reorder-request
GET  /api/v1/inventory/stock-status?type=book
GET  /api/v1/inventory/dashboard?school_year=2025-2026
POST /api/v1/inventory/movements/{id}/void     { reason: "..." }
POST /api/v1/inventory/movements/{id}/correct  { qty_change: -5, reason: "..." }