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)])
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)
await stripe_service.handle_webhook(request)
return "OK"

View File

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

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 .PaymentLogs import PaymentLogs
from .PaymentSessions import PaymentSessions
from .Subscriptions import Subscriptions
__all__ = [
"Users",
@ -41,5 +42,6 @@ __all__ = [
"ClinicOffers",
"StripeUsers",
"PaymentLogs",
"PaymentSessions"
"PaymentSessions",
"Subscriptions"
]

View File

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

View File

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

View File

@ -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:
@ -106,9 +142,6 @@ class StripeServices:
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)
return payment_link.url
@ -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.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()

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