Files
bakery-ia/services/pos/app/repositories/pos_transaction_repository.py
2025-10-23 07:44:54 +02:00

363 lines
14 KiB
Python

"""
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