first commit
This commit is contained in:
122
.gitignore
vendored
Normal file
122
.gitignore
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
# 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/
|
||||
app/package.json
|
||||
app/package-lock.json
|
||||
.vscode/
|
||||
|
||||
|
||||
instance/config.py
|
||||
.idea/
|
||||
/passwords.py
|
||||
|
||||
getnewitems.py
|
||||
helperfunctions/
|
||||
test.py
|
||||
tools/
|
||||
nginx.txt
|
||||
app/node_modules/
|
||||
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 80
|
||||
|
||||
COPY . /app
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||
0
__init__.py
Normal file
0
__init__.py
Normal file
29
config.py
Normal file
29
config.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import os
|
||||
|
||||
def load_config(mode=os.environ.get('MODE')):
|
||||
"""
|
||||
Load application configuration from environment variables.
|
||||
The MODE environment variable determines which default CORS origins to use,
|
||||
but all secrets are loaded from environment variables.
|
||||
"""
|
||||
from settings import ApplicationConfig
|
||||
|
||||
print(f"Mode is {mode or 'not set (defaulting to DEVELOPMENT)'}")
|
||||
|
||||
# Validate required configuration
|
||||
try:
|
||||
ApplicationConfig.validate_required()
|
||||
except ValueError as e:
|
||||
print(f"\033[91mConfiguration Error: {e}\033[0m")
|
||||
raise
|
||||
|
||||
# Print appropriate warning based on database
|
||||
if ApplicationConfig.POSTGRES_DBNAME00 == 'eamco':
|
||||
print("\033[92mTest database confirmed\033[0m")
|
||||
else:
|
||||
print("\033[91mPRODUCTION DATABASE\033[0m")
|
||||
|
||||
if mode == 'PRODUCTION':
|
||||
print("\033[91mPRODUCTION MODE\033[0m")
|
||||
|
||||
return ApplicationConfig
|
||||
36
database.py
Normal file
36
database.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
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+asyncpg",
|
||||
username=ApplicationConfig.POSTGRES_USERNAME,
|
||||
password=ApplicationConfig.POSTGRES_PW,
|
||||
host=ApplicationConfig.POSTGRES_SERVER,
|
||||
database=ApplicationConfig.POSTGRES_DBNAME00,
|
||||
port=ApplicationConfig.POSTGRES_PORT
|
||||
)
|
||||
|
||||
engine = create_async_engine(url, echo=False)
|
||||
|
||||
AsyncSessionLocal = sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
# Dependency to get DB session
|
||||
async def get_db() -> AsyncSession:
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
43
main.py
Normal file
43
main.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from contextlib import asynccontextmanager
|
||||
from routes import auth, info, order, payment
|
||||
import os
|
||||
from config import load_config
|
||||
from database import engine, Base
|
||||
|
||||
|
||||
ApplicationConfig = load_config()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup: Create database tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
# Shutdown: cleanup if needed
|
||||
|
||||
app = FastAPI(title="Oil Customer Gateway API", version="1.0.0", lifespan=lifespan)
|
||||
|
||||
app.include_router(auth, prefix="/auth", tags=["auth"])
|
||||
app.include_router(info, prefix="/info", tags=["info"])
|
||||
app.include_router(order, prefix="/order", tags=["order"])
|
||||
app.include_router(payment, prefix="/payment", tags=["payment"])
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Oil Customer Gateway API", "status": "Api is online"}
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=ApplicationConfig.origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Mount static files for uploaded images
|
||||
app.mount("/images", StaticFiles(directory="/images"), name="images")
|
||||
15
models/__init__.py
Normal file
15
models/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
def get_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
from .customer import Customer_Customer, Customer_Description, Customer_Tank_Inspection, Customer_Stats
|
||||
from .delivery import Delivery_Delivery
|
||||
from .pricing import Pricing_Oil_Oil
|
||||
from .company import Company_Company
|
||||
from .account import Account_User
|
||||
from .card import Card, Transaction
|
||||
62
models/account.py
Normal file
62
models/account.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from sqlalchemy import Column, Integer, String, TIMESTAMP, TEXT, VARCHAR
|
||||
from datetime import datetime, timezone
|
||||
from . import Base
|
||||
|
||||
class Account_User(Base):
|
||||
__tablename__ = 'portal_user'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, autoincrement=True, primary_key=True, unique=True)
|
||||
username = Column(String(50))
|
||||
account_number = Column(String(32))
|
||||
house_number = Column(String(32))
|
||||
email = Column(VARCHAR(350))
|
||||
password_hash = Column(TEXT)
|
||||
member_since = Column(TIMESTAMP(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
last_seen = Column(TIMESTAMP(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
password_reset_token = Column(TEXT, nullable=True)
|
||||
password_reset_expires = Column(TIMESTAMP(timezone=True), nullable=True)
|
||||
admin = Column(Integer)
|
||||
admin_role = Column(Integer)
|
||||
confirmed = Column(Integer)
|
||||
active = Column(Integer, default=1)
|
||||
user_id = Column(Integer, nullable=True) # References Customer_Customer.id
|
||||
|
||||
def __init__(self,
|
||||
username,
|
||||
account_number,
|
||||
house_number,
|
||||
password_hash,
|
||||
member_since,
|
||||
email,
|
||||
last_seen,
|
||||
admin,
|
||||
admin_role,
|
||||
confirmed,
|
||||
active=1,
|
||||
user_id=None,
|
||||
):
|
||||
self.username = username
|
||||
self.account_number = account_number
|
||||
self.house_number = house_number
|
||||
self.password_hash = password_hash
|
||||
self.member_since = member_since
|
||||
self.email = email
|
||||
self.last_seen = last_seen
|
||||
self.admin = admin
|
||||
self.admin_role = admin_role
|
||||
self.confirmed = confirmed
|
||||
self.active = active
|
||||
self.user_id = user_id
|
||||
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
def is_anonymous(self):
|
||||
return False
|
||||
|
||||
def get_id(self):
|
||||
return str(self.id)
|
||||
46
models/card.py
Normal file
46
models/card.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from sqlalchemy import Column, Integer, String, TIMESTAMP, TEXT, VARCHAR, Numeric, DateTime, Boolean
|
||||
from datetime import datetime, timezone
|
||||
from . import Base
|
||||
|
||||
|
||||
class Card(Base):
|
||||
__tablename__ = "card_card"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
date_added = Column(DateTime, default=datetime.utcnow)
|
||||
user_id = Column(Integer, nullable=False)
|
||||
|
||||
# This stores the payment profile ID for this specific card from Authorize.Net's CIM.
|
||||
auth_net_payment_profile_id = Column(String, nullable=True)
|
||||
|
||||
# Columns to store non-sensitive card info for display purposes
|
||||
card_number = Column(String(50), nullable=True)
|
||||
last_four_digits = Column(Integer, nullable=False)
|
||||
name_on_card = Column(String(500), nullable=True)
|
||||
expiration_month = Column(String(20), nullable=False)
|
||||
expiration_year = Column(String(20), nullable=False)
|
||||
type_of_card = Column(String(50), nullable=True)
|
||||
security_number = Column(String(10), nullable=True)
|
||||
accepted_or_declined = Column(Integer, nullable=True)
|
||||
main_card = Column(Boolean, nullable=True)
|
||||
zip_code = Column(String(20), nullable=True)
|
||||
|
||||
|
||||
class Transaction(Base):
|
||||
__tablename__ = "transactions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
# Recommended change: Use Numeric for precision
|
||||
preauthorize_amount = Column(Numeric(10, 2), nullable=True)
|
||||
charge_amount = Column(Numeric(10, 2), nullable=True)
|
||||
customer_id = Column(Integer)
|
||||
transaction_type = Column(Integer)# 0 = charge, 1 = auth, 2 = capture
|
||||
status = Column(Integer)
|
||||
auth_net_transaction_id = Column(String, unique=True, index=True, nullable=True)
|
||||
service_id = Column(Integer, nullable=True)
|
||||
delivery_id = Column(Integer, nullable=True)
|
||||
auto_id = Column(Integer, nullable=True)
|
||||
card_id = Column(Integer, nullable=True)
|
||||
payment_gateway = Column(Integer, default=1)
|
||||
rejection_reason = Column(String, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
14
models/company.py
Normal file
14
models/company.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DECIMAL, TIMESTAMP, DATE, TEXT, VARCHAR
|
||||
from . import Base
|
||||
|
||||
class Company_Company(Base):
|
||||
__tablename__ = 'company_company'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
|
||||
|
||||
company_dba_name = Column(VARCHAR(250))
|
||||
company_llc_name = Column(VARCHAR(250))
|
||||
company_town = Column(VARCHAR(140))
|
||||
company_state = Column(Integer)
|
||||
company_zip = Column(VARCHAR(25))
|
||||
65
models/customer.py
Normal file
65
models/customer.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DECIMAL, TIMESTAMP, DATE, TEXT, VARCHAR, JSON
|
||||
from . import Base
|
||||
|
||||
class Customer_Customer(Base):
|
||||
__tablename__ = 'customer_customer'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
|
||||
auth_net_profile_id = Column(String, unique=True, index=True, nullable=True)
|
||||
account_number = Column(VARCHAR(25))
|
||||
customer_last_name = Column(VARCHAR(250))
|
||||
customer_first_name = Column(VARCHAR(250))
|
||||
customer_town = Column(VARCHAR(140))
|
||||
customer_state = Column(Integer)
|
||||
customer_zip = Column(VARCHAR(25))
|
||||
customer_first_call = Column(TIMESTAMP())
|
||||
customer_email = Column(VARCHAR(500))
|
||||
customer_automatic = Column(Integer)
|
||||
customer_phone_number = Column(VARCHAR(25))
|
||||
customer_home_type = Column(Integer)
|
||||
customer_apt = Column(VARCHAR(140))
|
||||
customer_address = Column(VARCHAR(1000))
|
||||
company_id = Column(Integer)
|
||||
customer_latitude = Column(VARCHAR(250))
|
||||
customer_longitude = Column(VARCHAR(250))
|
||||
correct_address = Column(Boolean)
|
||||
|
||||
class Customer_Description(Base):
|
||||
__tablename__ = 'customer_description'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
|
||||
customer_id = Column(Integer)
|
||||
account_number = Column(VARCHAR(25))
|
||||
company_id = Column(Integer)
|
||||
fill_location = Column(Integer)
|
||||
description = Column(VARCHAR(2000))
|
||||
|
||||
class Customer_Tank_Inspection(Base):
|
||||
__tablename__ = 'customer_tank'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
|
||||
customer_id = Column(Integer)
|
||||
last_tank_inspection = Column(DATE)
|
||||
tank_status = Column(Boolean)
|
||||
outside_or_inside = Column(Boolean)
|
||||
tank_size = Column(Integer)
|
||||
tank_images = Column(Integer, default=0) # Number of image sets uploaded (each set = 3 images)
|
||||
tank_image_upload_dates = Column(JSON, default=list) # List of upload dates for each set
|
||||
|
||||
class Customer_Stats(Base):
|
||||
__tablename__ = 'stats_customer'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
|
||||
customer_id = Column(Integer)
|
||||
total_calls = Column(Integer, default=0)
|
||||
service_calls_total = Column(Integer, default=0)
|
||||
service_calls_total_spent = Column(DECIMAL(6, 2), default=0.00)
|
||||
service_calls_total_profit = Column(DECIMAL(6, 2), default=0.00)
|
||||
oil_deliveries = Column(Integer, default=0)
|
||||
oil_total_gallons = Column(DECIMAL(6, 2), default=0.00)
|
||||
oil_total_spent = Column(DECIMAL(6, 2), default=0.00)
|
||||
oil_total_profit = Column(DECIMAL(6, 2), default=0.00)
|
||||
77
models/delivery.py
Normal file
77
models/delivery.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DECIMAL, TIMESTAMP, DATE, TEXT, VARCHAR
|
||||
from . import Base
|
||||
|
||||
class Delivery_Delivery(Base):
|
||||
__tablename__ = 'delivery_delivery'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
|
||||
|
||||
customer_id = Column(Integer)
|
||||
customer_name = Column(VARCHAR(1000))
|
||||
customer_address = Column(VARCHAR(1000))
|
||||
customer_town = Column(VARCHAR(140))
|
||||
customer_state = Column(VARCHAR(140))
|
||||
customer_zip = Column(Integer)
|
||||
# how many gallons ordered
|
||||
gallons_ordered = Column(Integer)
|
||||
# if customer asked for a fill
|
||||
customer_asked_for_fill = Column(Integer)
|
||||
# integer value if delivered, waiting, cancelled etc
|
||||
gallons_delivered = Column(DECIMAL(6, 2))
|
||||
# if customer has a full tank
|
||||
customer_filled = Column(Integer)
|
||||
# integer value if delivered, waiting, cancelled etc
|
||||
# waiting = 0
|
||||
# cancelled = 1
|
||||
# out for delivery = 2
|
||||
# tommorrow = 3
|
||||
# issue = 5
|
||||
# finalized = 10
|
||||
|
||||
delivery_status = Column(Integer)
|
||||
|
||||
# when the call to order took place
|
||||
when_ordered = Column(DATE(), default=None)
|
||||
# when the delivery date happened
|
||||
when_delivered = Column(DATE(), default=None)
|
||||
# when the delivery is expected ie what day
|
||||
expected_delivery_date = Column(DATE(), default=None)
|
||||
# automatic delivery
|
||||
automatic = Column(Integer)
|
||||
automatic_id = Column(Integer)
|
||||
# OIL info and id from table
|
||||
oil_id = Column(Integer)
|
||||
supplier_price = Column(DECIMAL(6, 2))
|
||||
customer_price = Column(DECIMAL(6, 2))
|
||||
# weather
|
||||
customer_temperature = Column(DECIMAL(6, 2))
|
||||
|
||||
dispatcher_notes = Column(TEXT())
|
||||
|
||||
|
||||
prime = Column(Integer)
|
||||
same_day = Column(Integer)
|
||||
emergency = Column(Integer)
|
||||
|
||||
# cash = 0
|
||||
# credit = 1
|
||||
# credit/cash = 2
|
||||
# check = 3
|
||||
# other = 4
|
||||
payment_type = Column(Integer)
|
||||
payment_card_id = Column(Integer)
|
||||
cash_recieved = Column(DECIMAL(6, 2))
|
||||
|
||||
driver_employee_id = Column(Integer)
|
||||
driver_first_name = Column(VARCHAR(140))
|
||||
driver_last_name = Column(VARCHAR(140))
|
||||
|
||||
pre_charge_amount = Column(DECIMAL(6, 2))
|
||||
total_price = Column(DECIMAL(6, 2))
|
||||
final_price = Column(DECIMAL(6, 2))
|
||||
check_number = Column(VARCHAR(20))
|
||||
|
||||
|
||||
promo_id = Column(Integer)
|
||||
promo_money_discount = Column(DECIMAL(6, 2))
|
||||
17
models/pricing.py
Normal file
17
models/pricing.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DECIMAL, TIMESTAMP, DATE, TEXT, VARCHAR
|
||||
from datetime import datetime
|
||||
from . import Base
|
||||
|
||||
class Pricing_Oil_Oil(Base):
|
||||
__tablename__ = 'pricing_oil_oil'
|
||||
__table_args__ = {"schema": "public"}
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, unique=False)
|
||||
|
||||
price_from_supplier = Column(DECIMAL(6, 2))
|
||||
price_for_customer = Column(DECIMAL(6, 2))
|
||||
price_for_employee = Column(DECIMAL(6, 2))
|
||||
price_same_day = Column(DECIMAL(6, 2))
|
||||
price_prime = Column(DECIMAL(6, 2))
|
||||
price_emergency = Column(DECIMAL(6, 2))
|
||||
date = Column(TIMESTAMP(), default=datetime.utcnow())
|
||||
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
alembic==1.13.1
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
asyncpg==0.29.0
|
||||
psycopg2-binary==2.9.9
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
Pillow==10.1.0
|
||||
requests==2.31.0
|
||||
4
routes/__init__.py
Normal file
4
routes/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .auth import router as auth
|
||||
from .info import router as info
|
||||
from .order import router as order
|
||||
from .payment import router as payment
|
||||
13
routes/auth/__init__.py
Normal file
13
routes/auth/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from fastapi import APIRouter
|
||||
from .login import router as login_router
|
||||
from .register import router as register_router
|
||||
from .new import router as new_router
|
||||
from .current_user import router as current_user_router, oauth2_scheme
|
||||
from .lost_password import router as lost_password_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(login_router)
|
||||
router.include_router(register_router)
|
||||
router.include_router(new_router)
|
||||
router.include_router(current_user_router)
|
||||
router.include_router(lost_password_router)
|
||||
191
routes/auth/current_user.py
Normal file
191
routes/auth/current_user.py
Normal file
@@ -0,0 +1,191 @@
|
||||
from fastapi import Depends, HTTPException, status, APIRouter
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
import logging
|
||||
from database import get_db
|
||||
from models import Account_User, Customer_Customer, Customer_Description, Card
|
||||
from schemas import TokenData, CustomerUpdate
|
||||
from jose import JWTError, jwt
|
||||
from config import load_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load JWT configuration from environment
|
||||
ApplicationConfig = load_config()
|
||||
SECRET_KEY = ApplicationConfig.JWT_SECRET_KEY
|
||||
ALGORITHM = ApplicationConfig.JWT_ALGORITHM
|
||||
|
||||
async def get_current_user(token: str = Depends(OAuth2PasswordBearer(tokenUrl="auth/login")), db: AsyncSession = Depends(get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(username=username)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = await db.execute(select(Account_User).where(Account_User.username == token_data.username))
|
||||
user = user.scalar_one_or_none()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/me")
|
||||
async def read_users_me(current_user: Account_User = Depends(get_current_user), db: AsyncSession = Depends(get_db)):
|
||||
# Get customer details for the current user using user_id
|
||||
customer = None
|
||||
if current_user.user_id:
|
||||
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.id == current_user.user_id))
|
||||
customer = customer_result.scalar_one_or_none()
|
||||
|
||||
# Get house description if exists
|
||||
house_description = None
|
||||
if customer:
|
||||
description_record = await db.execute(
|
||||
select(Customer_Description).where(Customer_Description.customer_id == customer.id)
|
||||
)
|
||||
description_record = description_record.scalar_one_or_none()
|
||||
if description_record:
|
||||
house_description = description_record.description
|
||||
|
||||
# Map state code to name
|
||||
state_mapping = {0: "MA", 1: "NH"} # Add more as needed
|
||||
state_name = state_mapping.get(customer.customer_state, str(customer.customer_state)) if customer else None
|
||||
|
||||
return {
|
||||
"id": current_user.id,
|
||||
"username": current_user.username,
|
||||
"email": current_user.email,
|
||||
"account_number": current_user.account_number,
|
||||
"customer_first_name": customer.customer_first_name if customer else None,
|
||||
"customer_last_name": customer.customer_last_name if customer else None,
|
||||
"customer_address": customer.customer_address if customer else None,
|
||||
"customer_apt": customer.customer_apt if customer else None,
|
||||
"customer_town": customer.customer_town if customer else None,
|
||||
"customer_state": state_name,
|
||||
"customer_zip": customer.customer_zip if customer else None,
|
||||
"customer_phone_number": customer.customer_phone_number if customer else None,
|
||||
"customer_home_type": customer.customer_home_type if customer else None,
|
||||
"customer_email": customer.customer_email if customer else None,
|
||||
"house_description": house_description,
|
||||
"member_since": current_user.member_since,
|
||||
"last_seen": current_user.last_seen
|
||||
}
|
||||
|
||||
@router.put("/update-customer")
|
||||
async def update_customer_info(
|
||||
customer_data: CustomerUpdate,
|
||||
current_user: Account_User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if not current_user.user_id:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
try:
|
||||
# Update Customer_Customer table
|
||||
customer_update_data = customer_data.dict(exclude_unset=True)
|
||||
customer_fields = {
|
||||
'customer_first_name', 'customer_last_name', 'customer_address', 'customer_apt',
|
||||
'customer_town', 'customer_state', 'customer_zip', 'customer_phone_number',
|
||||
'customer_home_type', 'customer_email'
|
||||
}
|
||||
|
||||
customer_update_dict = {k: v for k, v in customer_update_data.items() if k in customer_fields}
|
||||
|
||||
if customer_update_dict:
|
||||
await db.execute(
|
||||
update(Customer_Customer)
|
||||
.where(Customer_Customer.id == current_user.user_id)
|
||||
.values(**customer_update_dict)
|
||||
)
|
||||
|
||||
# Update house description if provided
|
||||
if 'house_description' in customer_update_data and customer_update_data['house_description']:
|
||||
# Get customer record
|
||||
customer = await db.execute(
|
||||
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
|
||||
)
|
||||
customer = customer.scalar_one_or_none()
|
||||
|
||||
if customer:
|
||||
# Check if description record exists
|
||||
description_record = await db.execute(
|
||||
select(Customer_Description).where(Customer_Description.customer_id == customer.id)
|
||||
)
|
||||
description_record = description_record.scalar_one_or_none()
|
||||
|
||||
if description_record:
|
||||
# Update existing
|
||||
await db.execute(
|
||||
update(Customer_Description)
|
||||
.where(Customer_Description.customer_id == customer.id)
|
||||
.values(description=customer_update_data['house_description'])
|
||||
)
|
||||
else:
|
||||
# Create new
|
||||
new_description = Customer_Description(
|
||||
customer_id=customer.id,
|
||||
account_number=current_user.account_number,
|
||||
company_id=customer.company_id or 1,
|
||||
fill_location=0,
|
||||
description=customer_update_data['house_description']
|
||||
)
|
||||
db.add(new_description)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Sync billing info to Authorize.net if address-related fields were updated
|
||||
billing_fields = {
|
||||
'customer_first_name', 'customer_last_name', 'customer_address',
|
||||
'customer_town', 'customer_state', 'customer_zip', 'customer_phone_number',
|
||||
'customer_email'
|
||||
}
|
||||
if any(k in customer_update_dict for k in billing_fields):
|
||||
# Late import to avoid circular dependency
|
||||
from routes.payment.routes import update_customer_profile, update_payment_profile_billing
|
||||
|
||||
# Refetch the updated customer
|
||||
customer_result = await db.execute(
|
||||
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
|
||||
)
|
||||
updated_customer = customer_result.scalar_one_or_none()
|
||||
|
||||
if updated_customer and updated_customer.auth_net_profile_id:
|
||||
# Update customer profile in Authorize.net
|
||||
update_customer_profile(updated_customer.auth_net_profile_id, updated_customer)
|
||||
|
||||
# Update all saved payment profiles with new billing info
|
||||
cards_result = await db.execute(
|
||||
select(Card).where(Card.user_id == updated_customer.id)
|
||||
)
|
||||
cards = cards_result.scalars().all()
|
||||
|
||||
for card in cards:
|
||||
if card.auth_net_payment_profile_id:
|
||||
update_payment_profile_billing(
|
||||
customer_profile_id=updated_customer.auth_net_profile_id,
|
||||
payment_profile_id=card.auth_net_payment_profile_id,
|
||||
customer=updated_customer,
|
||||
card=card
|
||||
)
|
||||
|
||||
logger.info(f"Synced billing info to Authorize.net for customer {updated_customer.id}")
|
||||
|
||||
return {"message": "Customer information updated successfully"}
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update customer information: {str(e)}"
|
||||
)
|
||||
60
routes/auth/login.py
Normal file
60
routes/auth/login.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from database import get_db
|
||||
from models import Account_User
|
||||
from schemas import UserLogin, Token
|
||||
from passlib.context import CryptContext
|
||||
from jose import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from config import load_config
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
# Load JWT configuration from environment
|
||||
ApplicationConfig = load_config()
|
||||
SECRET_KEY = ApplicationConfig.JWT_SECRET_KEY
|
||||
ALGORITHM = ApplicationConfig.JWT_ALGORITHM
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = ApplicationConfig.JWT_ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def create_access_token(data: dict, expires_delta = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
async def authenticate_user(db: AsyncSession, email: str, password: str):
|
||||
user = await db.execute(select(Account_User).where(Account_User.email == email))
|
||||
user = user.scalar_one_or_none()
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.password_hash):
|
||||
return False
|
||||
# Update last_seen
|
||||
user.last_seen = datetime.utcnow()
|
||||
await db.commit()
|
||||
return user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(user: UserLogin, db: AsyncSession = Depends(get_db)):
|
||||
user_obj = await authenticate_user(db, user.email, user.password)
|
||||
if not user_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user_obj.username}, expires_delta=access_token_expires
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
126
routes/auth/lost_password.py
Normal file
126
routes/auth/lost_password.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import secrets
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from database import get_db
|
||||
from models import Account_User
|
||||
from schemas import ForgotPasswordRequest, ResetPasswordRequest
|
||||
from passlib.context import CryptContext
|
||||
from config import load_config
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
# Load configuration from environment
|
||||
ApplicationConfig = load_config()
|
||||
|
||||
# Email configuration from environment variables
|
||||
SMTP_SERVER = ApplicationConfig.SMTP_SERVER
|
||||
SMTP_PORT = ApplicationConfig.SMTP_PORT
|
||||
SMTP_USERNAME = ApplicationConfig.SMTP_USERNAME
|
||||
SMTP_PASSWORD = ApplicationConfig.SMTP_PASSWORD
|
||||
FROM_EMAIL = ApplicationConfig.SMTP_FROM_EMAIL
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def generate_reset_token():
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
def send_reset_email(to_email: str, reset_token: str):
|
||||
"""Send password reset email with link"""
|
||||
reset_url = f"{ApplicationConfig.FRONTEND_URL}/reset-password?token={reset_token}"
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = FROM_EMAIL
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = "Password Reset - Oil Customer Gateway"
|
||||
|
||||
body = f"""
|
||||
You have requested a password reset for your Oil Customer Gateway account.
|
||||
|
||||
Click the following link to reset your password:
|
||||
{reset_url}
|
||||
|
||||
This link will expire in 24 hours.
|
||||
|
||||
If you didn't request this reset, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
Oil Customer Gateway Team
|
||||
"""
|
||||
|
||||
msg.attach(MIMEText(body, 'plain'))
|
||||
|
||||
try:
|
||||
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
|
||||
server.starttls()
|
||||
server.login(SMTP_USERNAME, SMTP_PASSWORD)
|
||||
text = msg.as_string()
|
||||
server.sendmail(FROM_EMAIL, to_email, text)
|
||||
server.quit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Email sending failed: {e}")
|
||||
return False
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Request password reset - sends email with reset link"""
|
||||
# Find user by email
|
||||
result = await db.execute(select(Account_User).where(Account_User.email == request.email))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
# Don't reveal if email exists or not for security
|
||||
return {"message": "If an account with that email exists, a password reset link has been sent."}
|
||||
|
||||
# Generate reset token and expiry (24 hours)
|
||||
reset_token = generate_reset_token()
|
||||
reset_expires = datetime.utcnow() + timedelta(hours=24)
|
||||
|
||||
# Update user with reset token
|
||||
user.password_reset_token = reset_token
|
||||
user.password_reset_expires = reset_expires
|
||||
await db.commit()
|
||||
|
||||
# Send email
|
||||
if send_reset_email(user.email, reset_token):
|
||||
return {"message": "Password reset link sent to your email."}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Failed to send reset email")
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(request: ResetPasswordRequest, db: AsyncSession = Depends(get_db)):
|
||||
"""Reset password using token"""
|
||||
# Verify passwords match
|
||||
if request.password != request.confirm_password:
|
||||
raise HTTPException(status_code=400, detail="Passwords do not match")
|
||||
|
||||
# Find user by reset token
|
||||
result = await db.execute(
|
||||
select(Account_User).where(
|
||||
Account_User.password_reset_token == request.token,
|
||||
Account_User.password_reset_expires > datetime.utcnow()
|
||||
)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=400, detail="Invalid or expired reset token")
|
||||
|
||||
# Update password
|
||||
hashed_password = get_password_hash(request.password)
|
||||
user.password_hash = hashed_password
|
||||
user.password_reset_token = None
|
||||
user.password_reset_expires = None
|
||||
user.last_seen = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Password reset successfully"}
|
||||
588
routes/auth/new.py
Normal file
588
routes/auth/new.py
Normal file
@@ -0,0 +1,588 @@
|
||||
import random
|
||||
import string
|
||||
import os
|
||||
import shutil
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from database import get_db
|
||||
from models import Customer_Customer, Customer_Description, Customer_Tank_Inspection, Customer_Stats, Account_User
|
||||
from schemas import NewCustomerCreate, UserResponse, CustomerCreateStep1, CustomerAccountCreate
|
||||
from passlib.context import CryptContext
|
||||
from datetime import datetime
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
def generate_random_number_string(length):
|
||||
if length < 1:
|
||||
raise ValueError("Length must be at least 1")
|
||||
random_number = ''.join(random.choices(string.digits, k=length))
|
||||
return random_number
|
||||
|
||||
def get_password_hash(password):
|
||||
# Truncate password to 72 bytes max for bcrypt compatibility
|
||||
truncated_password = password.encode('utf-8')[:72].decode('utf-8', errors='ignore')
|
||||
return pwd_context.hash(truncated_password)
|
||||
|
||||
def resize_image(image_data, max_size=(1024, 1024), quality=85):
|
||||
"""
|
||||
Resize image to fit within max_size while maintaining aspect ratio.
|
||||
Convert to JPEG format and strip metadata for security.
|
||||
"""
|
||||
try:
|
||||
# Open image from bytes
|
||||
img = Image.open(io.BytesIO(image_data))
|
||||
|
||||
# Convert to RGB if necessary (for PNG with transparency, etc.)
|
||||
if img.mode in ('RGBA', 'LA', 'P'):
|
||||
img = img.convert('RGB')
|
||||
|
||||
# Resize if larger than max_size
|
||||
if img.width > max_size[0] or img.height > max_size[1]:
|
||||
img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
||||
|
||||
# Save as JPEG with specified quality
|
||||
output = io.BytesIO()
|
||||
img.save(output, format='JPEG', quality=quality, optimize=True)
|
||||
output.seek(0)
|
||||
|
||||
return output.getvalue()
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/new", response_model=UserResponse)
|
||||
async def register_new_customer(customer: NewCustomerCreate, db: AsyncSession = Depends(get_db)):
|
||||
# Verify passwords match
|
||||
if customer.password != customer.confirm_password:
|
||||
raise HTTPException(status_code=400, detail="Passwords do not match")
|
||||
|
||||
# Check if email already registered
|
||||
result = await db.execute(select(Account_User).where(Account_User.email == customer.customer_email))
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
while True:
|
||||
random_part = generate_random_number_string(6)
|
||||
account_number = 'AO-' + random_part
|
||||
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
|
||||
existing_customer = result.scalar_one_or_none()
|
||||
if not existing_customer:
|
||||
break
|
||||
|
||||
# Create customer
|
||||
customer_data = customer.model_dump()
|
||||
customer_data.pop('password')
|
||||
customer_data.pop('confirm_password')
|
||||
customer_data.update({
|
||||
'account_number': account_number,
|
||||
'customer_state': 1, # Default
|
||||
'customer_automatic': 0,
|
||||
'company_id': 1, # Default
|
||||
'customer_latitude': '0',
|
||||
'customer_longitude': '0',
|
||||
'correct_address': True,
|
||||
'customer_first_call': datetime.utcnow()
|
||||
})
|
||||
db_customer = Customer_Customer(**customer_data)
|
||||
db.add(db_customer)
|
||||
await db.commit()
|
||||
await db.refresh(db_customer)
|
||||
|
||||
# Extract house number from customer address (first part before space)
|
||||
house_number = customer.customer_address.split()[0] if customer.customer_address else ''
|
||||
|
||||
# Create account user
|
||||
username = account_number
|
||||
hashed_password = get_password_hash(customer.password)
|
||||
db_user = Account_User(
|
||||
username=username,
|
||||
account_number=account_number,
|
||||
house_number=house_number,
|
||||
password_hash=hashed_password,
|
||||
member_since=datetime.utcnow(),
|
||||
email=customer.customer_email,
|
||||
last_seen=datetime.utcnow(),
|
||||
admin=0,
|
||||
admin_role=0,
|
||||
confirmed=1,
|
||||
active=1,
|
||||
user_id=db_customer.id
|
||||
)
|
||||
db.add(db_user)
|
||||
await db.commit()
|
||||
await db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
@router.post("/step3")
|
||||
async def upload_tank_images(
|
||||
account_number: str = Form(...),
|
||||
tank_image_1: UploadFile = File(...),
|
||||
tank_image_2: UploadFile = File(...),
|
||||
tank_image_3: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
print("=== STEP3 DEBUG START ===")
|
||||
print(f"Account number received: '{account_number}'")
|
||||
|
||||
# Debug: Check all parameters received
|
||||
images = [tank_image_1, tank_image_2, tank_image_3]
|
||||
for i, image in enumerate(images, 1):
|
||||
print(f"Image {i}: filename='{image.filename}', content_type='{image.content_type}', size={image.size}")
|
||||
|
||||
# Validate account number
|
||||
if not account_number:
|
||||
print("ERROR: Account number is empty")
|
||||
raise HTTPException(status_code=400, detail="Account number is required")
|
||||
|
||||
# Get customer info for description record
|
||||
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
|
||||
customer = customer_result.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=400, detail="Customer not found")
|
||||
|
||||
print(f"Creating directory: /images/{account_number}")
|
||||
# Create directory for account number in the mounted images volume
|
||||
account_dir = f"/images/{account_number}"
|
||||
os.makedirs(account_dir, exist_ok=True)
|
||||
|
||||
current_datetime = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") # YYYY-MM-DD_HH-MM-SS format
|
||||
|
||||
# Track if images were uploaded successfully
|
||||
images_uploaded_successfully = False
|
||||
|
||||
# Validate and save images
|
||||
saved_files = []
|
||||
|
||||
try:
|
||||
for i, image in enumerate(images, 1):
|
||||
print(f"Processing image {i}...")
|
||||
# Read image data
|
||||
image_data = await image.read()
|
||||
print(f"Image {i} data read: {len(image_data)} bytes")
|
||||
|
||||
# Validate file size (max 20MB before processing)
|
||||
if len(image_data) > 20 * 1024 * 1024:
|
||||
print(f"ERROR: Image {i} too large: {len(image_data)} bytes")
|
||||
raise HTTPException(status_code=400, detail=f"File {i} is too large (max 20MB)")
|
||||
|
||||
# Resize and process image (this also validates it's a valid image)
|
||||
print(f"Resizing image {i}...")
|
||||
processed_image_data = resize_image(image_data, max_size=(1024, 1024), quality=85)
|
||||
print(f"Image {i} resized: {len(processed_image_data)} bytes")
|
||||
|
||||
# Save processed image with datetime-based filename
|
||||
filename = f"{current_datetime}-{i}.jpg"
|
||||
file_path = os.path.join(account_dir, filename)
|
||||
print(f"Saving image {i} to: {file_path}")
|
||||
|
||||
with open(file_path, "wb") as buffer:
|
||||
buffer.write(processed_image_data)
|
||||
|
||||
saved_files.append(filename)
|
||||
print(f"Image {i} saved successfully")
|
||||
|
||||
images_uploaded_successfully = True
|
||||
print(f"All images processed successfully. Saved files: {saved_files}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR processing images: {str(e)}")
|
||||
# Don't raise exception - we want to track the failure in the database
|
||||
images_uploaded_successfully = False
|
||||
|
||||
# Update or create customer tank inspection record with tank_images status
|
||||
print("Updating customer tank inspection record...")
|
||||
tank_result = await db.execute(
|
||||
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
|
||||
)
|
||||
existing_tank = tank_result.scalar_one_or_none()
|
||||
|
||||
if existing_tank:
|
||||
# Update existing record
|
||||
if images_uploaded_successfully:
|
||||
existing_tank.tank_images += 1 # Increment count of image sets
|
||||
# Append current datetime to upload dates list
|
||||
current_dates = existing_tank.tank_image_upload_dates or []
|
||||
current_dates.append(current_datetime)
|
||||
existing_tank.tank_image_upload_dates = current_dates
|
||||
await db.commit()
|
||||
print(f"Updated existing tank inspection record with tank_images = {existing_tank.tank_images}, dates = {existing_tank.tank_image_upload_dates}")
|
||||
else:
|
||||
# Create new record
|
||||
new_tank = Customer_Tank_Inspection(
|
||||
customer_id=customer.id,
|
||||
tank_images=1 if images_uploaded_successfully else 0,
|
||||
tank_image_upload_dates=[current_datetime] if images_uploaded_successfully else []
|
||||
# Other fields will be null/None initially
|
||||
)
|
||||
db.add(new_tank)
|
||||
await db.commit()
|
||||
print(f"Created new tank inspection record with tank_images = {new_tank.tank_images}, dates = {new_tank.tank_image_upload_dates}")
|
||||
|
||||
print("=== STEP3 DEBUG END ===")
|
||||
|
||||
if images_uploaded_successfully:
|
||||
return {
|
||||
"message": "Tank images uploaded successfully",
|
||||
"account_number": account_number,
|
||||
"uploaded_files": saved_files
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"message": "Tank images upload skipped or failed",
|
||||
"account_number": account_number,
|
||||
"uploaded_files": []
|
||||
}
|
||||
|
||||
@router.post("/upload-tank-images")
|
||||
async def upload_additional_tank_images(
|
||||
account_number: str = Form(...),
|
||||
tank_image_1: UploadFile = File(...),
|
||||
tank_image_2: UploadFile = File(...),
|
||||
tank_image_3: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Endpoint for customers to upload additional sets of 3 tank images after registration.
|
||||
Similar to step3 but for existing customers.
|
||||
"""
|
||||
print("=== UPLOAD ADDITIONAL TANK IMAGES DEBUG START ===")
|
||||
print(f"Account number received: '{account_number}'")
|
||||
|
||||
# Debug: Check all parameters received
|
||||
images = [tank_image_1, tank_image_2, tank_image_3]
|
||||
for i, image in enumerate(images, 1):
|
||||
print(f"Image {i}: filename='{image.filename}', content_type='{image.content_type}', size={image.size}")
|
||||
|
||||
# Validate account number
|
||||
if not account_number:
|
||||
print("ERROR: Account number is empty")
|
||||
raise HTTPException(status_code=400, detail="Account number is required")
|
||||
|
||||
# Get customer info
|
||||
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
|
||||
customer = customer_result.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=400, detail="Customer not found")
|
||||
|
||||
print(f"Creating directory: /images/{account_number}")
|
||||
# Create directory for account number in the mounted images volume
|
||||
account_dir = f"/images/{account_number}"
|
||||
os.makedirs(account_dir, exist_ok=True)
|
||||
|
||||
current_datetime = datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S") # YYYY-MM-DD_HH-MM-SS format
|
||||
|
||||
# Track if images were uploaded successfully
|
||||
images_uploaded_successfully = False
|
||||
|
||||
# Validate and save images
|
||||
saved_files = []
|
||||
|
||||
try:
|
||||
for i, image in enumerate(images, 1):
|
||||
print(f"Processing image {i}...")
|
||||
# Read image data
|
||||
image_data = await image.read()
|
||||
print(f"Image {i} data read: {len(image_data)} bytes")
|
||||
|
||||
# Validate file size (max 20MB before processing)
|
||||
if len(image_data) > 20 * 1024 * 1024:
|
||||
print(f"ERROR: Image {i} too large: {len(image_data)} bytes")
|
||||
raise HTTPException(status_code=400, detail=f"File {i} is too large (max 20MB)")
|
||||
|
||||
# Resize and process image (this also validates it's a valid image)
|
||||
print(f"Resizing image {i}...")
|
||||
processed_image_data = resize_image(image_data, max_size=(1024, 1024), quality=85)
|
||||
print(f"Image {i} resized: {len(processed_image_data)} bytes")
|
||||
|
||||
# Save processed image with datetime-based filename
|
||||
filename = f"{current_datetime}-{i}.jpg"
|
||||
file_path = os.path.join(account_dir, filename)
|
||||
print(f"Saving image {i} to: {file_path}")
|
||||
|
||||
with open(file_path, "wb") as buffer:
|
||||
buffer.write(processed_image_data)
|
||||
|
||||
saved_files.append(filename)
|
||||
print(f"Image {i} saved successfully")
|
||||
|
||||
images_uploaded_successfully = True
|
||||
print(f"All images processed successfully. Saved files: {saved_files}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR processing images: {str(e)}")
|
||||
# Don't raise exception - we want to track the failure in the database
|
||||
images_uploaded_successfully = False
|
||||
|
||||
# Update customer tank inspection record
|
||||
print("Updating customer tank inspection record...")
|
||||
tank_result = await db.execute(
|
||||
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
|
||||
)
|
||||
existing_tank = tank_result.scalar_one_or_none()
|
||||
|
||||
if existing_tank:
|
||||
# Update existing record
|
||||
if images_uploaded_successfully:
|
||||
existing_tank.tank_images += 1 # Increment count of image sets
|
||||
# Append current datetime to upload dates list
|
||||
current_dates = existing_tank.tank_image_upload_dates or []
|
||||
current_dates.append(current_datetime)
|
||||
existing_tank.tank_image_upload_dates = current_dates
|
||||
await db.commit()
|
||||
print(f"Updated existing tank inspection record with tank_images = {existing_tank.tank_images}, dates = {existing_tank.tank_image_upload_dates}")
|
||||
else:
|
||||
# This shouldn't happen for additional uploads, but handle it just in case
|
||||
new_tank = Customer_Tank_Inspection(
|
||||
customer_id=customer.id,
|
||||
tank_images=1 if images_uploaded_successfully else 0,
|
||||
tank_image_upload_dates=[current_datetime] if images_uploaded_successfully else []
|
||||
)
|
||||
db.add(new_tank)
|
||||
await db.commit()
|
||||
print(f"Created new tank inspection record with tank_images = {new_tank.tank_images}, dates = {new_tank.tank_image_upload_dates}")
|
||||
|
||||
print("=== UPLOAD ADDITIONAL TANK IMAGES DEBUG END ===")
|
||||
|
||||
if images_uploaded_successfully:
|
||||
return {
|
||||
"message": "Additional tank images uploaded successfully",
|
||||
"account_number": account_number,
|
||||
"uploaded_files": saved_files,
|
||||
"upload_date": current_datetime
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Failed to upload tank images")
|
||||
|
||||
@router.get("/tank-images/{account_number}")
|
||||
async def get_tank_images(account_number: str, db: AsyncSession = Depends(get_db)):
|
||||
"""
|
||||
Get tank images information for a customer including upload dates.
|
||||
"""
|
||||
# Get customer info
|
||||
customer_result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
|
||||
customer = customer_result.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
# Get tank inspection record
|
||||
tank_result = await db.execute(
|
||||
select(Customer_Tank_Inspection).where(Customer_Tank_Inspection.customer_id == customer.id)
|
||||
)
|
||||
tank_record = tank_result.scalar_one_or_none()
|
||||
|
||||
# Build image sets - first from database, then scan for any additional files
|
||||
image_sets = []
|
||||
upload_dates = tank_record.tank_image_upload_dates or [] if tank_record else []
|
||||
|
||||
# Add sets from database
|
||||
for i, upload_date in enumerate(upload_dates):
|
||||
# Handle backward compatibility: old format used date-only strings and tank_*.jpg files
|
||||
# New format uses datetime strings and {datetime}-*.jpg files
|
||||
if "_" in upload_date:
|
||||
# New datetime format (YYYY-MM-DD_HH-MM-SS)
|
||||
image_set = {
|
||||
"date": upload_date,
|
||||
"images": [
|
||||
f"/images/{account_number}/{upload_date}-1.jpg",
|
||||
f"/images/{account_number}/{upload_date}-2.jpg",
|
||||
f"/images/{account_number}/{upload_date}-3.jpg"
|
||||
]
|
||||
}
|
||||
else:
|
||||
# Old date-only format (YYYY-MM-DD) - uses tank_*.jpg files
|
||||
image_set = {
|
||||
"date": upload_date,
|
||||
"images": [
|
||||
f"/images/{account_number}/tank_1.jpg",
|
||||
f"/images/{account_number}/tank_2.jpg",
|
||||
f"/images/{account_number}/tank_3.jpg"
|
||||
]
|
||||
}
|
||||
image_sets.append(image_set)
|
||||
|
||||
# Scan for any additional image sets that might not be in database
|
||||
account_dir = f"/images/{account_number}"
|
||||
if os.path.exists(account_dir):
|
||||
# Find all image files
|
||||
all_files = os.listdir(account_dir)
|
||||
image_files = [f for f in all_files if f.endswith('.jpg')]
|
||||
|
||||
# Group by datetime prefix
|
||||
datetime_groups = {}
|
||||
for file in image_files:
|
||||
if file.startswith('tank_'):
|
||||
# Old tank_*.jpg files - already handled above
|
||||
continue
|
||||
elif '_' in file and file.endswith('.jpg'): # datetime format like 2026-01-08_23-25-31-1.jpg
|
||||
# Extract datetime prefix by removing the image number and .jpg
|
||||
# Example: 2026-01-08_23-25-31-1.jpg -> 2026-01-08_23-25-31
|
||||
datetime_prefix = file.rsplit('-', 1)[0] # Remove everything after last dash
|
||||
if datetime_prefix not in datetime_groups:
|
||||
datetime_groups[datetime_prefix] = []
|
||||
datetime_groups[datetime_prefix].append(file)
|
||||
|
||||
# Add any datetime groups not already in database
|
||||
for datetime_prefix, files in datetime_groups.items():
|
||||
if datetime_prefix not in upload_dates and len(files) >= 3:
|
||||
# Sort files to ensure correct order
|
||||
sorted_files = sorted(files)
|
||||
image_set = {
|
||||
"date": datetime_prefix,
|
||||
"images": [
|
||||
f"/images/{account_number}/{sorted_files[0]}",
|
||||
f"/images/{account_number}/{sorted_files[1]}",
|
||||
f"/images/{account_number}/{sorted_files[2]}"
|
||||
]
|
||||
}
|
||||
image_sets.append(image_set)
|
||||
|
||||
# Also check for date-prefixed files (like 2026-01-08-1.jpg)
|
||||
date_groups = {}
|
||||
for file in image_files:
|
||||
if file.startswith('tank_') or '_' in file:
|
||||
continue # Skip old format and datetime format
|
||||
parts = file.split('-')
|
||||
if len(parts) == 4 and parts[3] in ['1.jpg', '2.jpg', '3.jpg']:
|
||||
date_prefix = '-'.join(parts[:3]) # 2026-01-08
|
||||
if date_prefix not in date_groups:
|
||||
date_groups[date_prefix] = []
|
||||
date_groups[date_prefix].append(file)
|
||||
|
||||
# Add date groups not already in database
|
||||
for date_prefix, files in date_groups.items():
|
||||
if date_prefix not in upload_dates and len(files) >= 3:
|
||||
# Sort files to ensure correct order
|
||||
sorted_files = sorted(files)
|
||||
image_set = {
|
||||
"date": date_prefix,
|
||||
"images": [
|
||||
f"/images/{account_number}/{sorted_files[0]}",
|
||||
f"/images/{account_number}/{sorted_files[1]}",
|
||||
f"/images/{account_number}/{sorted_files[2]}"
|
||||
]
|
||||
}
|
||||
image_sets.append(image_set)
|
||||
|
||||
# Sort image sets by date descending (newest first)
|
||||
def sort_key(item):
|
||||
date_str = item['date']
|
||||
try:
|
||||
if '_' in date_str:
|
||||
return datetime.strptime(date_str, "%Y-%m-%d_%H-%M-%S")
|
||||
else:
|
||||
return datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except:
|
||||
return datetime.min
|
||||
|
||||
image_sets.sort(key=sort_key, reverse=True)
|
||||
|
||||
return {
|
||||
"account_number": account_number,
|
||||
"image_sets": image_sets
|
||||
}
|
||||
|
||||
@router.post("/step1")
|
||||
async def create_customer_step1(customer: CustomerCreateStep1, db: AsyncSession = Depends(get_db)):
|
||||
while True:
|
||||
random_part = generate_random_number_string(6)
|
||||
account_number = 'AO-' + random_part
|
||||
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_number))
|
||||
existing_customer = result.scalar_one_or_none()
|
||||
if not existing_customer:
|
||||
break
|
||||
|
||||
# Extract house_description for separate table
|
||||
house_description = customer.house_description
|
||||
|
||||
# Create customer
|
||||
customer_data = customer.model_dump()
|
||||
customer_data.pop('house_description') # Remove from customer data
|
||||
customer_data.update({
|
||||
'account_number': account_number,
|
||||
'customer_state': 1, # Default
|
||||
'customer_automatic': 0,
|
||||
'company_id': 1, # Default
|
||||
'customer_latitude': '0',
|
||||
'customer_longitude': '0',
|
||||
'correct_address': True,
|
||||
'customer_first_call': datetime.utcnow()
|
||||
})
|
||||
db_customer = Customer_Customer(**customer_data)
|
||||
db.add(db_customer)
|
||||
await db.commit()
|
||||
await db.refresh(db_customer)
|
||||
|
||||
# Create customer description if house_description provided
|
||||
if house_description:
|
||||
db_description = Customer_Description(
|
||||
customer_id=db_customer.id,
|
||||
account_number=account_number,
|
||||
company_id=1, # Default
|
||||
fill_location=None, # Will work on this later
|
||||
description=house_description
|
||||
# tank_images is now tracked in customer_tank table
|
||||
)
|
||||
db.add(db_description)
|
||||
await db.commit()
|
||||
|
||||
# Create customer stats record for tracking metrics
|
||||
db_stats = Customer_Stats(
|
||||
customer_id=db_customer.id
|
||||
# All other fields default to 0/0.00 as defined in the model
|
||||
)
|
||||
db.add(db_stats)
|
||||
await db.commit()
|
||||
|
||||
return {"account_number": account_number}
|
||||
|
||||
@router.post("/step2", response_model=UserResponse)
|
||||
async def create_customer_account(account_data: CustomerAccountCreate, db: AsyncSession = Depends(get_db)):
|
||||
# Verify passwords match
|
||||
if account_data.password != account_data.confirm_password:
|
||||
raise HTTPException(status_code=400, detail="Passwords do not match")
|
||||
|
||||
# Check if customer exists
|
||||
result = await db.execute(select(Customer_Customer).where(Customer_Customer.account_number == account_data.account_number))
|
||||
customer = result.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=400, detail="Customer not found")
|
||||
|
||||
# Check if email already registered
|
||||
result = await db.execute(select(Account_User).where(Account_User.email == account_data.customer_email))
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
# Update customer email
|
||||
customer.customer_email = account_data.customer_email
|
||||
db.add(customer)
|
||||
await db.commit()
|
||||
|
||||
# Extract house number from customer address (first part before space)
|
||||
house_number = customer.customer_address.split()[0] if customer.customer_address else ''
|
||||
|
||||
# Create account user
|
||||
username = account_data.account_number
|
||||
hashed_password = get_password_hash(account_data.password)
|
||||
db_user = Account_User(
|
||||
username=username,
|
||||
account_number=account_data.account_number,
|
||||
house_number=house_number,
|
||||
password_hash=hashed_password,
|
||||
member_since=datetime.utcnow(),
|
||||
email=account_data.customer_email,
|
||||
last_seen=datetime.utcnow(),
|
||||
admin=0,
|
||||
admin_role=0,
|
||||
confirmed=1,
|
||||
active=1,
|
||||
user_id=customer.id
|
||||
)
|
||||
db.add(db_user)
|
||||
await db.commit()
|
||||
await db.refresh(db_user)
|
||||
return db_user
|
||||
71
routes/auth/register.py
Normal file
71
routes/auth/register.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from database import get_db
|
||||
from models import Account_User, Customer_Customer
|
||||
from schemas import UserCreate, UserResponse
|
||||
from passlib.context import CryptContext
|
||||
from datetime import datetime
|
||||
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def escape_like_pattern(value: str) -> str:
|
||||
"""Escape special characters for SQL LIKE patterns.
|
||||
|
||||
Escapes %, _, and \ which have special meaning in LIKE clauses
|
||||
to prevent SQL injection via wildcards.
|
||||
"""
|
||||
# Escape backslash first, then the wildcards
|
||||
return value.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
# Verify passwords match
|
||||
if user.password != user.confirm_password:
|
||||
raise HTTPException(status_code=400, detail="Passwords do not match")
|
||||
|
||||
# Check if customer exists in Customer_Customer table
|
||||
# Escape SQL LIKE wildcards to prevent injection attacks
|
||||
escaped_house_number = escape_like_pattern(user.house_number)
|
||||
customer_result = await db.execute(
|
||||
select(Customer_Customer).where(
|
||||
(Customer_Customer.account_number == user.account_number) &
|
||||
(Customer_Customer.customer_address.like(f'{escaped_house_number} %', escape='\\'))
|
||||
)
|
||||
)
|
||||
customer = customer_result.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=400, detail="Customer not found with provided account and house number")
|
||||
|
||||
# Check if email already registered
|
||||
result = await db.execute(select(Account_User).where(Account_User.email == user.email))
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
username = f"{user.account_number}-{user.house_number}"
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = Account_User(
|
||||
username=username,
|
||||
account_number=user.account_number,
|
||||
house_number=user.house_number,
|
||||
password_hash=hashed_password,
|
||||
member_since=datetime.utcnow(),
|
||||
email=user.email,
|
||||
last_seen=datetime.utcnow(),
|
||||
admin=0,
|
||||
admin_role=0,
|
||||
confirmed=1,
|
||||
active=1,
|
||||
user_id=customer.id
|
||||
)
|
||||
db.add(db_user)
|
||||
await db.commit()
|
||||
await db.refresh(db_user)
|
||||
return db_user
|
||||
7
routes/info/__init__.py
Normal file
7
routes/info/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
from .deliveries import router as deliveries_router
|
||||
from .pricing import router as pricing_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(deliveries_router, tags=["info"])
|
||||
router.include_router(pricing_router, prefix="/pricing", tags=["pricing"])
|
||||
77
routes/info/deliveries.py
Normal file
77
routes/info/deliveries.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from fastapi import Depends, HTTPException, APIRouter
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, desc, func
|
||||
from database import get_db
|
||||
from models import Delivery_Delivery, Customer_Customer
|
||||
from routes.auth.current_user import get_current_user
|
||||
from models import Account_User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
STATUS_MAPPING = {
|
||||
0: "Waiting",
|
||||
1: "Cancelled",
|
||||
2: "Today",
|
||||
3: "Tomorrow_Delivery",
|
||||
4: "Partial_Delivery",
|
||||
5: "Issue_Delivery",
|
||||
9: "Pending",
|
||||
10: "Finalized"
|
||||
}
|
||||
|
||||
PAYMENT_MAPPING = {
|
||||
0: "Cash",
|
||||
1: "Credit",
|
||||
2: "Credit/Cash",
|
||||
3: "Check",
|
||||
4: "Other"
|
||||
}
|
||||
|
||||
@router.get("/deliveries")
|
||||
async def get_user_deliveries(
|
||||
current_user: Account_User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if not current_user.user_id:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
# Get customer from user_id
|
||||
customer = await db.execute(
|
||||
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
|
||||
)
|
||||
customer = customer.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
# Get last 20 deliveries ordered by delivery date (when_delivered or when_ordered) desc
|
||||
deliveries_query = await db.execute(
|
||||
select(Delivery_Delivery)
|
||||
.where(Delivery_Delivery.customer_id == customer.id)
|
||||
.order_by(desc(func.coalesce(Delivery_Delivery.when_delivered, Delivery_Delivery.when_ordered)))
|
||||
.limit(20)
|
||||
)
|
||||
deliveries = deliveries_query.scalars().all()
|
||||
|
||||
# Format deliveries
|
||||
result = []
|
||||
for delivery in deliveries:
|
||||
# Use when_delivered if available, otherwise when_ordered
|
||||
delivery_date = delivery.when_delivered or delivery.when_ordered
|
||||
# Use gallons_delivered if available, otherwise gallons_ordered
|
||||
gallons = delivery.gallons_delivered if delivery.gallons_delivered is not None else delivery.gallons_ordered
|
||||
|
||||
result.append({
|
||||
"id": delivery.id,
|
||||
"when_ordered": delivery.when_ordered.isoformat() if delivery.when_ordered else None,
|
||||
"when_delivered": delivery.when_delivered.isoformat() if delivery.when_delivered else None,
|
||||
"delivery_date": delivery_date.isoformat() if delivery_date else None,
|
||||
"gallons_ordered": delivery.gallons_ordered,
|
||||
"gallons_delivered": float(delivery.gallons_delivered) if delivery.gallons_delivered else None,
|
||||
"gallons": float(gallons) if gallons is not None else None,
|
||||
"customer_price": float(delivery.customer_price) if delivery.customer_price else None,
|
||||
"delivery_status": STATUS_MAPPING.get(delivery.delivery_status, f"Unknown ({delivery.delivery_status})"),
|
||||
"payment_type": PAYMENT_MAPPING.get(delivery.payment_type, f"Unknown ({delivery.payment_type})"),
|
||||
"dispatcher_notes": delivery.dispatcher_notes
|
||||
})
|
||||
|
||||
return result
|
||||
23
routes/info/pricing.py
Normal file
23
routes/info/pricing.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from database import get_db
|
||||
from models import Pricing_Oil_Oil
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/current")
|
||||
async def get_current_pricing(db: AsyncSession = Depends(get_db)):
|
||||
# Get the latest pricing record
|
||||
pricing = await db.execute(
|
||||
select(Pricing_Oil_Oil).order_by(Pricing_Oil_Oil.id.desc()).limit(1)
|
||||
)
|
||||
pricing = pricing.scalar_one_or_none()
|
||||
|
||||
if not pricing:
|
||||
return {"price_per_gallon": 0.00, "message": "No pricing available"}
|
||||
|
||||
return {
|
||||
"price_per_gallon": float(pricing.price_for_customer),
|
||||
"date": pricing.date.isoformat() if pricing.date else None
|
||||
}
|
||||
5
routes/order/__init__.py
Normal file
5
routes/order/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from . import deliveries
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(deliveries.router, prefix="/deliveries", tags=["deliveries"])
|
||||
95
routes/order/deliveries.py
Normal file
95
routes/order/deliveries.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from datetime import date, datetime
|
||||
from database import get_db
|
||||
from models import Delivery_Delivery, Customer_Customer, Pricing_Oil_Oil
|
||||
from schemas import DeliveryCreate
|
||||
from routes.auth.current_user import get_current_user
|
||||
from models import Account_User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# State mapping for converting integer codes to string abbreviations
|
||||
state_mapping = {0: "MA", 1: "NH"} # Add more as needed
|
||||
|
||||
@router.post("/")
|
||||
async def create_delivery(
|
||||
delivery_data: DeliveryCreate,
|
||||
current_user: Account_User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
if not current_user.user_id:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
# Get customer details
|
||||
customer = await db.execute(
|
||||
select(Customer_Customer).where(Customer_Customer.id == current_user.user_id)
|
||||
)
|
||||
customer = customer.scalar_one_or_none()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=404, detail="Customer not found")
|
||||
|
||||
# Validate gallons_ordered >= 100
|
||||
if delivery_data.gallons_ordered < 100:
|
||||
raise HTTPException(status_code=400, detail="Gallons ordered must be at least 100")
|
||||
|
||||
# Validate expected_delivery_date >= today
|
||||
today = date.today()
|
||||
if delivery_data.expected_delivery_date < today:
|
||||
raise HTTPException(status_code=400, detail="Expected delivery date cannot be in the past")
|
||||
|
||||
# Get latest oil pricing
|
||||
oil = await db.execute(
|
||||
select(Pricing_Oil_Oil).order_by(Pricing_Oil_Oil.id.desc()).limit(1)
|
||||
)
|
||||
oil = oil.scalar_one_or_none()
|
||||
if not oil:
|
||||
raise HTTPException(status_code=500, detail="No oil pricing available")
|
||||
|
||||
# Create delivery
|
||||
new_delivery = Delivery_Delivery(
|
||||
customer_id=customer.id,
|
||||
customer_name=f"{customer.customer_first_name} {customer.customer_last_name}",
|
||||
customer_address=customer.customer_address,
|
||||
customer_town=customer.customer_town,
|
||||
customer_state=str(customer.customer_state),
|
||||
customer_zip=int(customer.customer_zip),
|
||||
gallons_ordered=delivery_data.gallons_ordered,
|
||||
gallons_delivered=None,
|
||||
customer_filled=None,
|
||||
delivery_status=0, # waiting
|
||||
when_ordered=today,
|
||||
when_delivered=None,
|
||||
expected_delivery_date=delivery_data.expected_delivery_date,
|
||||
automatic=None,
|
||||
automatic_id=None,
|
||||
oil_id=oil.id,
|
||||
supplier_price=oil.price_for_customer, # assuming this is the price
|
||||
customer_price=oil.price_for_customer,
|
||||
dispatcher_notes=delivery_data.dispatcher_notes,
|
||||
driver_employee_id=None,
|
||||
driver_first_name=None,
|
||||
driver_last_name=None,
|
||||
promo_id=None,
|
||||
promo_money_discount=None,
|
||||
payment_type=delivery_data.payment_type,
|
||||
# Set other fields to None or defaults
|
||||
customer_asked_for_fill=0,
|
||||
customer_temperature=None,
|
||||
prime=delivery_data.prime,
|
||||
same_day=delivery_data.same_day,
|
||||
emergency=delivery_data.emergency,
|
||||
payment_card_id=None,
|
||||
cash_recieved=None,
|
||||
pre_charge_amount=delivery_data.pre_charge_amount,
|
||||
total_price=None,
|
||||
final_price=None,
|
||||
check_number=None,
|
||||
)
|
||||
|
||||
db.add(new_delivery)
|
||||
await db.commit()
|
||||
await db.refresh(new_delivery)
|
||||
|
||||
return {"message": "Delivery created successfully", "delivery_id": new_delivery.id}
|
||||
5
routes/payment/__init__.py
Normal file
5
routes/payment/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from . import routes
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(routes.router, tags=["payment"])
|
||||
1003
routes/payment/routes.py
Normal file
1003
routes/payment/routes.py
Normal file
File diff suppressed because it is too large
Load Diff
278
schemas.py
Normal file
278
schemas.py
Normal file
@@ -0,0 +1,278 @@
|
||||
from pydantic import BaseModel, field_validator
|
||||
from typing import Optional
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
|
||||
# Customer schemas
|
||||
class CustomerBase(BaseModel):
|
||||
auth_net_profile_id: Optional[str] = None
|
||||
account_number: str
|
||||
customer_last_name: str
|
||||
customer_first_name: str
|
||||
customer_town: str
|
||||
customer_state: int
|
||||
customer_zip: str
|
||||
customer_first_call: Optional[datetime] = None
|
||||
customer_email: str
|
||||
customer_automatic: int
|
||||
customer_phone_number: str
|
||||
customer_home_type: int
|
||||
customer_apt: str
|
||||
customer_address: str
|
||||
company_id: int
|
||||
customer_latitude: str
|
||||
customer_longitude: str
|
||||
correct_address: bool
|
||||
|
||||
class CustomerCreate(BaseModel):
|
||||
customer_first_name: str
|
||||
customer_last_name: str
|
||||
customer_phone_number: str
|
||||
customer_email: str
|
||||
customer_address: str
|
||||
customer_apt: Optional[str] = None
|
||||
customer_town: str
|
||||
customer_zip: str
|
||||
customer_home_type: int = 0
|
||||
personal_notes: Optional[str] = None
|
||||
house_description: Optional[str] = None
|
||||
|
||||
class CustomerCreateStep1(BaseModel):
|
||||
customer_first_name: str
|
||||
customer_last_name: str
|
||||
customer_phone_number: str # Now accepts formatted: (123) 456-7890
|
||||
customer_address: str
|
||||
customer_apt: Optional[str] = None
|
||||
customer_town: str
|
||||
customer_zip: str
|
||||
customer_home_type: int = 0
|
||||
house_description: Optional[str] = None
|
||||
|
||||
class CustomerAccountCreate(BaseModel):
|
||||
account_number: str
|
||||
password: str
|
||||
confirm_password: str
|
||||
customer_email: str
|
||||
|
||||
class NewCustomerCreate(CustomerCreate):
|
||||
password: str
|
||||
confirm_password: str
|
||||
|
||||
class CustomerUpdate(BaseModel):
|
||||
auth_net_profile_id: Optional[str] = None
|
||||
account_number: Optional[str] = None
|
||||
customer_last_name: Optional[str] = None
|
||||
customer_first_name: Optional[str] = None
|
||||
customer_town: Optional[str] = None
|
||||
customer_state: Optional[int] = None
|
||||
customer_zip: Optional[str] = None
|
||||
customer_first_call: Optional[datetime] = None
|
||||
customer_email: Optional[str] = None
|
||||
customer_automatic: Optional[int] = None
|
||||
customer_phone_number: Optional[str] = None
|
||||
customer_home_type: Optional[int] = None
|
||||
customer_apt: Optional[str] = None
|
||||
customer_address: Optional[str] = None
|
||||
company_id: Optional[int] = None
|
||||
customer_latitude: Optional[str] = None
|
||||
customer_longitude: Optional[str] = None
|
||||
correct_address: Optional[bool] = None
|
||||
|
||||
class CustomerResponse(CustomerBase):
|
||||
id: int
|
||||
|
||||
# Order schemas (Delivery_Delivery)
|
||||
class OrderBase(BaseModel):
|
||||
customer_id: int
|
||||
customer_name: str
|
||||
customer_address: str
|
||||
customer_town: str
|
||||
customer_state: str
|
||||
customer_zip: int
|
||||
gallons_ordered: int
|
||||
customer_asked_for_fill: int
|
||||
gallons_delivered: Decimal
|
||||
customer_filled: int
|
||||
delivery_status: int
|
||||
when_ordered: Optional[date] = None
|
||||
when_delivered: Optional[date] = None
|
||||
expected_delivery_date: Optional[date] = None
|
||||
automatic: int
|
||||
automatic_id: int
|
||||
oil_id: int
|
||||
supplier_price: Decimal
|
||||
customer_price: Decimal
|
||||
customer_temperature: Decimal
|
||||
dispatcher_notes: str
|
||||
prime: int
|
||||
same_day: int
|
||||
emergency: int
|
||||
payment_type: int
|
||||
payment_card_id: int
|
||||
cash_recieved: Decimal
|
||||
driver_employee_id: int
|
||||
driver_first_name: str
|
||||
driver_last_name: str
|
||||
pre_charge_amount: Decimal
|
||||
total_price: Decimal
|
||||
final_price: Decimal
|
||||
check_number: str
|
||||
promo_id: int
|
||||
promo_money_discount: Decimal
|
||||
|
||||
class OrderCreate(OrderBase):
|
||||
pass
|
||||
|
||||
class OrderUpdate(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[int] = None
|
||||
gallons_ordered: Optional[int] = None
|
||||
customer_asked_for_fill: Optional[int] = None
|
||||
gallons_delivered: Optional[Decimal] = None
|
||||
customer_filled: Optional[int] = None
|
||||
delivery_status: Optional[int] = None
|
||||
when_ordered: Optional[date] = None
|
||||
when_delivered: Optional[date] = None
|
||||
expected_delivery_date: Optional[date] = None
|
||||
automatic: Optional[int] = None
|
||||
automatic_id: Optional[int] = None
|
||||
oil_id: Optional[int] = None
|
||||
supplier_price: Optional[Decimal] = None
|
||||
customer_price: Optional[Decimal] = None
|
||||
customer_temperature: Optional[Decimal] = None
|
||||
dispatcher_notes: Optional[str] = None
|
||||
prime: Optional[int] = None
|
||||
same_day: Optional[int] = None
|
||||
emergency: Optional[int] = None
|
||||
payment_type: Optional[int] = None
|
||||
payment_card_id: Optional[int] = None
|
||||
cash_recieved: Optional[Decimal] = None
|
||||
driver_employee_id: Optional[int] = None
|
||||
driver_first_name: Optional[str] = None
|
||||
driver_last_name: Optional[str] = None
|
||||
pre_charge_amount: Optional[Decimal] = None
|
||||
total_price: Optional[Decimal] = None
|
||||
final_price: Optional[Decimal] = None
|
||||
check_number: Optional[str] = None
|
||||
promo_id: Optional[int] = None
|
||||
promo_money_discount: Optional[Decimal] = None
|
||||
|
||||
class OrderResponse(OrderBase):
|
||||
id: int
|
||||
|
||||
class DeliveryCreate(BaseModel):
|
||||
gallons_ordered: int
|
||||
expected_delivery_date: date
|
||||
dispatcher_notes: Optional[str] = None
|
||||
payment_type: int # 0=cash, 1=credit
|
||||
prime: Optional[int] = 0
|
||||
same_day: Optional[int] = 0
|
||||
emergency: Optional[int] = 0
|
||||
pre_charge_amount: Optional[float] = None
|
||||
|
||||
# Card schemas
|
||||
class CardBase(BaseModel):
|
||||
auth_net_payment_profile_id: Optional[str] = None
|
||||
card_number: Optional[str] = None
|
||||
last_four_digits: int
|
||||
name_on_card: Optional[str] = None
|
||||
expiration_month: str
|
||||
expiration_year: str
|
||||
type_of_card: Optional[str] = None
|
||||
security_number: Optional[str] = None
|
||||
accepted_or_declined: Optional[int] = None
|
||||
main_card: Optional[bool] = None
|
||||
zip_code: Optional[str] = None
|
||||
|
||||
class CardCreate(BaseModel):
|
||||
card_number: str
|
||||
name_on_card: str
|
||||
expiration_month: str
|
||||
expiration_year: str
|
||||
security_number: str
|
||||
zip_code: str
|
||||
save_card: bool = False
|
||||
|
||||
class CardResponse(CardBase):
|
||||
id: int
|
||||
date_added: datetime
|
||||
user_id: int
|
||||
|
||||
class CardUpdate(BaseModel):
|
||||
name_on_card: Optional[str] = None
|
||||
expiration_month: Optional[str] = None
|
||||
expiration_year: Optional[str] = None
|
||||
zip_code: Optional[str] = None
|
||||
|
||||
# Payment schemas
|
||||
class PaymentCreate(BaseModel):
|
||||
amount: Decimal
|
||||
card_id: Optional[int] = None # For saved cards
|
||||
card_details: Optional[CardCreate] = None # For new cards
|
||||
delivery_id: Optional[int] = None
|
||||
|
||||
class PaymentResponse(BaseModel):
|
||||
transaction_id: str
|
||||
status: str
|
||||
amount: Decimal
|
||||
auth_net_transaction_id: str
|
||||
|
||||
# Auth schemas
|
||||
class UserCreate(BaseModel):
|
||||
account_number: str
|
||||
house_number: str
|
||||
email: str
|
||||
password: str
|
||||
confirm_password: str
|
||||
|
||||
@field_validator('house_number')
|
||||
@classmethod
|
||||
def validate_house_number(cls, v: str) -> str:
|
||||
"""Validate house_number contains only safe characters.
|
||||
|
||||
Allows alphanumeric characters, spaces, hyphens, and forward slashes
|
||||
(for addresses like "123-A" or "45 1/2").
|
||||
"""
|
||||
import re
|
||||
if not v or not v.strip():
|
||||
raise ValueError('House number cannot be empty')
|
||||
# Allow alphanumeric, spaces, hyphens, forward slashes
|
||||
if not re.match(r'^[a-zA-Z0-9\s\-/]+$', v):
|
||||
raise ValueError('House number contains invalid characters')
|
||||
if len(v) > 20:
|
||||
raise ValueError('House number is too long')
|
||||
return v.strip()
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Optional[str] = None
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
member_since: datetime
|
||||
last_seen: datetime
|
||||
admin: int
|
||||
admin_role: int
|
||||
confirmed: int
|
||||
active: int
|
||||
|
||||
class ForgotPasswordRequest(BaseModel):
|
||||
email: str
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
token: str
|
||||
password: str
|
||||
confirm_password: str
|
||||
154
settings.py
Normal file
154
settings.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Unified configuration that reads secrets from environment variables.
|
||||
Non-secret configuration (like CORS origins) can have defaults per environment.
|
||||
"""
|
||||
import os
|
||||
|
||||
|
||||
class ApplicationConfig:
|
||||
"""
|
||||
Application configuration loaded from environment variables.
|
||||
All secrets MUST be provided via environment variables.
|
||||
"""
|
||||
|
||||
# Current environment mode
|
||||
CURRENT_SETTINGS = os.environ.get('MODE', 'DEVELOPMENT')
|
||||
|
||||
# ===========================================
|
||||
# DATABASE CONFIGURATION (Required)
|
||||
# ===========================================
|
||||
POSTGRES_USERNAME = os.environ.get('POSTGRES_USERNAME')
|
||||
POSTGRES_PW = os.environ.get('POSTGRES_PASSWORD')
|
||||
POSTGRES_SERVER = os.environ.get('POSTGRES_SERVER')
|
||||
POSTGRES_PORT = os.environ.get('POSTGRES_PORT', '5432')
|
||||
POSTGRES_DBNAME00 = os.environ.get('POSTGRES_DBNAME')
|
||||
|
||||
# ===========================================
|
||||
# JWT CONFIGURATION (Required)
|
||||
# ===========================================
|
||||
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
|
||||
JWT_ALGORITHM = os.environ.get('JWT_ALGORITHM', 'HS256')
|
||||
# Token expiry in minutes (default: 30 min for prod, can override for dev)
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES = int(os.environ.get('JWT_ACCESS_TOKEN_EXPIRE_MINUTES', '30'))
|
||||
|
||||
# ===========================================
|
||||
# AUTHORIZE.NET CONFIGURATION (Required for payments)
|
||||
# ===========================================
|
||||
AUTH_NET_API_LOGIN_ID = os.environ.get('AUTH_NET_API_LOGIN_ID')
|
||||
AUTH_NET_TRANSACTION_KEY = os.environ.get('AUTH_NET_TRANSACTION_KEY')
|
||||
|
||||
# ===========================================
|
||||
# SMTP CONFIGURATION (Required for password reset)
|
||||
# ===========================================
|
||||
SMTP_SERVER = os.environ.get('SMTP_SERVER', 'smtp.gmail.com')
|
||||
SMTP_PORT = int(os.environ.get('SMTP_PORT', '587'))
|
||||
SMTP_USERNAME = os.environ.get('SMTP_USERNAME')
|
||||
SMTP_PASSWORD = os.environ.get('SMTP_PASSWORD')
|
||||
SMTP_FROM_EMAIL = os.environ.get('SMTP_FROM_EMAIL')
|
||||
|
||||
# ===========================================
|
||||
# FRONTEND URL (Required for password reset links)
|
||||
# ===========================================
|
||||
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000')
|
||||
|
||||
# ===========================================
|
||||
# CORS ORIGINS - Environment specific defaults
|
||||
# ===========================================
|
||||
_origins_env = os.environ.get('CORS_ORIGINS', '')
|
||||
|
||||
if _origins_env:
|
||||
# Use comma-separated list from environment
|
||||
origins = [origin.strip() for origin in _origins_env.split(',') if origin.strip()]
|
||||
else:
|
||||
# Default origins based on MODE
|
||||
_mode = os.environ.get('MODE', 'DEVELOPMENT')
|
||||
if _mode == 'PRODUCTION':
|
||||
origins = [
|
||||
"https://oil.edwineames.com",
|
||||
"https://apiauto.edwineames.com",
|
||||
"https://portal.auburnoil.com",
|
||||
]
|
||||
elif _mode == 'LOCAL':
|
||||
origins = [
|
||||
"http://192.168.1.204:9000",
|
||||
"http://192.168.1.204:9613",
|
||||
"http://192.168.1.204:9614",
|
||||
"http://192.168.1.204:9612",
|
||||
"http://192.168.1.204:9611",
|
||||
]
|
||||
else: # DEVELOPMENT
|
||||
origins = [
|
||||
"http://localhost:9000",
|
||||
"https://localhost:9513",
|
||||
"http://localhost:9514",
|
||||
"http://localhost:9512",
|
||||
"http://localhost:9511",
|
||||
"http://localhost:5173",
|
||||
"http://localhost:8000",
|
||||
]
|
||||
|
||||
# Legacy compatibility - build SQLAlchemy URI if all parts provided
|
||||
if all([POSTGRES_USERNAME, POSTGRES_PW, POSTGRES_SERVER, POSTGRES_DBNAME00]):
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}:{}/{}".format(
|
||||
POSTGRES_USERNAME,
|
||||
POSTGRES_PW,
|
||||
POSTGRES_SERVER,
|
||||
POSTGRES_PORT,
|
||||
POSTGRES_DBNAME00
|
||||
)
|
||||
SQLALCHEMY_BINDS = {POSTGRES_DBNAME00: SQLALCHEMY_DATABASE_URI}
|
||||
else:
|
||||
SQLALCHEMY_DATABASE_URI = None
|
||||
SQLALCHEMY_BINDS = {}
|
||||
|
||||
@classmethod
|
||||
def validate_required(cls):
|
||||
"""Validate that all required environment variables are set."""
|
||||
required = {
|
||||
'POSTGRES_USERNAME': cls.POSTGRES_USERNAME,
|
||||
'POSTGRES_PASSWORD': cls.POSTGRES_PW,
|
||||
'POSTGRES_SERVER': cls.POSTGRES_SERVER,
|
||||
'POSTGRES_DBNAME': cls.POSTGRES_DBNAME00,
|
||||
'JWT_SECRET_KEY': cls.JWT_SECRET_KEY,
|
||||
}
|
||||
|
||||
missing = [key for key, value in required.items() if not value]
|
||||
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"Missing required environment variables: {', '.join(missing)}\n"
|
||||
"Please set these in your .env file or docker-compose environment."
|
||||
)
|
||||
|
||||
# Warn about weak JWT secret in production
|
||||
if cls.CURRENT_SETTINGS == 'PRODUCTION':
|
||||
if cls.JWT_SECRET_KEY and len(cls.JWT_SECRET_KEY) < 32:
|
||||
print("\033[93mWARNING: JWT_SECRET_KEY should be at least 32 characters for production\033[0m")
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def validate_payment_config(cls):
|
||||
"""Validate payment configuration (call before processing payments)."""
|
||||
if not cls.AUTH_NET_API_LOGIN_ID or not cls.AUTH_NET_TRANSACTION_KEY:
|
||||
raise ValueError(
|
||||
"Payment processing requires AUTH_NET_API_LOGIN_ID and AUTH_NET_TRANSACTION_KEY"
|
||||
)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def validate_email_config(cls):
|
||||
"""Validate email configuration (call before sending emails)."""
|
||||
required = {
|
||||
'SMTP_USERNAME': cls.SMTP_USERNAME,
|
||||
'SMTP_PASSWORD': cls.SMTP_PASSWORD,
|
||||
'SMTP_FROM_EMAIL': cls.SMTP_FROM_EMAIL,
|
||||
}
|
||||
|
||||
missing = [key for key, value in required.items() if not value]
|
||||
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"Email sending requires: {', '.join(missing)}"
|
||||
)
|
||||
return True
|
||||
34
settings_dev.py
Normal file
34
settings_dev.py
Normal file
@@ -0,0 +1,34 @@
|
||||
|
||||
|
||||
class ApplicationConfig:
|
||||
"""
|
||||
Basic Configuration for a generic User
|
||||
"""
|
||||
CURRENT_SETTINGS = 'DEVELOPMENT'
|
||||
# databases info
|
||||
POSTGRES_USERNAME = 'postgres'
|
||||
POSTGRES_PW = 'password'
|
||||
POSTGRES_SERVER = '192.168.1.204'
|
||||
POSTGRES_PORT = '5432'
|
||||
POSTGRES_DBNAME00 = 'eamco'
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
|
||||
POSTGRES_PW,
|
||||
POSTGRES_SERVER,
|
||||
POSTGRES_DBNAME00
|
||||
)
|
||||
SQLALCHEMY_BINDS = {'eamco': SQLALCHEMY_DATABASE_URI}
|
||||
|
||||
origins = [
|
||||
"http://localhost:9000",
|
||||
"https://localhost:9513",
|
||||
"http://localhost:9514",
|
||||
"http://localhost:9512",
|
||||
"http://localhost:9511",
|
||||
"http://localhost:5173",
|
||||
]
|
||||
|
||||
FRONTEND_URL = "http://localhost:3000"
|
||||
|
||||
# Authorize.net credentials
|
||||
AUTH_NET_API_LOGIN_ID = '9U6w96gZmX'
|
||||
AUTH_NET_TRANSACTION_KEY = '94s6Qy458mMNJr7G'
|
||||
31
settings_local.py
Normal file
31
settings_local.py
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
|
||||
class ApplicationConfig:
|
||||
"""
|
||||
Basic Configuration for a generic User
|
||||
"""
|
||||
CURRENT_SETTINGS = 'LOCAL'
|
||||
# databases info
|
||||
POSTGRES_USERNAME = 'postgres'
|
||||
POSTGRES_PW = 'password'
|
||||
POSTGRES_SERVER = '192.168.1.204'
|
||||
POSTGRES_PORT = '5432'
|
||||
POSTGRES_DBNAME00 = 'auburnoil'
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
|
||||
POSTGRES_PW,
|
||||
POSTGRES_SERVER,
|
||||
POSTGRES_DBNAME00
|
||||
)
|
||||
|
||||
SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI}
|
||||
|
||||
|
||||
origins = [
|
||||
"http://192.168.1.204:9000",
|
||||
"http://192.168.1.204:9613",
|
||||
"http://192.168.1.204:9614",
|
||||
"http://192.168.1.204:9612",
|
||||
"http://192.168.1.204:9611",
|
||||
]
|
||||
|
||||
FRONTEND_URL = "http://localhost:3000"
|
||||
28
settings_prod.py
Normal file
28
settings_prod.py
Normal file
@@ -0,0 +1,28 @@
|
||||
class ApplicationConfig:
|
||||
"""
|
||||
Basic Configuration for a generic User
|
||||
"""
|
||||
CURRENT_SETTINGS = 'PRODUCTION'
|
||||
# databases info
|
||||
POSTGRES_USERNAME = 'postgres'
|
||||
POSTGRES_PW = 'password'
|
||||
POSTGRES_SERVER = '192.168.1.204'
|
||||
POSTGRES_PORT = '5432'
|
||||
POSTGRES_DBNAME00 = 'auburnoil'
|
||||
SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME,
|
||||
POSTGRES_PW,
|
||||
POSTGRES_SERVER,
|
||||
POSTGRES_DBNAME00
|
||||
)
|
||||
SQLALCHEMY_BINDS = {'auburnoil': SQLALCHEMY_DATABASE_URI}
|
||||
|
||||
origins = [
|
||||
"https://oil.edwineames.com",
|
||||
"https://apiauto.edwineames.com",
|
||||
]
|
||||
|
||||
FRONTEND_URL = "https://portal.auburnoil.com"
|
||||
|
||||
# Authorize.net credentials
|
||||
AUTH_NET_API_LOGIN_ID = '4d2Mn6H23R'
|
||||
AUTH_NET_TRANSACTION_KEY = '7B94d8xfTQXv37WS'
|
||||
Reference in New Issue
Block a user