Billing System
This document provides comprehensive coverage of Kulcare's billing system, designed to handle healthcare subscription management, payments, messaging credits, and financial transactions.
Overview
The Kulcare billing system is a comprehensive solution that manages:
- Subscription Management: Recurring billing plans with various frequencies
- Payment Processing: Razorpay integration for secure payments
- Messaging Credits: SMS/WhatsApp credit management system
- Billing Automation: Automated invoicing and payment collection
- Refunds & Transfers: Account management and financial reconciliation
- Promotional Codes: Discount and promotional campaigns
Core Concepts
From the billing overview documentation:
- Bills are core functionality with biller, recipient, and bill services
- Biller: The service provider (clinic)
- Recipient: The service consumer (patient)
- Bill Services: List of delivered services with amounts, discounts, quantities
Architecture
The billing system follows the application's parts-based architecture:
parts/billing/
├── controllers/ # API endpoints for billing operations
├── interactors/ # Business logic using interactor pattern
├── models/ # Data models and business logic
├── services/ # External service integrations (Razorpay)
├── policies/ # Authorization policies
├── serializers/ # JSON response formatters
├── mailers/ # Email notification system
├── migrate/ # Database migrations
└── routes.rb # Billing routesTechnology Stack
- Framework: Ruby on Rails 7.1.3.4 with parts-based architecture
- Payment Gateway: Razorpay for subscription and payment processing
- Pattern: Interactor pattern for business logic encapsulation
- Database: PostgreSQL with JSONB fields for flexible data
- Background Jobs: Sidekiq for payment processing and notifications
- Email: ActionMailer for invoice and payment notifications
Core Data Models
Billing Plans & Subscriptions
BillingPlan
Core subscription plans with various frequencies and pricing.
# Table: billing_plans
class BillingPlan < ApplicationRecord
belongs_to :account
belongs_to :basic_billing_frequency # Daily, Weekly, Monthly, Yearly
belongs_to :basic_billing_currency # INR, USD
has_many :billing_plan_products
has_many :billing_plan_rules
# Key columns:
# name: string - Plan display name
# amount: decimal - Base amount
# interval: integer - Billing interval (1 month, 3 months, etc.)
# quantity: integer - Included quantity/usage limits
# active: boolean - Plan availability status
# razorpay_plan_id: string - Razorpay plan identifier
scope :active, -> { where(active: true) }
def frequency
basic_billing_frequency&.name # 'monthly', 'yearly', etc.
end
def plan_period
case frequency.downcase
when 'daily' then interval.days
when 'weekly' then interval.weeks
when 'monthly' then interval.months
when 'yearly' then interval.years
end
end
endBillingSubscription
Active subscriptions linking customers to billing plans.
# Table: billing_subscriptions
class BillingSubscription < ApplicationRecord
belongs_to :billing_plan
belongs_to :billing_customer
belongs_to :basic_billing_subscription_status
belongs_to :billing_promo_code, optional: true
has_many :billing_subscription_addons
has_many :billing_payments
# Key columns:
# current_start: datetime - Current billing period start
# current_end: datetime - Current billing period end
# razorpay_subscription_id: string - Razorpay subscription ID
# cancelled: boolean - Cancellation status
# cancelled_at: datetime - Cancellation timestamp
def is_active?
return false if current_end < Time.now
active_status_ids = BasicBillingSubscriptionStatus.where(
default_name: %w[active authenticated to_be_cancelled]
).pluck(:id)
active_status_ids.include?(basic_billing_subscription_status_id)
end
def remaining_quantity
billable_events_count = billing_customer&.user&.billable_events_count
billing_plan.quantity - billable_events_count
end
endCustomer & Payment Management
BillingCustomer
Customer information and billing details.
# Table: billing_customers
class BillingCustomer < ApplicationRecord
belongs_to :user
has_many :billing_subscriptions
has_many :billing_payments
has_one :billing_address
# Key columns:
# user_id: integer - Associated user
# razorpay_customer_id: string - Razorpay customer identifier
# gstin: string - GST identification number
# unique_id: string - Internal unique identifier
endBillingPayment
Payment transaction records.
# Table: billing_payments
class BillingPayment < ApplicationRecord
belongs_to :billing_subscription
belongs_to :billing_customer
belongs_to :billing_plan, optional: true
belongs_to :billing_order, optional: true
belongs_to :basic_billing_payment_status
# Key columns:
# amount: decimal - Payment amount
# discount_amount: decimal - Applied discount
# razorpay_payment_id: string - Razorpay payment ID
# payment_details: jsonb - Payment method details
# billing_address_details: jsonb - Billing address snapshot
# status: string - Payment status (pending, paid, failed)
endMessaging Credit System
MsgCredit
SMS/WhatsApp credit management for clinics.
# Table: msg_credits
class MsgCredit < ApplicationRecord
belongs_to :clinic
has_many :msg_credit_logs
# Key columns:
# clinic_id: integer - Associated clinic
# available: decimal - Available credits
# consumed: decimal - Used credits
# purchased: decimal - Total purchased credits
def balance
available - consumed
end
endMsgCreditOrder
Credit purchase orders.
# Table: msg_credit_orders
class MsgCreditOrder < ApplicationRecord
belongs_to :clinic
belongs_to :msg_credit_plan
has_many :msg_credit_order_items
has_many :billing_payments
# Key columns:
# clinic_id: integer - Purchasing clinic
# msg_credit_plan_id: integer - Selected credit plan
# quantity: integer - Number of credits purchased
# amount: decimal - Total amount
# razorpay_order_id: string - Razorpay order identifier
endPromotional & Add-on Systems
BillingPromoCode
Promotional codes and discounts.
# Table: billing_promo_codes
class BillingPromoCode < ApplicationRecord
has_many :billing_subscriptions
# Key columns:
# code: string - Promotional code
# discount_type: string - 'percentage' or 'fixed'
# discount_value: decimal - Discount amount/percentage
# valid_from: datetime - Validity start date
# valid_until: datetime - Validity end date
# usage_limit: integer - Maximum usage count
# used: integer - Current usage count
# active: boolean - Code availability
endBillingAddon
Additional services and features.
# Table: billing_addons
class BillingAddon < ApplicationRecord
belongs_to :account
has_many :billing_subscription_addons
# Key columns:
# name: string - Addon name
# amount: decimal - Addon price
# description: text - Addon description
# active: boolean - Addon availability
# razorpay_addon_id: string - Razorpay addon identifier
endAPI Endpoints
Subscription Management
Billing Plans
GET /billing_plans # List available plans
GET /billing_plans/:id # Get specific plan details
GET /billing_plans/active_plan # Get current active plan for userExample Response:
{
"success": true,
"data": {
"billing_plans": [
{
"id": 1,
"name": "Basic Monthly",
"amount": 999.0,
"frequency": "monthly",
"interval": 1,
"quantity": 100,
"currency": "INR",
"features": ["Basic Support", "SMS Credits"],
"active": true
}
]
}
}Billing Addons
GET /billing_addons # List available addons
GET /billing_addons/active_addons # Get active addons for userSubscription Operations
POST /billing_subscriptions # Create new subscription
PUT /billing_subscriptions/update # Update existing subscription
POST /billing_subscriptions/cancel # Cancel subscription
POST /billing_subscriptions/create_subscription_addons # Add addons
GET /billing_subscriptions/summary # Get subscription summary
POST /billing_subscriptions/zero_payment # Handle zero-amount subscriptionsCreate Subscription Example:
{
"billing_subscription": {
"billing_plan_id": 1,
"billing_promo_code": "DISCOUNT20",
"billing_address_attributes": {
"name": "Clinic Name",
"address_line_1": "123 Main Street",
"city": "Mumbai",
"state": "Maharashtra",
"pincode": "400001"
}
}
}Payment Processing
Payments & Invoices
POST /billing_payments # Create payment
GET /billing_payments # List payments
GET /billing_payments/billing_invoices/:id # Get invoice detailsWebhook Handling
POST /razorpay/webhooks # Razorpay webhook endpointMessaging Credits
Credit Management
GET /msg_credits # Get credit balance
POST /msg_credit_orders # Purchase credits
GET /msg_credit_orders/:id # Get order detailsCredit Purchase Example:
{
"msg_credit_order": {
"msg_credit_plan_id": 1,
"quantity": 1000
}
}Billing Addresses & Promo Codes
Address Management
POST /billing_addresses # Create billing address
GET /billing_addresses # List billing addressesPromotional Codes
GET /billing_promo_codes # List available promo codes
GET /billing_promo_codes/validate # Validate promo codeBusiness Logic (Interactors)
Subscription Management
Creating Subscriptions
# parts/billing/interactors/subscription/subscribe.rb
class Subscription::Subscribe
include Interactor
def call
# 1. Validate billing plan and customer
# 2. Apply promotional codes if provided
# 3. Create Razorpay subscription
# 4. Create billing subscription record
# 5. Handle initial payment
# 6. Send confirmation email
end
endPayment Processing
# parts/billing/interactors/subscription/payment_from_webhook.rb
class Subscription::PaymentFromWebhook
include Interactor
def call
# 1. Verify webhook authenticity
# 2. Extract payment information
# 3. Update subscription status
# 4. Record payment details
# 5. Send payment confirmation
# 6. Update credit balances
end
endMessage Credit Management
Credit Purchase Flow
# parts/billing/interactors/msg_credit_order_actions/create.rb
class MsgCreditOrderActions::Create
include Interactor
def call
# 1. Validate credit plan and quantity
# 2. Calculate total amount
# 3. Create Razorpay order
# 4. Create msg_credit_order record
# 5. Return payment details for frontend
end
endCredit Allocation
# parts/billing/interactors/msg_credit_payment_actions/create_from_webhook.rb
class MsgCreditPaymentActions::CreateFromWebhook
include Interactor
def call
# 1. Process webhook payment data
# 2. Update msg_credit_order status
# 3. Add credits to clinic account
# 4. Create credit log entry
# 5. Send purchase confirmation
end
endRazorpay Integration
Service Classes
Customer Management
# parts/billing/services/create_razorpay_customer.rb
class CreateRazorpayCustomer
def self.call(billing_customer)
razorpay = Razorpay::Customer.create(
name: billing_customer.user.full_name,
email: billing_customer.user.email,
contact: billing_customer.user.complete_phone_number
)
billing_customer.update!(razorpay_customer_id: razorpay.id)
razorpay
end
endSubscription Management
# parts/billing/services/generate_razorpay_subscription.rb
class GenerateRazorpaySubscription
def self.call(billing_subscription)
subscription = Razorpay::Subscription.create(
plan_id: billing_subscription.billing_plan.razorpay_plan_id,
customer_id: billing_subscription.billing_customer.razorpay_customer_id,
total_count: calculate_total_count(billing_subscription),
start_at: billing_subscription.current_start.to_i
)
billing_subscription.update!(
razorpay_subscription_id: subscription.id
)
subscription
end
endWebhook Processing
Payment Events
# parts/billing/interactors/webhooks/payment_change.rb
class Webhooks::PaymentChange
include Interactor
def call
case context.event
when 'payment.captured'
handle_payment_success
when 'payment.failed'
handle_payment_failure
when 'subscription.charged'
handle_subscription_renewal
end
end
private
def handle_payment_success
# Update payment status
# Extend subscription period
# Add messaging credits if applicable
# Send success notification
end
def handle_payment_failure
# Update payment status
# Send failure notification
# Handle retry logic
end
endBilling Rules & Validation
Plan Rules System
# Table: billing_plan_rules
class BillingPlanRule < ApplicationRecord
belongs_to :billing_plan
# Key columns:
# rule_type: string - Type of rule (quantity_limit, feature_access)
# rule_value: jsonb - Rule configuration
# active: boolean - Rule status
# Example rule configurations:
# Quantity limits: { "max_patients": 100, "max_doctors": 5 }
# Feature access: { "telemedicine": true, "reports": false }
endUsage Tracking
# Table: billable_events
class BillableEvent < ApplicationRecord
belongs_to :user
# Key columns:
# event_type: string - Type of billable event
# event_data: jsonb - Event details
# amount: decimal - Associated cost (if any)
# created_at: datetime - Event timestamp
# Common event types:
# - "appointment_created"
# - "sms_sent"
# - "patient_added"
# - "prescription_generated"
endFinancial Management
Account Management
# Table: billing_accounts
class BillingAccount < ApplicationRecord
belongs_to :clinic
has_many :billing_transfers
has_many :billing_refunds
# Key columns:
# clinic_id: integer - Associated clinic
# balance: decimal - Current account balance
# consultation_percentage: decimal - Commission percentage
# active: boolean - Account status
endTransfers & Refunds
# Table: billing_transfers
class BillingTransfer < ApplicationRecord
belongs_to :billing_account
# Key columns:
# amount: decimal - Transfer amount
# transfer_type: string - Type (payout, commission)
# status: string - Transfer status
# razorpay_transfer_id: string - Razorpay transfer ID
end
# Table: billing_refunds
class BillingRefund < ApplicationRecord
belongs_to :billing_payment
# Key columns:
# amount: decimal - Refund amount
# reason: string - Refund reason
# status: string - Refund status
# razorpay_refund_id: string - Razorpay refund ID
endEmail Notifications
Billing Mailers
Pricing Communications
# parts/billing/mailers/pricing_mailer.rb
class PricingMailer < ApplicationMailer
def subscription_ending(user, days_remaining)
@user = user
@days_remaining = days_remaining
mail(
to: user.email,
subject: "Your Kulcare subscription expires in #{days_remaining} days"
)
end
def trial_ending(user, days_remaining)
@user = user
@days_remaining = days_remaining
mail(
to: user.email,
subject: "Your Kulcare trial expires in #{days_remaining} days"
)
end
endRazorpay Notifications
# parts/billing/mailers/razorpay_mailer.rb
class RazorpayMailer < ApplicationMailer
def send_invoice(billing_payment)
@billing_payment = billing_payment
@user = billing_payment.billing_customer.user
mail(
to: @user.email,
subject: "Payment Receipt - Kulcare"
)
end
endUsage Patterns & Examples
Subscription Creation Flow
1. Plan Selection
# Get available plans for clinic account
plans = BillingPlan.joins(:account)
.where(account: { name: 'clinic' })
.active
.order(:amount)2. Create Subscription
result = Subscription::Subscribe.call(
billing_plan_id: selected_plan.id,
user: current_user,
billing_address_params: address_params,
promo_code: 'DISCOUNT20'
)
if result.success?
# Subscription created successfully
subscription = result.billing_subscription
payment_url = result.razorpay_order.short_url
else
# Handle errors
errors = result.message
endMessage Credit Purchase
1. Check Current Balance
clinic = current_user.clinics.first
msg_credit = clinic.msg_credits.first || MsgCredit.create(clinic: clinic)
balance = msg_credit.available - msg_credit.consumed2. Purchase Credits
result = MsgCreditOrderActions::Create.call(
clinic: clinic,
msg_credit_plan_id: plan_id,
quantity: 1000
)
if result.success?
order = result.msg_credit_order
payment_url = result.razorpay_order.short_url
# Redirect user to payment gateway
endWebhook Processing
Handle Razorpay Webhooks
# In BillingWebhooksController
def webhooks
# Verify webhook signature
webhook_signature = request.headers['X-Razorpay-Signature']
unless Razorpay::Utility.verify_webhook_signature(
request.raw_post, webhook_signature, Rails.application.credentials.razorpay_webhook_secret
)
head :bad_request and return
end
# Process webhook
result = Webhooks::Receive.call(webhook_data: webhook_params)
if result.success?
head :ok
else
head :unprocessable_entity
end
endSecurity & Compliance
Payment Security
- Razorpay Integration: PCI-compliant payment processing
- Webhook Verification: Cryptographic signature validation
- Secure Storage: Encrypted payment method details
- Audit Trails: Complete transaction logging
Data Protection
- Sensitive Data: Credit card details never stored locally
- Access Control: Role-based access to billing information
- Encryption: Database encryption for sensitive fields
- GDPR Compliance: Data retention and deletion policies
Financial Controls
- Reconciliation: Regular payment reconciliation with Razorpay
- Fraud Detection: Suspicious transaction monitoring
- Refund Management: Controlled refund processes
- Account Monitoring: Balance and usage monitoring
Error Handling
Common Error Scenarios
- Payment Failures: Card declined, insufficient funds
- Subscription Issues: Plan changes, cancellations
- Webhook Failures: Network issues, processing errors
- Credit Insufficiency: Running out of messaging credits
Error Recovery
- Automatic Retry: Failed payment retry mechanism
- Grace Periods: Service continuation during payment issues
- Manual Intervention: Admin tools for issue resolution
- User Notifications: Clear error communication
Monitoring & Analytics
Key Metrics
- Revenue Tracking: Monthly recurring revenue (MRR)
- Churn Rates: Subscription cancellation rates
- Payment Success: Payment failure and retry rates
- Credit Usage: Messaging credit consumption patterns
Health Checks
# Billing system health check
def billing_health_check
{
active_subscriptions: BillingSubscription.joins(:basic_billing_subscription_status)
.where(basic_billing_subscription_statuses: { default_name: 'active' })
.count,
failed_payments_today: BillingPayment.joins(:basic_billing_payment_status)
.where(basic_billing_payment_statuses: { default_name: 'failed' })
.where('billing_payments.created_at > ?', 1.day.ago)
.count,
webhook_errors_today: RazorpayWebhookError.where('created_at > ?', 1.day.ago).count,
total_credit_balance: MsgCredit.sum(:available)
}
endBest Practices
For Frontend Integration
- Payment Flow: Use Razorpay's hosted checkout for security
- Error Handling: Graceful handling of payment failures
- Loading States: Show progress during payment processing
- Receipt Management: Automatic receipt generation and email
- Credit Monitoring: Real-time credit balance updates
For Backend Development
- Idempotency: Ensure webhook processing is idempotent
- Logging: Comprehensive audit trails for financial transactions
- Testing: Mock Razorpay responses for testing
- Monitoring: Alert on payment failures and webhook issues
- Reconciliation: Regular financial reconciliation processes
Related Documentation
- Authentication System - User authentication and roles
- API Reference - Billing APIs - Complete API documentation
- Clinic APIs - Clinic management endpoints
- Background Jobs - Sidekiq configuration for billing jobs