From 532e0a32882bb56ce7d2ee0e8906833eeb0946f1 Mon Sep 17 00:00:00 2001 From: deepvasoya Date: Mon, 2 Jun 2025 12:22:41 +0530 Subject: [PATCH] feat: invoice apis refactor: minor api response --- apis/endpoints/stripe.py | 9 +- enums/enums.py | 1 + ...ed8ac3d258c_updated_enums_clinic_status.py | 37 ++++++ models/Subscriptions.py | 16 +++ models/__init__.py | 4 +- services/authService.py | 32 ++++- services/clinicServices.py | 24 +++- services/stripeServices.py | 110 +++++++++++++++--- services/userServices.py | 20 +++- 9 files changed, 223 insertions(+), 30 deletions(-) create mode 100644 migrations/versions/5ed8ac3d258c_updated_enums_clinic_status.py create mode 100644 models/Subscriptions.py diff --git a/apis/endpoints/stripe.py b/apis/endpoints/stripe.py index 03b6eeb..d989d21 100644 --- a/apis/endpoints/stripe.py +++ b/apis/endpoints/stripe.py @@ -26,6 +26,12 @@ stripe_service = StripeServices() # ) +@router.get("/get-invoice", dependencies=[Depends(auth_required)]) +async def get_invoice(req:Request): + invoice_url = await stripe_service.get_invoice(req.state.user) + return ApiResponse(data=invoice_url, message="Invoice URL retrieved successfully") + + @router.post("/create-payment-session", dependencies=[Depends(auth_required)]) async def create_payment_session(req:Request): session = await stripe_service.create_payment_session(req.state.user) @@ -33,4 +39,5 @@ async def create_payment_session(req:Request): @router.post("/webhook") async def stripe_webhook(request: Request): - return await stripe_service.handle_webhook(request) \ No newline at end of file + await stripe_service.handle_webhook(request) + return "OK" \ No newline at end of file diff --git a/enums/enums.py b/enums/enums.py index dc912a1..96ef0b3 100644 --- a/enums/enums.py +++ b/enums/enums.py @@ -19,6 +19,7 @@ class ClinicStatus(Enum): REQUESTED_DOC = "requested_doc" REJECTED = "rejected" PAYMENT_DUE = "payment_due" + SUBSCRIPTION_ENDED = "subscription_ended" class ClinicUserRoles(Enum): DIRECTOR = "director" diff --git a/migrations/versions/5ed8ac3d258c_updated_enums_clinic_status.py b/migrations/versions/5ed8ac3d258c_updated_enums_clinic_status.py new file mode 100644 index 0000000..160724b --- /dev/null +++ b/migrations/versions/5ed8ac3d258c_updated_enums_clinic_status.py @@ -0,0 +1,37 @@ +"""updated_enums_clinic_status + +Revision ID: 5ed8ac3d258c +Revises: a19fede0cdc6 +Create Date: 2025-06-02 11:11:56.589321 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5ed8ac3d258c' +down_revision: Union[str, None] = 'a19fede0cdc6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + # Add new status to clinicstatus enum + op.execute("ALTER TYPE clinicstatus ADD VALUE IF NOT EXISTS 'SUBSCRIPTION_ENDED' AFTER 'PAYMENT_DUE'") + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + # Note: In PostgreSQL, you cannot directly remove an enum value. + # You would need to create a new enum type, update the column to use the new type, + # and then drop the old type. This is a complex operation and might not be needed. + # The upgrade will be reverted when applying previous migrations. + pass + # ### end Alembic commands ### diff --git a/models/Subscriptions.py b/models/Subscriptions.py new file mode 100644 index 0000000..42013f7 --- /dev/null +++ b/models/Subscriptions.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, Integer, String +from database import Base +from .CustomBase import CustomBase + +class Subscriptions(Base, CustomBase): + __tablename__ = "subscriptions" + id = Column(Integer, primary_key=True, index=True) + session_id = Column(String(255), index=True) + customer_id = Column(String,index=True) + account_id = Column(String,index=True) + subscription_id = Column(String,index=True) + clinic_id = Column(Integer, index=True) + status = Column(String) + current_period_start = Column(String) # unix timestamp + current_period_end = Column(String) # unix timestamp + metadata_logs = Column(String) \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py index 279e551..cffb161 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -19,6 +19,7 @@ from .ClinicOffers import ClinicOffers from .StripeUsers import StripeUsers from .PaymentLogs import PaymentLogs from .PaymentSessions import PaymentSessions +from .Subscriptions import Subscriptions __all__ = [ "Users", @@ -41,5 +42,6 @@ __all__ = [ "ClinicOffers", "StripeUsers", "PaymentLogs", - "PaymentSessions" + "PaymentSessions", + "Subscriptions" ] diff --git a/services/authService.py b/services/authService.py index d3e8819..424a489 100644 --- a/services/authService.py +++ b/services/authService.py @@ -55,8 +55,36 @@ class AuthService: return token async def register(self, user_data: UserCreate, background_tasks=None): - response = await self.user_service.create_user(user_data, background_tasks) - return response + try: + resp = await self.user_service.create_user(user_data, background_tasks) + + # Get the SQLAlchemy model instance + user_obj = resp["user"] + + # create token with user data + user_data = { + "id": user_obj["id"], + "username": user_obj["username"], + "email": user_obj["email"], + "clinicRole": user_obj["clinicRole"], + "userType": user_obj["userType"], + "mobile": user_obj["mobile"], + "clinicId": user_obj["clinicId"] + } + token = create_jwt_token(user_data) + + # Update response with token + resp["token"] = token + + response = { + "url": resp.get("url"), + "token": token + } + + return response + except Exception as e: + self.logger.error(f"Error registering user: {e}") + raise e def blockEmailSNS(self, body: str): try: diff --git a/services/clinicServices.py b/services/clinicServices.py index e53cc5b..112a110 100644 --- a/services/clinicServices.py +++ b/services/clinicServices.py @@ -52,8 +52,8 @@ class ClinicServices: count_query = text(""" SELECT COUNT(*) as total, - COUNT(CASE WHEN status = 'ACTIVE' OR status = 'INACTIVE' THEN 1 END) as active, - COUNT(CASE WHEN status != 'ACTIVE' AND status != 'INACTIVE' THEN 1 END) as rejected + COUNT(CASE WHEN status = 'ACTIVE' OR status = 'INACTIVE' OR '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 """) @@ -127,6 +127,8 @@ class ClinicServices: finally: self.db.close() + + async def get_clinic_files(self, clinic_id: int): try: clinic_files = self.db.query(ClinicFileVerifications).filter(ClinicFileVerifications.clinic_id == clinic_id).first() @@ -166,7 +168,18 @@ class ClinicServices: self.db.refresh(clinic) clinic_response = Clinic(**clinic.__dict__.copy()) - clinic_response.logo = get_signed_url(clinic_response.logo) if clinic_response.logo else None + + # update clinic files + clinic_files = await self.get_clinic_files(clinic_id) + + if clinic_data.abn_doc: + clinic_files.abn_doc_is_verified = None + if clinic_data.contract_doc: + clinic_files.contract_doc_is_verified = None + + self.db.add(clinic_files) + self.db.commit() + self.db.refresh(clinic_files) return clinic_response except Exception as e: @@ -202,7 +215,8 @@ class ClinicServices: ClinicStatus.INACTIVE, ClinicStatus.UNDER_REVIEW, ClinicStatus.REJECTED, - ClinicStatus.PAYMENT_DUE + ClinicStatus.PAYMENT_DUE, + ClinicStatus.SUBSCRIPTION_ENDED ]) ).group_by(Clinics.status).all() @@ -212,7 +226,7 @@ class ClinicServices: "totalActiveClinics": 0, "totalUnderReviewClinics": 0, "totalRejectedClinics": 0, - "totalPaymentDueClinics": 0 + "totalPaymentDueClinics": 0, } # Map status values to their respective count keys diff --git a/services/stripeServices.py b/services/stripeServices.py index 9fc72ed..4fff653 100644 --- a/services/stripeServices.py +++ b/services/stripeServices.py @@ -11,7 +11,7 @@ from services.dashboardService import DashboardService from exceptions.validation_exception import ValidationException from exceptions.resource_not_found_exception import ResourceNotFoundException from exceptions.unauthorized_exception import UnauthorizedException -from models import Clinics,PaymentSessions +from models import Clinics,PaymentSessions, Subscriptions import uuid from fastapi import Request from datetime import datetime @@ -75,6 +75,42 @@ class StripeServices: self.logger.error(f"Error deleting account: {e}") raise + async def get_invoice(self, user): + try: + + if user["userType"] != UserType.CLINIC_ADMIN: + raise UnauthorizedException("User is not authorized to perform this action") + + clinic = self.db.query(Clinics).filter(Clinics.creator_id == user["id"]).first() + + if not clinic: + raise ResourceNotFoundException("Clinic not found!") + + customer = self.db.query(StripeUsers).filter(StripeUsers.user_id == user["id"]).first() + + if not customer: + raise ResourceNotFoundException("Customer not found!") + + subscription = self.db.query(Subscriptions).filter(Subscriptions.clinic_id == clinic.id, Subscriptions.customer_id == customer.customer_id, Subscriptions.status == "active").first() + + if not subscription: + raise ResourceNotFoundException("Subscription not found!") + + stripe_subscription = stripe.Subscription.retrieve(subscription.subscription_id) + + invoice = stripe.Invoice.retrieve( + stripe_subscription["latest_invoice"] + ) + return invoice.hosted_invoice_url + except stripe.error.StripeError as e: + self.logger.error(f"Error getting invoice: {e}") + raise + except Exception as e: + self.logger.error(f"Error getting invoice: {e}") + raise + finally: + self.db.close() + async def create_payment_session(self, user): try: if user["userType"] != UserType.CLINIC_ADMIN: @@ -105,9 +141,6 @@ class StripeServices: fees_to_be["setup_fees"] = clinic_offers.setup_fees fees_to_be["per_call_charges"] = clinic_offers.per_call_charges fees_to_be["total"] = clinic_offers.setup_fees + fees_to_be["subscription_fees"] + clinic_offers.per_call_charges - - # remove previouis payment session - self.db.query(PaymentSessions).filter(PaymentSessions.clinic_id == clinic["id"]).delete() payment_link = await self.create_subscription_checkout(fees_to_be, clinic["id"], customer.account_id, customer.customer_id) @@ -138,8 +171,8 @@ class StripeServices: expand=["payment_intent"], mode="payment", payment_intent_data={"metadata": {"order_id": "1"}}, - success_url="http://54.79.156.66/", - cancel_url="http://54.79.156.66/", + success_url="http://54.79.156.66/auth/waiting", + cancel_url="http://54.79.156.66/auth/waiting", metadata={"user_id": user_id}, ) return checkout_session @@ -253,7 +286,6 @@ class StripeServices: finally: self.db.close() - async def handle_webhook(self, request: Request): try: payload = await request.body() @@ -262,24 +294,31 @@ class StripeServices: ) self.logger.info(f"Stripe webhook event type: {event['type']}") - if event["type"] == "invoice.payment_succeeded": + if event["type"] == "checkout.session.expired": pass - if event["type"] == "checkout.session.async_payment_succeeded": - self.logger.info("Async payment succeeded") - - elif event["type"] == "checkout.session.completed": + if event["type"] == "checkout.session.completed": unique_clinic_id = event["data"]["object"]["metadata"]["unique_clinic_id"] clinic_id = event["data"]["object"]["metadata"]["clinic_id"] customer_id = event["data"]["object"]["metadata"]["customer_id"] account_id = event["data"]["object"]["metadata"]["account_id"] total = event["data"]["object"]["amount_total"] metadata = event["data"]["object"]["metadata"] - self.update_payment_log(unique_clinic_id, clinic_id, customer_id, account_id, total, metadata) + session_id = event["data"]["object"]["id"] + subscription_id = event["data"]["object"]["subscription"] + self._update_payment_log(unique_clinic_id, clinic_id, customer_id, account_id, total, metadata, session_id) + + self._create_subscription_entry({ + "clinic_id": clinic_id, + "customer_id": customer_id, + "account_id": account_id, + "session_id": session_id, + "subscription_id": subscription_id, + }) # TODO: handle subscription period end - return event + return "OK" except ValueError as e: self.logger.error(f"Invalid payload: {e}") except stripe.error.SignatureVerificationError as e: @@ -287,7 +326,7 @@ class StripeServices: finally: self.db.close() - def update_payment_log(self, unique_clinic_id:str, clinic_id:int, customer_id:str, account_id:str, total:float, metadata:any): + def _update_payment_log(self, unique_clinic_id:str, clinic_id:int, customer_id:str, account_id:str, total:float, metadata:any, session_id:str): try: self.db.query(PaymentSessions).filter(PaymentSessions.clinic_id == clinic_id).delete() @@ -301,15 +340,48 @@ class StripeServices: metadata_logs=json.dumps(metadata.to_dict()) ) self.db.add(payment_log) - self.db.commit() clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first() + if clinic: clinic.status = ClinicStatus.UNDER_REVIEW self.db.add(clinic) - self.db.commit() - + return except Exception as e: self.logger.error(f"Error updating payment log: {e}") finally: - self.db.close() \ No newline at end of file + self.db.commit() + self.db.close() + + def _create_subscription_entry(self,data:dict): + try: + + subscription = stripe.Subscription.retrieve(data["subscription_id"]) + + new_subscription = Subscriptions( + clinic_id=data["clinic_id"], + customer_id=data["customer_id"], + account_id=data["account_id"], + session_id=data["session_id"], + subscription_id=data["subscription_id"], + status=subscription.status, + current_period_start=subscription["items"]["data"][0]["current_period_start"], + current_period_end=subscription["items"]["data"][0]["current_period_end"], + metadata_logs=json.dumps(subscription.metadata) + ) + self.db.add(new_subscription) + + payment_session = PaymentSessions( + session_id=data["session_id"], + customer_id=data["customer_id"], + clinic_id=data["clinic_id"], + status="paid" + ) + self.db.add(payment_session) + return + except Exception as e: + self.logger.error(f"Error creating subscription entry: {e}") + finally: + self.db.commit() + self.db.close() + \ No newline at end of file diff --git a/services/userServices.py b/services/userServices.py index 08b823f..d65c44a 100644 --- a/services/userServices.py +++ b/services/userServices.py @@ -156,7 +156,24 @@ class UserServices: payment_link = await self.stripe_service.create_subscription_checkout(fees_to_be, new_clinic.id, stripe_account.id,stripe_customer.id) - return payment_link.url + self.db.commit() + + # Convert the user object to a dictionary before the session is closed + user_dict = { + "id": new_user.id, + "username": new_user.username, + "email": new_user.email, + "clinicRole": new_user.clinicRole, + "userType": new_user.userType, + "mobile": new_user.mobile, + "clinicId": new_clinic.id + } + + return { + "url": payment_link.url, + "user": user_dict, + } + except Exception as e: logger.error(f"Error creating user: {str(e)}") # Rollback the transaction if any error occurs @@ -171,7 +188,6 @@ class UserServices: # Use the centralized exception handler DBExceptionHandler.handle_exception(e, context="creating user") finally: - self.db.commit() self.db.close()