Kulcare Docs
GeneralStandards

Coding Standards & Style Guide

This document outlines the coding standards, conventions, and best practices for the Kulcare API Backend codebase.

Ruby Style Guide

We follow the community Ruby Style Guide with some project-specific modifications.

Code Formatting

Indentation and Spacing

# Use 2 spaces for indentation (no tabs)
class User < ApplicationRecord
  has_many :clinics
  
  def full_name
    "#{first_name} #{last_name}"
  end
end

# Line length: 100 characters max
def long_method_name(parameter_one, parameter_two, parameter_three)
  # Break long lines logically
  some_very_long_method_call(
    parameter_one,
    parameter_two,
    parameter_three
  )
end

# Align parameters when breaking lines
User.where(
  country_code: '91',
  verified: true,
  created_at: 1.week.ago..Time.current
)

String Literals

# Use single quotes for simple strings
name = 'John Doe'

# Use double quotes when interpolation or escape sequences needed
message = "Hello #{name}!"
error_message = "Error: \n#{error.message}"

# Use %w for word arrays
VALID_STATUSES = %w[pending confirmed completed cancelled].freeze

# Use heredoc for multi-line strings
email_template = <<~HTML
  <html>
    <body>
      <h1>Welcome to Kulcare!</h1>
    </body>
  </html>
HTML

Naming Conventions

Variables and Methods

# Use snake_case for variables and methods
user_count = 10
appointment_date = Date.today

def calculate_consultation_fee
  base_fee + additional_charges
end

# Use descriptive names
# Bad
def calc(u, d)
  u.appts.where(date: d).count
end

# Good
def count_appointments_for_date(user, date)
  user.appointments.where(appointment_date: date).count
end

# Boolean methods end with ?
def verified?
  verification_status == 'verified'
end

# Dangerous methods (destructive) end with !
def verify_user!
  update!(verified: true)
  send_welcome_email
end

Constants and Classes

# Use SCREAMING_SNAKE_CASE for constants
MAX_APPOINTMENT_DURATION = 60.minutes
DEFAULT_COUNTRY_CODE = '91'

# Use PascalCase for classes and modules
class AppointmentScheduler
  module TimeSlotCalculator
  end
end

# Use descriptive class names
class CreateAppointment < BaseInteractor  # Good
class CA < BI                            # Bad

Method Definitions

# Prefer explicit return for clarity in complex methods
def calculate_total_fee
  base_fee = consultation_fee
  additional_fee = calculate_additional_charges
  
  return 0 if patient.has_free_consultation?
  
  base_fee + additional_fee
end

# Use keyword arguments for methods with multiple parameters
def create_appointment(patient:, doctor:, date:, time:, notes: nil)
  Appointment.create!(
    patient: patient,
    doctor: doctor,
    appointment_date: date,
    appointment_time: time,
    notes: notes
  )
end

# Use default parameter values when appropriate
def send_notification(message, delivery_method: :sms, urgent: false)
  # implementation
end

Exception Handling

# Use specific exception classes
class InvalidAppointmentTime < StandardError; end
class PatientNotFound < StandardError; end

# Handle exceptions gracefully
def book_appointment(appointment_params)
  validate_appointment_time(appointment_params[:time])
  create_appointment(appointment_params)
rescue InvalidAppointmentTime => e
  Rails.logger.warn "Invalid appointment time: #{e.message}"
  { success: false, error: e.message }
rescue StandardError => e
  Rails.logger.error "Unexpected error: #{e.message}"
  { success: false, error: 'Something went wrong' }
end

# Use ensure for cleanup
def process_medical_document
  temp_file = create_temp_file
  process_file(temp_file)
ensure
  temp_file&.close
  temp_file&.unlink
end

Rails Conventions

Models

ActiveRecord Models

class Appointment < ApplicationRecord
  # Constants first
  VALID_STATUSES = %w[scheduled confirmed completed cancelled].freeze
  
  # Associations
  belongs_to :clinic
  belongs_to :patient
  belongs_to :doctor
  
  # Validations
  validates :appointment_date, presence: true
  validates :appointment_time, presence: true
  validates :status, inclusion: { in: VALID_STATUSES }
  
  # Scopes
  scope :today, -> { where(appointment_date: Date.current) }
  scope :upcoming, -> { where('appointment_date >= ?', Date.current) }
  scope :by_status, ->(status) { where(status: status) }
  
  # Callbacks (in order of execution)
  before_validation :set_default_status
  before_create :generate_confirmation_code
  after_create :send_confirmation
  after_update :handle_status_change
  
  # Class methods
  def self.available_slots(doctor, date)
    # implementation
  end
  
  # Instance methods
  def can_be_cancelled?
    scheduled? && appointment_date > Date.current
  end
  
  def display_time
    appointment_time.strftime('%I:%M %p')
  end
  
  private
  
  # Private methods last
  def set_default_status
    self.status ||= 'scheduled'
  end
end

Model Validations

class User < ApplicationRecord
  # Use appropriate validation types
  validates :phone_number, 
    presence: true,
    uniqueness: { scope: :country_code },
    format: { with: /\A\d{10,15}\z/ }
  
  validates :email, 
    format: { with: URI::MailTo::EMAIL_REGEXP },
    allow_blank: true
  
  validates :password, 
    length: { minimum: 8 },
    format: { 
      with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+\z/,
      message: 'must include uppercase, lowercase, and digit'
    },
    if: :password_required?
  
  # Custom validations
  validate :phone_number_format
  validate :future_birth_date, if: :date_of_birth?
  
  private
  
  def phone_number_format
    return unless phone_number.present?
    
    unless phone_number.match?(/\A\d{10,15}\z/)
      errors.add(:phone_number, 'must be 10-15 digits')
    end
  end
  
  def future_birth_date
    return unless date_of_birth.present?
    
    if date_of_birth > Date.current
      errors.add(:date_of_birth, 'cannot be in the future')
    end
  end
end

Controllers

Parts-Based Controllers

# parts/clinic/controllers/appointments_controller.rb
class AppointmentsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_appointment, only: [:show, :update, :destroy]
  before_action :authorize_appointment, only: [:show, :update, :destroy]
  
  # Use consistent action structure
  def index
    result = FetchAppointments.call(
      clinic: current_clinic,
      params: filter_params
    )
    
    if result.success?
      render json: result.appointments,
             each_serializer: AppointmentSerializer,
             meta: pagination_meta(result.appointments)
    else
      render_error(result.error)
    end
  end
  
  def show
    render json: @appointment, serializer: AppointmentDetailSerializer
  end
  
  def create
    result = CreateAppointment.call(
      appointment_params.merge(
        clinic: current_clinic,
        current_user: current_user
      )
    )
    
    if result.success?
      render json: result.appointment,
             serializer: AppointmentSerializer,
             status: :created
    else
      render_error(result.error, :unprocessable_entity)
    end
  end
  
  def update
    result = UpdateAppointment.call(
      appointment: @appointment,
      params: appointment_params,
      current_user: current_user
    )
    
    if result.success?
      render json: result.appointment, serializer: AppointmentSerializer
    else
      render_error(result.error, :unprocessable_entity)
    end
  end
  
  def destroy
    result = CancelAppointment.call(
      appointment: @appointment,
      current_user: current_user
    )
    
    if result.success?
      head :no_content
    else
      render_error(result.error)
    end
  end
  
  private
  
  def set_appointment
    @appointment = current_clinic.appointments.find(params[:id])
  rescue ActiveRecord::RecordNotFound
    render_error('Appointment not found', :not_found)
  end
  
  def authorize_appointment
    authorize @appointment
  end
  
  def appointment_params
    params.require(:appointment).permit(
      :patient_id, :doctor_id, :appointment_date, :appointment_time,
      :duration, :notes, :status
    )
  end
  
  def filter_params
    params.permit(:date, :status, :doctor_id, :page, :per_page)
  end
end

Interactors

Interactor Structure

# parts/clinic/interactors/create_appointment.rb
class CreateAppointment < BaseInteractor
  # Define expected context early
  delegate :patient_id, :doctor_id, :appointment_date, :clinic, :current_user,
           to: :context
  
  def call
    validate_inputs
    check_availability
    create_appointment
    send_notifications
    log_activity
  rescue StandardError => e
    Rails.logger.error "CreateAppointment failed: #{e.message}"
    context.fail!(error: e.message)
  end
  
  private
  
  # Keep methods focused and single-purpose
  def validate_inputs
    validate_required_fields
    validate_appointment_date
    validate_appointment_time
  end
  
  def validate_required_fields
    context.fail!(error: 'Patient is required') unless patient_id.present?
    context.fail!(error: 'Doctor is required') unless doctor_id.present?
    context.fail!(error: 'Date is required') unless appointment_date.present?
  end
  
  def validate_appointment_date
    if appointment_date < Date.current
      context.fail!(error: 'Appointment date cannot be in the past')
    end
  end
  
  def check_availability
    return unless doctor_busy?
    context.fail!(error: 'Doctor is not available at this time')
  end
  
  def doctor_busy?
    clinic.appointments
          .where(doctor_id: doctor_id)
          .where(appointment_date: appointment_date)
          .where(appointment_time: context.appointment_time)
          .exists?
  end
  
  def create_appointment
    context.appointment = Appointment.create!(
      clinic: clinic,
      patient_id: patient_id,
      doctor_id: doctor_id,
      appointment_date: appointment_date,
      appointment_time: context.appointment_time,
      status: 'scheduled',
      created_by: current_user
    )
  end
  
  def send_notifications
    AppointmentConfirmationJob.perform_later(context.appointment.id)
  end
  
  def log_activity
    Rails.logger.info "Appointment created: #{context.appointment.id}"
  end
end

Services

Service Object Structure

# parts/clinic/services/appointment_scheduler.rb
class AppointmentScheduler
  include ActiveModel::Validations
  
  attr_reader :clinic, :doctor, :date
  
  validates :clinic, :doctor, :date, presence: true
  validate :date_not_in_past
  
  def initialize(clinic:, doctor:, date:)
    @clinic = clinic
    @doctor = doctor
    @date = date
  end
  
  def available_slots
    return [] unless valid?
    
    @available_slots ||= calculate_available_slots
  end
  
  def next_available_slot
    available_slots.first
  end
  
  def book_slot(time_slot, patient)
    validate_slot_availability(time_slot)
    
    Appointment.create!(
      clinic: clinic,
      doctor: doctor,
      patient: patient,
      appointment_date: date,
      appointment_time: time_slot
    )
  end
  
  private
  
  def calculate_available_slots
    all_possible_slots - booked_slots
  end
  
  def all_possible_slots
    schedule = doctor.schedule_for_date(date)
    return [] unless schedule
    
    slots = []
    current_time = schedule.start_time
    
    while current_time < schedule.end_time
      slots << current_time
      current_time += schedule.slot_duration.minutes
    end
    
    slots
  end
  
  def booked_slots
    clinic.appointments
          .where(doctor: doctor, appointment_date: date)
          .pluck(:appointment_time)
  end
  
  def validate_slot_availability(time_slot)
    unless available_slots.include?(time_slot)
      raise ArgumentError, 'Time slot not available'
    end
  end
  
  def date_not_in_past
    return unless date.present?
    
    if date < Date.current
      errors.add(:date, 'cannot be in the past')
    end
  end
end

Database Conventions

Migrations

# Use descriptive migration names
# Good: 20240115120000_add_verification_status_to_users.rb
# Bad: 20240115120000_update_users.rb

class AddVerificationStatusToUsers < ActiveRecord::Migration[7.1]
  def change
    add_column :users, :verification_status, :string, default: 'pending'
    add_column :users, :verified_at, :timestamp
    
    add_index :users, :verification_status
  end
end

# Use reversible migrations when needed
class UpdateExistingUserStatuses < ActiveRecord::Migration[7.1]
  def up
    User.where(verified: true).update_all(
      verification_status: 'verified',
      verified_at: Time.current
    )
  end
  
  def down
    User.update_all(
      verification_status: 'pending',
      verified_at: nil
    )
  end
end

# Use safe migrations in production
class AddIndexConcurrently < ActiveRecord::Migration[7.1]
  disable_ddl_transaction!
  
  def change
    add_index :appointments, :clinic_id, algorithm: :concurrently
  end
end

Database Queries

# Use includes to prevent N+1 queries
appointments = clinic.appointments
                     .includes(:patient, :doctor)
                     .where(appointment_date: Date.current)

# Use specific selects when you don't need all columns
patient_names = clinic.patients
                      .select(:id, :first_name, :last_name)
                      .active

# Use scopes for common queries
class Appointment < ApplicationRecord
  scope :today, -> { where(appointment_date: Date.current) }
  scope :upcoming, -> { where('appointment_date >= ?', Date.current) }
  scope :for_doctor, ->(doctor) { where(doctor: doctor) }
end

# Chain scopes for readable queries
appointments = Appointment.today.for_doctor(doctor).upcoming

Performance Best Practices

Query Optimization

# Use counter caches for expensive counts
class Clinic < ApplicationRecord
  has_many :appointments, counter_cache: true
  has_many :patients, counter_cache: true
end

# Use exists? instead of present? for existence checks
# Bad
if user.appointments.any?
  # do something
end

# Good
if user.appointments.exists?
  # do something
end

# Use pluck for single column selections
patient_ids = clinic.patients.active.pluck(:id)

# Use find_each for batch processing
Patient.find_each(batch_size: 1000) do |patient|
  # Process each patient
end

Memory Management

# Use lazy enumeration for large datasets
def export_appointments
  appointments = Appointment.includes(:patient, :doctor)
  
  CSV.generate do |csv|
    appointments.find_each do |appointment|
      csv << [appointment.id, appointment.patient.name, appointment.doctor.name]
    end
  end
end

# Use delegate to avoid unnecessary object creation
class AppointmentPresenter
  delegate :id, :appointment_date, :status, to: :appointment
  
  def initialize(appointment)
    @appointment = appointment
  end
  
  private
  
  attr_reader :appointment
end

Security Best Practices

Parameter Sanitization

# Always use strong parameters
def appointment_params
  params.require(:appointment).permit(
    :patient_id, :doctor_id, :appointment_date, :appointment_time,
    :notes, :duration
  )
end

# Validate and sanitize inputs
class AppointmentForm
  include ActiveModel::Model
  
  attr_accessor :patient_id, :doctor_id, :appointment_date
  
  validates :patient_id, presence: true, numericality: { greater_than: 0 }
  validates :appointment_date, presence: true
  
  validate :appointment_date_format
  
  private
  
  def appointment_date_format
    return unless appointment_date.present?
    
    Date.parse(appointment_date.to_s)
  rescue ArgumentError
    errors.add(:appointment_date, 'is invalid')
  end
end

SQL Injection Prevention

# Use parameterized queries
# Bad
User.where("name = '#{params[:name]}'")

# Good
User.where(name: params[:name])
User.where('created_at > ?', 1.week.ago)

# Use Arel for complex queries
users = User.arel_table
query = users[:name].matches("%#{search_term}%")
  .and(users[:verified].eq(true))
User.where(query)

Testing Standards

Test Organization

# spec/parts/clinic/models/appointment_spec.rb
RSpec.describe Appointment, type: :model do
  # Use descriptive describe blocks
  describe 'associations' do
    it { should belong_to(:clinic) }
    it { should belong_to(:patient) }
    it { should belong_to(:doctor) }
  end
  
  describe 'validations' do
    subject { build(:appointment) }
    
    it { should validate_presence_of(:appointment_date) }
    it { should validate_inclusion_of(:status).in_array(described_class::VALID_STATUSES) }
  end
  
  describe '#can_be_cancelled?' do
    context 'when appointment is scheduled' do
      let(:appointment) { build(:appointment, status: 'scheduled') }
      
      context 'when appointment is in the future' do
        before { appointment.appointment_date = Date.tomorrow }
        
        it 'returns true' do
          expect(appointment.can_be_cancelled?).to be true
        end
      end
      
      context 'when appointment is today' do
        before { appointment.appointment_date = Date.current }
        
        it 'returns false' do
          expect(appointment.can_be_cancelled?).to be false
        end
      end
    end
  end
end

Documentation Standards

Code Comments

# Use comments for complex business logic
class ConsultationFeeCalculator
  # Calculates consultation fee based on doctor's base rate,
  # patient's insurance coverage, and any applicable discounts.
  # 
  # Returns the final fee amount after all adjustments.
  def calculate_fee(doctor, patient, appointment_type)
    base_fee = doctor.consultation_fee
    
    # Apply insurance discount if patient has active coverage
    if patient.has_active_insurance?
      base_fee *= (1 - patient.insurance_coverage_percentage / 100.0)
    end
    
    # Apply first-time patient discount
    if first_time_patient?(patient)
      base_fee *= 0.9  # 10% discount
    end
    
    base_fee.round(2)
  end
  
  private
  
  def first_time_patient?(patient)
    patient.appointments.completed.count.zero?
  end
end

# Document API endpoints
class AppointmentsController < ApplicationController
  # GET /appointments
  # 
  # Fetches appointments for the current clinic with optional filtering.
  # 
  # Parameters:
  #   - date: Filter by specific date (YYYY-MM-DD)
  #   - status: Filter by appointment status
  #   - doctor_id: Filter by specific doctor
  #   - page: Page number for pagination
  # 
  # Returns: Paginated list of appointments with patient and doctor details
  def index
    # implementation
  end
end

README Documentation

# Feature Name

Brief description of what this feature does.

## Installation

Steps to set up the feature.

## Usage

```ruby
# Code examples
result = CreateAppointment.call(
  patient_id: 1,
  doctor_id: 2,
  appointment_date: Date.tomorrow
)

API Endpoints

  • GET /appointments - List appointments
  • POST /appointments - Create appointment

Configuration

Required environment variables or settings.


## Error Handling

### Custom Exceptions

```ruby
# Define domain-specific exceptions
module Appointments
  class Error < StandardError; end
  class TimeSlotUnavailable < Error; end
  class InvalidDate < Error; end
  class DoctorNotFound < Error; end
end

# Use specific exception handling
def create_appointment(params)
  validate_doctor_exists(params[:doctor_id])
  validate_time_slot(params[:time])
  # ... create appointment
rescue Appointments::DoctorNotFound => e
  { success: false, error: 'Doctor not found' }
rescue Appointments::TimeSlotUnavailable => e
  { success: false, error: 'Time slot is not available' }
rescue StandardError => e
  Rails.logger.error "Unexpected error: #{e.message}"
  { success: false, error: 'Something went wrong' }
end

Code Review Checklist

Before Submitting

  • Code follows style guide
  • All tests pass
  • Test coverage is adequate (>80%)
  • No security vulnerabilities
  • Performance considerations addressed
  • Documentation updated
  • Error handling implemented
  • Logging added where appropriate

Reviewer Checklist

  • Code is readable and maintainable
  • Architecture patterns followed
  • No code duplication
  • Edge cases handled
  • Database queries optimized
  • API contracts maintained
  • Breaking changes documented

Tools and Linters

RuboCop Configuration

# .rubocop.yml
inherit_from: .rubocop_todo.yml

AllCops:
  TargetRubyVersion: 3.2
  NewCops: enable
  Exclude:
    - 'db/schema.rb'
    - 'db/migrate/*'
    - 'vendor/**/*'
    - 'node_modules/**/*'

Layout/LineLength:
  Max: 100
  AllowedPatterns: ['\A\s*#.*\z']

Style/Documentation:
  Enabled: false

Style/StringLiterals:
  EnforcedStyle: single_quotes

Style/TrailingCommaInArrayLiteral:
  EnforcedStyleForMultiline: comma

Style/TrailingCommaInHashLiteral:
  EnforcedStyleForMultiline: comma

Running Linters

# Check code style
bundle exec rubocop

# Auto-fix issues
bundle exec rubocop -a

# Check specific files
bundle exec rubocop parts/clinic/controllers/

Continuous Integration

GitHub Actions

# .github/workflows/test.yml
name: Test Suite

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.2.0
        bundler-cache: true
    
    - name: Run RuboCop
      run: bundle exec rubocop
    
    - name: Run tests
      run: bundle exec rspec

This coding standards guide ensures consistency across the Kulcare codebase and helps maintain high code quality as the team grows.

On this page