feat: clinic bank details api

fix: relation betn stripe and user table
This commit is contained in:
2025-06-05 19:25:33 +05:30
parent 7f2f730426
commit 165385358f
13 changed files with 381 additions and 149 deletions
+10 -9
View File
@@ -3,14 +3,13 @@ from schemas.CreateSchemas import ClinicDoctorCreate
from schemas.UpdateSchemas import ClinicDoctorUpdate
from schemas.ResponseSchemas import ClinicDoctorResponse, MasterAppointmentTypeResponse
from database import get_db
from models import ClinicDoctors
from sqlalchemy.orm import Session, joinedload, selectinload
from services.clinicServices import ClinicServices
from exceptions import ResourceNotFoundException
from interface.common_response import CommonResponse
from sqlalchemy import func, or_, cast, String
from enums.enums import ClinicDoctorStatus, UserType
from models import MasterAppointmentTypes, AppointmentRelations
from models import MasterAppointmentTypes, AppointmentRelations, Users, ClinicDoctors
from utils.constants import DEFAULT_ORDER, DEFAULT_ORDER_BY
@@ -174,13 +173,14 @@ class ClinicDoctorsServices:
finally:
self.db.close()
async def get_doctor_status_count(self):
async def get_doctor_status_count(self, clinic_id:int):
try:
# Query to count doctors by status
status_counts = (
self.db.query(
ClinicDoctors.status, func.count(ClinicDoctors.id).label("count")
)
.filter(ClinicDoctors.clinic_id == clinic_id)
.group_by(ClinicDoctors.status)
.all()
)
@@ -198,17 +198,18 @@ class ClinicDoctorsServices:
finally:
self.db.close()
async def get_clinic_doctors(self, limit: int, offset: int, search: str = "", sort_by: str = DEFAULT_ORDER, sort_order: str = DEFAULT_ORDER_BY):
async def get_clinic_doctors(self,user, limit: int, offset: int, search: str = "", sort_by: str = DEFAULT_ORDER, sort_order: str = DEFAULT_ORDER_BY):
try:
clinic_doctors_query = (
self.db.query(ClinicDoctors)
.filter(ClinicDoctors.clinic_id == user["created_clinics"][0]["id"])
.options(
selectinload(ClinicDoctors.appointmentRelations)
.selectinload(AppointmentRelations.masterAppointmentTypes)
)
.order_by(
getattr(ClinicDoctors, sort_by).desc()
if sort_order == "desc"
getattr(ClinicDoctors, sort_by).desc()
if sort_order == "desc"
else getattr(ClinicDoctors, sort_by).asc()
)
)
@@ -230,7 +231,7 @@ class ClinicDoctorsServices:
total = clinic_doctors_query.count()
clinic_doctors = clinic_doctors_query.limit(limit).offset(offset).all()
# Build response data manually to include appointment types
response_data = []
for clinic_doctor in clinic_doctors:
@@ -246,7 +247,7 @@ class ClinicDoctorsServices:
update_time=relation.masterAppointmentTypes.update_time
)
)
# Create the clinic doctor response
clinic_doctor_data = ClinicDoctorResponse(
id=clinic_doctor.id,
@@ -258,7 +259,7 @@ class ClinicDoctorsServices:
appointmentTypes=appointment_types
)
response_data.append(clinic_doctor_data)
response = CommonResponse(
data=response_data,
total=total,
+13 -7
View File
@@ -8,6 +8,8 @@ from exceptions import UnauthorizedException
from enums.enums import UserType
from exceptions import ResourceNotFoundException
from loguru import logger
from models import Users
class DashboardService:
def __init__(self):
self.db = next(get_db())
@@ -15,13 +17,17 @@ class DashboardService:
self.clinicServices = ClinicServices()
self.logger = logger
async def get_dashboard_counts(self, isSuperAdmin: bool):
if isSuperAdmin:
clinicCounts = await self.clinicServices.get_clinic_count()
return clinicCounts
else:
clinicDoctorsCount = await self.clinicDoctorsServices.get_doctor_status_count()
return clinicDoctorsCount
async def get_dashboard_counts(self, user):
try:
if user["userType"] == UserType.SUPER_ADMIN:
clinicCounts = await self.clinicServices.get_clinic_count()
return clinicCounts
else:
clinicDoctorsCount = await self.clinicDoctorsServices.get_doctor_status_count(user["created_clinics"][0]["id"])
return clinicDoctorsCount
except Exception as e:
self.logger.error("Error getting dashboard counts: ", e)
raise e
async def update_signup_pricing_master(
self, user, pricing_data: SignupPricingMasterBase
+237 -92
View File
@@ -3,7 +3,6 @@ import os
import dotenv
dotenv.load_dotenv()
from models import ClinicOffers, StripeUsers
@@ -11,12 +10,13 @@ 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, Subscriptions
from models import Clinics, PaymentSessions, Subscriptions
import uuid
from fastapi import Request
from datetime import datetime
from models import PaymentLogs
from enums.enums import ClinicStatus, UserType
from schemas.ResponseSchemas import StripeUserReponse
from database import get_db
from sqlalchemy.orm import Session
@@ -75,28 +75,110 @@ class StripeServices:
self.logger.error(f"Error deleting account: {e}")
raise
async def get_stripe_data(self, clinic_id: int):
try:
user = (
self.db.query(StripeUsers)
.filter(StripeUsers.clinic_id == clinic_id)
.first()
)
if not user:
self.logger.error(f"User not found!")
raise ResourceNotFoundException("User not found!")
return StripeUserReponse.model_validate(user).model_dump()
except Exception as e:
self.logger.error(f"Error retrieving account data: {e}")
raise
finally:
self.db.close()
async def create_stripe_account_link(self, user):
try:
stripe_account = await self.get_stripe_data(user["created_clinics"][0]["id"])
if not stripe_account:
self.logger.error("Stripe account not found!")
raise ResourceNotFoundException("Stripe account not found!")
# Pass the account_id as a string, not as a dictionary
data = await stripe.AccountLink.create_async(
account=stripe_account["account_id"],
refresh_url=self.redirect_url,
return_url=self.redirect_url,
type="account_onboarding",
)
return data.url
except Exception as e:
self.logger.error(f"Error creating stripe account link: {e}")
raise
async def check_account_capabilities(self, user):
try:
stripe_account = await self.get_stripe_data(user["created_clinics"][0]["id"])
if not stripe_account:
self.logger.error("Stripe account not found!")
raise ResourceNotFoundException("Stripe account not found!")
data = await stripe.Account.retrieve_async(stripe_account["account_id"])
return {
"capabilities": data.capabilities,
"charges_enabled": data.charges_enabled,
"requirements": data.requirements.currently_due,
"error": data.requirements.errors,
}
except Exception as e:
self.logger.error(f"Error checking stripe account capabilities: {e}")
raise
finally:
self.db.close()
async def get_invoice(self, user):
try:
if user["userType"] != UserType.CLINIC_ADMIN:
raise UnauthorizedException("User is not authorized to perform this action")
raise UnauthorizedException(
"User is not authorized to perform this action"
)
clinic = self.db.query(Clinics).filter(Clinics.creator_id == user["id"]).first()
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()
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()
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 = await stripe.Subscription.retrieve_async(subscription.subscription_id)
stripe_subscription = await stripe.Subscription.retrieve_async(
subscription.subscription_id
)
invoice = await stripe.Invoice.retrieve_async(
stripe_subscription["latest_invoice"]
@@ -114,36 +196,54 @@ class StripeServices:
async def create_payment_session(self, user):
try:
if user["userType"] != UserType.CLINIC_ADMIN:
raise UnauthorizedException("User is not authorized to perform this action")
raise UnauthorizedException(
"User is not authorized to perform this action"
)
clinic = user["created_clinics"][0]
if clinic["status"] != ClinicStatus.PAYMENT_DUE:
raise ValidationException("Clinic is not due for payment")
customer = self.db.query(StripeUsers).filter(StripeUsers.user_id == user['id']).first()
customer = (
self.db.query(StripeUsers)
.filter(StripeUsers.user_id == user["id"])
.first()
)
if not customer:
raise ResourceNotFoundException("Customer not found")
clinic_offers = self.db.query(ClinicOffers).filter(ClinicOffers.clinic_email == clinic["email"]).first()
clinic_offers = (
self.db.query(ClinicOffers)
.filter(ClinicOffers.clinic_email == clinic["email"])
.first()
)
signup_pricing= await self.dashboard_service.get_signup_pricing_master()
signup_pricing = await self.dashboard_service.get_signup_pricing_master()
fees_to_be = {
"setup_fees": signup_pricing.setup_fees,
"subscription_fees": signup_pricing.subscription_fees,
"per_call_charges": signup_pricing.per_call_charges,
"total": signup_pricing.setup_fees + signup_pricing.subscription_fees + signup_pricing.per_call_charges
"total": signup_pricing.setup_fees
+ signup_pricing.subscription_fees
+ signup_pricing.per_call_charges,
}
if clinic_offers:
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
payment_link = await self.create_subscription_checkout(fees_to_be, clinic["id"], customer.account_id, customer.customer_id)
fees_to_be["total"] = (
clinic_offers.setup_fees
+ fees_to_be["subscription_fees"]
+ clinic_offers.per_call_charges
)
payment_link = await self.create_subscription_checkout(
fees_to_be, clinic["id"], customer.account_id, customer.customer_id
)
return payment_link.url
except Exception as e:
@@ -193,67 +293,79 @@ class StripeServices:
self.logger.error(f"Error creating setup intent: {e}")
raise
async def create_subscription_checkout(self, fees_to_be: dict, clinic_id: int, account_id: str, customer_id: str):
async def create_subscription_checkout(
self, fees_to_be: dict, clinic_id: int, account_id: str, customer_id: str
):
try:
unique_id = str(uuid.uuid4())
unique_clinic_id = f"clinic_{clinic_id}_{unique_id}"
line_items = [{
'price_data': {
'currency': 'aud',
'product_data': {
'name': 'Monthly Subscription',
line_items = [
{
"price_data": {
"currency": "aud",
"product_data": {
"name": "Monthly Subscription",
},
"unit_amount": int(
fees_to_be["subscription_fees"] * 100
), # Convert to cents
"recurring": {
"interval": "year",
},
},
'unit_amount': int(fees_to_be["subscription_fees"] * 100), # Convert to cents
'recurring': {
'interval': 'year',
},
},
'quantity': 1,
}]
"quantity": 1,
}
]
line_items.append({
'price_data': {
'currency': 'aud',
'product_data': {
'name': 'Per Call',
line_items.append(
{
"price_data": {
"currency": "aud",
"product_data": {
"name": "Per Call",
},
"unit_amount": int(
fees_to_be["per_call_charges"] * 100
), # Convert to cents
},
'unit_amount': int(fees_to_be["per_call_charges"] * 100), # Convert to cents
},
'quantity': 1,
})
"quantity": 1,
}
)
line_items.append({
'price_data': {
'currency': 'aud',
'product_data': {
'name': 'Setup Fee',
line_items.append(
{
"price_data": {
"currency": "aud",
"product_data": {
"name": "Setup Fee",
},
"unit_amount": int(
fees_to_be["setup_fees"] * 100
), # Convert to cents
},
'unit_amount': int(fees_to_be["setup_fees"] * 100), # Convert to cents
},
'quantity': 1,
})
"quantity": 1,
}
)
metadata = {
"clinic_id": clinic_id,
"unique_clinic_id": unique_clinic_id,
"account_id": account_id,
"customer_id": customer_id,
"fees_to_be": json.dumps(fees_to_be)
"fees_to_be": json.dumps(fees_to_be),
}
session_data = {
'customer': customer_id,
"payment_method_types": ["card","au_becs_debit"],
'mode': 'subscription',
'line_items': line_items,
'success_url': f"{self.redirect_url}auth/waiting",
'cancel_url': f"{self.redirect_url}auth/waiting",
'metadata': metadata,
'subscription_data': {
'metadata': metadata
}
"customer": customer_id,
"payment_method_types": ["card", "au_becs_debit"],
"mode": "subscription",
"line_items": line_items,
"success_url": f"{self.redirect_url}auth/waiting",
"cancel_url": f"{self.redirect_url}auth/waiting",
"metadata": metadata,
"subscription_data": {"metadata": metadata},
}
session = await stripe.checkout.Session.create_async(**session_data)
@@ -261,18 +373,20 @@ class StripeServices:
payment_log = PaymentLogs(
customer_id=customer_id,
account_id=account_id,
amount=Decimal(str(fees_to_be["total"])), # Keep as Decimal for database storage
amount=Decimal(
str(fees_to_be["total"])
), # Keep as Decimal for database storage
clinic_id=clinic_id,
unique_clinic_id=unique_clinic_id,
payment_status="pending",
metadata_logs=json.dumps(metadata)
metadata_logs=json.dumps(metadata),
)
new_payment_session = PaymentSessions(
session_id=session.id,
customer_id=customer_id,
clinic_id=clinic_id,
status="pending"
status="pending",
)
self.db.add(payment_log)
@@ -296,12 +410,16 @@ class StripeServices:
if event["type"] == "customer.subscription.deleted":
self.logger.info("customer subscription ended")
subscription_id = event["data"]["object"]["items"]["data"][0]["subscription"]
subscription_id = event["data"]["object"]["items"]["data"][0][
"subscription"
]
await self._subscription_expired(subscription_id)
if 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"]
customer_id = event["data"]["object"]["metadata"]["customer_id"]
account_id = event["data"]["object"]["metadata"]["account_id"]
@@ -310,15 +428,24 @@ class StripeServices:
session_id = event["data"]["object"]["id"]
subscription_id = event["data"]["object"]["subscription"]
await self._update_payment_log(unique_clinic_id, clinic_id, customer_id, account_id, total, metadata)
await self._update_payment_log(
unique_clinic_id,
clinic_id,
customer_id,
account_id,
total,
metadata,
)
await self._create_subscription_entry({
"clinic_id": clinic_id,
"customer_id": customer_id,
"account_id": account_id,
"session_id": session_id,
"subscription_id": subscription_id,
})
await 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 "OK"
@@ -329,9 +456,19 @@ class StripeServices:
finally:
self.db.close()
async def _update_payment_log(self, unique_clinic_id:str, clinic_id:int, customer_id:str, account_id:str, total:float, metadata:any):
async def _update_payment_log(
self,
unique_clinic_id: str,
clinic_id: int,
customer_id: str,
account_id: str,
total: float,
metadata: any,
):
try:
self.db.query(PaymentSessions).filter(PaymentSessions.clinic_id == clinic_id).delete()
self.db.query(PaymentSessions).filter(
PaymentSessions.clinic_id == clinic_id
).delete()
payment_log = PaymentLogs(
customer_id=customer_id,
@@ -340,12 +477,12 @@ class StripeServices:
clinic_id=clinic_id,
unique_clinic_id=unique_clinic_id,
payment_status="paid",
metadata_logs=json.dumps(metadata.to_dict())
metadata_logs=json.dumps(metadata.to_dict()),
)
self.db.add(payment_log)
clinic = self.db.query(Clinics).filter(Clinics.id == clinic_id).first()
if clinic:
clinic.status = ClinicStatus.UNDER_REVIEW
self.db.add(clinic)
@@ -356,12 +493,12 @@ class StripeServices:
self.db.commit()
self.db.close()
async def _create_subscription_entry(self,data:dict):
async def _create_subscription_entry(self, data: dict):
try:
subscription = stripe.Subscription.retrieve(data["subscription_id"])
metadata_dict = json.loads(subscription.metadata)
metadata_dict = subscription.metadata
fees_to_be = json.loads(metadata_dict["fees_to_be"])
new_subscription = Subscriptions(
@@ -374,9 +511,13 @@ class StripeServices:
per_call_charge=fees_to_be["per_call_charges"],
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)
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)
@@ -384,7 +525,7 @@ class StripeServices:
session_id=data["session_id"],
customer_id=data["customer_id"],
clinic_id=data["clinic_id"],
status="paid"
status="paid",
)
self.db.add(payment_session)
return
@@ -393,26 +534,30 @@ class StripeServices:
finally:
self.db.commit()
self.db.close()
async def _subscription_expired(self,subscription_id):
async def _subscription_expired(self, subscription_id):
try:
subscription = stripe.Subscription.retrieve(subscription_id)
db_subscription = self.db.query(Subscriptions).filter(Subscriptions.subscription_id == subscription_id).first()
db_subscription = (
self.db.query(Subscriptions)
.filter(Subscriptions.subscription_id == subscription_id)
.first()
)
if not db_subscription:
self.logger.error("Subscription not found!")
raise Exception("Subscription not found!")
db_subscription.status = subscription.status
self.db.add(db_subscription)
# TODO: update clinic status
# TODO: send email to user
return
except Exception as e:
self.logger.error(f"Error ending subscription: {e}")
finally:
self.db.commit()
self.db.close()
self.db.close()
+38 -19
View File
@@ -1,5 +1,6 @@
import asyncio
from loguru import logger
from sqlalchemy import or_
from sqlalchemy.orm import Session
from database import get_db
@@ -62,23 +63,6 @@ class UserServices:
self.db.add(new_user)
self.db.flush() # Flush to get the user ID without committing
stripe_customer, stripe_account = await asyncio.gather(
self.stripe_service.create_customer(
new_user.id, user.email, user.username
),
self.stripe_service.create_account(
new_user.id, user.email, user.username, user.mobile
),
)
# Create stripe user
stripe_user = StripeUsers(
user_id=new_user.id,
customer_id=stripe_customer.id,
account_id=stripe_account.id,
)
self.db.add(stripe_user)
# Get clinic data
clinic = user_data.clinic
@@ -90,18 +74,35 @@ class UserServices:
if char.isalnum() or char == "-" or char == "_"
)
existing_clinic = (
self.db.query(Clinics).filter(Clinics.domain == domain).first()
self.db.query(Clinics).filter(
or_(Clinics.domain == domain,
Clinics.email == clinic.email,
Clinics.phone == clinic.phone,
Clinics.emergency_phone == clinic.emergency_phone,
Clinics.abn_number == clinic.abn_number,
)
).first()
)
if existing_clinic:
# This will trigger rollback in the exception handler
raise ValidationException("Clinic with same domain already exists")
if existing_clinic.domain == domain:
raise ValidationException("Clinic with same name already exists")
if existing_clinic.email == clinic.email:
raise ValidationException("Clinic with same email already exists")
if existing_clinic.phone == clinic.phone:
raise ValidationException("Clinic with same phone already exists")
if existing_clinic.emergency_phone == clinic.emergency_phone:
raise ValidationException("Clinic with same emergency phone already exists")
if existing_clinic.abn_number == clinic.abn_number:
raise ValidationException("Clinic with same ABN already exists")
# Create clinic instance
new_clinic = Clinics(
name=clinic.name,
address=clinic.address,
phone=clinic.phone,
emergency_phone=clinic.emergency_phone,
email=clinic.email,
integration=clinic.integration,
pms_id=clinic.pms_id,
@@ -151,6 +152,24 @@ class UserServices:
clinic.email
)
stripe_customer, stripe_account = await asyncio.gather(
self.stripe_service.create_customer(
new_user.id, user.email, user.username
),
self.stripe_service.create_account(
new_user.id, user.email, user.username, user.mobile
),
)
# Create stripe user
stripe_user = StripeUsers(
user_id=new_user.id,
customer_id=stripe_customer.id,
account_id=stripe_account.id,
)
self.db.add(stripe_user)
signup_pricing = await self.dashboard_service.get_signup_pricing_master()
fees_to_be = {