Kulcare Docs
DeveloperAuthentication Documentation04 features

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 routes

Technology 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
end

BillingSubscription

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
end

Customer & 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
end

BillingPayment

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

Messaging 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
end

MsgCreditOrder

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
end

Promotional & 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
end

BillingAddon

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
end

API 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 user

Example 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 user

Subscription 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 subscriptions

Create 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 details

Webhook Handling

POST /razorpay/webhooks               # Razorpay webhook endpoint

Messaging Credits

Credit Management

GET  /msg_credits                     # Get credit balance
POST /msg_credit_orders               # Purchase credits
GET  /msg_credit_orders/:id           # Get order details

Credit 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 addresses

Promotional Codes

GET  /billing_promo_codes             # List available promo codes
GET  /billing_promo_codes/validate    # Validate promo code

Business 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
end

Payment 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
end

Message 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
end

Credit 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
end

Razorpay 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
end

Subscription 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
end

Webhook 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
end

Billing 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 }
end

Usage 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"
end

Financial 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
end

Transfers & 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
end

Email 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
end

Razorpay 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
end

Usage 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
end

Message 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.consumed

2. 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
end

Webhook 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
end

Security & Compliance

Payment Security

  1. Razorpay Integration: PCI-compliant payment processing
  2. Webhook Verification: Cryptographic signature validation
  3. Secure Storage: Encrypted payment method details
  4. Audit Trails: Complete transaction logging

Data Protection

  1. Sensitive Data: Credit card details never stored locally
  2. Access Control: Role-based access to billing information
  3. Encryption: Database encryption for sensitive fields
  4. GDPR Compliance: Data retention and deletion policies

Financial Controls

  1. Reconciliation: Regular payment reconciliation with Razorpay
  2. Fraud Detection: Suspicious transaction monitoring
  3. Refund Management: Controlled refund processes
  4. Account Monitoring: Balance and usage monitoring

Error Handling

Common Error Scenarios

  1. Payment Failures: Card declined, insufficient funds
  2. Subscription Issues: Plan changes, cancellations
  3. Webhook Failures: Network issues, processing errors
  4. Credit Insufficiency: Running out of messaging credits

Error Recovery

  1. Automatic Retry: Failed payment retry mechanism
  2. Grace Periods: Service continuation during payment issues
  3. Manual Intervention: Admin tools for issue resolution
  4. User Notifications: Clear error communication

Monitoring & Analytics

Key Metrics

  1. Revenue Tracking: Monthly recurring revenue (MRR)
  2. Churn Rates: Subscription cancellation rates
  3. Payment Success: Payment failure and retry rates
  4. 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)
  }
end

Best Practices

For Frontend Integration

  1. Payment Flow: Use Razorpay's hosted checkout for security
  2. Error Handling: Graceful handling of payment failures
  3. Loading States: Show progress during payment processing
  4. Receipt Management: Automatic receipt generation and email
  5. Credit Monitoring: Real-time credit balance updates

For Backend Development

  1. Idempotency: Ensure webhook processing is idempotent
  2. Logging: Comprehensive audit trails for financial transactions
  3. Testing: Mock Razorpay responses for testing
  4. Monitoring: Alert on payment failures and webhook issues
  5. Reconciliation: Regular financial reconciliation processes

On this page