Revenue Sharing & Commission System Implementation Plan
System Architecture Overview
1. Database Schema Design
1.1 Revenue Sharing Tables
Database Schema Diagram
revenue_share_configurations
Primary table for multi-location revenue sharing setups.
Fields:
id(primary key)location_id(foreign key to clinics table)-
Configuration for clinic-based revenue sharing rules.configuration_name(string, user-defined name)
Fields:
id(bigint, primary key)clinic_id(bigint, foreign key to clinics table)configuration_name(varchar(255), user-defined name)description(text, optional)distribution_rules(jsonb, distribution logic and rules)approval_required(boolean, default: true)effective_from(timestamp)effective_until(timestamp, nullable)status(enum: 'draft', 'active', 'inactive')created_by_user_id(bigint, foreign key to users)created_at(timestamp)updated_at(timestamp)
Indexes:
clinic_id,status,effective_fromcreated_by_user_id
revenue_share_stakeholders
Stakeholders for revenue sharing (linked to users with specific roles).
Fields:
id(bigint, primary key)configuration_id(bigint, foreign key to revenue_share_configurations)user_id(bigint, foreign key to users - must have clinic_owner/admin/staff/consultant role)stakeholder_type(enum: 'clinic_owner', 'partner', 'investor', 'referrer', 'consultant')share_percentage(decimal(5,2), nullable)share_amount(decimal(12,2), nullable)minimum_amount(decimal(12,2), default: 0)maximum_amount(decimal(12,2), nullable)conditions(jsonb, conditional distribution rules)billing_account_id(bigint, foreign key to billing_accounts, nullable)payout_method(enum: 'bank_transfer', 'razorpay', 'manual')active(boolean, default: true)created_at(timestamp)updated_at(timestamp)
Indexes:
configuration_id,user_id,activeuser_id,stakeholder_type
revenue_shares
Actual revenue distribution records linked to payments.
Fields:
id(bigint, primary key)billing_payment_id(bigint, foreign key to billing_payments)stakeholder_id(bigint, foreign key to revenue_share_stakeholders)amount(decimal(12,2), actual distributed amount)percentage(decimal(5,2), percentage used for calculation)calculation_data(jsonb, detailed calculation breakdown)status(enum: 'pending', 'approved', 'distributed', 'failed', 'cancelled')distributed_at(timestamp, nullable)created_at(timestamp)updated_at(timestamp)
Indexes:
billing_payment_id,statusstakeholder_id,status,distributed_at
1.2 Commission Management Tables
commission_rules
Commission rules for healthcare professionals.
Fields:
id(bigint, primary key)clinic_id(bigint, foreign key to clinics)rule_name(varchar(255), descriptive name)rule_type(enum: 'percentage', 'fixed_amount', 'tiered', 'performance_bonus')target_entity(enum: 'consultation', 'service', 'revenue', 'patient_satisfaction')conditions(jsonb, eligibility conditions)calculation_method(jsonb, calculation parameters)effective_from(timestamp)effective_until(timestamp, nullable)active(boolean, default: true)created_by_user_id(bigint, foreign key to users)created_at(timestamp)updated_at(timestamp)
Indexes:
clinic_id,active,effective_fromrule_type,target_entity
commission_calculations
Calculated commission amounts for healthcare professionals.
Fields:
id(bigint, primary key)commission_rule_id(bigint, foreign key to commission_rules)clinic_doctor_id(bigint, foreign key to clinic_doctors)billing_payment_id(bigint, foreign key to billing_payments, nullable)base_amount(decimal(12,2), amount used for calculation)commission_amount(decimal(12,2), calculated commission)calculation_details(jsonb, detailed breakdown)calculation_period(varchar(50), 'daily', 'weekly', 'monthly')status(enum: 'calculated', 'approved', 'paid', 'cancelled')approved_by_user_id(bigint, foreign key to users, nullable)calculated_at(timestamp)created_at(timestamp)updated_at(timestamp)
Indexes:
clinic_doctor_id,status,calculated_atcommission_rule_id,calculation_periodbilling_payment_id
commission_payments
Actual commission payment records.
Fields:
id(bigint, primary key)commission_calculation_id(bigint, foreign key to commission_calculations)billing_account_id(bigint, foreign key to billing_accounts)amount(decimal(12,2))payment_method(enum: 'bank_transfer', 'razorpay', 'manual')payment_reference(varchar(255), external payment reference)payment_status(enum: 'pending', 'processing', 'completed', 'failed')processed_at(timestamp, nullable)created_at(timestamp)updated_at(timestamp)
Indexes:
commission_calculation_idbilling_account_id,payment_statusprocessed_at
1.3 Workflow & Audit Tables
approval_workflows
Approval workflow tracking for revenue sharing and commissions.
Fields:
id(bigint, primary key)entity_type(enum: 'revenue_share', 'commission_calculation', 'commission_rule')entity_id(bigint, polymorphic reference)workflow_config(jsonb, workflow configuration)current_step(integer, current approval step)status(enum: 'pending', 'approved', 'rejected', 'escalated')approved_by_user_id(bigint, foreign key to users, nullable)approved_at(timestamp, nullable)rejection_reason(text, nullable)created_at(timestamp)updated_at(timestamp)
Indexes:
entity_type,entity_id,statusapproved_by_user_id
audit_trails
Comprehensive audit trail for all financial operations.
Fields:
id(bigint, primary key)entity_type(varchar(100), table name)entity_id(bigint, record id)action(enum: 'create', 'update', 'delete', 'approve', 'calculate', 'distribute')user_id(bigint, foreign key to users)changes(jsonb, before/after values)metadata(jsonb, additional context)ip_address(varchar(45))user_agent(text)created_at(timestamp)
Indexes:
entity_type,entity_id,created_atuser_id,created_ataction,created_at
1.4 Reporting & Analytics Tables
custom_reports
User-defined custom reports.
Fields:
id(bigint, primary key)clinic_id(bigint, foreign key to clinics)report_name(varchar(255))report_type(enum: 'revenue_sharing', 'commission', 'combined', 'custom')report_config(jsonb, report parameters and filters)created_by_user_id(bigint, foreign key to users)shared_with_users(jsonb, array of user IDs)active(boolean, default: true)created_at(timestamp)updated_at(timestamp)
Indexes:
clinic_id,activecreated_by_user_id
scheduled_reports
Automated report generation and delivery.
Fields:
id(bigint, primary key)custom_report_id(bigint, foreign key to custom_reports)schedule_config(jsonb, scheduling parameters)recipient_config(jsonb, delivery recipients and methods)last_run_at(timestamp, nullable)next_run_at(timestamp, nullable)active(boolean, default: true)created_at(timestamp)updated_at(timestamp)
Indexes:
next_run_at,activecustom_report_id
user_training_progress
User training completion tracking for system features.
Fields:
id(bigint, primary key)user_id(bigint, foreign key to users)training_module(enum: 'revenue_sharing_basic', 'commission_setup', 'report_creation', 'approval_workflow')completion_status(enum: 'not_started', 'in_progress', 'completed')completed_at(timestamp, nullable)quiz_score(integer, nullable)created_at(timestamp)updated_at(timestamp)
Indexes:
user_id,training_modulecompletion_status
2. Migration Scripts
2.1 Revenue Sharing Tables Migration
class CreateRevenueShareConfiguration < ActiveRecord::Migration[7.1]
def change
create_table :revenue_share_configurations do |t|
t.references :clinic, null: false, foreign_key: true
t.string :configuration_name, null: false, limit: 255
t.text :description
t.jsonb :distribution_rules, null: false, default: {}
t.boolean :approval_required, default: true
t.timestamp :effective_from, null: false
t.timestamp :effective_until
t.string :status, default: 'draft', null: false
t.references :created_by_user, null: false, foreign_key: { to_table: :users }
t.timestamps
t.index [:clinic_id, :status, :effective_from]
t.index :created_by_user_id
end
create_table :revenue_share_stakeholders do |t|
t.references :configuration, null: false, foreign_key: { to_table: :revenue_share_configurations }
t.references :user, null: false, foreign_key: true
t.string :stakeholder_type, null: false
t.decimal :share_percentage, precision: 5, scale: 2
t.decimal :share_amount, precision: 12, scale: 2
t.decimal :minimum_amount, precision: 12, scale: 2, default: 0
t.decimal :maximum_amount, precision: 12, scale: 2
t.jsonb :conditions, default: {}
t.references :billing_account, foreign_key: true
t.string :payout_method, null: false, default: 'manual'
t.boolean :active, default: true
t.timestamps
t.index [:configuration_id, :user_id, :active]
t.index [:user_id, :stakeholder_type]
end
create_table :revenue_shares do |t|
t.references :billing_payment, null: false, foreign_key: true
t.references :stakeholder, null: false, foreign_key: { to_table: :revenue_share_stakeholders }
t.decimal :amount, precision: 12, scale: 2, null: false
t.decimal :percentage, precision: 5, scale: 2, null: false
t.jsonb :calculation_data, default: {}
t.string :status, default: 'pending', null: false
t.timestamp :distributed_at
t.timestamps
t.index [:billing_payment_id, :status]
t.index [:stakeholder_id, :status, :distributed_at]
end
end
end2.2 Commission Tables Migration
class CreateCommissionTables < ActiveRecord::Migration[7.1]
def change
create_table :commission_rules do |t|
t.references :clinic, null: false, foreign_key: true
t.string :rule_name, null: false, limit: 255
t.string :rule_type, null: false
t.string :target_entity, null: false
t.jsonb :conditions, default: {}
t.jsonb :calculation_method, null: false, default: {}
t.timestamp :effective_from, null: false
t.timestamp :effective_until
t.boolean :active, default: true
t.references :created_by_user, null: false, foreign_key: { to_table: :users }
t.timestamps
t.index [:clinic_id, :active, :effective_from]
t.index [:rule_type, :target_entity]
end
create_table :commission_calculations do |t|
t.references :commission_rule, null: false, foreign_key: true
t.references :clinic_doctor, null: false, foreign_key: true
t.references :billing_payment, foreign_key: true
t.decimal :base_amount, precision: 12, scale: 2, null: false
t.decimal :commission_amount, precision: 12, scale: 2, null: false
t.jsonb :calculation_details, default: {}
t.string :calculation_period, limit: 50
t.string :status, default: 'calculated', null: false
t.references :approved_by_user, foreign_key: { to_table: :users }
t.timestamp :calculated_at, null: false
t.timestamps
t.index [:clinic_doctor_id, :status, :calculated_at]
t.index [:commission_rule_id, :calculation_period]
t.index :billing_payment_id
end
create_table :commission_payments do |t|
t.references :commission_calculation, null: false, foreign_key: true
t.references :billing_account, null: false, foreign_key: true
t.decimal :amount, precision: 12, scale: 2, null: false
t.string :payment_method, null: false
t.string :payment_reference, limit: 255
t.string :payment_status, default: 'pending', null: false
t.timestamp :processed_at
t.timestamps
t.index :commission_calculation_id
t.index [:billing_account_id, :payment_status]
t.index :processed_at
end
end
end2.3 Workflow & Audit Tables Migration
class CreateWorkflowAndAuditTables < ActiveRecord::Migration[7.1]
def change
create_table :approval_workflows do |t|
t.string :entity_type, null: false
t.bigint :entity_id, null: false
t.jsonb :workflow_config, default: {}
t.integer :current_step, default: 1
t.string :status, default: 'pending', null: false
t.references :approved_by_user, foreign_key: { to_table: :users }
t.timestamp :approved_at
t.text :rejection_reason
t.timestamps
t.index [:entity_type, :entity_id, :status]
t.index :approved_by_user_id
end
create_table :audit_trails do |t|
t.string :entity_type, null: false, limit: 100
t.bigint :entity_id, null: false
t.string :action, null: false
t.references :user, null: false, foreign_key: true
t.jsonb :changes, default: {}
t.jsonb :metadata, default: {}
t.string :ip_address, limit: 45
t.text :user_agent
t.timestamp :created_at, null: false
t.index [:entity_type, :entity_id, :created_at]
t.index [:user_id, :created_at]
t.index [:action, :created_at]
end
end
end2.4 Reporting Tables Migration
class CreateReportingTables < ActiveRecord::Migration[7.1]
def change
create_table :custom_reports do |t|
t.references :clinic, null: false, foreign_key: true
t.string :report_name, null: false, limit: 255
t.string :report_type, null: false
t.jsonb :report_config, null: false, default: {}
t.references :created_by_user, null: false, foreign_key: { to_table: :users }
t.jsonb :shared_with_users, default: []
t.boolean :active, default: true
t.timestamps
t.index [:clinic_id, :active]
t.index :created_by_user_id
end
create_table :scheduled_reports do |t|
t.references :custom_report, null: false, foreign_key: true
t.jsonb :schedule_config, null: false, default: {}
t.jsonb :recipient_config, null: false, default: {}
t.timestamp :last_run_at
t.timestamp :next_run_at
t.boolean :active, default: true
t.timestamps
t.index [:next_run_at, :active]
t.index :custom_report_id
end
create_table :user_training_progress do |t|
t.references :user, null: false, foreign_key: true
t.string :training_module, null: false
t.string :completion_status, default: 'not_started', null: false
t.timestamp :completed_at
t.integer :quiz_score
t.timestamps
t.index [:user_id, :training_module]
t.index :completion_status
end
end
end3. Business Logic Implementation
3.1 Service Classes Architecture
3.2 Core Models
RevenueShareConfiguration Model
# parts/billing/models/revenue_share_configuration.rb
class RevenueShareConfiguration < ApplicationRecord
# Associations
belongs_to :clinic
belongs_to :created_by_user, class_name: 'User'
has_many :revenue_share_stakeholders, dependent: :destroy
has_many :stakeholder_users, through: :revenue_share_stakeholders, source: :user
# Validations
validates :configuration_name, presence: true, length: { maximum: 255 }
validates :status, inclusion: { in: %w[draft active inactive] }
validates :effective_from, presence: true
validates :distribution_rules, presence: true
validate :effective_dates_consistency
validate :distribution_rules_valid
validate :total_percentage_valid
# Scopes
scope :active, -> { where(status: 'active') }
scope :for_clinic, ->(clinic_id) { where(clinic_id: clinic_id) }
scope :effective_on, ->(date) { where('effective_from <= ? AND (effective_until IS NULL OR effective_until >= ?)', date, date) }
# Enums
enum status: { draft: 'draft', active: 'active', inactive: 'inactive' }
private
def effective_dates_consistency
return unless effective_until.present? && effective_from > effective_until
errors.add(:effective_until, 'must be after effective_from')
end
def distribution_rules_valid
return if distribution_rules.is_a?(Hash) && distribution_rules.present?
errors.add(:distribution_rules, 'must be a valid JSON object')
end
def total_percentage_valid
total = revenue_share_stakeholders.sum(:share_percentage)
return if total == 100.0 || total == 0.0
errors.add(:base, 'Total percentage must equal 100%')
end
endRevenueShareStakeholder Model
# parts/billing/models/revenue_share_stakeholder.rb
class RevenueShareStakeholder < ApplicationRecord
# Associations
belongs_to :configuration, class_name: 'RevenueShareConfiguration'
belongs_to :user
belongs_to :billing_account, optional: true
has_many :revenue_shares, foreign_key: :stakeholder_id, dependent: :destroy
# Validations
validates :stakeholder_type, inclusion: { in: %w[clinic_owner partner investor referrer consultant] }
validates :payout_method, inclusion: { in: %w[bank_transfer razorpay manual] }
validates :share_percentage, numericality: { greater_than: 0, less_than_or_equal_to: 100 }, allow_nil: true
validates :share_amount, numericality: { greater_than: 0 }, allow_nil: true
validate :percentage_or_amount_present
validate :user_has_valid_role
# Scopes
scope :active, -> { where(active: true) }
scope :by_type, ->(type) { where(stakeholder_type: type) }
scope :for_user, ->(user_id) { where(user_id: user_id) }
# Enums
enum stakeholder_type: {
clinic_owner: 'clinic_owner',
partner: 'partner',
investor: 'investor',
referrer: 'referrer',
consultant: 'consultant'
}
enum payout_method: {
bank_transfer: 'bank_transfer',
razorpay: 'razorpay',
manual: 'manual'
}
private
def percentage_or_amount_present
return if share_percentage.present? || share_amount.present?
errors.add(:base, 'Either share percentage or share amount must be specified')
end
def user_has_valid_role
valid_roles = %w[clinic_owner admin staff consultant]
user_roles = user.user_accounts.joins(:role).pluck('roles.name')
return if (user_roles & valid_roles).any?
errors.add(:user, 'must have clinic_owner, admin, staff, or consultant role')
end
endCommissionRule Model
# parts/billing/models/commission_rule.rb
class CommissionRule < ApplicationRecord
# Associations
belongs_to :clinic
belongs_to :created_by_user, class_name: 'User'
has_many :commission_calculations, dependent: :destroy
# Validations
validates :rule_name, presence: true, length: { maximum: 255 }
validates :rule_type, inclusion: { in: %w[percentage fixed_amount tiered performance_bonus] }
validates :target_entity, inclusion: { in: %w[consultation service revenue patient_satisfaction] }
validates :effective_from, presence: true
validates :calculation_method, presence: true
validate :effective_dates_consistency
validate :calculation_method_valid
# Scopes
scope :active, -> { where(active: true) }
scope :for_clinic, ->(clinic_id) { where(clinic_id: clinic_id) }
scope :effective_on, ->(date) { where('effective_from <= ? AND (effective_until IS NULL OR effective_until >= ?)', date, date) }
scope :by_type, ->(type) { where(rule_type: type) }
scope :by_entity, ->(entity) { where(target_entity: entity) }
# Enums
enum rule_type: {
percentage: 'percentage',
fixed_amount: 'fixed_amount',
tiered: 'tiered',
performance_bonus: 'performance_bonus'
}
enum target_entity: {
consultation: 'consultation',
service: 'service',
revenue: 'revenue',
patient_satisfaction: 'patient_satisfaction'
}
private
def effective_dates_consistency
return unless effective_until.present? && effective_from > effective_until
errors.add(:effective_until, 'must be after effective_from')
end
def calculation_method_valid
return if calculation_method.is_a?(Hash) && calculation_method.present?
errors.add(:calculation_method, 'must be a valid JSON object')
end
endCommissionCalculation Model
# parts/billing/models/commission_calculation.rb
class CommissionCalculation < ApplicationRecord
# Associations
belongs_to :commission_rule
belongs_to :clinic_doctor
belongs_to :billing_payment, optional: true
belongs_to :approved_by_user, class_name: 'User', optional: true
has_many :commission_payments, dependent: :destroy
# Validations
validates :base_amount, :commission_amount, presence: true, numericality: { greater_than: 0 }
validates :status, inclusion: { in: %w[calculated approved paid cancelled] }
validates :calculation_period, inclusion: { in: %w[daily weekly monthly] }, allow_nil: true
validates :calculated_at, presence: true
# Scopes
scope :pending_approval, -> { where(status: 'calculated') }
scope :approved, -> { where(status: 'approved') }
scope :paid, -> { where(status: 'paid') }
scope :for_doctor, ->(doctor_id) { where(clinic_doctor_id: doctor_id) }
scope :for_period, ->(period) { where(calculation_period: period) }
scope :calculated_between, ->(start_date, end_date) { where(calculated_at: start_date..end_date) }
# Enums
enum status: {
calculated: 'calculated',
approved: 'approved',
paid: 'paid',
cancelled: 'cancelled'
}
# Methods
def doctor
clinic_doctor.doctor
end
def clinic
clinic_doctor.clinic
end
def total_amount_to_pay
commission_amount
end
end3.3 Service Classes
RevenueShareEngine Service
# parts/billing/services/revenue_share_engine.rb
class RevenueShareEngine < BaseInteractor
def call
validate_inputs
load_configurations
calculate_distributions
create_revenue_shares
process_approvals if approval_required?
notify_stakeholders
end
private
def validate_inputs
fail!('Clinic ID is required') unless context.clinic_id.present?
fail!('Billing payment ID is required') unless context.billing_payment_id.present?
@clinic = Clinic.find_by(id: context.clinic_id)
fail!('Clinic not found') unless @clinic
@billing_payment = BillingPayment.find_by(id: context.billing_payment_id)
fail!('Billing payment not found') unless @billing_payment
end
def load_configurations
@configurations = RevenueShareConfiguration
.active
.for_clinic(@clinic.id)
.effective_on(Date.current)
.includes(:revenue_share_stakeholders)
fail!('No active revenue sharing configurations found') if @configurations.empty?
end
def calculate_distributions
@distributions = []
@configurations.each do |config|
config.revenue_share_stakeholders.active.each do |stakeholder|
amount = calculate_stakeholder_amount(stakeholder)
next unless amount > 0
@distributions << {
stakeholder: stakeholder,
amount: amount,
percentage: calculate_percentage(amount),
config: config
}
end
end
end
def calculate_stakeholder_amount(stakeholder)
base_amount = @billing_payment.amount
if stakeholder.share_percentage.present?
calculated = (base_amount * stakeholder.share_percentage / 100).round(2)
else
calculated = stakeholder.share_amount
end
# Apply minimum and maximum constraints
calculated = [calculated, stakeholder.minimum_amount].max if stakeholder.minimum_amount > 0
calculated = [calculated, stakeholder.maximum_amount].min if stakeholder.maximum_amount.present?
calculated
end
def calculate_percentage(amount)
(amount / @billing_payment.amount * 100).round(2)
end
def create_revenue_shares
@revenue_shares = []
@distributions.each do |dist|
revenue_share = RevenueShare.create!(
billing_payment: @billing_payment,
stakeholder: dist[:stakeholder],
amount: dist[:amount],
percentage: dist[:percentage],
calculation_data: {
rule_applied: dist[:config].configuration_name,
base_amount: @billing_payment.amount,
calculation_method: dist[:stakeholder].share_percentage ? 'percentage' : 'fixed_amount',
calculated_at: Time.current
},
status: approval_required? ? 'pending' : 'approved'
)
@revenue_shares << revenue_share
# Log audit trail
AuditTrailService.call(
entity: revenue_share,
action: 'create',
user: context.current_user,
metadata: { billing_payment_id: @billing_payment.id }
)
end
context.revenue_shares = @revenue_shares
end
def approval_required?
@configurations.any?(&:approval_required)
end
def process_approvals
@revenue_shares.each do |revenue_share|
ApprovalWorkflowService.call(
entity: revenue_share,
entity_type: 'revenue_share',
workflow_config: default_approval_workflow
)
end
end
def default_approval_workflow
{
steps: [
{
step: 1,
approver_roles: ['clinic_owner', 'admin'],
required_approvals: 1
}
]
}
end
def notify_stakeholders
@revenue_shares.each do |revenue_share|
NotificationService.call(
recipient: revenue_share.stakeholder.user,
type: 'revenue_share_calculated',
data: {
amount: revenue_share.amount,
billing_payment_id: @billing_payment.id,
status: revenue_share.status
}
)
end
end
endCommissionCalculationEngine Service
# parts/billing/services/commission_calculation_engine.rb
class CommissionCalculationEngine < BaseInteractor
def call
validate_inputs
load_commission_rules
calculate_commissions
create_calculations
process_approvals if approval_required?
notify_doctors
end
private
def validate_inputs
fail!('Clinic doctor ID is required') unless context.clinic_doctor_id.present?
@clinic_doctor = ClinicDoctor.find_by(id: context.clinic_doctor_id)
fail!('Clinic doctor not found') unless @clinic_doctor
@calculation_period = context.calculation_period || 'monthly'
@period_start = context.period_start || 1.month.ago.beginning_of_month
@period_end = context.period_end || 1.month.ago.end_of_month
end
def load_commission_rules
@commission_rules = CommissionRule
.active
.for_clinic(@clinic_doctor.clinic_id)
.effective_on(@period_end)
fail!('No active commission rules found') if @commission_rules.empty?
end
def calculate_commissions
@calculations = []
@commission_rules.each do |rule|
case rule.rule_type
when 'percentage'
calculate_percentage_commission(rule)
when 'fixed_amount'
calculate_fixed_amount_commission(rule)
when 'tiered'
calculate_tiered_commission(rule)
when 'performance_bonus'
calculate_performance_bonus(rule)
end
end
end
def calculate_percentage_commission(rule)
base_amount = get_base_amount_for_rule(rule)
return unless base_amount > 0
percentage = rule.calculation_method['percentage'] || 0
commission_amount = (base_amount * percentage / 100).round(2)
@calculations << {
rule: rule,
base_amount: base_amount,
commission_amount: commission_amount,
calculation_details: {
calculation_type: 'percentage',
percentage_applied: percentage,
period_start: @period_start,
period_end: @period_end
}
}
end
def calculate_fixed_amount_commission(rule)
qualifying_count = get_qualifying_count_for_rule(rule)
return unless qualifying_count > 0
amount_per_unit = rule.calculation_method['amount_per_unit'] || 0
commission_amount = (qualifying_count * amount_per_unit).round(2)
@calculations << {
rule: rule,
base_amount: qualifying_count,
commission_amount: commission_amount,
calculation_details: {
calculation_type: 'fixed_amount',
qualifying_count: qualifying_count,
amount_per_unit: amount_per_unit,
period_start: @period_start,
period_end: @period_end
}
}
end
def calculate_tiered_commission(rule)
base_amount = get_base_amount_for_rule(rule)
return unless base_amount > 0
tiers = rule.calculation_method['tiers'] || []
commission_amount = 0
remaining_amount = base_amount
tiers.each do |tier|
tier_min = tier['min_amount'] || 0
tier_max = tier['max_amount']
tier_percentage = tier['percentage'] || 0
next if remaining_amount <= 0 || base_amount < tier_min
applicable_amount = if tier_max.present?
[remaining_amount, tier_max - tier_min].min
else
remaining_amount
end
tier_commission = (applicable_amount * tier_percentage / 100).round(2)
commission_amount += tier_commission
remaining_amount -= applicable_amount
end
@calculations << {
rule: rule,
base_amount: base_amount,
commission_amount: commission_amount,
calculation_details: {
calculation_type: 'tiered',
tiers_applied: tiers,
period_start: @period_start,
period_end: @period_end
}
}
end
def calculate_performance_bonus(rule)
performance_metrics = get_performance_metrics_for_doctor
return unless performance_metrics.any?
bonus_amount = 0
performance_details = {}
rule.calculation_method['performance_criteria'].each do |criteria|
metric_type = criteria['metric_type']
threshold = criteria['threshold']
bonus_per_unit = criteria['bonus_amount']
actual_value = performance_metrics[metric_type]
next unless actual_value && actual_value >= threshold
bonus_amount += bonus_per_unit
performance_details[metric_type] = {
actual: actual_value,
threshold: threshold,
bonus_earned: bonus_per_unit
}
end
return unless bonus_amount > 0
@calculations << {
rule: rule,
base_amount: bonus_amount,
commission_amount: bonus_amount,
calculation_details: {
calculation_type: 'performance_bonus',
performance_metrics: performance_details,
period_start: @period_start,
period_end: @period_end
}
}
end
def get_base_amount_for_rule(rule)
case rule.target_entity
when 'consultation'
get_consultation_revenue
when 'service'
get_service_revenue
when 'revenue'
get_total_revenue
else
0
end
end
def get_qualifying_count_for_rule(rule)
case rule.target_entity
when 'consultation'
get_consultation_count
when 'service'
get_service_count
else
0
end
end
def get_consultation_revenue
Appointment.joins(:billing_payments)
.where(clinic_doctor: @clinic_doctor)
.where(created_at: @period_start..@period_end)
.sum('billing_payments.amount')
end
def get_consultation_count
Appointment.where(clinic_doctor: @clinic_doctor)
.where(created_at: @period_start..@period_end)
.where(status: 'completed')
.count
end
def get_service_revenue
ClinicPatientService.joins(:billing_payments)
.where(clinic_id: @clinic_doctor.clinic_id)
.where(created_at: @period_start..@period_end)
.sum('billing_payments.amount')
end
def get_service_count
ClinicPatientService.where(clinic_id: @clinic_doctor.clinic_id)
.where(created_at: @period_start..@period_end)
.count
end
def get_total_revenue
BillingPayment.where(clinic_id: @clinic_doctor.clinic_id)
.where(created_at: @period_start..@period_end)
.sum(:amount)
end
def get_performance_metrics_for_doctor
# This would integrate with existing performance tracking
# For now, return mock data
{
'patient_satisfaction' => 4.5,
'consultation_completion_rate' => 95.0,
'revenue_target_achievement' => 120.0
}
end
def create_calculations
@commission_calculations = []
@calculations.each do |calc|
commission_calculation = CommissionCalculation.create!(
commission_rule: calc[:rule],
clinic_doctor: @clinic_doctor,
base_amount: calc[:base_amount],
commission_amount: calc[:commission_amount],
calculation_details: calc[:calculation_details],
calculation_period: @calculation_period,
status: 'calculated',
calculated_at: Time.current
)
@commission_calculations << commission_calculation
# Log audit trail
AuditTrailService.call(
entity: commission_calculation,
action: 'create',
user: context.current_user,
metadata: { clinic_doctor_id: @clinic_doctor.id, period: @calculation_period }
)
end
context.commission_calculations = @commission_calculations
end
def approval_required?
@commission_calculations.any? { |calc| calc.commission_amount > 1000 } # Example threshold
end
def process_approvals
@commission_calculations.each do |calculation|
ApprovalWorkflowService.call(
entity: calculation,
entity_type: 'commission_calculation',
workflow_config: default_approval_workflow
)
end
end
def default_approval_workflow
{
steps: [
{
step: 1,
approver_roles: ['clinic_owner', 'admin'],
required_approvals: 1
}
]
}
end
def notify_doctors
NotificationService.call(
recipient: @clinic_doctor.doctor.user,
type: 'commission_calculated',
data: {
total_amount: @commission_calculations.sum(&:commission_amount),
calculation_count: @commission_calculations.count,
period: @calculation_period
}
)
end
end4. API Implementation
4.1 Revenue Sharing APIs
Revenue Share Configuration Controller
# parts/billing/controllers/revenue_share_configurations_controller.rb
class RevenueShareConfigurationsController < ApplicationController
before_action :authenticate_user!
before_action :set_clinic
before_action :set_configuration, only: [:show, :update, :destroy, :activate, :deactivate]
# GET /api/clinics/:clinic_id/revenue_share_configurations
def index
@configurations = @clinic.revenue_share_configurations
.includes(:revenue_share_stakeholders, :created_by_user)
.order(created_at: :desc)
render json: {
data: @configurations.map { |config| configuration_json(config) },
meta: pagination_meta(@configurations)
}
end
# GET /api/clinics/:clinic_id/revenue_share_configurations/:id
def show
render json: {
data: configuration_json(@configuration, include_stakeholders: true)
}
end
# POST /api/clinics/:clinic_id/revenue_share_configurations
def create
@configuration = @clinic.revenue_share_configurations.build(configuration_params)
@configuration.created_by_user = current_user
if @configuration.save
create_stakeholders if params[:stakeholders].present?
render json: {
data: configuration_json(@configuration, include_stakeholders: true),
message: 'Revenue sharing configuration created successfully'
}, status: :created
else
render json: {
errors: @configuration.errors.full_messages
}, status: :unprocessable_entity
end
end
# PUT /api/clinics/:clinic_id/revenue_share_configurations/:id
def update
if @configuration.update(configuration_params)
update_stakeholders if params[:stakeholders].present?
render json: {
data: configuration_json(@configuration, include_stakeholders: true),
message: 'Revenue sharing configuration updated successfully'
}
else
render json: {
errors: @configuration.errors.full_messages
}, status: :unprocessable_entity
end
end
# DELETE /api/clinics/:clinic_id/revenue_share_configurations/:id
def destroy
if @configuration.destroy
render json: {
message: 'Revenue sharing configuration deleted successfully'
}
else
render json: {
errors: ['Configuration could not be deleted']
}, status: :unprocessable_entity
end
end
# POST /api/clinics/:clinic_id/revenue_share_configurations/:id/activate
def activate
if @configuration.update(status: 'active')
render json: {
data: configuration_json(@configuration),
message: 'Configuration activated successfully'
}
else
render json: {
errors: @configuration.errors.full_messages
}, status: :unprocessable_entity
end
end
# POST /api/clinics/:clinic_id/revenue_share_configurations/:id/deactivate
def deactivate
if @configuration.update(status: 'inactive')
render json: {
data: configuration_json(@configuration),
message: 'Configuration deactivated successfully'
}
else
render json: {
errors: @configuration.errors.full_messages
}, status: :unprocessable_entity
end
end
private
def set_clinic
@clinic = current_user.clinic_group&.clinics&.find(params[:clinic_id])
unless @clinic
render json: { error: 'Clinic not found or access denied' }, status: :not_found
return
end
end
def set_configuration
@configuration = @clinic.revenue_share_configurations.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'Configuration not found' }, status: :not_found
end
def configuration_params
params.require(:revenue_share_configuration).permit(
:configuration_name,
:description,
:approval_required,
:effective_from,
:effective_until,
:status,
distribution_rules: {}
)
end
def configuration_json(config, include_stakeholders: false)
json = {
id: config.id,
configuration_name: config.configuration_name,
description: config.description,
distribution_rules: config.distribution_rules,
approval_required: config.approval_required,
effective_from: config.effective_from,
effective_until: config.effective_until,
status: config.status,
created_by: {
id: config.created_by_user.id,
name: config.created_by_user.title
},
created_at: config.created_at,
updated_at: config.updated_at
}
if include_stakeholders
json[:stakeholders] = config.revenue_share_stakeholders.active.map do |stakeholder|
{
id: stakeholder.id,
user_id: stakeholder.user_id,
user_name: stakeholder.user.title,
stakeholder_type: stakeholder.stakeholder_type,
share_percentage: stakeholder.share_percentage,
share_amount: stakeholder.share_amount,
minimum_amount: stakeholder.minimum_amount,
maximum_amount: stakeholder.maximum_amount,
payout_method: stakeholder.payout_method,
active: stakeholder.active
}
end
end
json
end
def create_stakeholders
params[:stakeholders].each do |stakeholder_params|
@configuration.revenue_share_stakeholders.create!(
user_id: stakeholder_params[:user_id],
stakeholder_type: stakeholder_params[:stakeholder_type],
share_percentage: stakeholder_params[:share_percentage],
share_amount: stakeholder_params[:share_amount],
minimum_amount: stakeholder_params[:minimum_amount] || 0,
maximum_amount: stakeholder_params[:maximum_amount],
payout_method: stakeholder_params[:payout_method] || 'manual',
conditions: stakeholder_params[:conditions] || {}
)
end
end
def update_stakeholders
# Remove existing stakeholders not in the update
existing_ids = @configuration.revenue_share_stakeholders.pluck(:id)
new_ids = params[:stakeholders].filter_map { |s| s[:id] if s[:id].present? }
ids_to_remove = existing_ids - new_ids
@configuration.revenue_share_stakeholders.where(id: ids_to_remove).destroy_all
# Update or create stakeholders
params[:stakeholders].each do |stakeholder_params|
if stakeholder_params[:id].present?
stakeholder = @configuration.revenue_share_stakeholders.find(stakeholder_params[:id])
stakeholder.update!(stakeholder_update_params(stakeholder_params))
else
@configuration.revenue_share_stakeholders.create!(stakeholder_update_params(stakeholder_params))
end
end
end
def stakeholder_update_params(params)
params.permit(
:user_id,
:stakeholder_type,
:share_percentage,
:share_amount,
:minimum_amount,
:maximum_amount,
:payout_method,
:active,
conditions: {}
)
end
end4.2 Commission APIs
Commission Rules Controller
# parts/billing/controllers/commission_rules_controller.rb
class CommissionRulesController < ApplicationController
before_action :authenticate_user!
before_action :set_clinic
before_action :set_commission_rule, only: [:show, :update, :destroy, :activate, :deactivate]
# GET /api/clinics/:clinic_id/commission_rules
def index
@rules = @clinic.commission_rules
.includes(:created_by_user)
.order(created_at: :desc)
render json: {
data: @rules.map { |rule| commission_rule_json(rule) },
meta: pagination_meta(@rules)
}
end
# GET /api/clinics/:clinic_id/commission_rules/:id
def show
render json: {
data: commission_rule_json(@commission_rule)
}
end
# POST /api/clinics/:clinic_id/commission_rules
def create
@commission_rule = @clinic.commission_rules.build(commission_rule_params)
@commission_rule.created_by_user = current_user
if @commission_rule.save
render json: {
data: commission_rule_json(@commission_rule),
message: 'Commission rule created successfully'
}, status: :created
else
render json: {
errors: @commission_rule.errors.full_messages
}, status: :unprocessable_entity
end
end
# PUT /api/clinics/:clinic_id/commission_rules/:id
def update
if @commission_rule.update(commission_rule_params)
render json: {
data: commission_rule_json(@commission_rule),
message: 'Commission rule updated successfully'
}
else
render json: {
errors: @commission_rule.errors.full_messages
}, status: :unprocessable_entity
end
end
# DELETE /api/clinics/:clinic_id/commission_rules/:id
def destroy
if @commission_rule.destroy
render json: {
message: 'Commission rule deleted successfully'
}
else
render json: {
errors: ['Rule could not be deleted']
}, status: :unprocessable_entity
end
end
# POST /api/clinics/:clinic_id/commission_rules/:id/calculate
def calculate
result = CommissionCalculationEngine.call(
clinic_doctor_id: params[:clinic_doctor_id],
calculation_period: params[:period] || 'monthly',
period_start: params[:period_start],
period_end: params[:period_end],
current_user: current_user
)
if result.success?
render json: {
data: result.commission_calculations.map { |calc| commission_calculation_json(calc) },
message: 'Commission calculated successfully'
}
else
render json: {
errors: [result.message]
}, status: :unprocessable_entity
end
end
private
def set_clinic
@clinic = current_user.clinic_group&.clinics&.find(params[:clinic_id])
unless @clinic
render json: { error: 'Clinic not found or access denied' }, status: :not_found
return
end
end
def set_commission_rule
@commission_rule = @clinic.commission_rules.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'Commission rule not found' }, status: :not_found
end
def commission_rule_params
params.require(:commission_rule).permit(
:rule_name,
:rule_type,
:target_entity,
:effective_from,
:effective_until,
:active,
conditions: {},
calculation_method: {}
)
end
def commission_rule_json(rule)
{
id: rule.id,
rule_name: rule.rule_name,
rule_type: rule.rule_type,
target_entity: rule.target_entity,
conditions: rule.conditions,
calculation_method: rule.calculation_method,
effective_from: rule.effective_from,
effective_until: rule.effective_until,
active: rule.active,
created_by: {
id: rule.created_by_user.id,
name: rule.created_by_user.title
},
created_at: rule.created_at,
updated_at: rule.updated_at
}
end
def commission_calculation_json(calc)
{
id: calc.id,
commission_rule: {
id: calc.commission_rule.id,
name: calc.commission_rule.rule_name
},
doctor: {
id: calc.clinic_doctor.doctor.id,
name: calc.clinic_doctor.doctor.user.title
},
base_amount: calc.base_amount,
commission_amount: calc.commission_amount,
calculation_details: calc.calculation_details,
calculation_period: calc.calculation_period,
status: calc.status,
calculated_at: calc.calculated_at
}
end
end5. User Interface Components
5.1 Dashboard Components
Revenue Sharing Dashboard Workflow
5.2 Configuration Interface Workflow
Commission Rule Setup Workflow
6. Background Jobs & Processing
6.1 Job Classes
Revenue Distribution Job
# parts/billing/jobs/revenue_distribution_job.rb
class RevenueDistributionJob < ApplicationJob
queue_as :financial_processing
def perform(billing_payment_id, options = {})
@billing_payment = BillingPayment.find(billing_payment_id)
@clinic = @billing_payment.clinic
@options = options.with_indifferent_access
Rails.logger.info "Starting revenue distribution for payment #{billing_payment_id}"
result = RevenueShareEngine.call(
clinic_id: @clinic.id,
billing_payment_id: billing_payment_id,
current_user: system_user
)
if result.success?
process_automatic_payouts(result.revenue_shares) if @options[:auto_payout]
send_notifications(result.revenue_shares)
Rails.logger.info "Revenue distribution completed successfully"
else
Rails.logger.error "Revenue distribution failed: #{result.message}"
raise StandardError, result.message
end
end
private
def process_automatic_payouts(revenue_shares)
revenue_shares.each do |revenue_share|
next unless revenue_share.stakeholder.payout_method == 'razorpay'
next unless revenue_share.status == 'approved'
PaymentProcessorService.call(
revenue_share: revenue_share,
payment_method: 'razorpay'
)
end
end
def send_notifications(revenue_shares)
revenue_shares.each do |revenue_share|
NotificationService.call(
recipient: revenue_share.stakeholder.user,
type: 'revenue_share_distributed',
data: {
amount: revenue_share.amount,
clinic_name: @clinic.name,
payment_date: @billing_payment.created_at
}
)
end
end
def system_user
@system_user ||= User.find_by(email: 'system@kulcare.com') || User.first
end
endMonthly Commission Calculation Job
# parts/billing/jobs/monthly_commission_calculation_job.rb
class MonthlyCommissionCalculationJob < ApplicationJob
queue_as :financial_processing
def perform(clinic_id, month = nil, year = nil)
@clinic = Clinic.find(clinic_id)
@month = month || Date.current.last_month.month
@year = year || Date.current.last_month.year
@period_start = Date.new(@year, @month, 1).beginning_of_month
@period_end = @period_start.end_of_month
Rails.logger.info "Starting monthly commission calculation for clinic #{clinic_id}, period: #{@period_start} - #{@period_end}"
calculate_commissions_for_doctors
generate_monthly_reports
send_summary_notifications
Rails.logger.info "Monthly commission calculation completed"
end
private
def calculate_commissions_for_doctors
@clinic.clinic_doctors.active.find_each do |clinic_doctor|
result = CommissionCalculationEngine.call(
clinic_doctor_id: clinic_doctor.id,
calculation_period: 'monthly',
period_start: @period_start,
period_end: @period_end,
current_user: system_user
)
unless result.success?
Rails.logger.error "Commission calculation failed for doctor #{clinic_doctor.id}: #{result.message}"
end
end
end
def generate_monthly_reports
ReportGenerationService.call(
clinic: @clinic,
report_type: 'monthly_commission_summary',
period_start: @period_start,
period_end: @period_end
)
end
def send_summary_notifications
total_commissions = CommissionCalculation
.joins(:clinic_doctor)
.where(clinic_doctors: { clinic_id: @clinic.id })
.where(calculated_at: @period_start..@period_end)
.sum(:commission_amount)
NotificationService.call(
recipient: @clinic.clinic_group.user,
type: 'monthly_commission_summary',
data: {
clinic_name: @clinic.name,
total_amount: total_commissions,
period: "#{Date::MONTHNAMES[@month]} #{@year}",
doctor_count: @clinic.clinic_doctors.active.count
}
)
end
def system_user
@system_user ||= User.find_by(email: 'system@kulcare.com') || User.first
end
end7. Integration Specifications
7.1 Notification System Integration
SMS Notification Adapter
# parts/billing/services/sms_notification_adapter.rb
class SMSNotificationAdapter
def self.send_revenue_share_notification(user, data)
message = build_revenue_share_message(data)
TwilioService.send_sms(
to: user.complete_phone_number,
message: message
)
end
def self.send_commission_notification(user, data)
message = build_commission_message(data)
TwilioService.send_sms(
to: user.complete_phone_number,
message: message
)
end
private
def self.build_revenue_share_message(data)
"Revenue Share Update: You've received ₹#{data[:amount]} from #{data[:clinic_name]}. " \
"Payment processed on #{data[:payment_date].strftime('%d %b %Y')}. - Kulcare"
end
def self.build_commission_message(data)
"Commission Update: Your #{data[:period]} commission of ₹#{data[:total_amount]} has been calculated. " \
"Login to view details. - Kulcare"
end
endEmail Notification Service
# parts/billing/services/email_notification_service.rb
class EmailNotificationService
def self.send_revenue_share_report(user, revenue_shares, period)
RevenueShareMailer.monthly_report(user, revenue_shares, period).deliver_now
end
def self.send_commission_statement(user, commissions, period)
CommissionMailer.monthly_statement(user, commissions, period).deliver_now
end
def self.send_approval_notification(approver, entity)
ApprovalMailer.pending_approval(approver, entity).deliver_now
end
end7.2 External Accounting System Integration
QuickBooks Integration Adapter
# parts/billing/services/quickbooks_integration_adapter.rb
class QuickBooksIntegrationAdapter
def initialize(clinic)
@clinic = clinic
@qb_client = QuickBooksClient.new(@clinic.quickbooks_credentials)
end
def sync_revenue_shares(revenue_shares)
revenue_shares.each do |revenue_share|
create_journal_entry(revenue_share)
end
end
def sync_commission_payments(commission_payments)
commission_payments.each do |payment|
create_expense_entry(payment)
end
end
private
def create_journal_entry(revenue_share)
entry_data = {
TxnDate: revenue_share.distributed_at.strftime('%Y-%m-%d'),
Line: [
{
DetailType: 'JournalEntryLineDetail',
Amount: revenue_share.amount,
JournalEntryLineDetail: {
PostingType: 'Debit',
AccountRef: { value: revenue_sharing_expense_account }
},
Description: "Revenue share to #{revenue_share.stakeholder.user.title}"
},
{
DetailType: 'JournalEntryLineDetail',
Amount: revenue_share.amount,
JournalEntryLineDetail: {
PostingType: 'Credit',
AccountRef: { value: accounts_payable_account }
},
Description: "Revenue share liability"
}
]
}
@qb_client.create(:journal_entry, entry_data)
end
def create_expense_entry(commission_payment)
expense_data = {
TxnDate: commission_payment.processed_at.strftime('%Y-%m-%d'),
AccountRef: { value: commission_expense_account },
Line: [
{
DetailType: 'AccountBasedExpenseLineDetail',
Amount: commission_payment.amount,
AccountBasedExpenseLineDetail: {
AccountRef: { value: commission_expense_account }
},
Description: "Commission payment"
}
],
EntityRef: { value: get_vendor_ref(commission_payment.billing_account.user) }
}
@qb_client.create(:purchase, expense_data)
end
def revenue_sharing_expense_account
@clinic.accounting_configuration['revenue_sharing_expense_account']
end
def accounts_payable_account
@clinic.accounting_configuration['accounts_payable_account']
end
def commission_expense_account
@clinic.accounting_configuration['commission_expense_account']
end
def get_vendor_ref(user)
# Create or find vendor in QuickBooks
vendor_data = {
Name: user.title,
CompanyName: user.clinic&.name || user.title
}
vendor = @qb_client.create(:vendor, vendor_data)
vendor.Id
end
end7.3 Payment Gateway Integration
Razorpay Payout Integration
# parts/billing/services/razorpay_payout_service.rb
class RazorpayPayoutService
def initialize
@client = Razorpay::Client.new(
Rails.application.credentials.razorpay_key_id,
Rails.application.credentials.razorpay_key_secret
)
end
def process_revenue_share_payout(revenue_share)
payout_data = {
account_number: get_account_number(revenue_share.stakeholder.billing_account),
amount: (revenue_share.amount * 100).to_i, # Convert to paise
currency: 'INR',
mode: determine_payout_mode(revenue_share.stakeholder.billing_account),
purpose: 'revenue_share',
reference_id: "RS_#{revenue_share.id}",
narration: "Revenue share payment from #{revenue_share.billing_payment.clinic.name}"
}
response = @client.payout.create(payout_data)
if response['status'] == 'processing'
revenue_share.update!(
status: 'distributed',
distributed_at: Time.current
)
# Log successful payout
Rails.logger.info "Razorpay payout initiated for revenue share #{revenue_share.id}: #{response['id']}"
response
else
raise StandardError, "Payout failed: #{response['error']}"
end
rescue StandardError => e
Rails.logger.error "Razorpay payout failed for revenue share #{revenue_share.id}: #{e.message}"
revenue_share.update!(status: 'failed')
raise e
end
def process_commission_payout(commission_payment)
payout_data = {
account_number: get_account_number(commission_payment.billing_account),
amount: (commission_payment.amount * 100).to_i,
currency: 'INR',
mode: determine_payout_mode(commission_payment.billing_account),
purpose: 'commission',
reference_id: "CP_#{commission_payment.id}",
narration: "Commission payment"
}
response = @client.payout.create(payout_data)
if response['status'] == 'processing'
commission_payment.update!(
payment_status: 'processing',
payment_reference: response['id'],
processed_at: Time.current
)
Rails.logger.info "Razorpay payout initiated for commission payment #{commission_payment.id}: #{response['id']}"
response
else
raise StandardError, "Commission payout failed: #{response['error']}"
end
rescue StandardError => e
Rails.logger.error "Razorpay commission payout failed for commission payment #{commission_payment.id}: #{e.message}"
commission_payment.update!(payment_status: 'failed')
raise e
end
private
def get_account_number(billing_account)
billing_account.razorpay_account_id
end
def determine_payout_mode(billing_account)
# Default to IMPS for faster transfer
'IMPS'
end
end8. Testing Strategy
8.1 Model Tests
# spec/models/revenue_share_configuration_spec.rb
RSpec.describe RevenueShareConfiguration, type: :model do
describe 'associations' do
it { should belong_to(:clinic) }
it { should belong_to(:created_by_user).class_name('User') }
it { should have_many(:revenue_share_stakeholders).dependent(:destroy) }
end
describe 'validations' do
it { should validate_presence_of(:configuration_name) }
it { should validate_presence_of(:effective_from) }
it { should validate_inclusion_of(:status).in_array(%w[draft active inactive]) }
context 'when effective_until is present' do
it 'validates effective_until is after effective_from' do
config = build(:revenue_share_configuration,
effective_from: 1.day.from_now,
effective_until: 1.day.ago)
expect(config).not_to be_valid
expect(config.errors[:effective_until]).to include('must be after effective_from')
end
end
end
describe 'scopes' do
let!(:active_config) { create(:revenue_share_configuration, status: 'active') }
let!(:inactive_config) { create(:revenue_share_configuration, status: 'inactive') }
describe '.active' do
it 'returns only active configurations' do
expect(RevenueShareConfiguration.active).to contain_exactly(active_config)
end
end
describe '.effective_on' do
let!(:current_config) { create(:revenue_share_configuration,
effective_from: 1.month.ago,
effective_until: 1.month.from_now) }
let!(:expired_config) { create(:revenue_share_configuration,
effective_from: 2.months.ago,
effective_until: 1.month.ago) }
it 'returns configurations effective on given date' do
expect(RevenueShareConfiguration.effective_on(Date.current))
.to include(current_config)
.and not_include(expired_config)
end
end
end
end8.2 Service Tests
# spec/services/revenue_share_engine_spec.rb
RSpec.describe RevenueShareEngine, type: :interactor do
let(:clinic) { create(:clinic) }
let(:billing_payment) { create(:billing_payment, amount: 1000.00, clinic: clinic) }
let(:user) { create(:user) }
let!(:configuration) { create(:revenue_share_configuration,
clinic: clinic,
status: 'active',
effective_from: 1.month.ago) }
let!(:stakeholder1) { create(:revenue_share_stakeholder,
configuration: configuration,
share_percentage: 60.0) }
let!(:stakeholder2) { create(:revenue_share_stakeholder,
configuration: configuration,
share_percentage: 40.0) }
describe '.call' do
context 'with valid inputs' do
it 'successfully creates revenue shares' do
result = described_class.call(
clinic_id: clinic.id,
billing_payment_id: billing_payment.id,
current_user: user
)
expect(result).to be_success
expect(result.revenue_shares).to have(2).items
# Check distribution amounts
expect(result.revenue_shares.sum(&:amount)).to eq(1000.00)
# Check individual stakeholder amounts
stakeholder1_share = result.revenue_shares.find { |rs| rs.stakeholder == stakeholder1 }
expect(stakeholder1_share.amount).to eq(600.00)
stakeholder2_share = result.revenue_shares.find { |rs| rs.stakeholder == stakeholder2 }
expect(stakeholder2_share.amount).to eq(400.00)
end
it 'creates audit trail entries' do
expect {
described_class.call(
clinic_id: clinic.id,
billing_payment_id: billing_payment.id,
current_user: user
)
}.to change(AuditTrail, :count).by(2)
end
end
context 'with invalid inputs' do
it 'fails when clinic is not found' do
result = described_class.call(
clinic_id: 999999,
billing_payment_id: billing_payment.id,
current_user: user
)
expect(result).to be_failure
expect(result.message).to eq('Clinic not found')
end
it 'fails when no active configurations exist' do
configuration.update!(status: 'inactive')
result = described_class.call(
clinic_id: clinic.id,
billing_payment_id: billing_payment.id,
current_user: user
)
expect(result).to be_failure
expect(result.message).to eq('No active revenue sharing configurations found')
end
end
end
end8.3 Controller Tests
# spec/controllers/revenue_share_configurations_controller_spec.rb
RSpec.describe RevenueShareConfigurationsController, type: :controller do
let(:user) { create(:user_with_clinic) }
let(:clinic) { user.clinic }
before { sign_in user }
describe 'GET #index' do
let!(:configurations) { create_list(:revenue_share_configuration, 3, clinic: clinic) }
it 'returns all configurations for the clinic' do
get :index, params: { clinic_id: clinic.id }
expect(response).to have_http_status(:ok)
json_response = JSON.parse(response.body)
expect(json_response['data']).to have(3).items
end
end
describe 'POST #create' do
let(:valid_attributes) do
{
configuration_name: 'Test Configuration',
description: 'Test description',
distribution_rules: { rule_type: 'percentage' },
effective_from: 1.day.from_now,
status: 'draft',
stakeholders: [
{
user_id: user.id,
stakeholder_type: 'clinic_owner',
share_percentage: 100.0,
payout_method: 'manual'
}
]
}
end
context 'with valid parameters' do
it 'creates a new configuration' do
expect {
post :create, params: {
clinic_id: clinic.id,
revenue_share_configuration: valid_attributes,
stakeholders: valid_attributes[:stakeholders]
}
}.to change(RevenueShareConfiguration, :count).by(1)
expect(response).to have_http_status(:created)
json_response = JSON.parse(response.body)
expect(json_response['message']).to eq('Revenue sharing configuration created successfully')
end
it 'creates associated stakeholders' do
expect {
post :create, params: {
clinic_id: clinic.id,
revenue_share_configuration: valid_attributes,
stakeholders: valid_attributes[:stakeholders]
}
}.to change(RevenueShareStakeholder, :count).by(1)
end
end
context 'with invalid parameters' do
it 'returns validation errors' do
post :create, params: {
clinic_id: clinic.id,
revenue_share_configuration: { configuration_name: '' }
}
expect(response).to have_http_status(:unprocessable_entity)
json_response = JSON.parse(response.body)
expect(json_response['errors']).to be_present
end
end
end
describe 'PUT #update' do
let(:configuration) { create(:revenue_share_configuration, clinic: clinic) }
let(:update_attributes) { { configuration_name: 'Updated Name' } }
it 'updates the configuration' do
put :update, params: {
clinic_id: clinic.id,
id: configuration.id,
revenue_share_configuration: update_attributes
}
expect(response).to have_http_status(:ok)
configuration.reload
expect(configuration.configuration_name).to eq('Updated Name')
end
end
describe 'DELETE #destroy' do
let!(:configuration) { create(:revenue_share_configuration, clinic: clinic) }
it 'deletes the configuration' do
expect {
delete :destroy, params: { clinic_id: clinic.id, id: configuration.id }
}.to change(RevenueShareConfiguration, :count).by(-1)
expect(response).to have_http_status(:ok)
end
end
end8.4 Financial Accuracy Tests
# spec/financial_accuracy/commission_calculation_accuracy_spec.rb
RSpec.describe 'Commission Calculation Accuracy', type: :feature do
describe 'percentage-based commission calculations' do
let(:clinic) { create(:clinic) }
let(:doctor) { create(:clinic_doctor, clinic: clinic) }
let(:commission_rule) { create(:commission_rule,
clinic: clinic,
rule_type: 'percentage',
calculation_method: { 'percentage' => 15.0 }) }
it 'calculates commission accurately for various amounts' do
test_cases = [
{ base_amount: 1000.00, expected_commission: 150.00 },
{ base_amount: 2500.50, expected_commission: 375.08 },
{ base_amount: 10000.00, expected_commission: 1500.00 },
{ base_amount: 999.99, expected_commission: 150.00 }
]
test_cases.each do |test_case|
# Create mock data for the test case
create_billing_data(doctor, test_case[:base_amount])
result = CommissionCalculationEngine.call(
clinic_doctor_id: doctor.id,
calculation_period: 'monthly'
)
expect(result).to be_success
calculated_commission = result.commission_calculations.first.commission_amount
expect(calculated_commission).to eq(test_case[:expected_commission])
# Clean up for next test case
cleanup_test_data
end
end
end
describe 'tiered commission calculations' do
let(:clinic) { create(:clinic) }
let(:doctor) { create(:clinic_doctor, clinic: clinic) }
let(:tiered_rule) { create(:commission_rule,
clinic: clinic,
rule_type: 'tiered',
calculation_method: {
'tiers' => [
{ 'min_amount' => 0, 'max_amount' => 5000, 'percentage' => 10.0 },
{ 'min_amount' => 5000, 'max_amount' => 10000, 'percentage' => 15.0 },
{ 'min_amount' => 10000, 'percentage' => 20.0 }
]
}) }
it 'calculates tiered commissions accurately' do
test_cases = [
{ base_amount: 3000.00, expected_commission: 300.00 }, # First tier only
{ base_amount: 7000.00, expected_commission: 800.00 }, # First + second tier
{ base_amount: 15000.00, expected_commission: 1750.00 } # All tiers
]
test_cases.each do |test_case|
create_billing_data(doctor, test_case[:base_amount])
result = CommissionCalculationEngine.call(
clinic_doctor_id: doctor.id,
calculation_period: 'monthly'
)
calculated_commission = result.commission_calculations.first.commission_amount
expect(calculated_commission).to eq(test_case[:expected_commission])
cleanup_test_data
end
end
end
private
def create_billing_data(doctor, amount)
billing_payment = create(:billing_payment, amount: amount, clinic: doctor.clinic)
create(:appointment, clinic_doctor: doctor, billing_payment: billing_payment)
end
def cleanup_test_data
BillingPayment.destroy_all
Appointment.destroy_all
CommissionCalculation.destroy_all
end
end9. Deployment & Operations
9.1 Production Configuration
Environment Variables
# Revenue Sharing & Commission System Configuration
REVENUE_SHARING_ENABLED=true
COMMISSION_CALCULATION_ENABLED=true
AUTO_APPROVAL_THRESHOLD=1000.00
RAZORPAY_PAYOUT_ENABLED=true
NOTIFICATION_SYSTEM_ENABLED=true
# Audit & Compliance
AUDIT_TRAIL_RETENTION_DAYS=2555 # 7 years for financial records
COMPLIANCE_REPORTING_ENABLED=true
TAX_REPORTING_ENABLED=true
# Performance Settings
COMMISSION_CALCULATION_BATCH_SIZE=100
REVENUE_DISTRIBUTION_QUEUE_PRIORITY=high
BACKGROUND_JOB_RETRY_LIMIT=3
# Integration Settings
QUICKBOOKS_INTEGRATION_ENABLED=false
TALLY_INTEGRATION_ENABLED=false
SMS_NOTIFICATIONS_ENABLED=true
EMAIL_NOTIFICATIONS_ENABLED=trueCron Jobs Configuration
# config/schedule.rb
every 1.day, at: '2:00 AM' do
runner "RevenueDistributionJob.perform_later"
end
every 1.month, at: '3:00 AM' do
runner "MonthlyCommissionCalculationJob.perform_later"
end
every 1.week, at: '4:00 AM' do
runner "CommissionReportGenerationJob.perform_later"
end
every 1.day, at: '5:00 AM' do
runner "AuditTrailComplianceCheckJob.perform_later"
end
# Cleanup old audit trails (keep 7 years for compliance)
every 1.month, at: '6:00 AM' do
runner "AuditTrailCleanupJob.perform_later"
end9.2 Monitoring & Alerts
Performance Monitoring
# parts/billing/services/performance_monitor.rb
class PerformanceMonitor
def self.track_commission_calculation_time(clinic_doctor_id)
start_time = Time.current
yield if block_given?
execution_time = Time.current - start_time
if execution_time > 30.seconds
Rails.logger.warn "Slow commission calculation detected for doctor #{clinic_doctor_id}: #{execution_time}s"
# Send alert to operations team
SlackNotifier.notify_slow_calculation(clinic_doctor_id, execution_time)
end
# Track metrics
StatsD.timing('commission_calculation.duration', execution_time * 1000)
StatsD.increment('commission_calculation.count')
end
def self.track_revenue_distribution_success_rate
successful = RevenueShare.where(created_at: 1.hour.ago..Time.current)
.where(status: ['approved', 'distributed']).count
failed = RevenueShare.where(created_at: 1.hour.ago..Time.current)
.where(status: 'failed').count
total = successful + failed
return if total == 0
success_rate = (successful.to_f / total * 100).round(2)
if success_rate < 95.0
Rails.logger.error "Low revenue distribution success rate: #{success_rate}%"
AlertManager.send_critical_alert(
"Revenue Distribution Success Rate Alert",
"Success rate dropped to #{success_rate}% in the last hour"
)
end
StatsD.gauge('revenue_distribution.success_rate', success_rate)
end
endThis implementation plan provides a comprehensive, production-ready solution that aligns with the PRD requirements and follows the current project's architecture patterns. All stakeholders are properly linked to users with appropriate roles, clinic_id is used consistently, and the system includes robust error handling, testing, and monitoring capabilities.