demo seed change 3
This commit is contained in:
@@ -1,458 +0,0 @@
|
||||
# Architecture Alignment Complete ✅
|
||||
|
||||
**Date**: December 14, 2025
|
||||
**Status**: All Implementation Complete
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Successfully completed full alignment of the bakery-ia codebase with the new demo architecture specification. All 11 services now use standardized BASE_TS marker parsing, all legacy code has been removed, and 100% timezone-aware UTC datetimes are enforced throughout.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Service Standardization (COMPLETED)
|
||||
|
||||
### Services Updated
|
||||
|
||||
All 11 internal_demo.py files have been standardized:
|
||||
|
||||
| Service | Helper Added | BASE_REFERENCE_DATE Removed | Timezone Fixed | Status |
|
||||
|---------|--------------|----------------------------|----------------|--------|
|
||||
| **Inventory** | ✅ Already had | ✅ Already removed | ✅ 4 fixes | Complete |
|
||||
| **Production** | ✅ Already had | ✅ 15 references removed | ✅ No issues | Complete |
|
||||
| **Procurement** | ✅ Already had | ✅ Already removed | ✅ No issues | Complete |
|
||||
| **Orders** | ✅ Already had | ✅ 3 references removed | ✅ No issues | Complete |
|
||||
| **Forecasting** | ✅ Already had | ✅ 2 references removed | ✅ No issues | Complete |
|
||||
| **Sales** | ✅ **Added** | ✅ 1 reference removed | ✅ No issues | Complete |
|
||||
| **Recipes** | ✅ **Added** | ✅ 2 references removed | ✅ 2 fixes | Complete |
|
||||
| **Tenant** | ✅ **Added** | ✅ 2 references removed | ✅ No issues | Complete |
|
||||
| **Suppliers** | ✅ **Added** | ✅ 3 references removed | ✅ 2 fixes | Complete |
|
||||
| **Orchestrator** | ❌ Not needed | ✅ 3 references removed | ✅ No issues | Complete |
|
||||
| **Auth** | ❌ Not needed | ✅ Already clean | ✅ No issues | Complete |
|
||||
|
||||
**Total Changes:**
|
||||
- ✅ 8 services with `parse_date_field()` helper
|
||||
- ✅ 31 `BASE_REFERENCE_DATE` references removed
|
||||
- ✅ 8 timezone-naive `datetime.now()` calls fixed
|
||||
- ✅ 0 remaining issues
|
||||
|
||||
---
|
||||
|
||||
## Architecture Compliance Matrix
|
||||
|
||||
| **Requirement** | **Status** | **Evidence** |
|
||||
|----------------|------------|--------------|
|
||||
| All services use `parse_date_field()` helper | ✅ 100% | 8/8 services that need it |
|
||||
| No `BASE_REFERENCE_DATE` parameter | ✅ 100% | 0 import references, 1 comment only |
|
||||
| All datetimes timezone-aware UTC | ✅ 100% | 0 `datetime.now()` without timezone |
|
||||
| All JSON files use BASE_TS markers | ✅ 100% | 25/25 files validated |
|
||||
| No legacy `offset_days` fields | ✅ 100% | All removed via migration |
|
||||
| Edge cases dynamically created | ✅ 100% | 8 edge cases implemented |
|
||||
| Deterministic demo sessions | ✅ 100% | All dates relative to session time |
|
||||
| Decimal BASE_TS support | ✅ 100% | `0.5d`, `0.25d`, etc. supported |
|
||||
|
||||
---
|
||||
|
||||
## Critical Fix: Decimal BASE_TS Marker Support
|
||||
|
||||
### Problem Discovered
|
||||
|
||||
The procurement JSON files were using decimal BASE_TS markers:
|
||||
```json
|
||||
{
|
||||
"order_date": "BASE_TS - 0.5d", // 12 hours ago
|
||||
"required_delivery_date": "BASE_TS + 0.25d", // 6 hours ahead
|
||||
"estimated_delivery_date": "BASE_TS + 0.083d" // 2 hours ahead
|
||||
}
|
||||
```
|
||||
|
||||
The `resolve_time_marker()` function was using `int()` to parse values, causing errors:
|
||||
```python
|
||||
error="invalid literal for int() with base 10: '0.5'"
|
||||
```
|
||||
|
||||
### Solution Implemented
|
||||
|
||||
Updated [shared/utils/demo_dates.py:184-207](shared/utils/demo_dates.py#L184-L207):
|
||||
|
||||
```python
|
||||
# BEFORE (integer only)
|
||||
days = int(day_part)
|
||||
hours = int(hour_part)
|
||||
minutes = int(minute_part)
|
||||
|
||||
# AFTER (supports decimals)
|
||||
days = float(day_part) # 0.5d = 12 hours
|
||||
hours = float(hour_part) # 1.25h = 1h15m
|
||||
minutes = float(minute_part) # 30.5m = 30m30s
|
||||
```
|
||||
|
||||
### Supported Formats
|
||||
|
||||
Now supports both integer and decimal BASE_TS offsets:
|
||||
|
||||
| Format | Meaning | Example |
|
||||
|--------|---------|---------|
|
||||
| `BASE_TS + 1d` | 1 day ahead | `2025-12-15 06:00` |
|
||||
| `BASE_TS - 0.5d` | 12 hours ago | `2025-12-13 18:00` |
|
||||
| `BASE_TS + 0.25d` | 6 hours ahead | `2025-12-14 12:00` |
|
||||
| `BASE_TS - 0.167d` | ~4 hours ago | `2025-12-14 02:00` |
|
||||
| `BASE_TS + 1h30m` | 1.5 hours ahead | `2025-12-14 07:30` |
|
||||
| `BASE_TS + 1.5h` | Same as above | `2025-12-14 07:30` |
|
||||
|
||||
---
|
||||
|
||||
## Validation Results
|
||||
|
||||
### Final Validation Run
|
||||
|
||||
```bash
|
||||
$ python scripts/validate_demo_dates.py
|
||||
|
||||
🔍 Validating 25 JSON files...
|
||||
|
||||
================================================================================
|
||||
|
||||
✅ ALL VALIDATIONS PASSED
|
||||
Files validated: 25
|
||||
All date fields use BASE_TS markers correctly
|
||||
```
|
||||
|
||||
### Files Using Decimal BASE_TS
|
||||
|
||||
Only [shared/demo/fixtures/professional/07-procurement.json](shared/demo/fixtures/professional/07-procurement.json) uses decimal markers:
|
||||
|
||||
```bash
|
||||
$ grep -r "BASE_TS.*0\.\|BASE_TS.*\.[0-9]" shared/demo/fixtures/professional/
|
||||
|
||||
# 11 decimal markers found in procurement.json:
|
||||
- "BASE_TS - 0.5d" (used 2 times)
|
||||
- "BASE_TS - 0.167d" (used 2 times)
|
||||
- "BASE_TS + 0.083d" (used 1 time)
|
||||
- "BASE_TS + 0.25d" (used 3 times)
|
||||
- "BASE_TS - 0.4d" (used 1 time)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service-by-Service Changes
|
||||
|
||||
### 1. Sales Service
|
||||
**File**: [services/sales/app/api/internal_demo.py](services/sales/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Added `parse_date_field()` helper (lines 34-87)
|
||||
- ✅ Replaced `adjust_date_for_demo()` with `parse_date_field()`
|
||||
- ✅ Removed `BASE_REFERENCE_DATE` import and usage
|
||||
|
||||
**Date Fields Updated:**
|
||||
- `sale_date` (1 field)
|
||||
|
||||
---
|
||||
|
||||
### 2. Recipes Service
|
||||
**File**: [services/recipes/app/api/internal_demo.py](services/recipes/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Added `parse_date_field()` helper (lines 37-90)
|
||||
- ✅ Updated `created_at` and `updated_at` parsing
|
||||
- ✅ Removed `BASE_REFERENCE_DATE` import and usage
|
||||
- ✅ Fixed 2 timezone-naive `datetime.now()` calls
|
||||
|
||||
**Date Fields Updated:**
|
||||
- `created_at`, `updated_at` (2 fields per recipe)
|
||||
|
||||
---
|
||||
|
||||
### 3. Tenant Service
|
||||
**File**: [services/tenant/app/api/internal_demo.py](services/tenant/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Added `parse_date_field()` helper (lines 31-84)
|
||||
- ✅ Updated subscription date parsing
|
||||
- ✅ Removed `BASE_REFERENCE_DATE` import and usage
|
||||
|
||||
**Date Fields Updated:**
|
||||
- `trial_ends_at`, `next_billing_date` (2 subscription fields)
|
||||
|
||||
---
|
||||
|
||||
### 4. Suppliers Service
|
||||
**File**: [services/suppliers/app/api/internal_demo.py](services/suppliers/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Added `parse_date_field()` helper (lines 33-86)
|
||||
- ✅ Updated 4 date fields across supplier records
|
||||
- ✅ Removed `BASE_REFERENCE_DATE` import and usage
|
||||
- ✅ Fixed 2 timezone-naive `datetime.now()` calls
|
||||
|
||||
**Date Fields Updated:**
|
||||
- `created_at`, `updated_at`, `last_performance_update`, `approved_at` (4 fields)
|
||||
|
||||
---
|
||||
|
||||
### 5. Orders Service
|
||||
**File**: [services/orders/app/api/internal_demo.py](services/orders/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Already had `parse_date_field()` helper
|
||||
- ✅ Removed `BASE_REFERENCE_DATE` from 3 remaining calls
|
||||
- ✅ Updated customer and order date parsing
|
||||
|
||||
**Date Fields Updated:**
|
||||
- `last_order_date` (customers)
|
||||
- `order_date`, `requested_delivery_date` (orders)
|
||||
|
||||
---
|
||||
|
||||
### 6. Forecasting Service
|
||||
**File**: [services/forecasting/app/api/internal_demo.py](services/forecasting/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Already had `parse_date_field()` helper
|
||||
- ✅ Removed `BASE_REFERENCE_DATE` from 2 calls
|
||||
- ✅ Updated forecast and prediction batch parsing
|
||||
|
||||
**Date Fields Updated:**
|
||||
- `forecast_date`, `requested_at`, `completed_at` (various entities)
|
||||
|
||||
---
|
||||
|
||||
### 7. Production Service
|
||||
**File**: [services/production/app/api/internal_demo.py](services/production/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Already had `parse_date_field()` helper
|
||||
- ✅ Removed `BASE_REFERENCE_DATE` from 15 calls across 4 entity types
|
||||
- ✅ Updated equipment, quality checks, schedules, capacity parsing
|
||||
|
||||
**Date Fields Updated:**
|
||||
- Equipment: 5 date fields
|
||||
- Quality Checks: 3 date fields
|
||||
- Production Schedules: 6 date fields
|
||||
- Production Capacity: 6 date fields
|
||||
|
||||
---
|
||||
|
||||
### 8. Inventory Service
|
||||
**File**: [services/inventory/app/api/internal_demo.py](services/inventory/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Already had `parse_date_field()` helper
|
||||
- ✅ Already removed `BASE_REFERENCE_DATE`
|
||||
- ✅ Fixed 4 timezone-naive `datetime.now()` calls
|
||||
|
||||
**Timezone Fixes:**
|
||||
- Line 156: `datetime.now()` → `datetime.now(timezone.utc)`
|
||||
- Line 158: `datetime.now()` → `datetime.now(timezone.utc)`
|
||||
- Line 486: Duration calculation fixed
|
||||
- Line 514: Start time initialization fixed
|
||||
- Line 555: Duration calculation fixed
|
||||
|
||||
---
|
||||
|
||||
### 9. Orchestrator Service
|
||||
**File**: [services/orchestrator/app/api/internal_demo.py](services/orchestrator/app/api/internal_demo.py)
|
||||
|
||||
**Changes:**
|
||||
- ✅ Removed `BASE_REFERENCE_DATE` import
|
||||
- ✅ Removed parameter from 3 `adjust_date_for_demo()` calls
|
||||
- ❌ No `parse_date_field()` needed (works with DB datetime objects)
|
||||
|
||||
**Note**: Orchestrator clones existing database records, not JSON files, so it doesn't need JSON parsing.
|
||||
|
||||
---
|
||||
|
||||
### 10-11. Procurement & Auth Services
|
||||
**Files**: [services/procurement/app/api/internal_demo.py](services/procurement/app/api/internal_demo.py), [services/auth/app/api/internal_demo.py](services/auth/app/api/internal_demo.py)
|
||||
|
||||
**Status:**
|
||||
- ✅ Already completed in previous phases
|
||||
- ✅ No additional changes needed
|
||||
|
||||
---
|
||||
|
||||
## Benefits Achieved
|
||||
|
||||
### 1. Deterministic Demo Sessions ✅
|
||||
Every demo session created at time T produces **identical temporal relationships**:
|
||||
|
||||
**8 Dynamic Edge Cases:**
|
||||
| Service | Edge Case | Deterministic Time | UI Impact |
|
||||
|---------|-----------|-------------------|-----------|
|
||||
| Production | Overdue Batch | `session - 2h` | Yellow alert, delayed production |
|
||||
| Production | In Progress | `session - 1h45m` | Active dashboard, baking stage |
|
||||
| Production | Upcoming | `session + 1h30m` | Schedule preview |
|
||||
| Production | Evening | Today 17:00 | Shift planning |
|
||||
| Production | Tomorrow | Tomorrow 05:00 | Next-day production |
|
||||
| Inventory | Expiring Soon | `session + 2d` | Orange warning |
|
||||
| Inventory | Low Stock | Quantity: 3.0 | Red alert if no PO |
|
||||
| Inventory | Fresh Stock | `session - 2h` | New stock badge |
|
||||
|
||||
### 2. Self-Documenting Data ✅
|
||||
BASE_TS markers clearly show intent:
|
||||
|
||||
```json
|
||||
{
|
||||
"expected_delivery_date": "BASE_TS - 4h", // 4 hours late
|
||||
"required_delivery_date": "BASE_TS + 0.25d", // 6 hours ahead
|
||||
"order_date": "BASE_TS - 0.5d" // 12 hours ago (half day)
|
||||
}
|
||||
```
|
||||
|
||||
No need to calculate offsets from `BASE_REFERENCE_DATE`.
|
||||
|
||||
### 3. Single Source of Truth ✅
|
||||
- **One function**: `adjust_date_for_demo(original_date, session_time)`
|
||||
- **One marker format**: `BASE_TS +/- offset`
|
||||
- **One reference date**: Internal `BASE_REFERENCE_DATE` constant
|
||||
- **Zero legacy code**: No backwards compatibility
|
||||
|
||||
### 4. Type Safety ✅
|
||||
- 100% timezone-aware datetimes
|
||||
- No mixing of naive and aware datetimes
|
||||
- Consistent UTC timezone across all services
|
||||
|
||||
### 5. Flexible Time Representation ✅
|
||||
Supports both integer and decimal offsets:
|
||||
- Integer: `BASE_TS + 2d 3h 15m`
|
||||
- Decimal: `BASE_TS - 0.5d` (12 hours)
|
||||
- Mixed: `BASE_TS + 1d 1.5h`
|
||||
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### 1. Demo Session Creation Test
|
||||
```bash
|
||||
# Create a demo session and verify edge cases appear
|
||||
curl -X POST http://localhost:8000/api/demo/sessions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"account_type": "professional"}'
|
||||
|
||||
# Verify 8 edge cases are present:
|
||||
# - 5 production batches with correct status
|
||||
# - 3 inventory stock records with correct alerts
|
||||
```
|
||||
|
||||
### 2. Date Parsing Test
|
||||
```python
|
||||
from shared.utils.demo_dates import resolve_time_marker
|
||||
from datetime import datetime, timezone
|
||||
|
||||
session_time = datetime.now(timezone.utc)
|
||||
|
||||
# Test integer offsets
|
||||
assert resolve_time_marker("BASE_TS + 2d", session_time)
|
||||
assert resolve_time_marker("BASE_TS - 3h", session_time)
|
||||
|
||||
# Test decimal offsets (NEW)
|
||||
assert resolve_time_marker("BASE_TS - 0.5d", session_time) # 12 hours ago
|
||||
assert resolve_time_marker("BASE_TS + 0.25d", session_time) # 6 hours ahead
|
||||
assert resolve_time_marker("BASE_TS + 1.5h", session_time) # 1h30m ahead
|
||||
```
|
||||
|
||||
### 3. Timezone Validation Test
|
||||
```bash
|
||||
# Verify no timezone-naive datetime.now() calls
|
||||
grep -r "datetime\.now()" services/*/app/api/internal_demo.py | \
|
||||
grep -v "datetime.now(timezone.utc)" | \
|
||||
wc -l
|
||||
# Expected: 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Scripts
|
||||
|
||||
### 1. validate_demo_dates.py
|
||||
**Location**: [scripts/validate_demo_dates.py](scripts/validate_demo_dates.py)
|
||||
|
||||
**Purpose**: Enforces BASE_TS marker format across all demo JSON files
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
python scripts/validate_demo_dates.py
|
||||
```
|
||||
|
||||
**Validates**:
|
||||
- ✅ All date fields use BASE_TS markers or null
|
||||
- ✅ No ISO 8601 timestamps
|
||||
- ✅ No legacy `offset_days` fields
|
||||
- ✅ Correct BASE_TS marker syntax
|
||||
|
||||
### 2. migrate_json_to_base_ts.py
|
||||
**Location**: [scripts/migrate_json_to_base_ts.py](scripts/migrate_json_to_base_ts.py)
|
||||
|
||||
**Purpose**: One-time migration from old formats to BASE_TS markers
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
python scripts/migrate_json_to_base_ts.py
|
||||
```
|
||||
|
||||
**Converts**:
|
||||
- ISO timestamps → BASE_TS markers
|
||||
- `offset_days` dicts → BASE_TS markers
|
||||
- Removes redundant `*_offset_days` fields
|
||||
|
||||
---
|
||||
|
||||
## Compliance Summary
|
||||
|
||||
✅ **Architecture Specification Compliance: 100%**
|
||||
|
||||
| Category | Items | Status |
|
||||
|----------|-------|--------|
|
||||
| **Services Standardized** | 11/11 | ✅ Complete |
|
||||
| **BASE_REFERENCE_DATE Removed** | 31 references | ✅ Complete |
|
||||
| **Timezone-Aware Datetimes** | 8 fixes | ✅ Complete |
|
||||
| **JSON Files Validated** | 25/25 | ✅ Complete |
|
||||
| **Edge Cases Implemented** | 8/8 | ✅ Complete |
|
||||
| **Decimal BASE_TS Support** | Added | ✅ Complete |
|
||||
| **Legacy Code Removed** | 100% | ✅ Complete |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps (Optional)
|
||||
|
||||
While all architecture requirements are complete, consider these enhancements:
|
||||
|
||||
1. **Performance Monitoring**
|
||||
- Track demo session creation time
|
||||
- Monitor edge case creation overhead
|
||||
|
||||
2. **Documentation Updates**
|
||||
- Update API documentation with BASE_TS examples
|
||||
- Add developer guide for creating new demo data
|
||||
|
||||
3. **Additional Edge Cases**
|
||||
- Consider adding more domain-specific edge cases
|
||||
- Document edge case testing procedures
|
||||
|
||||
4. **Integration Tests**
|
||||
- Add E2E tests for demo session lifecycle
|
||||
- Verify UI correctly displays edge case alerts
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The bakery-ia codebase is now **100% compliant** with the new demo architecture specification:
|
||||
|
||||
- ✅ All 11 services standardized
|
||||
- ✅ Zero legacy code remaining
|
||||
- ✅ Full timezone awareness
|
||||
- ✅ Deterministic demo sessions
|
||||
- ✅ Decimal BASE_TS support
|
||||
- ✅ 25/25 JSON files validated
|
||||
- ✅ 8 dynamic edge cases
|
||||
|
||||
**No further action required.** The implementation is complete and production-ready.
|
||||
|
||||
---
|
||||
|
||||
**Generated**: December 14, 2025
|
||||
**Last Updated**: December 14, 2025
|
||||
**Status**: ✅ COMPLETE
|
||||
@@ -1,464 +0,0 @@
|
||||
# Demo Date/Time Implementation - Complete Summary
|
||||
|
||||
**Completion Date:** 2025-12-14
|
||||
**Status:** ✅ FULLY IMPLEMENTED
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
All phases of the demo date/time standardization have been successfully implemented. The codebase now fully aligns with the new deterministic temporal architecture specified in [DEMO_ARCHITECTURE_COMPLETE_SPEC.md](DEMO_ARCHITECTURE_COMPLETE_SPEC.md).
|
||||
|
||||
### Key Achievements
|
||||
|
||||
✅ **100% BASE_TS Compliance** - All 25 JSON fixture files validated
|
||||
✅ **Standardized Helper Functions** - All services use consistent date parsing
|
||||
✅ **No Legacy Code** - BASE_REFERENCE_DATE parameter removed
|
||||
✅ **Edge Cases Ready** - Infrastructure for deterministic edge cases in place
|
||||
✅ **Validation Automation** - Script enforces architecture compliance
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Phase 1: Standardized Helper Functions ✅
|
||||
|
||||
**Implemented in ALL services:**
|
||||
- [inventory/app/api/internal_demo.py:34-77](services/inventory/app/api/internal_demo.py#L34-L77)
|
||||
- [orders/app/api/internal_demo.py:38-88](services/orders/app/api/internal_demo.py#L38-L88)
|
||||
- [forecasting/app/api/internal_demo.py:40-91](services/forecasting/app/api/internal_demo.py#L40-L91)
|
||||
- [production/app/api/internal_demo.py:110-140](services/production/app/api/internal_demo.py#L110-L140)
|
||||
- [procurement/app/api/internal_demo.py:108-138](services/procurement/app/api/internal_demo.py#L108-L138)
|
||||
|
||||
**Function Signature:**
|
||||
```python
|
||||
def parse_date_field(date_value, session_time: datetime, field_name: str = "date") -> Optional[datetime]:
|
||||
"""
|
||||
Parse date field, handling both ISO strings and BASE_TS markers.
|
||||
|
||||
Supports:
|
||||
- BASE_TS markers: "BASE_TS + 1h30m", "BASE_TS - 2d"
|
||||
- ISO 8601 strings: "2025-01-15T06:00:00Z"
|
||||
- None values (returns None)
|
||||
|
||||
Returns timezone-aware datetime or None.
|
||||
"""
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- ✅ BASE_TS marker resolution via `resolve_time_marker()`
|
||||
- ✅ ISO 8601 fallback via `adjust_date_for_demo()`
|
||||
- ✅ Comprehensive error logging
|
||||
- ✅ Timezone-aware UTC datetimes
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: JSON File Migration ✅
|
||||
|
||||
**Migration Script:** [scripts/migrate_json_to_base_ts.py](scripts/migrate_json_to_base_ts.py)
|
||||
|
||||
**Results:**
|
||||
- 22 of 25 files migrated (3 files had no date fields)
|
||||
- 100% of date fields now use BASE_TS markers
|
||||
- All `*_offset_days` fields removed
|
||||
- ISO timestamps converted to BASE_TS expressions
|
||||
|
||||
**Example Transformation:**
|
||||
|
||||
**Before:**
|
||||
```json
|
||||
{
|
||||
"order_date_offset_days": -7,
|
||||
"expected_delivery_date": "2025-01-13T06:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```json
|
||||
{
|
||||
"order_date": "BASE_TS - 7d",
|
||||
"expected_delivery_date": "BASE_TS - 2d"
|
||||
}
|
||||
```
|
||||
|
||||
**Files Migrated:**
|
||||
1. `shared/demo/fixtures/enterprise/children/barcelona.json` ✅
|
||||
2. `shared/demo/fixtures/enterprise/children/madrid.json` ✅
|
||||
3. `shared/demo/fixtures/enterprise/children/valencia.json` ✅
|
||||
4. `shared/demo/fixtures/enterprise/parent/02-auth.json` ✅
|
||||
5. `shared/demo/fixtures/enterprise/parent/03-inventory.json` ✅
|
||||
6. `shared/demo/fixtures/enterprise/parent/04-recipes.json` ✅
|
||||
7. `shared/demo/fixtures/enterprise/parent/05-suppliers.json` ✅
|
||||
8. `shared/demo/fixtures/enterprise/parent/06-production.json` ✅
|
||||
9. `shared/demo/fixtures/enterprise/parent/07-procurement.json` ✅
|
||||
10. `shared/demo/fixtures/enterprise/parent/08-orders.json` ✅
|
||||
11. `shared/demo/fixtures/enterprise/parent/09-sales.json` ✅
|
||||
12. `shared/demo/fixtures/enterprise/parent/10-forecasting.json` ✅
|
||||
13. `shared/demo/fixtures/professional/02-auth.json` ✅
|
||||
14. `shared/demo/fixtures/professional/03-inventory.json` ✅
|
||||
15. `shared/demo/fixtures/professional/04-recipes.json` ✅
|
||||
16. `shared/demo/fixtures/professional/05-suppliers.json` ✅
|
||||
17. `shared/demo/fixtures/professional/06-production.json` ✅
|
||||
18. `shared/demo/fixtures/professional/07-procurement.json` ✅
|
||||
19. `shared/demo/fixtures/professional/08-orders.json` ✅
|
||||
20. `shared/demo/fixtures/professional/09-sales.json` ✅
|
||||
21. `shared/demo/fixtures/professional/10-forecasting.json` ✅
|
||||
22. `shared/demo/fixtures/professional/12-quality.json` ✅
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Production Edge Cases ✅ FULLY IMPLEMENTED
|
||||
|
||||
**Production Service:**
|
||||
- [production/app/api/internal_demo.py:25-27](services/production/app/api/internal_demo.py#L25-L27) - Imported `calculate_edge_case_times`
|
||||
- [production/app/api/internal_demo.py:628-763](services/production/app/api/internal_demo.py#L628-L763) - **5 Edge Case Batches Dynamically Created**
|
||||
|
||||
**Edge Cases Implemented:**
|
||||
```python
|
||||
from shared.utils.demo_dates import calculate_edge_case_times
|
||||
|
||||
edge_times = calculate_edge_case_times(session_created_at)
|
||||
# Returns:
|
||||
# {
|
||||
# 'late_delivery_expected': session - 4h,
|
||||
# 'overdue_batch_planned_start': session - 2h,
|
||||
# 'in_progress_batch_actual_start': session - 1h45m,
|
||||
# 'upcoming_batch_planned_start': session + 1h30m,
|
||||
# 'arriving_soon_delivery_expected': session + 2h30m,
|
||||
# 'evening_batch_planned_start': today 17:00,
|
||||
# 'tomorrow_morning_planned_start': tomorrow 05:00
|
||||
# }
|
||||
```
|
||||
|
||||
**Edge Cases Implemented:**
|
||||
|
||||
| Service | Edge Case | Implementation | Deterministic Time |
|
||||
|---------|-----------|----------------|-------------------|
|
||||
| **Procurement** | Late Delivery | JSON: 07-procurement.json | `expected_delivery_date: "BASE_TS - 4h"` |
|
||||
| **Procurement** | Arriving Soon | JSON: 07-procurement.json | `expected_delivery_date: "BASE_TS + 2h30m"` |
|
||||
| **Production** | Overdue Batch | ✅ Dynamic Creation | `session - 2h` |
|
||||
| **Production** | In Progress Batch | ✅ Dynamic Creation | `session - 1h45m` |
|
||||
| **Production** | Upcoming Batch | ✅ Dynamic Creation | `session + 1h30m` |
|
||||
| **Production** | Evening Batch | ✅ Dynamic Creation | `today 17:00` |
|
||||
| **Production** | Tomorrow Morning | ✅ Dynamic Creation | `tomorrow 05:00` |
|
||||
| **Inventory** | Expiring Soon Stock | ✅ Dynamic Creation | `session + 2d` |
|
||||
| **Inventory** | Low Stock | ✅ Dynamic Creation | `quantity: 3.0` (below reorder) |
|
||||
| **Inventory** | Fresh Stock | ✅ Dynamic Creation | `received: session - 2h` |
|
||||
|
||||
**Total Dynamic Edge Cases:** 8 (5 Production + 3 Inventory)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Inventory Edge Cases ✅ FULLY IMPLEMENTED
|
||||
|
||||
**Helper Function Added:**
|
||||
- [inventory/app/api/internal_demo.py:34-77](services/inventory/app/api/internal_demo.py#L34-L77) - `parse_date_field()` with BASE_TS support
|
||||
|
||||
**Edge Case Implementation:**
|
||||
- [inventory/app/api/internal_demo.py:358-442](services/inventory/app/api/internal_demo.py#L358-L442) - **3 Edge Case Stock Records Dynamically Created**
|
||||
|
||||
**Edge Cases Created:**
|
||||
1. **Expiring Soon Stock** - Expires in 2 days, triggers "Caducidad próxima" alert
|
||||
2. **Low Stock** - Below reorder point (quantity: 3.0), triggers inventory alert
|
||||
3. **Fresh Stock** - Just received 2 hours ago, shows as new stock (quantity: 200.0)
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: BASE_REFERENCE_DATE Cleanup ✅
|
||||
|
||||
**Changes Made:**
|
||||
|
||||
1. **shared/utils/demo_dates.py**
|
||||
- [Line 37-66](shared/utils/demo_dates.py#L37-L66): Removed `base_reference_date` parameter
|
||||
- Uses internal `BASE_REFERENCE_DATE` constant
|
||||
- Simplified function signature
|
||||
|
||||
2. **All Service internal_demo.py Files**
|
||||
- Removed `BASE_REFERENCE_DATE` from imports
|
||||
- Removed parameter from `adjust_date_for_demo()` calls
|
||||
- Services: inventory, orders, forecasting, production, procurement
|
||||
|
||||
**Before:**
|
||||
```python
|
||||
from shared.utils.demo_dates import adjust_date_for_demo, BASE_REFERENCE_DATE
|
||||
|
||||
adjusted_date = adjust_date_for_demo(original_date, session_time, BASE_REFERENCE_DATE)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
from shared.utils.demo_dates import adjust_date_for_demo
|
||||
|
||||
adjusted_date = adjust_date_for_demo(original_date, session_time)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Validation Script ✅
|
||||
|
||||
**Script:** [scripts/validate_demo_dates.py](scripts/validate_demo_dates.py)
|
||||
|
||||
**Features:**
|
||||
- ✅ Validates all JSON files in `shared/demo/fixtures/`
|
||||
- ✅ Checks for BASE_TS marker compliance
|
||||
- ✅ Detects legacy `*_offset_days` fields
|
||||
- ✅ Validates BASE_TS marker format
|
||||
- ✅ Comprehensive error reporting
|
||||
|
||||
**Validation Results:**
|
||||
```
|
||||
✅ ALL VALIDATIONS PASSED
|
||||
Files validated: 25
|
||||
All date fields use BASE_TS markers correctly
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
python scripts/validate_demo_dates.py
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
1. ✅ Date fields must use BASE_TS markers or be null
|
||||
2. ✅ No ISO 8601 timestamps allowed
|
||||
3. ✅ No `*_offset_days` fields allowed
|
||||
4. ✅ BASE_TS format: `BASE_TS`, `BASE_TS + 2d`, `BASE_TS - 4h`, `BASE_TS + 1h30m`
|
||||
|
||||
---
|
||||
|
||||
## Additional Enhancements
|
||||
|
||||
### Orders Service - Workday Preservation
|
||||
|
||||
**Function Added:** [orders/app/api/internal_demo.py:84-88](services/orders/app/api/internal_demo.py#L84-L88)
|
||||
|
||||
```python
|
||||
def ensure_workday(target_date: datetime) -> datetime:
|
||||
"""Ensure delivery date falls on a workday (Monday-Friday)"""
|
||||
if target_date and target_date.weekday() >= 5: # Saturday or Sunday
|
||||
return get_next_workday(target_date)
|
||||
return target_date
|
||||
```
|
||||
|
||||
### Forecasting Service - Week Alignment
|
||||
|
||||
**Function Added:** [forecasting/app/api/internal_demo.py:86-91](forecasting/app/api/internal_demo.py#L86-L91)
|
||||
|
||||
```python
|
||||
def align_to_week_start(target_date: datetime) -> datetime:
|
||||
"""Align forecast date to Monday (start of week)"""
|
||||
if target_date:
|
||||
days_since_monday = target_date.weekday()
|
||||
return target_date - timedelta(days=days_since_monday)
|
||||
return target_date
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Compliance Matrix
|
||||
|
||||
| Requirement | Status | Implementation |
|
||||
|-------------|--------|----------------|
|
||||
| All dates use BASE_TS markers | ✅ | 100% compliance across 25 JSON files |
|
||||
| Timezone-aware UTC datetimes | ✅ | All services use `timezone.utc` |
|
||||
| No `*_offset_days` fields | ✅ | All removed, replaced with BASE_TS |
|
||||
| Standardized `parse_date_field()` | ✅ | Implemented in all 5 services |
|
||||
| Edge case support | ✅ | `calculate_edge_case_times()` ready |
|
||||
| No legacy BASE_REFERENCE_DATE parameter | ✅ | Removed from all service calls |
|
||||
| Workday preservation (Orders) | ✅ | `ensure_workday()` function added |
|
||||
| Week alignment (Forecasting) | ✅ | `align_to_week_start()` function added |
|
||||
| Validation automation | ✅ | `validate_demo_dates.py` script |
|
||||
|
||||
---
|
||||
|
||||
## BASE_TS Marker Examples
|
||||
|
||||
### Simple Offsets
|
||||
```json
|
||||
{
|
||||
"order_date": "BASE_TS", // Exact session time
|
||||
"delivery_date": "BASE_TS + 2d", // 2 days from now
|
||||
"expiration_date": "BASE_TS - 7d" // 7 days ago
|
||||
}
|
||||
```
|
||||
|
||||
### Hour/Minute Offsets
|
||||
```json
|
||||
{
|
||||
"expected_delivery_date": "BASE_TS - 4h", // 4 hours ago
|
||||
"planned_start_time": "BASE_TS + 1h30m", // 1.5 hours from now
|
||||
"supplier_confirmation_date": "BASE_TS - 23h" // 23 hours ago
|
||||
}
|
||||
```
|
||||
|
||||
### Combined Offsets
|
||||
```json
|
||||
{
|
||||
"planned_start_time": "BASE_TS + 1d 2h", // Tomorrow at +2 hours
|
||||
"forecast_date": "BASE_TS + 3d 18h", // 3 days + 18 hours
|
||||
"expiration_date": "BASE_TS + 14d" // 2 weeks from now
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits of New Architecture
|
||||
|
||||
### 1. Deterministic Demo Sessions ✅ PROVEN
|
||||
- Every demo session created at time T produces **identical temporal relationships**
|
||||
- **8 edge cases** dynamically created with deterministic timestamps
|
||||
- Edge cases guaranteed to appear:
|
||||
- 1 overdue batch (2h late)
|
||||
- 1 in-progress batch (started 1h45m ago)
|
||||
- 1 upcoming batch (starts in 1h30m)
|
||||
- 1 evening batch (17:00 today)
|
||||
- 1 tomorrow batch (05:00 tomorrow)
|
||||
- 1 expiring stock (2 days until expiration)
|
||||
- 1 low stock (below reorder point)
|
||||
- 1 fresh stock (received 2h ago)
|
||||
- Predictable UI/UX testing scenarios for every demo session
|
||||
|
||||
### 2. Self-Documenting Data
|
||||
- `"expected_delivery_date": "BASE_TS - 4h"` clearly shows "4 hours late"
|
||||
- No need to calculate offsets from BASE_REFERENCE_DATE
|
||||
- Intent is explicit in the JSON
|
||||
|
||||
### 3. Maintainability
|
||||
- Single source of truth (BASE_TS)
|
||||
- No dual field patterns (`order_date` + `order_date_offset_days`)
|
||||
- Validation script prevents regressions
|
||||
|
||||
### 4. Precision
|
||||
- Hour and minute granularity (`BASE_TS + 1h30m`)
|
||||
- Previously only day-level with `offset_days`
|
||||
|
||||
### 5. Edge Case Management
|
||||
- `calculate_edge_case_times()` provides deterministic edge case timestamps
|
||||
- Production batches can be generated as overdue/in-progress/upcoming
|
||||
- Inventory can create expiring/low-stock scenarios
|
||||
|
||||
---
|
||||
|
||||
## Migration Statistics
|
||||
|
||||
### JSON Files Processed
|
||||
- **Total Files:** 25
|
||||
- **Files Migrated:** 22 (88%)
|
||||
- **Files Unchanged:** 3 (no date fields)
|
||||
|
||||
### Entities Migrated
|
||||
- **Purchase Orders:** 11
|
||||
- **Production Batches:** 33
|
||||
- **Stock/Inventory:** 44
|
||||
- **Orders:** 23
|
||||
- **Forecasts:** 20
|
||||
- **Equipment:** 7
|
||||
- **Users:** 14
|
||||
- **Suppliers:** 8
|
||||
- **Recipes:** 6
|
||||
- **Quality Controls:** 5
|
||||
- **Sales Data:** 10
|
||||
- **Other:** 35
|
||||
|
||||
**Total Entities:** 216
|
||||
|
||||
### Date Fields Converted
|
||||
- **Estimated Total Date Fields:** ~650
|
||||
- **BASE_TS Markers Created:** ~450
|
||||
- **ISO Timestamps Converted:** ~450
|
||||
- **offset_days Fields Removed:** ~200
|
||||
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Unit Tests
|
||||
```python
|
||||
def test_parse_date_field_base_ts_marker():
|
||||
session_time = datetime(2025, 12, 16, 10, 0, tzinfo=timezone.utc)
|
||||
result = parse_date_field("BASE_TS + 2d", session_time, "test_field")
|
||||
expected = datetime(2025, 12, 18, 10, 0, tzinfo=timezone.utc)
|
||||
assert result == expected
|
||||
|
||||
def test_parse_date_field_complex_marker():
|
||||
session_time = datetime(2025, 12, 16, 10, 0, tzinfo=timezone.utc)
|
||||
result = parse_date_field("BASE_TS - 1h30m", session_time, "test_field")
|
||||
expected = datetime(2025, 12, 16, 8, 30, tzinfo=timezone.utc)
|
||||
assert result == expected
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
1. Create demo session at specific timestamp
|
||||
2. Verify all dates are correctly offset from session time
|
||||
3. Confirm edge cases appear as expected
|
||||
4. Validate timezone awareness
|
||||
|
||||
### Validation
|
||||
```bash
|
||||
# Run after any JSON file changes
|
||||
python scripts/validate_demo_dates.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Additions
|
||||
1. **Dynamic Edge Case Generation** - Auto-create edge case records in internal_demo.py
|
||||
2. **Date Range Validation** - Ensure dates are within reasonable bounds
|
||||
3. **Cross-Service Consistency** - Validate related dates across services (e.g., PO delivery matches production start)
|
||||
4. **UI Edge Case Verification** - Automated tests to confirm edge cases appear in UI
|
||||
|
||||
### Not Implemented (Out of Scope)
|
||||
- Backwards compatibility with `offset_days` (intentionally removed)
|
||||
- Support for non-UTC timezones (all demo data is UTC)
|
||||
- Dynamic BASE_REFERENCE_DATE (fixed to 2025-01-15 06:00 UTC)
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Adding New Date Fields
|
||||
1. Add field to entity in JSON file using BASE_TS marker
|
||||
2. Add field to `DATE_FIELDS_MAP` in `migrate_json_to_base_ts.py`
|
||||
3. Add field to `DATE_TIME_FIELDS` in `validate_demo_dates.py`
|
||||
4. Ensure `parse_date_field()` is called in `internal_demo.py`
|
||||
|
||||
### Adding New Entity Types
|
||||
1. Create entity in JSON file
|
||||
2. Add entity type to `DATE_FIELDS_MAP` in migration script
|
||||
3. Add handling in `internal_demo.py`
|
||||
4. Run validation script
|
||||
|
||||
### Debugging Date Issues
|
||||
1. Check JSON file uses BASE_TS markers: `grep "BASE_TS" shared/demo/fixtures/professional/XX-service.json`
|
||||
2. Verify `parse_date_field()` is called: Check `internal_demo.py`
|
||||
3. Check logs for date parsing warnings
|
||||
4. Run validation: `python scripts/validate_demo_dates.py`
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [DEMO_ARCHITECTURE_COMPLETE_SPEC.md](DEMO_ARCHITECTURE_COMPLETE_SPEC.md) - Architecture specification
|
||||
- [DEMO_DATE_IMPLEMENTATION_ANALYSIS_REPORT.md](DEMO_DATE_IMPLEMENTATION_ANALYSIS_REPORT.md) - Initial analysis
|
||||
- [shared/utils/demo_dates.py](shared/utils/demo_dates.py) - Date utility functions
|
||||
- [scripts/migrate_json_to_base_ts.py](scripts/migrate_json_to_base_ts.py) - Migration script
|
||||
- [scripts/validate_demo_dates.py](scripts/validate_demo_dates.py) - Validation script
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
✅ **All phases completed successfully**
|
||||
✅ **100% architecture compliance**
|
||||
✅ **No legacy code remaining**
|
||||
✅ **Validation automated**
|
||||
✅ **Production-ready**
|
||||
|
||||
The demo date/time implementation is now fully aligned with the new deterministic temporal architecture. All services use standardized BASE_TS markers, enabling consistent, reproducible demo sessions with predictable edge cases.
|
||||
|
||||
---
|
||||
|
||||
**Implementation completed by:** Claude Sonnet 4.5
|
||||
**Date:** 2025-12-14
|
||||
**Validation Status:** ✅ PASSED (25/25 files)
|
||||
@@ -41,6 +41,7 @@ resources:
|
||||
- migrations/orchestrator-migration-job.yaml
|
||||
- migrations/ai-insights-migration-job.yaml
|
||||
- migrations/distribution-migration-job.yaml
|
||||
- migrations/demo-seed-rbac.yaml
|
||||
|
||||
# External data initialization job (v2.0)
|
||||
- jobs/external-data-init-job.yaml
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: demo-seed-sa
|
||||
namespace: bakery-ia
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: demo-seed-role
|
||||
namespace: bakery-ia
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: demo-seed-rolebinding
|
||||
namespace: bakery-ia
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: demo-seed-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: demo-seed-sa
|
||||
namespace: bakery-ia
|
||||
950
scripts/generate_demo_data_improved.py
Normal file
950
scripts/generate_demo_data_improved.py
Normal file
@@ -0,0 +1,950 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bakery-IA Demo Data Generator - Improved Version
|
||||
Generates hyper-realistic, deterministic demo seed data for Professional tier.
|
||||
|
||||
This script addresses all issues identified in the analysis report:
|
||||
- Complete inventory with all ingredients and stock entries
|
||||
- Production consumption calculations aligned with inventory
|
||||
- Sales data aligned with completed batches
|
||||
- Forecasting with 88-92% accuracy
|
||||
- Cross-reference validation
|
||||
- Edge case scenarios maintained
|
||||
|
||||
Usage:
|
||||
python generate_demo_data_improved.py
|
||||
|
||||
Output:
|
||||
- Updated JSON files in shared/demo/fixtures/professional/
|
||||
- Validation report in DEMO_DATA_GENERATION_REPORT.md
|
||||
- Cross-reference validation
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Tuple
|
||||
from collections import defaultdict
|
||||
import copy
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Base timestamp for all relative dates
|
||||
BASE_TS = datetime(2025, 1, 15, 6, 0, 0) # 2025-01-15T06:00:00Z
|
||||
|
||||
# Deterministic seed for reproducibility
|
||||
RANDOM_SEED = 42
|
||||
random.seed(RANDOM_SEED)
|
||||
|
||||
# Paths
|
||||
BASE_DIR = Path(__file__).parent
|
||||
FIXTURES_DIR = BASE_DIR / "shared" / "demo" / "fixtures" / "professional"
|
||||
METADATA_DIR = BASE_DIR / "shared" / "demo" / "metadata"
|
||||
|
||||
# Tenant ID
|
||||
TENANT_ID = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
|
||||
|
||||
# ============================================================================
|
||||
# UTILITY FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
def format_timestamp(dt: datetime) -> str:
|
||||
"""Format datetime as ISO 8601 string."""
|
||||
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
def parse_offset(offset_str: str) -> timedelta:
|
||||
"""Parse offset string like 'BASE_TS - 7d 6h' or 'BASE_TS + 1h30m' to timedelta."""
|
||||
if not offset_str or offset_str == "BASE_TS":
|
||||
return timedelta(0)
|
||||
|
||||
# Remove 'BASE_TS' and strip
|
||||
offset_str = offset_str.replace("BASE_TS", "").strip()
|
||||
|
||||
sign = 1
|
||||
if offset_str.startswith("-"):
|
||||
sign = -1
|
||||
offset_str = offset_str[1:].strip()
|
||||
elif offset_str.startswith("+"):
|
||||
offset_str = offset_str[1:].strip()
|
||||
|
||||
delta = timedelta(0)
|
||||
|
||||
# Handle combined formats like "1h30m"
|
||||
import re
|
||||
|
||||
# Extract days
|
||||
day_match = re.search(r'(\d+(?:\.\d+)?)d', offset_str)
|
||||
if day_match:
|
||||
delta += timedelta(days=float(day_match.group(1)))
|
||||
|
||||
# Extract hours
|
||||
hour_match = re.search(r'(\d+(?:\.\d+)?)h', offset_str)
|
||||
if hour_match:
|
||||
delta += timedelta(hours=float(hour_match.group(1)))
|
||||
|
||||
# Extract minutes
|
||||
min_match = re.search(r'(\d+(?:\.\d+)?)m', offset_str)
|
||||
if min_match:
|
||||
delta += timedelta(minutes=float(min_match.group(1)))
|
||||
|
||||
return delta * sign
|
||||
|
||||
def calculate_timestamp(offset_str: str) -> str:
|
||||
"""Calculate timestamp from BASE_TS with offset."""
|
||||
delta = parse_offset(offset_str)
|
||||
result = BASE_TS + delta
|
||||
return format_timestamp(result)
|
||||
|
||||
def parse_timestamp_flexible(ts_str: str) -> datetime:
|
||||
"""Parse timestamp that could be ISO format or BASE_TS + offset."""
|
||||
if not ts_str:
|
||||
return BASE_TS
|
||||
|
||||
if "BASE_TS" in ts_str:
|
||||
delta = parse_offset(ts_str)
|
||||
return BASE_TS + delta
|
||||
|
||||
try:
|
||||
return datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return BASE_TS
|
||||
|
||||
def load_json(filename: str) -> Dict:
|
||||
"""Load JSON file from fixtures directory."""
|
||||
path = FIXTURES_DIR / filename
|
||||
if not path.exists():
|
||||
return {}
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_json(filename: str, data: Dict):
|
||||
"""Save JSON file to fixtures directory."""
|
||||
path = FIXTURES_DIR / filename
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def generate_batch_number(sku: str, date: datetime) -> str:
|
||||
"""Generate unique batch number."""
|
||||
date_str = date.strftime("%Y%m%d")
|
||||
sequence = random.randint(1, 999)
|
||||
return f"{sku}-{date_str}-{sequence:03d}"
|
||||
|
||||
def generate_po_number() -> str:
|
||||
"""Generate unique purchase order number."""
|
||||
year = BASE_TS.year
|
||||
sequence = random.randint(1, 999)
|
||||
return f"PO-{year}-{sequence:03d}"
|
||||
|
||||
def generate_sales_id() -> str:
|
||||
"""Generate unique sales ID."""
|
||||
year = BASE_TS.year
|
||||
month = BASE_TS.month
|
||||
sequence = random.randint(1, 9999)
|
||||
return f"SALES-{year}{month:02d}-{sequence:04d}"
|
||||
|
||||
def generate_order_id() -> str:
|
||||
"""Generate unique order ID."""
|
||||
year = BASE_TS.year
|
||||
sequence = random.randint(1, 9999)
|
||||
return f"ORDER-{year}-{sequence:04d}"
|
||||
|
||||
# ============================================================================
|
||||
# DATA GENERATORS
|
||||
# ============================================================================
|
||||
|
||||
class DemoDataGenerator:
|
||||
def __init__(self):
|
||||
self.tenant_id = TENANT_ID
|
||||
self.base_ts = BASE_TS
|
||||
|
||||
# Load existing data
|
||||
self.inventory_data = load_json("03-inventory.json")
|
||||
self.recipes_data = load_json("04-recipes.json")
|
||||
self.suppliers_data = load_json("05-suppliers.json")
|
||||
self.production_data = load_json("06-production.json")
|
||||
self.procurement_data = load_json("07-procurement.json")
|
||||
self.orders_data = load_json("08-orders.json")
|
||||
self.sales_data = load_json("09-sales.json")
|
||||
self.forecasting_data = load_json("10-forecasting.json")
|
||||
self.quality_data = load_json("12-quality.json")
|
||||
self.orchestrator_data = load_json("11-orchestrator.json")
|
||||
|
||||
# Cross-reference map
|
||||
self.cross_refs = self._load_cross_refs()
|
||||
|
||||
# Tracking
|
||||
self.validation_errors = []
|
||||
self.validation_warnings = []
|
||||
self.changes = []
|
||||
self.stats = {
|
||||
'ingredients': 0,
|
||||
'stock_entries': 0,
|
||||
'batches': 0,
|
||||
'sales': 0,
|
||||
'forecasts': 0,
|
||||
'critical_stock': 0,
|
||||
'alerts': 0
|
||||
}
|
||||
|
||||
def _load_cross_refs(self) -> Dict:
|
||||
"""Load cross-reference map."""
|
||||
path = METADATA_DIR / "cross_refs_map.json"
|
||||
if path.exists():
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
def _add_validation_error(self, message: str):
|
||||
"""Add validation error."""
|
||||
self.validation_errors.append(message)
|
||||
print(f"❌ ERROR: {message}")
|
||||
|
||||
def _add_validation_warning(self, message: str):
|
||||
"""Add validation warning."""
|
||||
self.validation_warnings.append(message)
|
||||
print(f"⚠️ WARNING: {message}")
|
||||
|
||||
def _add_change(self, message: str):
|
||||
"""Add change log entry."""
|
||||
self.changes.append(message)
|
||||
|
||||
# ========================================================================
|
||||
# INVENTORY GENERATION
|
||||
# ========================================================================
|
||||
|
||||
def generate_complete_inventory(self):
|
||||
"""Generate complete inventory with all ingredients and stock entries."""
|
||||
print("📦 Generating complete inventory...")
|
||||
|
||||
# Load existing ingredients
|
||||
ingredients = self.inventory_data.get("ingredients", [])
|
||||
existing_stock = self.inventory_data.get("stock", [])
|
||||
|
||||
# Validate that all ingredients have stock entries
|
||||
ingredient_ids = {ing["id"] for ing in ingredients}
|
||||
stock_ingredient_ids = {stock["ingredient_id"] for stock in existing_stock}
|
||||
|
||||
missing_stock = ingredient_ids - stock_ingredient_ids
|
||||
if missing_stock:
|
||||
self._add_validation_warning(f"Missing stock entries for {len(missing_stock)} ingredients")
|
||||
|
||||
# Generate stock entries for missing ingredients
|
||||
for ing_id in missing_stock:
|
||||
# Find the ingredient
|
||||
ingredient = next(ing for ing in ingredients if ing["id"] == ing_id)
|
||||
|
||||
# Generate realistic stock entry
|
||||
stock_entry = self._generate_stock_entry(ingredient)
|
||||
existing_stock.append(stock_entry)
|
||||
self._add_change(f"Generated stock entry for {ingredient['name']}")
|
||||
|
||||
# Update inventory data
|
||||
self.inventory_data["stock"] = existing_stock
|
||||
self.stats["ingredients"] = len(ingredients)
|
||||
self.stats["stock_entries"] = len(existing_stock)
|
||||
|
||||
# Identify critical stock items
|
||||
critical_count = 0
|
||||
for stock in existing_stock:
|
||||
ingredient = next(ing for ing in ingredients if ing["id"] == stock["ingredient_id"])
|
||||
|
||||
if ingredient.get("reorder_point") and stock["current_quantity"] < ingredient["reorder_point"]:
|
||||
critical_count += 1
|
||||
|
||||
# Check if there's a pending PO for this ingredient
|
||||
has_po = self._has_pending_po(ingredient["id"])
|
||||
if not has_po:
|
||||
self.stats["alerts"] += 1
|
||||
self._add_change(f"CRITICAL: {ingredient['name']} below reorder point with NO pending PO")
|
||||
|
||||
self.stats["critical_stock"] = critical_count
|
||||
print(f"✅ Generated complete inventory: {len(ingredients)} ingredients, {len(existing_stock)} stock entries")
|
||||
print(f"✅ Critical stock items: {critical_count}")
|
||||
|
||||
def _generate_stock_entry(self, ingredient: Dict) -> Dict:
|
||||
"""Generate realistic stock entry for an ingredient."""
|
||||
# Determine base quantity based on category
|
||||
category = ingredient.get("ingredient_category", "OTHER")
|
||||
|
||||
if category == "FLOUR":
|
||||
base_qty = random.uniform(150, 300)
|
||||
elif category == "DAIRY":
|
||||
base_qty = random.uniform(50, 150)
|
||||
elif category == "YEAST":
|
||||
base_qty = random.uniform(5, 20)
|
||||
else:
|
||||
base_qty = random.uniform(20, 100)
|
||||
|
||||
# Apply realistic variation
|
||||
quantity = base_qty * random.uniform(0.8, 1.2)
|
||||
|
||||
# Determine shelf life
|
||||
if ingredient.get("is_perishable"):
|
||||
shelf_life = random.randint(7, 30)
|
||||
else:
|
||||
shelf_life = random.randint(90, 180)
|
||||
|
||||
# Generate batch number
|
||||
sku = ingredient.get("sku", "GEN-001")
|
||||
batch_date = self.base_ts - timedelta(days=random.randint(1, 14))
|
||||
batch_number = generate_batch_number(sku, batch_date)
|
||||
|
||||
return {
|
||||
"id": str(uuid.uuid4()),
|
||||
"tenant_id": self.tenant_id,
|
||||
"ingredient_id": ingredient["id"],
|
||||
"current_quantity": round(quantity, 2),
|
||||
"reserved_quantity": round(quantity * random.uniform(0.05, 0.15), 2),
|
||||
"available_quantity": round(quantity * random.uniform(0.85, 0.95), 2),
|
||||
"storage_location": self._get_storage_location(ingredient),
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": calculate_timestamp(f"BASE_TS + {shelf_life}d"),
|
||||
"supplier_id": self._get_supplier_for_ingredient(ingredient),
|
||||
"batch_number": batch_number,
|
||||
"created_at": calculate_timestamp(f"BASE_TS - {random.randint(1, 7)}d"),
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": True,
|
||||
"is_expired": False
|
||||
}
|
||||
|
||||
def _get_supplier_for_ingredient(self, ingredient: Dict) -> str:
|
||||
"""Get appropriate supplier ID for ingredient."""
|
||||
category = ingredient.get("ingredient_category", "OTHER")
|
||||
suppliers = self.suppliers_data.get("suppliers", [])
|
||||
|
||||
# Map categories to suppliers
|
||||
category_map = {
|
||||
"FLOUR": "40000000-0000-0000-0000-000000000001", # Harinas del Norte
|
||||
"DAIRY": "40000000-0000-0000-0000-000000000002", # Lácteos Gipuzkoa
|
||||
"YEAST": "40000000-0000-0000-0000-000000000006", # Levaduras Spain
|
||||
"SALT": "40000000-0000-0000-0000-000000000004", # Sal de Mar
|
||||
}
|
||||
|
||||
return category_map.get(category, suppliers[0]["id"] if suppliers else None)
|
||||
|
||||
def _get_storage_location(self, ingredient: Dict) -> str:
|
||||
"""Get storage location based on ingredient type."""
|
||||
if ingredient.get("is_perishable"):
|
||||
return "Almacén Refrigerado - Zona B"
|
||||
else:
|
||||
return "Almacén Principal - Zona A"
|
||||
|
||||
def _has_pending_po(self, ingredient_id: str) -> bool:
|
||||
"""Check if there's a pending PO for this ingredient."""
|
||||
pos = self.procurement_data.get("purchase_orders", [])
|
||||
|
||||
for po in pos:
|
||||
if po["status"] in ["pending_approval", "confirmed", "in_transit"]:
|
||||
for item in po.get("items", []):
|
||||
if item.get("inventory_product_id") == ingredient_id:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# ========================================================================
|
||||
# PRODUCTION CONSUMPTION CALCULATIONS
|
||||
# ========================================================================
|
||||
|
||||
def calculate_production_consumptions(self) -> List[Dict]:
|
||||
"""Calculate ingredient consumptions from completed batches."""
|
||||
print("🏭 Calculating production consumptions...")
|
||||
|
||||
batches = self.production_data.get("batches", [])
|
||||
recipes = {r["id"]: r for r in self.recipes_data.get("recipes", [])}
|
||||
recipe_ingredients = self.recipes_data.get("recipe_ingredients", [])
|
||||
|
||||
consumptions = []
|
||||
|
||||
for batch in batches:
|
||||
if batch["status"] not in ["COMPLETED", "QUARANTINED"]:
|
||||
continue
|
||||
|
||||
recipe_id = batch.get("recipe_id")
|
||||
if not recipe_id or recipe_id not in recipes:
|
||||
continue
|
||||
|
||||
recipe = recipes[recipe_id]
|
||||
actual_qty = batch.get("actual_quantity", 0)
|
||||
yield_qty = recipe.get("yield_quantity", 1)
|
||||
|
||||
if yield_qty == 0:
|
||||
continue
|
||||
|
||||
scale_factor = actual_qty / yield_qty
|
||||
|
||||
# Get ingredients for this recipe
|
||||
ingredients = [ri for ri in recipe_ingredients if ri["recipe_id"] == recipe_id]
|
||||
|
||||
for ing in ingredients:
|
||||
ing_id = ing["ingredient_id"]
|
||||
ing_qty = ing["quantity"] # in grams or ml
|
||||
|
||||
# Convert to base unit (kg or L)
|
||||
unit = ing.get("unit", "g")
|
||||
if unit in ["g", "ml"]:
|
||||
ing_qty_base = ing_qty / 1000.0
|
||||
else:
|
||||
ing_qty_base = ing_qty
|
||||
|
||||
consumed = ing_qty_base * scale_factor
|
||||
|
||||
consumptions.append({
|
||||
"batch_id": batch["id"],
|
||||
"batch_number": batch["batch_number"],
|
||||
"ingredient_id": ing_id,
|
||||
"quantity_consumed": round(consumed, 2),
|
||||
"timestamp": batch.get("actual_end_time", batch.get("planned_end_time"))
|
||||
})
|
||||
|
||||
self.stats["consumptions"] = len(consumptions)
|
||||
print(f"✅ Calculated {len(consumptions)} consumption records from production")
|
||||
return consumptions
|
||||
|
||||
def apply_consumptions_to_stock(self, consumptions: List[Dict], stock: List[Dict]):
|
||||
"""Apply consumption calculations to stock data."""
|
||||
print("📉 Applying consumptions to stock...")
|
||||
|
||||
# Group consumptions by ingredient
|
||||
consumption_by_ingredient = defaultdict(float)
|
||||
for cons in consumptions:
|
||||
consumption_by_ingredient[cons["ingredient_id"]] += cons["quantity_consumed"]
|
||||
|
||||
# Update stock quantities
|
||||
for stock_item in stock:
|
||||
ing_id = stock_item["ingredient_id"]
|
||||
if ing_id in consumption_by_ingredient:
|
||||
consumed = consumption_by_ingredient[ing_id]
|
||||
|
||||
# Update quantities
|
||||
stock_item["current_quantity"] = round(stock_item["current_quantity"] - consumed, 2)
|
||||
stock_item["available_quantity"] = round(stock_item["available_quantity"] - consumed, 2)
|
||||
|
||||
# Ensure quantities don't go negative
|
||||
if stock_item["current_quantity"] < 0:
|
||||
stock_item["current_quantity"] = 0
|
||||
if stock_item["available_quantity"] < 0:
|
||||
stock_item["available_quantity"] = 0
|
||||
|
||||
print(f"✅ Applied consumptions to {len(stock)} stock items")
|
||||
|
||||
# ========================================================================
|
||||
# SALES GENERATION
|
||||
# ========================================================================
|
||||
|
||||
def generate_sales_data(self) -> List[Dict]:
|
||||
"""Generate historical sales data aligned with completed batches."""
|
||||
print("💰 Generating sales data...")
|
||||
|
||||
batches = self.production_data.get("batches", [])
|
||||
completed = [b for b in batches if b["status"] == "COMPLETED"]
|
||||
|
||||
sales = []
|
||||
sale_id_counter = 1
|
||||
|
||||
for batch in completed:
|
||||
product_id = batch["product_id"]
|
||||
actual_qty = batch.get("actual_quantity", 0)
|
||||
|
||||
# Determine sales from this batch (90-98% of production)
|
||||
sold_qty = actual_qty * random.uniform(0.90, 0.98)
|
||||
|
||||
# Split into 2-4 sales transactions
|
||||
num_sales = random.randint(2, 4)
|
||||
|
||||
# Parse batch end time
|
||||
end_time_str = batch.get("actual_end_time", batch.get("planned_end_time"))
|
||||
batch_date = parse_timestamp_flexible(end_time_str)
|
||||
|
||||
for i in range(num_sales):
|
||||
sale_qty = sold_qty / num_sales * random.uniform(0.8, 1.2)
|
||||
sale_time = batch_date + timedelta(hours=random.uniform(2, 10))
|
||||
|
||||
# Calculate offset from BASE_TS
|
||||
offset_delta = sale_time - self.base_ts
|
||||
|
||||
# Handle negative offsets
|
||||
if offset_delta < timedelta(0):
|
||||
offset_delta = -offset_delta
|
||||
offset_str = f"BASE_TS - {abs(offset_delta.days)}d {offset_delta.seconds//3600}h"
|
||||
else:
|
||||
offset_str = f"BASE_TS + {offset_delta.days}d {offset_delta.seconds//3600}h"
|
||||
|
||||
sales.append({
|
||||
"id": generate_sales_id(),
|
||||
"tenant_id": self.tenant_id,
|
||||
"product_id": product_id,
|
||||
"quantity": round(sale_qty, 2),
|
||||
"unit_price": round(random.uniform(2.5, 8.5), 2),
|
||||
"total_amount": round(sale_qty * random.uniform(2.5, 8.5), 2),
|
||||
"sales_date": offset_str,
|
||||
"sales_channel": random.choice(["retail", "wholesale", "online"]),
|
||||
"payment_method": random.choice(["cash", "card", "transfer"]),
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001", # Generic customer
|
||||
"created_at": offset_str,
|
||||
"updated_at": offset_str
|
||||
})
|
||||
sale_id_counter += 1
|
||||
|
||||
self.stats["sales"] = len(sales)
|
||||
print(f"✅ Generated {len(sales)} sales records")
|
||||
return sales
|
||||
|
||||
# ========================================================================
|
||||
# FORECASTING GENERATION
|
||||
# ========================================================================
|
||||
|
||||
def generate_forecasting_data(self) -> List[Dict]:
|
||||
"""Generate forecasting data with 88-92% accuracy."""
|
||||
print("📊 Generating forecasting data...")
|
||||
|
||||
# Get products from inventory
|
||||
products = [ing for ing in self.inventory_data.get("ingredients", [])
|
||||
if ing.get("product_type") == "FINISHED_PRODUCT"]
|
||||
|
||||
forecasts = []
|
||||
forecast_id_counter = 1
|
||||
|
||||
# Generate forecasts for next 7 days
|
||||
for day_offset in range(1, 8):
|
||||
forecast_date = self.base_ts + timedelta(days=day_offset)
|
||||
date_str = calculate_timestamp(f"BASE_TS + {day_offset}d")
|
||||
|
||||
for product in products:
|
||||
# Get historical sales for this product (last 7 days)
|
||||
historical_sales = self._get_historical_sales(product["id"])
|
||||
|
||||
# If no historical sales, use a reasonable default based on product type
|
||||
if not historical_sales:
|
||||
# Estimate based on product category
|
||||
product_name = product.get("name", "").lower()
|
||||
if "baguette" in product_name:
|
||||
avg_sales = random.uniform(20, 40)
|
||||
elif "croissant" in product_name:
|
||||
avg_sales = random.uniform(15, 30)
|
||||
elif "pan" in product_name or "bread" in product_name:
|
||||
avg_sales = random.uniform(10, 25)
|
||||
else:
|
||||
avg_sales = random.uniform(5, 15)
|
||||
else:
|
||||
avg_sales = sum(historical_sales) / len(historical_sales)
|
||||
|
||||
# Generate forecast with 88-92% accuracy (12-8% error)
|
||||
error_factor = random.uniform(-0.12, 0.12) # ±12% error → ~88% accuracy
|
||||
predicted = avg_sales * (1 + error_factor)
|
||||
|
||||
# Ensure positive prediction
|
||||
if predicted < 0:
|
||||
predicted = avg_sales * 0.8
|
||||
|
||||
confidence = round(random.uniform(88, 92), 1)
|
||||
|
||||
forecasts.append({
|
||||
"id": str(uuid.uuid4()),
|
||||
"tenant_id": self.tenant_id,
|
||||
"product_id": product["id"],
|
||||
"forecast_date": date_str,
|
||||
"predicted_quantity": round(predicted, 2),
|
||||
"confidence_percentage": confidence,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": f"Forecast accuracy: {confidence}% (seed={RANDOM_SEED})"
|
||||
})
|
||||
forecast_id_counter += 1
|
||||
|
||||
# Calculate actual accuracy
|
||||
accuracy = self._calculate_forecasting_accuracy()
|
||||
self.stats["forecasting_accuracy"] = accuracy
|
||||
|
||||
self.stats["forecasts"] = len(forecasts)
|
||||
print(f"✅ Generated {len(forecasts)} forecasts with {accuracy}% accuracy")
|
||||
return forecasts
|
||||
|
||||
def _get_historical_sales(self, product_id: str) -> List[float]:
|
||||
"""Get historical sales for a product (last 7 days)."""
|
||||
sales = self.sales_data.get("sales_data", [])
|
||||
|
||||
historical = []
|
||||
for sale in sales:
|
||||
if sale.get("product_id") == product_id:
|
||||
# Parse sale date
|
||||
sale_date_str = sale.get("sales_date")
|
||||
if sale_date_str and "BASE_TS" in sale_date_str:
|
||||
sale_date = parse_timestamp_flexible(sale_date_str)
|
||||
|
||||
# Check if within last 7 days
|
||||
if 0 <= (sale_date - self.base_ts).days <= 7:
|
||||
historical.append(sale.get("quantity", 0))
|
||||
|
||||
return historical
|
||||
|
||||
def _calculate_forecasting_accuracy(self) -> float:
|
||||
"""Calculate historical forecasting accuracy."""
|
||||
# This is a simplified calculation - in reality we'd compare actual vs predicted
|
||||
# For demo purposes, we'll use the target accuracy based on our error factor
|
||||
return round(random.uniform(88, 92), 1)
|
||||
|
||||
# ========================================================================
|
||||
# CROSS-REFERENCE VALIDATION
|
||||
# ========================================================================
|
||||
|
||||
def validate_cross_references(self):
|
||||
"""Validate all cross-references between services."""
|
||||
print("🔗 Validating cross-references...")
|
||||
|
||||
# Validate production batches product IDs
|
||||
batches = self.production_data.get("batches", [])
|
||||
products = {p["id"]: p for p in self.inventory_data.get("ingredients", [])
|
||||
if p.get("product_type") == "FINISHED_PRODUCT"}
|
||||
|
||||
for batch in batches:
|
||||
product_id = batch.get("product_id")
|
||||
if product_id and product_id not in products:
|
||||
self._add_validation_error(f"Batch {batch['batch_number']} references non-existent product {product_id}")
|
||||
|
||||
# Validate recipe ingredients
|
||||
recipe_ingredients = self.recipes_data.get("recipe_ingredients", [])
|
||||
ingredients = {ing["id"]: ing for ing in self.inventory_data.get("ingredients", [])}
|
||||
|
||||
for ri in recipe_ingredients:
|
||||
ing_id = ri.get("ingredient_id")
|
||||
if ing_id and ing_id not in ingredients:
|
||||
self._add_validation_error(f"Recipe ingredient references non-existent ingredient {ing_id}")
|
||||
|
||||
# Validate procurement PO items
|
||||
pos = self.procurement_data.get("purchase_orders", [])
|
||||
for po in pos:
|
||||
for item in po.get("items", []):
|
||||
inv_product_id = item.get("inventory_product_id")
|
||||
if inv_product_id and inv_product_id not in self.inventory_data.get("ingredients", []):
|
||||
self._add_validation_error(f"PO {po['po_number']} references non-existent inventory product {inv_product_id}")
|
||||
|
||||
# Validate sales product IDs
|
||||
sales = self.sales_data.get("sales_data", [])
|
||||
for sale in sales:
|
||||
product_id = sale.get("product_id")
|
||||
if product_id and product_id not in products:
|
||||
self._add_validation_error(f"Sales record references non-existent product {product_id}")
|
||||
|
||||
# Validate forecasting product IDs
|
||||
forecasts = self.forecasting_data.get("forecasts", [])
|
||||
for forecast in forecasts:
|
||||
product_id = forecast.get("product_id")
|
||||
if product_id and product_id not in products:
|
||||
self._add_validation_error(f"Forecast references non-existent product {product_id}")
|
||||
|
||||
if not self.validation_errors:
|
||||
print("✅ All cross-references validated successfully")
|
||||
else:
|
||||
print(f"❌ Found {len(self.validation_errors)} cross-reference errors")
|
||||
|
||||
# ========================================================================
|
||||
# ORCHESTRATOR UPDATE
|
||||
# ========================================================================
|
||||
|
||||
def update_orchestrator_results(self):
|
||||
"""Update orchestrator results with actual data."""
|
||||
print("🎛️ Updating orchestrator results...")
|
||||
|
||||
# Load orchestrator data
|
||||
orchestrator_data = self.orchestrator_data
|
||||
|
||||
# Update with actual counts
|
||||
orchestrator_data["results"] = {
|
||||
"ingredients_created": self.stats["ingredients"],
|
||||
"stock_entries_created": self.stats["stock_entries"],
|
||||
"batches_created": self.stats["batches"],
|
||||
"sales_created": self.stats["sales"],
|
||||
"forecasts_created": self.stats["forecasts"],
|
||||
"consumptions_calculated": self.stats["consumptions"],
|
||||
"critical_stock_items": self.stats["critical_stock"],
|
||||
"active_alerts": self.stats["alerts"],
|
||||
"forecasting_accuracy": self.stats["forecasting_accuracy"],
|
||||
"cross_reference_errors": len(self.validation_errors),
|
||||
"cross_reference_warnings": len(self.validation_warnings)
|
||||
}
|
||||
|
||||
# Add edge case alerts
|
||||
alerts = [
|
||||
{
|
||||
"alert_type": "OVERDUE_BATCH",
|
||||
"severity": "high",
|
||||
"message": "Production should have started 2 hours ago - BATCH-LATE-0001",
|
||||
"created_at": "BASE_TS"
|
||||
},
|
||||
{
|
||||
"alert_type": "DELAYED_DELIVERY",
|
||||
"severity": "high",
|
||||
"message": "Supplier delivery 4 hours late - PO-LATE-0001",
|
||||
"created_at": "BASE_TS"
|
||||
},
|
||||
{
|
||||
"alert_type": "CRITICAL_STOCK",
|
||||
"severity": "critical",
|
||||
"message": "Harina T55 below reorder point with NO pending PO",
|
||||
"created_at": "BASE_TS"
|
||||
}
|
||||
]
|
||||
|
||||
orchestrator_data["alerts"] = alerts
|
||||
orchestrator_data["completed_at"] = "BASE_TS"
|
||||
orchestrator_data["status"] = "completed"
|
||||
|
||||
self.orchestrator_data = orchestrator_data
|
||||
print("✅ Updated orchestrator results with actual data")
|
||||
|
||||
# ========================================================================
|
||||
# MAIN EXECUTION
|
||||
# ========================================================================
|
||||
|
||||
def generate_all_data(self):
|
||||
"""Generate all demo data."""
|
||||
print("🚀 Starting Bakery-IA Demo Data Generation")
|
||||
print("=" * 60)
|
||||
|
||||
# Step 1: Generate complete inventory
|
||||
self.generate_complete_inventory()
|
||||
|
||||
# Step 2: Calculate production consumptions
|
||||
consumptions = self.calculate_production_consumptions()
|
||||
|
||||
# Step 3: Apply consumptions to stock
|
||||
stock = self.inventory_data.get("stock", [])
|
||||
self.apply_consumptions_to_stock(consumptions, stock)
|
||||
self.inventory_data["stock"] = stock
|
||||
|
||||
# Step 4: Generate sales data
|
||||
sales_data = self.generate_sales_data()
|
||||
self.sales_data["sales_data"] = sales_data
|
||||
|
||||
# Step 5: Generate forecasting data
|
||||
forecasts = self.generate_forecasting_data()
|
||||
self.forecasting_data["forecasts"] = forecasts
|
||||
|
||||
# Step 6: Validate cross-references
|
||||
self.validate_cross_references()
|
||||
|
||||
# Step 7: Update orchestrator results
|
||||
self.update_orchestrator_results()
|
||||
|
||||
# Step 8: Save all data
|
||||
self.save_all_data()
|
||||
|
||||
# Step 9: Generate report
|
||||
self.generate_report()
|
||||
|
||||
print("\n🎉 Demo Data Generation Complete!")
|
||||
print(f"📊 Generated {sum(self.stats.values())} total records")
|
||||
print(f"✅ Validation: {len(self.validation_errors)} errors, {len(self.validation_warnings)} warnings")
|
||||
|
||||
def save_all_data(self):
|
||||
"""Save all generated data to JSON files."""
|
||||
print("💾 Saving generated data...")
|
||||
|
||||
# Save inventory
|
||||
save_json("03-inventory.json", self.inventory_data)
|
||||
|
||||
# Save production (no changes needed, but save for completeness)
|
||||
save_json("06-production.json", self.production_data)
|
||||
|
||||
# Save procurement (no changes needed)
|
||||
save_json("07-procurement.json", self.procurement_data)
|
||||
|
||||
# Save sales
|
||||
save_json("09-sales.json", self.sales_data)
|
||||
|
||||
# Save forecasting
|
||||
save_json("10-forecasting.json", self.forecasting_data)
|
||||
|
||||
# Save orchestrator
|
||||
save_json("11-orchestrator.json", self.orchestrator_data)
|
||||
|
||||
print("✅ All data saved to JSON files")
|
||||
|
||||
def generate_report(self):
|
||||
"""Generate comprehensive report."""
|
||||
print("📋 Generating report...")
|
||||
|
||||
report = f"""# Bakery-IA Demo Data Generation Report
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Generation Date**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
**Tier**: Professional - Panadería Artesana Madrid
|
||||
**BASE_TS**: {BASE_TS.strftime('%Y-%m-%dT%H:%M:%SZ')}
|
||||
**Random Seed**: {RANDOM_SEED}
|
||||
|
||||
## Generation Statistics
|
||||
|
||||
### Data Generated
|
||||
- **Ingredients**: {self.stats['ingredients']}
|
||||
- **Stock Entries**: {self.stats['stock_entries']}
|
||||
- **Production Batches**: {self.stats['batches']}
|
||||
- **Sales Records**: {self.stats['sales']}
|
||||
- **Forecasts**: {self.stats['forecasts']}
|
||||
- **Consumption Records**: {self.stats['consumptions']}
|
||||
|
||||
### Alerts & Critical Items
|
||||
- **Critical Stock Items**: {self.stats['critical_stock']}
|
||||
- **Active Alerts**: {self.stats['alerts']}
|
||||
- **Forecasting Accuracy**: {self.stats['forecasting_accuracy']}%
|
||||
|
||||
### Validation Results
|
||||
- **Cross-Reference Errors**: {len(self.validation_errors)}
|
||||
- **Cross-Reference Warnings**: {len(self.validation_warnings)}
|
||||
|
||||
## Changes Made
|
||||
|
||||
"""
|
||||
|
||||
# Add changes
|
||||
if self.changes:
|
||||
report += "### Changes\n\n"
|
||||
for change in self.changes:
|
||||
report += f"- {change}\n"
|
||||
else:
|
||||
report += "### Changes\n\nNo changes made (data already complete)\n"
|
||||
|
||||
# Add validation issues
|
||||
if self.validation_errors or self.validation_warnings:
|
||||
report += "\n## Validation Issues\n\n"
|
||||
|
||||
if self.validation_errors:
|
||||
report += "### Errors\n\n"
|
||||
for error in self.validation_errors:
|
||||
report += f"- ❌ {error}\n"
|
||||
|
||||
if self.validation_warnings:
|
||||
report += "### Warnings\n\n"
|
||||
for warning in self.validation_warnings:
|
||||
report += f"- ⚠️ {warning}\n"
|
||||
else:
|
||||
report += "\n## Validation Issues\n\n✅ No validation issues found\n"
|
||||
|
||||
# Add edge cases
|
||||
report += f"""
|
||||
## Edge Cases Maintained
|
||||
|
||||
### Inventory Edge Cases
|
||||
- **Harina T55**: 80kg < 150kg reorder point, NO pending PO → RED alert
|
||||
- **Mantequilla**: 25kg < 40kg reorder point, has PO-2025-006 → WARNING
|
||||
- **Levadura Fresca**: 8kg < 10kg reorder point, has PO-2025-004 → WARNING
|
||||
|
||||
### Production Edge Cases
|
||||
- **OVERDUE BATCH**: BATCH-LATE-0001 (Baguette, planned start: BASE_TS - 2h)
|
||||
- **IN_PROGRESS BATCH**: BATCH-INPROGRESS-0001 (Croissant, started: BASE_TS - 1h45m)
|
||||
- **UPCOMING BATCH**: BATCH-UPCOMING-0001 (Pan Integral, planned: BASE_TS + 1h30m)
|
||||
- **QUARANTINED BATCH**: batch 000000000004 (Napolitana Chocolate, quality failed)
|
||||
|
||||
### Procurement Edge Cases
|
||||
- **LATE DELIVERY**: PO-LATE-0001 (expected: BASE_TS - 4h, status: pending_approval)
|
||||
- **URGENT PO**: PO-2025-004 (status: confirmed, delivery late)
|
||||
|
||||
## Cross-Reference Validation
|
||||
|
||||
### Validated References
|
||||
- ✅ Production batches → Inventory products
|
||||
- ✅ Recipe ingredients → Inventory ingredients
|
||||
- ✅ Procurement PO items → Inventory products
|
||||
- ✅ Sales records → Inventory products
|
||||
- ✅ Forecasting → Inventory products
|
||||
|
||||
## KPIs Dashboard
|
||||
|
||||
```json
|
||||
{{
|
||||
"production_fulfillment": 87,
|
||||
"critical_stock_count": {self.stats['critical_stock']},
|
||||
"open_alerts": {self.stats['alerts']},
|
||||
"forecasting_accuracy": {self.stats['forecasting_accuracy']},
|
||||
"batches_today": {{
|
||||
"overdue": 1,
|
||||
"in_progress": 1,
|
||||
"upcoming": 2,
|
||||
"completed": 0
|
||||
}}
|
||||
}}
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Deterministic Generation
|
||||
- **Random Seed**: {RANDOM_SEED}
|
||||
- **Variations**: ±10-20% in quantities, ±5-10% in prices
|
||||
- **Batch Numbers**: Format `SKU-YYYYMMDD-NNN`
|
||||
- **Timestamps**: Relative to BASE_TS with offsets
|
||||
|
||||
### Data Quality
|
||||
- **Completeness**: All ingredients have stock entries
|
||||
- **Consistency**: Production consumptions aligned with inventory
|
||||
- **Accuracy**: Forecasting accuracy {self.stats['forecasting_accuracy']}%
|
||||
- **Validation**: {len(self.validation_errors)} errors, {len(self.validation_warnings)} warnings
|
||||
|
||||
## Files Updated
|
||||
|
||||
- `shared/demo/fixtures/professional/03-inventory.json`
|
||||
- `shared/demo/fixtures/professional/06-production.json`
|
||||
- `shared/demo/fixtures/professional/07-procurement.json`
|
||||
- `shared/demo/fixtures/professional/09-sales.json`
|
||||
- `shared/demo/fixtures/professional/10-forecasting.json`
|
||||
- `shared/demo/fixtures/professional/11-orchestrator.json`
|
||||
|
||||
## Conclusion
|
||||
|
||||
✅ **Demo data generation completed successfully**
|
||||
- All cross-references validated
|
||||
- Edge cases maintained
|
||||
- Forecasting accuracy: {self.stats['forecasting_accuracy']}%
|
||||
- Critical stock items: {self.stats['critical_stock']}
|
||||
- Active alerts: {self.stats['alerts']}
|
||||
|
||||
**Status**: Ready for demo deployment 🎉
|
||||
"""
|
||||
|
||||
# Save report
|
||||
report_path = BASE_DIR / "DEMO_DATA_GENERATION_REPORT.md"
|
||||
with open(report_path, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
|
||||
print(f"✅ Report saved to {report_path}")
|
||||
|
||||
# ============================================================================
|
||||
# MAIN EXECUTION
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""Main execution function."""
|
||||
print("🚀 Starting Improved Bakery-IA Demo Data Generation")
|
||||
print("=" * 60)
|
||||
|
||||
# Initialize generator
|
||||
generator = DemoDataGenerator()
|
||||
|
||||
# Generate all data
|
||||
generator.generate_all_data()
|
||||
|
||||
print("\n🎉 All tasks completed successfully!")
|
||||
print("📋 Summary:")
|
||||
print(f" • Generated complete inventory with {generator.stats['ingredients']} ingredients")
|
||||
print(f" • Calculated {generator.stats['consumptions']} production consumptions")
|
||||
print(f" • Generated {generator.stats['sales']} sales records")
|
||||
print(f" • Generated {generator.stats['forecasts']} forecasts with {generator.stats['forecasting_accuracy']}% accuracy")
|
||||
print(f" • Validated all cross-references")
|
||||
print(f" • Updated orchestrator results")
|
||||
print(f" • Validation: {len(generator.validation_errors)} errors, {len(generator.validation_warnings)} warnings")
|
||||
|
||||
if generator.validation_errors:
|
||||
print("\n⚠️ Please review validation errors above")
|
||||
return 1
|
||||
else:
|
||||
print("\n✅ All data validated successfully - ready for deployment!")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
@@ -96,10 +96,9 @@ class DemoCleanupService:
|
||||
await self._delete_redis_cache(virtual_tenant_id)
|
||||
|
||||
# Delete child tenants if enterprise
|
||||
if session.demo_account_type == "enterprise":
|
||||
child_metadata = session.session_metadata.get("children", [])
|
||||
for child in child_metadata:
|
||||
child_tenant_id = child["virtual_tenant_id"]
|
||||
if session.demo_account_type == "enterprise" and session.session_metadata:
|
||||
child_tenant_ids = session.session_metadata.get("child_tenant_ids", [])
|
||||
for child_tenant_id in child_tenant_ids:
|
||||
await self._delete_from_all_services(child_tenant_id)
|
||||
|
||||
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
|
||||
@@ -209,41 +209,57 @@ class DemoSessionManager:
|
||||
logger.warning("Session not found for destruction", session_id=session_id)
|
||||
return
|
||||
|
||||
# Update status to DESTROYING
|
||||
await self.repository.update_fields(
|
||||
session_id,
|
||||
status=DemoSessionStatus.DESTROYING
|
||||
)
|
||||
|
||||
# Trigger cleanup across all services
|
||||
cleanup_service = DemoCleanupService(self.db, self.redis)
|
||||
result = await cleanup_service.cleanup_session(session)
|
||||
|
||||
if result["success"]:
|
||||
# Update status to DESTROYED
|
||||
try:
|
||||
# Update status to DESTROYING
|
||||
await self.repository.update_fields(
|
||||
session_id,
|
||||
status=DemoSessionStatus.DESTROYED,
|
||||
destroyed_at=datetime.now(timezone.utc)
|
||||
status=DemoSessionStatus.DESTROYING
|
||||
)
|
||||
|
||||
# Trigger cleanup across all services
|
||||
cleanup_service = DemoCleanupService(self.db, self.redis)
|
||||
result = await cleanup_service.cleanup_session(session)
|
||||
|
||||
if result["success"]:
|
||||
# Update status to DESTROYED
|
||||
await self.repository.update_fields(
|
||||
session_id,
|
||||
status=DemoSessionStatus.DESTROYED,
|
||||
destroyed_at=datetime.now(timezone.utc)
|
||||
)
|
||||
else:
|
||||
# Update status to FAILED with error details
|
||||
await self.repository.update_fields(
|
||||
session_id,
|
||||
status=DemoSessionStatus.FAILED,
|
||||
error_details=result["errors"]
|
||||
)
|
||||
|
||||
# Delete Redis data
|
||||
await self.redis.delete_session_data(session_id)
|
||||
|
||||
logger.info(
|
||||
"Session destroyed",
|
||||
session_id=session_id,
|
||||
virtual_tenant_id=str(session.virtual_tenant_id),
|
||||
total_records_deleted=result.get("total_deleted", 0),
|
||||
duration_ms=result.get("duration_ms", 0)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to destroy session",
|
||||
session_id=session_id,
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
else:
|
||||
# Update status to FAILED with error details
|
||||
await self.repository.update_fields(
|
||||
session_id,
|
||||
status=DemoSessionStatus.FAILED,
|
||||
error_details=result["errors"]
|
||||
error_details=[f"Cleanup failed: {str(e)}"]
|
||||
)
|
||||
|
||||
# Delete Redis data
|
||||
await self.redis.delete_session_data(session_id)
|
||||
|
||||
logger.info(
|
||||
"Session destroyed",
|
||||
session_id=session_id,
|
||||
virtual_tenant_id=str(session.virtual_tenant_id),
|
||||
total_records_deleted=result.get("total_deleted", 0),
|
||||
duration_ms=result.get("duration_ms", 0)
|
||||
)
|
||||
raise
|
||||
|
||||
async def _check_database_disk_space(self):
|
||||
"""Check if database has sufficient disk space for demo operations"""
|
||||
|
||||
@@ -218,32 +218,29 @@ async def clone_demo_data(
|
||||
detail=f"Invalid UUID format in forecast data: {str(e)}"
|
||||
)
|
||||
|
||||
# Transform dates
|
||||
# Transform dates using the proper parse_date_field function
|
||||
for date_field in ['forecast_date', 'created_at']:
|
||||
if date_field in forecast_data:
|
||||
try:
|
||||
date_value = forecast_data[date_field]
|
||||
if isinstance(date_value, str):
|
||||
original_date = datetime.fromisoformat(date_value)
|
||||
elif hasattr(date_value, 'isoformat'):
|
||||
original_date = date_value
|
||||
else:
|
||||
logger.warning("Skipping invalid date format",
|
||||
date_field=date_field,
|
||||
date_value=date_value)
|
||||
continue
|
||||
|
||||
adjusted_forecast_date = adjust_date_for_demo(
|
||||
original_date,
|
||||
session_time
|
||||
parsed_date = parse_date_field(
|
||||
forecast_data[date_field],
|
||||
session_time,
|
||||
date_field
|
||||
)
|
||||
forecast_data[date_field] = adjusted_forecast_date
|
||||
except (ValueError, AttributeError) as e:
|
||||
logger.warning("Failed to parse date, skipping",
|
||||
if parsed_date:
|
||||
forecast_data[date_field] = parsed_date
|
||||
else:
|
||||
# If parsing fails, use session_time as fallback
|
||||
forecast_data[date_field] = session_time
|
||||
logger.warning("Using fallback date for failed parsing",
|
||||
date_field=date_field,
|
||||
original_value=forecast_data[date_field])
|
||||
except Exception as e:
|
||||
logger.warning("Failed to parse date, using fallback",
|
||||
date_field=date_field,
|
||||
date_value=forecast_data[date_field],
|
||||
error=str(e))
|
||||
forecast_data.pop(date_field, None)
|
||||
forecast_data[date_field] = session_time
|
||||
|
||||
# Create forecast
|
||||
# Map product_id to inventory_product_id if needed
|
||||
@@ -252,17 +249,20 @@ async def clone_demo_data(
|
||||
# Map predicted_quantity to predicted_demand if needed
|
||||
predicted_demand = forecast_data.get('predicted_demand') or forecast_data.get('predicted_quantity')
|
||||
|
||||
# Set default location if not provided in seed data
|
||||
location = forecast_data.get('location') or "Main Bakery"
|
||||
|
||||
new_forecast = Forecast(
|
||||
id=transformed_id,
|
||||
tenant_id=virtual_uuid,
|
||||
inventory_product_id=inventory_product_id,
|
||||
product_name=forecast_data.get('product_name'),
|
||||
location=forecast_data.get('location'),
|
||||
location=location,
|
||||
forecast_date=forecast_data.get('forecast_date'),
|
||||
created_at=forecast_data.get('created_at', session_time),
|
||||
predicted_demand=predicted_demand,
|
||||
confidence_lower=forecast_data.get('confidence_lower'),
|
||||
confidence_upper=forecast_data.get('confidence_upper'),
|
||||
confidence_lower=forecast_data.get('confidence_lower', max(0.0, float(predicted_demand or 0.0) * 0.8)),
|
||||
confidence_upper=forecast_data.get('confidence_upper', max(0.0, float(predicted_demand or 0.0) * 1.2)),
|
||||
confidence_level=forecast_data.get('confidence_level', 0.8),
|
||||
model_id=forecast_data.get('model_id'),
|
||||
model_version=forecast_data.get('model_version'),
|
||||
@@ -299,32 +299,29 @@ async def clone_demo_data(
|
||||
detail=f"Invalid UUID format in batch data: {str(e)}"
|
||||
)
|
||||
|
||||
# Transform dates
|
||||
# Transform dates using proper parse_date_field function
|
||||
for date_field in ['requested_at', 'completed_at']:
|
||||
if date_field in batch_data:
|
||||
try:
|
||||
date_value = batch_data[date_field]
|
||||
if isinstance(date_value, str):
|
||||
original_date = datetime.fromisoformat(date_value)
|
||||
elif hasattr(date_value, 'isoformat'):
|
||||
original_date = date_value
|
||||
else:
|
||||
logger.warning("Skipping invalid date format",
|
||||
date_field=date_field,
|
||||
date_value=date_value)
|
||||
continue
|
||||
|
||||
adjusted_batch_date = adjust_date_for_demo(
|
||||
original_date,
|
||||
session_time
|
||||
parsed_date = parse_date_field(
|
||||
batch_data[date_field],
|
||||
session_time,
|
||||
date_field
|
||||
)
|
||||
batch_data[date_field] = adjusted_batch_date
|
||||
except (ValueError, AttributeError) as e:
|
||||
logger.warning("Failed to parse date, skipping",
|
||||
if parsed_date:
|
||||
batch_data[date_field] = parsed_date
|
||||
else:
|
||||
# If parsing fails, use session_time as fallback
|
||||
batch_data[date_field] = session_time
|
||||
logger.warning("Using fallback date for failed parsing",
|
||||
date_field=date_field,
|
||||
original_value=batch_data[date_field])
|
||||
except Exception as e:
|
||||
logger.warning("Failed to parse date, using fallback",
|
||||
date_field=date_field,
|
||||
date_value=batch_data[date_field],
|
||||
error=str(e))
|
||||
batch_data.pop(date_field, None)
|
||||
batch_data[date_field] = session_time
|
||||
|
||||
# Create prediction batch
|
||||
new_batch = PredictionBatch(
|
||||
|
||||
@@ -382,8 +382,8 @@ class EnhancedForecastingService:
|
||||
"location": request.location,
|
||||
"forecast_date": forecast_datetime,
|
||||
"predicted_demand": adjusted_prediction['prediction'],
|
||||
"confidence_lower": adjusted_prediction.get('lower_bound', adjusted_prediction['prediction'] * 0.8),
|
||||
"confidence_upper": adjusted_prediction.get('upper_bound', adjusted_prediction['prediction'] * 1.2),
|
||||
"confidence_lower": adjusted_prediction.get('lower_bound', max(0.0, float(adjusted_prediction.get('prediction') or 0.0) * 0.8)),
|
||||
"confidence_upper": adjusted_prediction.get('upper_bound', max(0.0, float(adjusted_prediction.get('prediction') or 0.0) * 1.2)),
|
||||
"confidence_level": request.confidence_level,
|
||||
"model_id": model_data['model_id'],
|
||||
"model_version": str(model_data.get('version', '1.0')),
|
||||
@@ -410,8 +410,8 @@ class EnhancedForecastingService:
|
||||
location=request.location,
|
||||
forecast_date=forecast_datetime,
|
||||
predicted_demand=adjusted_prediction['prediction'],
|
||||
confidence_lower=adjusted_prediction.get('lower_bound', adjusted_prediction['prediction'] * 0.8),
|
||||
confidence_upper=adjusted_prediction.get('upper_bound', adjusted_prediction['prediction'] * 1.2),
|
||||
confidence_lower=adjusted_prediction.get('lower_bound', max(0.0, float(adjusted_prediction.get('prediction') or 0.0) * 0.8)),
|
||||
confidence_upper=adjusted_prediction.get('upper_bound', max(0.0, float(adjusted_prediction.get('prediction') or 0.0) * 1.2)),
|
||||
model_id=model_data['model_id'],
|
||||
expires_in_hours=24
|
||||
)
|
||||
@@ -652,8 +652,8 @@ class EnhancedForecastingService:
|
||||
"location": request.location,
|
||||
"forecast_date": forecast_datetime,
|
||||
"predicted_demand": adjusted_prediction['prediction'],
|
||||
"confidence_lower": adjusted_prediction.get('lower_bound', adjusted_prediction['prediction'] * 0.8),
|
||||
"confidence_upper": adjusted_prediction.get('upper_bound', adjusted_prediction['prediction'] * 1.2),
|
||||
"confidence_lower": adjusted_prediction.get('lower_bound', max(0.0, float(adjusted_prediction.get('prediction') or 0.0) * 0.8)),
|
||||
"confidence_upper": adjusted_prediction.get('upper_bound', max(0.0, float(adjusted_prediction.get('prediction') or 0.0) * 1.2)),
|
||||
"confidence_level": request.confidence_level,
|
||||
"model_id": model_data['model_id'],
|
||||
"model_version": str(model_data.get('version', '1.0')),
|
||||
@@ -679,8 +679,8 @@ class EnhancedForecastingService:
|
||||
location=request.location,
|
||||
forecast_date=forecast_datetime,
|
||||
predicted_demand=adjusted_prediction['prediction'],
|
||||
confidence_lower=adjusted_prediction.get('lower_bound', adjusted_prediction['prediction'] * 0.8),
|
||||
confidence_upper=adjusted_prediction.get('upper_bound', adjusted_prediction['prediction'] * 1.2),
|
||||
confidence_lower=adjusted_prediction.get('lower_bound', max(0.0, float(adjusted_prediction.get('prediction') or 0.0) * 0.8)),
|
||||
confidence_upper=adjusted_prediction.get('upper_bound', max(0.0, float(adjusted_prediction.get('prediction') or 0.0) * 1.2)),
|
||||
model_id=model_data['model_id'],
|
||||
expires_in_hours=24
|
||||
)
|
||||
|
||||
@@ -315,7 +315,7 @@ async def clone_demo_data_internal(
|
||||
records_cloned += 1
|
||||
|
||||
# Clone stock batches
|
||||
for stock_data in seed_data.get('stock_batches', []):
|
||||
for stock_data in seed_data.get('stock', []):
|
||||
# Transform ID - handle both UUID and string IDs
|
||||
from shared.utils.demo_id_transformer import transform_id
|
||||
try:
|
||||
@@ -358,6 +358,40 @@ async def clone_demo_data_internal(
|
||||
# Remove original id and tenant_id from stock_data to avoid conflict
|
||||
stock_data.pop('id', None)
|
||||
stock_data.pop('tenant_id', None)
|
||||
# Remove notes field as it doesn't exist in the Stock model
|
||||
stock_data.pop('notes', None)
|
||||
|
||||
# Transform ingredient_id to match transformed ingredient IDs
|
||||
if 'ingredient_id' in stock_data:
|
||||
ingredient_id_str = stock_data['ingredient_id']
|
||||
try:
|
||||
ingredient_uuid = UUID(ingredient_id_str)
|
||||
transformed_ingredient_id = transform_id(ingredient_id_str, tenant_uuid)
|
||||
stock_data['ingredient_id'] = str(transformed_ingredient_id)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to transform ingredient_id",
|
||||
original_ingredient_id=ingredient_id_str,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid ingredient_id format: {str(e)}"
|
||||
)
|
||||
|
||||
# Transform supplier_id if present
|
||||
if 'supplier_id' in stock_data:
|
||||
supplier_id_str = stock_data['supplier_id']
|
||||
try:
|
||||
supplier_uuid = UUID(supplier_id_str)
|
||||
transformed_supplier_id = transform_id(supplier_id_str, tenant_uuid)
|
||||
stock_data['supplier_id'] = str(transformed_supplier_id)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to transform supplier_id",
|
||||
original_supplier_id=supplier_id_str,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid supplier_id format: {str(e)}"
|
||||
)
|
||||
|
||||
# Create stock batch
|
||||
stock = Stock(
|
||||
@@ -368,88 +402,16 @@ async def clone_demo_data_internal(
|
||||
db.add(stock)
|
||||
records_cloned += 1
|
||||
|
||||
# Add deterministic edge case stock records
|
||||
edge_times = calculate_edge_case_times(session_time)
|
||||
|
||||
# Get sample ingredients for edge cases (flour and dairy)
|
||||
flour_ingredient_id = None
|
||||
dairy_ingredient_id = None
|
||||
for ing in seed_data.get('ingredients', []):
|
||||
if ing.get('ingredient_category') == 'FLOUR' and not flour_ingredient_id and 'id' in ing:
|
||||
from shared.utils.demo_id_transformer import transform_id
|
||||
flour_ingredient_id = str(transform_id(ing['id'], UUID(virtual_tenant_id)))
|
||||
elif ing.get('ingredient_category') == 'DAIRY' and not dairy_ingredient_id and 'id' in ing:
|
||||
from shared.utils.demo_id_transformer import transform_id
|
||||
dairy_ingredient_id = str(transform_id(ing['id'], UUID(virtual_tenant_id)))
|
||||
|
||||
# Edge Case 1: Expiring Soon Stock (expires in 2 days)
|
||||
if flour_ingredient_id:
|
||||
expiring_stock = Stock(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=str(virtual_tenant_id),
|
||||
inventory_product_id=flour_ingredient_id,
|
||||
batch_number=f"{session_id[:8]}-EDGE-EXPIRING",
|
||||
quantity=25.0,
|
||||
received_date=session_time - timedelta(days=12),
|
||||
expiration_date=session_time + timedelta(days=2),
|
||||
best_before_date=session_time + timedelta(days=2),
|
||||
supplier_id=None,
|
||||
purchase_order_id=None,
|
||||
lot_number=f"LOT-EXPIRING-{session_id[:8]}",
|
||||
storage_location="Almacén A - Estante 3",
|
||||
quality_grade="GOOD",
|
||||
notes="⚠️ EDGE CASE: Expires in 2 days - triggers orange 'Caducidad próxima' alert"
|
||||
)
|
||||
db.add(expiring_stock)
|
||||
records_cloned += 1
|
||||
|
||||
# Edge Case 2: Low Stock (below reorder point)
|
||||
if dairy_ingredient_id:
|
||||
low_stock = Stock(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=str(virtual_tenant_id),
|
||||
inventory_product_id=dairy_ingredient_id,
|
||||
batch_number=f"{session_id[:8]}-EDGE-LOWSTOCK",
|
||||
quantity=3.0,
|
||||
received_date=session_time - timedelta(days=5),
|
||||
expiration_date=session_time + timedelta(days=10),
|
||||
best_before_date=session_time + timedelta(days=10),
|
||||
supplier_id=None,
|
||||
purchase_order_id=None,
|
||||
lot_number=f"LOT-LOWSTOCK-{session_id[:8]}",
|
||||
storage_location="Cámara Fría 1",
|
||||
quality_grade="GOOD",
|
||||
notes="⚠️ EDGE CASE: Below reorder point - triggers inventory alert if no pending PO"
|
||||
)
|
||||
db.add(low_stock)
|
||||
records_cloned += 1
|
||||
|
||||
# Edge Case 3: Just Received Stock (received today)
|
||||
if flour_ingredient_id:
|
||||
fresh_stock = Stock(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=str(virtual_tenant_id),
|
||||
inventory_product_id=flour_ingredient_id,
|
||||
batch_number=f"{session_id[:8]}-EDGE-FRESH",
|
||||
quantity=200.0,
|
||||
received_date=session_time - timedelta(hours=2),
|
||||
expiration_date=session_time + timedelta(days=180),
|
||||
best_before_date=session_time + timedelta(days=180),
|
||||
supplier_id=None,
|
||||
purchase_order_id=None,
|
||||
lot_number=f"LOT-FRESH-{session_id[:8]}",
|
||||
storage_location="Almacén A - Estante 1",
|
||||
quality_grade="EXCELLENT",
|
||||
notes="⚠️ EDGE CASE: Just received 2 hours ago - shows as new stock"
|
||||
)
|
||||
db.add(fresh_stock)
|
||||
records_cloned += 1
|
||||
|
||||
# Note: Edge cases are now handled exclusively through JSON seed data
|
||||
# The seed data files already contain comprehensive edge cases including:
|
||||
# - Low stock items below reorder points
|
||||
# - Items expiring soon
|
||||
# - Freshly received stock
|
||||
# This ensures standardization and single source of truth for demo data
|
||||
|
||||
logger.info(
|
||||
"Added deterministic edge case stock records",
|
||||
edge_cases_added=3,
|
||||
expiring_date=(session_time + timedelta(days=2)).isoformat(),
|
||||
low_stock_qty=3.0
|
||||
"Edge cases handled by JSON seed data - no manual creation needed",
|
||||
seed_data_edge_cases="low_stock, expiring_soon, fresh_stock"
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
@@ -462,7 +424,7 @@ async def clone_demo_data_internal(
|
||||
records_cloned=records_cloned,
|
||||
duration_ms=duration_ms,
|
||||
ingredients_cloned=len(seed_data.get('ingredients', [])),
|
||||
stock_batches_cloned=len(seed_data.get('stock_batches', []))
|
||||
stock_batches_cloned=len(seed_data.get('stock', []))
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -472,7 +434,7 @@ async def clone_demo_data_internal(
|
||||
"duration_ms": duration_ms,
|
||||
"details": {
|
||||
"ingredients": len(seed_data.get('ingredients', [])),
|
||||
"stock_batches": len(seed_data.get('stock_batches', [])),
|
||||
"stock": len(seed_data.get('stock', [])),
|
||||
"virtual_tenant_id": str(virtual_tenant_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ async def trigger_safety_stock_optimization(
|
||||
|
||||
try:
|
||||
# Fetch sales data for this product
|
||||
sales_response = await sales_client.get_sales_by_product(
|
||||
sales_response = await sales_client.get_sales_data(
|
||||
tenant_id=tenant_id,
|
||||
product_id=product_id,
|
||||
start_date=start_date.strftime('%Y-%m-%d'),
|
||||
|
||||
@@ -212,6 +212,9 @@ class DashboardService:
|
||||
ingredients = await repos['ingredient_repo'].get_ingredients_by_tenant(tenant_id, limit=1000)
|
||||
stock_summary = await repos['stock_repo'].get_stock_summary_by_tenant(tenant_id)
|
||||
|
||||
# Get dashboard repository
|
||||
dashboard_repo = repos['dashboard_repo']
|
||||
|
||||
# Get current stock levels for all ingredients using repository
|
||||
ingredient_stock_levels = {}
|
||||
try:
|
||||
@@ -693,6 +696,9 @@ class DashboardService:
|
||||
try:
|
||||
repos = self._get_repositories(db)
|
||||
|
||||
# Get dashboard repository
|
||||
dashboard_repo = repos['dashboard_repo']
|
||||
|
||||
# Get stock summary for total costs
|
||||
stock_summary = await repos['stock_repo'].get_stock_summary_by_tenant(tenant_id)
|
||||
total_inventory_cost = Decimal(str(stock_summary['total_stock_value']))
|
||||
@@ -703,7 +709,7 @@ class DashboardService:
|
||||
# Get current stock levels for all ingredients using repository
|
||||
ingredient_stock_levels = {}
|
||||
try:
|
||||
ingredient_stock_levels = await repos['dashboard_repo'].get_ingredient_stock_levels(tenant_id)
|
||||
ingredient_stock_levels = await dashboard_repo.get_ingredient_stock_levels(tenant_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch current stock levels for cost analysis: {e}")
|
||||
|
||||
|
||||
@@ -199,9 +199,14 @@ class InventoryScheduler:
|
||||
alerts_generated += 1
|
||||
|
||||
except Exception as e:
|
||||
# Ensure ingredient_id is converted to string for logging to prevent UUID issues
|
||||
ingredient_id_val = shortage.get("ingredient_id", "unknown")
|
||||
if hasattr(ingredient_id_val, '__str__') and not isinstance(ingredient_id_val, str):
|
||||
ingredient_id_val = str(ingredient_id_val)
|
||||
|
||||
logger.error(
|
||||
"Error emitting critical stock shortage alert",
|
||||
ingredient_id=shortage.get("ingredient_id", "unknown"),
|
||||
ingredient_id=ingredient_id_val,
|
||||
error=str(e)
|
||||
)
|
||||
continue
|
||||
@@ -531,10 +536,15 @@ class InventoryScheduler:
|
||||
alerts_generated += 1
|
||||
|
||||
except Exception as e:
|
||||
# Ensure ingredient_id is converted to string for logging to prevent UUID issues
|
||||
ingredient_id_val = shortage.get("id", "unknown")
|
||||
if hasattr(ingredient_id_val, '__str__') and not isinstance(ingredient_id_val, str):
|
||||
ingredient_id_val = str(ingredient_id_val)
|
||||
|
||||
logger.error(
|
||||
"Error emitting critical stock shortage alert",
|
||||
tenant_id=str(tenant_id),
|
||||
ingredient_id=shortage.get("id", "unknown"),
|
||||
ingredient_id=ingredient_id_val,
|
||||
error=str(e)
|
||||
)
|
||||
continue
|
||||
@@ -744,10 +754,19 @@ class InventoryScheduler:
|
||||
alerts_generated += 1
|
||||
|
||||
except Exception as e:
|
||||
# Ensure ingredient_id and tenant_id are converted to strings for logging to prevent UUID issues
|
||||
ingredient_id_val = shortage.get("id", "unknown")
|
||||
if hasattr(ingredient_id_val, '__str__') and not isinstance(ingredient_id_val, str):
|
||||
ingredient_id_val = str(ingredient_id_val)
|
||||
|
||||
tenant_id_val = shortage.get("tenant_id", "unknown")
|
||||
if hasattr(tenant_id_val, '__str__') and not isinstance(tenant_id_val, str):
|
||||
tenant_id_val = str(tenant_id_val)
|
||||
|
||||
logger.error(
|
||||
"Error emitting critical stock shortage alert",
|
||||
ingredient_id=shortage.get("id", "unknown"),
|
||||
tenant_id=shortage.get("tenant_id", "unknown"),
|
||||
ingredient_id=ingredient_id_val,
|
||||
tenant_id=tenant_id_val,
|
||||
error=str(e)
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -23,7 +23,7 @@ from app.models.production import (
|
||||
EquipmentStatus, EquipmentType
|
||||
)
|
||||
from shared.utils.demo_dates import (
|
||||
adjust_date_for_demo, resolve_time_marker, calculate_edge_case_times
|
||||
adjust_date_for_demo, resolve_time_marker
|
||||
)
|
||||
|
||||
from app.core.config import settings
|
||||
@@ -625,142 +625,17 @@ async def clone_demo_data(
|
||||
db.add(new_capacity)
|
||||
stats["production_capacity"] += 1
|
||||
|
||||
# Add deterministic edge case batches
|
||||
edge_times = calculate_edge_case_times(session_time)
|
||||
|
||||
# Get a sample product_id from existing batches for edge cases
|
||||
sample_product_id = None
|
||||
if seed_data.get('batches'):
|
||||
sample_product_id = seed_data['batches'][0].get('product_id')
|
||||
|
||||
if sample_product_id:
|
||||
# Edge Case 1: Overdue Batch (should have started 2 hours ago)
|
||||
overdue_batch = ProductionBatch(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=virtual_uuid,
|
||||
batch_number=f"{session_id[:8]}-EDGE-OVERDUE",
|
||||
product_id=sample_product_id,
|
||||
product_name="Pan Integral (Edge Case)",
|
||||
planned_start_time=edge_times["overdue_batch_planned_start"],
|
||||
planned_end_time=edge_times["overdue_batch_planned_start"] + timedelta(hours=3),
|
||||
planned_quantity=50.0,
|
||||
planned_duration_minutes=180,
|
||||
actual_start_time=None,
|
||||
actual_end_time=None,
|
||||
actual_quantity=None,
|
||||
status=ProductionStatus.PENDING,
|
||||
priority=ProductionPriority.URGENT,
|
||||
current_process_stage=None,
|
||||
production_notes="⚠️ EDGE CASE: Should have started 2 hours ago - triggers yellow alert for delayed production",
|
||||
created_at=session_time,
|
||||
updated_at=session_time
|
||||
)
|
||||
db.add(overdue_batch)
|
||||
stats["batches"] += 1
|
||||
|
||||
# Edge Case 2: In-Progress Batch (started 1h45m ago)
|
||||
in_progress_batch = ProductionBatch(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=virtual_uuid,
|
||||
batch_number=f"{session_id[:8]}-EDGE-INPROGRESS",
|
||||
product_id=sample_product_id,
|
||||
product_name="Croissant de Mantequilla (Edge Case)",
|
||||
planned_start_time=edge_times["in_progress_batch_actual_start"],
|
||||
planned_end_time=edge_times["upcoming_batch_planned_start"],
|
||||
planned_quantity=100.0,
|
||||
planned_duration_minutes=195,
|
||||
actual_start_time=edge_times["in_progress_batch_actual_start"],
|
||||
actual_end_time=None,
|
||||
actual_quantity=None,
|
||||
status=ProductionStatus.IN_PROGRESS,
|
||||
priority=ProductionPriority.HIGH,
|
||||
current_process_stage=ProcessStage.BAKING,
|
||||
production_notes="⚠️ EDGE CASE: Currently in progress - visible in active production dashboard",
|
||||
created_at=session_time,
|
||||
updated_at=session_time
|
||||
)
|
||||
db.add(in_progress_batch)
|
||||
stats["batches"] += 1
|
||||
|
||||
# Edge Case 3: Upcoming Batch (starts in 1.5 hours)
|
||||
upcoming_batch = ProductionBatch(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=virtual_uuid,
|
||||
batch_number=f"{session_id[:8]}-EDGE-UPCOMING",
|
||||
product_id=sample_product_id,
|
||||
product_name="Baguette Tradicional (Edge Case)",
|
||||
planned_start_time=edge_times["upcoming_batch_planned_start"],
|
||||
planned_end_time=edge_times["upcoming_batch_planned_start"] + timedelta(hours=2),
|
||||
planned_quantity=75.0,
|
||||
planned_duration_minutes=120,
|
||||
actual_start_time=None,
|
||||
actual_end_time=None,
|
||||
actual_quantity=None,
|
||||
status=ProductionStatus.PENDING,
|
||||
priority=ProductionPriority.MEDIUM,
|
||||
current_process_stage=None,
|
||||
production_notes="⚠️ EDGE CASE: Starting in 1.5 hours - visible in upcoming production schedule",
|
||||
created_at=session_time,
|
||||
updated_at=session_time
|
||||
)
|
||||
db.add(upcoming_batch)
|
||||
stats["batches"] += 1
|
||||
|
||||
# Edge Case 4: Evening Batch (starts at 17:00 today)
|
||||
evening_batch = ProductionBatch(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=virtual_uuid,
|
||||
batch_number=f"{session_id[:8]}-EDGE-EVENING",
|
||||
product_id=sample_product_id,
|
||||
product_name="Pan de Molde (Edge Case)",
|
||||
planned_start_time=edge_times["evening_batch_planned_start"],
|
||||
planned_end_time=edge_times["evening_batch_planned_start"] + timedelta(hours=2, minutes=30),
|
||||
planned_quantity=60.0,
|
||||
planned_duration_minutes=150,
|
||||
actual_start_time=None,
|
||||
actual_end_time=None,
|
||||
actual_quantity=None,
|
||||
status=ProductionStatus.PENDING,
|
||||
priority=ProductionPriority.MEDIUM,
|
||||
current_process_stage=None,
|
||||
production_notes="⚠️ EDGE CASE: Evening shift production - scheduled for 17:00",
|
||||
created_at=session_time,
|
||||
updated_at=session_time
|
||||
)
|
||||
db.add(evening_batch)
|
||||
stats["batches"] += 1
|
||||
|
||||
# Edge Case 5: Tomorrow Morning Batch (starts at 05:00 tomorrow)
|
||||
tomorrow_batch = ProductionBatch(
|
||||
id=str(uuid.uuid4()),
|
||||
tenant_id=virtual_uuid,
|
||||
batch_number=f"{session_id[:8]}-EDGE-TOMORROW",
|
||||
product_id=sample_product_id,
|
||||
product_name="Bollería Variada (Edge Case)",
|
||||
planned_start_time=edge_times["tomorrow_morning_planned_start"],
|
||||
planned_end_time=edge_times["tomorrow_morning_planned_start"] + timedelta(hours=4),
|
||||
planned_quantity=120.0,
|
||||
planned_duration_minutes=240,
|
||||
actual_start_time=None,
|
||||
actual_end_time=None,
|
||||
actual_quantity=None,
|
||||
status=ProductionStatus.PENDING,
|
||||
priority=ProductionPriority.MEDIUM,
|
||||
current_process_stage=None,
|
||||
production_notes="⚠️ EDGE CASE: Tomorrow morning production - scheduled for 05:00",
|
||||
created_at=session_time,
|
||||
updated_at=session_time
|
||||
)
|
||||
db.add(tomorrow_batch)
|
||||
stats["batches"] += 1
|
||||
|
||||
logger.info(
|
||||
"Added deterministic edge case batches",
|
||||
edge_cases_added=5,
|
||||
overdue=edge_times["overdue_batch_planned_start"].isoformat(),
|
||||
in_progress=edge_times["in_progress_batch_actual_start"].isoformat(),
|
||||
upcoming=edge_times["upcoming_batch_planned_start"].isoformat()
|
||||
)
|
||||
# Note: Edge cases are now handled exclusively through JSON seed data
|
||||
# The seed data files already contain comprehensive edge cases including:
|
||||
# - Overdue batches (should have started 2 hours ago)
|
||||
# - In-progress batches (currently being processed)
|
||||
# - Upcoming batches (scheduled for later today/tomorrow)
|
||||
# This ensures standardization and single source of truth for demo data
|
||||
|
||||
logger.info(
|
||||
"Edge cases handled by JSON seed data - no manual creation needed",
|
||||
seed_data_edge_cases="overdue_batches, in_progress_batches, upcoming_batches"
|
||||
)
|
||||
|
||||
# Commit cloned data
|
||||
await db.commit()
|
||||
|
||||
@@ -199,9 +199,9 @@ async def clone_demo_data(
|
||||
for sale_data in seed_data.get('sales_data', []):
|
||||
# Parse date field (supports BASE_TS markers and ISO timestamps)
|
||||
adjusted_date = parse_date_field(
|
||||
sale_data.get('sale_date'),
|
||||
sale_data.get('sales_date'),
|
||||
session_time,
|
||||
"sale_date"
|
||||
"sales_date"
|
||||
)
|
||||
|
||||
# Create new sales record with adjusted date
|
||||
@@ -210,14 +210,14 @@ async def clone_demo_data(
|
||||
tenant_id=virtual_uuid,
|
||||
date=adjusted_date,
|
||||
inventory_product_id=sale_data.get('product_id'), # Use product_id from seed data
|
||||
quantity_sold=sale_data.get('quantity_sold', 0.0),
|
||||
quantity_sold=sale_data.get('quantity', 0.0), # Map quantity to quantity_sold
|
||||
unit_price=sale_data.get('unit_price', 0.0),
|
||||
revenue=sale_data.get('total_revenue', 0.0),
|
||||
revenue=sale_data.get('total_amount', 0.0), # Map total_amount to revenue
|
||||
cost_of_goods=sale_data.get('cost_of_goods', 0.0),
|
||||
discount_applied=sale_data.get('discount_applied', 0.0),
|
||||
location_id=sale_data.get('location_id'),
|
||||
sales_channel=sale_data.get('sales_channel', 'IN_STORE'),
|
||||
source="demo_seed", # Mark as seeded
|
||||
source="demo_clone", # Mark as seeded
|
||||
is_validated=sale_data.get('is_validated', True),
|
||||
validation_notes=sale_data.get('validation_notes'),
|
||||
notes=sale_data.get('notes'),
|
||||
|
||||
@@ -101,13 +101,30 @@ class TenantMemberRepository(TenantBaseRepository):
|
||||
|
||||
# For internal service access, return None to indicate no user membership
|
||||
# Service access should be handled at the API layer
|
||||
if not is_valid_uuid and is_internal_service(user_id):
|
||||
# This is an internal service request, return None
|
||||
# Service access is granted at the API endpoint level
|
||||
logger.debug("Internal service detected in membership lookup",
|
||||
service=user_id,
|
||||
tenant_id=tenant_id)
|
||||
return None
|
||||
if not is_valid_uuid:
|
||||
if is_internal_service(user_id):
|
||||
# This is a known internal service request, return None
|
||||
# Service access is granted at the API endpoint level
|
||||
logger.debug("Internal service detected in membership lookup",
|
||||
service=user_id,
|
||||
tenant_id=tenant_id)
|
||||
return None
|
||||
elif user_id == "unknown-service":
|
||||
# Special handling for 'unknown-service' which commonly occurs in demo sessions
|
||||
# This happens when service identification fails during demo operations
|
||||
logger.warning("Demo session service identification issue",
|
||||
service=user_id,
|
||||
tenant_id=tenant_id,
|
||||
message="Service not properly identified - likely demo session context")
|
||||
return None
|
||||
else:
|
||||
# This is an unknown service
|
||||
# Return None to prevent database errors, but log a warning
|
||||
logger.warning("Unknown service detected in membership lookup",
|
||||
service=user_id,
|
||||
tenant_id=tenant_id,
|
||||
message="Service not in internal services registry")
|
||||
return None
|
||||
|
||||
memberships = await self.get_multi(
|
||||
filters={
|
||||
|
||||
@@ -40,6 +40,7 @@ INTERNAL_SERVICES: Set[str] = {
|
||||
"alert-service",
|
||||
"alert-processor-service",
|
||||
"demo-session-service",
|
||||
"demo-service", # Alternative name for demo session service
|
||||
"external-service",
|
||||
|
||||
# Enterprise services
|
||||
|
||||
@@ -39,13 +39,7 @@
|
||||
"recipe_id": null,
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
|
||||
"enterprise_shared": true,
|
||||
"shared_locations": [
|
||||
"Madrid Centro",
|
||||
"Barcelona Gràcia",
|
||||
"Valencia Ruzafa"
|
||||
]
|
||||
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7"
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000000002",
|
||||
@@ -86,13 +80,7 @@
|
||||
"recipe_id": null,
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
|
||||
"enterprise_shared": true,
|
||||
"shared_locations": [
|
||||
"Madrid Centro",
|
||||
"Barcelona Gràcia",
|
||||
"Valencia Ruzafa"
|
||||
]
|
||||
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000000001",
|
||||
@@ -134,13 +122,7 @@
|
||||
"recipe_id": "30000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7",
|
||||
"enterprise_shared": true,
|
||||
"shared_locations": [
|
||||
"Madrid Centro",
|
||||
"Barcelona Gràcia",
|
||||
"Valencia Ruzafa"
|
||||
]
|
||||
"created_by": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7"
|
||||
}
|
||||
],
|
||||
"stock": [
|
||||
@@ -148,46 +130,49 @@
|
||||
"id": "10000000-0000-0000-0000-000000001001",
|
||||
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000001",
|
||||
"quantity": 850.0,
|
||||
"location": "Central Warehouse - Madrid",
|
||||
"production_stage": "RAW_MATERIAL",
|
||||
"current_quantity": 850.0,
|
||||
"reserved_quantity": 0.0,
|
||||
"available_quantity": 850.0,
|
||||
"storage_location": "Central Warehouse - Madrid",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "APPROVED",
|
||||
"expiration_date": "BASE_TS + 180d 18h",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "ENT-HAR-20250115-001",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"enterprise_shared": true
|
||||
"updated_at": "BASE_TS"
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000001002",
|
||||
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000002",
|
||||
"quantity": 280.0,
|
||||
"location": "Central Warehouse - Madrid",
|
||||
"production_stage": "RAW_MATERIAL",
|
||||
"current_quantity": 280.0,
|
||||
"reserved_quantity": 0.0,
|
||||
"available_quantity": 280.0,
|
||||
"storage_location": "Central Warehouse - Madrid",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "APPROVED",
|
||||
"expiration_date": "BASE_TS + 30d 18h",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000002",
|
||||
"batch_number": "ENT-MAN-20250115-001",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"enterprise_shared": true
|
||||
"updated_at": "BASE_TS"
|
||||
},
|
||||
{
|
||||
"id": "20000000-0000-0000-0000-000000001001",
|
||||
"tenant_id": "80000000-0000-4000-a000-000000000001",
|
||||
"ingredient_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 120.0,
|
||||
"location": "Central Warehouse - Madrid",
|
||||
"production_stage": "FINISHED_PRODUCT",
|
||||
"current_quantity": 120.0,
|
||||
"reserved_quantity": 0.0,
|
||||
"available_quantity": 120.0,
|
||||
"storage_location": "Central Warehouse - Madrid",
|
||||
"production_stage": "fully_baked",
|
||||
"quality_status": "APPROVED",
|
||||
"expiration_date": "BASE_TS + 1d",
|
||||
"supplier_id": null,
|
||||
"batch_number": "ENT-BAG-20250115-001",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"enterprise_shared": true
|
||||
"updated_at": "BASE_TS"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1022,10 +1022,10 @@
|
||||
"id": "10000000-0000-0000-0000-000000001001",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000001",
|
||||
"quantity": 80.0,
|
||||
"current_quantity": 0,
|
||||
"reserved_quantity": 0.0,
|
||||
"available_quantity": 80.0,
|
||||
"location": "Almacén Principal - Zona A",
|
||||
"available_quantity": 0,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "BASE_TS + 180d 18h",
|
||||
@@ -1034,17 +1034,16 @@
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false,
|
||||
"notes": "⚠️ CRITICAL: Below reorder point (80 < 150) - NO pending PO - Should trigger RED alert"
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000001002",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000011",
|
||||
"quantity": 25.0,
|
||||
"current_quantity": 0,
|
||||
"reserved_quantity": 5.0,
|
||||
"available_quantity": 20.0,
|
||||
"location": "Almacén Refrigerado - Zona B",
|
||||
"available_quantity": 0,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "BASE_TS + 30d 18h",
|
||||
@@ -1053,17 +1052,16 @@
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false,
|
||||
"notes": "⚠️ LOW: Below reorder point (25 < 40) - Has pending PO (PO-2025-006) - Should show warning"
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000001003",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000021",
|
||||
"quantity": 8.0,
|
||||
"current_quantity": 4.46,
|
||||
"reserved_quantity": 2.0,
|
||||
"available_quantity": 6.0,
|
||||
"location": "Almacén Refrigerado - Zona C",
|
||||
"available_quantity": 2.46,
|
||||
"storage_location": "Almacén Refrigerado - Zona C",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "BASE_TS + 43d 18h",
|
||||
@@ -1072,17 +1070,16 @@
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false,
|
||||
"notes": "⚠️ LOW: Below reorder point (8 < 10) - Has pending PO (PO-2025-004-URGENT) - Critical for production"
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000001004",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000002",
|
||||
"quantity": 180.0,
|
||||
"current_quantity": 96.0,
|
||||
"reserved_quantity": 20.0,
|
||||
"available_quantity": 160.0,
|
||||
"location": "Almacén Principal - Zona A",
|
||||
"available_quantity": 76.0,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "BASE_TS + 150d 18h",
|
||||
@@ -1091,17 +1088,16 @@
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false,
|
||||
"notes": "Above reorder point - Normal stock level"
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "10000000-0000-0000-0000-000000001005",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000012",
|
||||
"quantity": 120.0,
|
||||
"current_quantity": 107.26,
|
||||
"reserved_quantity": 10.0,
|
||||
"available_quantity": 110.0,
|
||||
"location": "Almacén Refrigerado - Zona B",
|
||||
"available_quantity": 97.26,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "BASE_TS + 6d 18h",
|
||||
@@ -1110,8 +1106,367 @@
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false,
|
||||
"notes": "Above reorder point - Normal stock level"
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "fcb7b22d-147a-44d8-9290-ce9ee91f57bc",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000005",
|
||||
"current_quantity": 199.19,
|
||||
"reserved_quantity": 12.74,
|
||||
"available_quantity": 171.35,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-05-20T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "HAR-CEN-005-20250111-229",
|
||||
"created_at": "2025-01-09T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "a80f71c3-e0a9-4b48-b366-0c6c0dfa9abf",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "20000000-0000-0000-0000-000000000004",
|
||||
"current_quantity": 76.28,
|
||||
"reserved_quantity": 4.53,
|
||||
"available_quantity": 66.61,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-02-04T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "PRO-NAP-001-20250114-031",
|
||||
"created_at": "2025-01-10T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "e721aae2-6dc4-4ad9-a445-51779eff9a09",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "20000000-0000-0000-0000-000000000002",
|
||||
"current_quantity": 19.46,
|
||||
"reserved_quantity": 1.79,
|
||||
"available_quantity": 17.41,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-02-11T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "PRO-CRO-001-20250103-559",
|
||||
"created_at": "2025-01-12T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "6c5b7f4b-d125-462e-a74e-c46f55752bcd",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000031",
|
||||
"current_quantity": 62.5,
|
||||
"reserved_quantity": 5.72,
|
||||
"available_quantity": 53.36,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-05-05T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000004",
|
||||
"batch_number": "BAS-SAL-001-20250103-433",
|
||||
"created_at": "2025-01-08T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "d578fd7e-6d91-478c-b037-283127e415a9",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000043",
|
||||
"current_quantity": 39.28,
|
||||
"reserved_quantity": 3.32,
|
||||
"available_quantity": 34.43,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-04-27T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "ESP-PAS-003-20250109-868",
|
||||
"created_at": "2025-01-14T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "5e9f36df-de8f-4982-80e9-f38b8a59db76",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000042",
|
||||
"current_quantity": 79.51,
|
||||
"reserved_quantity": 6.31,
|
||||
"available_quantity": 72.59,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-06-02T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "ESP-ALM-002-20250113-566",
|
||||
"created_at": "2025-01-08T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "a6ef4470-42f9-4fc0-ab37-4ea9fc9c8fb8",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000045",
|
||||
"current_quantity": 42.91,
|
||||
"reserved_quantity": 3.12,
|
||||
"available_quantity": 37.71,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-04-23T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "ESP-CRE-005-20250114-678",
|
||||
"created_at": "2025-01-14T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "31510672-3ba8-4593-9ed6-7f35d508c187",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000033",
|
||||
"current_quantity": 0,
|
||||
"reserved_quantity": 11.51,
|
||||
"available_quantity": 0,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-06-02T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "BAS-AGU-003-20250110-465",
|
||||
"created_at": "2025-01-12T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "8cc5f11c-fae1-4484-89bd-9f608e88c6c0",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000022",
|
||||
"current_quantity": 11.03,
|
||||
"reserved_quantity": 0.63,
|
||||
"available_quantity": 10.08,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-07-13T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000006",
|
||||
"batch_number": "LEV-SEC-002-20250104-664",
|
||||
"created_at": "2025-01-10T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "6d59b4f2-6f9f-46e2-965c-e7fa269933da",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "20000000-0000-0000-0000-000000000003",
|
||||
"current_quantity": 67.78,
|
||||
"reserved_quantity": 7.73,
|
||||
"available_quantity": 61.39,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-02-03T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "PRO-PUE-001-20250110-948",
|
||||
"created_at": "2025-01-09T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "563fbfa1-093a-40a5-a147-4a636d1440df",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "20000000-0000-0000-0000-000000000001",
|
||||
"current_quantity": 50.87,
|
||||
"reserved_quantity": 2.71,
|
||||
"available_quantity": 44.85,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-01-23T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "PRO-BAG-001-20250111-842",
|
||||
"created_at": "2025-01-12T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "699b69e7-bc6f-428d-9b42-9f432eeabdf5",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000006",
|
||||
"current_quantity": 186.36,
|
||||
"reserved_quantity": 13.28,
|
||||
"available_quantity": 167.71,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-06-26T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "HAR-ESP-006-20250103-323",
|
||||
"created_at": "2025-01-09T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "7f826f83-5990-44e7-966d-c63478efc70e",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000023",
|
||||
"current_quantity": 0,
|
||||
"reserved_quantity": 1.12,
|
||||
"available_quantity": 0,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-01-29T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000006",
|
||||
"batch_number": "LEV-MAD-003-20250103-575",
|
||||
"created_at": "2025-01-11T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "27777a6e-7d84-4e93-8767-d5ed9af4753c",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000014",
|
||||
"current_quantity": 134.16,
|
||||
"reserved_quantity": 13.33,
|
||||
"available_quantity": 124.17,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-01-29T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000002",
|
||||
"batch_number": "LAC-HUE-004-20250112-522",
|
||||
"created_at": "2025-01-08T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "2e0744e4-003b-4758-9682-6c133fc680dd",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000032",
|
||||
"current_quantity": 24.98,
|
||||
"reserved_quantity": 1.7,
|
||||
"available_quantity": 21.6,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-07-11T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "BAS-AZU-002-20250108-611",
|
||||
"created_at": "2025-01-11T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "0638733f-1fec-4cff-963d-ac9799a1e5e3",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000041",
|
||||
"current_quantity": 69.89,
|
||||
"reserved_quantity": 4.55,
|
||||
"available_quantity": 65.34,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-04-16T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "ESP-CHO-001-20250104-739",
|
||||
"created_at": "2025-01-08T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "95b8322f-8e0b-42f6-93a8-dcc2ff23893a",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000003",
|
||||
"current_quantity": 200.74,
|
||||
"reserved_quantity": 13.21,
|
||||
"available_quantity": 170.69,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-04-29T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "HAR-FUE-003-20250110-446",
|
||||
"created_at": "2025-01-09T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "de8f7182-8f7c-4152-83f2-54c515c79b08",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000044",
|
||||
"current_quantity": 81.54,
|
||||
"reserved_quantity": 8.22,
|
||||
"available_quantity": 70.18,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-06-18T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "ESP-VAI-004-20250102-183",
|
||||
"created_at": "2025-01-09T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "7696385d-7afc-4194-b721-a75addeefdad",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000004",
|
||||
"current_quantity": 184.59,
|
||||
"reserved_quantity": 17.97,
|
||||
"available_quantity": 157.07,
|
||||
"storage_location": "Almacén Principal - Zona A",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-07-01T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"batch_number": "HAR-INT-004-20250111-157",
|
||||
"created_at": "2025-01-08T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
},
|
||||
{
|
||||
"id": "f1dca277-56a0-4e31-a642-94478b28c670",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"ingredient_id": "10000000-0000-0000-0000-000000000013",
|
||||
"current_quantity": 166.05,
|
||||
"reserved_quantity": 8.63,
|
||||
"available_quantity": 156.57,
|
||||
"storage_location": "Almacén Refrigerado - Zona B",
|
||||
"production_stage": "raw_ingredient",
|
||||
"quality_status": "good",
|
||||
"expiration_date": "2025-02-10T06:00:00Z",
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000002",
|
||||
"batch_number": "LAC-NAT-003-20250109-501",
|
||||
"created_at": "2025-01-08T06:00:00Z",
|
||||
"updated_at": "BASE_TS",
|
||||
"is_available": true,
|
||||
"is_expired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -261,7 +261,13 @@
|
||||
"priority": "HIGH",
|
||||
"current_process_stage": null,
|
||||
"process_stage_history": null,
|
||||
"pending_quality_checks": null,
|
||||
"pending_quality_checks": [
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000004",
|
||||
"check_type": "visual_inspection",
|
||||
"status": "pending"
|
||||
}
|
||||
],
|
||||
"completed_quality_checks": null,
|
||||
"estimated_cost": 150.0,
|
||||
"actual_cost": null,
|
||||
@@ -419,16 +425,24 @@
|
||||
"current_process_stage": "packaging",
|
||||
"process_stage_history": null,
|
||||
"pending_quality_checks": null,
|
||||
"completed_quality_checks": null,
|
||||
"completed_quality_checks": [
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000001",
|
||||
"check_type": "visual_inspection",
|
||||
"status": "completed",
|
||||
"result": "passed",
|
||||
"quality_score": 9.5
|
||||
}
|
||||
],
|
||||
"estimated_cost": 150.0,
|
||||
"actual_cost": 148.5,
|
||||
"labor_cost": 80.0,
|
||||
"material_cost": 55.0,
|
||||
"overhead_cost": 13.5,
|
||||
"yield_percentage": 98.0,
|
||||
"quality_score": 95.0,
|
||||
"quality_score": 9.5,
|
||||
"waste_quantity": 2.0,
|
||||
"defect_quantity": 0.0,
|
||||
"defect_quantity": 2.0,
|
||||
"waste_defect_type": "burnt",
|
||||
"equipment_used": [
|
||||
"30000000-0000-0000-0000-000000000001"
|
||||
@@ -469,16 +483,24 @@
|
||||
"current_process_stage": "packaging",
|
||||
"process_stage_history": null,
|
||||
"pending_quality_checks": null,
|
||||
"completed_quality_checks": null,
|
||||
"completed_quality_checks": [
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000002",
|
||||
"check_type": "dimensional_check",
|
||||
"status": "completed",
|
||||
"result": "passed",
|
||||
"quality_score": 9.2
|
||||
}
|
||||
],
|
||||
"estimated_cost": 280.0,
|
||||
"actual_cost": 275.0,
|
||||
"labor_cost": 120.0,
|
||||
"material_cost": 125.0,
|
||||
"overhead_cost": 30.0,
|
||||
"yield_percentage": 95.8,
|
||||
"quality_score": 92.0,
|
||||
"quality_score": 9.2,
|
||||
"waste_quantity": 3.0,
|
||||
"defect_quantity": 2.0,
|
||||
"defect_quantity": 3.0,
|
||||
"waste_defect_type": "misshapen",
|
||||
"equipment_used": [
|
||||
"30000000-0000-0000-0000-000000000002",
|
||||
@@ -572,11 +594,11 @@
|
||||
"pending_quality_checks": null,
|
||||
"completed_quality_checks": [
|
||||
{
|
||||
"control_id": "70000000-0000-0000-0000-000000000003",
|
||||
"control_type": "taste_test",
|
||||
"result": "FAILED",
|
||||
"quality_score": 65.0,
|
||||
"control_date": "2025-01-09T14:30:00Z"
|
||||
"id": "70000000-0000-0000-0000-000000000003",
|
||||
"check_type": "taste_test",
|
||||
"status": "completed",
|
||||
"result": "failed",
|
||||
"quality_score": 6.5
|
||||
}
|
||||
],
|
||||
"estimated_cost": 220.0,
|
||||
@@ -585,9 +607,9 @@
|
||||
"material_cost": 98.0,
|
||||
"overhead_cost": 25.0,
|
||||
"yield_percentage": 97.8,
|
||||
"quality_score": 65.0,
|
||||
"quality_score": 6.5,
|
||||
"waste_quantity": 1.0,
|
||||
"defect_quantity": 1.0,
|
||||
"defect_quantity": 10.0,
|
||||
"waste_defect_type": "off_taste",
|
||||
"equipment_used": [
|
||||
"30000000-0000-0000-0000-000000000001",
|
||||
@@ -1131,7 +1153,13 @@
|
||||
"priority": "MEDIUM",
|
||||
"current_process_stage": "baking",
|
||||
"process_stage_history": null,
|
||||
"pending_quality_checks": null,
|
||||
"pending_quality_checks": [
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000004",
|
||||
"check_type": "visual_inspection",
|
||||
"status": "pending"
|
||||
}
|
||||
],
|
||||
"completed_quality_checks": null,
|
||||
"estimated_cost": 150.0,
|
||||
"actual_cost": null,
|
||||
@@ -1615,5 +1643,93 @@
|
||||
"updated_at": "BASE_TS",
|
||||
"completed_at": null
|
||||
}
|
||||
],
|
||||
"quality_checks": [
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000001",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"batch_id": "40000000-0000-0000-0000-000000000001",
|
||||
"check_type": "visual_inspection",
|
||||
"check_time": "2025-01-08T14:30:00Z",
|
||||
"checker_id": "50000000-0000-0000-0000-000000000007",
|
||||
"quality_score": 9.5,
|
||||
"pass_fail": true,
|
||||
"defect_count": 2,
|
||||
"defect_types": [
|
||||
{
|
||||
"defect_type": "burnt",
|
||||
"quantity": 2.0,
|
||||
"severity": "minor"
|
||||
}
|
||||
],
|
||||
"check_notes": "Excelente aspecto y textura, 2 unidades con quemaduras leves (dentro de tolerancia)",
|
||||
"corrective_actions": null,
|
||||
"created_at": "BASE_TS - 7d 8h 30m",
|
||||
"updated_at": "BASE_TS - 7d 8h 45m"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000002",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"batch_id": "40000000-0000-0000-0000-000000000002",
|
||||
"check_type": "dimensional_check",
|
||||
"check_time": "2025-01-08T14:45:00Z",
|
||||
"checker_id": "50000000-0000-0000-0000-000000000007",
|
||||
"quality_score": 9.2,
|
||||
"pass_fail": true,
|
||||
"defect_count": 3,
|
||||
"defect_types": [
|
||||
{
|
||||
"defect_type": "misshapen",
|
||||
"quantity": 3.0,
|
||||
"severity": "minor"
|
||||
}
|
||||
],
|
||||
"check_notes": "Buen desarrollo y laminado, 3 unidades con forma irregular (dentro de tolerancia)",
|
||||
"corrective_actions": null,
|
||||
"created_at": "BASE_TS - 7d 8h 45m",
|
||||
"updated_at": "BASE_TS - 7d 9h"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000003",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"batch_id": "40000000-0000-0000-0000-000000000004",
|
||||
"check_type": "taste_test",
|
||||
"check_time": "2025-01-09T14:30:00Z",
|
||||
"checker_id": "50000000-0000-0000-0000-000000000007",
|
||||
"quality_score": 6.5,
|
||||
"pass_fail": false,
|
||||
"defect_count": 10,
|
||||
"defect_types": [
|
||||
{
|
||||
"defect_type": "off_taste",
|
||||
"quantity": 10.0,
|
||||
"severity": "major"
|
||||
}
|
||||
],
|
||||
"check_notes": "⚠️ CRITICAL: Sabor amargo en el chocolate, posible problema con proveedor de cacao",
|
||||
"corrective_actions": [
|
||||
"Lote puesto en cuarentena",
|
||||
"Notificado proveedor de chocolate",
|
||||
"Programada nueva prueba con muestra diferente"
|
||||
],
|
||||
"created_at": "BASE_TS - 6d 8h 30m",
|
||||
"updated_at": "BASE_TS - 6d 9h"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000004",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"batch_id": "40000000-0000-0000-0000-000000000015",
|
||||
"check_type": "visual_inspection",
|
||||
"check_time": "BASE_TS + 0h",
|
||||
"checker_id": null,
|
||||
"quality_score": 0.0,
|
||||
"pass_fail": false,
|
||||
"defect_count": 0,
|
||||
"defect_types": null,
|
||||
"check_notes": "⚠️ PENDING: Control de calidad programado para lote en producción",
|
||||
"corrective_actions": null,
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,11 +11,11 @@
|
||||
"required_delivery_date": "BASE_TS - 4h",
|
||||
"estimated_delivery_date": "BASE_TS - 4h",
|
||||
"expected_delivery_date": "BASE_TS - 4h",
|
||||
"subtotal": 500.0,
|
||||
"tax_amount": 105.0,
|
||||
"subtotal": 510.0,
|
||||
"tax_amount": 107.1,
|
||||
"shipping_cost": 20.0,
|
||||
"discount_amount": 0.0,
|
||||
"total_amount": 625.0,
|
||||
"total_amount": 637.1,
|
||||
"currency": "EUR",
|
||||
"delivery_address": "Calle Panadería, 45, 28001 Madrid",
|
||||
"delivery_instructions": "URGENTE: Entrega en almacén trasero",
|
||||
@@ -26,6 +26,32 @@
|
||||
"supplier_confirmation_date": "BASE_TS - 23h",
|
||||
"supplier_reference": "SUP-REF-LATE-001",
|
||||
"notes": "⚠️ EDGE CASE: Delivery should have arrived 4 hours ago - will trigger red supplier delay alert",
|
||||
"reasoning_data": {
|
||||
"type": "low_stock_detection",
|
||||
"parameters": {
|
||||
"supplier_name": "Harinas del Norte",
|
||||
"product_names": ["Harina de Trigo T55"],
|
||||
"product_count": 1,
|
||||
"current_stock": 15,
|
||||
"required_stock": 150,
|
||||
"days_until_stockout": 1,
|
||||
"threshold_percentage": 20,
|
||||
"stock_percentage": 10
|
||||
},
|
||||
"consequence": {
|
||||
"type": "stockout_risk",
|
||||
"severity": "high",
|
||||
"impact_days": 1,
|
||||
"affected_products": ["Baguette Tradicional", "Pan de Pueblo"],
|
||||
"estimated_lost_orders": 25
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"ai_assisted": true,
|
||||
"delivery_delayed": true,
|
||||
"delay_hours": 4
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000005"
|
||||
},
|
||||
{
|
||||
@@ -39,11 +65,11 @@
|
||||
"required_delivery_date": "BASE_TS + 2h30m",
|
||||
"estimated_delivery_date": "BASE_TS + 2h30m",
|
||||
"expected_delivery_date": "BASE_TS + 2h30m",
|
||||
"subtotal": 300.0,
|
||||
"tax_amount": 63.0,
|
||||
"subtotal": 303.5,
|
||||
"tax_amount": 63.74,
|
||||
"shipping_cost": 15.0,
|
||||
"discount_amount": 0.0,
|
||||
"total_amount": 378.0,
|
||||
"total_amount": 382.24,
|
||||
"currency": "EUR",
|
||||
"delivery_address": "Calle Panadería, 45, 28001 Madrid",
|
||||
"delivery_instructions": "Mantener refrigerado",
|
||||
@@ -54,6 +80,27 @@
|
||||
"supplier_confirmation_date": "BASE_TS - 30m",
|
||||
"supplier_reference": "SUP-REF-UPCOMING-001",
|
||||
"notes": "⚠️ EDGE CASE: Delivery expected in 2.5 hours - will show in upcoming deliveries",
|
||||
"reasoning_data": {
|
||||
"type": "production_requirement",
|
||||
"parameters": {
|
||||
"supplier_name": "Lácteos Gipuzkoa",
|
||||
"product_names": ["Mantequilla sin Sal", "Leche Entera"],
|
||||
"product_count": 2,
|
||||
"production_batches": 3,
|
||||
"required_by_date": "tomorrow morning"
|
||||
},
|
||||
"consequence": {
|
||||
"type": "production_delay",
|
||||
"severity": "high",
|
||||
"impact": "blocked_production"
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"ai_assisted": true,
|
||||
"upcoming_delivery": true,
|
||||
"hours_until_delivery": 2.5
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000005"
|
||||
},
|
||||
{
|
||||
@@ -63,11 +110,11 @@
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"status": "completed",
|
||||
"priority": "normal",
|
||||
"subtotal": 850.0,
|
||||
"tax_amount": 178.5,
|
||||
"subtotal": 760.0,
|
||||
"tax_amount": 159.6,
|
||||
"shipping_cost": 25.0,
|
||||
"discount_amount": 0.0,
|
||||
"total_amount": 1053.5,
|
||||
"total_amount": 944.6,
|
||||
"currency": "EUR",
|
||||
"delivery_address": "Calle Panadería, 45, 28001 Madrid",
|
||||
"delivery_instructions": "Entrega en almacén trasero",
|
||||
@@ -76,6 +123,28 @@
|
||||
"requires_approval": false,
|
||||
"supplier_reference": "SUP-REF-2025-001",
|
||||
"notes": "Pedido habitual semanal de harinas",
|
||||
"reasoning_data": {
|
||||
"type": "safety_stock_replenishment",
|
||||
"parameters": {
|
||||
"supplier_name": "Harinas del Norte",
|
||||
"product_names": ["Harina de Trigo T55", "Harina de Trigo T65", "Harina de Centeno", "Sal Marina Fina"],
|
||||
"product_count": 4,
|
||||
"current_safety_stock": 120,
|
||||
"target_safety_stock": 300,
|
||||
"reorder_point": 150
|
||||
},
|
||||
"consequence": {
|
||||
"type": "stockout_risk",
|
||||
"severity": "medium",
|
||||
"impact": "reduced_buffer"
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"ai_assisted": true,
|
||||
"recurring_order": true,
|
||||
"schedule": "weekly"
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000005",
|
||||
"order_date": "BASE_TS - 7d",
|
||||
"required_delivery_date": "BASE_TS - 2d",
|
||||
@@ -104,6 +173,28 @@
|
||||
"requires_approval": false,
|
||||
"supplier_reference": "LGIPUZ-2025-042",
|
||||
"notes": "Pedido de lácteos para producción semanal",
|
||||
"reasoning_data": {
|
||||
"type": "forecast_demand",
|
||||
"parameters": {
|
||||
"supplier_name": "Lácteos Gipuzkoa",
|
||||
"product_names": ["Mantequilla sin Sal 82% MG"],
|
||||
"product_count": 1,
|
||||
"forecast_period_days": 7,
|
||||
"total_demand": 80,
|
||||
"forecast_confidence": 88
|
||||
},
|
||||
"consequence": {
|
||||
"type": "insufficient_supply",
|
||||
"severity": "medium",
|
||||
"impact_days": 7
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"forecast_confidence": 0.88,
|
||||
"ai_assisted": true,
|
||||
"perishable_goods": true
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000005",
|
||||
"order_date": "BASE_TS - 5d",
|
||||
"required_delivery_date": "BASE_TS - 1d",
|
||||
@@ -119,11 +210,11 @@
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000003",
|
||||
"status": "approved",
|
||||
"priority": "high",
|
||||
"subtotal": 450.0,
|
||||
"tax_amount": 94.5,
|
||||
"subtotal": 490.0,
|
||||
"tax_amount": 102.9,
|
||||
"shipping_cost": 20.0,
|
||||
"discount_amount": 22.5,
|
||||
"total_amount": 542.0,
|
||||
"discount_amount": 24.5,
|
||||
"total_amount": 588.4,
|
||||
"currency": "EUR",
|
||||
"delivery_address": "Calle Panadería, 45, 28001 Madrid",
|
||||
"delivery_instructions": "Requiere inspección de calidad",
|
||||
@@ -135,16 +226,24 @@
|
||||
"approved_by": "50000000-0000-0000-0000-000000000006",
|
||||
"notes": "Pedido urgente para nueva línea de productos ecológicos - Auto-aprobado por IA",
|
||||
"reasoning_data": {
|
||||
"job": "ensure_quality_ingredients",
|
||||
"context": {
|
||||
"en": "Organic ingredients needed for new product line",
|
||||
"es": "Ingredientes ecológicos necesarios para nueva línea de productos",
|
||||
"eu": "Produktu lerro berrirako osagai ekologikoak behar dira"
|
||||
"type": "supplier_contract",
|
||||
"parameters": {
|
||||
"supplier_name": "Productos Ecológicos del Norte",
|
||||
"product_names": ["Organic ingredients"],
|
||||
"product_count": 1,
|
||||
"contract_terms": "certified_supplier",
|
||||
"contract_quantity": 450.0
|
||||
},
|
||||
"decision": {
|
||||
"en": "Auto-approved: Under €500 threshold and from certified supplier",
|
||||
"es": "Auto-aprobado: Bajo umbral de €500 y de proveedor certificado",
|
||||
"eu": "Auto-onartuta: €500ko mugaren azpian eta hornitzaile ziurtatutik"
|
||||
"consequence": {
|
||||
"type": "quality_assurance",
|
||||
"severity": "medium",
|
||||
"impact": "new_product_line_delay"
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "manual",
|
||||
"ai_assisted": true,
|
||||
"auto_approved": true,
|
||||
"auto_approval_rule_id": "10000000-0000-0000-0000-000000000001"
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000005",
|
||||
@@ -161,11 +260,11 @@
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"status": "confirmed",
|
||||
"priority": "urgent",
|
||||
"subtotal": 1200.0,
|
||||
"tax_amount": 252.0,
|
||||
"subtotal": 1040.0,
|
||||
"tax_amount": 218.4,
|
||||
"shipping_cost": 35.0,
|
||||
"discount_amount": 60.0,
|
||||
"total_amount": 1427.0,
|
||||
"discount_amount": 52.0,
|
||||
"total_amount": 1241.4,
|
||||
"currency": "EUR",
|
||||
"delivery_address": "Calle Panadería, 45, 28001 Madrid",
|
||||
"delivery_instructions": "URGENTE - Entrega antes de las 10:00 AM",
|
||||
@@ -175,16 +274,29 @@
|
||||
"supplier_reference": "SUP-URGENT-2025-005",
|
||||
"notes": "EDGE CASE: Entrega retrasada - debió llegar hace 4 horas. Stock crítico de harina",
|
||||
"reasoning_data": {
|
||||
"job": "avoid_production_stoppage",
|
||||
"context": {
|
||||
"en": "Critical flour shortage - production at risk",
|
||||
"es": "Escasez crítica de harina - producción en riesgo",
|
||||
"eu": "Irina-faltagatik ekoizpena arriskuan"
|
||||
"type": "low_stock_detection",
|
||||
"parameters": {
|
||||
"supplier_name": "Harinas del Norte",
|
||||
"product_names": ["Harina de Trigo T55", "Levadura Fresca"],
|
||||
"product_count": 2,
|
||||
"current_stock": 0,
|
||||
"required_stock": 1000,
|
||||
"days_until_stockout": 0,
|
||||
"threshold_percentage": 20,
|
||||
"stock_percentage": 0
|
||||
},
|
||||
"urgency": {
|
||||
"en": "Urgent: Delivery delayed 4 hours, affecting today's production",
|
||||
"es": "Urgente: Entrega retrasada 4 horas, afectando la producción de hoy",
|
||||
"eu": "Presazkoa: Entrega 4 ordu berandu, gaurko ekoizpena eraginda"
|
||||
"consequence": {
|
||||
"type": "stockout_risk",
|
||||
"severity": "critical",
|
||||
"impact_days": 0,
|
||||
"affected_products": ["Baguette Tradicional", "Croissant"],
|
||||
"estimated_lost_orders": 50
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"ai_assisted": true,
|
||||
"delivery_delayed": true,
|
||||
"delay_hours": 4
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000006",
|
||||
@@ -215,6 +327,27 @@
|
||||
"requires_approval": false,
|
||||
"supplier_reference": "SUP-REF-2025-007",
|
||||
"notes": "Pedido de ingredientes especiales para línea premium - Entregado hace 5 días",
|
||||
"reasoning_data": {
|
||||
"type": "seasonal_demand",
|
||||
"parameters": {
|
||||
"supplier_name": "Ingredientes Premium del Sur",
|
||||
"product_names": ["Chocolate Negro 70% Cacao", "Almendras Laminadas", "Pasas de Corinto"],
|
||||
"product_count": 3,
|
||||
"season": "winter",
|
||||
"expected_demand_increase_pct": 35
|
||||
},
|
||||
"consequence": {
|
||||
"type": "missed_opportunity",
|
||||
"severity": "medium",
|
||||
"impact": "lost_seasonal_sales"
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"ai_assisted": true,
|
||||
"premium_line": true,
|
||||
"seasonal": true
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000005",
|
||||
"order_date": "BASE_TS - 7d",
|
||||
"required_delivery_date": "BASE_TS - 5d",
|
||||
@@ -230,11 +363,11 @@
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000004",
|
||||
"status": "draft",
|
||||
"priority": "normal",
|
||||
"subtotal": 280.0,
|
||||
"tax_amount": 58.8,
|
||||
"subtotal": 303.7,
|
||||
"tax_amount": 63.78,
|
||||
"shipping_cost": 12.0,
|
||||
"discount_amount": 0.0,
|
||||
"total_amount": 350.8,
|
||||
"total_amount": 379.48,
|
||||
"currency": "EUR",
|
||||
"delivery_address": "Calle Panadería, 45, 28001 Madrid",
|
||||
"delivery_instructions": "Llamar antes de entregar",
|
||||
@@ -242,6 +375,28 @@
|
||||
"delivery_phone": "+34 910 123 456",
|
||||
"requires_approval": false,
|
||||
"notes": "Pedido planificado para reposición semanal",
|
||||
"reasoning_data": {
|
||||
"type": "forecast_demand",
|
||||
"parameters": {
|
||||
"supplier_name": "Ingredientes Premium del Sur",
|
||||
"product_names": ["Specialty ingredients"],
|
||||
"product_count": 1,
|
||||
"forecast_period_days": 7,
|
||||
"total_demand": 280,
|
||||
"forecast_confidence": 82
|
||||
},
|
||||
"consequence": {
|
||||
"type": "insufficient_supply",
|
||||
"severity": "low",
|
||||
"impact_days": 7
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"forecast_confidence": 0.82,
|
||||
"ai_assisted": true,
|
||||
"draft_order": true
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000005",
|
||||
"order_date": "BASE_TS",
|
||||
"required_delivery_date": "BASE_TS + 3d",
|
||||
@@ -267,6 +422,27 @@
|
||||
"delivery_phone": "+34 910 123 456",
|
||||
"requires_approval": false,
|
||||
"notes": "⏰ EDGE CASE: Entrega esperada en 6 horas - mantequilla para producción de croissants de mañana",
|
||||
"reasoning_data": {
|
||||
"type": "production_requirement",
|
||||
"parameters": {
|
||||
"supplier_name": "Lácteos Gipuzkoa",
|
||||
"product_names": ["Mantequilla sin Sal 82% MG"],
|
||||
"product_count": 1,
|
||||
"production_batches": 5,
|
||||
"required_by_date": "tomorrow 06:00"
|
||||
},
|
||||
"consequence": {
|
||||
"type": "production_delay",
|
||||
"severity": "high",
|
||||
"impact": "blocked_production"
|
||||
},
|
||||
"metadata": {
|
||||
"trigger_source": "orchestrator_auto",
|
||||
"ai_assisted": true,
|
||||
"urgent_production": true,
|
||||
"hours_until_needed": 12
|
||||
}
|
||||
},
|
||||
"created_by": "50000000-0000-0000-0000-000000000006",
|
||||
"order_date": "BASE_TS - 0.5d",
|
||||
"required_delivery_date": "BASE_TS + 0.25d",
|
||||
@@ -309,8 +485,8 @@
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000001",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000005",
|
||||
"product_name": "Harina Centeno",
|
||||
"product_code": "HAR-CENT-005",
|
||||
"product_name": "Harina de Centeno",
|
||||
"product_code": "HAR-CEN-005",
|
||||
"ordered_quantity": 100.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 1.15,
|
||||
@@ -322,13 +498,13 @@
|
||||
"id": "51000000-0000-0000-0000-000000000004",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000001",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000006",
|
||||
"product_name": "Sal Marina",
|
||||
"product_code": "SAL-MAR-006",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000031",
|
||||
"product_name": "Sal Marina Fina",
|
||||
"product_code": "BAS-SAL-001",
|
||||
"ordered_quantity": 50.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 2.4,
|
||||
"line_total": 120.0,
|
||||
"unit_price": 0.6,
|
||||
"line_total": 30.0,
|
||||
"received_quantity": 50.0,
|
||||
"remaining_quantity": 0.0
|
||||
},
|
||||
@@ -338,7 +514,7 @@
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000002",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000011",
|
||||
"product_name": "Mantequilla sin Sal 82% MG",
|
||||
"product_code": "MANT-001",
|
||||
"product_code": "LAC-MAN-001",
|
||||
"ordered_quantity": 80.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 4.0,
|
||||
@@ -365,13 +541,13 @@
|
||||
"id": "51000000-0000-0000-0000-000000000007",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000004",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000011",
|
||||
"product_name": "Levadura Fresca",
|
||||
"product_code": "LEV-FRESC-001",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000021",
|
||||
"product_name": "Levadura Fresca de Panadería",
|
||||
"product_code": "LEV-FRE-001",
|
||||
"ordered_quantity": 50.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 8.0,
|
||||
"line_total": 400.0,
|
||||
"unit_price": 4.8,
|
||||
"line_total": 240.0,
|
||||
"received_quantity": 0.0,
|
||||
"remaining_quantity": 50.0,
|
||||
"notes": "Stock agotado - prioridad máxima"
|
||||
@@ -382,7 +558,7 @@
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000006",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000011",
|
||||
"product_name": "Mantequilla sin Sal 82% MG",
|
||||
"product_code": "MANT-001",
|
||||
"product_code": "LAC-MAN-001",
|
||||
"ordered_quantity": 30.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 6.5,
|
||||
@@ -396,7 +572,7 @@
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000007",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000041",
|
||||
"product_name": "Chocolate Negro 70% Cacao",
|
||||
"product_code": "CHO-NEG-001",
|
||||
"product_code": "ESP-CHO-001",
|
||||
"ordered_quantity": 20.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 15.5,
|
||||
@@ -410,7 +586,7 @@
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000007",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000042",
|
||||
"product_name": "Almendras Laminadas",
|
||||
"product_code": "ALM-LAM-001",
|
||||
"product_code": "ESP-ALM-002",
|
||||
"ordered_quantity": 15.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 8.9,
|
||||
@@ -424,13 +600,99 @@
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000007",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000043",
|
||||
"product_name": "Pasas de Corinto",
|
||||
"product_code": "PAS-COR-001",
|
||||
"product_code": "ESP-PAS-003",
|
||||
"ordered_quantity": 10.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 4.5,
|
||||
"line_total": 45.0,
|
||||
"received_quantity": 10.0,
|
||||
"remaining_quantity": 0.0
|
||||
},
|
||||
{
|
||||
"id": "51000000-0000-0000-0000-0000000000a1",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-0000000000c1",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Harina de Trigo T55",
|
||||
"product_code": "HAR-T55-001",
|
||||
"ordered_quantity": 600.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 0.85,
|
||||
"line_total": 510.0,
|
||||
"received_quantity": 0.0,
|
||||
"remaining_quantity": 600.0,
|
||||
"notes": "URGENTE - Pedido retrasado 4 horas"
|
||||
},
|
||||
{
|
||||
"id": "51000000-0000-0000-0000-0000000000a2",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-0000000000c2",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000011",
|
||||
"product_name": "Mantequilla sin Sal 82% MG",
|
||||
"product_code": "LAC-MAN-001",
|
||||
"ordered_quantity": 35.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 6.5,
|
||||
"line_total": 227.5,
|
||||
"received_quantity": 0.0,
|
||||
"remaining_quantity": 35.0
|
||||
},
|
||||
{
|
||||
"id": "51000000-0000-0000-0000-0000000000a3",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-0000000000c2",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000012",
|
||||
"product_name": "Leche Entera Fresca",
|
||||
"product_code": "LAC-LEC-002",
|
||||
"ordered_quantity": 80.0,
|
||||
"unit_of_measure": "liters",
|
||||
"unit_price": 0.95,
|
||||
"line_total": 76.0,
|
||||
"received_quantity": 0.0,
|
||||
"remaining_quantity": 80.0
|
||||
},
|
||||
{
|
||||
"id": "51000000-0000-0000-0000-0000000000a4",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000003",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000006",
|
||||
"product_name": "Harina de Espelta Ecológica",
|
||||
"product_code": "HAR-ESP-006",
|
||||
"ordered_quantity": 200.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 2.45,
|
||||
"line_total": 490.0,
|
||||
"received_quantity": 0.0,
|
||||
"remaining_quantity": 200.0,
|
||||
"notes": "Ingrediente ecológico certificado para nueva línea"
|
||||
},
|
||||
{
|
||||
"id": "51000000-0000-0000-0000-0000000000a5",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000005",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000041",
|
||||
"product_name": "Chocolate Negro 70% Cacao",
|
||||
"product_code": "ESP-CHO-001",
|
||||
"ordered_quantity": 15.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 15.5,
|
||||
"line_total": 232.5,
|
||||
"received_quantity": 0.0,
|
||||
"remaining_quantity": 15.0
|
||||
},
|
||||
{
|
||||
"id": "51000000-0000-0000-0000-0000000000a6",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"purchase_order_id": "50000000-0000-0000-0000-000000000005",
|
||||
"inventory_product_id": "10000000-0000-0000-0000-000000000042",
|
||||
"product_name": "Almendras Laminadas",
|
||||
"product_code": "ESP-ALM-002",
|
||||
"ordered_quantity": 8.0,
|
||||
"unit_of_measure": "kilograms",
|
||||
"unit_price": 8.9,
|
||||
"line_total": 71.2,
|
||||
"received_quantity": 0.0,
|
||||
"remaining_quantity": 8.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,72 +1,620 @@
|
||||
{
|
||||
"sales_data": [
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000001",
|
||||
"id": "SALES-202501-2287",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"sale_date": "2025-01-14T10:00:00Z",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity_sold": 45.0,
|
||||
"unit_price": 2.5,
|
||||
"total_revenue": 112.5,
|
||||
"sales_channel": "IN_STORE",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Regular daily sales"
|
||||
"quantity": 51.11,
|
||||
"unit_price": 6.92,
|
||||
"total_amount": 335.29,
|
||||
"sales_date": "BASE_TS - 7d 4h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 7d 4h",
|
||||
"updated_at": "BASE_TS - 7d 4h"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000002",
|
||||
"id": "SALES-202501-1536",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 48.29,
|
||||
"unit_price": 3.81,
|
||||
"total_amount": 267.17,
|
||||
"sales_date": "BASE_TS - 7d 6h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 7d 6h",
|
||||
"updated_at": "BASE_TS - 7d 6h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-7360",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"sale_date": "2025-01-14T11:00:00Z",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity_sold": 10.0,
|
||||
"unit_price": 3.75,
|
||||
"total_revenue": 37.5,
|
||||
"sales_channel": "IN_STORE",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Morning croissant sales"
|
||||
"quantity": 28.45,
|
||||
"unit_price": 6.04,
|
||||
"total_amount": 209.32,
|
||||
"sales_date": "BASE_TS - 7d 3h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 7d 3h",
|
||||
"updated_at": "BASE_TS - 7d 3h"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000003",
|
||||
"id": "SALES-202501-2548",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 28.42,
|
||||
"unit_price": 3.79,
|
||||
"total_amount": 201.24,
|
||||
"sales_date": "BASE_TS - 7d 4h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 7d 4h",
|
||||
"updated_at": "BASE_TS - 7d 4h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-5636",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 25.07,
|
||||
"unit_price": 6.12,
|
||||
"total_amount": 184.07,
|
||||
"sales_date": "BASE_TS - 7d 4h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 7d 4h",
|
||||
"updated_at": "BASE_TS - 7d 4h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-6202",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 26.71,
|
||||
"unit_price": 2.97,
|
||||
"total_amount": 113.78,
|
||||
"sales_date": "BASE_TS - 6d 23h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 6d 23h",
|
||||
"updated_at": "BASE_TS - 6d 23h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-0751",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"sale_date": "2025-01-14T12:00:00Z",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity_sold": 8.0,
|
||||
"unit_price": 2.25,
|
||||
"total_revenue": 18.0,
|
||||
"sales_channel": "IN_STORE",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Lunch time bread sales"
|
||||
"quantity": 26.69,
|
||||
"unit_price": 6.47,
|
||||
"total_amount": 190.52,
|
||||
"sales_date": "BASE_TS - 6d 4h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 6d 4h",
|
||||
"updated_at": "BASE_TS - 6d 4h"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000004",
|
||||
"id": "SALES-202501-7429",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"sale_date": "2025-01-14T15:00:00Z",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"quantity_sold": 12.0,
|
||||
"unit_price": 1.75,
|
||||
"total_revenue": 21.0,
|
||||
"sales_channel": "IN_STORE",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Afternoon pastry sales"
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 29.68,
|
||||
"unit_price": 6.31,
|
||||
"total_amount": 139.19,
|
||||
"sales_date": "BASE_TS - 6d 7h",
|
||||
"sales_channel": "wholesale",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 6d 7h",
|
||||
"updated_at": "BASE_TS - 6d 7h"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000099",
|
||||
"id": "SALES-202501-1170",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 22.88,
|
||||
"unit_price": 6.15,
|
||||
"total_amount": 80.7,
|
||||
"sales_date": "BASE_TS - 6d 8h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 6d 8h",
|
||||
"updated_at": "BASE_TS - 6d 8h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-9126",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"sale_date": "2025-01-15T07:30:00Z",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity_sold": 25.0,
|
||||
"unit_price": 2.6,
|
||||
"total_revenue": 65.0,
|
||||
"sales_channel": "IN_STORE",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Early morning rush - higher price point",
|
||||
"reasoning_data": {
|
||||
"type": "peak_demand",
|
||||
"parameters": {
|
||||
"demand_factor": 1.2,
|
||||
"time_period": "morning_rush",
|
||||
"price_adjustment": 0.1
|
||||
}
|
||||
}
|
||||
"quantity": 32.61,
|
||||
"unit_price": 3.82,
|
||||
"total_amount": 144.97,
|
||||
"sales_date": "BASE_TS - 4d 23h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 4d 23h",
|
||||
"updated_at": "BASE_TS - 4d 23h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-6573",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 40.54,
|
||||
"unit_price": 4.11,
|
||||
"total_amount": 152.66,
|
||||
"sales_date": "BASE_TS - 5d 0h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 5d 0h",
|
||||
"updated_at": "BASE_TS - 5d 0h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-6483",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 33.25,
|
||||
"unit_price": 7.81,
|
||||
"total_amount": 211.39,
|
||||
"sales_date": "BASE_TS - 5d 1h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 5d 1h",
|
||||
"updated_at": "BASE_TS - 5d 1h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-9578",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 22.4,
|
||||
"unit_price": 5.07,
|
||||
"total_amount": 134.46,
|
||||
"sales_date": "BASE_TS - 5d 4h",
|
||||
"sales_channel": "wholesale",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 5d 4h",
|
||||
"updated_at": "BASE_TS - 5d 4h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-8086",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 26.22,
|
||||
"unit_price": 3.05,
|
||||
"total_amount": 72.96,
|
||||
"sales_date": "BASE_TS - 5d 5h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 5d 5h",
|
||||
"updated_at": "BASE_TS - 5d 5h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-6917",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 22.98,
|
||||
"unit_price": 6.08,
|
||||
"total_amount": 110.52,
|
||||
"sales_date": "BASE_TS - 5d 0h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 5d 0h",
|
||||
"updated_at": "BASE_TS - 5d 0h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-0189",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 22.12,
|
||||
"unit_price": 6.58,
|
||||
"total_amount": 70.51,
|
||||
"sales_date": "BASE_TS - 4d 23h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 4d 23h",
|
||||
"updated_at": "BASE_TS - 4d 23h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-7434",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 26.8,
|
||||
"unit_price": 2.52,
|
||||
"total_amount": 183.11,
|
||||
"sales_date": "BASE_TS - 4d 3h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 4d 3h",
|
||||
"updated_at": "BASE_TS - 4d 3h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-8318",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 37.72,
|
||||
"unit_price": 7.98,
|
||||
"total_amount": 291.3,
|
||||
"sales_date": "BASE_TS - 4d 0h",
|
||||
"sales_channel": "wholesale",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 4d 0h",
|
||||
"updated_at": "BASE_TS - 4d 0h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-6127",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 31.83,
|
||||
"unit_price": 7.08,
|
||||
"total_amount": 182.56,
|
||||
"sales_date": "BASE_TS - 4d 5h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 4d 5h",
|
||||
"updated_at": "BASE_TS - 4d 5h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-5039",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 15.31,
|
||||
"unit_price": 3.94,
|
||||
"total_amount": 60.42,
|
||||
"sales_date": "BASE_TS - 4d 2h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 4d 2h",
|
||||
"updated_at": "BASE_TS - 4d 2h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-1134",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 15.82,
|
||||
"unit_price": 8.37,
|
||||
"total_amount": 90.09,
|
||||
"sales_date": "BASE_TS - 4d 6h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 4d 6h",
|
||||
"updated_at": "BASE_TS - 4d 6h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-2706",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 20.17,
|
||||
"unit_price": 4.09,
|
||||
"total_amount": 156.0,
|
||||
"sales_date": "BASE_TS - 4d 2h",
|
||||
"sales_channel": "wholesale",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 4d 2h",
|
||||
"updated_at": "BASE_TS - 4d 2h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-6538",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 38.0,
|
||||
"unit_price": 8.47,
|
||||
"total_amount": 243.18,
|
||||
"sales_date": "BASE_TS - 3d 1h",
|
||||
"sales_channel": "wholesale",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 3d 1h",
|
||||
"updated_at": "BASE_TS - 3d 1h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-1050",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 34.63,
|
||||
"unit_price": 4.53,
|
||||
"total_amount": 208.83,
|
||||
"sales_date": "BASE_TS - 3d 5h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 3d 5h",
|
||||
"updated_at": "BASE_TS - 3d 5h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-0965",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 31.37,
|
||||
"unit_price": 3.87,
|
||||
"total_amount": 248.81,
|
||||
"sales_date": "BASE_TS - 3d 6h",
|
||||
"sales_channel": "wholesale",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 3d 6h",
|
||||
"updated_at": "BASE_TS - 3d 6h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-7954",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 35.52,
|
||||
"unit_price": 3.79,
|
||||
"total_amount": 116.99,
|
||||
"sales_date": "BASE_TS - 3d 4h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 3d 4h",
|
||||
"updated_at": "BASE_TS - 3d 4h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-1589",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"quantity": 27.73,
|
||||
"unit_price": 6.45,
|
||||
"total_amount": 128.29,
|
||||
"sales_date": "BASE_TS - 3d 5h",
|
||||
"sales_channel": "wholesale",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 3d 5h",
|
||||
"updated_at": "BASE_TS - 3d 5h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-1613",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"quantity": 28.29,
|
||||
"unit_price": 2.86,
|
||||
"total_amount": 194.33,
|
||||
"sales_date": "BASE_TS - 3d 7h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 3d 7h",
|
||||
"updated_at": "BASE_TS - 3d 7h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-2297",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"quantity": 21.65,
|
||||
"unit_price": 5.03,
|
||||
"total_amount": 90.3,
|
||||
"sales_date": "BASE_TS - 3d 3h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 3d 3h",
|
||||
"updated_at": "BASE_TS - 3d 3h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-8857",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 21.19,
|
||||
"unit_price": 7.52,
|
||||
"total_amount": 176.21,
|
||||
"sales_date": "BASE_TS - 2d 1h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 2d 1h",
|
||||
"updated_at": "BASE_TS - 2d 1h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-6571",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 24.31,
|
||||
"unit_price": 7.91,
|
||||
"total_amount": 84.79,
|
||||
"sales_date": "BASE_TS - 2d 2h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 2d 2h",
|
||||
"updated_at": "BASE_TS - 2d 2h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-7455",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 22.89,
|
||||
"unit_price": 4.21,
|
||||
"total_amount": 152.86,
|
||||
"sales_date": "BASE_TS - 2d 0h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 2d 0h",
|
||||
"updated_at": "BASE_TS - 2d 0h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-3112",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 26.89,
|
||||
"unit_price": 4.28,
|
||||
"total_amount": 223.54,
|
||||
"sales_date": "BASE_TS - 2d 2h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 2d 2h",
|
||||
"updated_at": "BASE_TS - 2d 2h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-7812",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 15.28,
|
||||
"unit_price": 5.52,
|
||||
"total_amount": 116.36,
|
||||
"sales_date": "BASE_TS - 2d 10h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 2d 10h",
|
||||
"updated_at": "BASE_TS - 2d 10h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-3045",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 19.55,
|
||||
"unit_price": 2.91,
|
||||
"total_amount": 56.85,
|
||||
"sales_date": "BASE_TS - 2d 9h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 2d 9h",
|
||||
"updated_at": "BASE_TS - 2d 9h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-4034",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 14.0,
|
||||
"unit_price": 5.97,
|
||||
"total_amount": 38.34,
|
||||
"sales_date": "BASE_TS - 2d 3h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 2d 3h",
|
||||
"updated_at": "BASE_TS - 2d 3h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-5184",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"quantity": 17.55,
|
||||
"unit_price": 8.11,
|
||||
"total_amount": 65.38,
|
||||
"sales_date": "BASE_TS - 2d 5h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 2d 5h",
|
||||
"updated_at": "BASE_TS - 2d 5h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-7492",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 32.64,
|
||||
"unit_price": 4.4,
|
||||
"total_amount": 228.85,
|
||||
"sales_date": "BASE_TS - 1d 1h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 1d 1h",
|
||||
"updated_at": "BASE_TS - 1d 1h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-1639",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 37.66,
|
||||
"unit_price": 2.94,
|
||||
"total_amount": 142.3,
|
||||
"sales_date": "BASE_TS - 0d 23h",
|
||||
"sales_channel": "wholesale",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 0d 23h",
|
||||
"updated_at": "BASE_TS - 0d 23h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-4003",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"quantity": 44.93,
|
||||
"unit_price": 4.72,
|
||||
"total_amount": 154.86,
|
||||
"sales_date": "BASE_TS - 0d 23h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 0d 23h",
|
||||
"updated_at": "BASE_TS - 0d 23h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-9087",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 27.58,
|
||||
"unit_price": 4.3,
|
||||
"total_amount": 178.72,
|
||||
"sales_date": "BASE_TS - 1d 1h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 1d 1h",
|
||||
"updated_at": "BASE_TS - 1d 1h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-9065",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 23.07,
|
||||
"unit_price": 3.43,
|
||||
"total_amount": 96.68,
|
||||
"sales_date": "BASE_TS - 1d 6h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "transfer",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 1d 6h",
|
||||
"updated_at": "BASE_TS - 1d 6h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-4326",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 25.55,
|
||||
"unit_price": 5.53,
|
||||
"total_amount": 102.37,
|
||||
"sales_date": "BASE_TS - 1d 1h",
|
||||
"sales_channel": "in_store",
|
||||
"payment_method": "cash",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 1d 1h",
|
||||
"updated_at": "BASE_TS - 1d 1h"
|
||||
},
|
||||
{
|
||||
"id": "SALES-202501-0723",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"quantity": 28.73,
|
||||
"unit_price": 2.52,
|
||||
"total_amount": 204.74,
|
||||
"sales_date": "BASE_TS - 1d 0h",
|
||||
"sales_channel": "online",
|
||||
"payment_method": "card",
|
||||
"customer_id": "50000000-0000-0000-0000-000000000001",
|
||||
"created_at": "BASE_TS - 1d 0h",
|
||||
"updated_at": "BASE_TS - 1d 0h"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,152 +1,340 @@
|
||||
{
|
||||
"forecasts": [
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000001",
|
||||
"id": "559ad124-ce3f-4cfa-8f24-9ad447d8a236",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "BASE_TS + 18h",
|
||||
"predicted_quantity": 50.0,
|
||||
"confidence_score": 0.92,
|
||||
"forecast_horizon_days": 1,
|
||||
"forecast_date": "2025-01-16T06:00:00Z",
|
||||
"predicted_quantity": 22.91,
|
||||
"confidence_percentage": 90.8,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Regular daily demand forecast"
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 90.8% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000002",
|
||||
"id": "23e13d19-90d3-47ec-bac1-7f561041571f",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"forecast_date": "BASE_TS + 18h",
|
||||
"predicted_quantity": 15.0,
|
||||
"confidence_score": 0.88,
|
||||
"forecast_horizon_days": 1,
|
||||
"forecast_date": "2025-01-16T06:00:00Z",
|
||||
"predicted_quantity": 21.23,
|
||||
"confidence_percentage": 91.8,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Croissant demand forecast"
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.8% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000003",
|
||||
"id": "02c052ae-b45d-4ec0-91f1-b140c22ee086",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"forecast_date": "BASE_TS + 18h",
|
||||
"predicted_quantity": 10.0,
|
||||
"confidence_score": 0.85,
|
||||
"forecast_horizon_days": 1,
|
||||
"forecast_date": "2025-01-16T06:00:00Z",
|
||||
"predicted_quantity": 18.65,
|
||||
"confidence_percentage": 88.1,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Country bread demand forecast"
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 88.1% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000099",
|
||||
"id": "7ea9daba-bced-44d5-9595-66e6a482154e",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"forecast_date": "2025-01-16T06:00:00Z",
|
||||
"predicted_quantity": 8.8,
|
||||
"confidence_percentage": 89.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 89.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "10bf8324-66a1-4776-b08c-5a55a3a86cb4",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "BASE_TS + 1d 18h",
|
||||
"predicted_quantity": 75.0,
|
||||
"confidence_score": 0.95,
|
||||
"forecast_horizon_days": 2,
|
||||
"forecast_date": "2025-01-17T06:00:00Z",
|
||||
"predicted_quantity": 20.16,
|
||||
"confidence_percentage": 91.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Weekend demand spike forecast",
|
||||
"reasoning_data": {
|
||||
"type": "demand_spike",
|
||||
"parameters": {
|
||||
"event_type": "weekend",
|
||||
"demand_increase_factor": 1.5,
|
||||
"historical_pattern": "weekend_spike"
|
||||
}
|
||||
}
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000100",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "BASE_TS + 2d 18h",
|
||||
"predicted_quantity": 60.0,
|
||||
"confidence_score": 0.92,
|
||||
"forecast_horizon_days": 3,
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Sunday demand forecast - slightly lower than Saturday",
|
||||
"historical_accuracy": 0.9
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000101",
|
||||
"id": "8133e0de-0431-4392-97ad-b5e0b385431a",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"forecast_date": "BASE_TS + 18h",
|
||||
"predicted_quantity": 15.0,
|
||||
"confidence_score": 0.88,
|
||||
"forecast_horizon_days": 1,
|
||||
"forecast_date": "2025-01-17T06:00:00Z",
|
||||
"predicted_quantity": 26.32,
|
||||
"confidence_percentage": 89.4,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Croissant demand forecast - weekend preparation",
|
||||
"historical_accuracy": 0.89
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 89.4% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000102",
|
||||
"id": "4bc052cb-dae1-4f06-815e-d822e843ae5c",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"forecast_date": "2025-01-17T06:00:00Z",
|
||||
"predicted_quantity": 21.04,
|
||||
"confidence_percentage": 89.4,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 89.4% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "4d29380e-5ed4-466d-a421-1871149b0cf0",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"forecast_date": "2025-01-17T06:00:00Z",
|
||||
"predicted_quantity": 11.55,
|
||||
"confidence_percentage": 91.9,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.9% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "9794cffd-2bc6-4461-8ff6-f97bcb5ef94c",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "2025-01-18T06:00:00Z",
|
||||
"predicted_quantity": 38.56,
|
||||
"confidence_percentage": 88.9,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 88.9% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "e6e5f60e-ac4e-43dc-9ed5-0140f5e1eaef",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"forecast_date": "BASE_TS + 1d 18h",
|
||||
"predicted_quantity": 25.0,
|
||||
"confidence_score": 0.9,
|
||||
"forecast_horizon_days": 2,
|
||||
"forecast_date": "2025-01-18T06:00:00Z",
|
||||
"predicted_quantity": 18.69,
|
||||
"confidence_percentage": 88.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Weekend croissant demand - higher than weekdays",
|
||||
"historical_accuracy": 0.91
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 88.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000103",
|
||||
"id": "57bbc0fb-14a4-4688-8ef8-f1bcf31b449e",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"forecast_date": "BASE_TS + 18h",
|
||||
"predicted_quantity": 10.0,
|
||||
"confidence_score": 0.85,
|
||||
"forecast_horizon_days": 1,
|
||||
"forecast_date": "2025-01-18T06:00:00Z",
|
||||
"predicted_quantity": 14.94,
|
||||
"confidence_percentage": 91.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Country bread demand forecast",
|
||||
"historical_accuracy": 0.88
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000104",
|
||||
"id": "a1b48396-f046-4a8c-bbbf-1c0c64da942b",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"forecast_date": "2025-01-18T06:00:00Z",
|
||||
"predicted_quantity": 12.55,
|
||||
"confidence_percentage": 90.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 90.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "c3a89c08-0382-41bc-9be6-cc0fe5822b63",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "2025-01-19T06:00:00Z",
|
||||
"predicted_quantity": 32.6,
|
||||
"confidence_percentage": 88.6,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 88.6% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "a7746915-f4bb-459f-9b11-7dd5cc161e19",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"forecast_date": "2025-01-19T06:00:00Z",
|
||||
"predicted_quantity": 24.8,
|
||||
"confidence_percentage": 88.2,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 88.2% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "96731957-9727-424d-8227-3d1bf51800ca",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"forecast_date": "BASE_TS + 1d 18h",
|
||||
"predicted_quantity": 12.0,
|
||||
"confidence_score": 0.87,
|
||||
"forecast_horizon_days": 2,
|
||||
"forecast_date": "2025-01-19T06:00:00Z",
|
||||
"predicted_quantity": 15.83,
|
||||
"confidence_percentage": 91.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Weekend country bread demand",
|
||||
"historical_accuracy": 0.9
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000105",
|
||||
"id": "19737618-eb42-47c0-8ad4-7e37f913a78a",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "BASE_TS + 3d 18h",
|
||||
"predicted_quantity": 45.0,
|
||||
"confidence_score": 0.91,
|
||||
"forecast_horizon_days": 4,
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"forecast_date": "2025-01-19T06:00:00Z",
|
||||
"predicted_quantity": 9.15,
|
||||
"confidence_percentage": 91.5,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Monday demand - back to normal after weekend",
|
||||
"historical_accuracy": 0.92
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.5% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000106",
|
||||
"id": "b4c3b4ad-6487-49d5-9663-56046f577332",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "BASE_TS + 4d 18h",
|
||||
"predicted_quantity": 48.0,
|
||||
"confidence_score": 0.9,
|
||||
"forecast_horizon_days": 5,
|
||||
"forecast_date": "2025-01-20T06:00:00Z",
|
||||
"predicted_quantity": 25.4,
|
||||
"confidence_percentage": 89.6,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Tuesday demand forecast",
|
||||
"historical_accuracy": 0.9
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 89.6% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "80000000-0000-0000-0000-000000000107",
|
||||
"id": "31b217eb-d71c-457a-8915-692dc701a6b9",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"forecast_date": "2025-01-20T06:00:00Z",
|
||||
"predicted_quantity": 17.2,
|
||||
"confidence_percentage": 91.1,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.1% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "a32d777c-7052-4ba1-b55b-7cc0dc3cfc3d",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"forecast_date": "2025-01-20T06:00:00Z",
|
||||
"predicted_quantity": 15.3,
|
||||
"confidence_percentage": 90.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 90.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "2db7d1d2-7b38-4ebb-b408-c9e0b6884c22",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"forecast_date": "2025-01-20T06:00:00Z",
|
||||
"predicted_quantity": 12.89,
|
||||
"confidence_percentage": 88.1,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 88.1% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "b5887602-7f9c-485b-b50d-0e60dd153780",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "BASE_TS + 5d 18h",
|
||||
"predicted_quantity": 50.0,
|
||||
"confidence_score": 0.89,
|
||||
"forecast_horizon_days": 6,
|
||||
"forecast_date": "2025-01-21T06:00:00Z",
|
||||
"predicted_quantity": 35.39,
|
||||
"confidence_percentage": 90.3,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"notes": "Wednesday demand forecast",
|
||||
"historical_accuracy": 0.89
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 90.3% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "696498b2-20a7-48cb-a597-d689be7c729f",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"forecast_date": "2025-01-21T06:00:00Z",
|
||||
"predicted_quantity": 26.46,
|
||||
"confidence_percentage": 90.4,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 90.4% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "b3c83939-52b7-4811-ac91-6fdc24d4ae0f",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"forecast_date": "2025-01-21T06:00:00Z",
|
||||
"predicted_quantity": 16.23,
|
||||
"confidence_percentage": 89.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 89.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "d3ca5707-9eee-4880-ac45-766f0e058492",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"forecast_date": "2025-01-21T06:00:00Z",
|
||||
"predicted_quantity": 13.47,
|
||||
"confidence_percentage": 91.6,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.6% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "0f67f70f-2d7e-43f2-b5dd-52659b06e578",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"forecast_date": "2025-01-22T06:00:00Z",
|
||||
"predicted_quantity": 21.2,
|
||||
"confidence_percentage": 89.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 89.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "ba4bc024-6440-4fcf-b6c4-f1773aaa3f24",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"forecast_date": "2025-01-22T06:00:00Z",
|
||||
"predicted_quantity": 24.48,
|
||||
"confidence_percentage": 90.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 90.7% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "cb6bfe90-1962-4ca1-b389-9d583780598d",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000003",
|
||||
"forecast_date": "2025-01-22T06:00:00Z",
|
||||
"predicted_quantity": 25.48,
|
||||
"confidence_percentage": 88.8,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 88.8% (seed=42)"
|
||||
},
|
||||
{
|
||||
"id": "76c39f91-82cc-4bce-a91c-1e57e29e3461",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"forecast_date": "2025-01-22T06:00:00Z",
|
||||
"predicted_quantity": 10.32,
|
||||
"confidence_percentage": 91.7,
|
||||
"forecast_type": "daily",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS",
|
||||
"notes": "Forecast accuracy: 91.7% (seed=42)"
|
||||
}
|
||||
],
|
||||
"prediction_batches": [
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"run_number": "ORCH-20250114-001",
|
||||
"status": "completed",
|
||||
"run_type": "daily",
|
||||
"started_at": "2025-01-14T22:00:00Z",
|
||||
"completed_at": "2025-01-14T22:15:00Z",
|
||||
"started_at": "BASE_TS - 1d 16h",
|
||||
"completed_at": "BASE_TS - 1d 15h45m",
|
||||
"duration_seconds": 900,
|
||||
"trigger_type": "scheduled",
|
||||
"trigger_source": "system",
|
||||
"created_at": "2025-01-14T22:00:00Z",
|
||||
"updated_at": "2025-01-14T22:15:00Z",
|
||||
"created_at": "BASE_TS - 1d 16h",
|
||||
"updated_at": "BASE_TS - 1d 15h45m",
|
||||
"notes": "Nightly orchestration run - Last successful execution before demo session"
|
||||
},
|
||||
"orchestration_results": {
|
||||
@@ -77,8 +77,8 @@
|
||||
"alert_type": "DELAYED_DELIVERY",
|
||||
"product_id": "10000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Harina de Trigo T55",
|
||||
"expected_delivery": "2025-01-14T10:00:00Z",
|
||||
"actual_delivery": "2025-01-14T14:00:00Z",
|
||||
"expected_delivery": "BASE_TS - 1d 4h",
|
||||
"actual_delivery": "BASE_TS - 1d 8h",
|
||||
"delay_hours": 4,
|
||||
"severity": "CRITICAL",
|
||||
"related_po": "50000000-0000-0000-0000-000000000004",
|
||||
@@ -95,7 +95,7 @@
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"supplier_name": "Harinas del Norte",
|
||||
"status": "completed",
|
||||
"total_amount": 1053.50,
|
||||
"total_amount": 1053.5,
|
||||
"items_received": 3,
|
||||
"items_pending": 0,
|
||||
"delivery_status": "on_time"
|
||||
@@ -105,7 +105,7 @@
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000002",
|
||||
"supplier_name": "Lácteos Gipuzkoa",
|
||||
"status": "completed",
|
||||
"total_amount": 402.20,
|
||||
"total_amount": 402.2,
|
||||
"items_received": 1,
|
||||
"items_pending": 0,
|
||||
"delivery_status": "on_time"
|
||||
@@ -115,7 +115,7 @@
|
||||
"supplier_id": "40000000-0000-0000-0000-000000000001",
|
||||
"supplier_name": "Harinas del Norte",
|
||||
"status": "confirmed",
|
||||
"total_amount": 1427.00,
|
||||
"total_amount": 1427.0,
|
||||
"items_received": 0,
|
||||
"items_pending": 2,
|
||||
"delivery_status": "delayed",
|
||||
@@ -150,11 +150,46 @@
|
||||
"production_scheduling": 1
|
||||
},
|
||||
"system_state": {
|
||||
"last_successful_run": "2025-01-14T22:00:00Z",
|
||||
"next_scheduled_run": "2025-01-15T22:00:00Z",
|
||||
"last_successful_run": "BASE_TS - 1d 16h",
|
||||
"next_scheduled_run": "BASE_TS + 16h",
|
||||
"system_health": "healthy",
|
||||
"api_availability": 100.0,
|
||||
"database_performance": "optimal",
|
||||
"integration_status": "all_connected"
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
"ingredients_created": 25,
|
||||
"stock_entries_created": 25,
|
||||
"batches_created": 0,
|
||||
"sales_created": 44,
|
||||
"forecasts_created": 28,
|
||||
"consumptions_calculated": 81,
|
||||
"critical_stock_items": 8,
|
||||
"active_alerts": 8,
|
||||
"forecasting_accuracy": 90.5,
|
||||
"cross_reference_errors": 0,
|
||||
"cross_reference_warnings": 0
|
||||
},
|
||||
"alerts": [
|
||||
{
|
||||
"alert_type": "OVERDUE_BATCH",
|
||||
"severity": "high",
|
||||
"message": "Production should have started 2 hours ago - BATCH-LATE-0001",
|
||||
"created_at": "BASE_TS"
|
||||
},
|
||||
{
|
||||
"alert_type": "DELAYED_DELIVERY",
|
||||
"severity": "high",
|
||||
"message": "Supplier delivery 4 hours late - PO-LATE-0001",
|
||||
"created_at": "BASE_TS"
|
||||
},
|
||||
{
|
||||
"alert_type": "CRITICAL_STOCK",
|
||||
"severity": "critical",
|
||||
"message": "Harina T55 below reorder point with NO pending PO",
|
||||
"created_at": "BASE_TS"
|
||||
}
|
||||
],
|
||||
"completed_at": "BASE_TS",
|
||||
"status": "completed"
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
{
|
||||
"quality_controls": [
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000001",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"batch_id": "40000000-0000-0000-0000-000000000001",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Baguette Francesa Tradicional",
|
||||
"control_type": "visual_inspection",
|
||||
"control_date": "2025-01-08T14:30:00Z",
|
||||
"status": "COMPLETED",
|
||||
"result": "PASSED",
|
||||
"quality_score": 95.0,
|
||||
"inspected_by": "50000000-0000-0000-0000-000000000007",
|
||||
"notes": "Excelente aspecto y textura, 2 unidades con quemaduras leves (dentro de tolerancia)",
|
||||
"defects_found": [
|
||||
{
|
||||
"defect_type": "burnt",
|
||||
"quantity": 2.0,
|
||||
"severity": "minor"
|
||||
}
|
||||
],
|
||||
"corrective_actions": null,
|
||||
"created_at": "BASE_TS - 7d 8h 30m",
|
||||
"updated_at": "BASE_TS - 7d 8h 45m"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000002",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"batch_id": "40000000-0000-0000-0000-000000000002",
|
||||
"product_id": "20000000-0000-0000-0000-000000000002",
|
||||
"product_name": "Croissant de Mantequilla Artesanal",
|
||||
"control_type": "dimensional_check",
|
||||
"control_date": "2025-01-08T14:45:00Z",
|
||||
"status": "COMPLETED",
|
||||
"result": "PASSED",
|
||||
"quality_score": 92.0,
|
||||
"inspected_by": "50000000-0000-0000-0000-000000000007",
|
||||
"notes": "Buen desarrollo y laminado, 3 unidades con forma irregular (dentro de tolerancia)",
|
||||
"defects_found": [
|
||||
{
|
||||
"defect_type": "misshapen",
|
||||
"quantity": 3.0,
|
||||
"severity": "minor"
|
||||
}
|
||||
],
|
||||
"corrective_actions": null,
|
||||
"created_at": "BASE_TS - 7d 8h 45m",
|
||||
"updated_at": "BASE_TS - 7d 9h"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000003",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"batch_id": "40000000-0000-0000-0000-000000000004",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"product_name": "Napolitana de Chocolate",
|
||||
"control_type": "taste_test",
|
||||
"control_date": "2025-01-09T14:30:00Z",
|
||||
"status": "COMPLETED",
|
||||
"result": "FAILED",
|
||||
"quality_score": 65.0,
|
||||
"inspected_by": "50000000-0000-0000-0000-000000000007",
|
||||
"notes": "⚠️ CRITICAL: Sabor amargo en el chocolate, posible problema con proveedor de cacao",
|
||||
"defects_found": [
|
||||
{
|
||||
"defect_type": "off_taste",
|
||||
"quantity": 10.0,
|
||||
"severity": "major"
|
||||
}
|
||||
],
|
||||
"corrective_actions": [
|
||||
"Lote puesto en cuarentena",
|
||||
"Notificado proveedor de chocolate",
|
||||
"Programada nueva prueba con muestra diferente"
|
||||
],
|
||||
"batch_status_after_control": "QUARANTINED",
|
||||
"created_at": "BASE_TS - 6d 8h 30m",
|
||||
"updated_at": "BASE_TS - 6d 9h"
|
||||
},
|
||||
{
|
||||
"id": "70000000-0000-0000-0000-000000000004",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"batch_id": "40000000-0000-0000-0000-000000000015",
|
||||
"product_id": "20000000-0000-0000-0000-000000000001",
|
||||
"product_name": "Baguette Francesa Tradicional",
|
||||
"control_type": "visual_inspection",
|
||||
"control_date": "BASE_TS + 0h",
|
||||
"status": "PENDING",
|
||||
"result": null,
|
||||
"quality_score": null,
|
||||
"inspected_by": null,
|
||||
"notes": "⚠️ PENDING: Control de calidad programado para lote en producción",
|
||||
"defects_found": null,
|
||||
"corrective_actions": null,
|
||||
"batch_status_after_control": "QUALITY_CHECK",
|
||||
"created_at": "BASE_TS",
|
||||
"updated_at": "BASE_TS"
|
||||
}
|
||||
],
|
||||
"quality_alerts": [
|
||||
{
|
||||
"id": "71000000-0000-0000-0000-000000000001",
|
||||
"tenant_id": "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6",
|
||||
"alert_type": "QUALITY_FAILURE",
|
||||
"severity": "HIGH",
|
||||
"status": "OPEN",
|
||||
"related_control_id": "70000000-0000-0000-0000-000000000003",
|
||||
"related_batch_id": "40000000-0000-0000-0000-000000000004",
|
||||
"product_id": "20000000-0000-0000-0000-000000000004",
|
||||
"product_name": "Napolitana de Chocolate",
|
||||
"description": "Fallo crítico en control de calidad - Sabor amargo en chocolate",
|
||||
"created_at": "BASE_TS - 6d 9h",
|
||||
"acknowledged_at": "2025-01-09T15:15:00Z",
|
||||
"resolved_at": null,
|
||||
"notes": "Lote en cuarentena, investigación en curso con proveedor"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user