Files
bakery-ia/services/pos/app/repositories/pos_transaction_repository.py

363 lines
14 KiB
Python
Raw Normal View History

2026-01-21 17:17:16 +01:00
"""
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