From f2faced23872084a14900dba54faa7bfd1fb8363 Mon Sep 17 00:00:00 2001 From: Edwin Eames Date: Mon, 22 Sep 2025 21:18:24 -0400 Subject: [PATCH] first commit --- Dockerfile.dev | 14 ++++++++ Dockerfile.local | 14 ++++++++ Dockerfile.prod | 16 +++++++++ app/__init__.py | 0 app/config.py | 22 ++++++++++++ app/database.py | 34 ++++++++++++++++++ app/main.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ app/models.py | 41 ++++++++++++++++++++++ app/voipms_client.py | 56 ++++++++++++++++++++++++++++++ config.py | 26 ++++++++++++++ requirements.txt | 4 +++ settings_dev.py | 35 +++++++++++++++++++ settings_local.py | 30 ++++++++++++++++ settings_prod.py | 22 ++++++++++++ 14 files changed, 396 insertions(+) create mode 100644 Dockerfile.dev create mode 100644 Dockerfile.local create mode 100644 Dockerfile.prod create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/database.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/voipms_client.py create mode 100644 config.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/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..508ae4b --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,14 @@ +FROM python:3.9 +ENV PYTHONFAULTHANDLER=1 + +ENV PYTHONUNBUFFERED=1 + +ENV MODE="DEVELOPMENT" +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..a9e93e0 --- /dev/null +++ b/Dockerfile.local @@ -0,0 +1,14 @@ +FROM python:3.9 +ENV PYTHONFAULTHANDLER=1 + +ENV PYTHONUNBUFFERED=1 + +ENV MODE="LOCAL" +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..92320fc --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,16 @@ +FROM python:3.9 + +ENV PYTHONFAULTHANDLER=1 + +ENV PYTHONUNBUFFERED=1 + +ENV MODE="PRODUCTION" + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..d4d46fd --- /dev/null +++ b/app/config.py @@ -0,0 +1,22 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + +class Settings(BaseSettings): + # Load settings from the .env file + model_config = SettingsConfigDict(env_file="../.env", env_file_encoding='utf-8') + + # VoIP.ms Credentials + voipms_api_username: str + voipms_api_password: str + + # Target DID and Destinations + target_did: str + target_sip_account: str + target_cellphone_1: str + target_cellphone_2: str + + # VoIP.ms API endpoint + voipms_api_url: str = "https://voip.ms/api/v1/rest.php" + + +# Create a single instance of the settings to be used throughout the app +settings = Settings() \ No newline at end of file diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000..d586acb --- /dev/null +++ b/app/database.py @@ -0,0 +1,34 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.engine import URL +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) + + +def get_db(): + db = Session() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..2a19f37 --- /dev/null +++ b/app/main.py @@ -0,0 +1,82 @@ +from fastapi import FastAPI, HTTPException, status +from .config import settings +from .voipms_client import update_did_routing +from .database import Session +from .models import Call + +app = FastAPI( + title="EAMCO VoIP.ms Controller", + description="An API to manage routing for a VoIP.ms DID.", + version="1.0.0", +) + + + +@app.get("/", tags=["General"]) +def read_root(): + """A simple root endpoint to confirm the API is running.""" + return {"message": f"Welcome to the VoIP.ms API for DID: {settings.target_did}"} + + +@app.post("/route/sip", status_code=status.HTTP_200_OK, tags=["DID Routing"]) +def route_to_sip_account(): + """ + Routes the target DID to the pre-configured SIP account. + """ + routing_string = f"sip:{settings.target_sip_account}" + try: + result = update_did_routing(did=settings.target_did, routing=routing_string) + target_phone = routing_string.split(':')[1] + db = Session() + db.add(Call(current_phone=target_phone)) + db.commit() + db.close() + return { + "message": f"Successfully routed DID {settings.target_did} to SIP account {settings.target_sip_account}", + "voipms_response": result + } + except HTTPException as e: + # Re-raise the exception to let FastAPI handle the response + raise e + + +@app.post("/route/cellphone1", status_code=status.HTTP_200_OK, tags=["DID Routing"]) +def route_to_cellphone_1(): + """ + Routes the target DID to the pre-configured Cellphone #1. + """ + routing_string = f"fwd:{settings.target_cellphone_1}" + try: + result = update_did_routing(did=settings.target_did, routing=routing_string) + target_phone = routing_string.split(':')[1] + db = Session() + db.add(Call(current_phone=target_phone)) + db.commit() + db.close() + return { + "message": f"Successfully routed DID {settings.target_did} to Cellphone #1 ({settings.target_cellphone_1})", + "voipms_response": result + } + except HTTPException as e: + raise e + + +@app.post("/route/cellphone2", status_code=status.HTTP_200_OK, tags=["DID Routing"]) +def route_to_cellphone_2(): + """ + Routes the target DID to the pre-configured Cellphone #2. + """ + routing_string = f"fwd:{settings.target_cellphone_2}" + try: + result = update_did_routing(did=settings.target_did, routing=routing_string) + target_phone = routing_string.split(':')[1] + db = Session() + db.add(Call(current_phone=target_phone)) + db.commit() + db.close() + return { + "message": f"Successfully routed DID {settings.target_did} to Cellphone #2 ({settings.target_cellphone_2})", + "voipms_response": result + } + except HTTPException as e: + raise e diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..308c5d6 --- /dev/null +++ b/app/models.py @@ -0,0 +1,41 @@ +## File: your_app/models.py + +from sqlalchemy import Column, Integer, String, Float, DateTime, Boolean, ForeignKey, Numeric +from .database import Base +import datetime + +class Customer(Base): + __tablename__ = "customer_customer" + + id = Column(Integer, primary_key=True, index=True) + + # --- ADD THIS COLUMN --- + # This stores the master profile ID from Authorize.Net's CIM. + auth_net_profile_id = Column(String(100)) + + # --- YOUR EXISTING COLUMNS --- + account_number = Column(String(25)) + customer_last_name = Column(String(250)) + customer_first_name = Column(String(250)) + customer_town = Column(String(140)) + customer_state = Column(Integer) + customer_zip = Column(String(25)) + customer_first_call = Column(DateTime) + customer_email = Column(String(500)) + customer_automatic = Column(Integer) + customer_phone_number = Column(String(25)) + customer_home_type = Column(Integer) + customer_apt = Column(String(140)) + customer_address = Column(String(1000)) + company_id = Column(Integer) + customer_latitude = Column(String(250)) + customer_longitude = Column(String(250)) + correct_address = Column(Boolean) + +# --- ADD THIS ENTIRE NEW MODEL --- +class Call(Base): + __tablename__ = "call_call" + + id = Column(Integer, primary_key=True, index=True) + current_phone = Column(String(500)) + created_at = Column(DateTime, default=datetime.datetime.utcnow) \ No newline at end of file diff --git a/app/voipms_client.py b/app/voipms_client.py new file mode 100644 index 0000000..446cf4f --- /dev/null +++ b/app/voipms_client.py @@ -0,0 +1,56 @@ +import requests +from fastapi import HTTPException, status + +from .config import settings + +def update_did_routing(did: str, routing: str): + """ + Calls the VoIP.ms API to update the routing for a specific DID. + + Args: + did (str): The phone number (DID) to update. + routing (str): The new routing string (e.g., 'sip:user@server' or 'fwd:15551234567'). + + Raises: + HTTPException: If the API call fails or returns an error. + + Returns: + dict: The JSON response from the VoIP.ms API on success. + """ + params = { + "api_username": settings.voipms_api_username, + "api_password": settings.voipms_api_password, + "method": "setDIDInfo", + "did": did, + "routing": routing, + } + + try: + response = requests.get(settings.voipms_api_url, params=params) + response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx) + + data = response.json() + + # VoIP.ms API has its own status field in the JSON response + if data.get("status") != "success": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"VoIP.ms API Error: {data.get('status')}" + ) + + return data + + except requests.exceptions.RequestException as e: + # Handle network errors, timeouts, etc. + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Failed to connect to VoIP.ms API: {e}" + ) + except Exception as e: + # Catch any other exceptions, including the ones we raised manually + if isinstance(e, HTTPException): + raise e # Re-raise if it's already an HTTPException + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An unexpected error occurred: {e}" + ) \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..ad92505 --- /dev/null +++ b/config.py @@ -0,0 +1,26 @@ +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': + print("poop") + print("poop") + print("poop") + 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/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c58e901 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi +uvicorn[standard] +pydantic-settings +requests \ No newline at end of file diff --git a/settings_dev.py b/settings_dev.py new file mode 100644 index 0000000..f9cbf65 --- /dev/null +++ b/settings_dev.py @@ -0,0 +1,35 @@ + + +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 port + "http://localhost:9516", # Authorize service port + +] + + # # Authorize.net credentials (Sandbox Test Credentials) + # API_LOGIN_ID = '5KP3u95bQpv' + # TRANSACTION_KEY = '346HZ32z3fP4hTG2' diff --git a/settings_local.py b/settings_local.py new file mode 100644 index 0000000..e249762 --- /dev/null +++ b/settings_local.py @@ -0,0 +1,30 @@ + + +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", +] + diff --git a/settings_prod.py b/settings_prod.py new file mode 100644 index 0000000..f2dbbc8 --- /dev/null +++ b/settings_prod.py @@ -0,0 +1,22 @@ +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", +]