diff --git a/apis/__init__.py b/apis/__init__.py index f411d02..65e728a 100644 --- a/apis/__init__.py +++ b/apis/__init__.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, Security +from fastapi import APIRouter, Depends from middleware.auth_dependency import auth_required from fastapi.security import HTTPBearer @@ -8,7 +8,7 @@ bearer_scheme = HTTPBearer(scheme_name="Bearer Authentication") from .endpoints import clinics, doctors, calender, appointments, patients, admin, auth, s3, users, clinicDoctor, dashboard, call_transcripts, notifications,sns, stripe api_router = APIRouter() -# api_router.include_router(twilio.router, prefix="/twilio") + api_router.include_router(clinics.router, prefix="/clinics", tags=["clinics"], dependencies=[Depends(auth_required)]) api_router.include_router(doctors.router, prefix="/doctors", tags=["doctors"]) @@ -24,9 +24,9 @@ api_router.include_router(sns.router, prefix="/sns", tags=["sns"], include_in_sc api_router.include_router(stripe.router, prefix="/stripe", tags=["stripe"]) api_router.include_router( - admin.router, - prefix="/admin", - dependencies=[Depends(auth_required)], + admin.router, + prefix="/admin", + dependencies=[Depends(auth_required)], tags=["admin"]) api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) diff --git a/apis/endpoints/auth.py b/apis/endpoints/auth.py index e12bb7e..e1beddc 100644 --- a/apis/endpoints/auth.py +++ b/apis/endpoints/auth.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, BackgroundTasks +from fastapi import APIRouter, BackgroundTasks, Request, status from services.authService import AuthService from schemas.CreateSchemas import UserCreate from schemas.ApiResponse import ApiResponse @@ -58,4 +58,10 @@ async def verify_otp(data: AuthOTP): return ApiResponse( data="OK", message="OTP verified successfully" - ) \ No newline at end of file + ) + +@router.get("/is-valid-domain") +async def is_valid_domain(req:Request): + host = req.headers.get("host") + is_valid = await ClinicServices().is_valid_domain(host) + return status.HTTP_200_OK if is_valid else status.HTTP_404_NOT_FOUND diff --git a/apis/endpoints/stripe.py b/apis/endpoints/stripe.py index e7353c3..46989be 100644 --- a/apis/endpoints/stripe.py +++ b/apis/endpoints/stripe.py @@ -11,19 +11,19 @@ stripe_service = StripeServices() # async def create_checkout_session(user_id: int): # return await stripe_service.create_checkout_session(1) -# @router.post("/create-subscription-checkout") -# async def create_subscription_checkout(): -# return await stripe_service.create_subscription_checkout( -# fees_to_be={ -# "per_call_charges": 10, -# "setup_fees": 100, -# "subscription_fees": 100, -# "total": 210 -# }, -# clinic_id=1, -# account_id="acct_1RT1UFPTNqn2kWQ8", -# customer_id="cus_SNn49FDltUcSLP" -# ) +@router.post("/create-subscription-checkout") +async def create_subscription_checkout(): + return await stripe_service.create_subscription_checkout( + fees_to_be={ + "per_call_charges": 10, + "setup_fees": 100, + "subscription_fees": 100, + "total": 210 + }, + clinic_id=1, + account_id="acct_1RT1UFPTNqn2kWQ8", + customer_id="cus_SNn49FDltUcSLP" + ) @router.get("/create-stripe-account-link", dependencies=[Depends(auth_required)]) async def create_stripe_account_link(req:Request): diff --git a/apis/endpoints/twilio.py b/apis/endpoints/twilio.py deleted file mode 100644 index 3fb5be6..0000000 --- a/apis/endpoints/twilio.py +++ /dev/null @@ -1,183 +0,0 @@ -# import asyncio -# import json -# import logging -# import os -# from fastapi import APIRouter, Request, WebSocket, status -# from twilio.twiml.voice_response import VoiceResponse, Connect -# from twilio.rest import Client -# from fastapi import WebSocket, Request, Response -# from enum import Enum -# from typing import Optional - -# from services.bot import run_bot -# from services.call_state import CallState - -# logger = logging.getLogger(__name__) - - -# router = APIRouter() - -# BASE_WS_URL = "wss://13.229.247.61:8000/api/twilio" -# BASE_URL = "http://13.229.247.61:8000/api/twilio" - -# DTMF_SWITCH_KEY = "*" # Key to switch between models - -# LOG_EVENT_TYPES = [ -# "error", -# "response.content.done", -# "rate_limits.updated", -# "response.done", -# "input_audio_buffer.committed", -# "input_audio_buffer.speech_stopped", -# "input_audio_buffer.speech_started", -# "session.created", -# ] - -# SHOW_TIMING_MATH = False - -# MENU_OPTIONS = """ -# Press 1 for Model 1. -# Press 2 for Model 2. -# Press 3 for Model 3. -# Press 4 for Model 4. -# Press 5 for Model 5. -# Press 0 to repeat the options. -# """ - -# call_state = CallState - - -# # Define model options as enum for type safety -# class ModelOption(int, Enum): -# MODEL_1 = 1 -# MODEL_2 = 2 -# MODEL_3 = 3 -# MODEL_4 = 4 -# MODEL_5 = 5 - - -# @router.post("/call") -# async def get_call(): - -# SID = os.getenv("TWILIO_SID") -# AUTH_TOKEN = os.getenv("TWILIO_AUTH") - -# client = Client(SID, AUTH_TOKEN) - -# call = client.calls.create( -# from_="+14149466486", -# to="+917984372159", -# record=True, -# url=f"{BASE_URL}/receive", -# ) - -# return status.HTTP_200_OK - - -# # @router.websocket("/media-stream/{id}") -# # async def handle_media_stream(websocket: WebSocket, id: int): -# # """Handle WebSocket connections between Twilio and OpenAI.""" -# # print(f"Client connected with id: {id}") -# # await websocket.accept() -# # start_data = websocket.iter_text() -# # await start_data.__anext__() -# # call_data = json.loads(await start_data.__anext__()) -# # print(call_data, flush=True) -# # stream_sid = call_data["start"]["streamSid"] -# # print("WebSocket connection accepted") -# # await run_bot(websocket, stream_sid, False, option=id) - - -# @router.websocket("/media-stream/{id}") -# async def handle_media_stream(websocket: WebSocket, id: int): -# """Handle WebSocket connections between Twilio and OpenAI.""" -# logger.info(f"Client connected with id: {id}") -# print(f"Client connected with id: {id}") -# await websocket.accept() -# start_data = websocket.iter_text() -# await start_data.__anext__() -# call_data = json.loads(await start_data.__anext__()) -# print(call_data, flush=True) -# logger.info(call_data) -# stream_sid = call_data["start"]["streamSid"] -# print("WebSocket connection accepted") -# logger.info("WebSocket connection accepted") -# await run_bot(websocket, stream_sid, False, option=id) - - -# # @router.post("/receive") -# # async def receive_call(req: Request): -# # print("----------------- received call -----------------") - -# # resp = VoiceResponse() -# # connect = Connect() -# # connect.stream( -# # url=f"wss://allegedly-known-wasp.ngrok-free.app/api/twilio/media-stream" -# # ) -# # resp.append(connect) -# # return Response(content=str(resp), media_type="application/xml") - - -# @router.post("/receive") -# async def receive_call(req: Request): -# print("----------------- received call -----------------") -# resp = VoiceResponse() - -# # Gather DTMF input -# gather = resp.gather( -# num_digits=1, action="/api/twilio/handle-key", method="POST", timeout=10 -# ) - -# # Initial greeting and menu options -# gather.say(MENU_OPTIONS) - -# # If no input received, redirect back to the main menu -# resp.redirect("/api/twilio/receive") - -# return Response(content=str(resp), media_type="application/xml") - - -# @router.post("/handle-key") -# async def handle_key_press(req: Request): -# """Process the key pressed by the caller and connect to the appropriate model.""" -# try: -# form_data = await req.form() -# digits_pressed = form_data.get("Digits", "") -# print(f"User pressed: {digits_pressed}") - -# resp = VoiceResponse() - -# if digits_pressed == "0": -# # Repeat options -# resp.redirect("/api/twilio/receive") -# elif digits_pressed in "12345": -# # Valid model selection -# model_id = int(digits_pressed) -# resp.say(f"You have selected model {model_id}.") - -# # Connect to WebSocket -# connect = Connect() -# connect.stream(url=f"{BASE_WS_URL}/media-stream/{model_id}") -# resp.append(connect) -# else: -# # Invalid selection -# resp.say("Invalid selection. Please try again.") -# resp.redirect("/api/twilio/receive") - -# return Response(content=str(resp), media_type="application/xml") - -# except Exception as e: -# print(f"Error handling key press: {str(e)}") -# resp = VoiceResponse() -# resp.say( -# "We encountered a problem processing your request. Please try again later." -# ) -# return Response(content=str(resp), media_type="application/xml") - - -# @router.post("/error") -# async def read_item(req: Request): -# print(await req.body()) -# print(await req.form()) -# logger.error(await req.body()) -# return status.HTTP_200_OK diff --git a/services/clinicServices.py b/services/clinicServices.py index 6551127..ea85494 100644 --- a/services/clinicServices.py +++ b/services/clinicServices.py @@ -1,12 +1,12 @@ from database import get_db from sqlalchemy.orm import Session, joinedload -from schemas.UpdateSchemas import ClinicStatusUpdate, ClinicUpdate +from schemas.UpdateSchemas import ClinicUpdate from schemas.ResponseSchemas import Clinic, ClinicOfferResponse -from typing import List, Literal, Optional, Union +from typing import Literal, Optional, Union from exceptions import ResourceNotFoundException, ValidationException from enums.enums import ClinicStatus, UserType from exceptions.unauthorized_exception import UnauthorizedException -from interface.common_response import CommonResponse +from interface.common_response import CommonResponse from sqlalchemy import or_,not_ from sqlalchemy import text @@ -18,6 +18,7 @@ from loguru import logger from sqlalchemy import func from exceptions.db_exceptions import DBExceptionHandler + class ClinicServices: def __init__(self): self.db: Session = next(get_db()) @@ -48,17 +49,17 @@ class ClinicServices: ) clinics = clinics_query.offset(offset).limit(limit).all() - + count_query = text(""" - SELECT + SELECT COUNT(*) as total, COUNT(CASE WHEN status = 'ACTIVE' OR status = 'INACTIVE' OR status ='SUBSCRIPTION_ENDED' THEN 1 END) as active, COUNT(CASE WHEN status != 'ACTIVE' AND status != 'INACTIVE' AND status != 'SUBSCRIPTION_ENDED' THEN 1 END) as rejected FROM clinics """) - + result = self.db.execute(count_query).first() - + totalClinics = result.total or 0 totalRegisteredClinics = result.active or 0 totalRejectedClinics = result.rejected or 0 @@ -100,7 +101,7 @@ class ClinicServices: raise ResourceNotFoundException("Clinic not found") clinic_response = Clinic(**clinic.__dict__.copy()) - + clinic_response.logo = await get_signed_url(clinic_response.logo) if clinic_response.logo else None clinic_response.abn_doc = await get_signed_url(clinic_response.abn_doc) if clinic_response.abn_doc else None clinic_response.contract_doc = await get_signed_url(clinic_response.contract_doc) if clinic_response.contract_doc else None @@ -127,7 +128,7 @@ class ClinicServices: finally: self.db.close() - + async def get_clinic_files(self, clinic_id: int): try: @@ -162,7 +163,7 @@ class ClinicServices: clinic_data.logo = get_file_key(clinic_data.logo) update_data = clinic_data.model_dump(exclude_unset=True) - + for key, value in update_data.items(): setattr(clinic, key, value) @@ -171,10 +172,10 @@ class ClinicServices: self.db.refresh(clinic) clinic_response = Clinic(**clinic.__dict__.copy()) - + # update clinic files clinic_files = self.db.query(ClinicFileVerifications).filter(ClinicFileVerifications.clinic_id == clinic_id).first() - + if clinic_data.abn_doc: clinic_files.abn_doc_is_verified = None if clinic_data.contract_doc: @@ -207,10 +208,10 @@ class ClinicServices: try: # Get total count totalClinics = self.db.query(Clinics).count() - + # Get counts for specific statuses in a single query status_counts = self.db.query( - Clinics.status, + Clinics.status, func.count(Clinics.id).label('count') ).filter( Clinics.status.in_([ @@ -222,7 +223,7 @@ class ClinicServices: ClinicStatus.SUBSCRIPTION_ENDED ]) ).group_by(Clinics.status).all() - + # Initialize counts with 0 counts = { "totalClinics": totalClinics, @@ -231,7 +232,7 @@ class ClinicServices: "totalRejectedClinics": 0, "totalPaymentDueClinics": 0, } - + # Map status values to their respective count keys status_to_key = { ClinicStatus.ACTIVE: "totalActiveClinics", @@ -239,7 +240,7 @@ class ClinicServices: ClinicStatus.PAYMENT_DUE: "totalPaymentDueClinics", ClinicStatus.REJECTED: "totalRejectedClinics" } - + # Update counts with actual values from the query for status, count in status_counts: key = status_to_key.get(status) @@ -247,14 +248,14 @@ class ClinicServices: counts[key] += count else: counts["totalActiveClinics"] += count - + return counts except Exception as e: DBExceptionHandler.handle_exception(e, context="getting clinic count") finally: self.db.close() - - + + async def update_clinic_status(self, user, clinic_id: int, status: ClinicStatus, documentStatus: Optional[dict] = None, rejection_reason: Optional[str] = None): try: if user["userType"] != UserType.SUPER_ADMIN: @@ -324,7 +325,7 @@ class ClinicServices: # self.db.commit() pass - return + return except Exception as e: DBExceptionHandler.handle_exception(e, context="updating clinic status") finally: @@ -338,7 +339,7 @@ class ClinicServices: DBExceptionHandler.handle_exception(e, context="getting clinic offer by clinic email") finally: self.db.close() - + async def get_clinic_offers(self, user, limit:int, offset:int, search:str = ""): try: if user["userType"] != UserType.SUPER_ADMIN: @@ -423,4 +424,19 @@ class ClinicServices: except Exception as e: DBExceptionHandler.handle_exception(e, context="deleting clinic offer") finally: - self.db.close() \ No newline at end of file + self.db.close() + + async def is_valid_domain(self, domain:str): + + subdomain = domain.split(".")[0] + + # allow main domain, backend and admin domains + if(subdomain in ["toBeDomain", "backend", "admin"]): + return True + + + # check if any clinic has domain + clinic_domain = self.db.query(Clinic).filter(Clinic.domain == subdomain).first() + if clinic_domain: + return True + return False diff --git a/services/stripeServices.py b/services/stripeServices.py index c9ca351..aeeae52 100644 --- a/services/stripeServices.py +++ b/services/stripeServices.py @@ -193,6 +193,7 @@ class StripeServices: finally: self.db.close() + # NOTE: in case when checkout session expired or not created async def create_payment_session(self, user): try: if user["userType"] != UserType.CLINIC_ADMIN: @@ -252,46 +253,34 @@ class StripeServices: finally: self.db.close() - async def create_checkout_session(self, user_id: int): - try: - checkout_session = await stripe.checkout.Session.create_async( - payment_method_types=["card"], - line_items=[ - { - "price_data": { - "currency": "aud", - "product_data": { - "name": "Willio Voice Subscription", - }, - "unit_amount": 5000, - }, - "quantity": 1, - } - ], - expand=["payment_intent"], - mode="payment", - payment_intent_data={"metadata": {"order_id": "1"}}, - success_url=f"{self.redirect_url}auth/waiting", - cancel_url=f"{self.redirect_url}auth/waiting", - metadata={"user_id": user_id}, - ) - return checkout_session - except stripe.error.StripeError as e: - self.logger.error(f"Error creating checkout session: {e}") - raise - - async def create_setup_fees(self, customer_id: str, amount: int): - try: - setup_intent = await stripe.InvoiceItem.create_async( - customer=customer_id, - amount=amount, - currency="aud", - description="Setup Fees", - ) - return setup_intent - except stripe.error.StripeError as e: - self.logger.error(f"Error creating setup intent: {e}") - raise + # NOTE: This is not used + # async def create_checkout_session(self, user_id: int): + # try: + # checkout_session = await stripe.checkout.Session.create_async( + # payment_method_types=["card"], + # line_items=[ + # { + # "price_data": { + # "currency": "aud", + # "product_data": { + # "name": "Willio Voice Subscription", + # }, + # "unit_amount": 5000, + # }, + # "quantity": 1, + # } + # ], + # expand=["payment_intent"], + # mode="payment", + # payment_intent_data={"metadata": {"order_id": "1"}}, + # success_url=f"{self.redirect_url}auth/waiting", + # cancel_url=f"{self.redirect_url}auth/waiting", + # metadata={"user_id": user_id}, + # ) + # return checkout_session + # except stripe.error.StripeError as e: + # self.logger.error(f"Error creating checkout session: {e}") + # raise async def create_subscription_checkout( self, fees_to_be: dict, clinic_id: int, account_id: str, customer_id: str @@ -313,6 +302,7 @@ class StripeServices: ), # Convert to cents "recurring": { "interval": "year", + "interval_count": 3, #NOTE: max 3 years supported by stripe }, }, "quantity": 1,