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

900 lines
31 KiB
Markdown
Raw Normal View History

2025-11-06 14:10:04 +01:00
# POS Service
## Overview
The **POS (Point of Sale) Service** integrates with popular POS systems like Square, Toast, and Lightspeed to automatically sync sales transactions into Bakery-IA. It eliminates manual sales data entry, ensures real-time sales tracking, and provides the foundation for accurate demand forecasting. This service bridges the gap between retail operations and business intelligence, making the platform immediately valuable for bakeries already using modern POS systems.
## Key Features
### Multi-POS Integration
- **Square Integration** - Full API integration with Square POS
- **Toast Integration** - Restaurant POS system integration
- **Lightspeed Integration** - Retail POS system integration
- **Webhook Support** - Real-time transaction sync via webhooks
- **OAuth Authentication** - Secure POS account linking
- **Multi-Location Support** - Handle multiple store locations
- **Automatic Reconnection** - Handle API token expiration gracefully
### Sales Data Synchronization
- **Real-Time Sync** - Transactions sync within seconds
- **Historical Import** - Import past sales data on initial setup
- **Product Mapping** - Map POS products to Bakery-IA products
- **Transaction Deduplication** - Prevent duplicate entries
- **Data Validation** - Ensure data quality and accuracy
- **Sync Status Tracking** - Monitor sync health and errors
- **Manual Sync Trigger** - Force sync on demand
### Transaction Processing
- **Line Item Details** - Product, quantity, price per transaction
- **Payment Methods** - Cash, card, contactless tracking
- **Customer Data** - Customer name, email if available
- **Discounts & Taxes** - Full transaction details preserved
- **Refunds & Voids** - Handle transaction cancellations
- **Tips & Gratuities** - Track additional revenue
- **Transaction Metadata** - Store name, cashier, timestamp
### Product Catalog Sync
- **Product Import** - Sync product catalog from POS
- **Category Mapping** - Map POS categories to Bakery-IA
- **Price Sync** - Keep prices updated
- **Product Updates** - Detect new products automatically
- **SKU Matching** - Match by SKU, name, or manual mapping
- **Inventory Integration** - Link POS products to inventory items
### Analytics & Monitoring
- **Sync Dashboard** - Monitor sync status across POS systems
- **Error Tracking** - Log and alert on sync failures
- **Data Quality Metrics** - Track unmapped products, errors
- **Sync Performance** - Monitor sync speed and latency
- **Transaction Volume** - Daily/hourly transaction counts
- **API Health Monitoring** - Track POS API availability
### Configuration Management
- **POS Account Linking** - Connect POS accounts via OAuth
- **Mapping Configuration** - Product and category mappings
- **Sync Schedule** - Configure sync frequency
- **Webhook Management** - Register/update webhook endpoints
- **API Credentials** - Secure storage of API keys
- **Multi-Tenant Isolation** - Separate POS accounts per tenant
## Business Value
### For Bakery Owners
- **Zero Manual Entry** - Sales automatically sync to Bakery-IA
- **Real-Time Visibility** - Know sales performance instantly
- **Accurate Forecasting** - ML models use actual sales data
- **Time Savings** - Eliminate daily sales data entry
- **Data Accuracy** - 99.9%+ vs. manual entry errors
- **Immediate ROI** - Value from day one of POS connection
### Quantifiable Impact
- **Time Savings**: 5-8 hours/week eliminating manual entry
- **Data Accuracy**: 99.9%+ vs. 85-95% manual entry
- **Forecast Improvement**: 10-20% better accuracy with real data
- **Revenue Tracking**: Real-time vs. end-of-day manual reconciliation
- **Setup Time**: 15 minutes to connect vs. hours of manual entry
- **Error Elimination**: Zero transcription errors
### For Sales Staff
- **No Extra Work** - POS integration is invisible to staff
- **Focus on Customers** - No post-sale data entry
- **Instant Reporting** - Managers see sales in real-time
### For Managers
- **Real-Time Dashboards** - Sales performance updates live
- **Product Performance** - Know what's selling instantly
- **Multi-Store Visibility** - All locations in one view
- **Trend Detection** - Spot patterns as they emerge
## Technology Stack
- **Framework**: FastAPI (Python 3.11+) - Async web framework
- **Database**: PostgreSQL 17 - Transaction and mapping data
- **Caching**: Redis 7.4 - Transaction deduplication cache
- **Messaging**: RabbitMQ 4.1 - Transaction event publishing
- **HTTP Client**: HTTPx - Async API calls to POS systems
- **OAuth**: Authlib - OAuth 2.0 flows for POS authentication
- **Webhooks**: FastAPI webhook receivers
- **Logging**: Structlog - Structured JSON logging
- **Metrics**: Prometheus Client - Sync metrics
## API Endpoints (Key Routes)
### POS Account Management
- `GET /api/v1/pos/accounts` - List connected POS accounts
- `POST /api/v1/pos/accounts` - Connect new POS account
- `GET /api/v1/pos/accounts/{account_id}` - Get account details
- `PUT /api/v1/pos/accounts/{account_id}` - Update account
- `DELETE /api/v1/pos/accounts/{account_id}` - Disconnect account
- `POST /api/v1/pos/accounts/{account_id}/reconnect` - Refresh OAuth tokens
### OAuth & Authentication
- `GET /api/v1/pos/oauth/square/authorize` - Start Square OAuth flow
- `GET /api/v1/pos/oauth/square/callback` - Square OAuth callback
- `GET /api/v1/pos/oauth/toast/authorize` - Start Toast OAuth flow
- `GET /api/v1/pos/oauth/toast/callback` - Toast OAuth callback
- `GET /api/v1/pos/oauth/lightspeed/authorize` - Start Lightspeed OAuth
- `GET /api/v1/pos/oauth/lightspeed/callback` - Lightspeed callback
### Synchronization
- `POST /api/v1/pos/sync/{account_id}` - Trigger manual sync
- `POST /api/v1/pos/sync/{account_id}/historical` - Import historical data
- `GET /api/v1/pos/sync/{account_id}/status` - Get sync status
- `GET /api/v1/pos/sync/{account_id}/history` - Sync history log
### Product Mapping
- `GET /api/v1/pos/mappings` - List product mappings
- `POST /api/v1/pos/mappings` - Create product mapping
- `PUT /api/v1/pos/mappings/{mapping_id}` - Update mapping
- `DELETE /api/v1/pos/mappings/{mapping_id}` - Delete mapping
- `GET /api/v1/pos/mappings/unmapped` - List unmapped POS products
- `POST /api/v1/pos/mappings/auto-map` - Auto-map by name/SKU
### Webhooks
- `POST /api/v1/pos/webhooks/square` - Square webhook receiver
- `POST /api/v1/pos/webhooks/toast` - Toast webhook receiver
- `POST /api/v1/pos/webhooks/lightspeed` - Lightspeed webhook receiver
- `POST /api/v1/pos/accounts/{account_id}/webhooks/register` - Register webhooks
### Analytics
- `GET /api/v1/pos/analytics/dashboard` - POS sync dashboard
- `GET /api/v1/pos/analytics/sync-health` - Sync health metrics
- `GET /api/v1/pos/analytics/unmapped-revenue` - Revenue from unmapped products
## Database Schema
### Main Tables
**pos_accounts**
```sql
CREATE TABLE pos_accounts (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
pos_provider VARCHAR(50) NOT NULL, -- square, toast, lightspeed
account_name VARCHAR(255),
location_id VARCHAR(255), -- POS location identifier
location_name VARCHAR(255),
-- OAuth credentials (encrypted)
access_token TEXT,
refresh_token TEXT,
token_expires_at TIMESTAMP,
merchant_id VARCHAR(255),
-- Sync configuration
sync_enabled BOOLEAN DEFAULT TRUE,
sync_frequency_minutes INTEGER DEFAULT 15,
last_sync_at TIMESTAMP,
last_successful_sync_at TIMESTAMP,
next_sync_at TIMESTAMP,
-- Webhook configuration
webhook_id VARCHAR(255),
webhook_url VARCHAR(500),
webhook_signature_key TEXT,
-- Status
status VARCHAR(50) DEFAULT 'active', -- active, disconnected, error
error_message TEXT,
error_count INTEGER DEFAULT 0,
last_error_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(tenant_id, pos_provider, location_id)
);
```
**pos_transactions**
```sql
CREATE TABLE pos_transactions (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
pos_account_id UUID REFERENCES pos_accounts(id) ON DELETE CASCADE,
pos_transaction_id VARCHAR(255) NOT NULL, -- Original POS transaction ID
pos_provider VARCHAR(50) NOT NULL,
-- Transaction details
transaction_date TIMESTAMP NOT NULL,
transaction_type VARCHAR(50) DEFAULT 'sale', -- sale, refund, void
status VARCHAR(50), -- completed, pending, failed
-- Financial
subtotal DECIMAL(10, 2) NOT NULL,
tax_amount DECIMAL(10, 2) DEFAULT 0.00,
discount_amount DECIMAL(10, 2) DEFAULT 0.00,
tip_amount DECIMAL(10, 2) DEFAULT 0.00,
total_amount DECIMAL(10, 2) NOT NULL,
currency VARCHAR(10) DEFAULT 'EUR',
-- Payment
payment_method VARCHAR(50), -- cash, card, contactless, mobile
card_last_four VARCHAR(4),
card_brand VARCHAR(50),
-- Customer (if available)
customer_name VARCHAR(255),
customer_email VARCHAR(255),
customer_phone VARCHAR(50),
-- Metadata
cashier_name VARCHAR(255),
device_name VARCHAR(255),
receipt_number VARCHAR(100),
-- Processing
synced_to_sales BOOLEAN DEFAULT FALSE,
sales_record_id UUID,
sync_error TEXT,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(tenant_id, pos_provider, pos_transaction_id)
);
```
**pos_transaction_items**
```sql
CREATE TABLE pos_transaction_items (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
pos_transaction_id UUID REFERENCES pos_transactions(id) ON DELETE CASCADE,
pos_item_id VARCHAR(255), -- POS product ID
-- Product details
product_name VARCHAR(255) NOT NULL,
product_sku VARCHAR(100),
category VARCHAR(100),
quantity DECIMAL(10, 2) NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
discount_amount DECIMAL(10, 2) DEFAULT 0.00,
line_total DECIMAL(10, 2) NOT NULL,
-- Mapping
mapped_product_id UUID, -- Bakery-IA product ID
is_mapped BOOLEAN DEFAULT FALSE,
-- Modifiers (e.g., "Extra frosting")
modifiers JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
```
**pos_product_mappings**
```sql
CREATE TABLE pos_product_mappings (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
pos_account_id UUID REFERENCES pos_accounts(id) ON DELETE CASCADE,
pos_product_id VARCHAR(255) NOT NULL,
pos_product_name VARCHAR(255) NOT NULL,
pos_product_sku VARCHAR(100),
pos_category VARCHAR(100),
-- Mapping
bakery_product_id UUID NOT NULL, -- Link to products catalog
bakery_product_name VARCHAR(255) NOT NULL,
-- Configuration
mapping_type VARCHAR(50) DEFAULT 'manual', -- manual, auto, sku
confidence_score DECIMAL(3, 2), -- For auto-mapping
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(tenant_id, pos_account_id, pos_product_id)
);
```
**pos_sync_logs**
```sql
CREATE TABLE pos_sync_logs (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
pos_account_id UUID REFERENCES pos_accounts(id) ON DELETE CASCADE,
sync_started_at TIMESTAMP NOT NULL,
sync_completed_at TIMESTAMP,
sync_duration_seconds INTEGER,
-- Status
status VARCHAR(50) NOT NULL, -- success, partial, failed
error_message TEXT,
-- Metrics
transactions_fetched INTEGER DEFAULT 0,
transactions_processed INTEGER DEFAULT 0,
transactions_failed INTEGER DEFAULT 0,
new_products_discovered INTEGER DEFAULT 0,
unmapped_products_count INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
```
**pos_webhooks**
```sql
CREATE TABLE pos_webhooks (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
pos_account_id UUID REFERENCES pos_accounts(id) ON DELETE CASCADE,
webhook_event_id VARCHAR(255), -- POS webhook event ID
event_type VARCHAR(100) NOT NULL, -- payment.created, order.updated, etc.
event_data JSONB NOT NULL,
received_at TIMESTAMP DEFAULT NOW(),
processed_at TIMESTAMP,
processing_status VARCHAR(50) DEFAULT 'pending', -- pending, processed, failed
error_message TEXT,
retry_count INTEGER DEFAULT 0
);
```
### Indexes for Performance
```sql
CREATE INDEX idx_pos_accounts_tenant ON pos_accounts(tenant_id, status);
CREATE INDEX idx_pos_transactions_tenant_date ON pos_transactions(tenant_id, transaction_date DESC);
CREATE INDEX idx_pos_transactions_account ON pos_transactions(pos_account_id);
CREATE INDEX idx_pos_transactions_synced ON pos_transactions(tenant_id, synced_to_sales) WHERE synced_to_sales = FALSE;
CREATE INDEX idx_pos_transaction_items_transaction ON pos_transaction_items(pos_transaction_id);
CREATE INDEX idx_pos_transaction_items_unmapped ON pos_transaction_items(tenant_id, is_mapped) WHERE is_mapped = FALSE;
CREATE INDEX idx_pos_mappings_account ON pos_product_mappings(pos_account_id);
CREATE INDEX idx_pos_sync_logs_account_date ON pos_sync_logs(pos_account_id, sync_started_at DESC);
```
## Business Logic Examples
### Square Transaction Sync
```python
async def sync_square_transactions(pos_account_id: UUID, start_date: datetime = None) -> dict:
"""
Sync transactions from Square POS.
"""
# Get POS account
pos_account = await get_pos_account(pos_account_id)
if pos_account.pos_provider != 'square':
raise ValueError("Not a Square account")
# Check token expiration
if pos_account.token_expires_at and pos_account.token_expires_at < datetime.utcnow():
await refresh_square_oauth_token(pos_account)
# Create sync log
sync_log = POSSyncLog(
tenant_id=pos_account.tenant_id,
pos_account_id=pos_account.id,
sync_started_at=datetime.utcnow(),
status='in_progress'
)
db.add(sync_log)
await db.flush()
try:
# Default to last sync time or 24 hours ago
if not start_date:
start_date = pos_account.last_successful_sync_at or (datetime.utcnow() - timedelta(days=1))
# Call Square API
async with httpx.AsyncClient() as client:
response = await client.post(
f"https://connect.squareup.com/v2/payments/list",
headers={
"Authorization": f"Bearer {pos_account.access_token}",
"Content-Type": "application/json"
},
json={
"location_id": pos_account.location_id,
"begin_time": start_date.isoformat(),
"end_time": datetime.utcnow().isoformat(),
"limit": 100
}
)
if response.status_code != 200:
raise Exception(f"Square API error: {response.text}")
data = response.json()
payments = data.get('payments', [])
transactions_processed = 0
transactions_failed = 0
for payment in payments:
try:
# Check for duplicate
existing = await db.query(POSTransaction).filter(
POSTransaction.tenant_id == pos_account.tenant_id,
POSTransaction.pos_transaction_id == payment['id']
).first()
if existing:
continue # Skip duplicates
# Create transaction
transaction = POSTransaction(
tenant_id=pos_account.tenant_id,
pos_account_id=pos_account.id,
pos_transaction_id=payment['id'],
pos_provider='square',
transaction_date=datetime.fromisoformat(payment['created_at'].replace('Z', '+00:00')),
transaction_type='sale' if payment['status'] == 'COMPLETED' else 'pending',
status=payment['status'].lower(),
total_amount=Decimal(payment['amount_money']['amount']) / 100,
currency=payment['amount_money']['currency'],
payment_method=payment.get('card_details', {}).get('card', {}).get('card_brand', 'unknown').lower(),
card_last_four=payment.get('card_details', {}).get('card', {}).get('last_4'),
receipt_number=payment.get('receipt_number')
)
db.add(transaction)
await db.flush()
# Get line items from order
if 'order_id' in payment:
order_response = await client.get(
f"https://connect.squareup.com/v2/orders/{payment['order_id']}",
headers={"Authorization": f"Bearer {pos_account.access_token}"}
)
if order_response.status_code == 200:
order = order_response.json().get('order', {})
line_items = order.get('line_items', [])
for item in line_items:
# Create transaction item
pos_item = POSTransactionItem(
tenant_id=pos_account.tenant_id,
pos_transaction_id=transaction.id,
pos_item_id=item.get('catalog_object_id'),
product_name=item['name'],
quantity=Decimal(item['quantity']),
unit_price=Decimal(item['base_price_money']['amount']) / 100,
line_total=Decimal(item['total_money']['amount']) / 100
)
# Check for mapping
mapping = await get_product_mapping(
pos_account.id,
item.get('catalog_object_id')
)
if mapping:
pos_item.mapped_product_id = mapping.bakery_product_id
pos_item.is_mapped = True
db.add(pos_item)
# Sync to sales service
await sync_transaction_to_sales(transaction)
transactions_processed += 1
except Exception as e:
logger.error("Failed to process Square payment",
payment_id=payment.get('id'),
error=str(e))
transactions_failed += 1
continue
# Update sync log
sync_log.sync_completed_at = datetime.utcnow()
sync_log.sync_duration_seconds = int((sync_log.sync_completed_at - sync_log.sync_started_at).total_seconds())
sync_log.status = 'success' if transactions_failed == 0 else 'partial'
sync_log.transactions_fetched = len(payments)
sync_log.transactions_processed = transactions_processed
sync_log.transactions_failed = transactions_failed
# Update pos account
pos_account.last_sync_at = datetime.utcnow()
pos_account.last_successful_sync_at = datetime.utcnow()
pos_account.error_count = 0
await db.commit()
# Publish sync completed event
await publish_event('pos', 'pos.sync_completed', {
'tenant_id': str(pos_account.tenant_id),
'pos_account_id': str(pos_account.id),
'transactions_processed': transactions_processed,
'transactions_failed': transactions_failed
})
return {
'status': 'success',
'transactions_processed': transactions_processed,
'transactions_failed': transactions_failed
}
except Exception as e:
sync_log.status = 'failed'
sync_log.error_message = str(e)
sync_log.sync_completed_at = datetime.utcnow()
pos_account.error_count += 1
pos_account.last_error_at = datetime.utcnow()
pos_account.error_message = str(e)
await db.commit()
logger.error("Square sync failed",
pos_account_id=str(pos_account_id),
error=str(e))
raise
```
### Auto Product Mapping
```python
async def auto_map_products(pos_account_id: UUID) -> dict:
"""
Automatically map POS products to Bakery-IA products using name/SKU matching.
"""
# Get unmapped transaction items
unmapped_items = await db.query(POSTransactionItem).filter(
POSTransactionItem.pos_account_id == pos_account_id,
POSTransactionItem.is_mapped == False
).all()
# Get unique products
unique_products = {}
for item in unmapped_items:
key = (item.pos_item_id, item.product_name, item.product_sku)
if key not in unique_products:
unique_products[key] = item
# Get all Bakery-IA products
bakery_products = await get_all_products(pos_account.tenant_id)
mapped_count = 0
high_confidence_count = 0
for (pos_id, pos_name, pos_sku), item in unique_products.items():
best_match = None
confidence = 0.0
# Try SKU match first (highest confidence)
if pos_sku:
for product in bakery_products:
if product.sku and product.sku.upper() == pos_sku.upper():
best_match = product
confidence = 1.0
break
# Try name match (fuzzy matching)
if not best_match:
from difflib import SequenceMatcher
for product in bakery_products:
# Calculate similarity ratio
ratio = SequenceMatcher(None, pos_name.lower(), product.name.lower()).ratio()
if ratio > confidence and ratio > 0.80: # 80% similarity threshold
best_match = product
confidence = ratio
# Create mapping if confidence is high enough
if best_match and confidence >= 0.80:
mapping = POSProductMapping(
tenant_id=pos_account.tenant_id,
pos_account_id=pos_account_id,
pos_product_id=pos_id,
pos_product_name=pos_name,
pos_product_sku=pos_sku,
bakery_product_id=best_match.id,
bakery_product_name=best_match.name,
mapping_type='auto',
confidence_score=Decimal(str(round(confidence, 2)))
)
db.add(mapping)
# Update all unmapped items with this product
await db.query(POSTransactionItem).filter(
POSTransactionItem.pos_account_id == pos_account_id,
POSTransactionItem.pos_item_id == pos_id,
POSTransactionItem.is_mapped == False
).update({
'mapped_product_id': best_match.id,
'is_mapped': True
})
mapped_count += 1
if confidence >= 0.95:
high_confidence_count += 1
await db.commit()
return {
'total_unmapped_products': len(unique_products),
'products_mapped': mapped_count,
'high_confidence_mappings': high_confidence_count,
'remaining_unmapped': len(unique_products) - mapped_count
}
```
### Webhook Handler
```python
async def handle_square_webhook(request: Request) -> dict:
"""
Handle incoming webhook from Square.
"""
# Verify webhook signature
signature = request.headers.get('X-Square-Signature')
body = await request.body()
# Signature verification (simplified)
# In production, use proper HMAC verification with webhook signature key
# Parse webhook payload
payload = await request.json()
event_type = payload.get('type')
merchant_id = payload.get('merchant_id')
# Find POS account
pos_account = await db.query(POSAccount).filter(
POSAccount.pos_provider == 'square',
POSAccount.merchant_id == merchant_id,
POSAccount.status == 'active'
).first()
if not pos_account:
logger.warning("Webhook received for unknown merchant", merchant_id=merchant_id)
return {'status': 'ignored', 'reason': 'unknown_merchant'}
# Store webhook for processing
webhook = POSWebhook(
tenant_id=pos_account.tenant_id,
pos_account_id=pos_account.id,
webhook_event_id=payload.get('event_id'),
event_type=event_type,
event_data=payload,
processing_status='pending'
)
db.add(webhook)
await db.commit()
# Process webhook asynchronously
# (In production, use background task queue)
try:
if event_type == 'payment.created':
# Sync this specific payment
payment_id = payload.get('data', {}).get('id')
await sync_specific_square_payment(pos_account, payment_id)
webhook.processing_status = 'processed'
webhook.processed_at = datetime.utcnow()
except Exception as e:
webhook.processing_status = 'failed'
webhook.error_message = str(e)
logger.error("Webhook processing failed",
webhook_id=str(webhook.id),
error=str(e))
await db.commit()
return {'status': 'received'}
```
## Events & Messaging
### Published Events (RabbitMQ)
**Exchange**: `pos`
**Routing Keys**: `pos.sync_completed`, `pos.mapping_needed`, `pos.error`
**POS Sync Completed Event**
```json
{
"event_type": "pos_sync_completed",
"tenant_id": "uuid",
"pos_account_id": "uuid",
"pos_provider": "square",
"location_name": "VUE Madrid - Centro",
"transactions_processed": 45,
"transactions_failed": 0,
"new_products_discovered": 3,
"sync_duration_seconds": 12,
"timestamp": "2025-11-06T10:30:00Z"
}
```
**POS Mapping Needed Alert**
```json
{
"event_type": "pos_mapping_needed",
"tenant_id": "uuid",
"pos_account_id": "uuid",
"unmapped_products_count": 5,
"unmapped_revenue_euros": 125.50,
"sample_unmapped_products": [
{"pos_product_name": "Croissant Especial", "transaction_count": 12},
{"pos_product_name": "Pan Integral Grande", "transaction_count": 8}
],
"timestamp": "2025-11-06T14:00:00Z"
}
```
**POS Error Alert**
```json
{
"event_type": "pos_error",
"tenant_id": "uuid",
"pos_account_id": "uuid",
"pos_provider": "square",
"error_type": "authentication_failed",
"error_message": "OAuth token expired",
"consecutive_failures": 3,
"action_required": "Reconnect POS account",
"timestamp": "2025-11-06T11:30:00Z"
}
```
### Consumed Events
- **From Sales**: Sales data validation triggers re-sync if discrepancies found
- **From Orchestrator**: Daily sync triggers for all active POS accounts
## Custom Metrics (Prometheus)
```python
# POS metrics
pos_accounts_total = Gauge(
'pos_accounts_total',
'Total connected POS accounts',
['tenant_id', 'pos_provider', 'status']
)
pos_transactions_synced_total = Counter(
'pos_transactions_synced_total',
'Total transactions synced from POS',
['tenant_id', 'pos_provider']
)
pos_sync_duration_seconds = Histogram(
'pos_sync_duration_seconds',
'POS sync duration',
['tenant_id', 'pos_provider'],
buckets=[5, 10, 30, 60, 120, 300]
)
pos_sync_errors_total = Counter(
'pos_sync_errors_total',
'Total POS sync errors',
['tenant_id', 'pos_provider', 'error_type']
)
pos_unmapped_products_total = Gauge(
'pos_unmapped_products_total',
'Products without mapping',
['tenant_id', 'pos_account_id']
)
```
## Configuration
### Environment Variables
**Service Configuration:**
- `PORT` - Service port (default: 8013)
- `DATABASE_URL` - PostgreSQL connection string
- `REDIS_URL` - Redis connection string
- `RABBITMQ_URL` - RabbitMQ connection string
**POS Provider Configuration:**
- `SQUARE_APP_ID` - Square application ID
- `SQUARE_APP_SECRET` - Square application secret
- `TOAST_CLIENT_ID` - Toast client ID
- `TOAST_CLIENT_SECRET` - Toast client secret
- `LIGHTSPEED_CLIENT_ID` - Lightspeed client ID
- `LIGHTSPEED_CLIENT_SECRET` - Lightspeed client secret
**Sync Configuration:**
- `DEFAULT_SYNC_FREQUENCY_MINUTES` - Default sync interval (default: 15)
- `ENABLE_WEBHOOKS` - Use webhooks for real-time sync (default: true)
- `MAX_SYNC_RETRIES` - Max retry attempts (default: 3)
- `HISTORICAL_IMPORT_DAYS` - Days to import on initial setup (default: 90)
**Mapping Configuration:**
- `AUTO_MAPPING_ENABLED` - Enable automatic product mapping (default: true)
- `AUTO_MAPPING_CONFIDENCE_THRESHOLD` - Minimum confidence (default: 0.80)
- `ALERT_ON_UNMAPPED_PRODUCTS` - Alert for unmapped products (default: true)
## Development Setup
### Prerequisites
- Python 3.11+
- PostgreSQL 17
- Redis 7.4
- RabbitMQ 4.1
- POS system developer accounts (Square, Toast, Lightspeed)
### Local Development
```bash
cd services/pos
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
export DATABASE_URL=postgresql://user:pass@localhost:5432/pos
export REDIS_URL=redis://localhost:6379/0
export RABBITMQ_URL=amqp://guest:guest@localhost:5672/
export SQUARE_APP_ID=your_square_app_id
export SQUARE_APP_SECRET=your_square_app_secret
alembic upgrade head
python main.py
```
## Integration Points
### Dependencies
- **POS Providers** - Square, Toast, Lightspeed APIs
- **Auth Service** - User authentication
- **PostgreSQL** - Transaction and mapping data
- **Redis** - Deduplication cache
- **RabbitMQ** - Event publishing
### Dependents
- **Sales Service** - Receives synced transaction data
- **Forecasting Service** - Uses sales data for ML models
- **Inventory Service** - Stock deduction from sales
- **Notification Service** - Sync error alerts
- **Frontend Dashboard** - POS connection and mapping UI
## Business Value for VUE Madrid
### Problem Statement
Spanish bakeries struggle with:
- Hours of daily manual sales data entry
- Transcription errors reducing forecast accuracy
- Delayed visibility into sales performance
- No integration between POS and business intelligence
- Double data entry (POS + spreadsheets/accounting)
### Solution
Bakery-IA POS Service provides:
- **Zero Manual Entry**: Automatic transaction sync from POS
- **Real-Time Data**: Sales data available within seconds
- **Higher Accuracy**: 99.9%+ vs. 85-95% manual entry
- **Immediate Value**: Works from day one, no setup needed
- **Universal Compatibility**: Works with popular POS systems
### Quantifiable Impact
**Time Savings:**
- 5-8 hours/week eliminating manual data entry
- 1-2 hours/week on sales reconciliation
- **Total: 6-10 hours/week saved**
**Data Quality:**
- 99.9%+ accuracy vs. 85-95% manual entry
- Zero transcription errors
- Real-time vs. end-of-day data availability
- 10-20% forecast accuracy improvement
**Operational Efficiency:**
- 15-minute setup vs. hours of daily manual entry
- Automatic sync every 15 minutes
- Multi-location support in single dashboard
- Instant error detection and alerts
### Target Market Fit (Spanish Bakeries)
- **POS Adoption**: Growing use of Square, Toast, Lightspeed in Spain
- **Labor Costs**: Spanish minimum wage makes manual entry expensive
- **Modernization**: New generation of bakery owners embrace technology
- **Market Trend**: Digital transformation in retail/food service
### ROI Calculation
**Investment**: €0 additional (included in platform subscription)
**Time Savings Value**: 6-10 hours/week × €15/hour = €360-600/month
**Forecast Improvement Value**: 10-20% better accuracy = €100-400/month
**Total Monthly Value**: €460-1,000
**Annual ROI**: €5,520-12,000 value per bakery
**Payback**: Immediate (included in subscription)
### Competitive Advantage
- **First-Mover**: Few Spanish bakery platforms offer POS integration
- **Multi-POS Support**: Flexibility for customers to choose POS
- **Plug-and-Play**: 15-minute setup vs. competitors requiring IT setup
- **Real-Time**: Webhook support for instant sync vs. batch processing
---
**Copyright © 2025 Bakery-IA. All rights reserved.**