Files
bakery-ia/services/inventory/README.md

1120 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Inventory Service
## Overview
The **Inventory Service** is the operational backbone of Bakery-IA, managing ingredient tracking, stock levels, expiration dates, and food safety compliance. It implements FIFO (First-In-First-Out) consumption logic, automated low-stock alerts, and HACCP-compliant temperature monitoring. This service is critical for achieving zero food waste, maintaining food safety standards, and ensuring bakeries never run out of essential ingredients.
## Key Features
### Comprehensive Ingredient Management
- **Ingredient Catalog** - Complete database of all ingredients with categories
- **Stock Tracking** - Real-time stock levels with FIFO consumption
- **Batch Tracking** - Lot numbers and traceability for food safety
- **Expiration Management** - Automated expiry alerts and FIFO rotation
- **Low Stock Alerts** - Configurable threshold notifications
- **Barcode Support** - Barcode scanning for quick stock updates
- **Multi-Location** - Track inventory across multiple storage locations
### Stock Movement Tracking
- **In/Out Transactions** - Complete audit trail of stock movements
- **Product Transformations** - Track ingredient consumption in production
- **Adjustment Logging** - Record inventory adjustments with reasons
- **Historical Analysis** - Analyze consumption patterns over time
- **Waste Tracking** - Monitor and categorize waste (expired, damaged, etc.)
### Automatic Stock Updates from Deliveries (🆕)
- **Event-Driven Stock Updates** - Automatically updates stock when deliveries are received from procurement
- **Batch & Expiry Tracking** - Records batch/lot numbers and expiration dates from delivery receipts
- **Audit Trail** - Creates complete stock entry records for each delivery
- **Zero Manual Entry** - Eliminates manual stock entry after deliveries
- **Real-Time Synchronization** - Stock levels update immediately when deliveries are recorded
### 🆕 Enterprise Tier: Internal Transfer Processing (NEW)
- **Automatic Ownership Transfer** - When shipments are delivered, inventory ownership automatically transfers from parent to child
- **Stock Deduction at Parent** - Parent's inventory is reduced when shipment departs
- **Stock Addition at Child** - Child's inventory increases when shipment is delivered
- **Transfer Event Processing** - Consumes `shipment.delivered` events from Distribution Service
- **Dual-Sided Recording** - Creates stock movement records for both source (parent) and destination (child)
- **Transfer Movement Type** - Special stock movement type `transfer_out` (parent) and `transfer_in` (child)
- **Audit Trail** - Complete visibility into inter-location transfers
- **Subscription Validation** - Enterprise transfer processing requires Enterprise tier
### Food Safety Compliance (HACCP)
- **Temperature Monitoring** - Critical control point temperature logs
- **Food Safety Alerts** - Automated safety notifications
- **Compliance Tracking** - HACCP compliance audit trail
- **Expiry Management** - Prevent use of expired ingredients
- **Lot Traceability** - Complete ingredient traceability
- **Safety Checklists** - Digital food safety inspection forms
### Sustainability & Reporting
- **Waste Reduction Tracking** - Monitor progress toward zero waste
- **Environmental Impact** - Carbon footprint and sustainability metrics
- **SDG Compliance** - Sustainable Development Goals reporting
- **Grant Reporting** - EU grant compliance reports
- **Business Model Detection** - Auto-detect B2B/B2C inventory patterns
### Dashboard & Analytics
- **Real-Time KPIs** - Current stock levels, expiring items, low stock warnings
- **Consumption Analytics** - Usage patterns and forecasting input
- **Valuation Reports** - Current inventory value and cost tracking
- **Reorder Recommendations** - Intelligent reorder point suggestions
- **Expiry Calendar** - Visual timeline of expiring products
## Business Value
### For Bakery Owners
- **Zero Food Waste Goal** - Reduce waste 20-40% through expiry management and FIFO
- **Food Safety Compliance** - HACCP compliance built-in, avoid health violations
- **Cost Control** - Track inventory value, prevent over-purchasing
- **Never Stock Out** - Automated low-stock alerts ensure continuous operations
- **Traceability** - Complete ingredient tracking for recalls and audits
### Quantifiable Impact
- **Waste Reduction**: 20-40% through FIFO and expiry management
- **Cost Savings**: €200-600/month from reduced waste and better purchasing
- **Time Savings**: 8-12 hours/week on manual inventory tracking
- **Compliance**: 100% HACCP compliance, avoid €5,000+ fines
- **Inventory Accuracy**: 95%+ vs. 70-80% with manual tracking
### For Operations Managers
- **Multi-Location Visibility** - See all inventory across locations
- **Automated Reordering** - System suggests what and when to order
- **Waste Analysis** - Identify patterns and reduce waste
- **Compliance Reporting** - Generate HACCP reports for inspections
## Technology Stack
- **Framework**: FastAPI (Python 3.11+) - Async web framework
- **Database**: PostgreSQL 17 - Ingredient and stock data
- **Caching**: Redis 7.4 - Dashboard KPI cache
- **Messaging**: RabbitMQ 4.1 - Alert publishing
- **ORM**: SQLAlchemy 2.0 (async) - Database abstraction
- **Logging**: Structlog - Structured JSON logging
- **Metrics**: Prometheus Client - Custom metrics
## API Endpoints (Key Routes)
### Ingredient Management
- `POST /api/v1/inventory/ingredients` - Create ingredient
- `GET /api/v1/inventory/ingredients` - List all ingredients
- `GET /api/v1/inventory/ingredients/{ingredient_id}` - Get ingredient details
- `PUT /api/v1/inventory/ingredients/{ingredient_id}` - Update ingredient
- `DELETE /api/v1/inventory/ingredients/{ingredient_id}` - Delete ingredient
### Stock Management
- `GET /api/v1/inventory/stock` - List current stock levels
- `GET /api/v1/inventory/stock/{stock_id}` - Get stock item details
- `POST /api/v1/inventory/stock/adjustment` - Adjust stock levels
- `POST /api/v1/inventory/stock/receive` - Receive new stock
- `POST /api/v1/inventory/stock/consume` - Consume stock (production use)
- `GET /api/v1/inventory/stock/movements` - Stock movement history
### Alerts & Monitoring
- `GET /api/v1/inventory/alerts` - Get active alerts
- `GET /api/v1/inventory/alerts/low-stock` - Low stock items
- `GET /api/v1/inventory/alerts/expiring` - Items expiring soon
- `POST /api/v1/inventory/alerts/configure` - Configure alert thresholds
### Food Safety
- `GET /api/v1/inventory/food-safety/compliance` - HACCP compliance status
- `POST /api/v1/inventory/food-safety/temperature-log` - Log temperature reading
- `GET /api/v1/inventory/food-safety/temperature-logs` - Temperature history
- `POST /api/v1/inventory/food-safety/alert` - Report food safety issue
### Analytics & Reporting
- `GET /api/v1/inventory/dashboard` - Dashboard KPIs
- `GET /api/v1/inventory/analytics/consumption` - Consumption patterns
- `GET /api/v1/inventory/analytics/waste` - Waste analysis
- `GET /api/v1/inventory/analytics/valuation` - Current inventory value
- `GET /api/v1/inventory/reports/haccp` - HACCP compliance report
- `GET /api/v1/inventory/reports/sustainability` - Sustainability report
## Database Schema
### Main Tables
**ingredients**
```sql
CREATE TABLE ingredients (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
name VARCHAR(255) NOT NULL,
category VARCHAR(100), -- flour, sugar, dairy, etc.
unit VARCHAR(50) NOT NULL, -- kg, liters, units
supplier_id UUID,
reorder_point DECIMAL(10, 2), -- Minimum stock level
reorder_quantity DECIMAL(10, 2), -- Standard order quantity
unit_cost DECIMAL(10, 2),
barcode VARCHAR(100),
storage_location VARCHAR(255),
storage_temperature_min DECIMAL(5, 2),
storage_temperature_max DECIMAL(5, 2),
shelf_life_days INTEGER,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(tenant_id, name)
);
```
**stock**
```sql
CREATE TABLE stock (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
ingredient_id UUID REFERENCES ingredients(id),
quantity DECIMAL(10, 2) NOT NULL,
unit VARCHAR(50) NOT NULL,
lot_number VARCHAR(100),
received_date DATE NOT NULL,
expiry_date DATE,
supplier_id UUID,
location VARCHAR(255),
unit_cost DECIMAL(10, 2),
status VARCHAR(50) DEFAULT 'available', -- available, reserved, expired, damaged
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
INDEX idx_tenant_ingredient (tenant_id, ingredient_id),
INDEX idx_expiry (expiry_date),
INDEX idx_status (status)
);
```
**stock_movements**
```sql
CREATE TABLE stock_movements (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
stock_id UUID REFERENCES stock(id),
ingredient_id UUID REFERENCES ingredients(id),
movement_type VARCHAR(50) NOT NULL, -- in, out, adjustment, waste, production, 🆕 transfer_in, transfer_out (NEW)
quantity DECIMAL(10, 2) NOT NULL,
unit VARCHAR(50) NOT NULL,
reference_id UUID, -- production_batch_id, order_id, shipment_id, etc.
reference_type VARCHAR(50), -- production, sale, adjustment, waste, 🆕 internal_transfer (NEW)
reason TEXT,
performed_by UUID,
-- 🆕 Enterprise internal transfer fields (NEW)
source_tenant_id UUID, -- For transfer_out: parent tenant
destination_tenant_id UUID, -- For transfer_in: child tenant
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_tenant_date (tenant_id, created_at),
INDEX idx_ingredient (ingredient_id),
-- 🆕 NEW index for internal transfers
INDEX idx_transfer_tenants (source_tenant_id, destination_tenant_id) WHERE reference_type = 'internal_transfer'
);
```
**stock_alerts**
```sql
CREATE TABLE stock_alerts (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
ingredient_id UUID REFERENCES ingredients(id),
alert_type VARCHAR(50) NOT NULL, -- low_stock, expiring_soon, expired, temperature
severity VARCHAR(20) NOT NULL, -- low, medium, high, urgent
message TEXT NOT NULL,
current_quantity DECIMAL(10, 2),
threshold_quantity DECIMAL(10, 2),
expiry_date DATE,
days_until_expiry INTEGER,
is_acknowledged BOOLEAN DEFAULT FALSE,
acknowledged_at TIMESTAMP,
acknowledged_by UUID,
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_tenant_active (tenant_id, is_acknowledged)
);
```
**product_transformations**
```sql
CREATE TABLE product_transformations (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
production_batch_id UUID,
ingredient_id UUID REFERENCES ingredients(id),
quantity_consumed DECIMAL(10, 2) NOT NULL,
unit VARCHAR(50) NOT NULL,
stock_consumed JSONB, -- Array of stock IDs with quantities (FIFO)
created_at TIMESTAMP DEFAULT NOW()
);
```
**food_safety_compliance**
```sql
CREATE TABLE food_safety_compliance (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
compliance_type VARCHAR(100), -- haccp, temperature, expiry, cleaning
check_date DATE NOT NULL,
status VARCHAR(50) NOT NULL, -- compliant, non_compliant, warning
details TEXT,
corrective_actions TEXT,
verified_by UUID,
created_at TIMESTAMP DEFAULT NOW()
);
```
**temperature_logs**
```sql
CREATE TABLE temperature_logs (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
location VARCHAR(255) NOT NULL, -- freezer_1, fridge_2, storage_room
temperature DECIMAL(5, 2) NOT NULL,
unit VARCHAR(10) DEFAULT 'C',
is_within_range BOOLEAN,
min_acceptable DECIMAL(5, 2),
max_acceptable DECIMAL(5, 2),
recorded_by UUID,
recorded_at TIMESTAMP DEFAULT NOW(),
INDEX idx_tenant_location (tenant_id, location, recorded_at)
);
```
**food_safety_alerts**
```sql
CREATE TABLE food_safety_alerts (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
alert_type VARCHAR(100), -- temperature_violation, expired_used, contamination
severity VARCHAR(20) NOT NULL,
location VARCHAR(255),
ingredient_id UUID,
description TEXT,
corrective_action_required TEXT,
status VARCHAR(50) DEFAULT 'open', -- open, investigating, resolved
resolved_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
```
## Events & Messaging
### Published Events (RabbitMQ)
**Exchange**: `inventory`
**Routing Keys**: `inventory.low_stock`, `inventory.expiring`, `inventory.food_safety`
**Low Stock Alert Event**
```json
{
"event_type": "low_stock_alert",
"tenant_id": "uuid",
"ingredient_id": "uuid",
"ingredient_name": "Harina integral",
"current_quantity": 15.5,
"unit": "kg",
"reorder_point": 50.0,
"recommended_order_quantity": 100.0,
"days_until_stockout": 3,
"severity": "high",
"message": "Stock bajo: Harina integral (15.5 kg). Punto de reorden: 50 kg.",
"timestamp": "2025-11-06T10:30:00Z"
}
```
**Expiring Soon Alert Event**
```json
{
"event_type": "expiring_soon_alert",
"tenant_id": "uuid",
"stock_id": "uuid",
"ingredient_id": "uuid",
"ingredient_name": "Leche fresca",
"quantity": 20.0,
"unit": "liters",
"lot_number": "LOT-2025-1105",
"expiry_date": "2025-11-09",
"days_until_expiry": 3,
"location": "Nevera 1",
"severity": "medium",
"message": "Leche fresca expira en 3 días (20 litros, LOT-2025-1105)",
"recommended_action": "Usar en producción antes del 09/11/2025",
"timestamp": "2025-11-06T10:30:00Z"
}
```
**Food Safety Alert Event**
```json
{
"event_type": "food_safety_alert",
"tenant_id": "uuid",
"alert_type": "temperature_violation",
"severity": "urgent",
"location": "Congelador 2",
"temperature": -12.5,
"acceptable_range": {
"min": -18.0,
"max": -15.0
},
"duration_minutes": 45,
"affected_items": ["uuid1", "uuid2"],
"message": "Violación de temperatura en Congelador 2: -12.5°C (rango: -18°C a -15°C)",
"corrective_action_required": "Revisar congelador inmediatamente y verificar integridad de productos",
"timestamp": "2025-11-06T10:30:00Z"
}
```
### Consumed Events
**From Procurement Service (🆕)**
- **Delivery Received** (`delivery.received`) - Automatically updates stock levels when deliveries are recorded
- Creates/updates Stock records for each inventory product
- Creates StockEntry audit records with batch/lot numbers and expiry dates
- Handles accepted quantities from delivery receipts
- Links stock movements to delivery reference IDs for full traceability
**🆕 From Distribution Service (NEW)**
- **Shipment Delivered** (`shipment.delivered`) - Automatically processes internal transfers when shipments are delivered
- Decreases stock at parent tenant (creates `transfer_out` stock movement)
- Increases stock at child tenant (creates `transfer_in` stock movement)
- Records source_tenant_id and destination_tenant_id for full transfer traceability
- Links both movements to shipment_id for audit trail
- Enterprise tier validation required
**From Other Services**
- **From Production**: Ingredient consumption in production
- **From Sales**: Finished product sales (for inventory valuation)
## Custom Metrics (Prometheus)
```python
# Stock level metrics
stock_quantity_gauge = Gauge(
'inventory_stock_quantity',
'Current stock quantity',
['tenant_id', 'ingredient_id', 'ingredient_name']
)
low_stock_items_total = Gauge(
'inventory_low_stock_items',
'Number of items below reorder point',
['tenant_id']
)
expiring_items_total = Gauge(
'inventory_expiring_items',
'Number of items expiring within 7 days',
['tenant_id']
)
# Waste metrics
waste_quantity = Counter(
'inventory_waste_quantity_total',
'Total waste quantity',
['tenant_id', 'ingredient_category', 'reason'] # expired, damaged, etc.
)
waste_value_euros = Counter(
'inventory_waste_value_euros_total',
'Total waste value in euros',
['tenant_id']
)
# Inventory valuation
inventory_value_total = Gauge(
'inventory_value_euros',
'Total inventory value',
['tenant_id']
)
# Food safety metrics
temperature_violations = Counter(
'inventory_temperature_violations_total',
'Temperature violations detected',
['tenant_id', 'location']
)
food_safety_alerts_total = Counter(
'inventory_food_safety_alerts_total',
'Food safety alerts generated',
['tenant_id', 'alert_type', 'severity']
)
```
## Configuration
### Environment Variables
**Service Configuration:**
- `PORT` - Service port (default: 8005)
- `DATABASE_URL` - PostgreSQL connection string
- `REDIS_URL` - Redis connection string
- `RABBITMQ_URL` - RabbitMQ connection string
**Alert Configuration:**
- `LOW_STOCK_CHECK_INTERVAL_HOURS` - How often to check (default: 6)
- `EXPIRY_WARNING_DAYS` - Days before expiry to alert (default: 7)
- `URGENT_EXPIRY_DAYS` - Days for urgent expiry alerts (default: 3)
- `ENABLE_AUTO_ALERTS` - Automatic alert generation (default: true)
**FIFO Configuration:**
- `ENABLE_FIFO_ENFORCEMENT` - Enforce FIFO consumption (default: true)
- `FIFO_VIOLATION_ALERT` - Alert on FIFO violations (default: true)
**Food Safety Configuration:**
- `TEMPERATURE_CHECK_INTERVAL_MINUTES` - Temp log frequency (default: 60)
- `TEMPERATURE_VIOLATION_THRESHOLD_MINUTES` - Time before alert (default: 30)
- `ENABLE_HACCP_COMPLIANCE` - Enable HACCP tracking (default: true)
**Sustainability Configuration:**
- `TRACK_CARBON_FOOTPRINT` - Enable carbon tracking (default: true)
- `TRACK_SDG_METRICS` - Enable SDG reporting (default: true)
## Development Setup
### Prerequisites
- Python 3.11+
- PostgreSQL 17
- Redis 7.4
- RabbitMQ 4.1 (optional)
### Local Development
```bash
cd services/inventory
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
export DATABASE_URL=postgresql://user:pass@localhost:5432/inventory
export REDIS_URL=redis://localhost:6379/0
export RABBITMQ_URL=amqp://guest:guest@localhost:5672/
alembic upgrade head
python main.py
```
### Testing
```bash
# Unit tests
pytest tests/unit/ -v
# Integration tests
pytest tests/integration/ -v
# FIFO logic tests
pytest tests/test_fifo.py -v
# Test with coverage
pytest --cov=app tests/ --cov-report=html
```
## Integration Points
### Dependencies
- **Procurement Service** - Receive stock from purchase orders via delivery events (🆕)
- **Production Service** - Consume ingredients in production
- **Forecasting Service** - Provide consumption data for forecasts
- **Suppliers Service** - Supplier information for stock items
- **🆕 Distribution Service** (NEW) - Process internal transfers via shipment.delivered events
- **🆕 Tenant Service** (NEW) - Validate tenant hierarchy for internal transfers
- **PostgreSQL** - Inventory data storage
- **Redis** - Dashboard KPI cache
- **RabbitMQ** - Alert publishing and delivery event consumption (🆕)
### Dependents
- **Production Service** - Check ingredient availability
- **Procurement Service** - Get reorder recommendations
- **AI Insights Service** - Analyze inventory patterns
- **Frontend Dashboard** - Display inventory status
- **Notification Service** - Send inventory alerts
- **🆕 Distribution Service** (NEW) - Verify inventory availability before creating shipments
## Delivery Event Processing (🆕)
### Automatic Stock Updates from Deliveries
The inventory service automatically updates stock levels when deliveries are recorded in the procurement service through event-driven architecture.
**How It Works:**
1. User records delivery receipt in procurement service (frontend `DeliveryReceiptModal`)
2. Procurement service publishes `delivery.received` event to RabbitMQ
3. Inventory service's `DeliveryEventConsumer` listens for and processes the event
4. For each delivered item:
- Gets or creates Stock record for the inventory product
- Updates `quantity_available` by adding accepted quantity
- Creates StockEntry audit record with:
- Transaction type: `RESTOCK`
- Reference type: `DELIVERY`
- Batch/lot number from delivery
- Expiration date from delivery
- Quantity change and new balance
**Benefits:**
- **Zero Manual Entry** - Eliminates duplicate data entry in inventory after recording deliveries
- **Real-Time Updates** - Stock levels update within seconds of delivery recording
- **Complete Traceability** - Every stock change linked to source delivery
- **Batch Tracking** - Automatic batch/lot number recording for food safety compliance
- **Expiry Management** - Expiration dates automatically captured from delivery receipts
### Delivery Event Consumer Implementation
```python
async def process_delivery_stock_update(self, event_data: Dict[str, Any]) -> bool:
"""
Process delivery event and update stock levels.
Listens to: procurement.events exchange, routing key: delivery.received
Creates: Stock records and StockEntry audit records
"""
data = event_data.get('data', {})
tenant_id = uuid.UUID(data.get('tenant_id'))
delivery_id = uuid.UUID(data.get('delivery_id'))
items = data.get('items', [])
async with database_manager.get_session() as session:
stock_repo = StockRepository(session)
entry_repo = StockEntryRepository(session)
for item in items:
inventory_product_id = uuid.UUID(item.get('inventory_product_id'))
accepted_quantity = Decimal(str(item.get('accepted_quantity', 0)))
if accepted_quantity <= 0:
continue # Skip rejected items
# Get or create stock record
stock = await stock_repo.get_stock_by_product(
tenant_id=tenant_id,
inventory_product_id=inventory_product_id
)
if not stock:
# Create new stock record
stock = await stock_repo.create_stock({
'tenant_id': tenant_id,
'inventory_product_id': inventory_product_id,
'quantity_available': Decimal('0'),
# ... other fields
})
# Update stock quantity
new_quantity = stock.quantity_available + accepted_quantity
await stock_repo.update_stock(
stock_id=stock.id,
tenant_id=tenant_id,
update_data={
'quantity_available': new_quantity,
'last_restocked_at': datetime.now(timezone.utc)
}
)
# Create stock entry for audit trail
entry = await entry_repo.create_entry({
'tenant_id': tenant_id,
'stock_id': stock.id,
'transaction_type': 'RESTOCK',
'quantity_change': accepted_quantity,
'quantity_after': new_quantity,
'reference_type': 'DELIVERY',
'reference_id': delivery_id,
'batch_number': item.get('batch_lot_number'),
'expiry_date': item.get('expiry_date'),
'notes': f"Delivery received from PO. Batch: {item.get('batch_lot_number', 'N/A')}"
})
await session.commit()
return True
```
**Event Schema:**
```json
{
"event_id": "uuid",
"event_type": "delivery.received",
"service_name": "procurement",
"timestamp": "2025-11-12T10:30:00Z",
"data": {
"tenant_id": "uuid",
"delivery_id": "uuid",
"po_id": "uuid",
"received_by": "uuid",
"received_at": "2025-11-12T10:30:00Z",
"items": [
{
"inventory_product_id": "uuid",
"ordered_quantity": 100.0,
"delivered_quantity": 98.0,
"accepted_quantity": 95.0,
"rejected_quantity": 3.0,
"batch_lot_number": "LOT-2025-1112",
"expiry_date": "2025-12-12",
"rejection_reason": "3 damaged units"
}
]
}
}
```
## Stock Receipt System
The Stock Receipt System provides lot-level tracking for incoming deliveries with complete food safety compliance and traceability.
### Overview
When deliveries arrive, users record receipts through the frontend `StockReceiptModal`, capturing:
- **Lot-Level Details** - Multiple lots per line item (e.g., 100kg delivered in 4×25kg lots)
- **Expiration Dates** - Required for all food ingredients (HACCP compliance)
- **Discrepancy Tracking** - Record when actual ≠ expected quantities
- **Draft Workflow** - Save progress while completing receipt details
### Database Schema
**stock_receipts**
```sql
CREATE TABLE stock_receipts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
po_id UUID NOT NULL, -- Links to purchase order
po_number VARCHAR(50) NOT NULL,
-- Receipt details
received_at TIMESTAMP NOT NULL,
received_by_user_id UUID NOT NULL,
status VARCHAR(50) NOT NULL, -- draft, confirmed, cancelled
-- Supplier information
supplier_id UUID NOT NULL,
supplier_name VARCHAR(255) NOT NULL,
-- Metadata
notes TEXT,
has_discrepancies BOOLEAN DEFAULT FALSE,
-- Timestamps
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
-- Foreign keys
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (po_id) REFERENCES purchase_orders(id) ON DELETE CASCADE
);
CREATE INDEX idx_stock_receipts_tenant_po ON stock_receipts(tenant_id, po_id);
CREATE INDEX idx_stock_receipts_status ON stock_receipts(status);
```
**stock_receipt_line_items**
```sql
CREATE TABLE stock_receipt_line_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
receipt_id UUID NOT NULL,
-- Product reference
ingredient_id UUID NOT NULL,
ingredient_name VARCHAR(255) NOT NULL,
po_line_id UUID, -- Links to PO line item
-- Quantities
expected_quantity DECIMAL(10, 2) NOT NULL,
actual_quantity DECIMAL(10, 2) NOT NULL,
unit_of_measure VARCHAR(50) NOT NULL,
-- Discrepancy tracking
has_discrepancy BOOLEAN DEFAULT FALSE,
discrepancy_reason TEXT,
-- Costing
unit_cost DECIMAL(10, 2),
total_cost DECIMAL(10, 2),
-- Timestamps
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
-- Foreign keys
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (receipt_id) REFERENCES stock_receipts(id) ON DELETE CASCADE,
FOREIGN KEY (ingredient_id) REFERENCES ingredients(id) ON DELETE CASCADE
);
CREATE INDEX idx_receipt_line_items_receipt ON stock_receipt_line_items(receipt_id);
```
**stock_lots**
```sql
CREATE TABLE stock_lots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
line_item_id UUID NOT NULL,
stock_id UUID, -- Links to Stock table after confirmation
-- Lot identification
lot_number VARCHAR(100), -- Internal lot number
supplier_lot_number VARCHAR(100), -- Supplier's lot number
-- Quantity
quantity DECIMAL(10, 2) NOT NULL,
unit_of_measure VARCHAR(50) NOT NULL,
-- Food safety (REQUIRED)
expiration_date DATE NOT NULL, -- ⭐ REQUIRED for food safety
best_before_date DATE,
-- Storage
warehouse_location VARCHAR(255),
storage_zone VARCHAR(100), -- freezer, fridge, dry_storage
-- Quality
quality_notes TEXT,
-- Timestamps
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
-- Foreign keys
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (line_item_id) REFERENCES stock_receipt_line_items(id) ON DELETE CASCADE,
FOREIGN KEY (stock_id) REFERENCES stock(id) ON DELETE SET NULL,
-- Validation: expiration_date is required
CONSTRAINT check_expiration_date_not_null CHECK (expiration_date IS NOT NULL)
);
CREATE INDEX idx_stock_lots_line_item ON stock_lots(line_item_id);
CREATE INDEX idx_stock_lots_expiration ON stock_lots(expiration_date);
```
### API Endpoints
**POST /api/v1/inventory/stock-receipts**
Create draft stock receipt
```json
{
"tenant_id": "uuid",
"po_id": "uuid",
"received_at": "2025-11-26T10:30:00Z",
"received_by_user_id": "uuid",
"line_items": [
{
"ingredient_id": "uuid",
"expected_quantity": 100.0,
"actual_quantity": 98.0,
"unit_of_measure": "kg",
"has_discrepancy": true,
"discrepancy_reason": "2kg damaged during transport",
"lots": [
{
"quantity": 25.0,
"expiration_date": "2025-12-26",
"lot_number": "LOT-A-001",
"supplier_lot_number": "SUP-2025-1126"
},
{
"quantity": 25.0,
"expiration_date": "2025-12-26",
"lot_number": "LOT-A-002"
},
{
"quantity": 25.0,
"expiration_date": "2025-12-27",
"lot_number": "LOT-B-001"
},
{
"quantity": 23.0,
"expiration_date": "2025-12-27",
"lot_number": "LOT-B-002"
}
]
}
]
}
```
**Response**: Created stock receipt with `status=draft`
**GET /api/v1/inventory/stock-receipts/{receipt_id}**
Retrieve stock receipt details
- Returns full receipt with nested line items and lots
- Used to resume editing draft receipts
**PUT /api/v1/inventory/stock-receipts/{receipt_id}**
Update draft stock receipt
- Save progress while filling in details
- Validates lot quantity sums
- Replaces line items (cascade delete-and-recreate)
**POST /api/v1/inventory/stock-receipts/{receipt_id}/confirm**
Finalize stock receipt (atomic transaction)
- Creates Stock records (one per lot)
- Creates StockMovement records (type: PURCHASE)
- Marks receipt status as `confirmed`
- Links lots to stock records via `stock_id`
- **Atomic**: All operations succeed or all fail
```json
// Request body (minimal)
{
"confirmed_at": "2025-11-26T10:45:00Z"
}
```
**Response**:
```json
{
"receipt_id": "uuid",
"status": "confirmed",
"stock_records_created": 4,
"total_quantity_added": 98.0,
"stock_ids": ["uuid1", "uuid2", "uuid3", "uuid4"]
}
```
### Validation Rules
**Lot Quantity Validation**:
```python
# Sum of lot quantities must equal actual quantity
sum(lot.quantity for lot in line_item.lots) == line_item.actual_quantity
```
**Expiration Date Validation**:
```python
# All food ingredients REQUIRE expiration date
if ingredient.category in ['perishable', 'dairy', 'meat', 'produce']:
if not lot.expiration_date:
raise ValidationError("Expiration date required for food safety")
```
**Discrepancy Validation**:
```python
# Discrepancy reason required when actual ≠ expected
if line_item.actual_quantity != line_item.expected_quantity:
line_item.has_discrepancy = True
if not line_item.discrepancy_reason:
raise ValidationError("Discrepancy reason required")
```
**Tenant Security**:
```python
# All operations check tenant_id
if receipt.tenant_id != current_user.tenant_id:
raise PermissionError("Access denied")
```
### Integration with Alert System
The stock receipt system integrates tightly with the alert system to guide users through the delivery workflow:
**DELIVERY_ARRIVING_SOON Alert** (T-2 hours):
- Alert triggers StockReceiptModal in `create` mode
- User creates draft receipt while delivery is in transit
- Pre-fills PO details automatically
**DELIVERY_OVERDUE Alert** (T+30 minutes):
- Critical priority alert
- Primary action: "Contact Supplier"
- Secondary action: "Mark as Received" (opens StockReceiptModal)
**STOCK_RECEIPT_INCOMPLETE Alert** (Post-window):
- Important priority alert
- Opens StockReceiptModal in `edit` mode if draft exists
- Shows incomplete line items and missing lot details
**Workflow**:
```
1. DELIVERY_ARRIVING_SOON alert appears
2. User clicks "Mark as Received"
3. StockReceiptModal opens (create mode)
4. User enters lot details and expiration dates
5. Clicks "Save Draft" (can finish later)
6. Later: Returns and clicks "Complete Stock Receipt"
7. Reviews, adds missing details
8. Clicks "Confirm Receipt"
9. System confirms receipt (atomic transaction)
10. Triggers delivery.received event
11. Auto-resolves all delivery alerts for this PO
```
### Food Safety Compliance
**Why Expiration Dates are Required**:
- HACCP (Hazard Analysis and Critical Control Points) compliance
- Prevents use of expired ingredients
- Enables FIFO consumption based on expiration
- Required for food safety audits in EU/Spain
- Supports lot recall procedures
**Lot Traceability**:
- Each lot links to supplier lot number
- Complete chain: Supplier → Delivery → Stock → Production → Customer
- Enables rapid recall if contamination detected
- Audit trail for food safety inspectors
**Storage Zone Tracking**:
- Warehouse location captured per lot
- Storage zone (freezer/fridge/dry) enforces temperature requirements
- Supports temperature monitoring integration
### Example: Receiving 100kg Flour Delivery
**Scenario**: PO for 100kg flour arrives in 4×25kg bags
**Step 1**: Create Draft Receipt
```bash
POST /api/v1/inventory/stock-receipts
{
"po_id": "PO-12345",
"received_at": "2025-11-26T10:30:00Z",
"line_items": [
{
"ingredient_id": "flour-00-uuid",
"expected_quantity": 100.0,
"actual_quantity": 100.0,
"unit_of_measure": "kg",
"lots": [
{
"quantity": 25.0,
"expiration_date": "2026-05-26",
"supplier_lot_number": "MILL-2025-W47-A",
"warehouse_location": "Almacén Principal - Estante 3"
},
// ... 3 more lots
]
}
]
}
```
**Step 2**: Confirm Receipt
```bash
POST /api/v1/inventory/stock-receipts/{receipt_id}/confirm
{
"confirmed_at": "2025-11-26T10:45:00Z"
}
```
**Result**:
- 4 Stock records created (one per lot)
- 4 StockMovement records (type: PURCHASE)
- Receipt status: `confirmed`
- Inventory available quantity increases by 100kg
- FIFO will consume oldest expiration date first
## FIFO Implementation
### FIFO Consumption Logic
```python
async def consume_ingredient_fifo(
tenant_id: str,
ingredient_id: str,
quantity_needed: float
) -> list[dict]:
"""Consume ingredients using FIFO (First-In-First-Out)"""
# Get available stock ordered by received_date (oldest first)
available_stock = await db.query(Stock).filter(
Stock.tenant_id == tenant_id,
Stock.ingredient_id == ingredient_id,
Stock.status == 'available',
Stock.quantity > 0
).order_by(Stock.received_date.asc()).all()
consumed_items = []
remaining_needed = quantity_needed
for stock_item in available_stock:
if remaining_needed <= 0:
break
quantity_to_consume = min(stock_item.quantity, remaining_needed)
# Update stock quantity
stock_item.quantity -= quantity_to_consume
if stock_item.quantity == 0:
stock_item.status = 'depleted'
# Record consumption
consumed_items.append({
'stock_id': stock_item.id,
'lot_number': stock_item.lot_number,
'quantity_consumed': quantity_to_consume,
'received_date': stock_item.received_date,
'expiry_date': stock_item.expiry_date
})
remaining_needed -= quantity_to_consume
if remaining_needed > 0:
raise InsufficientStockError(
f"Insufficient stock. Needed: {quantity_needed}, "
f"Available: {quantity_needed - remaining_needed}"
)
await db.commit()
return consumed_items
```
## Security Measures
### Data Protection
- **Tenant Isolation** - All inventory scoped to tenant_id
- **Input Validation** - Validate all quantities and dates
- **Audit Trail** - Complete history of stock movements
- **Access Control** - Role-based permissions
### Food Safety Security
- **Temperature Log Integrity** - Tamper-proof temperature records
- **Lot Traceability** - Complete ingredient tracking for recalls
- **Audit Compliance** - HACCP-compliant record keeping
- **Alert Escalation** - Critical food safety alerts escalate automatically
## Troubleshooting
### Common Issues
**Issue**: FIFO not working correctly
- **Cause**: Stock items missing received_date
- **Solution**: Ensure all stock has received_date set
**Issue**: Low stock alerts not firing
- **Cause**: Reorder points not configured
- **Solution**: Set reorder_point for each ingredient
**Issue**: Expiry alerts too frequent
- **Cause**: `EXPIRY_WARNING_DAYS` set too high
- **Solution**: Adjust to 3-5 days instead of 7
**Issue**: Temperature violations not detected
- **Cause**: Temperature logs not being recorded
- **Solution**: Check temperature monitoring device integration
## Competitive Advantages
1. **FIFO Enforcement** - Automatic expiry prevention
2. **Food Safety Built-In** - HACCP compliance out-of-the-box
3. **Sustainability Tracking** - SDG reporting for EU grants
4. **Barcode Support** - Quick stock updates
5. **Multi-Location** - Track inventory across sites
6. **Spanish Market** - HACCP compliant for Spanish regulations
7. **Zero Waste Focus** - Waste reduction analytics
## Future Enhancements
- **IoT Sensor Integration** - Automatic temperature monitoring
- **AI-Powered Reorder Points** - Dynamic reorder point calculation
- **Image Recognition** - Photo-based stock counting
- **Blockchain Traceability** - Immutable ingredient tracking
- **Mobile Barcode App** - Smartphone barcode scanning
- **Supplier Integration** - Direct supplier ordering
- **Predictive Expiry** - Predict expiry based on storage conditions
---
**For VUE Madrid Business Plan**: The Inventory Service demonstrates commitment to food safety (HACCP compliance), sustainability (20-40% waste reduction), and operational excellence. The FIFO enforcement and expiry management features directly address EU food waste regulations and support SDG goals, making this ideal for grant applications. The €200-600/month cost savings and compliance benefits provide clear ROI for bakery owners.