feat: invoice apis

refactor: minor api response
This commit is contained in:
deepvasoya 2025-06-02 12:22:41 +05:30
parent 1a0109bebd
commit 532e0a3288
9 changed files with 223 additions and 30 deletions

View File

@ -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)]) @router.post("/create-payment-session", dependencies=[Depends(auth_required)])
async def create_payment_session(req:Request): async def create_payment_session(req:Request):
session = await stripe_service.create_payment_session(req.state.user) session = await stripe_service.create_payment_session(req.state.user)
@ -33,4 +39,5 @@ async def create_payment_session(req:Request):
@router.post("/webhook") @router.post("/webhook")
async def stripe_webhook(request: Request): async def stripe_webhook(request: Request):
return await stripe_service.handle_webhook(request) await stripe_service.handle_webhook(request)
return "OK"

View File

@ -19,6 +19,7 @@ class ClinicStatus(Enum):
REQUESTED_DOC = "requested_doc" REQUESTED_DOC = "requested_doc"
REJECTED = "rejected" REJECTED = "rejected"
PAYMENT_DUE = "payment_due" PAYMENT_DUE = "payment_due"
SUBSCRIPTION_ENDED = "subscription_ended"
class ClinicUserRoles(Enum): class ClinicUserRoles(Enum):
DIRECTOR = "director" DIRECTOR = "director"

View File

@ -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 ###

16
models/Subscriptions.py Normal file
View File

@ -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)

View File

@ -19,6 +19,7 @@ from .ClinicOffers import ClinicOffers
from .StripeUsers import StripeUsers from .StripeUsers import StripeUsers
from .PaymentLogs import PaymentLogs from .PaymentLogs import PaymentLogs
from .PaymentSessions import PaymentSessions from .PaymentSessions import PaymentSessions
from .Subscriptions import Subscriptions
__all__ = [ __all__ = [
"Users", "Users",
@ -41,5 +42,6 @@ __all__ = [
"ClinicOffers", "ClinicOffers",
"StripeUsers", "StripeUsers",
"PaymentLogs", "PaymentLogs",
"PaymentSessions" "PaymentSessions",
"Subscriptions"
] ]

View File

@ -55,8 +55,36 @@ class AuthService:
return token return token
async def register(self, user_data: UserCreate, background_tasks=None): async def register(self, user_data: UserCreate, background_tasks=None):
response = await self.user_service.create_user(user_data, background_tasks) try:
return response 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): def blockEmailSNS(self, body: str):
try: try:

View File

@ -52,8 +52,8 @@ class ClinicServices:
count_query = text(""" count_query = text("""
SELECT SELECT
COUNT(*) as total, COUNT(*) as total,
COUNT(CASE WHEN status = 'ACTIVE' OR status = 'INACTIVE' THEN 1 END) as active, COUNT(CASE WHEN status = 'ACTIVE' OR status = 'INACTIVE' OR 'SUBSCRIPTION_ENDED' THEN 1 END) as active,
COUNT(CASE WHEN status != 'ACTIVE' AND status != 'INACTIVE' THEN 1 END) as rejected COUNT(CASE WHEN status != 'ACTIVE' AND status != 'INACTIVE' AND status != 'SUBSCRIPTION_ENDED' THEN 1 END) as rejected
FROM clinics FROM clinics
""") """)
@ -127,6 +127,8 @@ class ClinicServices:
finally: finally:
self.db.close() self.db.close()
async def get_clinic_files(self, clinic_id: int): async def get_clinic_files(self, clinic_id: int):
try: try:
clinic_files = self.db.query(ClinicFileVerifications).filter(ClinicFileVerifications.clinic_id == clinic_id).first() clinic_files = self.db.query(ClinicFileVerifications).filter(ClinicFileVerifications.clinic_id == clinic_id).first()
@ -166,7 +168,18 @@ class ClinicServices:
self.db.refresh(clinic) self.db.refresh(clinic)
clinic_response = Clinic(**clinic.__dict__.copy()) 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 return clinic_response
except Exception as e: except Exception as e:
@ -202,7 +215,8 @@ class ClinicServices:
ClinicStatus.INACTIVE, ClinicStatus.INACTIVE,
ClinicStatus.UNDER_REVIEW, ClinicStatus.UNDER_REVIEW,
ClinicStatus.REJECTED, ClinicStatus.REJECTED,
ClinicStatus.PAYMENT_DUE ClinicStatus.PAYMENT_DUE,
ClinicStatus.SUBSCRIPTION_ENDED
]) ])
).group_by(Clinics.status).all() ).group_by(Clinics.status).all()
@ -212,7 +226,7 @@ class ClinicServices:
"totalActiveClinics": 0, "totalActiveClinics": 0,
"totalUnderReviewClinics": 0, "totalUnderReviewClinics": 0,
"totalRejectedClinics": 0, "totalRejectedClinics": 0,
"totalPaymentDueClinics": 0 "totalPaymentDueClinics": 0,
} }
# Map status values to their respective count keys # Map status values to their respective count keys

View File

@ -11,7 +11,7 @@ from services.dashboardService import DashboardService
from exceptions.validation_exception import ValidationException from exceptions.validation_exception import ValidationException
from exceptions.resource_not_found_exception import ResourceNotFoundException from exceptions.resource_not_found_exception import ResourceNotFoundException
from exceptions.unauthorized_exception import UnauthorizedException from exceptions.unauthorized_exception import UnauthorizedException
from models import Clinics,PaymentSessions from models import Clinics,PaymentSessions, Subscriptions
import uuid import uuid
from fastapi import Request from fastapi import Request
from datetime import datetime from datetime import datetime
@ -75,6 +75,42 @@ class StripeServices:
self.logger.error(f"Error deleting account: {e}") self.logger.error(f"Error deleting account: {e}")
raise 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): async def create_payment_session(self, user):
try: try:
if user["userType"] != UserType.CLINIC_ADMIN: if user["userType"] != UserType.CLINIC_ADMIN:
@ -106,9 +142,6 @@ class StripeServices:
fees_to_be["per_call_charges"] = clinic_offers.per_call_charges 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 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) payment_link = await self.create_subscription_checkout(fees_to_be, clinic["id"], customer.account_id, customer.customer_id)
return payment_link.url return payment_link.url
@ -138,8 +171,8 @@ class StripeServices:
expand=["payment_intent"], expand=["payment_intent"],
mode="payment", mode="payment",
payment_intent_data={"metadata": {"order_id": "1"}}, payment_intent_data={"metadata": {"order_id": "1"}},
success_url="http://54.79.156.66/", success_url="http://54.79.156.66/auth/waiting",
cancel_url="http://54.79.156.66/", cancel_url="http://54.79.156.66/auth/waiting",
metadata={"user_id": user_id}, metadata={"user_id": user_id},
) )
return checkout_session return checkout_session
@ -253,7 +286,6 @@ class StripeServices:
finally: finally:
self.db.close() self.db.close()
async def handle_webhook(self, request: Request): async def handle_webhook(self, request: Request):
try: try:
payload = await request.body() payload = await request.body()
@ -262,24 +294,31 @@ class StripeServices:
) )
self.logger.info(f"Stripe webhook event type: {event['type']}") self.logger.info(f"Stripe webhook event type: {event['type']}")
if event["type"] == "invoice.payment_succeeded": if event["type"] == "checkout.session.expired":
pass pass
if event["type"] == "checkout.session.async_payment_succeeded": if event["type"] == "checkout.session.completed":
self.logger.info("Async payment succeeded")
elif event["type"] == "checkout.session.completed":
unique_clinic_id = event["data"]["object"]["metadata"]["unique_clinic_id"] unique_clinic_id = event["data"]["object"]["metadata"]["unique_clinic_id"]
clinic_id = event["data"]["object"]["metadata"]["clinic_id"] clinic_id = event["data"]["object"]["metadata"]["clinic_id"]
customer_id = event["data"]["object"]["metadata"]["customer_id"] customer_id = event["data"]["object"]["metadata"]["customer_id"]
account_id = event["data"]["object"]["metadata"]["account_id"] account_id = event["data"]["object"]["metadata"]["account_id"]
total = event["data"]["object"]["amount_total"] total = event["data"]["object"]["amount_total"]
metadata = event["data"]["object"]["metadata"] 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 # TODO: handle subscription period end
return event return "OK"
except ValueError as e: except ValueError as e:
self.logger.error(f"Invalid payload: {e}") self.logger.error(f"Invalid payload: {e}")
except stripe.error.SignatureVerificationError as e: except stripe.error.SignatureVerificationError as e:
@ -287,7 +326,7 @@ class StripeServices:
finally: finally:
self.db.close() 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: try:
self.db.query(PaymentSessions).filter(PaymentSessions.clinic_id == clinic_id).delete() self.db.query(PaymentSessions).filter(PaymentSessions.clinic_id == clinic_id).delete()
@ -301,15 +340,48 @@ class StripeServices:
metadata_logs=json.dumps(metadata.to_dict()) metadata_logs=json.dumps(metadata.to_dict())
) )
self.db.add(payment_log) self.db.add(payment_log)
self.db.commit()
clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first() clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first()
if clinic: if clinic:
clinic.status = ClinicStatus.UNDER_REVIEW clinic.status = ClinicStatus.UNDER_REVIEW
self.db.add(clinic) self.db.add(clinic)
self.db.commit() return
except Exception as e: except Exception as e:
self.logger.error(f"Error updating payment log: {e}") self.logger.error(f"Error updating payment log: {e}")
finally: finally:
self.db.commit()
self.db.close() 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()

View File

@ -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) 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: except Exception as e:
logger.error(f"Error creating user: {str(e)}") logger.error(f"Error creating user: {str(e)}")
# Rollback the transaction if any error occurs # Rollback the transaction if any error occurs
@ -171,7 +188,6 @@ class UserServices:
# Use the centralized exception handler # Use the centralized exception handler
DBExceptionHandler.handle_exception(e, context="creating user") DBExceptionHandler.handle_exception(e, context="creating user")
finally: finally:
self.db.commit()
self.db.close() self.db.close()