Spaces:
Paused
Paused
Upload 77 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env +0 -0
- __init__.py +3 -0
- __pycache__/main.cpython-312.pyc +0 -0
- app/__init__.py +3 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/api/__init__.py +1 -0
- app/api/__pycache__/__init__.cpython-312.pyc +0 -0
- app/api/api_v1/__init__.py +1 -0
- app/api/api_v1/__pycache__/__init__.cpython-312.pyc +0 -0
- app/api/api_v1/__pycache__/api.cpython-312.pyc +0 -0
- app/api/api_v1/api.py +10 -0
- app/api/api_v1/endpoints/__init__.py +1 -0
- app/api/api_v1/endpoints/__pycache__/__init__.cpython-312.pyc +0 -0
- app/api/api_v1/endpoints/__pycache__/auth.cpython-312.pyc +0 -0
- app/api/api_v1/endpoints/__pycache__/menu.cpython-312.pyc +0 -0
- app/api/api_v1/endpoints/__pycache__/orders.cpython-312.pyc +0 -0
- app/api/api_v1/endpoints/__pycache__/payments.cpython-312.pyc +0 -0
- app/api/api_v1/endpoints/__pycache__/reports.cpython-312.pyc +0 -0
- app/api/api_v1/endpoints/auth.py +13 -0
- app/api/api_v1/endpoints/menu.py +19 -0
- app/api/api_v1/endpoints/orders.py +36 -0
- app/api/api_v1/endpoints/payments.py +27 -0
- app/api/api_v1/endpoints/reports.py +36 -0
- app/core/__pycache__/config.cpython-312.pyc +0 -0
- app/core/auth.py +0 -0
- app/core/config.py +15 -0
- app/core/database.py +0 -0
- app/core/dependencies.py +0 -0
- app/db/__pycache__/crud.cpython-312.pyc +0 -0
- app/db/__pycache__/database.cpython-312.pyc +0 -0
- app/db/crud.py +200 -0
- app/db/database.py +83 -0
- app/db/models.py +0 -0
- app/main.py +72 -0
- app/models/__pycache__/auth.cpython-312.pyc +0 -0
- app/models/__pycache__/database.cpython-312.pyc +0 -0
- app/models/__pycache__/menu.cpython-312.pyc +0 -0
- app/models/__pycache__/orders.cpython-312.pyc +0 -0
- app/models/__pycache__/payment.cpython-312.pyc +0 -0
- app/models/__pycache__/reports.cpython-312.pyc +0 -0
- app/models/auth.py +17 -0
- app/models/database.py +72 -0
- app/models/menu.py +32 -0
- app/models/orders.py +37 -0
- app/models/payment.py +24 -0
- app/models/payments.py +42 -0
- app/models/reports.py +50 -0
- app/routes/auth.py +24 -0
- app/routes/menu.py +52 -0
- app/routes/orders.py +63 -0
.env
ADDED
|
File without changes
|
__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
POS Backend Package
|
| 3 |
+
"""
|
__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (3.23 kB). View file
|
|
|
app/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
POS Backend Application Package
|
| 3 |
+
"""
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (218 Bytes). View file
|
|
|
app/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Empty file to mark directory as Python package
|
app/api/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (172 Bytes). View file
|
|
|
app/api/api_v1/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Empty file to mark directory as Python package
|
app/api/api_v1/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (179 Bytes). View file
|
|
|
app/api/api_v1/__pycache__/api.cpython-312.pyc
ADDED
|
Binary file (931 Bytes). View file
|
|
|
app/api/api_v1/api.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from app.api.api_v1.endpoints import auth, menu, orders, payments, reports
|
| 3 |
+
|
| 4 |
+
api_router = APIRouter()
|
| 5 |
+
|
| 6 |
+
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
| 7 |
+
api_router.include_router(menu.router, prefix="/menu", tags=["menu"])
|
| 8 |
+
api_router.include_router(orders.router, prefix="/orders", tags=["orders"])
|
| 9 |
+
api_router.include_router(payments.router, prefix="/payments", tags=["payments"])
|
| 10 |
+
api_router.include_router(reports.router, prefix="/reports", tags=["reports"])
|
app/api/api_v1/endpoints/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Empty file to mark directory as Python package
|
app/api/api_v1/endpoints/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (189 Bytes). View file
|
|
|
app/api/api_v1/endpoints/__pycache__/auth.cpython-312.pyc
ADDED
|
Binary file (1.15 kB). View file
|
|
|
app/api/api_v1/endpoints/__pycache__/menu.cpython-312.pyc
ADDED
|
Binary file (1.52 kB). View file
|
|
|
app/api/api_v1/endpoints/__pycache__/orders.cpython-312.pyc
ADDED
|
Binary file (2.44 kB). View file
|
|
|
app/api/api_v1/endpoints/__pycache__/payments.cpython-312.pyc
ADDED
|
Binary file (1.71 kB). View file
|
|
|
app/api/api_v1/endpoints/__pycache__/reports.cpython-312.pyc
ADDED
|
Binary file (1.38 kB). View file
|
|
|
app/api/api_v1/endpoints/auth.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 2 |
+
from app.services.auth_service import authenticate_user, create_access_token
|
| 3 |
+
from app.models.auth import LoginRequest, LoginResponse
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
@router.post("/login", response_model=LoginResponse)
|
| 8 |
+
async def login(user_data: LoginRequest):
|
| 9 |
+
user = await authenticate_user(user_data.email, user_data.password)
|
| 10 |
+
if not user:
|
| 11 |
+
raise HTTPException(status_code=401, detail="Invalid credentials")
|
| 12 |
+
access_token = create_access_token(data={"sub": user.email})
|
| 13 |
+
return LoginResponse(access_token=access_token, token_type="bearer", user=user)
|
app/api/api_v1/endpoints/menu.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 2 |
+
from app.services.menu_service import MenuService
|
| 3 |
+
from app.models.menu import MenuItem, Category
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
router = APIRouter()
|
| 7 |
+
menu_service = MenuService()
|
| 8 |
+
|
| 9 |
+
@router.get("/items", response_model=List[MenuItem])
|
| 10 |
+
async def get_menu_items():
|
| 11 |
+
return await menu_service.get_menu_items()
|
| 12 |
+
|
| 13 |
+
@router.post("/items", response_model=MenuItem, status_code=status.HTTP_201_CREATED)
|
| 14 |
+
async def create_menu_item(item: MenuItem):
|
| 15 |
+
return await menu_service.create_menu_item(item)
|
| 16 |
+
|
| 17 |
+
@router.get("/categories", response_model=List[Category])
|
| 18 |
+
async def get_categories():
|
| 19 |
+
return await menu_service.get_categories()
|
app/api/api_v1/endpoints/orders.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, status, Depends
|
| 2 |
+
from app.services.order_service import OrderService
|
| 3 |
+
from app.models.orders import Order, OrderStatus
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
router = APIRouter()
|
| 7 |
+
|
| 8 |
+
@router.post("/", response_model=Order, status_code=status.HTTP_201_CREATED)
|
| 9 |
+
async def create_order(order: Order, service: OrderService = Depends(OrderService)):
|
| 10 |
+
return await service.create_order(order)
|
| 11 |
+
|
| 12 |
+
@router.get("/{order_id}", response_model=Order)
|
| 13 |
+
async def get_order(order_id: int, service: OrderService = Depends(OrderService)):
|
| 14 |
+
return await service.get_order(order_id)
|
| 15 |
+
|
| 16 |
+
@router.put("/{order_id}/status", response_model=Order)
|
| 17 |
+
async def update_order_status(
|
| 18 |
+
order_id: int,
|
| 19 |
+
status: OrderStatus,
|
| 20 |
+
service: OrderService = Depends(OrderService)
|
| 21 |
+
):
|
| 22 |
+
return await service.update_status(order_id, status)
|
| 23 |
+
|
| 24 |
+
@router.get("/status/{status}", response_model=List[Order])
|
| 25 |
+
async def get_orders_by_status(
|
| 26 |
+
status: OrderStatus,
|
| 27 |
+
service: OrderService = Depends(OrderService)
|
| 28 |
+
):
|
| 29 |
+
return await service.get_orders_by_status(status)
|
| 30 |
+
|
| 31 |
+
@router.get("/daily/{date}", response_model=List[Order])
|
| 32 |
+
async def get_daily_orders(
|
| 33 |
+
date: str,
|
| 34 |
+
service: OrderService = Depends(OrderService)
|
| 35 |
+
):
|
| 36 |
+
return await service.get_daily_orders(date)
|
app/api/api_v1/endpoints/payments.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, status, Depends
|
| 2 |
+
from app.services.payment_service import PaymentService
|
| 3 |
+
from app.models.payment import Payment, PaymentStatus
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
@router.post("/", response_model=Payment, status_code=status.HTTP_201_CREATED)
|
| 8 |
+
async def process_payment(
|
| 9 |
+
payment: Payment,
|
| 10 |
+
service: PaymentService = Depends(PaymentService)
|
| 11 |
+
):
|
| 12 |
+
return await service.process_payment(payment)
|
| 13 |
+
|
| 14 |
+
@router.get("/{payment_id}", response_model=Payment)
|
| 15 |
+
async def get_payment(
|
| 16 |
+
payment_id: int,
|
| 17 |
+
service: PaymentService = Depends(PaymentService)
|
| 18 |
+
):
|
| 19 |
+
return await service.get_payment(payment_id)
|
| 20 |
+
|
| 21 |
+
@router.put("/{payment_id}/status", response_model=Payment)
|
| 22 |
+
async def update_payment_status(
|
| 23 |
+
payment_id: int,
|
| 24 |
+
status: PaymentStatus,
|
| 25 |
+
service: PaymentService = Depends(PaymentService)
|
| 26 |
+
):
|
| 27 |
+
return await service.update_status(payment_id, status)
|
app/api/api_v1/endpoints/reports.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends
|
| 2 |
+
from app.services.report_service import ReportService
|
| 3 |
+
from app.models.reports import DailyReport, WeeklyReport
|
| 4 |
+
from datetime import date
|
| 5 |
+
from fastapi.responses import JSONResponse
|
| 6 |
+
from fastapi import HTTPException, status
|
| 7 |
+
|
| 8 |
+
router = APIRouter()
|
| 9 |
+
|
| 10 |
+
@router.get("/daily/{date}", response_model=DailyReport)
|
| 11 |
+
async def get_daily_report(
|
| 12 |
+
date: date,
|
| 13 |
+
service: ReportService = Depends(ReportService)
|
| 14 |
+
):
|
| 15 |
+
try:
|
| 16 |
+
return await service.generate_daily_report(date.isoformat())
|
| 17 |
+
except Exception as e:
|
| 18 |
+
raise HTTPException(
|
| 19 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 20 |
+
detail=f"Failed to generate daily report: {str(e)}"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
@router.get("/weekly", response_model=WeeklyReport)
|
| 24 |
+
async def get_weekly_report(
|
| 25 |
+
end_date: date = None,
|
| 26 |
+
service: ReportService = Depends(ReportService)
|
| 27 |
+
):
|
| 28 |
+
try:
|
| 29 |
+
return await service.generate_weekly_report(
|
| 30 |
+
end_date.isoformat() if end_date else None
|
| 31 |
+
)
|
| 32 |
+
except Exception as e:
|
| 33 |
+
raise HTTPException(
|
| 34 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 35 |
+
detail=f"Failed to generate weekly report: {str(e)}"
|
| 36 |
+
)
|
app/core/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (1.16 kB). View file
|
|
|
app/core/auth.py
ADDED
|
File without changes
|
app/core/config.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
|
| 3 |
+
class Settings(BaseSettings):
|
| 4 |
+
PROJECT_NAME: str = "POS Backend"
|
| 5 |
+
API_V1_STR: str = "/api/v1"
|
| 6 |
+
SECRET_KEY: str = "your-secret-key-here"
|
| 7 |
+
ALGORITHM: str = "HS256"
|
| 8 |
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
| 9 |
+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
| 10 |
+
DATABASE_URL: str = "postgresql+asyncpg://postgres.juycnkjuzylnbruwaqmp:Lovyelias5584.@aws-0-eu-central-1.pooler.supabase.com:5432/postgres"
|
| 11 |
+
|
| 12 |
+
class Config:
|
| 13 |
+
env_file = ".env"
|
| 14 |
+
|
| 15 |
+
settings = Settings()
|
app/core/database.py
ADDED
|
File without changes
|
app/core/dependencies.py
ADDED
|
File without changes
|
app/db/__pycache__/crud.cpython-312.pyc
ADDED
|
Binary file (14.6 kB). View file
|
|
|
app/db/__pycache__/database.cpython-312.pyc
ADDED
|
Binary file (4.76 kB). View file
|
|
|
app/db/crud.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.ext.asyncio import AsyncSession
|
| 2 |
+
from sqlalchemy import select, update, delete, and_, func
|
| 3 |
+
from sqlalchemy.orm import selectinload
|
| 4 |
+
from ..models.database import MenuItem, Order, Payment, User, Category, OrderItem
|
| 5 |
+
from ..models.orders import OrderStatus
|
| 6 |
+
from ..models.payment import PaymentStatus
|
| 7 |
+
from typing import List, Optional, Dict
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
# User Operations
|
| 11 |
+
async def get_user_by_email(db: AsyncSession, email: str) -> Optional[User]:
|
| 12 |
+
result = await db.execute(select(User).where(User.email == email))
|
| 13 |
+
return result.scalar_one_or_none()
|
| 14 |
+
|
| 15 |
+
async def create_user(db: AsyncSession, user: User) -> User:
|
| 16 |
+
db.add(user)
|
| 17 |
+
await db.commit()
|
| 18 |
+
await db.refresh(user)
|
| 19 |
+
return user
|
| 20 |
+
|
| 21 |
+
# Menu Operations
|
| 22 |
+
async def get_menu_items(db: AsyncSession) -> List[MenuItem]:
|
| 23 |
+
result = await db.execute(
|
| 24 |
+
select(MenuItem).options(selectinload(MenuItem.category))
|
| 25 |
+
)
|
| 26 |
+
return result.scalars().all()
|
| 27 |
+
|
| 28 |
+
async def get_menu_item_by_id(db: AsyncSession, item_id: int) -> Optional[MenuItem]:
|
| 29 |
+
result = await db.execute(
|
| 30 |
+
select(MenuItem)
|
| 31 |
+
.options(selectinload(MenuItem.category))
|
| 32 |
+
.where(MenuItem.id == item_id)
|
| 33 |
+
)
|
| 34 |
+
return result.scalar_one_or_none()
|
| 35 |
+
|
| 36 |
+
async def create_menu_item_db(db: AsyncSession, item: MenuItem) -> MenuItem:
|
| 37 |
+
db.add(item)
|
| 38 |
+
await db.commit()
|
| 39 |
+
await db.refresh(item)
|
| 40 |
+
return item
|
| 41 |
+
|
| 42 |
+
async def update_menu_item_db(db: AsyncSession, item_id: int, item: MenuItem) -> Optional[MenuItem]:
|
| 43 |
+
query = update(MenuItem).where(MenuItem.id == item_id).values(**item.dict(exclude={'id'}))
|
| 44 |
+
await db.execute(query)
|
| 45 |
+
await db.commit()
|
| 46 |
+
return await get_menu_item_by_id(db, item_id)
|
| 47 |
+
|
| 48 |
+
async def delete_menu_item_db(db: AsyncSession, item_id: int) -> bool:
|
| 49 |
+
query = delete(MenuItem).where(MenuItem.id == item_id)
|
| 50 |
+
result = await db.execute(query)
|
| 51 |
+
await db.commit()
|
| 52 |
+
return result.rowcount > 0
|
| 53 |
+
|
| 54 |
+
async def get_categories_db(db: AsyncSession) -> List[Category]:
|
| 55 |
+
result = await db.execute(select(Category))
|
| 56 |
+
return result.scalars().all()
|
| 57 |
+
|
| 58 |
+
# Order Operations
|
| 59 |
+
async def create_order_db(db: AsyncSession, order: Order) -> Order:
|
| 60 |
+
db.add(order)
|
| 61 |
+
await db.commit()
|
| 62 |
+
await db.refresh(order)
|
| 63 |
+
return order
|
| 64 |
+
|
| 65 |
+
async def get_order_by_id(db: AsyncSession, order_id: int) -> Optional[Order]:
|
| 66 |
+
result = await db.execute(
|
| 67 |
+
select(Order)
|
| 68 |
+
.options(selectinload(Order.items))
|
| 69 |
+
.where(Order.id == order_id)
|
| 70 |
+
)
|
| 71 |
+
return result.scalar_one_or_none()
|
| 72 |
+
|
| 73 |
+
async def update_order_status(db: AsyncSession, order_id: int, new_status: OrderStatus) -> Order:
|
| 74 |
+
"""Update the status of an order."""
|
| 75 |
+
query = update(Order).where(Order.id == order_id).values(status=new_status)
|
| 76 |
+
await db.execute(query)
|
| 77 |
+
await db.commit()
|
| 78 |
+
return await get_order_by_id(db, order_id)
|
| 79 |
+
|
| 80 |
+
async def get_orders_by_status(db: AsyncSession, status: OrderStatus) -> List[Order]:
|
| 81 |
+
"""Get all orders with a specific status."""
|
| 82 |
+
result = await db.execute(
|
| 83 |
+
select(Order)
|
| 84 |
+
.options(selectinload(Order.items))
|
| 85 |
+
.where(Order.status == status)
|
| 86 |
+
.order_by(Order.created_at.desc())
|
| 87 |
+
)
|
| 88 |
+
return result.scalars().all()
|
| 89 |
+
|
| 90 |
+
async def get_orders_by_date_range(db: AsyncSession, start_date: str, end_date: str) -> List[Order]:
|
| 91 |
+
"""Get orders within a date range."""
|
| 92 |
+
start = datetime.strptime(start_date, "%Y-%m-%d")
|
| 93 |
+
end = datetime.strptime(end_date, "%Y-%m-%d").replace(hour=23, minute=59, second=59)
|
| 94 |
+
|
| 95 |
+
result = await db.execute(
|
| 96 |
+
select(Order)
|
| 97 |
+
.options(selectinload(Order.items))
|
| 98 |
+
.where(and_(Order.created_at >= start, Order.created_at <= end))
|
| 99 |
+
.order_by(Order.created_at.desc())
|
| 100 |
+
)
|
| 101 |
+
return result.scalars().all()
|
| 102 |
+
|
| 103 |
+
# Payment Operations
|
| 104 |
+
async def create_payment_db(db: AsyncSession, payment: Payment) -> Payment:
|
| 105 |
+
db.add(payment)
|
| 106 |
+
await db.commit()
|
| 107 |
+
await db.refresh(payment)
|
| 108 |
+
return payment
|
| 109 |
+
|
| 110 |
+
async def get_payment_by_id(db: AsyncSession, payment_id: int) -> Optional[Payment]:
|
| 111 |
+
result = await db.execute(select(Payment).where(Payment.id == payment_id))
|
| 112 |
+
return result.scalar_one_or_none()
|
| 113 |
+
|
| 114 |
+
async def update_payment_status(db: AsyncSession, payment_id: int, new_status: PaymentStatus) -> Optional[Payment]:
|
| 115 |
+
"""Update the status of a payment transaction."""
|
| 116 |
+
query = (
|
| 117 |
+
update(Payment)
|
| 118 |
+
.where(Payment.id == payment_id)
|
| 119 |
+
.values(
|
| 120 |
+
status=new_status,
|
| 121 |
+
updated_at=datetime.utcnow()
|
| 122 |
+
)
|
| 123 |
+
)
|
| 124 |
+
await db.execute(query)
|
| 125 |
+
await db.commit()
|
| 126 |
+
return await get_payment_by_id(db, payment_id)
|
| 127 |
+
|
| 128 |
+
# Report Operations
|
| 129 |
+
async def get_sales_by_date_range(db: AsyncSession, start_date: str, end_date: str) -> List[Order]:
|
| 130 |
+
"""Get all sales within a date range."""
|
| 131 |
+
start = datetime.fromisoformat(start_date)
|
| 132 |
+
end = datetime.fromisoformat(end_date).replace(hour=23, minute=59, second=59)
|
| 133 |
+
|
| 134 |
+
result = await db.execute(
|
| 135 |
+
select(Order)
|
| 136 |
+
.options(selectinload(Order.items))
|
| 137 |
+
.where(
|
| 138 |
+
and_(
|
| 139 |
+
Order.created_at >= start,
|
| 140 |
+
Order.created_at <= end,
|
| 141 |
+
Order.status == OrderStatus.COMPLETED
|
| 142 |
+
)
|
| 143 |
+
)
|
| 144 |
+
.order_by(Order.created_at.desc())
|
| 145 |
+
)
|
| 146 |
+
return result.scalars().all()
|
| 147 |
+
|
| 148 |
+
async def get_top_selling_items(db: AsyncSession, date: str, limit: int = 5) -> List[Dict]:
|
| 149 |
+
"""Get top selling items for a specific date."""
|
| 150 |
+
day_start = datetime.fromisoformat(date)
|
| 151 |
+
day_end = day_start.replace(hour=23, minute=59, second=59)
|
| 152 |
+
|
| 153 |
+
result = await db.execute(
|
| 154 |
+
select(
|
| 155 |
+
MenuItem.name,
|
| 156 |
+
func.sum(OrderItem.quantity).label('total_quantity'),
|
| 157 |
+
func.sum(OrderItem.quantity * OrderItem.unit_price).label('total_revenue')
|
| 158 |
+
)
|
| 159 |
+
.join(OrderItem, MenuItem.id == OrderItem.menu_item_id)
|
| 160 |
+
.join(Order, OrderItem.order_id == Order.id)
|
| 161 |
+
.where(
|
| 162 |
+
and_(
|
| 163 |
+
Order.created_at >= day_start,
|
| 164 |
+
Order.created_at <= day_end,
|
| 165 |
+
Order.status == OrderStatus.COMPLETED
|
| 166 |
+
)
|
| 167 |
+
)
|
| 168 |
+
.group_by(MenuItem.id, MenuItem.name)
|
| 169 |
+
.order_by(func.sum(OrderItem.quantity).desc())
|
| 170 |
+
.limit(limit)
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
return [
|
| 174 |
+
{
|
| 175 |
+
"item_name": row[0],
|
| 176 |
+
"quantity_sold": row[1],
|
| 177 |
+
"total_revenue": row[2]
|
| 178 |
+
}
|
| 179 |
+
for row in result.all()
|
| 180 |
+
]
|
| 181 |
+
|
| 182 |
+
async def get_daily_revenue(db: AsyncSession, date: str) -> float:
|
| 183 |
+
"""Get total revenue for a specific date."""
|
| 184 |
+
day_start = datetime.fromisoformat(date)
|
| 185 |
+
day_end = day_start.replace(hour=23, minute=59, second=59)
|
| 186 |
+
|
| 187 |
+
result = await db.execute(
|
| 188 |
+
select(func.sum(Payment.amount))
|
| 189 |
+
.join(Order, Payment.order_id == Order.id)
|
| 190 |
+
.where(
|
| 191 |
+
and_(
|
| 192 |
+
Payment.created_at >= day_start,
|
| 193 |
+
Payment.created_at <= day_end,
|
| 194 |
+
Payment.status == PaymentStatus.COMPLETED
|
| 195 |
+
)
|
| 196 |
+
)
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
total = result.scalar_one_or_none()
|
| 200 |
+
return float(total) if total is not None else 0.0
|
app/db/database.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
| 2 |
+
from sqlalchemy.orm import DeclarativeBase
|
| 3 |
+
from sqlalchemy import text
|
| 4 |
+
from ..core.config import settings
|
| 5 |
+
import logging
|
| 6 |
+
import ssl
|
| 7 |
+
import platform
|
| 8 |
+
import asyncio
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
def get_ssl_context():
|
| 13 |
+
"""Create an SSL context for database connection."""
|
| 14 |
+
ssl_context = ssl.create_default_context()
|
| 15 |
+
ssl_context.check_hostname = False
|
| 16 |
+
ssl_context.verify_mode = ssl.CERT_NONE
|
| 17 |
+
return ssl_context
|
| 18 |
+
|
| 19 |
+
# Configure Windows-specific event loop policy
|
| 20 |
+
if platform.system() == 'Windows':
|
| 21 |
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
| 22 |
+
|
| 23 |
+
# Create async engine with connection pooling
|
| 24 |
+
engine = create_async_engine(
|
| 25 |
+
settings.DATABASE_URL,
|
| 26 |
+
echo=True,
|
| 27 |
+
pool_pre_ping=True,
|
| 28 |
+
pool_size=20,
|
| 29 |
+
max_overflow=10,
|
| 30 |
+
pool_timeout=30,
|
| 31 |
+
connect_args={
|
| 32 |
+
"ssl": get_ssl_context(),
|
| 33 |
+
"server_settings": {
|
| 34 |
+
"application_name": "pos_backend",
|
| 35 |
+
"statement_timeout": "60000", # 60 seconds
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# Create async session maker with pooling configuration
|
| 41 |
+
async_session_maker = async_sessionmaker(
|
| 42 |
+
engine,
|
| 43 |
+
class_=AsyncSession,
|
| 44 |
+
expire_on_commit=False,
|
| 45 |
+
autocommit=False,
|
| 46 |
+
autoflush=False
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
class Base(DeclarativeBase):
|
| 50 |
+
pass
|
| 51 |
+
|
| 52 |
+
async def verify_connection():
|
| 53 |
+
try:
|
| 54 |
+
async with engine.connect() as conn:
|
| 55 |
+
await conn.execute(text("SELECT 1"))
|
| 56 |
+
logger.info("Database connection verified successfully")
|
| 57 |
+
return True
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.error(f"Database connection failed: {str(e)}")
|
| 60 |
+
raise
|
| 61 |
+
|
| 62 |
+
async def get_db():
|
| 63 |
+
async with async_session_maker() as session:
|
| 64 |
+
try:
|
| 65 |
+
await verify_connection()
|
| 66 |
+
yield session
|
| 67 |
+
await session.commit()
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.error(f"Database session error: {str(e)}")
|
| 70 |
+
await session.rollback()
|
| 71 |
+
raise
|
| 72 |
+
finally:
|
| 73 |
+
await session.close()
|
| 74 |
+
|
| 75 |
+
async def init_db():
|
| 76 |
+
try:
|
| 77 |
+
await verify_connection()
|
| 78 |
+
async with engine.begin() as conn:
|
| 79 |
+
await conn.run_sync(Base.metadata.create_all)
|
| 80 |
+
logger.info("Database initialized successfully")
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.error(f"Database initialization failed: {str(e)}")
|
| 83 |
+
raise
|
app/db/models.py
ADDED
|
File without changes
|
app/main.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from fastapi import FastAPI, HTTPException
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.responses import JSONResponse
|
| 7 |
+
from .core.config import settings
|
| 8 |
+
from .api.api_v1.api import api_router
|
| 9 |
+
from .db.database import init_db
|
| 10 |
+
import logging
|
| 11 |
+
import asyncio
|
| 12 |
+
|
| 13 |
+
# Set up Python path
|
| 14 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 15 |
+
sys.path.append(str(BASE_DIR))
|
| 16 |
+
|
| 17 |
+
# Configure logging
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
def create_app() -> FastAPI:
|
| 22 |
+
app = FastAPI(
|
| 23 |
+
title=settings.PROJECT_NAME,
|
| 24 |
+
openapi_url=f"{settings.API_V1_STR}/openapi.json"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Configure CORS
|
| 28 |
+
app.add_middleware(
|
| 29 |
+
CORSMiddleware,
|
| 30 |
+
allow_origins=["*"],
|
| 31 |
+
allow_credentials=True,
|
| 32 |
+
allow_methods=["*"],
|
| 33 |
+
allow_headers=["*"],
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
# Include API router
|
| 37 |
+
app.include_router(api_router, prefix=settings.API_V1_STR)
|
| 38 |
+
|
| 39 |
+
@app.exception_handler(HTTPException)
|
| 40 |
+
async def http_exception_handler(request, exc):
|
| 41 |
+
return JSONResponse(
|
| 42 |
+
status_code=exc.status_code,
|
| 43 |
+
content={"detail": exc.detail}
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
@app.get("/")
|
| 47 |
+
async def root():
|
| 48 |
+
return {"message": "POS Backend API", "version": "1.0.0"}
|
| 49 |
+
|
| 50 |
+
@app.on_event("startup")
|
| 51 |
+
async def startup_event():
|
| 52 |
+
try:
|
| 53 |
+
await init_db()
|
| 54 |
+
logger.info("Database initialized successfully")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
logger.error(f"Failed to initialize database: {e}")
|
| 57 |
+
raise
|
| 58 |
+
|
| 59 |
+
return app
|
| 60 |
+
|
| 61 |
+
app = create_app()
|
| 62 |
+
|
| 63 |
+
if __name__ == "__main__":
|
| 64 |
+
import uvicorn
|
| 65 |
+
|
| 66 |
+
uvicorn.run(
|
| 67 |
+
"app.main:app",
|
| 68 |
+
host="0.0.0.0",
|
| 69 |
+
port=8000,
|
| 70 |
+
reload=True,
|
| 71 |
+
log_level="info"
|
| 72 |
+
)
|
app/models/__pycache__/auth.cpython-312.pyc
ADDED
|
Binary file (1.16 kB). View file
|
|
|
app/models/__pycache__/database.cpython-312.pyc
ADDED
|
Binary file (3.99 kB). View file
|
|
|
app/models/__pycache__/menu.cpython-312.pyc
ADDED
|
Binary file (1.78 kB). View file
|
|
|
app/models/__pycache__/orders.cpython-312.pyc
ADDED
|
Binary file (2.05 kB). View file
|
|
|
app/models/__pycache__/payment.cpython-312.pyc
ADDED
|
Binary file (1.51 kB). View file
|
|
|
app/models/__pycache__/reports.cpython-312.pyc
ADDED
|
Binary file (2.82 kB). View file
|
|
|
app/models/auth.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, EmailStr, Field
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
class LoginRequest(BaseModel):
|
| 5 |
+
email: EmailStr
|
| 6 |
+
password: str = Field(..., min_length=6)
|
| 7 |
+
|
| 8 |
+
class LoginResponse(BaseModel):
|
| 9 |
+
access_token: str
|
| 10 |
+
token_type: str
|
| 11 |
+
user: 'UserProfile'
|
| 12 |
+
|
| 13 |
+
class UserProfile(BaseModel):
|
| 14 |
+
id: Optional[int] = None
|
| 15 |
+
email: EmailStr
|
| 16 |
+
full_name: str
|
| 17 |
+
role: str
|
app/models/database.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime, Boolean, JSON, Enum
|
| 2 |
+
from sqlalchemy.orm import relationship
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from .menu import CategoryType
|
| 5 |
+
from ..db.database import Base
|
| 6 |
+
|
| 7 |
+
class User(Base):
|
| 8 |
+
__tablename__ = "users"
|
| 9 |
+
|
| 10 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 11 |
+
email = Column(String, unique=True, index=True)
|
| 12 |
+
full_name = Column(String)
|
| 13 |
+
hashed_password = Column(String)
|
| 14 |
+
role = Column(String)
|
| 15 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 16 |
+
|
| 17 |
+
class Category(Base):
|
| 18 |
+
__tablename__ = "categories"
|
| 19 |
+
|
| 20 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 21 |
+
name = Column(String, index=True)
|
| 22 |
+
type = Column(Enum(CategoryType))
|
| 23 |
+
description = Column(String, nullable=True)
|
| 24 |
+
items = relationship("MenuItem", back_populates="category")
|
| 25 |
+
|
| 26 |
+
class MenuItem(Base):
|
| 27 |
+
__tablename__ = "menu_items"
|
| 28 |
+
|
| 29 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 30 |
+
name = Column(String, index=True)
|
| 31 |
+
description = Column(String, nullable=True)
|
| 32 |
+
price = Column(Float)
|
| 33 |
+
category_id = Column(Integer, ForeignKey("categories.id"))
|
| 34 |
+
image_url = Column(String, nullable=True)
|
| 35 |
+
is_available = Column(Boolean, default=True)
|
| 36 |
+
allergens = Column(JSON, nullable=True)
|
| 37 |
+
preparation_time = Column(Integer, nullable=True)
|
| 38 |
+
category = relationship("Category", back_populates="items")
|
| 39 |
+
|
| 40 |
+
class Order(Base):
|
| 41 |
+
__tablename__ = "orders"
|
| 42 |
+
|
| 43 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 44 |
+
user_id = Column(Integer, ForeignKey("users.id"))
|
| 45 |
+
total_amount = Column(Float)
|
| 46 |
+
status = Column(String)
|
| 47 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 48 |
+
items = relationship("OrderItem", back_populates="order")
|
| 49 |
+
payment = relationship("Payment", back_populates="order")
|
| 50 |
+
|
| 51 |
+
class OrderItem(Base):
|
| 52 |
+
__tablename__ = "order_items"
|
| 53 |
+
|
| 54 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 55 |
+
order_id = Column(Integer, ForeignKey("orders.id"))
|
| 56 |
+
menu_item_id = Column(Integer, ForeignKey("menu_items.id"))
|
| 57 |
+
quantity = Column(Integer)
|
| 58 |
+
unit_price = Column(Float)
|
| 59 |
+
order = relationship("Order", back_populates="items")
|
| 60 |
+
menu_item = relationship("MenuItem")
|
| 61 |
+
|
| 62 |
+
class Payment(Base):
|
| 63 |
+
__tablename__ = "payments"
|
| 64 |
+
|
| 65 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 66 |
+
order_id = Column(Integer, ForeignKey("orders.id"))
|
| 67 |
+
amount = Column(Float)
|
| 68 |
+
method = Column(String)
|
| 69 |
+
status = Column(String)
|
| 70 |
+
transaction_id = Column(String, nullable=True)
|
| 71 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 72 |
+
order = relationship("Order", back_populates="payment")
|
app/models/menu.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, List
|
| 3 |
+
from decimal import Decimal
|
| 4 |
+
from enum import Enum
|
| 5 |
+
|
| 6 |
+
class CategoryType(str, Enum):
|
| 7 |
+
SOUPS = "soups" # For Egusi, Efo riro, Ogbono, etc.
|
| 8 |
+
SWALLOW = "swallow" # For Pounded yam, Amala, Eba, etc.
|
| 9 |
+
RICE_DISHES = "rice_dishes" # For Jollof rice, Fried rice, Native rice
|
| 10 |
+
PROTEINS = "proteins" # For Suya, Asun, Grilled fish, etc.
|
| 11 |
+
SMALL_CHOPS = "small_chops" # For Puff puff, Samosa, Spring rolls
|
| 12 |
+
PEPPER_SOUP = "pepper_soup" # For various pepper soup varieties
|
| 13 |
+
SIDES = "sides" # For Plantains, Moin moin, etc.
|
| 14 |
+
DRINKS = "drinks" # For Nigerian drinks and beverages
|
| 15 |
+
|
| 16 |
+
class Category(BaseModel):
|
| 17 |
+
id: Optional[int] = None
|
| 18 |
+
name: str
|
| 19 |
+
type: CategoryType
|
| 20 |
+
description: Optional[str] = None
|
| 21 |
+
|
| 22 |
+
class MenuItem(BaseModel):
|
| 23 |
+
id: Optional[int] = None
|
| 24 |
+
name: str
|
| 25 |
+
description: Optional[str] = None
|
| 26 |
+
price: Decimal = Field(..., gt=0)
|
| 27 |
+
category_id: int
|
| 28 |
+
image_url: Optional[str] = None
|
| 29 |
+
is_available: bool = True
|
| 30 |
+
allergens: Optional[List[str]] = None
|
| 31 |
+
preparation_time: Optional[int] = None # in minutes
|
| 32 |
+
calories: Optional[int] = None
|
app/models/orders.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from enum import Enum
|
| 5 |
+
|
| 6 |
+
class OrderStatus(str, Enum):
|
| 7 |
+
PENDING = "pending"
|
| 8 |
+
PREPARING = "preparing"
|
| 9 |
+
READY = "ready"
|
| 10 |
+
DELIVERED = "delivered"
|
| 11 |
+
CANCELLED = "cancelled"
|
| 12 |
+
|
| 13 |
+
class OrderItem(BaseModel):
|
| 14 |
+
menu_item_id: int
|
| 15 |
+
quantity: int
|
| 16 |
+
unit_price: float
|
| 17 |
+
notes: Optional[str] = None
|
| 18 |
+
|
| 19 |
+
class Order(BaseModel):
|
| 20 |
+
id: Optional[int] = None
|
| 21 |
+
customer_id: int
|
| 22 |
+
items: List[OrderItem]
|
| 23 |
+
total_amount: float
|
| 24 |
+
status: OrderStatus
|
| 25 |
+
created_at: datetime
|
| 26 |
+
updated_at: Optional[datetime] = None
|
| 27 |
+
|
| 28 |
+
class OrderDetails(Order):
|
| 29 |
+
payment_status: str
|
| 30 |
+
server_name: str
|
| 31 |
+
table_number: Optional[int] = None
|
| 32 |
+
|
| 33 |
+
class OrderStats(BaseModel):
|
| 34 |
+
total_orders: int
|
| 35 |
+
total_revenue: float
|
| 36 |
+
average_order_value: float
|
| 37 |
+
orders_by_status: dict[OrderStatus, int]
|
app/models/payment.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import Optional
|
| 5 |
+
|
| 6 |
+
class PaymentMethod(str, Enum):
|
| 7 |
+
CASH = "cash"
|
| 8 |
+
CARD = "card"
|
| 9 |
+
|
| 10 |
+
class PaymentStatus(str, Enum):
|
| 11 |
+
PENDING = "pending"
|
| 12 |
+
PROCESSING = "processing"
|
| 13 |
+
COMPLETED = "completed"
|
| 14 |
+
FAILED = "failed"
|
| 15 |
+
|
| 16 |
+
class Payment(BaseModel):
|
| 17 |
+
id: Optional[int] = None
|
| 18 |
+
order_id: int
|
| 19 |
+
amount: float = Field(..., gt=0)
|
| 20 |
+
method: PaymentMethod
|
| 21 |
+
status: PaymentStatus = PaymentStatus.PENDING
|
| 22 |
+
transaction_id: Optional[str] = None
|
| 23 |
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
| 24 |
+
updated_at: Optional[datetime] = None
|
app/models/payments.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from decimal import Decimal
|
| 5 |
+
from enum import Enum
|
| 6 |
+
|
| 7 |
+
class PaymentMethod(str, Enum):
|
| 8 |
+
CASH = "cash"
|
| 9 |
+
CREDIT_CARD = "credit_card"
|
| 10 |
+
DEBIT_CARD = "debit_card"
|
| 11 |
+
MOBILE_PAYMENT = "mobile_payment"
|
| 12 |
+
|
| 13 |
+
class PaymentStatus(str, Enum):
|
| 14 |
+
PENDING = "pending"
|
| 15 |
+
PROCESSING = "processing"
|
| 16 |
+
COMPLETED = "completed"
|
| 17 |
+
FAILED = "failed"
|
| 18 |
+
REFUNDED = "refunded"
|
| 19 |
+
|
| 20 |
+
class PaymentSession(BaseModel):
|
| 21 |
+
id: str
|
| 22 |
+
order_id: int
|
| 23 |
+
amount: Decimal
|
| 24 |
+
payment_method: PaymentMethod
|
| 25 |
+
status: PaymentStatus
|
| 26 |
+
created_at: datetime
|
| 27 |
+
|
| 28 |
+
class PaymentVerification(BaseModel):
|
| 29 |
+
payment_id: str
|
| 30 |
+
verification_code: str
|
| 31 |
+
status: PaymentStatus
|
| 32 |
+
message: Optional[str] = None
|
| 33 |
+
|
| 34 |
+
class Transaction(BaseModel):
|
| 35 |
+
id: str
|
| 36 |
+
order_id: int
|
| 37 |
+
amount: Decimal
|
| 38 |
+
payment_method: PaymentMethod
|
| 39 |
+
status: PaymentStatus
|
| 40 |
+
created_at: datetime
|
| 41 |
+
updated_at: Optional[datetime] = None
|
| 42 |
+
reference_id: Optional[str] = None
|
app/models/reports.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import Dict, List
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from decimal import Decimal
|
| 5 |
+
|
| 6 |
+
class TopSellingItem(BaseModel):
|
| 7 |
+
item_name: str
|
| 8 |
+
quantity_sold: int
|
| 9 |
+
total_revenue: float
|
| 10 |
+
|
| 11 |
+
class DailyReport(BaseModel):
|
| 12 |
+
date: str
|
| 13 |
+
total_sales: int
|
| 14 |
+
total_revenue: float
|
| 15 |
+
top_selling_items: List[TopSellingItem]
|
| 16 |
+
|
| 17 |
+
class DailyBreakdown(BaseModel):
|
| 18 |
+
date: str
|
| 19 |
+
revenue: float
|
| 20 |
+
|
| 21 |
+
class WeeklyReport(BaseModel):
|
| 22 |
+
period: str
|
| 23 |
+
total_sales: int
|
| 24 |
+
daily_breakdown: List[DailyBreakdown]
|
| 25 |
+
|
| 26 |
+
# Future report models
|
| 27 |
+
class SalesReport(BaseModel):
|
| 28 |
+
total_sales: Decimal
|
| 29 |
+
sales_by_category: Dict[str, Decimal]
|
| 30 |
+
sales_by_item: Dict[str, Decimal]
|
| 31 |
+
sales_by_hour: Dict[int, Decimal]
|
| 32 |
+
average_order_value: Decimal
|
| 33 |
+
peak_hours: List[int]
|
| 34 |
+
period_start: datetime
|
| 35 |
+
period_end: datetime
|
| 36 |
+
|
| 37 |
+
class InventoryReport(BaseModel):
|
| 38 |
+
items_in_stock: Dict[str, int]
|
| 39 |
+
low_stock_items: List[str]
|
| 40 |
+
out_of_stock_items: List[str]
|
| 41 |
+
total_inventory_value: Decimal
|
| 42 |
+
last_updated: datetime
|
| 43 |
+
|
| 44 |
+
class PerformanceMetrics(BaseModel):
|
| 45 |
+
order_fulfillment_time: Dict[str, float] # avg time in minutes by order type
|
| 46 |
+
server_performance: Dict[str, Dict[str, float]] # metrics by server
|
| 47 |
+
customer_satisfaction: float
|
| 48 |
+
busy_periods: List[Dict[str, str]]
|
| 49 |
+
period_start: datetime
|
| 50 |
+
period_end: datetime
|
app/routes/auth.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 2 |
+
from fastapi.security import OAuth2PasswordRequestForm
|
| 3 |
+
from ..models.auth import LoginRequest, LoginResponse, UserProfile
|
| 4 |
+
from ..core.auth import get_current_user
|
| 5 |
+
from ..services.auth_service import authenticate_user, create_access_token
|
| 6 |
+
|
| 7 |
+
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
| 8 |
+
|
| 9 |
+
@router.post("/login", response_model=LoginResponse)
|
| 10 |
+
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
| 11 |
+
user = await authenticate_user(form_data.username, form_data.password)
|
| 12 |
+
if not user:
|
| 13 |
+
raise HTTPException(status_code=401, detail="Incorrect email or password")
|
| 14 |
+
access_token = create_access_token(data={"sub": user.email})
|
| 15 |
+
return LoginResponse(access_token=access_token)
|
| 16 |
+
|
| 17 |
+
@router.post("/logout")
|
| 18 |
+
async def logout(current_user = Depends(get_current_user)):
|
| 19 |
+
# TODO: Implement token blacklisting or session invalidation
|
| 20 |
+
return {"message": "Successfully logged out"}
|
| 21 |
+
|
| 22 |
+
@router.get("/profile", response_model=UserProfile)
|
| 23 |
+
async def get_profile(current_user = Depends(get_current_user)):
|
| 24 |
+
return current_user
|
app/routes/menu.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 2 |
+
from typing import List
|
| 3 |
+
from ..models.menu import MenuItem, Category
|
| 4 |
+
from ..core.auth import get_current_user
|
| 5 |
+
from ..services.menu_service import MenuService
|
| 6 |
+
|
| 7 |
+
router = APIRouter(prefix="/api/menu", tags=["menu"])
|
| 8 |
+
|
| 9 |
+
@router.get("", response_model=List[MenuItem])
|
| 10 |
+
async def get_menu_items(
|
| 11 |
+
current_user = Depends(get_current_user),
|
| 12 |
+
menu_service: MenuService = Depends()
|
| 13 |
+
):
|
| 14 |
+
return await menu_service.get_menu_items()
|
| 15 |
+
|
| 16 |
+
@router.post("", response_model=MenuItem)
|
| 17 |
+
async def create_menu_item(
|
| 18 |
+
item: MenuItem,
|
| 19 |
+
current_user = Depends(get_current_user),
|
| 20 |
+
menu_service: MenuService = Depends()
|
| 21 |
+
):
|
| 22 |
+
return await menu_service.create_menu_item(item)
|
| 23 |
+
|
| 24 |
+
@router.put("/{item_id}", response_model=MenuItem)
|
| 25 |
+
async def update_menu_item(
|
| 26 |
+
item_id: int,
|
| 27 |
+
item: MenuItem,
|
| 28 |
+
current_user = Depends(get_current_user),
|
| 29 |
+
menu_service: MenuService = Depends()
|
| 30 |
+
):
|
| 31 |
+
updated_item = await menu_service.update_menu_item(item_id, item)
|
| 32 |
+
if not updated_item:
|
| 33 |
+
raise HTTPException(status_code=404, detail="Menu item not found")
|
| 34 |
+
return updated_item
|
| 35 |
+
|
| 36 |
+
@router.delete("/{item_id}")
|
| 37 |
+
async def delete_menu_item(
|
| 38 |
+
item_id: int,
|
| 39 |
+
current_user = Depends(get_current_user),
|
| 40 |
+
menu_service: MenuService = Depends()
|
| 41 |
+
):
|
| 42 |
+
success = await menu_service.delete_menu_item(item_id)
|
| 43 |
+
if not success:
|
| 44 |
+
raise HTTPException(status_code=404, detail="Menu item not found")
|
| 45 |
+
return {"message": "Menu item deleted successfully"}
|
| 46 |
+
|
| 47 |
+
@router.get("/categories", response_model=List[Category])
|
| 48 |
+
async def get_categories(
|
| 49 |
+
current_user = Depends(get_current_user),
|
| 50 |
+
menu_service: MenuService = Depends()
|
| 51 |
+
):
|
| 52 |
+
return await menu_service.get_categories()
|
app/routes/orders.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 2 |
+
from typing import List
|
| 3 |
+
from ..models.orders import Order, OrderDetails, OrderStats
|
| 4 |
+
from ..core.auth import get_current_user
|
| 5 |
+
from ..services.order_service import OrderService
|
| 6 |
+
|
| 7 |
+
router = APIRouter(prefix="/api/orders", tags=["orders"])
|
| 8 |
+
|
| 9 |
+
@router.get("", response_model=List[Order])
|
| 10 |
+
async def get_orders(
|
| 11 |
+
current_user = Depends(get_current_user),
|
| 12 |
+
order_service: OrderService = Depends()
|
| 13 |
+
):
|
| 14 |
+
return await order_service.get_orders()
|
| 15 |
+
|
| 16 |
+
@router.post("", response_model=Order)
|
| 17 |
+
async def create_order(
|
| 18 |
+
order: Order,
|
| 19 |
+
current_user = Depends(get_current_user),
|
| 20 |
+
order_service: OrderService = Depends()
|
| 21 |
+
):
|
| 22 |
+
return await order_service.create_order(order)
|
| 23 |
+
|
| 24 |
+
@router.get("/{order_id}", response_model=OrderDetails)
|
| 25 |
+
async def get_order(
|
| 26 |
+
order_id: int,
|
| 27 |
+
current_user = Depends(get_current_user),
|
| 28 |
+
order_service: OrderService = Depends()
|
| 29 |
+
):
|
| 30 |
+
order = await order_service.get_order(order_id)
|
| 31 |
+
if not order:
|
| 32 |
+
raise HTTPException(status_code=404, detail="Order not found")
|
| 33 |
+
return order
|
| 34 |
+
|
| 35 |
+
@router.put("/{order_id}", response_model=Order)
|
| 36 |
+
async def update_order(
|
| 37 |
+
order_id: int,
|
| 38 |
+
order: Order,
|
| 39 |
+
current_user = Depends(get_current_user),
|
| 40 |
+
order_service: OrderService = Depends()
|
| 41 |
+
):
|
| 42 |
+
updated_order = await order_service.update_order(order_id, order)
|
| 43 |
+
if not updated_order:
|
| 44 |
+
raise HTTPException(status_code=404, detail="Order not found")
|
| 45 |
+
return updated_order
|
| 46 |
+
|
| 47 |
+
@router.delete("/{order_id}")
|
| 48 |
+
async def delete_order(
|
| 49 |
+
order_id: int,
|
| 50 |
+
current_user = Depends(get_current_user),
|
| 51 |
+
order_service: OrderService = Depends()
|
| 52 |
+
):
|
| 53 |
+
success = await order_service.delete_order(order_id)
|
| 54 |
+
if not success:
|
| 55 |
+
raise HTTPException(status_code=404, detail="Order not found")
|
| 56 |
+
return {"message": "Order deleted successfully"}
|
| 57 |
+
|
| 58 |
+
@router.get("/stats", response_model=OrderStats)
|
| 59 |
+
async def get_order_stats(
|
| 60 |
+
current_user = Depends(get_current_user),
|
| 61 |
+
order_service: OrderService = Depends()
|
| 62 |
+
):
|
| 63 |
+
return await order_service.get_stats()
|