From 148200dee212a863df3c8baafc15a71b790b960c Mon Sep 17 00:00:00 2001 From: Edwin Eames Date: Mon, 15 Sep 2025 15:39:31 -0400 Subject: [PATCH] first commit --- .gitignore | 122 +++++++++++++++ Dockerfile.dev | 19 +++ Dockerfile.local | 19 +++ Dockerfile.prod | 19 +++ README.md | 3 + app/__init__.py | 0 app/models/__init__.py | 0 app/models/auto.py | 86 +++++++++++ app/models/cards.py | 29 ++++ app/models/customer.py | 31 ++++ app/models/delivery.py | 51 +++++++ app/models/employee.py | 29 ++++ app/models/money.py | 28 ++++ app/models/pricing.py | 34 +++++ app/routers/__init__.py | 0 app/routers/confirm.py | 106 +++++++++++++ app/routers/delivery.py | 95 ++++++++++++ app/routers/main.py | 305 ++++++++++++++++++++++++++++++++++++++ app/schema/customer.py | 0 app/schema/delivery.py | 46 ++++++ app/schema/money.py | 21 +++ app/script/update_auto.py | 26 ++++ config.py | 22 +++ database.py | 28 ++++ main.py | 41 +++++ requirements.txt | 7 + settings_dev.py | 20 +++ settings_local.py | 20 +++ settings_prod.py | 17 +++ 29 files changed, 1224 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile.dev create mode 100644 Dockerfile.local create mode 100644 Dockerfile.prod create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/models/__init__.py create mode 100644 app/models/auto.py create mode 100644 app/models/cards.py create mode 100644 app/models/customer.py create mode 100644 app/models/delivery.py create mode 100644 app/models/employee.py create mode 100644 app/models/money.py create mode 100644 app/models/pricing.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/confirm.py create mode 100644 app/routers/delivery.py create mode 100644 app/routers/main.py create mode 100644 app/schema/customer.py create mode 100644 app/schema/delivery.py create mode 100644 app/schema/money.py create mode 100644 app/script/update_auto.py create mode 100644 config.py create mode 100644 database.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 settings_dev.py create mode 100644 settings_local.py create mode 100644 settings_prod.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c87328 --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..d501bc1 --- /dev/null +++ b/Dockerfile.dev @@ -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 \ No newline at end of file diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..386dd58 --- /dev/null +++ b/Dockerfile.local @@ -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 \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..265b50b --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,19 @@ +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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8aadaa3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Eamco Playground + +This is a FastAPI project for Eamco playground application. diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/auto.py b/app/models/auto.py new file mode 100644 index 0000000..92f412d --- /dev/null +++ b/app/models/auto.py @@ -0,0 +1,86 @@ +from sqlalchemy import (Column, Integer, + DECIMAL, TEXT, + VARCHAR, DATE, INTEGER) +from datetime import datetime +from database import Base + + + + +class Auto_Update(Base): + __tablename__ = 'auto_update' + + id = Column(Integer, + primary_key=True, + autoincrement=True, + unique=False) + + last_updated = Column(DATE()) + + +class Auto_Temp(Base): + __tablename__ = 'auto_temp' + + id = Column(Integer, + primary_key=True, + autoincrement=True, + unique=False) + + todays_date = Column(DATE) + temp = Column(DECIMAL(5, 2)) + temp_max = Column(DECIMAL(5, 2)) + temp_min = Column(DECIMAL(5, 2)) + temp_avg = Column(DECIMAL(5, 2)) + degree_day = Column(INTEGER()) + + + +class Auto_Delivery(Base): + __tablename__ = 'auto_delivery' + + id = Column(Integer, + primary_key=True, + autoincrement=True, + unique=False) + + customer_id = Column(INTEGER()) + account_number = Column(VARCHAR(25)) + customer_town = Column(VARCHAR(140)) + customer_state = Column(INTEGER) + customer_address = Column(VARCHAR(1000)) + customer_zip = Column(VARCHAR(25)) + customer_full_name = Column(VARCHAR(250)) + last_fill = Column(DATE()) + days_since_last_fill = Column(INTEGER()) + last_updated = Column(DATE()) + estimated_gallons_left = Column(DECIMAL(6, 2)) + estimated_gallons_left_prev_day = Column(DECIMAL(6, 2)) + tank_height = Column(VARCHAR(25)) + tank_size = Column(VARCHAR(25)) + house_factor = Column(DECIMAL(5, 2)) + auto_status = Column(INTEGER()) + + +class Tickets_Auto_Delivery(Base): + __tablename__ = 'auto_tickets' + + id = Column(Integer, + primary_key=True, + autoincrement=True, + unique=False) + customer_id = Column(INTEGER()) + account_number = Column(VARCHAR(25)) + + customer_town = Column(VARCHAR(140)) + customer_state = Column(Integer) + customer_address = Column(VARCHAR(1000)) + customer_zip = Column(VARCHAR(25)) + customer_full_name = Column(VARCHAR(250)) + customer_zip = Column(VARCHAR(25)) + + oil_prices_id = Column(INTEGER()) + fill_date = Column(DATE()) + gallons_delivered = Column(DECIMAL(6, 2)) + price_per_gallon = Column(DECIMAL(6, 2)) + + total_amount_customer = Column(DECIMAL(6, 2)) diff --git a/app/models/cards.py b/app/models/cards.py new file mode 100644 index 0000000..3a60ec7 --- /dev/null +++ b/app/models/cards.py @@ -0,0 +1,29 @@ + +from sqlalchemy import (Column, Integer, + BOOLEAN, DATE, String, + VARCHAR, + INTEGER) + +from database import Base + + +class Card_Card(Base): + __tablename__ = 'card_card' + __table_args__ = {"schema": "public"} + + id = Column(Integer, + primary_key=True, + autoincrement=True, + unique=False) + date_added = Column(DATE()) + user_id = Column(INTEGER()) + card_number = Column(VARCHAR(50)) + last_four_digits = Column(INTEGER()) + name_on_card = Column(VARCHAR(500)) + expiration_month = Column(INTEGER()) + expiration_year = Column(INTEGER()) + type_of_card = Column(VARCHAR(500)) + security_number = Column(INTEGER()) + accepted_or_declined = Column(INTEGER()) + main_card = Column(BOOLEAN()) + auth_net_payment_profile_id = Column(String, unique=True, index=True, nullable=False) \ No newline at end of file diff --git a/app/models/customer.py b/app/models/customer.py new file mode 100644 index 0000000..d3e51fd --- /dev/null +++ b/app/models/customer.py @@ -0,0 +1,31 @@ +from sqlalchemy import (Column, Integer, + String, + VARCHAR, + DATE, INTEGER) +from database import Base + + +class Customer_Customer(Base): + __tablename__ = 'customer_customer' + + 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(DATE()) + 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) + + diff --git a/app/models/delivery.py b/app/models/delivery.py new file mode 100644 index 0000000..4af39fe --- /dev/null +++ b/app/models/delivery.py @@ -0,0 +1,51 @@ +from sqlalchemy import (Column, INTEGER, + DECIMAL, TEXT, + VARCHAR, TIMESTAMP, DATE) +import datetime +from database import Base + + +class Delivery(Base): + __tablename__ = "delivery_delivery" + + 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(VARCHAR(10)) + + gallons_ordered = Column(INTEGER) + customer_asked_for_fill = Column(INTEGER) + gallons_delivered = Column(DECIMAL(6, 2)) + customer_filled = Column(INTEGER) + + delivery_status = Column(INTEGER) + when_ordered = Column(DATE(), ) + when_delivered = Column(DATE()) + expected_delivery_date = Column(DATE(), default=None) + automatic = Column(INTEGER) + automatic_id = Column(INTEGER) + oil_id = Column(INTEGER) + supplier_price = Column(DECIMAL(6, 2)) + customer_price = Column(DECIMAL(6, 2)) + customer_temperature = Column(DECIMAL(6, 2)) + dispatcher_notes = Column(TEXT()) + prime = Column(INTEGER) + same_day = Column(INTEGER) + 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)) \ No newline at end of file diff --git a/app/models/employee.py b/app/models/employee.py new file mode 100644 index 0000000..af5c284 --- /dev/null +++ b/app/models/employee.py @@ -0,0 +1,29 @@ +from sqlalchemy import (Column, INTEGER, + DECIMAL, TEXT, + VARCHAR, TIMESTAMP, DATE) +import datetime +from database import Base + + +class Employee_Employee(Base): + __tablename__ = 'employee_employee' + __table_args__ = {"schema": "public"} + + id = Column(INTEGER, + primary_key=True, + autoincrement=True, + unique=False) + + user_id = Column(INTEGER) + employee_first_name = Column(VARCHAR(250)) + employee_last_name = Column(VARCHAR(250)) + employee_apt = Column(VARCHAR(250)) + employee_address = Column(VARCHAR(1000)) + employee_town = Column(VARCHAR(140)) + employee_state = Column(VARCHAR(140)) + employee_zip = Column(VARCHAR(25)) + employee_birthday = Column(DATE()) + employee_type = Column(INTEGER) + employee_phone_number = Column(VARCHAR(25)) + employee_start_date = Column(DATE()) + employee_end_date = Column(DATE(), default=None) diff --git a/app/models/money.py b/app/models/money.py new file mode 100644 index 0000000..f9cd8ad --- /dev/null +++ b/app/models/money.py @@ -0,0 +1,28 @@ +from sqlalchemy import (Column, Integer, + DECIMAL, Text, + VARCHAR, TIMESTAMP, Date) +from datetime import datetime, timezone +from database import Base + + +class MoneyDelivery(Base): + __tablename__ = 'money_delivery' + + id = Column(Integer, + primary_key=True, + autoincrement=True, + unique=False) + + delivery_id = Column(Integer) + time_added = Column(TIMESTAMP(), default=datetime.utcnow()) + gallons_delivered = Column(DECIMAL(6, 2)) + supplier_price = Column(DECIMAL(6, 2)) + customer_price = Column(DECIMAL(6, 2)) + total_amount_oil = Column(DECIMAL(6, 2)) + total_amount_prime = Column(DECIMAL(6, 2)) + total_amount_same_day = Column(DECIMAL(6, 2)) + total_amount_fee = Column(DECIMAL(6, 2)) + total_amount = Column(DECIMAL(6, 2)) + taxes_paid = Column(DECIMAL(6, 2)) + total_profit = Column(DECIMAL(6, 2)) + total_profit_oil = Column(DECIMAL(6, 2)) \ No newline at end of file diff --git a/app/models/pricing.py b/app/models/pricing.py new file mode 100644 index 0000000..4fac899 --- /dev/null +++ b/app/models/pricing.py @@ -0,0 +1,34 @@ +from sqlalchemy import (Column, Integer, DECIMAL, TIMESTAMP) +from datetime import datetime +from database import Base + + +class Pricing_Oil_Oil(Base): + __tablename__ = 'pricing_oil_oil' + + + 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)) + date = Column(TIMESTAMP(), default=datetime.utcnow()) + + + +class Pricing_Taxes(Base): + __tablename__ = 'taxes_pricing' + + + id = Column(Integer, + primary_key=True, + autoincrement=True, + unique=False) + state_id = Column(Integer) + taxes_oil = Column(DECIMAL(6, 2)) + taxes_other = Column(DECIMAL(6, 2)) \ No newline at end of file diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/confirm.py b/app/routers/confirm.py new file mode 100644 index 0000000..18ff681 --- /dev/null +++ b/app/routers/confirm.py @@ -0,0 +1,106 @@ + +from fastapi import APIRouter, Request +from datetime import date +from database import session +from pyowm import OWM +from decimal import Decimal +from app.models.auto import Auto_Delivery, Tickets_Auto_Delivery +from app.models.pricing import Pricing_Oil_Oil + + + +from app.script.update_auto import calc_home_factor + +router = APIRouter( + prefix="/confirm", + tags=["confirm"], + responses={404: {"description": "Not found"}}, +) + + +@router.put("/auto/update/{autoid}") +async def update_auto(autoid: int, request: Request): + + request_body = await request.json() + gallons_delivered = request_body['gallons_delivered'] + gallons_delivered = Decimal(gallons_delivered) + + + get_auto_delivery = (session + .query(Auto_Delivery) + .filter(Auto_Delivery.id == autoid) + .first()) + + gallons_put_in_home = Decimal(gallons_delivered) + + customer_home_factor = get_auto_delivery.house_factor + + + new_home_factor = calc_home_factor(gallons_put_in_home = gallons_put_in_home, + current_house_factor=customer_home_factor) + + gallons_left_buffer = int(get_auto_delivery.tank_size) - 30 + + + get_auto_delivery.house_factor = new_home_factor + get_auto_delivery.tank_height = 'Full' + get_auto_delivery.last_fill = date.today() + get_auto_delivery.estimated_gallons_left = gallons_left_buffer + get_auto_delivery.estimated_gallons_left_prev_day = gallons_left_buffer + get_auto_delivery.auto_status = 1 + get_auto_delivery.days_since_last_fill = 0 + + session.add(get_auto_delivery) + + session.commit() + + return ({"ok": True}), 200 + + + + +@router.post("/auto/create/{autoid}") +async def create_auto_ticket(autoid: int, request: Request): + + + request_body = await request.json() + gallons_delivered = request_body['gallons_delivered'] + gallons_delivered = Decimal(gallons_delivered) + + + get_auto_delivery = (session + .query(Auto_Delivery) + .filter(Auto_Delivery.id == autoid) + .first()) + get_todays_price = (session.query(Pricing_Oil_Oil) + .order_by(Pricing_Oil_Oil.id.desc()) + .first()) + gallons_put_in_home = Decimal(gallons_delivered) + todays_price = Decimal(get_todays_price.price_for_customer) + total_amount = gallons_put_in_home * todays_price + + create_new_ticket = Tickets_Auto_Delivery( + customer_id = get_auto_delivery.customer_id, + account_number = get_auto_delivery.account_number, + customer_town = get_auto_delivery.customer_town, + customer_state = get_auto_delivery.customer_state, + customer_address = get_auto_delivery.customer_address, + customer_zip =get_auto_delivery.customer_zip, + customer_full_name = get_auto_delivery.customer_full_name, + oil_prices_id = get_todays_price.id, + gallons_delivered = gallons_delivered, + price_per_gallon = get_todays_price.price_for_customer, + total_amount_customer = total_amount, + fill_date = date.today(), + ) + + + session.add(create_new_ticket) + + session.commit() + + return ({ + "ok": True, + "auto_ticket_id":create_new_ticket.id + }), 200 + diff --git a/app/routers/delivery.py b/app/routers/delivery.py new file mode 100644 index 0000000..c2b9e9d --- /dev/null +++ b/app/routers/delivery.py @@ -0,0 +1,95 @@ +from fastapi import APIRouter, Request, Depends +from fastapi.responses import JSONResponse +from fastapi.encoders import jsonable_encoder +from database import session + +from app.models.auto import Auto_Delivery, Tickets_Auto_Delivery +from app.models.delivery import Delivery + + + +router = APIRouter( + prefix="/delivery", + tags=["delivery"], + responses={404: {"description": "Not found"}}, +) + + + +@router.get("/past/{customer_id}", status_code=200) +def past(customer_id: int): + + get_customer_past_delivery = (session + .query(Delivery) + .filter(Delivery.customer_id == customer_id) + .order_by(Delivery.id.desc()) + .all()) + print(get_customer_past_delivery) + return JSONResponse(content=jsonable_encoder(get_customer_past_delivery), status_code=200) + + + +@router.get("/all/customers", status_code=200) +def get_delivery_customers(): + + + automatics = ( + session.query(Auto_Delivery) + .filter(Auto_Delivery.auto_status == 1) + .order_by(Auto_Delivery.estimated_gallons_left.asc()) + .all() + ) + + return JSONResponse(content=jsonable_encoder(automatics), status_code=200) + + + +@router.get("/driver/{driver_employee_id}", status_code=200) +def get_delivery_for_specific_driver(driver_employee_id: int): + automatics = ( + session.query(Delivery) + .filter(Delivery.driver_employee_id == driver_employee_id) + .filter(Delivery.automatic == 1) + .filter(Delivery.delivery_status == 0) + .all() + ) + + return JSONResponse(content=jsonable_encoder(automatics), status_code=200) + + +@router.get("/{auto_id}", status_code=200) +def get_auto(auto_id): + + get_delivery = ( + session.query(Auto_Delivery) + .filter(Auto_Delivery.id == auto_id) + .first() + ) + + return JSONResponse(content=jsonable_encoder(get_delivery), status_code=200) + + + +@router.get("/delivery/{delivery_id_order}", status_code=200) +def get_delivery(delivery_id_order): + + get_delivery = ( + session.query(Tickets_Auto_Delivery) + .filter(Tickets_Auto_Delivery.id == delivery_id_order) + .first() + ) + + return JSONResponse(content=jsonable_encoder(get_delivery), status_code=200) + + +@router.get("/all/profile/{customer_id}", status_code=200) +def get_autos_customers(customer_id): + + get_delivery = ( + session.query(Tickets_Auto_Delivery) + .filter(Tickets_Auto_Delivery.customer_id == customer_id) + .order_by(Tickets_Auto_Delivery.id.asc()) + .all() + ) + + return JSONResponse(content=jsonable_encoder(get_delivery), status_code=200) \ No newline at end of file diff --git a/app/routers/main.py b/app/routers/main.py new file mode 100644 index 0000000..539c99d --- /dev/null +++ b/app/routers/main.py @@ -0,0 +1,305 @@ +## File: app/routers/main.py (or your equivalent path) + +from fastapi import APIRouter, HTTPException +from datetime import datetime +from sqlalchemy.orm import Session +from sqlalchemy import func + + +# Your existing imports +from database import session +from pyowm import OWM + +from app.models.customer import Customer_Customer +from app.models.cards import Card_Card +# Imports needed for the migration task. +# YOU MUST ADJUST THESE IMPORTS TO MATCH YOUR PROJECT'S ACTUAL STRUCTURE. +from database import session # Assuming your SessionLocal is here for a clean session +from authorizenet import apicontractsv1 +from authorizenet.constants import constants +from authorizenet.apicontrollers import ( + createCustomerProfileController, + createCustomerPaymentProfileController, + getCustomerProfileIdsController, + getCustomerProfileController +) +# --- ROUTER DEFINITION --- + +router = APIRouter( + prefix="/main", + tags=["main"], + responses={404: {"description": "Not found"}}, +) + +API_LOGIN_ID = '9U6w96gZmX' +TRANSACTION_KEY = '94s6Qy458mMNJr7G' + +constants.show_url_on_request = True +constants.environment = constants.SANDBOX + + +@router.post("/maintenance/migrate-cards-now") +def run_card_migration_synchronously(): + """ + Triggers a one-time, SYNCHRONOUS task to migrate all existing, insecurely + stored credit cards from the `card_card` table to secure Authorize.Net CIM profiles. + This function is self-contained and does not use `crud` or `schemas`. + + WARNING: This is a long-running process and the request may time out. + Monitor server logs for completion. This endpoint should be removed after use. + """ + + def _create_authnet_profile(customer_obj: Customer_Customer, card_obj: Card_Card): + """Helper to create a new Authorize.Net Customer Profile.""" + merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) + expiration_date_str = f"{card_obj.expiration_year}-{str(card_obj.expiration_month).zfill(2)}" + + # Validate expiration + month = None + year = None + if card_obj.expiration_month: + try: + month = int(card_obj.expiration_month) + except: + pass + if card_obj.expiration_year: + try: + year = int(card_obj.expiration_year) + except: + pass + if not year or not month or month < 1 or month > 12 or year < 2000 or year > 2050: + raise ValueError(f"Invalid expiration year={card_obj.expiration_year}, month={card_obj.expiration_month} for card ID {card_obj.id}") + expiration_date_str = f"{year:04d}-{month:02d}" + # // FIX 1: Sanitize card number by removing all non-digit characters + sanitized_card_number = ''.join(filter(str.isdigit, str(card_obj.card_number))) + if len(sanitized_card_number) < 13 or len(sanitized_card_number) > 19 or not sanitized_card_number.isdigit(): + raise ValueError(f"Invalid card number length {len(sanitized_card_number)} or non-numeric for card ID {card_obj.id}") + # Sanitize CVV + cardCode = str(card_obj.security_number).strip() + if len(cardCode) < 3 or len(cardCode) > 4 or not cardCode.isdigit(): + raise ValueError(f"Invalid CVV length {len(cardCode)} or non-numeric for card ID {card_obj.id}") + # Debug print + print(f"DEBUG SEND CREATE: cardNumber='{sanitized_card_number}', expirationDate='{expiration_date_str}', cardCode='{cardCode}', customerID={customer_obj.id}, name='{customer_obj.customer_first_name} {customer_obj.customer_last_name}', address='{customer_obj.customer_address}', city='{customer_obj.customer_town}', zip='{customer_obj.customer_zip}', email='{customer_obj.customer_email}'") + creditCard = apicontractsv1.creditCardType( + cardNumber=sanitized_card_number, + expirationDate=expiration_date_str, + cardCode=cardCode + ) + billTo = apicontractsv1.customerAddressType( + firstName=customer_obj.customer_first_name, + lastName=customer_obj.customer_last_name, + address=customer_obj.customer_address, + city=customer_obj.customer_town, + zip=customer_obj.customer_zip + ) + paymentProfile = apicontractsv1.customerPaymentProfileType( + billTo=billTo, + payment=apicontractsv1.paymentType(creditCard=creditCard) + ) + customerProfile = apicontractsv1.customerProfileType( + merchantCustomerId=str(customer_obj.id), + email=customer_obj.customer_email, + paymentProfiles=[paymentProfile] + ) + request = apicontractsv1.createCustomerProfileRequest( + merchantAuthentication=merchantAuth, + profile=customerProfile, + validationMode="testMode" + ) + controller = createCustomerProfileController(request) + controller.execute() + response = controller.getresponse() + + if response is not None and hasattr(response, 'messages') and response.messages.resultCode == "Ok": + profile_id = response.customerProfileId if hasattr(response, 'customerProfileId') else None + payment_ids = response.customerPaymentProfileIdList.numericString if hasattr(response, 'customerPaymentProfileIdList') and hasattr(response.customerPaymentProfileIdList, 'numericString') else [] + payment_id = payment_ids[0] if payment_ids else None + return str(profile_id), str(payment_id) if payment_id else None + else: + # Handle errors + error_msg = "Unknown Authorize.Net Error" + if response is not None and hasattr(response, 'messages') and response.messages is not None and hasattr(response.messages, 'message') and response.messages.message: + msg = response.messages.message[0] if isinstance(response.messages.message, list) else response.messages.message + if msg and hasattr(msg, 'text') and msg.text is not None: + if isinstance(msg.text, str): + error_msg = msg.text + else: + # Handle case where text might be an object with 'text' attribute + error_msg = msg.text.text if hasattr(msg.text, 'text') else str(msg.text) + print(f" AUTH.NET ERROR: {error_msg}") + return None, None + + def _add_authnet_payment_profile(profile_id: str, customer_obj: Customer_Customer, card_obj: Card_Card): + """Helper to add a new Payment Profile to an existing Customer Profile.""" + merchantAuth = apicontractsv1.merchantAuthenticationType(name=API_LOGIN_ID, transactionKey=TRANSACTION_KEY) + expiration_date_str = f"{card_obj.expiration_year}-{str(card_obj.expiration_month).zfill(2)}" + + # // FIX 1 (Applied here as well): Sanitize card number + sanitized_card_number = ''.join(filter(str.isdigit, str(card_obj.card_number))) + + creditCard = apicontractsv1.creditCardType( + cardNumber=sanitized_card_number, + expirationDate=expiration_date_str, + cardCode=str(card_obj.security_number).strip() + ) + paymentProfile = apicontractsv1.customerPaymentProfileType( + billTo=apicontractsv1.customerAddressType(firstName=customer_obj.customer_first_name, lastName=customer_obj.customer_last_name), + payment=apicontractsv1.paymentType(creditCard=creditCard) + ) + request = apicontractsv1.createCustomerPaymentProfileRequest( + merchantAuthentication=merchantAuth, + customerProfileId=profile_id, + paymentProfile=paymentProfile, + validationMode="testMode" + ) + controller = createCustomerPaymentProfileController(request) + controller.execute() + response = controller.getresponse() + if response is not None and response.messages.resultCode == "Ok": + return str(response.customerPaymentProfileId) + else: + # // FIX 2 (Applied here as well): Robust error message parsing + error_msg = "Unknown Authorize.Net Error" + if response is not None and hasattr(response, 'messages') and response.messages is not None and hasattr(response.messages, 'message') and response.messages.message: + msg = response.messages.message[0] + if hasattr(msg, 'text') and msg.text is not None: + error_msg = msg.text.text + print(f" AUTH.NET ERROR: {error_msg}") + return None + + # --- MIGRATION SCRIPT LOGIC --- + print("="*60) + print("MIGRATION STARTED: Migrating all customer cards to Authorize.Net CIM.") + print(f"Start Time: {datetime.now()}") + print("="*60) + + customers_processed = 0 + cards_migrated = 0 + error_count = 0 + + try: + customers_to_migrate = (session + .query(Customer_Customer) + .filter(Customer_Customer.auth_net_profile_id == None) + .order_by(func.random()) + .limit(1) + .all()) + + total_customers = len(customers_to_migrate) + print(f"Found {total_customers} customers to migrate.") + + for index, customer in enumerate(customers_to_migrate): + customers_processed += 1 + print(f"\n--- Processing Customer {index + 1}/{total_customers} (ID: {customer.id}, Name: {customer.customer_first_name}) ---") + + insecure_cards = (session + .query(Card_Card) + .filter(Card_Card.user_id == customer.id) + .all()) + + print(f"DEBUG: Customer {customer.id} has {len(insecure_cards)} cards") + customer_profile_id_for_session = None + + # # Check for existing profile + # existing_profile, existing_payment_ids = _get_existing_profile(str(customer.id)) + # if existing_profile and existing_payment_ids: + # print(f" -> Found existing Authorize.Net profile ID {existing_profile}") + # customer.auth_net_profile_id = existing_profile + # session.add(customer) + # session.commit() + # for i, card_data in enumerate(insecure_cards): + # if i < len(existing_payment_ids): + # display_card_number = ''.join(filter(str.isdigit, str(card_data.card_number))) + # if not display_card_number: + # print(f" SKIPPING CARD: Card number is empty after sanitization for card ID {card_data.id}") + # error_count += 1 + # continue + # card_data.auth_net_payment_profile_id = existing_payment_ids[i] + # session.add(card_data) + # session.commit() + # print(f" SUCCESS: Mapped existing Payment ID {existing_payment_ids[i]} to card ending ...{display_card_number[-4:]}") + # cards_migrated += 1 + # else: + # print(f" WARNING: No existing payment profile Kawasaki for card {card_data.id}") + # error_count += 1 + # continue # skip creation loop + + for i, card_data in enumerate(insecure_cards): + try: + # Basic data validation before sending + if not card_data.card_number or not card_data.expiration_year or not card_data.expiration_month: + print(f" SKIPPING CARD: Incomplete card data for card ID {card_data.id}.") + error_count += 1 + continue + + print(f"DEBUG RAW: Card ID {card_data.id}: number='{card_data.card_number}', year='{card_data.expiration_year}', month='{card_data.expiration_month}', cvv='{card_data.security_number}', name='{customer.customer_first_name} {customer.customer_last_name}'") + + # Sanitize card number for display + display_card_number = ''.join(filter(str.isdigit, str(card_data.card_number))) + + if not display_card_number: + print(f" SKIPPING CARD: Card number is empty after sanitization for card ID {card_data.id}.") + error_count += 1 + continue + + if i == 0: + print(f" -> Creating new CIM profile with card ending in ...{display_card_number[-4:]}") + profile_id, payment_id = _create_authnet_profile(customer, card_data) + + if profile_id and payment_id: + customer.auth_net_profile_id = profile_id + session.add(customer) + + customer_profile_id_for_session = profile_id + + card_data.auth_net_payment_profile_id = payment_id + session.add(card_data) + session.commit() + + print(f" SUCCESS: Created Profile ID {profile_id} and Payment ID {payment_id}") + cards_migrated += 1 + else: + print(f" ERROR: Failed to create profile for customer {customer.id}.") + error_count += 1 + break + else: + if not display_card_number: + print(f" SKIPPING CARD: Card number is empty after sanitization for card ID {card_data.id}.") + error_count += 1 + continue + print(f" -> Adding additional card ending in ...{display_card_number[-4:]}") + payment_id = _add_authnet_payment_profile(customer_profile_id_for_session, customer, card_data) + if payment_id: + card_data.auth_net_payment_profile_id = payment_id + session.add(card_data) + session.commit() + print(f" SUCCESS: Added Payment ID {payment_id}") + cards_migrated += 1 + else: + print(f" ERROR: Failed to add additional card for customer {customer.id}.") + error_count += 1 + + except Exception as e: + print(f" CRITICAL ERROR processing a card for customer {customer.id}: {str(e)}") + error_count += 1 + session.rollback() + + except Exception as e: + print(f"A critical error occurred during the migration process: {e}") + session.rollback() + return {"ok": False, "error": f"Migration failed with a critical error: {e}"}, 500 + + finally: + print("="*60) + print("MIGRATION FINISHED!") + print(f"End Time: {datetime.now()}") + print(f"Summary: Processed {customers_processed} customers, migrated {cards_migrated} cards, encountered {error_count} errors.") + print("="*60) + + return { + "ok": True, + "message": "Migration process completed successfully.", + "customers_processed": customers_processed, + "cards_migrated": cards_migrated, + "errors": error_count, + }, 200 diff --git a/app/schema/customer.py b/app/schema/customer.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schema/delivery.py b/app/schema/delivery.py new file mode 100644 index 0000000..f20f825 --- /dev/null +++ b/app/schema/delivery.py @@ -0,0 +1,46 @@ +from pydantic import BaseModel, Field +from datetime import datetime, timezone + + +def datetime_now() -> datetime: + return datetime.now(timezone.utc) + +class DeliverPricing(BaseModel): + prime: bool + same_day: bool + gallons_delivered: float + + + +class SchemaDelivery(BaseModel): + customer_id: int + customer_name: str + customer_address: str + customer_town: str + customer_state: int + customer_zip: int + gallons_ordered: float + customer_asked_for_fill: int + gallons_delivered: float + customer_filled: int + delivery_status: int + when_ordered: datetime = Field(default_factory=datetime_now) + when_delivered: datetime = Field(default_factory=datetime_now) + expected_delivery_date: datetime = Field(default_factory=datetime_now) + automatic: int + oil_id: int + supplier_price: float + customer_price: float + customer_temperature: float + dispatcher_notes: str + prime: int + same_day: int + payment_type: int + payment_card_id: int + cash_recieved: float + driver_employee_id: int + driver_first_name: str + driver_last_name: str + pre_charge_amount: float + total_price: float + final_price: float \ No newline at end of file diff --git a/app/schema/money.py b/app/schema/money.py new file mode 100644 index 0000000..a80ac63 --- /dev/null +++ b/app/schema/money.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel, Field +from datetime import datetime, timezone + + +def datetime_now() -> datetime: + return datetime.now(timezone.utc) + +class SchemaMoney(BaseModel): + delivery_id: int + time_added: datetime + gallons_delivered: float + supplier_price: float + customer_price: float + total_amount_oil: float + total_amount_prime: float + total_amount_same_day: float + total_amount_fee: float + total_amount: float + taxes_paid: float + total_profit: float + diff --git a/app/script/update_auto.py b/app/script/update_auto.py new file mode 100644 index 0000000..e88678e --- /dev/null +++ b/app/script/update_auto.py @@ -0,0 +1,26 @@ +from decimal import Decimal +from database import session + + + + +def calc_home_factor(gallons_put_in_home, current_house_factor): + + + if 200.01 <= gallons_put_in_home <= 500: + customer_home_factor = Decimal(current_house_factor) + Decimal(1) + elif 170.01 <= gallons_put_in_home <= 200: + customer_home_factor = Decimal(current_house_factor) + Decimal(1.5) + elif 150.01 <= gallons_put_in_home <= 170: + customer_home_factor = Decimal(current_house_factor) + Decimal(1.25) + elif 120.01 <= gallons_put_in_home <= 150: + customer_home_factor = Decimal(current_house_factor) - Decimal(.25) + elif 90.01 <= gallons_put_in_home <= 120: + customer_home_factor = Decimal(current_house_factor) - Decimal(.50) + elif 0.01 <= gallons_put_in_home <= 90: + customer_home_factor = Decimal(current_house_factor) - Decimal(.75) + else: + customer_home_factor = Decimal(current_house_factor) + if customer_home_factor <= 0: + customer_home_factor = Decimal(.25) + return customer_home_factor \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..545e0d6 --- /dev/null +++ b/config.py @@ -0,0 +1,22 @@ +import os +def load_config(mode=os.environ.get('MODE')): + + try: + print(f"mode is {mode}") + 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 \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..1d2ab9b --- /dev/null +++ b/database.py @@ -0,0 +1,28 @@ +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_DBNAME00, + port=ApplicationConfig.POSTGRES_PORT +) + +engine = create_engine(url) + +Session = sessionmaker(autocommit=False, autoflush=False, bind=engine) +session = Session() + +Base = declarative_base() +Base.metadata.create_all(engine) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..532059d --- /dev/null +++ b/main.py @@ -0,0 +1,41 @@ +from fastapi import FastAPI +from app.routers import main, delivery, confirm +from fastapi.middleware.cors import CORSMiddleware + + +app = FastAPI() + + +app.include_router(main.router) +app.include_router(delivery.router) +app.include_router(confirm.router) + +origins = [ + "http://localhost:9000", + "https://localhost:9513", + "http://localhost:9514", + "http://localhost:9512", + "http://localhost:9511", + "http://192.168.1.204:9000", + "http://192.168.1.204:9513", + "http://192.168.1.204:9514", + "http://192.168.1.204:9512", + "http://192.168.1.204:9511", +] + + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + + + +@app.get("/") +def read_root(): + return {"Status": "Playground is online"} + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8fe2749 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn[standard] +psycopg2-binary +sqlalchemy +pyowm +setuptools +authorizenet \ No newline at end of file diff --git a/settings_dev.py b/settings_dev.py new file mode 100644 index 0000000..1c6e1d6 --- /dev/null +++ b/settings_dev.py @@ -0,0 +1,20 @@ + + +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 = 'eamco' + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME, + POSTGRES_PW, + POSTGRES_SERVER, + POSTGRES_DBNAME00 + ) + + SQLALCHEMY_BINDS = {'eamco': SQLALCHEMY_DATABASE_URI} \ No newline at end of file diff --git a/settings_local.py b/settings_local.py new file mode 100644 index 0000000..0c44f3f --- /dev/null +++ b/settings_local.py @@ -0,0 +1,20 @@ + + +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 = 'eamco' + SQLALCHEMY_DATABASE_URI = "postgresql+psycopg2://{}:{}@{}/{}".format(POSTGRES_USERNAME, + POSTGRES_PW, + POSTGRES_SERVER, + POSTGRES_DBNAME00 + ) + + SQLALCHEMY_BINDS = {'eamco': SQLALCHEMY_DATABASE_URI} diff --git a/settings_prod.py b/settings_prod.py new file mode 100644 index 0000000..be26947 --- /dev/null +++ b/settings_prod.py @@ -0,0 +1,17 @@ +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}