Improve the demo feature of the project

This commit is contained in:
Urtzi Alfaro
2025-10-12 18:47:33 +02:00
parent dbc7f2fa0d
commit 7556a00db7
168 changed files with 10102 additions and 18869 deletions

View File

@@ -1,123 +0,0 @@
# Database Demo Data
This directory contains comprehensive demo data for the bakery inventory system.
## Files
### `demo_inventory_data.sql`
Complete demo dataset that creates a realistic bakery inventory scenario. This file includes:
**Demo Configuration:**
- Tenant ID: `c464fb3e-7af2-46e6-9e43-85318f34199a`
- Demo User: `demo@panaderiasanpablo.com`
- Bakery: "Panadería San Pablo - Demo"
**What's Included:**
1. **Raw Ingredients (16 items):**
- Flours (Wheat, Whole wheat)
- Yeasts (Fresh, Dry active)
- Fats (Butter, Olive oil)
- Dairy & Eggs (Milk, Fresh eggs)
- Sugars (White, Brown)
- Seasonings (Salt, Chocolate, Vanilla, Cinnamon)
- Nuts & Fruits (Walnuts, Raisins)
2. **Finished Products (8 items):**
- Croissants (with par-baked and fully-baked stages)
- Breads (Whole wheat, Toasted)
- Pastries (Napolitanas, Palmeras, Magdalenas)
- Other products (Empanadas, Coffee with milk)
3. **Stock Lots with Diverse Scenarios:**
- **Good Stock**: Normal levels, fresh products
- **Low Stock**: Below threshold items (Yeast, Butter, Coffee)
- **Critical Stock**: Items needing immediate attention
- **Out of Stock**: Completely sold out (Napolitanas)
- **Expired Stock**: Items past expiration date (Some eggs)
- **Expires Soon**: Items expiring today/tomorrow (Milk, some croissants)
- **Overstock**: Items with excess inventory (Sugar, Salt)
4. **Production Stages:**
- `raw_ingredient`: Base materials
- `par_baked`: Semi-finished products from central bakery
- `fully_baked`: Ready-to-sell products
5. **Stock Movements History:**
- **Purchases**: Raw material deliveries
- **Production Use**: Materials consumed in production
- **Transformations**: Par-baked to fully-baked conversions
- **Sales**: Customer purchases
- **Waste**: Expired/damaged products
- **Reservations**: Items reserved for specific orders
## Usage
### Run the Demo Data Script
```sql
-- Connect to your PostgreSQL database
\i shared/database/demo_inventory_data.sql
```
### Expected Results
The script will create:
- **24 ingredients** (16 raw + 8 finished products)
- **25+ stock lots** with different scenarios
- **15+ stock movements** showing transaction history
- **Summary reports** showing inventory status
### Demo Scenarios Included
1. **Critical Alerts Testing:**
- Expired eggs (past expiration date)
- Low stock yeast (below 1.0kg threshold)
- Milk expiring today
- Out of stock napolitanas
2. **Production Workflow:**
- Par-baked croissants ready for final baking
- Fresh products baked this morning
- Reserved stock for afternoon production
3. **Sales Patterns:**
- Popular items sold out (napolitanas)
- Steady sales of bread and pastries
- Morning rush reflected in stock levels
4. **Inventory Management:**
- Multiple batches with different expiration dates
- FIFO rotation scenarios
- Waste tracking for expired items
## Customization
To modify the demo for different scenarios, edit the variables at the top of `demo_inventory_data.sql`:
```sql
-- Demo Configuration Variables
demo_tenant_id UUID := 'your-tenant-id'::UUID;
demo_user_email VARCHAR := 'your-demo-email@domain.com';
demo_bakery_name VARCHAR := 'Your Bakery Name';
```
## Testing Scenarios
The demo data is designed to test all major inventory features:
- ✅ Stock level calculations
- ✅ Expiration date tracking
- ✅ Low stock alerts
- ✅ Out of stock handling
- ✅ Multi-batch inventory
- ✅ Production stage tracking
- ✅ Movement history
- ✅ Waste management
- ✅ Reserved stock
- ✅ Cost calculations
- ✅ Storage location tracking
- ✅ Quality status monitoring
Perfect for demos, development, and testing!

View File

@@ -1,659 +0,0 @@
-- =====================================
-- BAKERY INVENTORY DEMO DATA
-- Comprehensive demo dataset for inventory system
-- Includes: Demo user/tenant, ingredients, stock with lots, movements
-- =====================================
-- Demo Configuration Variables
-- These can be modified to create data for different demo scenarios
DO $$
DECLARE
-- Demo User & Tenant Configuration
demo_tenant_id UUID := 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID;
demo_user_email VARCHAR := 'demo@panaderiasanpablo.com';
demo_user_name VARCHAR := 'Demo User - Panadería San Pablo';
demo_bakery_name VARCHAR := 'Panadería San Pablo - Demo';
-- Ingredient IDs (will be generated)
harina_trigo_id UUID;
harina_integral_id UUID;
levadura_fresca_id UUID;
levadura_seca_id UUID;
mantequilla_id UUID;
aceite_oliva_id UUID;
huevos_id UUID;
leche_id UUID;
azucar_blanca_id UUID;
azucar_morena_id UUID;
sal_id UUID;
chocolate_id UUID;
vainilla_id UUID;
canela_id UUID;
nueces_id UUID;
pasas_id UUID;
-- Finished Product IDs
croissant_id UUID;
pan_integral_id UUID;
napolitana_id UUID;
palmera_id UUID;
pan_tostado_id UUID;
magdalena_id UUID;
empanada_id UUID;
cafe_con_leche_id UUID;
BEGIN
-- ===========================================
-- 1. SETUP DEMO TENANT AND USER
-- ===========================================
RAISE NOTICE 'Setting up demo data for tenant: % (%)', demo_bakery_name, demo_tenant_id;
-- Clean existing demo data for this tenant
DELETE FROM stock_movements WHERE tenant_id = demo_tenant_id;
DELETE FROM stock WHERE tenant_id = demo_tenant_id;
DELETE FROM ingredients WHERE tenant_id = demo_tenant_id;
-- ===========================================
-- 2. RAW INGREDIENTS - COMPREHENSIVE BAKERY STOCK
-- ===========================================
-- Generate ingredient IDs
harina_trigo_id := gen_random_uuid();
harina_integral_id := gen_random_uuid();
levadura_fresca_id := gen_random_uuid();
levadura_seca_id := gen_random_uuid();
mantequilla_id := gen_random_uuid();
aceite_oliva_id := gen_random_uuid();
huevos_id := gen_random_uuid();
leche_id := gen_random_uuid();
azucar_blanca_id := gen_random_uuid();
azucar_morena_id := gen_random_uuid();
sal_id := gen_random_uuid();
chocolate_id := gen_random_uuid();
vainilla_id := gen_random_uuid();
canela_id := gen_random_uuid();
nueces_id := gen_random_uuid();
pasas_id := gen_random_uuid();
-- Insert raw ingredients
INSERT INTO ingredients (
id, tenant_id, name, product_type, ingredient_category, product_category,
unit_of_measure, low_stock_threshold, reorder_point, reorder_quantity,
is_active, is_perishable, shelf_life_days, average_cost, brand,
storage_instructions, created_at, updated_at
) VALUES
-- FLOURS
(harina_trigo_id, demo_tenant_id, 'Harina de Trigo 000', 'INGREDIENT', 'FLOUR', NULL, 'KILOGRAMS', 25.0, 50.0, 200.0, true, false, 365, 2.50, 'Molinos del Valle', 'Almacenar en lugar seco', NOW(), NOW()),
(harina_integral_id, demo_tenant_id, 'Harina Integral', 'INGREDIENT', 'FLOUR', NULL, 'KILOGRAMS', 15.0, 30.0, 100.0, true, false, 180, 3.20, 'Bio Natural', 'Almacenar en lugar seco', NOW(), NOW()),
-- YEASTS
(levadura_fresca_id, demo_tenant_id, 'Levadura Fresca', 'INGREDIENT', 'YEAST', NULL, 'KILOGRAMS', 1.0, 2.5, 10.0, true, true, 7, 8.50, 'Levapan', 'Refrigerar entre 2-8°C', NOW(), NOW()),
(levadura_seca_id, demo_tenant_id, 'Levadura Seca Activa', 'INGREDIENT', 'YEAST', NULL, 'KILOGRAMS', 0.5, 1.0, 5.0, true, false, 730, 12.00, 'Fleischmann', 'Lugar seco, temperatura ambiente', NOW(), NOW()),
-- FATS
(mantequilla_id, demo_tenant_id, 'Mantequilla', 'INGREDIENT', 'FATS', NULL, 'KILOGRAMS', 3.0, 8.0, 25.0, true, true, 30, 6.80, 'La Serenísima', 'Refrigerar', NOW(), NOW()),
(aceite_oliva_id, demo_tenant_id, 'Aceite de Oliva Extra Virgen', 'INGREDIENT', 'FATS', NULL, 'LITROS', 2.0, 5.0, 20.0, true, false, 730, 15.50, 'Cocinero', 'Lugar fresco y oscuro', NOW(), NOW()),
-- DAIRY & EGGS
(huevos_id, demo_tenant_id, 'Huevos Frescos', 'INGREDIENT', 'EGGS', NULL, 'UNITS', 36, 60, 180, true, true, 21, 0.25, 'Granja San José', 'Refrigerar', NOW(), NOW()),
(leche_id, demo_tenant_id, 'Leche Entera', 'INGREDIENT', 'DAIRY', NULL, 'LITROS', 5.0, 12.0, 50.0, true, true, 5, 1.80, 'La Serenísima', 'Refrigerar', NOW(), NOW()),
-- SUGARS
(azucar_blanca_id, demo_tenant_id, 'Azúcar Blanca', 'INGREDIENT', 'SUGAR', NULL, 'KILOGRAMS', 8.0, 20.0, 100.0, true, false, 730, 1.20, 'Ledesma', 'Lugar seco', NOW(), NOW()),
(azucar_morena_id, demo_tenant_id, 'Azúcar Morena', 'INGREDIENT', 'SUGAR', NULL, 'KILOGRAMS', 3.0, 8.0, 25.0, true, false, 365, 2.80, 'Orgánica', 'Lugar seco', NOW(), NOW()),
-- SALT & FLAVORINGS
(sal_id, demo_tenant_id, 'Sal Fina', 'INGREDIENT', 'SALT', NULL, 'KILOGRAMS', 2.0, 5.0, 20.0, true, false, 1095, 0.80, 'Celusal', 'Lugar seco', NOW(), NOW()),
(chocolate_id, demo_tenant_id, 'Chocolate Semiamargo', 'INGREDIENT', 'ADDITIVES', NULL, 'KILOGRAMS', 1.0, 3.0, 15.0, true, false, 365, 8.50, 'Águila', 'Lugar fresco', NOW(), NOW()),
(vainilla_id, demo_tenant_id, 'Extracto de Vainilla', 'INGREDIENT', 'SPICES', NULL, 'MILLILITERS', 100, 250, 1000, true, false, 1095, 0.15, 'McCormick', 'Lugar fresco', NOW(), NOW()),
(canela_id, demo_tenant_id, 'Canela en Polvo', 'INGREDIENT', 'SPICES', NULL, 'GRAMS', 50, 150, 500, true, false, 730, 0.08, 'Alicante', 'Lugar seco', NOW(), NOW()),
-- NUTS & FRUITS
(nueces_id, demo_tenant_id, 'Nueces Peladas', 'INGREDIENT', 'ADDITIVES', NULL, 'KILOGRAMS', 0.5, 1.5, 8.0, true, false, 180, 12.00, 'Los Nogales', 'Lugar fresco y seco', NOW(), NOW()),
(pasas_id, demo_tenant_id, 'Pasas de Uva', 'INGREDIENT', 'ADDITIVES', NULL, 'KILOGRAMS', 1.0, 2.0, 10.0, true, false, 365, 4.50, 'Mendoza Premium', 'Lugar seco', NOW(), NOW());
-- ===========================================
-- 3. FINISHED PRODUCTS
-- ===========================================
-- Generate finished product IDs
croissant_id := gen_random_uuid();
pan_integral_id := gen_random_uuid();
napolitana_id := gen_random_uuid();
palmera_id := gen_random_uuid();
pan_tostado_id := gen_random_uuid();
magdalena_id := gen_random_uuid();
empanada_id := gen_random_uuid();
cafe_con_leche_id := gen_random_uuid();
INSERT INTO ingredients (
id, tenant_id, name, product_type, ingredient_category, product_category,
unit_of_measure, low_stock_threshold, reorder_point, reorder_quantity,
is_active, is_perishable, shelf_life_days, average_cost,
created_at, updated_at
) VALUES
-- BAKERY PRODUCTS
(croissant_id, demo_tenant_id, 'Croissant Clásico', 'FINISHED_PRODUCT', NULL, 'CROISSANTS', 'PIECES', 12, 30, 80, true, true, 1, 1.20, NOW(), NOW()),
(pan_integral_id, demo_tenant_id, 'Pan Integral', 'FINISHED_PRODUCT', NULL, 'BREAD', 'PIECES', 8, 20, 50, true, true, 3, 2.50, NOW(), NOW()),
(napolitana_id, demo_tenant_id, 'Napolitana de Chocolate', 'FINISHED_PRODUCT', NULL, 'PASTRIES', 'PIECES', 10, 25, 60, true, true, 2, 1.80, NOW(), NOW()),
(palmera_id, demo_tenant_id, 'Palmera de Hojaldre', 'FINISHED_PRODUCT', NULL, 'PASTRIES', 'PIECES', 8, 20, 40, true, true, 2, 2.20, NOW(), NOW()),
(pan_tostado_id, demo_tenant_id, 'Pan Tostado', 'FINISHED_PRODUCT', NULL, 'BREAD', 'PIECES', 15, 35, 80, true, true, 5, 1.50, NOW(), NOW()),
(magdalena_id, demo_tenant_id, 'Magdalena de Vainilla', 'FINISHED_PRODUCT', NULL, 'PASTRIES', 'PIECES', 6, 18, 50, true, true, 3, 1.00, NOW(), NOW()),
(empanada_id, demo_tenant_id, 'Empanada de Carne', 'FINISHED_PRODUCT', NULL, 'OTHER_PRODUCTS', 'PIECES', 5, 15, 40, true, true, 1, 3.50, NOW(), NOW()),
(cafe_con_leche_id, demo_tenant_id, 'Café con Leche', 'FINISHED_PRODUCT', NULL, 'BEVERAGES', 'UNITS', 8, 20, 50, true, true, 1, 2.80, NOW(), NOW());
-- ===========================================
-- 4. STOCK LOTS - DIVERSE SCENARIOS
-- ===========================================
RAISE NOTICE 'Creating stock lots with diverse scenarios...';
-- RAW INGREDIENTS STOCK
-- HARINA DE TRIGO - Good stock with multiple lots
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, harina_trigo_id, 'raw_ingredient',
120.0, 15.0, 105.0, 'HARINA-TRI-20241201-001', NOW() - INTERVAL '5 days', NOW() + INTERVAL '360 days',
2.50, 300.0, 'Almacén Principal - Estante A1', true, false, 'good', NOW(), NOW()),
(gen_random_uuid(), demo_tenant_id, harina_trigo_id, 'raw_ingredient',
80.0, 5.0, 75.0, 'HARINA-TRI-20241125-002', NOW() - INTERVAL '12 days', NOW() + INTERVAL '353 days',
2.50, 200.0, 'Almacén Principal - Estante A2', true, false, 'good', NOW(), NOW());
-- HARINA INTEGRAL - Normal stock
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, harina_integral_id, 'raw_ingredient',
45.0, 8.0, 37.0, 'HARINA-INT-20241128-001', NOW() - INTERVAL '9 days', NOW() + INTERVAL '171 days',
3.20, 144.0, 'Almacén Principal - Estante A3', true, false, 'good', NOW(), NOW());
-- LEVADURA FRESCA - CRITICAL LOW (below threshold, expires soon)
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, levadura_fresca_id, 'raw_ingredient',
0.8, 0.3, 0.5, 'LEVAD-FRE-20241204-001', NOW() - INTERVAL '2 days', NOW() + INTERVAL '5 days',
8.50, 6.8, 'Cámara Fría - Nivel 2', true, false, 'good', NOW(), NOW());
-- LEVADURA SECA - Good stock
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, levadura_seca_id, 'raw_ingredient',
2.5, 0.0, 2.5, 'LEVAD-SEC-20241115-001', NOW() - INTERVAL '22 days', NOW() + INTERVAL '708 days',
12.00, 30.0, 'Almacén Principal - Estante B1', true, false, 'good', NOW(), NOW());
-- MANTEQUILLA - Low stock, expires soon
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, mantequilla_id, 'raw_ingredient',
2.5, 0.5, 2.0, 'MANT-20241203-001', NOW() - INTERVAL '3 days', NOW() + INTERVAL '27 days',
6.80, 17.0, 'Cámara Fría - Nivel 1', true, false, 'good', NOW(), NOW());
-- ACEITE DE OLIVA - Good stock
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, aceite_oliva_id, 'raw_ingredient',
12.0, 2.0, 10.0, 'ACEITE-20241120-001', NOW() - INTERVAL '17 days', NOW() + INTERVAL '713 days',
15.50, 186.0, 'Almacén Principal - Estante C1', true, false, 'good', NOW(), NOW());
-- HUEVOS - Multiple lots, one EXPIRED
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
-- Fresh eggs
(gen_random_uuid(), demo_tenant_id, huevos_id, 'raw_ingredient',
60, 6, 54, 'HUEVOS-20241205-001', NOW() - INTERVAL '1 day', NOW() + INTERVAL '20 days',
0.25, 15.0, 'Cámara Fría - Cajón 1', true, false, 'good', NOW(), NOW()),
-- EXPIRED eggs - should trigger alert
(gen_random_uuid(), demo_tenant_id, huevos_id, 'raw_ingredient',
18, 0, 18, 'HUEVOS-20241115-002', NOW() - INTERVAL '22 days', NOW() - INTERVAL '1 day',
0.25, 4.5, 'Cámara Fría - Cajón 2', true, true, 'expired', NOW(), NOW());
-- LECHE - Expires TODAY (critical)
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, leche_id, 'raw_ingredient',
8.0, 1.0, 7.0, 'LECHE-20241202-001', NOW() - INTERVAL '4 days', NOW() + INTERVAL '12 hours',
1.80, 14.4, 'Cámara Fría - Nivel 3', true, false, 'expires_today', NOW(), NOW());
-- AZUCAR BLANCA - Overstock
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, azucar_blanca_id, 'raw_ingredient',
150.0, 10.0, 140.0, 'AZUC-BLA-20241110-001', NOW() - INTERVAL '27 days', NOW() + INTERVAL '703 days',
1.20, 180.0, 'Almacén Principal - Estante D1', true, false, 'good', NOW(), NOW());
-- OTHER INGREDIENTS - Various scenarios
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
-- Azúcar morena - Good
(gen_random_uuid(), demo_tenant_id, azucar_morena_id, 'raw_ingredient',
15.0, 2.0, 13.0, 'AZUC-MOR-20241125-001', NOW() - INTERVAL '12 days', NOW() + INTERVAL '353 days',
2.80, 42.0, 'Almacén Principal - Estante D2', true, false, 'good', NOW(), NOW()),
-- Sal - Overstock
(gen_random_uuid(), demo_tenant_id, sal_id, 'raw_ingredient',
25.0, 1.0, 24.0, 'SAL-20240915-001', NOW() - INTERVAL '82 days', NOW() + INTERVAL '1013 days',
0.80, 20.0, 'Almacén Principal - Estante E1', true, false, 'good', NOW(), NOW()),
-- Chocolate - Normal
(gen_random_uuid(), demo_tenant_id, chocolate_id, 'raw_ingredient',
4.5, 0.5, 4.0, 'CHOC-20241201-001', NOW() - INTERVAL '5 days', NOW() + INTERVAL '360 days',
8.50, 38.25, 'Almacén Principal - Estante F1', true, false, 'good', NOW(), NOW());
-- ===========================================
-- 5. FINISHED PRODUCTS STOCK - REALISTIC BAKERY SCENARIOS
-- ===========================================
-- CROISSANTS - Central bakery model with different stages
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date, original_expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
-- Par-baked croissants (frozen, good stock)
(gen_random_uuid(), demo_tenant_id, croissant_id, 'par_baked',
75, 0, 75, 'CROIS-PAR-20241205-001', NOW() - INTERVAL '1 day', NOW() + INTERVAL '4 days', NOW() + INTERVAL '4 days',
0.85, 63.75, 'Congelador - Sección A', true, false, 'good', NOW(), NOW()),
-- Par-baked croissants (older batch, some reserved)
(gen_random_uuid(), demo_tenant_id, croissant_id, 'par_baked',
40, 15, 25, 'CROIS-PAR-20241203-002', NOW() - INTERVAL '3 days', NOW() + INTERVAL '2 days', NOW() + INTERVAL '2 days',
0.85, 34.0, 'Congelador - Sección B', true, false, 'good', NOW(), NOW()),
-- Fresh croissants (baked this morning)
(gen_random_uuid(), demo_tenant_id, croissant_id, 'fully_baked',
35, 5, 30, 'CROIS-FRESH-20241206-001', NOW() - INTERVAL '4 hours', NOW() + INTERVAL '20 hours',
1.20, 42.0, 'Vitrina Principal - Nivel 1', true, false, 'good', NOW(), NOW()),
-- Croissants from yesterday (expires soon, low stock)
(gen_random_uuid(), demo_tenant_id, croissant_id, 'fully_baked',
8, 0, 8, 'CROIS-FRESH-20241205-002', NOW() - INTERVAL '28 hours', NOW() + INTERVAL '8 hours',
1.20, 9.6, 'Vitrina Principal - Nivel 2', true, false, 'expires_today', NOW(), NOW());
-- PAN INTEGRAL - Different batches
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
-- Fresh bread
(gen_random_uuid(), demo_tenant_id, pan_integral_id, 'fully_baked',
25, 3, 22, 'PAN-INT-20241206-001', NOW() - INTERVAL '6 hours', NOW() + INTERVAL '66 hours',
2.50, 62.5, 'Estantería Pan - Nivel 1', true, false, 'good', NOW(), NOW()),
-- Day-old bread
(gen_random_uuid(), demo_tenant_id, pan_integral_id, 'fully_baked',
12, 1, 11, 'PAN-INT-20241205-002', NOW() - INTERVAL '30 hours', NOW() + INTERVAL '42 hours',
2.50, 30.0, 'Estantería Pan - Nivel 2', true, false, 'good', NOW(), NOW());
-- NAPOLITANAS - OUT OF STOCK (sold out scenario)
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, napolitana_id, 'fully_baked',
0, 0, 0, 'NAPO-20241206-001', NOW() - INTERVAL '6 hours', NOW() + INTERVAL '42 hours',
1.80, 0.0, 'Vitrina Pasteles - Nivel 1', false, false, 'sold_out', NOW(), NOW());
-- PALMERAS - Good stock
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, palmera_id, 'fully_baked',
28, 4, 24, 'PALM-20241206-001', NOW() - INTERVAL '3 hours', NOW() + INTERVAL '45 hours',
2.20, 61.6, 'Vitrina Pasteles - Nivel 2', true, false, 'good', NOW(), NOW());
-- PAN TOSTADO - Multiple batches
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
-- Fresh batch
(gen_random_uuid(), demo_tenant_id, pan_tostado_id, 'fully_baked',
42, 6, 36, 'PAN-TOS-20241206-001', NOW() - INTERVAL '2 hours', NOW() + INTERVAL '118 hours',
1.50, 63.0, 'Estantería Pan Tostado - A', true, false, 'good', NOW(), NOW()),
-- Older batch
(gen_random_uuid(), demo_tenant_id, pan_tostado_id, 'fully_baked',
18, 2, 16, 'PAN-TOS-20241204-002', NOW() - INTERVAL '26 hours', NOW() + INTERVAL '94 hours',
1.50, 27.0, 'Estantería Pan Tostado - B', true, false, 'good', NOW(), NOW());
-- MAGDALENAS - Low stock
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, magdalena_id, 'fully_baked',
5, 1, 4, 'MAGD-20241206-001', NOW() - INTERVAL '5 hours', NOW() + INTERVAL '67 hours',
1.00, 5.0, 'Vitrina Pasteles - Nivel 3', true, false, 'good', NOW(), NOW());
-- EMPANADAS - Fresh, good stock
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, empanada_id, 'fully_baked',
20, 3, 17, 'EMP-20241206-001', NOW() - INTERVAL '1 hour', NOW() + INTERVAL '23 hours',
3.50, 70.0, 'Vitrina Empanadas', true, false, 'good', NOW(), NOW());
-- CAFÉ CON LECHE - Low stock
INSERT INTO stock (
id, tenant_id, ingredient_id, production_stage,
current_quantity, reserved_quantity, available_quantity,
batch_number, received_date, expiration_date,
unit_cost, total_cost, storage_location, is_available, is_expired, quality_status,
created_at, updated_at
) VALUES
(gen_random_uuid(), demo_tenant_id, cafe_con_leche_id, 'fully_baked',
9, 1, 8, 'CAFE-20241206-001', NOW() - INTERVAL '2 hours', NOW() + INTERVAL '22 hours',
2.80, 25.2, 'Máquina de Café', true, false, 'good', NOW(), NOW());
RAISE NOTICE 'Stock lots created successfully with diverse scenarios';
END $$;
-- ===========================================
-- 6. STOCK MOVEMENTS - REALISTIC TRANSACTION HISTORY
-- ===========================================
DO $$
DECLARE
demo_tenant_id UUID := 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID;
movement_record RECORD;
BEGIN
RAISE NOTICE 'Creating realistic stock movement history...';
-- PURCHASES - Raw materials received
INSERT INTO stock_movements (
id, tenant_id, ingredient_id, stock_id, movement_type, quantity,
unit_cost, total_cost, quantity_before, quantity_after,
reference_number, notes, movement_date, created_at, created_by
)
SELECT
gen_random_uuid(),
demo_tenant_id,
s.ingredient_id,
s.id,
'PURCHASE',
s.current_quantity + s.reserved_quantity,
s.unit_cost,
s.total_cost,
0.0,
s.current_quantity + s.reserved_quantity,
'PO-' || TO_CHAR(s.received_date, 'YYYY-MM-DD-') || LPAD((ROW_NUMBER() OVER (ORDER BY s.received_date))::text, 3, '0'),
CASE
WHEN s.production_stage = 'raw_ingredient' THEN 'Recepción de materia prima - ' || i.name
ELSE 'Producción interna - ' || i.name
END,
s.received_date,
s.received_date,
NULL
FROM stock s
JOIN ingredients i ON s.ingredient_id = i.id
WHERE s.tenant_id = demo_tenant_id
AND s.production_stage = 'raw_ingredient';
-- PRODUCTION USE - Raw materials consumed
INSERT INTO stock_movements (
id, tenant_id, ingredient_id, stock_id, movement_type, quantity,
unit_cost, total_cost, quantity_before, quantity_after,
reference_number, notes, movement_date, created_at, created_by
) VALUES
-- Flour used for daily production
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Harina de Trigo 000' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Harina de Trigo 000' AND tenant_id = demo_tenant_id) LIMIT 1),
'PRODUCTION_USE', -35.0, 2.50, -87.5, 155.0, 120.0,
'PROD-20241206-001', 'Consumo para producción diaria de pan y pasteles',
NOW() - INTERVAL '6 hours', NOW() - INTERVAL '6 hours', NULL),
-- Yeast used (contributing to low stock)
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Levadura Fresca' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Levadura Fresca' AND tenant_id = demo_tenant_id) LIMIT 1),
'PRODUCTION_USE', -2.2, 8.50, -18.7, 3.0, 0.8,
'PROD-20241206-002', 'Consumo para fermentación - stock crítico',
NOW() - INTERVAL '4 hours', NOW() - INTERVAL '4 hours', NULL),
-- Butter used
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Mantequilla' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Mantequilla' AND tenant_id = demo_tenant_id) LIMIT 1),
'PRODUCTION_USE', -1.5, 6.80, -10.2, 4.0, 2.5,
'PROD-20241206-003', 'Consumo para croissants y pasteles',
NOW() - INTERVAL '5 hours', NOW() - INTERVAL '5 hours', NULL);
-- TRANSFORMATIONS - Par-baked to fully baked
INSERT INTO stock_movements (
id, tenant_id, ingredient_id, stock_id, movement_type, quantity,
unit_cost, total_cost, quantity_before, quantity_after,
reference_number, notes, movement_date, created_at, created_by
) VALUES
-- Morning croissant batch
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Croissant Clásico' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Croissant Clásico' AND tenant_id = demo_tenant_id) AND production_stage = 'fully_baked' AND batch_number LIKE 'CROIS-FRESH-20241206%'),
'PRODUCTION_USE', 35.0, 1.20, 42.0, 0.0, 35.0,
'TRANS-20241206-001', 'Horneado final de croissants congelados - lote mañana',
NOW() - INTERVAL '4 hours', NOW() - INTERVAL '4 hours', NULL);
-- SALES - Products sold to customers
INSERT INTO stock_movements (
id, tenant_id, ingredient_id, stock_id, movement_type, quantity,
unit_cost, total_cost, quantity_before, quantity_after,
reference_number, notes, movement_date, created_at, created_by
) VALUES
-- Napolitanas sold out completely
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Napolitana de Chocolate' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Napolitana de Chocolate' AND tenant_id = demo_tenant_id)),
'SALE', -30.0, 1.80, -54.0, 30.0, 0.0,
'SALE-20241206-001', 'Venta completa - napolitanas muy populares hoy',
NOW() - INTERVAL '2 hours', NOW() - INTERVAL '2 hours', NULL),
-- Croissant sales
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Croissant Clásico' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Croissant Clásico' AND tenant_id = demo_tenant_id) AND production_stage = 'fully_baked' AND batch_number LIKE 'CROIS-FRESH-20241206%'),
'SALE', -5.0, 1.20, -6.0, 40.0, 35.0,
'SALE-20241206-002', 'Ventas matutinas de croissants frescos',
NOW() - INTERVAL '3 hours', NOW() - INTERVAL '3 hours', NULL),
-- Palmera sales
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Palmera de Hojaldre' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Palmera de Hojaldre' AND tenant_id = demo_tenant_id)),
'SALE', -4.0, 2.20, -8.8, 32.0, 28.0,
'SALE-20241206-003', 'Ventas de palmeras - desayuno',
NOW() - INTERVAL '2 hours', NOW() - INTERVAL '2 hours', NULL);
-- WASTE - Expired or damaged products
INSERT INTO stock_movements (
id, tenant_id, ingredient_id, stock_id, movement_type, quantity,
unit_cost, total_cost, quantity_before, quantity_after,
reference_number, notes, movement_date, created_at, created_by
) VALUES
-- Expired eggs marked as waste
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Huevos Frescos' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Huevos Frescos' AND tenant_id = demo_tenant_id) AND is_expired = true),
'WASTE', -18.0, 0.25, -4.5, 18.0, 0.0,
'WASTE-20241206-001', 'Huevos vencidos - descarte por caducidad',
NOW() - INTERVAL '1 hour', NOW() - INTERVAL '1 hour', NULL);
-- RESERVATIONS - Products reserved for specific orders
INSERT INTO stock_movements (
id, tenant_id, ingredient_id, stock_id, movement_type, quantity,
unit_cost, total_cost, quantity_before, quantity_after,
reference_number, notes, movement_date, created_at, created_by
) VALUES
-- Croissants reserved for afternoon baking
(gen_random_uuid(), demo_tenant_id,
(SELECT id FROM ingredients WHERE name = 'Croissant Clásico' AND tenant_id = demo_tenant_id),
(SELECT id FROM stock WHERE ingredient_id = (SELECT id FROM ingredients WHERE name = 'Croissant Clásico' AND tenant_id = demo_tenant_id) AND production_stage = 'par_baked' AND batch_number LIKE 'CROIS-PAR-20241203%'),
'RESERVATION', -15.0, 0.85, -12.75, 40.0, 25.0,
'RESER-20241206-001', 'Reserva para horneado de la tarde - pedido especial',
NOW() - INTERVAL '30 minutes', NOW() - INTERVAL '30 minutes', NULL);
RAISE NOTICE 'Stock movements history created successfully';
END $$;
-- ===========================================
-- 7. SUMMARY REPORTS
-- ===========================================
\echo ''
\echo '========================================='
\echo 'DEMO INVENTORY DATA SUMMARY'
\echo '========================================='
SELECT 'INGREDIENTS OVERVIEW' as report_section;
SELECT
product_type,
COALESCE(ingredient_category::text, product_category::text, 'N/A') as category,
COUNT(*) as total_items,
COUNT(CASE WHEN is_active THEN 1 END) as active_items
FROM ingredients
WHERE tenant_id = 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID
GROUP BY product_type, COALESCE(ingredient_category::text, product_category::text, 'N/A')
ORDER BY product_type, category;
\echo ''
SELECT 'STOCK SUMMARY BY STAGE' as report_section;
SELECT
production_stage,
COUNT(*) as total_lots,
SUM(current_quantity) as total_quantity,
SUM(available_quantity) as available_quantity,
SUM(reserved_quantity) as reserved_quantity,
ROUND(SUM(total_cost), 2) as total_value
FROM stock
WHERE tenant_id = 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID
GROUP BY production_stage
ORDER BY production_stage;
\echo ''
SELECT 'CRITICAL STOCK ALERTS' as report_section;
SELECT
i.name,
s.production_stage,
s.current_quantity,
s.available_quantity,
i.low_stock_threshold,
s.batch_number,
s.expiration_date,
CASE
WHEN s.available_quantity = 0 THEN 'OUT_OF_STOCK'
WHEN s.is_expired THEN 'EXPIRED'
WHEN s.expiration_date <= NOW() + INTERVAL '24 hours' THEN 'EXPIRES_SOON'
WHEN s.available_quantity <= i.low_stock_threshold THEN 'LOW_STOCK'
ELSE 'OK'
END as alert_status,
s.storage_location
FROM stock s
JOIN ingredients i ON s.ingredient_id = i.id
WHERE s.tenant_id = 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID
AND (
s.available_quantity = 0
OR s.is_expired
OR s.expiration_date <= NOW() + INTERVAL '48 hours'
OR s.available_quantity <= i.low_stock_threshold
)
ORDER BY
CASE
WHEN s.available_quantity = 0 THEN 1
WHEN s.is_expired THEN 2
WHEN s.expiration_date <= NOW() + INTERVAL '24 hours' THEN 3
WHEN s.available_quantity <= i.low_stock_threshold THEN 4
ELSE 5
END,
s.expiration_date ASC;
\echo ''
SELECT 'MOVEMENT ACTIVITY (Last 24 hours)' as report_section;
SELECT
i.name,
sm.movement_type,
ABS(sm.quantity) as quantity,
sm.reference_number,
sm.notes,
sm.movement_date
FROM stock_movements sm
JOIN ingredients i ON sm.ingredient_id = i.id
WHERE sm.tenant_id = 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID
AND sm.movement_date >= NOW() - INTERVAL '24 hours'
ORDER BY sm.movement_date DESC
LIMIT 15;
\echo ''
\echo '========================================='
\echo 'DEMO DATA CREATION COMPLETED SUCCESSFULLY'
\echo 'Tenant ID: c464fb3e-7af2-46e6-9e43-85318f34199a'
\echo 'Total Ingredients: ' || (SELECT COUNT(*) FROM ingredients WHERE tenant_id = 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID)::text
\echo 'Total Stock Lots: ' || (SELECT COUNT(*) FROM stock WHERE tenant_id = 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID)::text
\echo 'Total Movements: ' || (SELECT COUNT(*) FROM stock_movements WHERE tenant_id = 'c464fb3e-7af2-46e6-9e43-85318f34199a'::UUID)::text
\echo '========================================='

View File

@@ -250,17 +250,21 @@ class RouteBuilder:
"""
base_prefix = f"{self.BASE_PATH}/tenants/{{tenant_id}}" if include_tenant_prefix else self.BASE_PATH
# Build path segments string, avoiding trailing slash when empty
segments_str = '/'.join(path_segments) if path_segments else ''
separator = '/' if segments_str else ''
if category == RouteCategory.BASE:
return f"{base_prefix}/{self.service_name}/{'/'.join(path_segments)}"
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
elif category == RouteCategory.DASHBOARD:
return f"{base_prefix}/{self.service_name}/dashboard/{'/'.join(path_segments)}"
return f"{base_prefix}/{self.service_name}/dashboard{separator}{segments_str}"
elif category == RouteCategory.ANALYTICS:
return f"{base_prefix}/{self.service_name}/analytics/{'/'.join(path_segments)}"
return f"{base_prefix}/{self.service_name}/analytics{separator}{segments_str}"
elif category == RouteCategory.OPERATIONS:
return f"{base_prefix}/{self.service_name}/operations/{'/'.join(path_segments)}"
return f"{base_prefix}/{self.service_name}/operations{separator}{segments_str}"
# Fallback to base
return f"{base_prefix}/{self.service_name}/{'/'.join(path_segments)}"
return f"{base_prefix}/{self.service_name}{separator}{segments_str}"
@staticmethod
def get_route_pattern(category: RouteCategory, service_name: Optional[str] = None) -> str:

138
shared/scripts/run_migrations.py Executable file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""
Enhanced Migration Runner
Handles automatic table creation and Alembic migrations for Kubernetes deployments.
Supports both first-time deployments and incremental migrations.
"""
import os
import sys
import asyncio
import argparse
import structlog
from pathlib import Path
# Add the project root to the Python path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from shared.database.base import DatabaseManager
from shared.database.init_manager import initialize_service_database
# Configure logging
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
async def run_service_migration(service_name: str, force_recreate: bool = False) -> bool:
"""
Run migrations for a specific service.
This script is for MIGRATION JOBS ONLY.
Services themselves never run migrations - they only verify DB is ready.
Args:
service_name: Name of the service (e.g., 'auth', 'inventory')
force_recreate: Whether to force recreate tables (development mode)
Returns:
True if successful, False otherwise
"""
logger.info("Migration job starting", service=service_name, force_recreate=force_recreate)
try:
# Get database URL from environment (try both constructed and direct approaches)
db_url_key = f"{service_name.upper().replace('-', '_')}_DATABASE_URL"
database_url = os.getenv(db_url_key) or os.getenv("DATABASE_URL")
# If no direct URL, construct from components
if not database_url:
host = os.getenv("POSTGRES_HOST")
port = os.getenv("POSTGRES_PORT")
db_name = os.getenv("POSTGRES_DB")
user = os.getenv("POSTGRES_USER")
password = os.getenv("POSTGRES_PASSWORD")
if all([host, port, db_name, user, password]):
database_url = f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{db_name}"
logger.info("Constructed database URL from components", host=host, port=port, db=db_name)
else:
logger.error("Database connection details not found",
db_url_key=db_url_key,
host=bool(host),
port=bool(port),
db=bool(db_name),
user=bool(user),
password=bool(password))
return False
# Create database manager
db_manager = DatabaseManager(database_url=database_url)
# Run migrations (verify_only=False means actually run migrations)
result = await initialize_service_database(
database_manager=db_manager,
service_name=service_name,
verify_only=False, # Migration jobs RUN migrations
force_recreate=force_recreate
)
logger.info("Migration job completed successfully", service=service_name, result=result)
return True
except Exception as e:
logger.error("Migration job failed", service=service_name, error=str(e))
return False
finally:
# Cleanup database connections
try:
await db_manager.close_connections()
except:
pass
async def main():
"""Main migration runner"""
parser = argparse.ArgumentParser(description="Enhanced Migration Runner")
parser.add_argument("service", help="Service name (e.g., auth, inventory)")
parser.add_argument("--force-recreate", action="store_true",
help="Force recreate tables (development mode)")
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose logging")
args = parser.parse_args()
if args.verbose:
logger.info("Starting migration runner", service=args.service,
force_recreate=args.force_recreate)
# Run the migration
success = await run_service_migration(args.service, args.force_recreate)
if success:
logger.info("Migration runner completed successfully")
sys.exit(0)
else:
logger.error("Migration runner failed")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())