584 lines
22 KiB
Python
584 lines
22 KiB
Python
|
|
"""
|
||
|
|
POS Event Consumer
|
||
|
|
Processes POS webhook events from RabbitMQ queue
|
||
|
|
Handles sales transactions, refunds, and inventory updates from various POS systems
|
||
|
|
"""
|
||
|
|
import json
|
||
|
|
import structlog
|
||
|
|
from typing import Dict, Any, Optional
|
||
|
|
from datetime import datetime
|
||
|
|
from decimal import Decimal
|
||
|
|
|
||
|
|
from shared.messaging import RabbitMQClient
|
||
|
|
from app.services.webhook_service import WebhookService
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
|
||
|
|
logger = structlog.get_logger()
|
||
|
|
|
||
|
|
|
||
|
|
class POSEventConsumer:
|
||
|
|
"""
|
||
|
|
Consumes POS webhook events from RabbitMQ and processes them
|
||
|
|
Supports multiple POS systems: Square, Shopify, Toast, etc.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, db_session: AsyncSession):
|
||
|
|
self.db_session = db_session
|
||
|
|
self.webhook_service = WebhookService()
|
||
|
|
|
||
|
|
async def consume_pos_events(
|
||
|
|
self,
|
||
|
|
rabbitmq_client: RabbitMQClient
|
||
|
|
):
|
||
|
|
"""
|
||
|
|
Start consuming POS events from RabbitMQ
|
||
|
|
"""
|
||
|
|
async def process_message(message):
|
||
|
|
"""Process a single POS event message"""
|
||
|
|
try:
|
||
|
|
async with message.process():
|
||
|
|
# Parse event data
|
||
|
|
event_data = json.loads(message.body.decode())
|
||
|
|
logger.info(
|
||
|
|
"Received POS event",
|
||
|
|
event_id=event_data.get('event_id'),
|
||
|
|
event_type=event_data.get('event_type'),
|
||
|
|
pos_system=event_data.get('data', {}).get('pos_system')
|
||
|
|
)
|
||
|
|
|
||
|
|
# Process the event
|
||
|
|
await self.process_pos_event(event_data)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error processing POS event",
|
||
|
|
error=str(e),
|
||
|
|
exc_info=True
|
||
|
|
)
|
||
|
|
|
||
|
|
# Start consuming events
|
||
|
|
await rabbitmq_client.consume_events(
|
||
|
|
exchange_name="pos.events",
|
||
|
|
queue_name="pos.processing.queue",
|
||
|
|
routing_key="pos.*",
|
||
|
|
callback=process_message
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info("Started consuming POS events")
|
||
|
|
|
||
|
|
async def process_pos_event(self, event_data: Dict[str, Any]) -> bool:
|
||
|
|
"""
|
||
|
|
Process a POS event based on type
|
||
|
|
|
||
|
|
Args:
|
||
|
|
event_data: Full event payload from RabbitMQ
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if processed successfully
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
data = event_data.get('data', {})
|
||
|
|
webhook_log_id = data.get('webhook_log_id')
|
||
|
|
pos_system = data.get('pos_system', 'unknown')
|
||
|
|
webhook_type = data.get('webhook_type')
|
||
|
|
payload = data.get('payload', {})
|
||
|
|
tenant_id = event_data.get('tenant_id')
|
||
|
|
|
||
|
|
if not webhook_log_id:
|
||
|
|
logger.warning("POS event missing webhook_log_id", event_data=event_data)
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Update webhook log status to processing
|
||
|
|
await self.webhook_service.update_webhook_status(
|
||
|
|
webhook_log_id,
|
||
|
|
status="processing",
|
||
|
|
notes="Event consumer processing"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Route to appropriate handler based on webhook type
|
||
|
|
success = False
|
||
|
|
if webhook_type in ['sale.completed', 'transaction.completed', 'order.completed']:
|
||
|
|
success = await self._handle_sale_completed(tenant_id, pos_system, payload)
|
||
|
|
elif webhook_type in ['sale.refunded', 'transaction.refunded', 'order.refunded']:
|
||
|
|
success = await self._handle_sale_refunded(tenant_id, pos_system, payload)
|
||
|
|
elif webhook_type in ['inventory.updated', 'stock.updated']:
|
||
|
|
success = await self._handle_inventory_updated(tenant_id, pos_system, payload)
|
||
|
|
else:
|
||
|
|
logger.warning("Unknown POS webhook type", webhook_type=webhook_type)
|
||
|
|
success = True # Mark as processed to avoid retry
|
||
|
|
|
||
|
|
# Update webhook log with final status
|
||
|
|
if success:
|
||
|
|
await self.webhook_service.update_webhook_status(
|
||
|
|
webhook_log_id,
|
||
|
|
status="completed",
|
||
|
|
notes="Successfully processed"
|
||
|
|
)
|
||
|
|
logger.info(
|
||
|
|
"POS event processed successfully",
|
||
|
|
webhook_log_id=webhook_log_id,
|
||
|
|
webhook_type=webhook_type
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
await self.webhook_service.update_webhook_status(
|
||
|
|
webhook_log_id,
|
||
|
|
status="failed",
|
||
|
|
notes="Processing failed"
|
||
|
|
)
|
||
|
|
logger.error(
|
||
|
|
"POS event processing failed",
|
||
|
|
webhook_log_id=webhook_log_id,
|
||
|
|
webhook_type=webhook_type
|
||
|
|
)
|
||
|
|
|
||
|
|
return success
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error in process_pos_event",
|
||
|
|
error=str(e),
|
||
|
|
event_id=event_data.get('event_id'),
|
||
|
|
exc_info=True
|
||
|
|
)
|
||
|
|
return False
|
||
|
|
|
||
|
|
async def _handle_sale_completed(
|
||
|
|
self,
|
||
|
|
tenant_id: str,
|
||
|
|
pos_system: str,
|
||
|
|
payload: Dict[str, Any]
|
||
|
|
) -> bool:
|
||
|
|
"""
|
||
|
|
Handle completed sale transaction
|
||
|
|
|
||
|
|
Updates:
|
||
|
|
- Inventory quantities (decrease stock)
|
||
|
|
- Sales analytics data
|
||
|
|
- Revenue tracking
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tenant_id: Tenant ID
|
||
|
|
pos_system: POS system name (square, shopify, toast, etc.)
|
||
|
|
payload: Sale data from POS system
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if handled successfully
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# Extract transaction data based on POS system format
|
||
|
|
transaction_data = self._parse_sale_data(pos_system, payload)
|
||
|
|
|
||
|
|
if not transaction_data:
|
||
|
|
logger.warning("Failed to parse sale data", pos_system=pos_system)
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Update inventory via inventory service client
|
||
|
|
from shared.clients.inventory_client import InventoryServiceClient
|
||
|
|
from shared.config.base import get_settings
|
||
|
|
|
||
|
|
config = get_settings()
|
||
|
|
inventory_client = InventoryServiceClient(config, "pos")
|
||
|
|
|
||
|
|
for item in transaction_data.get('items', []):
|
||
|
|
product_id = item.get('product_id')
|
||
|
|
quantity = item.get('quantity', 0)
|
||
|
|
unit_of_measure = item.get('unit_of_measure', 'units')
|
||
|
|
|
||
|
|
if not product_id or quantity <= 0:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Decrease inventory stock
|
||
|
|
try:
|
||
|
|
await inventory_client.adjust_stock(
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
product_id=product_id,
|
||
|
|
quantity=-quantity, # Negative for sale
|
||
|
|
unit_of_measure=unit_of_measure,
|
||
|
|
reason=f"POS sale - {pos_system}",
|
||
|
|
reference_id=transaction_data.get('transaction_id')
|
||
|
|
)
|
||
|
|
logger.info(
|
||
|
|
"Inventory updated for sale",
|
||
|
|
product_id=product_id,
|
||
|
|
quantity=quantity,
|
||
|
|
pos_system=pos_system
|
||
|
|
)
|
||
|
|
except Exception as inv_error:
|
||
|
|
logger.error(
|
||
|
|
"Failed to update inventory",
|
||
|
|
product_id=product_id,
|
||
|
|
error=str(inv_error)
|
||
|
|
)
|
||
|
|
# Continue processing other items even if one fails
|
||
|
|
|
||
|
|
# Publish sales data to sales service via RabbitMQ
|
||
|
|
from shared.messaging import get_rabbitmq_client
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
rabbitmq_client = get_rabbitmq_client()
|
||
|
|
if rabbitmq_client:
|
||
|
|
sales_event = {
|
||
|
|
"event_id": str(uuid.uuid4()),
|
||
|
|
"event_type": "sales.transaction.completed",
|
||
|
|
"timestamp": datetime.utcnow().isoformat(),
|
||
|
|
"tenant_id": tenant_id,
|
||
|
|
"data": {
|
||
|
|
"transaction_id": transaction_data.get('transaction_id'),
|
||
|
|
"pos_system": pos_system,
|
||
|
|
"total_amount": transaction_data.get('total_amount', 0),
|
||
|
|
"items": transaction_data.get('items', []),
|
||
|
|
"payment_method": transaction_data.get('payment_method'),
|
||
|
|
"transaction_date": transaction_data.get('transaction_date'),
|
||
|
|
"customer_id": transaction_data.get('customer_id')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
await rabbitmq_client.publish_event(
|
||
|
|
exchange_name="sales.events",
|
||
|
|
routing_key="sales.transaction.completed",
|
||
|
|
event_data=sales_event
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Published sales event",
|
||
|
|
event_id=sales_event["event_id"],
|
||
|
|
transaction_id=transaction_data.get('transaction_id')
|
||
|
|
)
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error handling sale completed",
|
||
|
|
error=str(e),
|
||
|
|
pos_system=pos_system,
|
||
|
|
exc_info=True
|
||
|
|
)
|
||
|
|
return False
|
||
|
|
|
||
|
|
async def _handle_sale_refunded(
|
||
|
|
self,
|
||
|
|
tenant_id: str,
|
||
|
|
pos_system: str,
|
||
|
|
payload: Dict[str, Any]
|
||
|
|
) -> bool:
|
||
|
|
"""
|
||
|
|
Handle refunded sale transaction
|
||
|
|
|
||
|
|
Updates:
|
||
|
|
- Inventory quantities (increase stock)
|
||
|
|
- Sales analytics (negative transaction)
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tenant_id: Tenant ID
|
||
|
|
pos_system: POS system name
|
||
|
|
payload: Refund data from POS system
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if handled successfully
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# Extract refund data based on POS system format
|
||
|
|
refund_data = self._parse_refund_data(pos_system, payload)
|
||
|
|
|
||
|
|
if not refund_data:
|
||
|
|
logger.warning("Failed to parse refund data", pos_system=pos_system)
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Update inventory via inventory service client
|
||
|
|
from shared.clients.inventory_client import InventoryServiceClient
|
||
|
|
from shared.config.base import get_settings
|
||
|
|
|
||
|
|
config = get_settings()
|
||
|
|
inventory_client = InventoryServiceClient(config, "pos")
|
||
|
|
|
||
|
|
for item in refund_data.get('items', []):
|
||
|
|
product_id = item.get('product_id')
|
||
|
|
quantity = item.get('quantity', 0)
|
||
|
|
unit_of_measure = item.get('unit_of_measure', 'units')
|
||
|
|
|
||
|
|
if not product_id or quantity <= 0:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Increase inventory stock (return to stock)
|
||
|
|
try:
|
||
|
|
await inventory_client.adjust_stock(
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
product_id=product_id,
|
||
|
|
quantity=quantity, # Positive for refund
|
||
|
|
unit_of_measure=unit_of_measure,
|
||
|
|
reason=f"POS refund - {pos_system}",
|
||
|
|
reference_id=refund_data.get('refund_id')
|
||
|
|
)
|
||
|
|
logger.info(
|
||
|
|
"Inventory updated for refund",
|
||
|
|
product_id=product_id,
|
||
|
|
quantity=quantity,
|
||
|
|
pos_system=pos_system
|
||
|
|
)
|
||
|
|
except Exception as inv_error:
|
||
|
|
logger.error(
|
||
|
|
"Failed to update inventory for refund",
|
||
|
|
product_id=product_id,
|
||
|
|
error=str(inv_error)
|
||
|
|
)
|
||
|
|
|
||
|
|
# Publish refund event to sales service
|
||
|
|
from shared.messaging import get_rabbitmq_client
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
rabbitmq_client = get_rabbitmq_client()
|
||
|
|
if rabbitmq_client:
|
||
|
|
refund_event = {
|
||
|
|
"event_id": str(uuid.uuid4()),
|
||
|
|
"event_type": "sales.transaction.refunded",
|
||
|
|
"timestamp": datetime.utcnow().isoformat(),
|
||
|
|
"tenant_id": tenant_id,
|
||
|
|
"data": {
|
||
|
|
"refund_id": refund_data.get('refund_id'),
|
||
|
|
"original_transaction_id": refund_data.get('original_transaction_id'),
|
||
|
|
"pos_system": pos_system,
|
||
|
|
"refund_amount": refund_data.get('refund_amount', 0),
|
||
|
|
"items": refund_data.get('items', []),
|
||
|
|
"refund_date": refund_data.get('refund_date')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
await rabbitmq_client.publish_event(
|
||
|
|
exchange_name="sales.events",
|
||
|
|
routing_key="sales.transaction.refunded",
|
||
|
|
event_data=refund_event
|
||
|
|
)
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"Published refund event",
|
||
|
|
event_id=refund_event["event_id"],
|
||
|
|
refund_id=refund_data.get('refund_id')
|
||
|
|
)
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error handling sale refunded",
|
||
|
|
error=str(e),
|
||
|
|
pos_system=pos_system,
|
||
|
|
exc_info=True
|
||
|
|
)
|
||
|
|
return False
|
||
|
|
|
||
|
|
async def _handle_inventory_updated(
|
||
|
|
self,
|
||
|
|
tenant_id: str,
|
||
|
|
pos_system: str,
|
||
|
|
payload: Dict[str, Any]
|
||
|
|
) -> bool:
|
||
|
|
"""
|
||
|
|
Handle inventory update from POS system
|
||
|
|
|
||
|
|
Syncs inventory levels from POS to our system
|
||
|
|
|
||
|
|
Args:
|
||
|
|
tenant_id: Tenant ID
|
||
|
|
pos_system: POS system name
|
||
|
|
payload: Inventory data from POS system
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
bool: True if handled successfully
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# Extract inventory data
|
||
|
|
inventory_data = self._parse_inventory_data(pos_system, payload)
|
||
|
|
|
||
|
|
if not inventory_data:
|
||
|
|
logger.warning("Failed to parse inventory data", pos_system=pos_system)
|
||
|
|
return False
|
||
|
|
|
||
|
|
# Update inventory via inventory service client
|
||
|
|
from shared.clients.inventory_client import InventoryServiceClient
|
||
|
|
from shared.config.base import get_settings
|
||
|
|
|
||
|
|
config = get_settings()
|
||
|
|
inventory_client = InventoryServiceClient(config, "pos")
|
||
|
|
|
||
|
|
for item in inventory_data.get('items', []):
|
||
|
|
product_id = item.get('product_id')
|
||
|
|
new_quantity = item.get('quantity', 0)
|
||
|
|
|
||
|
|
if not product_id:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Sync inventory level
|
||
|
|
try:
|
||
|
|
await inventory_client.sync_stock_level(
|
||
|
|
tenant_id=tenant_id,
|
||
|
|
product_id=product_id,
|
||
|
|
quantity=new_quantity,
|
||
|
|
source=f"POS sync - {pos_system}"
|
||
|
|
)
|
||
|
|
logger.info(
|
||
|
|
"Inventory synced from POS",
|
||
|
|
product_id=product_id,
|
||
|
|
new_quantity=new_quantity,
|
||
|
|
pos_system=pos_system
|
||
|
|
)
|
||
|
|
except Exception as inv_error:
|
||
|
|
logger.error(
|
||
|
|
"Failed to sync inventory",
|
||
|
|
product_id=product_id,
|
||
|
|
error=str(inv_error)
|
||
|
|
)
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(
|
||
|
|
"Error handling inventory updated",
|
||
|
|
error=str(e),
|
||
|
|
pos_system=pos_system,
|
||
|
|
exc_info=True
|
||
|
|
)
|
||
|
|
return False
|
||
|
|
|
||
|
|
def _parse_sale_data(self, pos_system: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||
|
|
"""
|
||
|
|
Parse sale data from various POS system formats
|
||
|
|
|
||
|
|
Args:
|
||
|
|
pos_system: POS system name
|
||
|
|
payload: Raw payload from POS webhook
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Normalized transaction data
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
if pos_system.lower() == 'square':
|
||
|
|
return self._parse_square_sale(payload)
|
||
|
|
elif pos_system.lower() == 'shopify':
|
||
|
|
return self._parse_shopify_sale(payload)
|
||
|
|
elif pos_system.lower() == 'toast':
|
||
|
|
return self._parse_toast_sale(payload)
|
||
|
|
else:
|
||
|
|
# Generic parser for custom POS systems
|
||
|
|
return self._parse_generic_sale(payload)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error("Error parsing sale data", pos_system=pos_system, error=str(e))
|
||
|
|
return None
|
||
|
|
|
||
|
|
def _parse_square_sale(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
"""Parse Square POS sale format"""
|
||
|
|
payment = payload.get('payment', {})
|
||
|
|
order = payment.get('order', {})
|
||
|
|
line_items = order.get('line_items', [])
|
||
|
|
|
||
|
|
items = []
|
||
|
|
for item in line_items:
|
||
|
|
items.append({
|
||
|
|
'product_id': item.get('catalog_object_id'),
|
||
|
|
'product_name': item.get('name'),
|
||
|
|
'quantity': float(item.get('quantity', 1)),
|
||
|
|
'unit_price': float(item.get('base_price_money', {}).get('amount', 0)) / 100,
|
||
|
|
'unit_of_measure': 'units'
|
||
|
|
})
|
||
|
|
|
||
|
|
return {
|
||
|
|
'transaction_id': payment.get('id'),
|
||
|
|
'total_amount': float(payment.get('amount_money', {}).get('amount', 0)) / 100,
|
||
|
|
'items': items,
|
||
|
|
'payment_method': payment.get('card_details', {}).get('card', {}).get('card_brand', 'unknown'),
|
||
|
|
'transaction_date': payment.get('created_at'),
|
||
|
|
'customer_id': payment.get('customer_id')
|
||
|
|
}
|
||
|
|
|
||
|
|
def _parse_shopify_sale(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
"""Parse Shopify POS sale format"""
|
||
|
|
line_items = payload.get('line_items', [])
|
||
|
|
|
||
|
|
items = []
|
||
|
|
for item in line_items:
|
||
|
|
items.append({
|
||
|
|
'product_id': str(item.get('product_id')),
|
||
|
|
'product_name': item.get('title'),
|
||
|
|
'quantity': float(item.get('quantity', 1)),
|
||
|
|
'unit_price': float(item.get('price', 0)),
|
||
|
|
'unit_of_measure': 'units'
|
||
|
|
})
|
||
|
|
|
||
|
|
return {
|
||
|
|
'transaction_id': str(payload.get('id')),
|
||
|
|
'total_amount': float(payload.get('total_price', 0)),
|
||
|
|
'items': items,
|
||
|
|
'payment_method': payload.get('payment_gateway_names', ['unknown'])[0],
|
||
|
|
'transaction_date': payload.get('created_at'),
|
||
|
|
'customer_id': str(payload.get('customer', {}).get('id')) if payload.get('customer') else None
|
||
|
|
}
|
||
|
|
|
||
|
|
def _parse_toast_sale(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
"""Parse Toast POS sale format"""
|
||
|
|
selections = payload.get('selections', [])
|
||
|
|
|
||
|
|
items = []
|
||
|
|
for item in selections:
|
||
|
|
items.append({
|
||
|
|
'product_id': item.get('guid'),
|
||
|
|
'product_name': item.get('displayName'),
|
||
|
|
'quantity': float(item.get('quantity', 1)),
|
||
|
|
'unit_price': float(item.get('preDiscountPrice', 0)),
|
||
|
|
'unit_of_measure': 'units'
|
||
|
|
})
|
||
|
|
|
||
|
|
return {
|
||
|
|
'transaction_id': payload.get('guid'),
|
||
|
|
'total_amount': float(payload.get('totalAmount', 0)),
|
||
|
|
'items': items,
|
||
|
|
'payment_method': payload.get('payments', [{}])[0].get('type', 'unknown'),
|
||
|
|
'transaction_date': payload.get('closedDate'),
|
||
|
|
'customer_id': payload.get('customer', {}).get('guid')
|
||
|
|
}
|
||
|
|
|
||
|
|
def _parse_generic_sale(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||
|
|
"""Parse generic/custom POS sale format"""
|
||
|
|
items = []
|
||
|
|
for item in payload.get('items', []):
|
||
|
|
items.append({
|
||
|
|
'product_id': item.get('product_id') or item.get('id'),
|
||
|
|
'product_name': item.get('name') or item.get('description'),
|
||
|
|
'quantity': float(item.get('quantity', 1)),
|
||
|
|
'unit_price': float(item.get('price', 0)),
|
||
|
|
'unit_of_measure': item.get('unit_of_measure', 'units')
|
||
|
|
})
|
||
|
|
|
||
|
|
return {
|
||
|
|
'transaction_id': payload.get('transaction_id') or payload.get('id'),
|
||
|
|
'total_amount': float(payload.get('total', 0)),
|
||
|
|
'items': items,
|
||
|
|
'payment_method': payload.get('payment_method', 'unknown'),
|
||
|
|
'transaction_date': payload.get('timestamp') or payload.get('created_at'),
|
||
|
|
'customer_id': payload.get('customer_id')
|
||
|
|
}
|
||
|
|
|
||
|
|
def _parse_refund_data(self, pos_system: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||
|
|
"""Parse refund data from various POS systems"""
|
||
|
|
# Similar parsing logic as sales, but for refunds
|
||
|
|
# Simplified for now - would follow same pattern as _parse_sale_data
|
||
|
|
return {
|
||
|
|
'refund_id': payload.get('refund_id') or payload.get('id'),
|
||
|
|
'original_transaction_id': payload.get('original_transaction_id'),
|
||
|
|
'refund_amount': float(payload.get('amount', 0)),
|
||
|
|
'items': payload.get('items', []),
|
||
|
|
'refund_date': payload.get('refund_date') or payload.get('created_at')
|
||
|
|
}
|
||
|
|
|
||
|
|
def _parse_inventory_data(self, pos_system: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||
|
|
"""Parse inventory data from various POS systems"""
|
||
|
|
return {
|
||
|
|
'items': payload.get('items', [])
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# Factory function for creating consumer instance
|
||
|
|
def create_pos_event_consumer(db_session: AsyncSession) -> POSEventConsumer:
|
||
|
|
"""Create POS event consumer instance"""
|
||
|
|
return POSEventConsumer(db_session)
|