Fred808 commited on
Commit
5111c4b
·
verified ·
1 Parent(s): e0cef91

Upload 77 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +0 -0
  2. __init__.py +3 -0
  3. __pycache__/main.cpython-312.pyc +0 -0
  4. app/__init__.py +3 -0
  5. app/__pycache__/__init__.cpython-312.pyc +0 -0
  6. app/api/__init__.py +1 -0
  7. app/api/__pycache__/__init__.cpython-312.pyc +0 -0
  8. app/api/api_v1/__init__.py +1 -0
  9. app/api/api_v1/__pycache__/__init__.cpython-312.pyc +0 -0
  10. app/api/api_v1/__pycache__/api.cpython-312.pyc +0 -0
  11. app/api/api_v1/api.py +10 -0
  12. app/api/api_v1/endpoints/__init__.py +1 -0
  13. app/api/api_v1/endpoints/__pycache__/__init__.cpython-312.pyc +0 -0
  14. app/api/api_v1/endpoints/__pycache__/auth.cpython-312.pyc +0 -0
  15. app/api/api_v1/endpoints/__pycache__/menu.cpython-312.pyc +0 -0
  16. app/api/api_v1/endpoints/__pycache__/orders.cpython-312.pyc +0 -0
  17. app/api/api_v1/endpoints/__pycache__/payments.cpython-312.pyc +0 -0
  18. app/api/api_v1/endpoints/__pycache__/reports.cpython-312.pyc +0 -0
  19. app/api/api_v1/endpoints/auth.py +13 -0
  20. app/api/api_v1/endpoints/menu.py +19 -0
  21. app/api/api_v1/endpoints/orders.py +36 -0
  22. app/api/api_v1/endpoints/payments.py +27 -0
  23. app/api/api_v1/endpoints/reports.py +36 -0
  24. app/core/__pycache__/config.cpython-312.pyc +0 -0
  25. app/core/auth.py +0 -0
  26. app/core/config.py +15 -0
  27. app/core/database.py +0 -0
  28. app/core/dependencies.py +0 -0
  29. app/db/__pycache__/crud.cpython-312.pyc +0 -0
  30. app/db/__pycache__/database.cpython-312.pyc +0 -0
  31. app/db/crud.py +200 -0
  32. app/db/database.py +83 -0
  33. app/db/models.py +0 -0
  34. app/main.py +72 -0
  35. app/models/__pycache__/auth.cpython-312.pyc +0 -0
  36. app/models/__pycache__/database.cpython-312.pyc +0 -0
  37. app/models/__pycache__/menu.cpython-312.pyc +0 -0
  38. app/models/__pycache__/orders.cpython-312.pyc +0 -0
  39. app/models/__pycache__/payment.cpython-312.pyc +0 -0
  40. app/models/__pycache__/reports.cpython-312.pyc +0 -0
  41. app/models/auth.py +17 -0
  42. app/models/database.py +72 -0
  43. app/models/menu.py +32 -0
  44. app/models/orders.py +37 -0
  45. app/models/payment.py +24 -0
  46. app/models/payments.py +42 -0
  47. app/models/reports.py +50 -0
  48. app/routes/auth.py +24 -0
  49. app/routes/menu.py +52 -0
  50. 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()