Initial commit: Add EAMCO Service API
- Add FastAPI service for managing oil delivery services - Implement service scheduling and management endpoints - Add customer service history tracking - Include database models for services, customers, and auto-delivery - Add authentication and authorization middleware - Configure Docker support for local, dev, and prod environments - Add comprehensive .gitignore for Python projects
This commit is contained in:
123
.gitignore
vendored
Normal file
123
.gitignore
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.idea
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
*.iml
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
venvwindows/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.vscode/
|
||||
|
||||
instance/config.py
|
||||
.idea/
|
||||
/passwords.py
|
||||
|
||||
getnewitems.py
|
||||
helperfunctions/
|
||||
test.py
|
||||
tools/
|
||||
nginx.txt
|
||||
|
||||
# Service-specific settings
|
||||
settings_dev.py
|
||||
settings_local.py
|
||||
settings_prod.py
|
||||
19
Dockerfile.dev
Normal file
19
Dockerfile.dev
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.12-bullseye
|
||||
|
||||
ENV PYTHONFAULTHANDLER=1
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
ENV MODE="DEVELOPMENT"
|
||||
|
||||
RUN mkdir -p /app
|
||||
|
||||
COPY requirements.txt /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
COPY . /app
|
||||
19
Dockerfile.local
Normal file
19
Dockerfile.local
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.12-bullseye
|
||||
|
||||
ENV PYTHONFAULTHANDLER=1
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
ENV MODE="LOCAL"
|
||||
|
||||
RUN mkdir -p /app
|
||||
|
||||
COPY requirements.txt /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
COPY . /app
|
||||
21
Dockerfile.prod
Normal file
21
Dockerfile.prod
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM python:3.12-bullseye
|
||||
|
||||
ENV PYTHONFAULTHANDLER=1
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
ENV MODE="PRODUCTION"
|
||||
|
||||
RUN mkdir -p /app
|
||||
|
||||
COPY requirements.txt /app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip3 install -r requirements.txt
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
COPY . /app
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
37
README.md
Normal file
37
README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# EAMCO Service API
|
||||
|
||||
Service management API for EAMCO oil delivery system.
|
||||
|
||||
## Features
|
||||
|
||||
- Service scheduling and management
|
||||
- Customer service history tracking
|
||||
- Integration with EAMCO office API
|
||||
|
||||
## Setup
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml up --build
|
||||
```
|
||||
|
||||
### Development Environment
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
### Production Environment
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml up --build
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
Once running, visit `/docs` for interactive API documentation.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
See `settings_*.py` files for environment-specific configuration.
|
||||
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# eamco_service app module
|
||||
41
app/auth.py
Normal file
41
app/auth.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import logging
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from database import get_db
|
||||
from app.models.auth import Auth_User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
) -> Auth_User:
|
||||
"""
|
||||
Validates the Bearer token and returns the authenticated user.
|
||||
Raises HTTPException 401 if token is invalid or user not found.
|
||||
"""
|
||||
token = credentials.credentials
|
||||
|
||||
user = db.query(Auth_User).filter(Auth_User.api_key == token).first()
|
||||
|
||||
if not user:
|
||||
logger.warning("Authentication failed: invalid API key")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if user.active != 1:
|
||||
logger.warning(f"Authentication failed: user {user.username} is inactive")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User account is inactive",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return user
|
||||
1
app/constants.py
Normal file
1
app/constants.py
Normal file
@@ -0,0 +1 @@
|
||||
DEFAULT_PAGE_SIZE = 50
|
||||
5
app/models/__init__.py
Normal file
5
app/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Models module
|
||||
from .service import Service_Service, Service_Parts, Service_Plans
|
||||
from .customer import Customer_Customer
|
||||
from .auto import Auto_Delivery
|
||||
from .auth import Auth_User
|
||||
20
app/models/auth.py
Normal file
20
app/models/auth.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, TIMESTAMP
|
||||
from database import Base
|
||||
|
||||
|
||||
class Auth_User(Base):
|
||||
__tablename__ = 'auth_users'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
uuid = Column(String(32))
|
||||
api_key = Column(Text)
|
||||
username = Column(String(40))
|
||||
password_hash = Column(Text)
|
||||
member_since = Column(TIMESTAMP)
|
||||
email = Column(String(350))
|
||||
last_seen = Column(TIMESTAMP)
|
||||
admin = Column(Integer)
|
||||
admin_role = Column(Integer)
|
||||
confirmed = Column(Integer)
|
||||
active = Column(Integer, default=1)
|
||||
28
app/models/auto.py
Normal file
28
app/models/auto.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from sqlalchemy import Column, Integer, String, Numeric, Date
|
||||
from database import Base
|
||||
|
||||
|
||||
class Auto_Delivery(Base):
|
||||
"""Read-only auto delivery model for syncing hot_water_summer."""
|
||||
__tablename__ = 'auto_delivery'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
customer_id = Column(Integer)
|
||||
account_number = Column(String(25))
|
||||
customer_town = Column(String(140))
|
||||
customer_state = Column(Integer)
|
||||
customer_address = Column(String(1000))
|
||||
customer_zip = Column(String(25))
|
||||
customer_full_name = Column(String(250))
|
||||
last_fill = Column(Date)
|
||||
days_since_last_fill = Column(Integer)
|
||||
last_updated = Column(Date)
|
||||
estimated_gallons_left = Column(Numeric(6, 2))
|
||||
estimated_gallons_left_prev_day = Column(Numeric(6, 2))
|
||||
tank_height = Column(String(25))
|
||||
tank_size = Column(String(25))
|
||||
house_factor = Column(Numeric(5, 2))
|
||||
hot_water_summer = Column(Integer)
|
||||
auto_status = Column(Integer)
|
||||
open_ticket_id = Column(Integer)
|
||||
28
app/models/customer.py
Normal file
28
app/models/customer.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from sqlalchemy import Column, Integer, String, TIMESTAMP, Boolean
|
||||
from database import Base
|
||||
|
||||
|
||||
class Customer_Customer(Base):
|
||||
"""Read-only customer model for reference."""
|
||||
__tablename__ = 'customer_customer'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
auth_net_profile_id = Column(String, unique=True, index=True, nullable=True)
|
||||
account_number = Column(String(25))
|
||||
customer_last_name = Column(String(250))
|
||||
customer_first_name = Column(String(250))
|
||||
customer_town = Column(String(140))
|
||||
customer_state = Column(Integer)
|
||||
customer_zip = Column(String(25))
|
||||
customer_first_call = Column(TIMESTAMP)
|
||||
customer_email = Column(String(500))
|
||||
customer_automatic = Column(Integer)
|
||||
customer_phone_number = Column(String(25))
|
||||
customer_home_type = Column(Integer)
|
||||
customer_apt = Column(String(140))
|
||||
customer_address = Column(String(1000))
|
||||
company_id = Column(Integer)
|
||||
customer_latitude = Column(String(250))
|
||||
customer_longitude = Column(String(250))
|
||||
correct_address = Column(Boolean)
|
||||
48
app/models/service.py
Normal file
48
app/models/service.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from sqlalchemy import Column, Integer, String, Numeric, DateTime, Text, Date
|
||||
from database import Base
|
||||
|
||||
|
||||
class Service_Service(Base):
|
||||
__tablename__ = 'service_service'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
customer_id = Column(Integer)
|
||||
customer_name = Column(String(1000))
|
||||
customer_address = Column(String(1000))
|
||||
customer_town = Column(String(140))
|
||||
customer_state = Column(String(140))
|
||||
customer_zip = Column(String(10))
|
||||
# tune-up = 0, no heat = 1, fix = 2, tank = 3, other = 4
|
||||
type_service_call = Column(Integer)
|
||||
when_ordered = Column(DateTime)
|
||||
scheduled_date = Column(DateTime)
|
||||
description = Column(Text)
|
||||
service_cost = Column(Numeric(10, 2), nullable=True)
|
||||
payment_type = Column(Integer, nullable=True)
|
||||
payment_card_id = Column(Integer, nullable=True)
|
||||
payment_status = Column(Integer, nullable=True)
|
||||
|
||||
|
||||
class Service_Parts(Base):
|
||||
__tablename__ = 'service_parts'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
customer_id = Column(Integer)
|
||||
oil_filter = Column(String(100))
|
||||
oil_filter_2 = Column(String(100))
|
||||
oil_nozzle = Column(String(10))
|
||||
oil_nozzle_2 = Column(String(10))
|
||||
hot_water_tank = Column(Integer)
|
||||
|
||||
|
||||
class Service_Plans(Base):
|
||||
__tablename__ = 'service_plans'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
customer_id = Column(Integer)
|
||||
contract_plan = Column(Integer, default=0) # 0=no contract, 1=standard, 2=premium
|
||||
contract_years = Column(Integer, default=1)
|
||||
contract_start_date = Column(Date)
|
||||
1
app/routers/__init__.py
Normal file
1
app/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Routers module
|
||||
579
app/routers/service.py
Normal file
579
app/routers/service.py
Normal file
@@ -0,0 +1,579 @@
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, date, timedelta
|
||||
|
||||
from database import get_db
|
||||
from app.auth import get_current_user
|
||||
from app.models.service import Service_Service, Service_Parts, Service_Plans
|
||||
from app.models.customer import Customer_Customer
|
||||
from app.models.auto import Auto_Delivery
|
||||
from app.models.auth import Auth_User
|
||||
from app.constants import DEFAULT_PAGE_SIZE
|
||||
from app.schema.service import (
|
||||
ServiceResponse, ServiceCreateRequest, ServiceUpdateRequest,
|
||||
ServiceCostUpdateRequest, ServicePartsResponse, ServicePartsUpdateRequest,
|
||||
ServicePlanResponse, ServicePlanCreateRequest, ServicePlanUpdateRequest,
|
||||
CalendarEvent
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/service",
|
||||
tags=["service"],
|
||||
)
|
||||
|
||||
|
||||
def success_response(data: dict = None, status_code: int = 200):
|
||||
"""Standard success response."""
|
||||
response = {"ok": True}
|
||||
if data:
|
||||
response.update(data)
|
||||
return JSONResponse(content=jsonable_encoder(response), status_code=status_code)
|
||||
|
||||
|
||||
def error_response(message: str, status_code: int = 400):
|
||||
"""Standard error response."""
|
||||
return JSONResponse(
|
||||
content={"ok": False, "error": message},
|
||||
status_code=status_code
|
||||
)
|
||||
|
||||
|
||||
def serialize_service(service: Service_Service) -> dict:
|
||||
"""Serialize a service record to dict with proper date formatting."""
|
||||
return {
|
||||
"id": service.id,
|
||||
"customer_id": service.customer_id,
|
||||
"customer_name": service.customer_name,
|
||||
"customer_address": service.customer_address,
|
||||
"customer_town": service.customer_town,
|
||||
"customer_state": service.customer_state,
|
||||
"customer_zip": service.customer_zip,
|
||||
"type_service_call": service.type_service_call,
|
||||
"when_ordered": service.when_ordered.isoformat() if service.when_ordered else None,
|
||||
"scheduled_date": service.scheduled_date.isoformat() if service.scheduled_date else None,
|
||||
"description": service.description,
|
||||
"service_cost": str(service.service_cost) if service.service_cost is not None else None,
|
||||
"payment_type": service.payment_type,
|
||||
"payment_card_id": service.payment_card_id,
|
||||
"payment_status": service.payment_status,
|
||||
}
|
||||
|
||||
|
||||
def serialize_parts(parts: Service_Parts) -> dict:
|
||||
"""Serialize a service parts record to dict."""
|
||||
return {
|
||||
"id": parts.id,
|
||||
"customer_id": parts.customer_id,
|
||||
"oil_filter": parts.oil_filter,
|
||||
"oil_filter_2": parts.oil_filter_2,
|
||||
"oil_nozzle": parts.oil_nozzle,
|
||||
"oil_nozzle_2": parts.oil_nozzle_2,
|
||||
"hot_water_tank": parts.hot_water_tank,
|
||||
}
|
||||
|
||||
|
||||
def serialize_plan(plan: Service_Plans) -> dict:
|
||||
"""Serialize a service plan record to dict."""
|
||||
return {
|
||||
"id": plan.id,
|
||||
"customer_id": plan.customer_id,
|
||||
"contract_plan": plan.contract_plan,
|
||||
"contract_years": plan.contract_years,
|
||||
"contract_start_date": plan.contract_start_date.isoformat() if plan.contract_start_date else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/all", status_code=200)
|
||||
def get_all_service_calls(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all services for calendar view."""
|
||||
logger.info("GET /service/all - Fetching all service calls for calendar")
|
||||
try:
|
||||
all_services = db.query(Service_Service).all()
|
||||
color_map = {
|
||||
0: {"backgroundColor": "blue", "textColor": "white"},
|
||||
1: {"backgroundColor": "red", "textColor": "white"},
|
||||
2: {"backgroundColor": "green", "textColor": "white"},
|
||||
3: {"backgroundColor": "yellow", "textColor": "black"},
|
||||
4: {"backgroundColor": "black", "textColor": "white"}
|
||||
}
|
||||
service_type_map = {0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other'}
|
||||
|
||||
calendar_events = []
|
||||
for service_record in all_services:
|
||||
service_type_text = service_type_map.get(service_record.type_service_call, 'Service')
|
||||
event_title = f"{service_type_text}: {service_record.customer_name}"
|
||||
event_colors = color_map.get(service_record.type_service_call, {"backgroundColor": "gray", "textColor": "white"})
|
||||
|
||||
start_date = service_record.scheduled_date.isoformat() if service_record.scheduled_date else None
|
||||
|
||||
event_data = {
|
||||
"id": service_record.id,
|
||||
"title": event_title,
|
||||
"start": start_date,
|
||||
"end": None,
|
||||
"extendedProps": {
|
||||
"customer_id": service_record.customer_id,
|
||||
"description": service_record.description,
|
||||
"type_service_call": service_record.type_service_call,
|
||||
"service_cost": str(service_record.service_cost) if service_record.service_cost is not None else None
|
||||
},
|
||||
"backgroundColor": event_colors.get("backgroundColor"),
|
||||
"textColor": event_colors.get("textColor"),
|
||||
"borderColor": event_colors.get("backgroundColor")
|
||||
}
|
||||
calendar_events.append(event_data)
|
||||
|
||||
return success_response({"events": calendar_events})
|
||||
except Exception as e:
|
||||
logger.error(f"Error in /service/all: {e}")
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.get("/upcoming", status_code=200)
|
||||
def get_upcoming_service_calls(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Fetches a list of all future service calls from today onwards."""
|
||||
logger.info("GET /service/upcoming - Fetching upcoming service calls")
|
||||
now = datetime.now()
|
||||
upcoming_services = (
|
||||
db.query(Service_Service)
|
||||
.filter(Service_Service.scheduled_date >= now)
|
||||
.order_by(Service_Service.scheduled_date.asc())
|
||||
.limit(DEFAULT_PAGE_SIZE)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = [serialize_service(s) for s in upcoming_services]
|
||||
return success_response({"services": result})
|
||||
|
||||
|
||||
@router.get("/past", status_code=200)
|
||||
def get_past_service_calls(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Fetches a list of all past service calls before today."""
|
||||
past_services = (
|
||||
db.query(Service_Service)
|
||||
.filter(Service_Service.scheduled_date < datetime.combine(date.today(), datetime.min.time()))
|
||||
.order_by(Service_Service.scheduled_date.asc())
|
||||
.limit(DEFAULT_PAGE_SIZE)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = [serialize_service(s) for s in past_services]
|
||||
return success_response({"services": result})
|
||||
|
||||
|
||||
@router.get("/today", status_code=200)
|
||||
def get_today_service_calls(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Fetches a list of all service calls for today."""
|
||||
start_of_today = datetime.combine(date.today(), datetime.min.time())
|
||||
start_of_tomorrow = datetime.combine(date.today() + timedelta(days=1), datetime.min.time())
|
||||
today_services = (
|
||||
db.query(Service_Service)
|
||||
.filter(Service_Service.scheduled_date >= start_of_today)
|
||||
.filter(Service_Service.scheduled_date < start_of_tomorrow)
|
||||
.order_by(Service_Service.scheduled_date.asc())
|
||||
.limit(DEFAULT_PAGE_SIZE)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = [serialize_service(s) for s in today_services]
|
||||
return success_response({"services": result})
|
||||
|
||||
|
||||
@router.get("/upcoming/count", status_code=200)
|
||||
def get_upcoming_service_calls_count(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Get count of upcoming service calls."""
|
||||
now = datetime.now()
|
||||
try:
|
||||
count = db.query(Service_Service).filter(Service_Service.scheduled_date >= now).count()
|
||||
return success_response({"count": count})
|
||||
except Exception as e:
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.get("/for-customer/{customer_id}", status_code=200)
|
||||
def get_service_calls_for_customer(
|
||||
customer_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all service calls for a specific customer."""
|
||||
service_records = (
|
||||
db.query(Service_Service)
|
||||
.filter(Service_Service.customer_id == customer_id)
|
||||
.order_by(Service_Service.scheduled_date.desc())
|
||||
.all()
|
||||
)
|
||||
result = [serialize_service(s) for s in service_records]
|
||||
return success_response({"services": result})
|
||||
|
||||
|
||||
@router.get("/{id}", status_code=200)
|
||||
def get_service_by_id(
|
||||
id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Get a specific service call by ID."""
|
||||
service_record = db.query(Service_Service).filter(Service_Service.id == id).first()
|
||||
if not service_record:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
return success_response({"service": serialize_service(service_record)})
|
||||
|
||||
|
||||
@router.post("/create", status_code=201)
|
||||
async def create_service_call(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Create a new service call."""
|
||||
data = await request.json()
|
||||
if not data:
|
||||
return error_response("No data provided", 400)
|
||||
|
||||
cus_id = data.get('customer_id')
|
||||
get_customer = db.query(Customer_Customer).filter(Customer_Customer.id == cus_id).first()
|
||||
if not get_customer:
|
||||
return error_response(f"Customer with id {cus_id} not found.", 404)
|
||||
|
||||
scheduled_datetime_str = data.get('expected_delivery_date')
|
||||
scheduled_datetime_obj = datetime.fromisoformat(scheduled_datetime_str)
|
||||
|
||||
new_service_call = Service_Service(
|
||||
customer_id=get_customer.id,
|
||||
customer_name=get_customer.customer_first_name + ' ' + get_customer.customer_last_name,
|
||||
customer_address=get_customer.customer_address,
|
||||
customer_town=get_customer.customer_town,
|
||||
customer_state=get_customer.customer_state,
|
||||
customer_zip=get_customer.customer_zip,
|
||||
type_service_call=data.get('type_service_call'),
|
||||
when_ordered=datetime.utcnow(),
|
||||
scheduled_date=scheduled_datetime_obj,
|
||||
description=data.get('description'),
|
||||
service_cost=None,
|
||||
)
|
||||
db.add(new_service_call)
|
||||
db.commit()
|
||||
return success_response({"id": new_service_call.id}, 201)
|
||||
|
||||
|
||||
@router.put("/update/{id}", status_code=200)
|
||||
async def update_service_call(
|
||||
id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Update a service call."""
|
||||
service_record = db.query(Service_Service).filter(Service_Service.id == id).first()
|
||||
if not service_record:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
data = await request.json()
|
||||
if not data:
|
||||
return error_response("No data provided", 400)
|
||||
|
||||
scheduled_datetime_str = data.get('scheduled_date')
|
||||
if scheduled_datetime_str:
|
||||
service_record.scheduled_date = datetime.fromisoformat(scheduled_datetime_str)
|
||||
|
||||
service_record.type_service_call = data.get('type_service_call', service_record.type_service_call)
|
||||
service_record.description = data.get('description', service_record.description)
|
||||
service_record.service_cost = data.get('service_cost', service_record.service_cost)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
return success_response({"service": serialize_service(service_record)})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.put("/update-cost/{id}", status_code=200)
|
||||
async def update_service_cost(
|
||||
id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Dedicated endpoint to update only the service cost for a service call."""
|
||||
try:
|
||||
service_record = db.query(Service_Service).filter(Service_Service.id == id).first()
|
||||
if not service_record:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
data = await request.json()
|
||||
if not data:
|
||||
return error_response("No data provided", 400)
|
||||
|
||||
new_cost = data.get('service_cost')
|
||||
if new_cost is None:
|
||||
return error_response("service_cost is required", 400)
|
||||
|
||||
try:
|
||||
new_cost_float = float(new_cost)
|
||||
except (ValueError, TypeError):
|
||||
return error_response("service_cost must be a valid number", 400)
|
||||
|
||||
service_record.service_cost = new_cost_float
|
||||
db.commit()
|
||||
|
||||
return success_response({
|
||||
"service_id": id,
|
||||
"service_cost_updated": new_cost_float,
|
||||
"message": f"Service {id} cost updated to ${new_cost_float}"
|
||||
})
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating service cost for service {id}: {e}")
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.delete("/delete/{id}", status_code=200)
|
||||
def delete_service_call(
|
||||
id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete a service call."""
|
||||
service_record = db.query(Service_Service).filter(Service_Service.id == id).first()
|
||||
if not service_record:
|
||||
raise HTTPException(status_code=404, detail="Service not found")
|
||||
|
||||
try:
|
||||
db.delete(service_record)
|
||||
db.commit()
|
||||
return success_response({"message": "Service deleted successfully"})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
# Service Parts endpoints
|
||||
|
||||
@router.get("/parts/customer/{customer_id}", status_code=200)
|
||||
def get_service_parts(
|
||||
customer_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Get service parts for a customer."""
|
||||
parts = db.query(Service_Parts).filter(Service_Parts.customer_id == customer_id).first()
|
||||
if parts:
|
||||
return success_response({"parts": serialize_parts(parts)})
|
||||
else:
|
||||
return success_response({"parts": {
|
||||
"customer_id": customer_id,
|
||||
"oil_filter": "",
|
||||
"oil_filter_2": "",
|
||||
"oil_nozzle": "",
|
||||
"oil_nozzle_2": "",
|
||||
"hot_water_tank": 0
|
||||
}})
|
||||
|
||||
|
||||
@router.post("/parts/update/{customer_id}", status_code=200)
|
||||
async def update_service_parts(
|
||||
customer_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Update service parts for a customer."""
|
||||
try:
|
||||
data = await request.json()
|
||||
if not data:
|
||||
return error_response("No data provided", 400)
|
||||
|
||||
get_customer = db.query(Customer_Customer).filter(Customer_Customer.id == customer_id).first()
|
||||
parts = db.query(Service_Parts).filter(Service_Parts.customer_id == customer_id).first()
|
||||
|
||||
if not parts:
|
||||
parts = Service_Parts(customer_id=customer_id)
|
||||
db.add(parts)
|
||||
|
||||
parts.oil_filter = data.get('oil_filter', parts.oil_filter)
|
||||
parts.oil_filter_2 = data.get('oil_filter_2', parts.oil_filter_2)
|
||||
parts.oil_nozzle = data.get('oil_nozzle', parts.oil_nozzle)
|
||||
parts.oil_nozzle_2 = data.get('oil_nozzle_2', parts.oil_nozzle_2)
|
||||
parts.hot_water_tank = data.get('hot_water_tank', parts.hot_water_tank if parts.hot_water_tank is not None else 0)
|
||||
|
||||
# Sync to Auto_Delivery if customer is automatic
|
||||
if get_customer and get_customer.customer_automatic == 1:
|
||||
get_auto = db.query(Auto_Delivery).filter(Auto_Delivery.customer_id == customer_id).first()
|
||||
if get_auto:
|
||||
get_auto.hot_water_summer = parts.hot_water_tank
|
||||
db.add(get_auto)
|
||||
|
||||
db.commit()
|
||||
return success_response({"message": "Service parts updated successfully"})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
# Service Plans endpoints
|
||||
|
||||
@router.get("/plans/active", status_code=200)
|
||||
def get_active_service_plans(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all active service plans (contract_plan > 0)."""
|
||||
try:
|
||||
plans = db.query(Service_Plans).filter(Service_Plans.contract_plan > 0).all()
|
||||
result = []
|
||||
|
||||
for plan in plans:
|
||||
plan_dict = serialize_plan(plan)
|
||||
customer = db.query(Customer_Customer).filter(Customer_Customer.id == plan.customer_id).first()
|
||||
if customer:
|
||||
plan_dict['customer_name'] = f"{customer.customer_first_name} {customer.customer_last_name}"
|
||||
plan_dict['customer_address'] = customer.customer_address
|
||||
plan_dict['customer_town'] = customer.customer_town
|
||||
result.append(plan_dict)
|
||||
|
||||
return success_response({"plans": result})
|
||||
except Exception as e:
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.get("/plans/customer/{customer_id}", status_code=200)
|
||||
def get_customer_service_plan(
|
||||
customer_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Get service plan for a specific customer."""
|
||||
try:
|
||||
plan = db.query(Service_Plans).filter(Service_Plans.customer_id == customer_id).first()
|
||||
if plan:
|
||||
return success_response({"plan": serialize_plan(plan)})
|
||||
else:
|
||||
return success_response({"plan": None})
|
||||
except Exception as e:
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.post("/plans/create", status_code=201)
|
||||
async def create_service_plan(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Create a new service plan for a customer."""
|
||||
data = await request.json()
|
||||
if not data:
|
||||
return error_response("No data provided", 400)
|
||||
|
||||
try:
|
||||
new_plan = Service_Plans(
|
||||
customer_id=data['customer_id'],
|
||||
contract_plan=data['contract_plan'],
|
||||
contract_years=data['contract_years'],
|
||||
contract_start_date=datetime.fromisoformat(data['contract_start_date']).date()
|
||||
)
|
||||
db.add(new_plan)
|
||||
db.commit()
|
||||
|
||||
return success_response({"plan": serialize_plan(new_plan)}, 201)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.put("/plans/update/{customer_id}", status_code=200)
|
||||
async def update_service_plan(
|
||||
customer_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Update existing service plan for a customer."""
|
||||
data = await request.json()
|
||||
if not data:
|
||||
return error_response("No data provided", 400)
|
||||
|
||||
try:
|
||||
plan = db.query(Service_Plans).filter(Service_Plans.customer_id == customer_id).first()
|
||||
if not plan:
|
||||
# Create new plan if it doesn't exist
|
||||
plan = Service_Plans(customer_id=customer_id)
|
||||
db.add(plan)
|
||||
|
||||
plan.contract_plan = data.get('contract_plan', plan.contract_plan)
|
||||
plan.contract_years = data.get('contract_years', plan.contract_years)
|
||||
if data.get('contract_start_date'):
|
||||
plan.contract_start_date = datetime.fromisoformat(data['contract_start_date']).date()
|
||||
|
||||
db.commit()
|
||||
return success_response({"plan": serialize_plan(plan)})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.delete("/plans/delete/{customer_id}", status_code=200)
|
||||
def delete_service_plan(
|
||||
customer_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete service plan for a customer."""
|
||||
try:
|
||||
plan = db.query(Service_Plans).filter(Service_Plans.customer_id == customer_id).first()
|
||||
if not plan:
|
||||
return error_response("Service plan not found", 404)
|
||||
|
||||
db.delete(plan)
|
||||
db.commit()
|
||||
return success_response({"message": "Service plan deleted successfully"})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(str(e), 500)
|
||||
|
||||
|
||||
@router.put("/payment/{service_id}/{payment_type}", status_code=200)
|
||||
def process_service_payment(
|
||||
service_id: int,
|
||||
payment_type: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: Auth_User = Depends(get_current_user)
|
||||
):
|
||||
"""Process payment for a service call."""
|
||||
service = db.query(Service_Service).filter(Service_Service.id == service_id).first()
|
||||
if not service:
|
||||
return error_response("Service not found", 404)
|
||||
|
||||
service.payment_type = payment_type
|
||||
service.payment_status = 2
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
return success_response({})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return error_response(str(e), 500)
|
||||
1
app/schema/__init__.py
Normal file
1
app/schema/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Schema module
|
||||
111
app/schema/service.py
Normal file
111
app/schema/service.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
class ServiceBase(BaseModel):
|
||||
customer_id: Optional[int] = None
|
||||
customer_name: Optional[str] = None
|
||||
customer_address: Optional[str] = None
|
||||
customer_town: Optional[str] = None
|
||||
customer_state: Optional[str] = None
|
||||
customer_zip: Optional[str] = None
|
||||
type_service_call: Optional[int] = None
|
||||
scheduled_date: Optional[datetime] = None
|
||||
description: Optional[str] = None
|
||||
service_cost: Optional[Decimal] = None
|
||||
payment_type: Optional[int] = None
|
||||
payment_card_id: Optional[int] = None
|
||||
payment_status: Optional[int] = None
|
||||
|
||||
|
||||
class ServiceResponse(ServiceBase):
|
||||
id: int
|
||||
when_ordered: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ServiceCreateRequest(BaseModel):
|
||||
customer_id: int
|
||||
expected_delivery_date: str
|
||||
type_service_call: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ServiceUpdateRequest(BaseModel):
|
||||
scheduled_date: Optional[str] = None
|
||||
type_service_call: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
service_cost: Optional[Decimal] = None
|
||||
|
||||
|
||||
class ServiceCostUpdateRequest(BaseModel):
|
||||
service_cost: Decimal
|
||||
|
||||
|
||||
class ServicePartsBase(BaseModel):
|
||||
customer_id: Optional[int] = None
|
||||
oil_filter: Optional[str] = None
|
||||
oil_filter_2: Optional[str] = None
|
||||
oil_nozzle: Optional[str] = None
|
||||
oil_nozzle_2: Optional[str] = None
|
||||
hot_water_tank: Optional[int] = None
|
||||
|
||||
|
||||
class ServicePartsResponse(ServicePartsBase):
|
||||
id: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ServicePartsUpdateRequest(BaseModel):
|
||||
oil_filter: Optional[str] = None
|
||||
oil_filter_2: Optional[str] = None
|
||||
oil_nozzle: Optional[str] = None
|
||||
oil_nozzle_2: Optional[str] = None
|
||||
hot_water_tank: Optional[int] = None
|
||||
|
||||
|
||||
class ServicePlanBase(BaseModel):
|
||||
customer_id: Optional[int] = None
|
||||
contract_plan: Optional[int] = None
|
||||
contract_years: Optional[int] = None
|
||||
contract_start_date: Optional[date] = None
|
||||
|
||||
|
||||
class ServicePlanResponse(ServicePlanBase):
|
||||
id: Optional[int] = None
|
||||
customer_name: Optional[str] = None
|
||||
customer_address: Optional[str] = None
|
||||
customer_town: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ServicePlanCreateRequest(BaseModel):
|
||||
customer_id: int
|
||||
contract_plan: int
|
||||
contract_years: int
|
||||
contract_start_date: str
|
||||
|
||||
|
||||
class ServicePlanUpdateRequest(BaseModel):
|
||||
contract_plan: Optional[int] = None
|
||||
contract_years: Optional[int] = None
|
||||
contract_start_date: Optional[str] = None
|
||||
|
||||
|
||||
class CalendarEvent(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
start: Optional[str] = None
|
||||
end: Optional[str] = None
|
||||
backgroundColor: Optional[str] = None
|
||||
textColor: Optional[str] = None
|
||||
borderColor: Optional[str] = None
|
||||
extendedProps: Optional[dict] = None
|
||||
22
config.py
Normal file
22
config.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
|
||||
|
||||
def load_config(mode=os.environ.get('MODE')):
|
||||
try:
|
||||
if mode == 'PRODUCTION':
|
||||
from settings_prod import ApplicationConfig
|
||||
return ApplicationConfig
|
||||
|
||||
elif mode == 'LOCAL':
|
||||
from settings_local import ApplicationConfig
|
||||
return ApplicationConfig
|
||||
|
||||
elif mode == 'DEVELOPMENT':
|
||||
from settings_dev import ApplicationConfig
|
||||
return ApplicationConfig
|
||||
else:
|
||||
pass
|
||||
|
||||
except ImportError:
|
||||
from settings_local import ApplicationConfig
|
||||
return ApplicationConfig
|
||||
42
database.py
Normal file
42
database.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.engine import URL
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
from config import load_config
|
||||
|
||||
|
||||
ApplicationConfig = load_config()
|
||||
|
||||
url = URL.create(
|
||||
drivername="postgresql",
|
||||
username=ApplicationConfig.POSTGRES_USERNAME,
|
||||
password=ApplicationConfig.POSTGRES_PW,
|
||||
host=ApplicationConfig.POSTGRES_SERVER,
|
||||
database=ApplicationConfig.POSTGRES_DBNAME,
|
||||
port=ApplicationConfig.POSTGRES_PORT
|
||||
)
|
||||
|
||||
engine = create_engine(
|
||||
url,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10,
|
||||
pool_recycle=3600,
|
||||
)
|
||||
|
||||
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Keep the global session for backwards compatibility
|
||||
session = Session()
|
||||
|
||||
Base = declarative_base()
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
def get_db():
|
||||
db = Session()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
116
main.py
Normal file
116
main.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
import sys
|
||||
import uuid
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from config import load_config
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.routers import service
|
||||
|
||||
|
||||
ApplicationConfig = load_config()
|
||||
|
||||
|
||||
# Configure logging
|
||||
def setup_logging():
|
||||
"""Configure structured logging for the application."""
|
||||
log_level = logging.DEBUG if ApplicationConfig.CURRENT_SETTINGS != 'PRODUCTION' else logging.INFO
|
||||
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(log_level)
|
||||
root_logger.handlers.clear()
|
||||
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(log_level)
|
||||
console_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
logging.getLogger('uvicorn.access').setLevel(logging.WARNING)
|
||||
|
||||
return logging.getLogger('eamco_service')
|
||||
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
# Database setup with connection pooling
|
||||
engine = create_engine(
|
||||
ApplicationConfig.SQLALCHEMY_DATABASE_URI,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10,
|
||||
pool_recycle=3600,
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
def check_db_connection():
|
||||
"""Test database connectivity."""
|
||||
try:
|
||||
db = SessionLocal()
|
||||
db.execute(text("SELECT 1"))
|
||||
db.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
# Request ID middleware for request tracking/correlation
|
||||
class RequestIDMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())[:8]
|
||||
request.state.request_id = request_id
|
||||
response = await call_next(request)
|
||||
response.headers["X-Request-ID"] = request_id
|
||||
return response
|
||||
|
||||
|
||||
app.add_middleware(RequestIDMiddleware)
|
||||
|
||||
app.include_router(service.router)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=ApplicationConfig.CORS_ALLOWED_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def health_check():
|
||||
return {"ok": True, "service": "eamco_service"}
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Application startup - log configuration and test DB connection."""
|
||||
logger.info("eamco_service STARTING")
|
||||
mode = ApplicationConfig.CURRENT_SETTINGS.upper()
|
||||
if mode in ['DEVELOPMENT', 'DEV']:
|
||||
logger.info("Mode: Development")
|
||||
elif mode in ['PRODUCTION', 'PROD']:
|
||||
logger.info("WARNING PRODUCTION")
|
||||
# Sanitize DB URI to avoid logging credentials
|
||||
db_uri = ApplicationConfig.SQLALCHEMY_DATABASE_URI
|
||||
if '@' in db_uri:
|
||||
db_uri = db_uri.split('@')[-1]
|
||||
logger.info(f"DB: ...@{db_uri[:50]}")
|
||||
logger.info(f"CORS: {len(ApplicationConfig.CORS_ALLOWED_ORIGINS)} origins configured")
|
||||
|
||||
# Test database connection
|
||||
if check_db_connection():
|
||||
logger.info("DB Connection: OK")
|
||||
else:
|
||||
logger.info("DB Connection: FAILED")
|
||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
# eamco_service dependencies
|
||||
# FastAPI web framework and server
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.34.0
|
||||
|
||||
# Database
|
||||
SQLAlchemy==2.0.40
|
||||
psycopg2-binary==2.9.10
|
||||
|
||||
# Validation
|
||||
pydantic==2.10.4
|
||||
Reference in New Issue
Block a user