363 lines
14 KiB
Python
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
|