Kulcare Docs
DeveloperAuthentication Documentation04 features

Subscription-Based Email Communication

This document outlines automated emails sent to users with active subscriptions in the Kulcare platform.

Overview

Only users with active subscriptions receive automated engagement emails. This ensures we only send value-added content to paying customers.

Active Subscription Criteria

An active subscription is defined as:

  • Subscription status: active, authenticated, or to_be_cancelled
  • current_end date is in the future (>= current time)
  • Timezone-aware validation using system default timezone

Monthly Emails

Monthly Review Summary

Frequency: Monthly Recipients: Clinic owners/admins with active subscriptions Rake Task: backend/lib/tasks/cron_monthly_monthly_review_summary.rake Mailer: ClinicReviewMailer.monthly_review_summary

Purpose

Provides a comprehensive monthly summary of clinic performance:

  • Patient reviews and ratings
  • Review trends and insights
  • Response rate to patient feedback
  • Overall satisfaction metrics

Eligibility Criteria

  • ✅ Clinic has an active subscription
  • ✅ Clinic is linked to a user with billing customer record

Technical Implementation

# Query filters clinics with active subscriptions
current_time = Time.now.in_time_zone(Settings['default_timezone']).utc
active_status_ids = BasicBillingSubscriptionStatus.active_ids

# Skip if no active statuses configured
if active_status_ids.blank?
  Rails.logger.warn('No active subscription statuses; skipping monthly review summary task')
  next
end

Clinic.joins("INNER JOIN user_accounts ON user_accounts.profile_id = clinics.id
             AND user_accounts.account_id = #{Account::CLINIC_ID}")
      .joins('INNER JOIN users ON users.id = user_accounts.user_id')
      .joins('INNER JOIN billing_customers ON billing_customers.user_id = users.id')
      .joins('INNER JOIN billing_subscriptions ON billing_subscriptions.billing_customer_id = billing_customers.id')
      .where('billing_subscriptions.current_end >= ?', current_time)
      .where(billing_subscriptions: { basic_billing_subscription_status_id: active_status_ids })
      .distinct
      .find_in_batches(batch_size: 100) do |batch|
  batch.each do |clinic|
    ClinicReviewMailer.monthly_review_summary(clinic).deliver_now
    Rails.logger.info "Clinic id: #{clinic.id}"
  end
  GC.start
end

Delivery Method

  • Sent via: deliver_now (synchronous)
  • Batch size: 100 clinics per batch
  • Memory management: GC after each batch

Cron Schedule

# Runs on the 1st day of every month at 9:00 AM
0 9 1 * * bundle exec rake db:monthly_review_summary

Sample Output

2025-01-16 10:00:00 +0530
Rake Task Started....
------------------------------------------------------
Active subscription status IDs: 1, 2, 3
Clinic id: 12
Clinic id: 13
Sent to 15 clinics with active subscriptions
------------------------------------------------------
Rake Task Ended...
======================================================

Weekly Emails

Weekly Clinic Statistics

Frequency: Weekly Recipients: Non-private clinic owners with active subscriptions Rake Task: backend/lib/tasks/cron_weekly_clinic_stats_email.rake Mailer: ClinicMailer.weekly_stats

Purpose

Provides weekly operational statistics and insights:

  • Appointment metrics (total, completed, cancelled, no-shows)
  • Patient visit trends
  • Revenue summary
  • Week-over-week comparisons
  • Key performance indicators (KPIs)

Eligibility Criteria

  • ✅ Clinic has an active subscription
  • ✅ Clinic is NOT a private clinic (private_clinic: false)
  • ✅ Feature flag SEND_WEEKLY_CLINIC_EMAIL is enabled for the clinic
  • ✅ Stats have been calculated and stored in clinic_weekly_stats table

Two-Phase Process

Phase 1: Calculate Statistics

# Calculate stats for all eligible clinics
current_time = Time.now.in_time_zone(Settings['default_timezone']).utc
active_status_ids = BasicBillingSubscriptionStatus.active_ids

# Skip if no active statuses configured
if active_status_ids.blank?
  Rails.logger.warn('No active subscription statuses; skipping weekly stats email task')
  next
end

Clinic.where(private_clinic: false)
      .joins("INNER JOIN user_accounts ON user_accounts.profile_id = clinics.id
             AND user_accounts.account_id = #{Account::CLINIC_ID}")
      .joins('INNER JOIN users ON users.id = user_accounts.user_id')
      .joins('INNER JOIN billing_customers ON billing_customers.user_id = users.id')
      .joins('INNER JOIN billing_subscriptions ON billing_subscriptions.billing_customer_id = billing_customers.id')
      .where('billing_subscriptions.current_end >= ?', current_time)
      .where(billing_subscriptions: { basic_billing_subscription_status_id: active_status_ids })
      .distinct
      .find_in_batches(batch_size: 100) do |batch|
  batch.each do |clinic|
    # Do not send email if flag is turned off
    next if clinic&.feature_enabled?(FeatureFlag::SEND_WEEKLY_CLINIC_EMAIL) == false

    # Calculate stats for current week and store in clinic_weekly_stats
    CalculateClinicWeeklyStats.perform_now(clinic.id)
    Rails.logger.info "Clinic id: #{clinic.id}"
  end
  GC.start
end

Phase 2: Send Emails

# Send out emails for unprocessed stat entries
ClinicWeeklyStat.not_sent.find_in_batches(batch_size: 100) do |batch|
  batch.each do |clinic_weekly_stat|
    ClinicMailer.weekly_stats(clinic_weekly_stat).deliver_later
    clinic_weekly_stat.update!(status: 'SENT')
    Rails.logger.info "Clinic Weekly Stat id: #{clinic_weekly_stat.id}"
  end
  GC.start
end

Technical Implementation

  • Respects feature flag: FeatureFlag::SEND_WEEKLY_CLINIC_EMAIL
  • Stats are persisted in clinic_weekly_stats table
  • Delivery method: deliver_later (asynchronous via Sidekiq)
  • Batch size: 100 records per batch

Cron Schedule

# Runs every Monday at 10:00 AM
0 10 * * 1 bundle exec rake db:weekly_clinic_stats_email

Sample Output

2025-01-13 10:00:00 +0530
Rake Task Started....
------------------------------------------------------
Collecting Stats
------------------------------------------------------
Clinic id: 12
Clinic id: 13
------------------------------------------------------
Sending Emails
------------------------------------------------------
Clinic Weekly Stat id: 456
Clinic Weekly Stat id: 457
------------------------------------------------------
Rake Task Ended...
======================================================

Weekly News Articles

Frequency: Weekly Recipients: Doctors with active clinic subscriptions Rake Task: backend/lib/tasks/cron_weekly_notify_published_news.rake Mailer: DoctorMailer.news_publish

Purpose

Sends the latest published healthcare news articles to doctors:

  • Latest 3-6 curated news articles
  • Healthcare industry updates
  • Medical research highlights
  • Professional development content

Eligibility Criteria

  • ✅ Doctor is in the mailing list (MailingListItem.news_list)
  • ✅ Doctor's clinic has an active subscription
  • ✅ Doctor is associated with at least one clinic through ClinicDoctor

Technical Implementation

# Get current time with timezone for subscription check
current_time = Time.now.in_time_zone(Settings['default_timezone']).utc
active_status_ids = BasicBillingSubscriptionStatus.active_ids

# Skip if no active statuses configured
if active_status_ids.blank?
  Rails.logger.warn('No active subscription statuses; skipping published news task')
  return
end

# Get doctors with active subscriptions through clinic associations
doctors_with_active_subscriptions = ClinicDoctor.joins(:clinic)
  .joins("INNER JOIN user_accounts ON user_accounts.profile_id = clinics.id
          AND user_accounts.account_id = #{Account::CLINIC_ID}")
  .joins('INNER JOIN users ON users.id = user_accounts.user_id')
  .joins('INNER JOIN billing_customers ON billing_customers.user_id = users.id')
  .joins('INNER JOIN billing_subscriptions ON billing_subscriptions.billing_customer_id = billing_customers.id')
  .where('billing_subscriptions.current_end >= ?', current_time)
  .where(billing_subscriptions: { basic_billing_subscription_status_id: active_status_ids })
  .distinct
  .pluck(:doctor_id)

Rails.logger.info "Found #{doctors_with_active_subscriptions.count} doctors with active subscriptions"

# Filter mailing list to only include doctors with active subscriptions
news_list = MailingListItem.news_list.where(doctor_id: doctors_with_active_subscriptions)
news_list&.find_in_batches(batch_size: batch_size) do |doctors_list|
  doctors_list.each do |doc_details|
    doc = doc_details.doctor
    doctor_name = doc&.short_name || doc_details.doctor_name
    is_signed_up = doc.present?
    DoctorMailer.news_publish(doc_details.email, doctor_name, article_obj, is_signed_up).deliver_later
  end
  GC.start
end

News Article Source

Articles are selected from two sources:

  1. Weekly News Article List (WeeklyNewsArticleList.unpublished.first)

    • If available, uses curated articles from the weekly list
    • Marks list as published after sending
  2. Latest Published Articles (fallback)

    • If no weekly list exists, fetches 6 most recent published articles
    • Orders by latest publication date

Delivery Method

  • Sent via: deliver_later (asynchronous via Sidekiq)
  • Batch size: Configurable via Settings['search_indexes']['batch_size']
  • Memory management: GC after each batch

Cron Schedule

# Runs every week (exact day/time to be configured)
# Example: Every Friday at 9:00 AM
0 9 * * 5 bundle exec rake db:notify_published_news

Sample Output

2025-01-17 09:00:00 +0530
Rake Task Started....
------------------------------------------------------
Found 45 doctors with active subscriptions
------------------------------------------------------
Rake Task Ended...
2025-01-17 09:05:00 +0530
======================================================

Doctor Relationship Flow

Doctor → ClinicDoctor → Clinic → UserAccount → User → BillingCustomer → BillingSubscription

Note: Doctors can be associated with multiple clinics. If any of their associated clinics has an active subscription, they will receive the news email.


Database Relationships

Subscription Check Flow

Clinic → UserAccount (profile_id) → User → BillingCustomer → BillingSubscription

Key Models

BillingSubscription

belongs_to :billing_customer
belongs_to :basic_billing_subscription_status

scope :not_expired, -> { where('current_end >= ?', Time.current) }

BillingCustomer

belongs_to :user
has_many :billing_subscriptions

UserAccount

belongs_to :user
belongs_to :clinic, foreign_key: 'profile_id'
# Where account_id = Account::CLINIC_ID

Run Tasks Manually

# Monthly review summary
bundle exec rake db:monthly_review_summary

# Weekly clinic stats
bundle exec rake db:weekly_clinic_stats_email

# Weekly news articles
bundle exec rake db:notify_published_news

On this page