960 lines
32 KiB
Markdown
960 lines
32 KiB
Markdown
# Orders Service
|
|
|
|
## Overview
|
|
|
|
The **Orders Service** manages the complete customer order lifecycle from creation to fulfillment, tracking custom orders, wholesale orders, and direct sales. It maintains a comprehensive customer database with purchase history, enables order scheduling for pickup/delivery, and provides analytics on customer behavior and order patterns. This service is essential for B2B relationships with restaurants and hotels, as well as managing special orders for events and celebrations.
|
|
|
|
## Key Features
|
|
|
|
### Order Management
|
|
- **Multi-Channel Orders** - In-store, phone, online, wholesale
|
|
- **Order Lifecycle Tracking** - From pending to completed/cancelled
|
|
- **Custom Orders** - Special requests for events and celebrations
|
|
- **Recurring Orders** - Automated weekly/monthly orders for B2B
|
|
- **Order Scheduling** - Pickup/delivery date and time management
|
|
- **Order Priority** - Rush orders vs. standard processing
|
|
- **Order Status Updates** - Real-time status with customer notifications
|
|
|
|
### Customer Database
|
|
- **Customer Profiles** - Complete contact and preference information
|
|
- **Purchase History** - Track all orders per customer
|
|
- **Customer Segmentation** - B2B vs. B2C, loyalty tiers
|
|
- **Customer Preferences** - Favorite products, allergen notes
|
|
- **Credit Terms** - Payment terms for wholesale customers
|
|
- **Customer Analytics** - RFM analysis (Recency, Frequency, Monetary)
|
|
- **Customer Lifetime Value** - Total value per customer
|
|
|
|
### B2B Wholesale Management
|
|
- **Wholesale Pricing** - Custom pricing per B2B customer
|
|
- **Volume Discounts** - Automatic tier-based discounts
|
|
- **Delivery Routes** - Optimize delivery scheduling
|
|
- **Invoice Generation** - Automated invoicing with payment terms
|
|
- **Standing Orders** - Repeat orders without manual entry
|
|
- **Account Management** - Credit limits and payment tracking
|
|
|
|
### Order Fulfillment
|
|
- **Production Integration** - Orders trigger production planning
|
|
- **Inventory Reservation** - Reserve stock for confirmed orders
|
|
- **Fulfillment Status** - Track preparation and delivery
|
|
- **Delivery Management** - Route planning and tracking
|
|
- **Order Picking Lists** - Generate lists for warehouse staff
|
|
- **Quality Control** - Pre-delivery quality checks
|
|
|
|
### Payment Tracking
|
|
- **Payment Methods** - Cash, card, transfer, credit terms
|
|
- **Payment Status** - Paid, pending, overdue
|
|
- **Partial Payments** - Split payments over time
|
|
- **Invoice History** - Complete payment records
|
|
- **Overdue Alerts** - Automatic reminders for B2B accounts
|
|
- **Revenue Recognition** - Track revenue per order
|
|
|
|
### Analytics & Reporting
|
|
- **Order Dashboard** - Real-time order metrics
|
|
- **Customer Analytics** - Top customers, retention rates
|
|
- **Product Analytics** - Most ordered products
|
|
- **Revenue Analytics** - Daily/weekly/monthly revenue
|
|
- **Order Source Analysis** - Channel performance
|
|
- **Delivery Performance** - On-time delivery rates
|
|
|
|
## Business Value
|
|
|
|
### For Bakery Owners
|
|
- **Revenue Growth** - Better customer relationships drive repeat business
|
|
- **B2B Efficiency** - Automate wholesale order management
|
|
- **Cash Flow** - Track outstanding payments and credit terms
|
|
- **Customer Retention** - Purchase history enables personalized service
|
|
- **Order Accuracy** - Digital orders reduce errors vs. phone/paper
|
|
- **Analytics** - Understand customer behavior for marketing
|
|
|
|
### Quantifiable Impact
|
|
- **Revenue Growth**: 10-20% through improved B2B relationships
|
|
- **Time Savings**: 5-8 hours/week on order management
|
|
- **Order Accuracy**: 99%+ vs. 85-90% manual (phone/paper)
|
|
- **Payment Collection**: 30% faster with automated reminders
|
|
- **Customer Retention**: 15-25% improvement with history tracking
|
|
- **B2B Efficiency**: 50-70% time reduction on wholesale orders
|
|
|
|
### For Sales Staff
|
|
- **Quick Order Entry** - Fast order creation with customer lookup
|
|
- **Customer History** - See previous orders for upselling
|
|
- **Pricing Accuracy** - Automatic wholesale pricing application
|
|
- **Order Tracking** - Know exactly when orders will be ready
|
|
- **Customer Notes** - Allergen info and preferences visible
|
|
|
|
### For Customers
|
|
- **Order Confirmation** - Immediate confirmation with details
|
|
- **Order Tracking** - Real-time status updates
|
|
- **Order History** - View and repeat previous orders
|
|
- **Flexible Scheduling** - Choose pickup/delivery times
|
|
- **Payment Options** - Multiple payment methods
|
|
|
|
## Technology Stack
|
|
|
|
- **Framework**: FastAPI (Python 3.11+) - Async web framework
|
|
- **Database**: PostgreSQL 17 - Order and customer data
|
|
- **Caching**: Redis 7.4 - Customer and order cache
|
|
- **Messaging**: RabbitMQ 4.1 - Order event publishing
|
|
- **ORM**: SQLAlchemy 2.0 (async) - Database abstraction
|
|
- **Validation**: Pydantic 2.0 - Schema validation
|
|
- **Logging**: Structlog - Structured JSON logging
|
|
- **Metrics**: Prometheus Client - Order metrics
|
|
|
|
## API Endpoints (Key Routes)
|
|
|
|
### Order Management
|
|
- `GET /api/v1/orders` - List orders with filters
|
|
- `POST /api/v1/orders` - Create new order
|
|
- `GET /api/v1/orders/{order_id}` - Get order details
|
|
- `PUT /api/v1/orders/{order_id}` - Update order
|
|
- `DELETE /api/v1/orders/{order_id}` - Cancel order
|
|
- `PUT /api/v1/orders/{order_id}/status` - Update order status
|
|
- `POST /api/v1/orders/{order_id}/complete` - Mark order complete
|
|
|
|
### Order Items
|
|
- `GET /api/v1/orders/{order_id}/items` - List order items
|
|
- `POST /api/v1/orders/{order_id}/items` - Add item to order
|
|
- `PUT /api/v1/orders/{order_id}/items/{item_id}` - Update order item
|
|
- `DELETE /api/v1/orders/{order_id}/items/{item_id}` - Remove item
|
|
|
|
### Customer Management
|
|
- `GET /api/v1/customers` - List customers with filters
|
|
- `POST /api/v1/customers` - Create new customer
|
|
- `GET /api/v1/customers/{customer_id}` - Get customer details
|
|
- `PUT /api/v1/customers/{customer_id}` - Update customer
|
|
- `GET /api/v1/customers/{customer_id}/orders` - Get customer order history
|
|
- `GET /api/v1/customers/{customer_id}/analytics` - Customer analytics
|
|
|
|
### Wholesale Management
|
|
- `GET /api/v1/orders/wholesale` - List wholesale orders
|
|
- `POST /api/v1/orders/wholesale/recurring` - Create recurring order
|
|
- `GET /api/v1/orders/wholesale/invoices` - List invoices
|
|
- `POST /api/v1/orders/wholesale/invoices/{invoice_id}/send` - Send invoice
|
|
- `GET /api/v1/orders/wholesale/overdue` - List overdue payments
|
|
|
|
### Fulfillment
|
|
- `GET /api/v1/orders/fulfillment/pending` - Orders pending fulfillment
|
|
- `POST /api/v1/orders/{order_id}/prepare` - Start order preparation
|
|
- `POST /api/v1/orders/{order_id}/ready` - Mark order ready
|
|
- `POST /api/v1/orders/{order_id}/deliver` - Mark order delivered
|
|
- `GET /api/v1/orders/fulfillment/picking-list` - Generate picking list
|
|
|
|
### Analytics
|
|
- `GET /api/v1/orders/analytics/dashboard` - Order dashboard KPIs
|
|
- `GET /api/v1/orders/analytics/revenue` - Revenue analytics
|
|
- `GET /api/v1/orders/analytics/customers/top` - Top customers
|
|
- `GET /api/v1/orders/analytics/products/popular` - Most ordered products
|
|
- `GET /api/v1/orders/analytics/channels` - Order channel breakdown
|
|
|
|
## Database Schema
|
|
|
|
### Main Tables
|
|
|
|
**customers**
|
|
```sql
|
|
CREATE TABLE customers (
|
|
id UUID PRIMARY KEY,
|
|
tenant_id UUID NOT NULL,
|
|
customer_type VARCHAR(50) NOT NULL, -- retail, wholesale, restaurant, hotel
|
|
business_name VARCHAR(255), -- For B2B customers
|
|
contact_name VARCHAR(255) NOT NULL,
|
|
email VARCHAR(255),
|
|
phone VARCHAR(50) NOT NULL,
|
|
secondary_phone VARCHAR(50),
|
|
address_line1 VARCHAR(255),
|
|
address_line2 VARCHAR(255),
|
|
city VARCHAR(100),
|
|
postal_code VARCHAR(20),
|
|
country VARCHAR(100) DEFAULT 'España',
|
|
tax_id VARCHAR(50), -- CIF/NIF for businesses
|
|
credit_limit DECIMAL(10, 2), -- For B2B customers
|
|
credit_term_days INTEGER DEFAULT 0, -- Payment terms (e.g., Net 30)
|
|
payment_status VARCHAR(50) DEFAULT 'good_standing', -- good_standing, overdue, suspended
|
|
customer_notes TEXT,
|
|
allergen_notes TEXT,
|
|
preferred_contact_method VARCHAR(50), -- email, phone, whatsapp
|
|
loyalty_tier VARCHAR(50) DEFAULT 'standard', -- standard, silver, gold, platinum
|
|
total_lifetime_value DECIMAL(12, 2) DEFAULT 0.00,
|
|
total_orders INTEGER DEFAULT 0,
|
|
last_order_date DATE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(tenant_id, email),
|
|
UNIQUE(tenant_id, phone)
|
|
);
|
|
```
|
|
|
|
**orders**
|
|
```sql
|
|
CREATE TABLE orders (
|
|
id UUID PRIMARY KEY,
|
|
tenant_id UUID NOT NULL,
|
|
order_number VARCHAR(100) NOT NULL, -- Human-readable order number
|
|
customer_id UUID REFERENCES customers(id),
|
|
order_type VARCHAR(50) NOT NULL, -- retail, wholesale, custom, standing
|
|
order_source VARCHAR(50), -- in_store, phone, online, email
|
|
status VARCHAR(50) DEFAULT 'pending', -- pending, confirmed, preparing, ready, completed, cancelled
|
|
priority VARCHAR(50) DEFAULT 'standard', -- rush, standard, scheduled
|
|
order_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
requested_date DATE, -- Pickup/delivery date
|
|
requested_time TIME, -- Pickup/delivery time
|
|
fulfilled_date DATE,
|
|
subtotal DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
|
discount_amount DECIMAL(10, 2) DEFAULT 0.00,
|
|
tax_amount DECIMAL(10, 2) DEFAULT 0.00,
|
|
total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
|
payment_method VARCHAR(50), -- cash, card, transfer, credit
|
|
payment_status VARCHAR(50) DEFAULT 'unpaid', -- unpaid, paid, partial, overdue
|
|
payment_due_date DATE,
|
|
delivery_method VARCHAR(50), -- pickup, delivery, shipping
|
|
delivery_address TEXT,
|
|
delivery_notes TEXT,
|
|
internal_notes TEXT,
|
|
created_by UUID NOT NULL,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(tenant_id, order_number)
|
|
);
|
|
```
|
|
|
|
**order_items**
|
|
```sql
|
|
CREATE TABLE order_items (
|
|
id UUID PRIMARY KEY,
|
|
tenant_id UUID NOT NULL,
|
|
order_id UUID REFERENCES orders(id) ON DELETE CASCADE,
|
|
product_id UUID NOT NULL,
|
|
product_name VARCHAR(255) NOT NULL, -- Cached for performance
|
|
quantity DECIMAL(10, 2) NOT NULL,
|
|
unit VARCHAR(50) NOT NULL,
|
|
unit_price DECIMAL(10, 2) NOT NULL,
|
|
discount_percentage DECIMAL(5, 2) DEFAULT 0.00,
|
|
line_total DECIMAL(10, 2) NOT NULL,
|
|
custom_instructions TEXT,
|
|
recipe_id UUID, -- Link to recipe if applicable
|
|
production_batch_id UUID, -- Link to production batch
|
|
fulfilled_quantity DECIMAL(10, 2) DEFAULT 0.00,
|
|
fulfillment_status VARCHAR(50) DEFAULT 'pending', -- pending, reserved, prepared, fulfilled
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
**customer_pricing**
|
|
```sql
|
|
CREATE TABLE customer_pricing (
|
|
id UUID PRIMARY KEY,
|
|
tenant_id UUID NOT NULL,
|
|
customer_id UUID REFERENCES customers(id) ON DELETE CASCADE,
|
|
product_id UUID NOT NULL,
|
|
custom_price DECIMAL(10, 2) NOT NULL,
|
|
discount_percentage DECIMAL(5, 2),
|
|
min_quantity DECIMAL(10, 2), -- Minimum order quantity for price
|
|
valid_from DATE DEFAULT CURRENT_DATE,
|
|
valid_until DATE,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
notes TEXT,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(tenant_id, customer_id, product_id)
|
|
);
|
|
```
|
|
|
|
**recurring_orders**
|
|
```sql
|
|
CREATE TABLE recurring_orders (
|
|
id UUID PRIMARY KEY,
|
|
tenant_id UUID NOT NULL,
|
|
customer_id UUID REFERENCES customers(id) ON DELETE CASCADE,
|
|
recurring_name VARCHAR(255) NOT NULL,
|
|
frequency VARCHAR(50) NOT NULL, -- daily, weekly, biweekly, monthly
|
|
delivery_day VARCHAR(50), -- Monday, Tuesday, etc.
|
|
delivery_time TIME,
|
|
order_items JSONB NOT NULL, -- Array of {product_id, quantity, unit}
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
next_order_date DATE,
|
|
last_generated_order_id UUID,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
**order_status_history**
|
|
```sql
|
|
CREATE TABLE order_status_history (
|
|
id UUID PRIMARY KEY,
|
|
tenant_id UUID NOT NULL,
|
|
order_id UUID REFERENCES orders(id) ON DELETE CASCADE,
|
|
from_status VARCHAR(50),
|
|
to_status VARCHAR(50) NOT NULL,
|
|
changed_by UUID NOT NULL,
|
|
notes TEXT,
|
|
changed_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
**invoices**
|
|
```sql
|
|
CREATE TABLE invoices (
|
|
id UUID PRIMARY KEY,
|
|
tenant_id UUID NOT NULL,
|
|
invoice_number VARCHAR(100) NOT NULL,
|
|
order_id UUID REFERENCES orders(id),
|
|
customer_id UUID REFERENCES customers(id),
|
|
invoice_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
due_date DATE NOT NULL,
|
|
subtotal DECIMAL(10, 2) NOT NULL,
|
|
tax_amount DECIMAL(10, 2) NOT NULL,
|
|
total_amount DECIMAL(10, 2) NOT NULL,
|
|
amount_paid DECIMAL(10, 2) DEFAULT 0.00,
|
|
amount_due DECIMAL(10, 2) NOT NULL,
|
|
status VARCHAR(50) DEFAULT 'sent', -- draft, sent, paid, overdue, cancelled
|
|
payment_terms VARCHAR(255),
|
|
notes TEXT,
|
|
sent_at TIMESTAMP,
|
|
paid_at TIMESTAMP,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE(tenant_id, invoice_number)
|
|
);
|
|
```
|
|
|
|
### Indexes for Performance
|
|
```sql
|
|
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
|
|
CREATE INDEX idx_orders_customer ON orders(customer_id);
|
|
CREATE INDEX idx_orders_date ON orders(tenant_id, order_date DESC);
|
|
CREATE INDEX idx_orders_requested_date ON orders(tenant_id, requested_date);
|
|
CREATE INDEX idx_customers_tenant_type ON customers(tenant_id, customer_type);
|
|
CREATE INDEX idx_order_items_order ON order_items(order_id);
|
|
CREATE INDEX idx_order_items_product ON order_items(tenant_id, product_id);
|
|
CREATE INDEX idx_invoices_status ON invoices(tenant_id, status);
|
|
CREATE INDEX idx_invoices_due_date ON invoices(tenant_id, due_date) WHERE status != 'paid';
|
|
```
|
|
|
|
## Business Logic Examples
|
|
|
|
### Order Creation with Pricing
|
|
```python
|
|
async def create_order(order_data: OrderCreate, current_user: User) -> Order:
|
|
"""
|
|
Create new order with automatic pricing and customer detection.
|
|
"""
|
|
# Get or create customer
|
|
customer = await get_or_create_customer(
|
|
order_data.customer_phone,
|
|
order_data.customer_name,
|
|
order_data.customer_email
|
|
)
|
|
|
|
# Generate order number
|
|
order_number = await generate_order_number(current_user.tenant_id)
|
|
|
|
# Create order
|
|
order = Order(
|
|
tenant_id=current_user.tenant_id,
|
|
order_number=order_number,
|
|
customer_id=customer.id,
|
|
order_type=order_data.order_type,
|
|
order_source=order_data.order_source,
|
|
status='pending',
|
|
order_date=date.today(),
|
|
requested_date=order_data.requested_date,
|
|
created_by=current_user.id
|
|
)
|
|
db.add(order)
|
|
await db.flush() # Get order.id
|
|
|
|
# Add order items with pricing
|
|
subtotal = Decimal('0.00')
|
|
for item_data in order_data.items:
|
|
# Get product price
|
|
base_price = await get_product_price(item_data.product_id)
|
|
|
|
# Check for customer-specific pricing
|
|
custom_price = await get_customer_price(
|
|
customer.id,
|
|
item_data.product_id,
|
|
item_data.quantity
|
|
)
|
|
unit_price = custom_price if custom_price else base_price
|
|
|
|
# Apply wholesale discount if applicable
|
|
if customer.customer_type == 'wholesale':
|
|
discount_pct = await calculate_volume_discount(
|
|
item_data.product_id,
|
|
item_data.quantity
|
|
)
|
|
else:
|
|
discount_pct = Decimal('0.00')
|
|
|
|
# Calculate line total
|
|
line_total = (unit_price * item_data.quantity) * (1 - discount_pct / 100)
|
|
|
|
# Create order item
|
|
order_item = OrderItem(
|
|
tenant_id=current_user.tenant_id,
|
|
order_id=order.id,
|
|
product_id=item_data.product_id,
|
|
product_name=item_data.product_name,
|
|
quantity=item_data.quantity,
|
|
unit=item_data.unit,
|
|
unit_price=unit_price,
|
|
discount_percentage=discount_pct,
|
|
line_total=line_total
|
|
)
|
|
db.add(order_item)
|
|
subtotal += line_total
|
|
|
|
# Calculate tax (e.g., Spanish IVA 10% for food)
|
|
tax_rate = Decimal('0.10')
|
|
tax_amount = subtotal * tax_rate
|
|
total_amount = subtotal + tax_amount
|
|
|
|
# Update order totals
|
|
order.subtotal = subtotal
|
|
order.tax_amount = tax_amount
|
|
order.total_amount = total_amount
|
|
|
|
# Set payment terms for B2B
|
|
if customer.customer_type == 'wholesale':
|
|
order.payment_due_date = date.today() + timedelta(days=customer.credit_term_days)
|
|
order.payment_status = 'unpaid'
|
|
else:
|
|
order.payment_status = 'paid' # Retail assumes immediate payment
|
|
|
|
await db.commit()
|
|
await db.refresh(order)
|
|
|
|
# Publish order created event
|
|
await publish_event('orders', 'order.created', {
|
|
'order_id': str(order.id),
|
|
'customer_id': str(customer.id),
|
|
'total_amount': float(order.total_amount),
|
|
'requested_date': order.requested_date.isoformat() if order.requested_date else None
|
|
})
|
|
|
|
return order
|
|
```
|
|
|
|
### Recurring Order Generation
|
|
```python
|
|
async def generate_recurring_orders(tenant_id: UUID):
|
|
"""
|
|
Generate orders from recurring order templates.
|
|
Run daily via orchestrator.
|
|
"""
|
|
# Get active recurring orders due today
|
|
today = date.today()
|
|
recurring_orders = await db.query(RecurringOrder).filter(
|
|
RecurringOrder.tenant_id == tenant_id,
|
|
RecurringOrder.is_active == True,
|
|
RecurringOrder.next_order_date <= today
|
|
).all()
|
|
|
|
generated_count = 0
|
|
for recurring in recurring_orders:
|
|
try:
|
|
# Create order from template
|
|
order = Order(
|
|
tenant_id=tenant_id,
|
|
order_number=await generate_order_number(tenant_id),
|
|
customer_id=recurring.customer_id,
|
|
order_type='standing',
|
|
order_source='auto_recurring',
|
|
status='confirmed',
|
|
order_date=today,
|
|
requested_date=recurring.next_order_date,
|
|
requested_time=recurring.delivery_time
|
|
)
|
|
db.add(order)
|
|
await db.flush()
|
|
|
|
# Add items from template
|
|
subtotal = Decimal('0.00')
|
|
for item_template in recurring.order_items:
|
|
product_price = await get_product_price(item_template['product_id'])
|
|
line_total = product_price * Decimal(str(item_template['quantity']))
|
|
|
|
order_item = OrderItem(
|
|
tenant_id=tenant_id,
|
|
order_id=order.id,
|
|
product_id=UUID(item_template['product_id']),
|
|
product_name=item_template['product_name'],
|
|
quantity=Decimal(str(item_template['quantity'])),
|
|
unit=item_template['unit'],
|
|
unit_price=product_price,
|
|
line_total=line_total
|
|
)
|
|
db.add(order_item)
|
|
subtotal += line_total
|
|
|
|
# Calculate totals
|
|
tax_amount = subtotal * Decimal('0.10')
|
|
order.subtotal = subtotal
|
|
order.tax_amount = tax_amount
|
|
order.total_amount = subtotal + tax_amount
|
|
|
|
# Update recurring order
|
|
recurring.last_generated_order_id = order.id
|
|
recurring.next_order_date = calculate_next_order_date(
|
|
recurring.next_order_date,
|
|
recurring.frequency
|
|
)
|
|
|
|
await db.commit()
|
|
generated_count += 1
|
|
|
|
# Publish event
|
|
await publish_event('orders', 'recurring_order.generated', {
|
|
'order_id': str(order.id),
|
|
'recurring_order_id': str(recurring.id),
|
|
'customer_id': str(recurring.customer_id)
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to generate recurring order",
|
|
recurring_id=str(recurring.id),
|
|
error=str(e))
|
|
continue
|
|
|
|
logger.info("Generated recurring orders",
|
|
tenant_id=str(tenant_id),
|
|
count=generated_count)
|
|
|
|
return generated_count
|
|
```
|
|
|
|
### Customer RFM Analysis
|
|
```python
|
|
async def calculate_customer_rfm(customer_id: UUID) -> dict:
|
|
"""
|
|
Calculate RFM (Recency, Frequency, Monetary) metrics for customer.
|
|
"""
|
|
# Get customer orders
|
|
orders = await db.query(Order).filter(
|
|
Order.customer_id == customer_id,
|
|
Order.status.in_(['completed'])
|
|
).order_by(Order.order_date.desc()).all()
|
|
|
|
if not orders:
|
|
return {"rfm_score": 0, "segment": "inactive"}
|
|
|
|
# Recency: Days since last order
|
|
last_order_date = orders[0].order_date
|
|
recency_days = (date.today() - last_order_date).days
|
|
|
|
# Frequency: Number of orders in last 365 days
|
|
one_year_ago = date.today() - timedelta(days=365)
|
|
recent_orders = [o for o in orders if o.order_date >= one_year_ago]
|
|
frequency = len(recent_orders)
|
|
|
|
# Monetary: Total spend in last 365 days
|
|
monetary = sum(o.total_amount for o in recent_orders)
|
|
|
|
# Score each dimension (1-5 scale)
|
|
recency_score = 5 if recency_days <= 30 else \
|
|
4 if recency_days <= 60 else \
|
|
3 if recency_days <= 90 else \
|
|
2 if recency_days <= 180 else 1
|
|
|
|
frequency_score = 5 if frequency >= 12 else \
|
|
4 if frequency >= 6 else \
|
|
3 if frequency >= 3 else \
|
|
2 if frequency >= 1 else 1
|
|
|
|
monetary_score = 5 if monetary >= 5000 else \
|
|
4 if monetary >= 2000 else \
|
|
3 if monetary >= 500 else \
|
|
2 if monetary >= 100 else 1
|
|
|
|
# Overall RFM score
|
|
rfm_score = (recency_score + frequency_score + monetary_score) / 3
|
|
|
|
# Customer segment
|
|
if rfm_score >= 4.5:
|
|
segment = "champion"
|
|
elif rfm_score >= 3.5:
|
|
segment = "loyal"
|
|
elif rfm_score >= 2.5:
|
|
segment = "potential"
|
|
elif rfm_score >= 1.5:
|
|
segment = "at_risk"
|
|
else:
|
|
segment = "inactive"
|
|
|
|
return {
|
|
"rfm_score": round(rfm_score, 2),
|
|
"recency_days": recency_days,
|
|
"recency_score": recency_score,
|
|
"frequency": frequency,
|
|
"frequency_score": frequency_score,
|
|
"monetary": float(monetary),
|
|
"monetary_score": monetary_score,
|
|
"segment": segment
|
|
}
|
|
```
|
|
|
|
## Events & Messaging
|
|
|
|
### Published Events (RabbitMQ)
|
|
|
|
**Exchange**: `orders`
|
|
**Routing Keys**: `orders.created`, `orders.completed`, `orders.cancelled`, `orders.overdue`
|
|
|
|
**Order Created Event**
|
|
```json
|
|
{
|
|
"event_type": "order_created",
|
|
"tenant_id": "uuid",
|
|
"order_id": "uuid",
|
|
"order_number": "ORD-2025-1106-001",
|
|
"customer_id": "uuid",
|
|
"customer_name": "Restaurante El Prado",
|
|
"order_type": "wholesale",
|
|
"total_amount": 450.00,
|
|
"requested_date": "2025-11-07",
|
|
"requested_time": "06:00:00",
|
|
"item_count": 12,
|
|
"timestamp": "2025-11-06T10:30:00Z"
|
|
}
|
|
```
|
|
|
|
**Order Completed Event**
|
|
```json
|
|
{
|
|
"event_type": "order_completed",
|
|
"tenant_id": "uuid",
|
|
"order_id": "uuid",
|
|
"order_number": "ORD-2025-1106-001",
|
|
"customer_id": "uuid",
|
|
"total_amount": 450.00,
|
|
"payment_status": "paid",
|
|
"completed_at": "2025-11-07T06:15:00Z",
|
|
"timestamp": "2025-11-07T06:15:00Z"
|
|
}
|
|
```
|
|
|
|
**Payment Overdue Alert**
|
|
```json
|
|
{
|
|
"event_type": "payment_overdue",
|
|
"tenant_id": "uuid",
|
|
"invoice_id": "uuid",
|
|
"invoice_number": "INV-2025-1106-001",
|
|
"customer_id": "uuid",
|
|
"customer_name": "Hotel Gran Vía",
|
|
"amount_due": 850.00,
|
|
"days_overdue": 15,
|
|
"due_date": "2025-10-22",
|
|
"timestamp": "2025-11-06T09:00:00Z"
|
|
}
|
|
```
|
|
|
|
### Alert Events
|
|
|
|
The Orders service also publishes procurement-related alerts through the alert processor.
|
|
|
|
**Exchange**: `events.exchange`
|
|
**Domain**: `procurement`
|
|
|
|
#### 1. POs Pending Approval Alert
|
|
**Event Type**: `procurement.pos_pending_approval`
|
|
**Severity**: urgent (>€10,000), high (>€5,000 or critical POs), medium (otherwise)
|
|
**Trigger**: New purchase orders created and awaiting approval
|
|
|
|
```json
|
|
{
|
|
"event_type": "procurement.pos_pending_approval",
|
|
"severity": "high",
|
|
"metadata": {
|
|
"tenant_id": "uuid",
|
|
"pos_count": 3,
|
|
"total_amount": 6500.00,
|
|
"critical_count": 1,
|
|
"pos": [
|
|
{
|
|
"po_id": "uuid",
|
|
"po_number": "PO-001",
|
|
"supplier_id": "uuid",
|
|
"total_amount": 3000.00,
|
|
"auto_approved": false
|
|
}
|
|
],
|
|
"action_required": true,
|
|
"action_url": "/app/comprar"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2. Approval Reminder Alert
|
|
**Event Type**: `procurement.approval_reminder`
|
|
**Severity**: high (>36 hours pending), medium (otherwise)
|
|
**Trigger**: PO not approved within threshold time
|
|
|
|
```json
|
|
{
|
|
"event_type": "procurement.approval_reminder",
|
|
"severity": "high",
|
|
"metadata": {
|
|
"tenant_id": "uuid",
|
|
"po_id": "uuid",
|
|
"po_number": "PO-001",
|
|
"supplier_name": "Supplier ABC",
|
|
"total_amount": 3000.00,
|
|
"hours_pending": 40,
|
|
"created_at": "2025-12-18T10:00:00Z",
|
|
"action_required": true,
|
|
"action_url": "/app/comprar?po=uuid"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. Critical PO Escalation Alert
|
|
**Event Type**: `procurement.critical_po_escalation`
|
|
**Severity**: urgent
|
|
**Trigger**: Critical/urgent PO not approved in time
|
|
|
|
```json
|
|
{
|
|
"event_type": "procurement.critical_po_escalation",
|
|
"severity": "urgent",
|
|
"metadata": {
|
|
"tenant_id": "uuid",
|
|
"po_id": "uuid",
|
|
"po_number": "PO-001",
|
|
"supplier_name": "Supplier ABC",
|
|
"total_amount": 5000.00,
|
|
"priority": "urgent",
|
|
"required_delivery_date": "2025-12-22",
|
|
"hours_pending": 48,
|
|
"escalated": true,
|
|
"action_required": true,
|
|
"action_url": "/app/comprar?po=uuid"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 4. Auto-Approval Summary (Notification)
|
|
**Event Type**: `procurement.auto_approval_summary`
|
|
**Type**: Notification (not alert)
|
|
**Trigger**: Daily summary of auto-approved POs
|
|
|
|
```json
|
|
{
|
|
"event_type": "procurement.auto_approval_summary",
|
|
"metadata": {
|
|
"tenant_id": "uuid",
|
|
"auto_approved_count": 5,
|
|
"total_auto_approved_amount": 8500.00,
|
|
"manual_approval_count": 2,
|
|
"summary_date": "2025-12-19",
|
|
"auto_approved_pos": [...],
|
|
"pending_approval_pos": [...],
|
|
"action_url": "/app/comprar"
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 5. PO Approved Confirmation (Notification)
|
|
**Event Type**: `procurement.po_approved_confirmation`
|
|
**Type**: Notification (not alert)
|
|
**Trigger**: Purchase order approved
|
|
|
|
```json
|
|
{
|
|
"event_type": "procurement.po_approved_confirmation",
|
|
"metadata": {
|
|
"tenant_id": "uuid",
|
|
"po_id": "uuid",
|
|
"po_number": "PO-001",
|
|
"supplier_name": "Supplier ABC",
|
|
"total_amount": 3000.00,
|
|
"approved_by": "user@example.com",
|
|
"auto_approved": false,
|
|
"approved_at": "2025-12-19T14:30:00Z",
|
|
"action_url": "/app/comprar?po=uuid"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Consumed Events
|
|
- **From Production**: Batch completion updates order fulfillment status
|
|
- **From Inventory**: Stock availability affects order confirmation
|
|
- **From Forecasting**: Demand forecasts inform production for pending orders
|
|
|
|
## Custom Metrics (Prometheus)
|
|
|
|
```python
|
|
# Order metrics
|
|
orders_total = Counter(
|
|
'orders_total',
|
|
'Total orders created',
|
|
['tenant_id', 'order_type', 'order_source', 'status']
|
|
)
|
|
|
|
order_value_euros = Histogram(
|
|
'order_value_euros',
|
|
'Order value distribution',
|
|
['tenant_id', 'order_type'],
|
|
buckets=[10, 25, 50, 100, 200, 500, 1000, 2000, 5000]
|
|
)
|
|
|
|
# Customer metrics
|
|
customers_total = Gauge(
|
|
'customers_total',
|
|
'Total customers',
|
|
['tenant_id', 'customer_type']
|
|
)
|
|
|
|
customer_lifetime_value_euros = Histogram(
|
|
'customer_lifetime_value_euros',
|
|
'Customer lifetime value distribution',
|
|
['tenant_id', 'customer_type'],
|
|
buckets=[100, 500, 1000, 2000, 5000, 10000, 20000, 50000]
|
|
)
|
|
|
|
# Fulfillment metrics
|
|
order_fulfillment_time_hours = Histogram(
|
|
'order_fulfillment_time_hours',
|
|
'Time from order to fulfillment',
|
|
['tenant_id', 'order_type'],
|
|
buckets=[1, 6, 12, 24, 48, 72]
|
|
)
|
|
|
|
# Payment metrics
|
|
invoice_payment_time_days = Histogram(
|
|
'invoice_payment_time_days',
|
|
'Days from invoice to payment',
|
|
['tenant_id'],
|
|
buckets=[0, 7, 14, 21, 30, 45, 60, 90]
|
|
)
|
|
|
|
overdue_invoices_total = Gauge(
|
|
'overdue_invoices_total',
|
|
'Total overdue invoices',
|
|
['tenant_id']
|
|
)
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### Environment Variables
|
|
|
|
**Service Configuration:**
|
|
- `PORT` - Service port (default: 8010)
|
|
- `DATABASE_URL` - PostgreSQL connection string
|
|
- `REDIS_URL` - Redis connection string
|
|
- `RABBITMQ_URL` - RabbitMQ connection string
|
|
|
|
**Order Configuration:**
|
|
- `AUTO_CONFIRM_RETAIL_ORDERS` - Auto-confirm retail orders (default: true)
|
|
- `ORDER_NUMBER_PREFIX` - Order number prefix (default: "ORD")
|
|
- `DEFAULT_TAX_RATE` - Default tax rate (default: 0.10 for Spain's 10% IVA)
|
|
- `ENABLE_RECURRING_ORDERS` - Enable recurring order generation (default: true)
|
|
|
|
**Payment Configuration:**
|
|
- `DEFAULT_CREDIT_TERMS_DAYS` - Default payment terms (default: 30)
|
|
- `OVERDUE_ALERT_THRESHOLD_DAYS` - Days before overdue alert (default: 7)
|
|
- `MAX_CREDIT_LIMIT` - Maximum credit limit per customer (default: 10000.00)
|
|
|
|
**Notification:**
|
|
- `SEND_ORDER_CONFIRMATION` - Send order confirmation to customer (default: true)
|
|
- `SEND_READY_NOTIFICATION` - Notify when order ready (default: true)
|
|
- `SEND_OVERDUE_REMINDERS` - Send overdue payment reminders (default: true)
|
|
|
|
## Development Setup
|
|
|
|
### Prerequisites
|
|
- Python 3.11+
|
|
- PostgreSQL 17
|
|
- Redis 7.4
|
|
- RabbitMQ 4.1
|
|
|
|
### Local Development
|
|
```bash
|
|
cd services/orders
|
|
python -m venv venv
|
|
source venv/bin/activate
|
|
|
|
pip install -r requirements.txt
|
|
|
|
export DATABASE_URL=postgresql://user:pass@localhost:5432/orders
|
|
export REDIS_URL=redis://localhost:6379/0
|
|
export RABBITMQ_URL=amqp://guest:guest@localhost:5672/
|
|
|
|
alembic upgrade head
|
|
python main.py
|
|
```
|
|
|
|
## Integration Points
|
|
|
|
### Dependencies
|
|
- **Customers Service** - Customer data (if separate)
|
|
- **Products Service** - Product catalog and pricing
|
|
- **Inventory Service** - Stock availability checks
|
|
- **Production Service** - Production planning for orders
|
|
- **Auth Service** - User authentication
|
|
- **PostgreSQL** - Order and customer data
|
|
- **Redis** - Caching
|
|
- **RabbitMQ** - Event publishing
|
|
|
|
### Dependents
|
|
- **Production Service** - Orders trigger production planning
|
|
- **Inventory Service** - Orders reserve stock
|
|
- **Invoicing/Accounting** - Financial reporting
|
|
- **Notification Service** - Order confirmations and alerts
|
|
- **AI Insights Service** - Customer behavior analysis
|
|
- **Frontend Dashboard** - Order management UI
|
|
|
|
## Business Value for VUE Madrid
|
|
|
|
### Problem Statement
|
|
Spanish bakeries struggle with:
|
|
- Manual order tracking on paper or spreadsheets
|
|
- Lost orders and miscommunication (especially phone orders)
|
|
- No customer purchase history for relationship management
|
|
- Complex wholesale order management with multiple B2B clients
|
|
- Overdue payment tracking for credit accounts
|
|
- No analytics on customer behavior or product popularity
|
|
|
|
### Solution
|
|
Bakery-IA Orders Service provides:
|
|
- **Digital Order Management**: Capture all orders across channels
|
|
- **Customer Database**: Complete purchase history and preferences
|
|
- **B2B Automation**: Recurring orders and automated invoicing
|
|
- **Payment Tracking**: Monitor outstanding payments with alerts
|
|
- **Analytics**: Customer segmentation and product performance
|
|
|
|
### Quantifiable Impact
|
|
|
|
**Revenue Growth:**
|
|
- 10-20% revenue increase through improved B2B relationships
|
|
- 5-10% from reduced lost orders (99% order accuracy)
|
|
- 15-25% customer retention improvement with history tracking
|
|
- **Total: €300-600/month additional revenue per bakery**
|
|
|
|
**Time Savings:**
|
|
- 5-8 hours/week on order management and tracking
|
|
- 2-3 hours/week on invoicing and payment follow-up
|
|
- 1-2 hours/week on customer lookup and history
|
|
- **Total: 8-13 hours/week saved**
|
|
|
|
**Financial Performance:**
|
|
- 30% faster payment collection (overdue alerts)
|
|
- 50-70% time reduction on wholesale order processing
|
|
- 99%+ order accuracy vs. 85-90% manual
|
|
|
|
### Target Market Fit (Spanish Bakeries)
|
|
- **B2B Focus**: Many Spanish bakeries supply restaurants, hotels, cafés
|
|
- **Payment Terms**: Spanish B2B typically uses Net 30-60 payment terms
|
|
- **Relationship-Driven**: Customer history critical for Spanish business culture
|
|
- **Regulatory**: Spanish tax law requires proper invoicing and records
|
|
|
|
### ROI Calculation
|
|
**Investment**: €0 additional (included in platform subscription)
|
|
**Monthly Value**: €300-600 additional revenue + cost savings
|
|
**Annual ROI**: €3,600-7,200 value per bakery
|
|
**Payback**: Immediate (included in subscription)
|
|
|
|
---
|
|
|
|
**Copyright © 2025 Bakery-IA. All rights reserved.**
|