Improve the frontend and repository layer
This commit is contained in:
@@ -13,6 +13,8 @@ from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role, admin_role_required
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
from app.services.pos_config_service import POSConfigurationService
|
||||
from app.schemas.pos_config import POSConfigurationListResponse
|
||||
|
||||
router = APIRouter()
|
||||
logger = structlog.get_logger()
|
||||
@@ -22,23 +24,41 @@ route_builder = RouteBuilder('pos')
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("configurations"),
|
||||
response_model=dict
|
||||
response_model=POSConfigurationListResponse
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def list_pos_configurations(
|
||||
tenant_id: UUID = Path(...),
|
||||
pos_system: Optional[str] = Query(None),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=100),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""List all POS configurations for a tenant"""
|
||||
try:
|
||||
return {
|
||||
"configurations": [],
|
||||
"total": 0,
|
||||
"supported_systems": ["square", "toast", "lightspeed"]
|
||||
}
|
||||
service = POSConfigurationService()
|
||||
|
||||
configurations = await service.get_configurations_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
pos_system=pos_system,
|
||||
is_active=is_active,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
total = await service.count_configurations_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
pos_system=pos_system,
|
||||
is_active=is_active
|
||||
)
|
||||
|
||||
return POSConfigurationListResponse(
|
||||
configurations=configurations,
|
||||
total=total,
|
||||
supported_systems=["square", "toast", "lightspeed"]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Failed to list POS configurations", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list configurations: {str(e)}")
|
||||
|
||||
@@ -14,6 +14,8 @@ from app.core.database import get_db
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role, admin_role_required
|
||||
from shared.routing import RouteBuilder
|
||||
from app.services.pos_transaction_service import POSTransactionService
|
||||
from app.services.pos_config_service import POSConfigurationService
|
||||
|
||||
router = APIRouter()
|
||||
logger = structlog.get_logger()
|
||||
@@ -74,15 +76,33 @@ async def get_sync_status(
|
||||
):
|
||||
"""Get synchronization status and recent sync history"""
|
||||
try:
|
||||
transaction_service = POSTransactionService()
|
||||
|
||||
# Get sync metrics from transaction service
|
||||
sync_metrics = await transaction_service.get_sync_metrics(tenant_id)
|
||||
|
||||
# Get last successful sync time
|
||||
sync_status = sync_metrics["sync_status"]
|
||||
last_successful_sync = sync_status.get("last_sync_at")
|
||||
|
||||
# Calculate sync success rate
|
||||
total = sync_metrics["total_transactions"]
|
||||
synced = sync_status.get("synced", 0)
|
||||
success_rate = (synced / total * 100) if total > 0 else 100.0
|
||||
|
||||
return {
|
||||
"current_sync": None,
|
||||
"last_successful_sync": None,
|
||||
"recent_syncs": [],
|
||||
"last_successful_sync": last_successful_sync.isoformat() if last_successful_sync else None,
|
||||
"recent_syncs": [], # Could be enhanced with actual sync history
|
||||
"sync_health": {
|
||||
"status": "healthy",
|
||||
"success_rate": 95.5,
|
||||
"average_duration_minutes": 3.2,
|
||||
"last_error": None
|
||||
"status": "healthy" if success_rate > 90 else "degraded" if success_rate > 70 else "unhealthy",
|
||||
"success_rate": round(success_rate, 2),
|
||||
"average_duration_minutes": 3.2, # Placeholder - could calculate from actual data
|
||||
"last_error": None,
|
||||
"total_transactions": total,
|
||||
"synced_count": synced,
|
||||
"pending_count": sync_status.get("pending", 0),
|
||||
"failed_count": sync_status.get("failed", 0)
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -159,12 +179,35 @@ async def test_pos_connection(
|
||||
):
|
||||
"""Test connection to POS system (Admin/Owner only)"""
|
||||
try:
|
||||
config_service = POSConfigurationService()
|
||||
|
||||
# Get the configuration to verify it exists
|
||||
configurations = await config_service.get_configurations_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
skip=0,
|
||||
limit=100
|
||||
)
|
||||
|
||||
config = next((c for c in configurations if str(c.id) == str(config_id)), None)
|
||||
|
||||
if not config:
|
||||
raise HTTPException(status_code=404, detail="Configuration not found")
|
||||
|
||||
# For demo purposes, we assume connection is successful if config exists
|
||||
# In production, this would actually test the POS API connection
|
||||
is_connected = config.is_connected and config.is_active
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Connection test successful",
|
||||
"success": is_connected,
|
||||
"status": "success" if is_connected else "failed",
|
||||
"message": f"Connection test {'successful' if is_connected else 'failed'} for {config.pos_system}",
|
||||
"tested_at": datetime.utcnow().isoformat(),
|
||||
"config_id": str(config_id)
|
||||
"config_id": str(config_id),
|
||||
"pos_system": config.pos_system,
|
||||
"health_status": config.health_status
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to test POS connection", error=str(e),
|
||||
tenant_id=tenant_id, config_id=config_id)
|
||||
|
||||
@@ -4,15 +4,22 @@ ATOMIC layer - Basic CRUD operations for POS transactions
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query
|
||||
from typing import Optional, Dict, Any
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
import structlog
|
||||
|
||||
from app.core.database import get_db
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.auth.access_control import require_user_role
|
||||
from shared.routing import RouteBuilder
|
||||
from app.services.pos_transaction_service import POSTransactionService
|
||||
from app.schemas.pos_transaction import (
|
||||
POSTransactionResponse,
|
||||
POSTransactionListResponse,
|
||||
POSTransactionDashboardSummary
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
logger = structlog.get_logger()
|
||||
@@ -21,7 +28,7 @@ route_builder = RouteBuilder('pos')
|
||||
|
||||
@router.get(
|
||||
route_builder.build_base_route("transactions"),
|
||||
response_model=dict
|
||||
response_model=POSTransactionListResponse
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def list_pos_transactions(
|
||||
@@ -38,20 +45,46 @@ async def list_pos_transactions(
|
||||
):
|
||||
"""List POS transactions for a tenant"""
|
||||
try:
|
||||
return {
|
||||
"transactions": [],
|
||||
"total": 0,
|
||||
"has_more": False,
|
||||
"summary": {
|
||||
"total_amount": 0,
|
||||
"transaction_count": 0,
|
||||
"sync_status": {
|
||||
"synced": 0,
|
||||
"pending": 0,
|
||||
"failed": 0
|
||||
}
|
||||
service = POSTransactionService()
|
||||
|
||||
transactions = await service.get_transactions_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
pos_system=pos_system,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
status=status,
|
||||
is_synced=is_synced,
|
||||
skip=offset,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
total = await service.count_transactions_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
pos_system=pos_system,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
status=status,
|
||||
is_synced=is_synced
|
||||
)
|
||||
|
||||
# Get sync metrics for summary
|
||||
sync_metrics = await service.get_sync_metrics(tenant_id)
|
||||
|
||||
# Calculate summary
|
||||
total_amount = sum(float(t.total_amount) for t in transactions if t.status == "completed")
|
||||
|
||||
has_more = (offset + limit) < total
|
||||
|
||||
return POSTransactionListResponse(
|
||||
transactions=transactions,
|
||||
total=total,
|
||||
has_more=has_more,
|
||||
summary={
|
||||
"total_amount": total_amount,
|
||||
"transaction_count": len(transactions),
|
||||
"sync_status": sync_metrics["sync_status"]
|
||||
}
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Failed to list POS transactions", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to list transactions: {str(e)}")
|
||||
@@ -59,7 +92,7 @@ async def list_pos_transactions(
|
||||
|
||||
@router.get(
|
||||
route_builder.build_resource_detail_route("transactions", "transaction_id"),
|
||||
response_model=dict
|
||||
response_model=POSTransactionResponse
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def get_pos_transaction(
|
||||
@@ -70,13 +103,46 @@ async def get_pos_transaction(
|
||||
):
|
||||
"""Get a specific POS transaction"""
|
||||
try:
|
||||
return {
|
||||
"id": str(transaction_id),
|
||||
"tenant_id": str(tenant_id),
|
||||
"status": "completed",
|
||||
"is_synced": True
|
||||
}
|
||||
service = POSTransactionService()
|
||||
|
||||
transaction = await service.get_transaction_with_items(
|
||||
transaction_id=transaction_id,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
if not transaction:
|
||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||
|
||||
return transaction
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Failed to get POS transaction", error=str(e),
|
||||
tenant_id=tenant_id, transaction_id=transaction_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get transaction: {str(e)}")
|
||||
|
||||
|
||||
@router.get(
|
||||
route_builder.build_operations_route("transactions-dashboard"),
|
||||
response_model=POSTransactionDashboardSummary
|
||||
)
|
||||
@require_user_role(['viewer', 'member', 'admin', 'owner'])
|
||||
async def get_transactions_dashboard(
|
||||
tenant_id: UUID = Path(...),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db=Depends(get_db)
|
||||
):
|
||||
"""Get dashboard summary for POS transactions"""
|
||||
try:
|
||||
service = POSTransactionService()
|
||||
|
||||
summary = await service.get_dashboard_summary(tenant_id)
|
||||
|
||||
logger.info("Transactions dashboard retrieved",
|
||||
tenant_id=str(tenant_id),
|
||||
total_today=summary.total_transactions_today)
|
||||
|
||||
return summary
|
||||
except Exception as e:
|
||||
logger.error("Failed to get transactions dashboard", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get dashboard: {str(e)}")
|
||||
|
||||
82
services/pos/app/repositories/pos_config_repository.py
Normal file
82
services/pos/app/repositories/pos_config_repository.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
POS Configuration Repository using Repository Pattern
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from sqlalchemy import select, and_, or_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import structlog
|
||||
|
||||
from app.models.pos_config import POSConfiguration
|
||||
from shared.database.repository import BaseRepository
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class POSConfigurationRepository(BaseRepository[POSConfiguration, dict, dict]):
|
||||
"""Repository for POS configuration operations"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
super().__init__(POSConfiguration, session)
|
||||
|
||||
async def get_configurations_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
pos_system: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[POSConfiguration]:
|
||||
"""Get POS configurations for a specific tenant with optional filters"""
|
||||
try:
|
||||
query = select(self.model).where(self.model.tenant_id == tenant_id)
|
||||
|
||||
# Apply filters
|
||||
conditions = []
|
||||
if pos_system:
|
||||
conditions.append(self.model.pos_system == pos_system)
|
||||
if is_active is not None:
|
||||
conditions.append(self.model.is_active == is_active)
|
||||
|
||||
if conditions:
|
||||
query = query.where(and_(*conditions))
|
||||
|
||||
query = query.offset(skip).limit(limit).order_by(self.model.created_at.desc())
|
||||
|
||||
result = await self.session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get configurations by tenant", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def count_configurations_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
pos_system: Optional[str] = None,
|
||||
is_active: Optional[bool] = None
|
||||
) -> int:
|
||||
"""Count POS configurations for a specific tenant with optional filters"""
|
||||
try:
|
||||
from sqlalchemy import func
|
||||
|
||||
query = select(func.count(self.model.id)).where(self.model.tenant_id == tenant_id)
|
||||
|
||||
# Apply filters
|
||||
conditions = []
|
||||
if pos_system:
|
||||
conditions.append(self.model.pos_system == pos_system)
|
||||
if is_active is not None:
|
||||
conditions.append(self.model.is_active == is_active)
|
||||
|
||||
if conditions:
|
||||
query = query.where(and_(*conditions))
|
||||
|
||||
result = await self.session.execute(query)
|
||||
count = result.scalar() or 0
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to count configurations by tenant", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
113
services/pos/app/repositories/pos_transaction_item_repository.py
Normal file
113
services/pos/app/repositories/pos_transaction_item_repository.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
POS Transaction Item Repository using Repository Pattern
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import structlog
|
||||
|
||||
from app.models.pos_transaction import POSTransactionItem
|
||||
from shared.database.repository import BaseRepository
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class POSTransactionItemRepository(BaseRepository[POSTransactionItem, dict, dict]):
|
||||
"""Repository for POS transaction item operations"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
super().__init__(POSTransactionItem, session)
|
||||
|
||||
async def get_items_by_transaction(
|
||||
self,
|
||||
transaction_id: UUID
|
||||
) -> List[POSTransactionItem]:
|
||||
"""Get all items for a transaction"""
|
||||
try:
|
||||
query = select(POSTransactionItem).where(
|
||||
POSTransactionItem.transaction_id == transaction_id
|
||||
).order_by(POSTransactionItem.created_at)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get transaction items",
|
||||
transaction_id=str(transaction_id),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def get_items_by_product(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
product_name: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[POSTransactionItem]:
|
||||
"""Get all transaction items for a specific product"""
|
||||
try:
|
||||
query = select(POSTransactionItem).where(
|
||||
and_(
|
||||
POSTransactionItem.tenant_id == tenant_id,
|
||||
POSTransactionItem.product_name.ilike(f"%{product_name}%")
|
||||
)
|
||||
).order_by(POSTransactionItem.created_at.desc()).offset(skip).limit(limit)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get items by product",
|
||||
product_name=product_name,
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def get_items_by_sku(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
sku: str
|
||||
) -> List[POSTransactionItem]:
|
||||
"""Get all transaction items for a specific SKU"""
|
||||
try:
|
||||
query = select(POSTransactionItem).where(
|
||||
and_(
|
||||
POSTransactionItem.tenant_id == tenant_id,
|
||||
POSTransactionItem.sku == sku
|
||||
)
|
||||
).order_by(POSTransactionItem.created_at.desc())
|
||||
|
||||
result = await self.session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get items by SKU",
|
||||
sku=sku,
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def get_items_by_category(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
category: str,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[POSTransactionItem]:
|
||||
"""Get all transaction items for a specific category"""
|
||||
try:
|
||||
query = select(POSTransactionItem).where(
|
||||
and_(
|
||||
POSTransactionItem.tenant_id == tenant_id,
|
||||
POSTransactionItem.product_category == category
|
||||
)
|
||||
).order_by(POSTransactionItem.created_at.desc()).offset(skip).limit(limit)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get items by category",
|
||||
category=category,
|
||||
error=str(e))
|
||||
raise
|
||||
362
services/pos/app/repositories/pos_transaction_repository.py
Normal file
362
services/pos/app/repositories/pos_transaction_repository.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
POS Transaction Repository using Repository Pattern
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from datetime import datetime, date, timedelta
|
||||
from sqlalchemy import select, func, and_, or_, desc
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
import structlog
|
||||
|
||||
from app.models.pos_transaction import POSTransaction, POSTransactionItem
|
||||
from shared.database.repository import BaseRepository
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class POSTransactionRepository(BaseRepository[POSTransaction, dict, dict]):
|
||||
"""Repository for POS transaction operations"""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
super().__init__(POSTransaction, session)
|
||||
|
||||
async def get_transactions_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
pos_system: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
status: Optional[str] = None,
|
||||
is_synced: Optional[bool] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50
|
||||
) -> List[POSTransaction]:
|
||||
"""Get POS transactions for a specific tenant with optional filters"""
|
||||
try:
|
||||
query = select(self.model).options(
|
||||
selectinload(POSTransaction.items)
|
||||
).where(self.model.tenant_id == tenant_id)
|
||||
|
||||
# Apply filters
|
||||
conditions = []
|
||||
if pos_system:
|
||||
conditions.append(self.model.pos_system == pos_system)
|
||||
if status:
|
||||
conditions.append(self.model.status == status)
|
||||
if is_synced is not None:
|
||||
conditions.append(self.model.is_synced_to_sales == is_synced)
|
||||
if start_date:
|
||||
conditions.append(self.model.transaction_date >= start_date)
|
||||
if end_date:
|
||||
conditions.append(self.model.transaction_date <= end_date)
|
||||
|
||||
if conditions:
|
||||
query = query.where(and_(*conditions))
|
||||
|
||||
query = query.order_by(desc(self.model.transaction_date)).offset(skip).limit(limit)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get transactions by tenant", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def count_transactions_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
pos_system: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
status: Optional[str] = None,
|
||||
is_synced: Optional[bool] = None
|
||||
) -> int:
|
||||
"""Count POS transactions for a specific tenant with optional filters"""
|
||||
try:
|
||||
query = select(func.count(self.model.id)).where(self.model.tenant_id == tenant_id)
|
||||
|
||||
# Apply filters
|
||||
conditions = []
|
||||
if pos_system:
|
||||
conditions.append(self.model.pos_system == pos_system)
|
||||
if status:
|
||||
conditions.append(self.model.status == status)
|
||||
if is_synced is not None:
|
||||
conditions.append(self.model.is_synced_to_sales == is_synced)
|
||||
if start_date:
|
||||
conditions.append(self.model.transaction_date >= start_date)
|
||||
if end_date:
|
||||
conditions.append(self.model.transaction_date <= end_date)
|
||||
|
||||
if conditions:
|
||||
query = query.where(and_(*conditions))
|
||||
|
||||
result = await self.session.execute(query)
|
||||
count = result.scalar() or 0
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to count transactions by tenant", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_transaction_with_items(
|
||||
self,
|
||||
transaction_id: UUID,
|
||||
tenant_id: UUID
|
||||
) -> Optional[POSTransaction]:
|
||||
"""Get transaction with all its items"""
|
||||
try:
|
||||
query = select(POSTransaction).options(
|
||||
selectinload(POSTransaction.items)
|
||||
).where(
|
||||
and_(
|
||||
POSTransaction.id == transaction_id,
|
||||
POSTransaction.tenant_id == tenant_id
|
||||
)
|
||||
)
|
||||
result = await self.session.execute(query)
|
||||
return result.scalar_one_or_none()
|
||||
except Exception as e:
|
||||
logger.error("Failed to get transaction with items",
|
||||
transaction_id=str(transaction_id),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def get_transactions_by_pos_config(
|
||||
self,
|
||||
pos_config_id: UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 50
|
||||
) -> List[POSTransaction]:
|
||||
"""Get transactions for a specific POS configuration"""
|
||||
try:
|
||||
query = select(POSTransaction).options(
|
||||
selectinload(POSTransaction.items)
|
||||
).where(
|
||||
POSTransaction.pos_config_id == pos_config_id
|
||||
).order_by(desc(POSTransaction.transaction_date)).offset(skip).limit(limit)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
return result.scalars().all()
|
||||
except Exception as e:
|
||||
logger.error("Failed to get transactions by pos config",
|
||||
pos_config_id=str(pos_config_id),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def get_transactions_by_date_range(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
start_date: date,
|
||||
end_date: date,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[POSTransaction]:
|
||||
"""Get transactions within date range"""
|
||||
try:
|
||||
start_datetime = datetime.combine(start_date, datetime.min.time())
|
||||
end_datetime = datetime.combine(end_date, datetime.max.time())
|
||||
|
||||
query = select(POSTransaction).options(
|
||||
selectinload(POSTransaction.items)
|
||||
).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.transaction_date >= start_datetime,
|
||||
POSTransaction.transaction_date <= end_datetime
|
||||
)
|
||||
).order_by(desc(POSTransaction.transaction_date)).offset(skip).limit(limit)
|
||||
|
||||
result = await self.session.execute(query)
|
||||
return result.scalars().all()
|
||||
except Exception as e:
|
||||
logger.error("Failed to get transactions by date range",
|
||||
start_date=str(start_date),
|
||||
end_date=str(end_date),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def get_dashboard_metrics(
|
||||
self,
|
||||
tenant_id: UUID
|
||||
) -> Dict[str, Any]:
|
||||
"""Get dashboard metrics for transactions"""
|
||||
try:
|
||||
# Today's metrics
|
||||
today = datetime.now().date()
|
||||
today_start = datetime.combine(today, datetime.min.time())
|
||||
today_end = datetime.combine(today, datetime.max.time())
|
||||
|
||||
week_start = today - timedelta(days=today.weekday())
|
||||
week_start_datetime = datetime.combine(week_start, datetime.min.time())
|
||||
|
||||
month_start = today.replace(day=1)
|
||||
month_start_datetime = datetime.combine(month_start, datetime.min.time())
|
||||
|
||||
# Transaction counts by period
|
||||
transactions_today = await self.session.execute(
|
||||
select(func.count()).select_from(POSTransaction).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.transaction_date >= today_start,
|
||||
POSTransaction.transaction_date <= today_end,
|
||||
POSTransaction.status == "completed"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
transactions_week = await self.session.execute(
|
||||
select(func.count()).select_from(POSTransaction).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.transaction_date >= week_start_datetime,
|
||||
POSTransaction.status == "completed"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
transactions_month = await self.session.execute(
|
||||
select(func.count()).select_from(POSTransaction).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.transaction_date >= month_start_datetime,
|
||||
POSTransaction.status == "completed"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Revenue by period
|
||||
revenue_today = await self.session.execute(
|
||||
select(func.coalesce(func.sum(POSTransaction.total_amount), 0)).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.transaction_date >= today_start,
|
||||
POSTransaction.transaction_date <= today_end,
|
||||
POSTransaction.status == "completed"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
revenue_week = await self.session.execute(
|
||||
select(func.coalesce(func.sum(POSTransaction.total_amount), 0)).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.transaction_date >= week_start_datetime,
|
||||
POSTransaction.status == "completed"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
revenue_month = await self.session.execute(
|
||||
select(func.coalesce(func.sum(POSTransaction.total_amount), 0)).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.transaction_date >= month_start_datetime,
|
||||
POSTransaction.status == "completed"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Status breakdown
|
||||
status_counts = await self.session.execute(
|
||||
select(POSTransaction.status, func.count()).select_from(POSTransaction).where(
|
||||
POSTransaction.tenant_id == tenant_id
|
||||
).group_by(POSTransaction.status)
|
||||
)
|
||||
|
||||
status_breakdown = {status: count for status, count in status_counts.fetchall()}
|
||||
|
||||
# Payment method breakdown
|
||||
payment_counts = await self.session.execute(
|
||||
select(POSTransaction.payment_method, func.count()).select_from(POSTransaction).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.status == "completed"
|
||||
)
|
||||
).group_by(POSTransaction.payment_method)
|
||||
)
|
||||
|
||||
payment_breakdown = {method: count for method, count in payment_counts.fetchall()}
|
||||
|
||||
# Average transaction value
|
||||
avg_transaction_value = await self.session.execute(
|
||||
select(func.coalesce(func.avg(POSTransaction.total_amount), 0)).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.status == "completed"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"total_transactions_today": transactions_today.scalar(),
|
||||
"total_transactions_this_week": transactions_week.scalar(),
|
||||
"total_transactions_this_month": transactions_month.scalar(),
|
||||
"revenue_today": float(revenue_today.scalar()),
|
||||
"revenue_this_week": float(revenue_week.scalar()),
|
||||
"revenue_this_month": float(revenue_month.scalar()),
|
||||
"status_breakdown": status_breakdown,
|
||||
"payment_method_breakdown": payment_breakdown,
|
||||
"average_transaction_value": float(avg_transaction_value.scalar())
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get dashboard metrics", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_sync_status_summary(
|
||||
self,
|
||||
tenant_id: UUID
|
||||
) -> Dict[str, Any]:
|
||||
"""Get sync status summary for transactions"""
|
||||
try:
|
||||
# Count synced vs unsynced
|
||||
synced_count = await self.session.execute(
|
||||
select(func.count()).select_from(POSTransaction).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.is_synced_to_sales == True
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
pending_count = await self.session.execute(
|
||||
select(func.count()).select_from(POSTransaction).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.is_synced_to_sales == False,
|
||||
POSTransaction.sync_error.is_(None)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
failed_count = await self.session.execute(
|
||||
select(func.count()).select_from(POSTransaction).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.is_synced_to_sales == False,
|
||||
POSTransaction.sync_error.isnot(None)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Get last sync time
|
||||
last_sync = await self.session.execute(
|
||||
select(func.max(POSTransaction.sync_completed_at)).where(
|
||||
and_(
|
||||
POSTransaction.tenant_id == tenant_id,
|
||||
POSTransaction.is_synced_to_sales == True
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"synced": synced_count.scalar(),
|
||||
"pending": pending_count.scalar(),
|
||||
"failed": failed_count.scalar(),
|
||||
"last_sync_at": last_sync.scalar()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Failed to get sync status summary", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
95
services/pos/app/schemas/pos_config.py
Normal file
95
services/pos/app/schemas/pos_config.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Pydantic schemas for POS configuration API requests and responses
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class POSProvider(str, Enum):
|
||||
"""POS provider types"""
|
||||
SQUARE = "square"
|
||||
TOAST = "toast"
|
||||
LIGHTSPEED = "lightspeed"
|
||||
|
||||
|
||||
class POSConfigurationBase(BaseModel):
|
||||
"""Base schema for POS configurations"""
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
|
||||
|
||||
class POSConfigurationResponse(POSConfigurationBase):
|
||||
"""Schema for POS configuration API responses"""
|
||||
id: str
|
||||
tenant_id: str
|
||||
pos_system: POSProvider
|
||||
provider_name: str
|
||||
is_active: bool
|
||||
is_connected: bool
|
||||
webhook_url: Optional[str] = None
|
||||
webhook_secret: Optional[str] = None
|
||||
environment: str = "sandbox"
|
||||
location_id: Optional[str] = None
|
||||
merchant_id: Optional[str] = None
|
||||
sync_enabled: bool = True
|
||||
sync_interval_minutes: str = "5"
|
||||
auto_sync_products: bool = True
|
||||
auto_sync_transactions: bool = True
|
||||
last_sync_at: Optional[datetime] = None
|
||||
last_successful_sync_at: Optional[datetime] = None
|
||||
last_sync_status: Optional[str] = None
|
||||
last_sync_message: Optional[str] = None
|
||||
provider_settings: Optional[Dict[str, Any]] = None
|
||||
last_health_check_at: Optional[datetime] = None
|
||||
health_status: str = "unknown"
|
||||
health_message: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
notes: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, obj):
|
||||
"""Convert ORM object to schema with proper UUID handling"""
|
||||
return cls(
|
||||
id=str(obj.id),
|
||||
tenant_id=str(obj.tenant_id),
|
||||
pos_system=obj.pos_system,
|
||||
provider_name=obj.provider_name,
|
||||
is_active=obj.is_active,
|
||||
is_connected=obj.is_connected,
|
||||
webhook_url=obj.webhook_url,
|
||||
webhook_secret=obj.webhook_secret,
|
||||
environment=obj.environment,
|
||||
location_id=obj.location_id,
|
||||
merchant_id=obj.merchant_id,
|
||||
sync_enabled=obj.sync_enabled,
|
||||
sync_interval_minutes=obj.sync_interval_minutes,
|
||||
auto_sync_products=obj.auto_sync_products,
|
||||
auto_sync_transactions=obj.auto_sync_transactions,
|
||||
last_sync_at=obj.last_sync_at,
|
||||
last_successful_sync_at=obj.last_successful_sync_at,
|
||||
last_sync_status=obj.last_sync_status,
|
||||
last_sync_message=obj.last_sync_message,
|
||||
provider_settings=obj.provider_settings,
|
||||
last_health_check_at=obj.last_health_check_at,
|
||||
health_status=obj.health_status,
|
||||
health_message=obj.health_message,
|
||||
created_at=obj.created_at,
|
||||
updated_at=obj.updated_at,
|
||||
notes=obj.notes
|
||||
)
|
||||
|
||||
|
||||
class POSConfigurationListResponse(BaseModel):
|
||||
"""Schema for POS configuration list API response"""
|
||||
configurations: List[POSConfigurationResponse]
|
||||
total: int
|
||||
supported_systems: List[str] = ["square", "toast", "lightspeed"]
|
||||
248
services/pos/app/schemas/pos_transaction.py
Normal file
248
services/pos/app/schemas/pos_transaction.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
Pydantic schemas for POS transaction API requests and responses
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TransactionType(str, Enum):
|
||||
"""Transaction type enumeration"""
|
||||
SALE = "sale"
|
||||
REFUND = "refund"
|
||||
VOID = "void"
|
||||
EXCHANGE = "exchange"
|
||||
|
||||
|
||||
class TransactionStatus(str, Enum):
|
||||
"""Transaction status enumeration"""
|
||||
COMPLETED = "completed"
|
||||
PENDING = "pending"
|
||||
FAILED = "failed"
|
||||
REFUNDED = "refunded"
|
||||
VOIDED = "voided"
|
||||
|
||||
|
||||
class PaymentMethod(str, Enum):
|
||||
"""Payment method enumeration"""
|
||||
CARD = "card"
|
||||
CASH = "cash"
|
||||
DIGITAL_WALLET = "digital_wallet"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class OrderType(str, Enum):
|
||||
"""Order type enumeration"""
|
||||
DINE_IN = "dine_in"
|
||||
TAKEOUT = "takeout"
|
||||
DELIVERY = "delivery"
|
||||
PICKUP = "pickup"
|
||||
|
||||
|
||||
class POSTransactionItemResponse(BaseModel):
|
||||
"""Schema for POS transaction item response"""
|
||||
id: str
|
||||
transaction_id: str
|
||||
tenant_id: str
|
||||
external_item_id: Optional[str] = None
|
||||
sku: Optional[str] = None
|
||||
product_name: str
|
||||
product_category: Optional[str] = None
|
||||
product_subcategory: Optional[str] = None
|
||||
quantity: Decimal
|
||||
unit_price: Decimal
|
||||
total_price: Decimal
|
||||
discount_amount: Decimal = Decimal("0")
|
||||
tax_amount: Decimal = Decimal("0")
|
||||
modifiers: Optional[Dict[str, Any]] = None
|
||||
inventory_product_id: Optional[str] = None
|
||||
is_mapped_to_inventory: bool = False
|
||||
is_synced_to_sales: bool = False
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None,
|
||||
Decimal: lambda v: float(v) if v else 0.0
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, obj):
|
||||
"""Convert ORM object to schema with proper UUID and Decimal handling"""
|
||||
return cls(
|
||||
id=str(obj.id),
|
||||
transaction_id=str(obj.transaction_id),
|
||||
tenant_id=str(obj.tenant_id),
|
||||
external_item_id=obj.external_item_id,
|
||||
sku=obj.sku,
|
||||
product_name=obj.product_name,
|
||||
product_category=obj.product_category,
|
||||
product_subcategory=obj.product_subcategory,
|
||||
quantity=obj.quantity,
|
||||
unit_price=obj.unit_price,
|
||||
total_price=obj.total_price,
|
||||
discount_amount=obj.discount_amount,
|
||||
tax_amount=obj.tax_amount,
|
||||
modifiers=obj.modifiers,
|
||||
inventory_product_id=str(obj.inventory_product_id) if obj.inventory_product_id else None,
|
||||
is_mapped_to_inventory=obj.is_mapped_to_inventory,
|
||||
is_synced_to_sales=obj.is_synced_to_sales,
|
||||
created_at=obj.created_at,
|
||||
updated_at=obj.updated_at
|
||||
)
|
||||
|
||||
|
||||
class POSTransactionResponse(BaseModel):
|
||||
"""Schema for POS transaction response"""
|
||||
id: str
|
||||
tenant_id: str
|
||||
pos_config_id: str
|
||||
pos_system: str
|
||||
external_transaction_id: str
|
||||
external_order_id: Optional[str] = None
|
||||
transaction_type: TransactionType
|
||||
status: TransactionStatus
|
||||
subtotal: Decimal
|
||||
tax_amount: Decimal
|
||||
tip_amount: Decimal
|
||||
discount_amount: Decimal
|
||||
total_amount: Decimal
|
||||
currency: str = "EUR"
|
||||
payment_method: Optional[PaymentMethod] = None
|
||||
payment_status: Optional[str] = None
|
||||
transaction_date: datetime
|
||||
pos_created_at: datetime
|
||||
pos_updated_at: Optional[datetime] = None
|
||||
location_id: Optional[str] = None
|
||||
location_name: Optional[str] = None
|
||||
staff_id: Optional[str] = None
|
||||
staff_name: Optional[str] = None
|
||||
customer_id: Optional[str] = None
|
||||
customer_email: Optional[str] = None
|
||||
customer_phone: Optional[str] = None
|
||||
order_type: Optional[OrderType] = None
|
||||
table_number: Optional[str] = None
|
||||
receipt_number: Optional[str] = None
|
||||
is_synced_to_sales: bool = False
|
||||
sales_record_id: Optional[str] = None
|
||||
sync_attempted_at: Optional[datetime] = None
|
||||
sync_completed_at: Optional[datetime] = None
|
||||
sync_error: Optional[str] = None
|
||||
sync_retry_count: int = 0
|
||||
is_processed: bool = False
|
||||
is_duplicate: bool = False
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
items: List[POSTransactionItemResponse] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None,
|
||||
Decimal: lambda v: float(v) if v else 0.0
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_orm(cls, obj):
|
||||
"""Convert ORM object to schema with proper UUID and Decimal handling"""
|
||||
return cls(
|
||||
id=str(obj.id),
|
||||
tenant_id=str(obj.tenant_id),
|
||||
pos_config_id=str(obj.pos_config_id),
|
||||
pos_system=obj.pos_system,
|
||||
external_transaction_id=obj.external_transaction_id,
|
||||
external_order_id=obj.external_order_id,
|
||||
transaction_type=obj.transaction_type,
|
||||
status=obj.status,
|
||||
subtotal=obj.subtotal,
|
||||
tax_amount=obj.tax_amount,
|
||||
tip_amount=obj.tip_amount,
|
||||
discount_amount=obj.discount_amount,
|
||||
total_amount=obj.total_amount,
|
||||
currency=obj.currency,
|
||||
payment_method=obj.payment_method,
|
||||
payment_status=obj.payment_status,
|
||||
transaction_date=obj.transaction_date,
|
||||
pos_created_at=obj.pos_created_at,
|
||||
pos_updated_at=obj.pos_updated_at,
|
||||
location_id=obj.location_id,
|
||||
location_name=obj.location_name,
|
||||
staff_id=obj.staff_id,
|
||||
staff_name=obj.staff_name,
|
||||
customer_id=obj.customer_id,
|
||||
customer_email=obj.customer_email,
|
||||
customer_phone=obj.customer_phone,
|
||||
order_type=obj.order_type,
|
||||
table_number=obj.table_number,
|
||||
receipt_number=obj.receipt_number,
|
||||
is_synced_to_sales=obj.is_synced_to_sales,
|
||||
sales_record_id=str(obj.sales_record_id) if obj.sales_record_id else None,
|
||||
sync_attempted_at=obj.sync_attempted_at,
|
||||
sync_completed_at=obj.sync_completed_at,
|
||||
sync_error=obj.sync_error,
|
||||
sync_retry_count=obj.sync_retry_count,
|
||||
is_processed=obj.is_processed,
|
||||
is_duplicate=obj.is_duplicate,
|
||||
created_at=obj.created_at,
|
||||
updated_at=obj.updated_at,
|
||||
items=[POSTransactionItemResponse.from_orm(item) for item in obj.items] if hasattr(obj, 'items') and obj.items else []
|
||||
)
|
||||
|
||||
|
||||
class POSTransactionSummary(BaseModel):
|
||||
"""Summary information for a transaction (lightweight)"""
|
||||
id: str
|
||||
external_transaction_id: str
|
||||
transaction_date: datetime
|
||||
total_amount: Decimal
|
||||
status: TransactionStatus
|
||||
payment_method: Optional[PaymentMethod] = None
|
||||
is_synced_to_sales: bool
|
||||
item_count: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
use_enum_values = True
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.isoformat() if v else None,
|
||||
Decimal: lambda v: float(v) if v else 0.0
|
||||
}
|
||||
|
||||
|
||||
class POSTransactionListResponse(BaseModel):
|
||||
"""Schema for paginated transaction list response"""
|
||||
transactions: List[POSTransactionResponse]
|
||||
total: int
|
||||
has_more: bool = False
|
||||
summary: Optional[Dict[str, Any]] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class POSTransactionDashboardSummary(BaseModel):
|
||||
"""Dashboard summary for POS transactions"""
|
||||
total_transactions_today: int = 0
|
||||
total_transactions_this_week: int = 0
|
||||
total_transactions_this_month: int = 0
|
||||
revenue_today: Decimal = Decimal("0")
|
||||
revenue_this_week: Decimal = Decimal("0")
|
||||
revenue_this_month: Decimal = Decimal("0")
|
||||
average_transaction_value: Decimal = Decimal("0")
|
||||
status_breakdown: Dict[str, int] = {}
|
||||
payment_method_breakdown: Dict[str, int] = {}
|
||||
sync_status: Dict[str, Any] = {}
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
json_encoders = {
|
||||
Decimal: lambda v: float(v) if v else 0.0,
|
||||
datetime: lambda v: v.isoformat() if v else None
|
||||
}
|
||||
76
services/pos/app/services/pos_config_service.py
Normal file
76
services/pos/app/services/pos_config_service.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
POS Configuration Service - Business Logic Layer
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
import structlog
|
||||
|
||||
from app.repositories.pos_config_repository import POSConfigurationRepository
|
||||
from app.schemas.pos_config import POSConfigurationResponse
|
||||
from app.core.database import get_db_transaction
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class POSConfigurationService:
|
||||
"""Service layer for POS configuration operations"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def get_configurations_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
pos_system: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[POSConfigurationResponse]:
|
||||
"""Get POS configurations for a tenant with filtering"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
repository = POSConfigurationRepository(db)
|
||||
|
||||
configurations = await repository.get_configurations_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
pos_system=pos_system,
|
||||
is_active=is_active,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
# Convert to response schemas using from_orm
|
||||
responses = []
|
||||
for config in configurations:
|
||||
response = POSConfigurationResponse.from_orm(config)
|
||||
responses.append(response)
|
||||
|
||||
return responses
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get configurations by tenant", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def count_configurations_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
pos_system: Optional[str] = None,
|
||||
is_active: Optional[bool] = None
|
||||
) -> int:
|
||||
"""Count POS configurations for a tenant with filtering"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
repository = POSConfigurationRepository(db)
|
||||
|
||||
count = await repository.count_configurations_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
pos_system=pos_system,
|
||||
is_active=is_active
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to count configurations by tenant", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
239
services/pos/app/services/pos_transaction_service.py
Normal file
239
services/pos/app/services/pos_transaction_service.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
POS Transaction Service - Business Logic Layer
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
import structlog
|
||||
|
||||
from app.repositories.pos_transaction_repository import POSTransactionRepository
|
||||
from app.repositories.pos_transaction_item_repository import POSTransactionItemRepository
|
||||
from app.schemas.pos_transaction import (
|
||||
POSTransactionResponse,
|
||||
POSTransactionDashboardSummary
|
||||
)
|
||||
from app.core.database import get_db_transaction
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class POSTransactionService:
|
||||
"""Service layer for POS transaction operations"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def get_transactions_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
pos_system: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
status: Optional[str] = None,
|
||||
is_synced: Optional[bool] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 50
|
||||
) -> List[POSTransactionResponse]:
|
||||
"""Get POS transactions for a tenant with filtering"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
repository = POSTransactionRepository(db)
|
||||
|
||||
transactions = await repository.get_transactions_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
pos_system=pos_system,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
status=status,
|
||||
is_synced=is_synced,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
# Convert to response schemas
|
||||
responses = []
|
||||
for transaction in transactions:
|
||||
response = POSTransactionResponse.from_orm(transaction)
|
||||
responses.append(response)
|
||||
|
||||
return responses
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get transactions by tenant", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def count_transactions_by_tenant(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
pos_system: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
status: Optional[str] = None,
|
||||
is_synced: Optional[bool] = None
|
||||
) -> int:
|
||||
"""Count POS transactions for a tenant with filtering"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
repository = POSTransactionRepository(db)
|
||||
|
||||
count = await repository.count_transactions_by_tenant(
|
||||
tenant_id=tenant_id,
|
||||
pos_system=pos_system,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
status=status,
|
||||
is_synced=is_synced
|
||||
)
|
||||
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to count transactions by tenant", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_transaction_with_items(
|
||||
self,
|
||||
transaction_id: UUID,
|
||||
tenant_id: UUID
|
||||
) -> Optional[POSTransactionResponse]:
|
||||
"""Get transaction with all its items"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
repository = POSTransactionRepository(db)
|
||||
|
||||
transaction = await repository.get_transaction_with_items(
|
||||
transaction_id=transaction_id,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
if not transaction:
|
||||
return None
|
||||
|
||||
return POSTransactionResponse.from_orm(transaction)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get transaction with items",
|
||||
transaction_id=str(transaction_id),
|
||||
error=str(e))
|
||||
raise
|
||||
|
||||
async def get_dashboard_summary(
|
||||
self,
|
||||
tenant_id: UUID
|
||||
) -> POSTransactionDashboardSummary:
|
||||
"""Get dashboard summary for POS transactions"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
repository = POSTransactionRepository(db)
|
||||
|
||||
# Get metrics from repository
|
||||
metrics = await repository.get_dashboard_metrics(tenant_id)
|
||||
|
||||
# Get sync status
|
||||
sync_status = await repository.get_sync_status_summary(tenant_id)
|
||||
|
||||
# Construct dashboard summary
|
||||
return POSTransactionDashboardSummary(
|
||||
total_transactions_today=metrics["total_transactions_today"],
|
||||
total_transactions_this_week=metrics["total_transactions_this_week"],
|
||||
total_transactions_this_month=metrics["total_transactions_this_month"],
|
||||
revenue_today=Decimal(str(metrics["revenue_today"])),
|
||||
revenue_this_week=Decimal(str(metrics["revenue_this_week"])),
|
||||
revenue_this_month=Decimal(str(metrics["revenue_this_month"])),
|
||||
average_transaction_value=Decimal(str(metrics["average_transaction_value"])),
|
||||
status_breakdown=metrics["status_breakdown"],
|
||||
payment_method_breakdown=metrics["payment_method_breakdown"],
|
||||
sync_status=sync_status
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get dashboard summary", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def get_sync_metrics(
|
||||
self,
|
||||
tenant_id: UUID
|
||||
) -> Dict[str, Any]:
|
||||
"""Get sync metrics for transactions"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
repository = POSTransactionRepository(db)
|
||||
|
||||
sync_status = await repository.get_sync_status_summary(tenant_id)
|
||||
|
||||
# Calculate sync rate
|
||||
total = sync_status["synced"] + sync_status["pending"] + sync_status["failed"]
|
||||
sync_rate = (sync_status["synced"] / total * 100) if total > 0 else 0
|
||||
|
||||
return {
|
||||
"sync_status": sync_status,
|
||||
"sync_rate_percentage": round(sync_rate, 2),
|
||||
"total_transactions": total
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to get sync metrics", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
|
||||
async def calculate_transaction_analytics(
|
||||
self,
|
||||
tenant_id: UUID,
|
||||
start_date: datetime,
|
||||
end_date: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""Calculate analytics for transactions within a date range"""
|
||||
try:
|
||||
async with get_db_transaction() as db:
|
||||
repository = POSTransactionRepository(db)
|
||||
|
||||
transactions = await repository.get_transactions_by_date_range(
|
||||
tenant_id=tenant_id,
|
||||
start_date=start_date.date(),
|
||||
end_date=end_date.date(),
|
||||
skip=0,
|
||||
limit=10000 # Large limit for analytics
|
||||
)
|
||||
|
||||
# Calculate analytics
|
||||
total_revenue = Decimal("0")
|
||||
total_transactions = len(transactions)
|
||||
payment_methods = {}
|
||||
order_types = {}
|
||||
hourly_distribution = {}
|
||||
|
||||
for transaction in transactions:
|
||||
if transaction.status == "completed":
|
||||
total_revenue += transaction.total_amount
|
||||
|
||||
# Payment method breakdown
|
||||
pm = transaction.payment_method or "unknown"
|
||||
payment_methods[pm] = payment_methods.get(pm, 0) + 1
|
||||
|
||||
# Order type breakdown
|
||||
ot = transaction.order_type or "unknown"
|
||||
order_types[ot] = order_types.get(ot, 0) + 1
|
||||
|
||||
# Hourly distribution
|
||||
hour = transaction.transaction_date.hour
|
||||
hourly_distribution[hour] = hourly_distribution.get(hour, 0) + 1
|
||||
|
||||
avg_transaction_value = (total_revenue / total_transactions) if total_transactions > 0 else Decimal("0")
|
||||
|
||||
return {
|
||||
"period": {
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat()
|
||||
},
|
||||
"total_revenue": float(total_revenue),
|
||||
"total_transactions": total_transactions,
|
||||
"average_transaction_value": float(avg_transaction_value),
|
||||
"payment_methods": payment_methods,
|
||||
"order_types": order_types,
|
||||
"hourly_distribution": hourly_distribution
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to calculate transaction analytics", error=str(e), tenant_id=tenant_id)
|
||||
raise
|
||||
Reference in New Issue
Block a user