Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Development Standards & Requirements

Table of Contents


Overview

This document outlines the mandatory requirements and best practices for all code contributions to the Pricing Engine project. All pull requests must comply with these standards before being merged.


Code Quality Requirements

Mandatory Checklist

Before submitting any PR, ensure:

  • All unit tests pass (bundle exec rspec spec/)
  • Rubocop violations are resolved (bundle exec rubocop)
  • Code coverage remains above 90%
  • No security vulnerabilities introduced
  • Postman collections updated (if API changes made)
  • Documentation updated (if behavior changes)
  • Database migrations are reversible
  • Environment variables documented in .env.example

Testing Standards

Test Coverage Requirements

Minimum Coverage: 90% overall

Required Test Types

1. Unit Tests

All models, services, and business logic MUST have unit tests.

Location: spec/models/, spec/services/, spec/lib/

Example:

# spec/services/pricing/calculator_spec.rb
require 'rails_helper'

RSpec.describe Pricing::Calculator do
  describe '#calculate' do
    it 'calculates base price correctly' do
      calculator = described_class.new(base_price: 100)
      expect(calculator.calculate).to eq(100)
    end

    context 'with discounts' do
      it 'applies percentage discount' do
        calculator = described_class.new(base_price: 100, discount: 10)
        expect(calculator.calculate).to eq(90)
      end
    end
  end
end

2. Integration Tests

API endpoints MUST have integration tests covering:

  • Happy path scenarios
  • Error cases (400, 401, 403, 404, 422, 500)
  • Edge cases
  • Authentication and authorization

Location: spec/requests/, spec/integration/

Example:

# spec/requests/api/partner/v1/pricing_rules_spec.rb
require 'swagger_helper'

RSpec.describe 'API::Partner::V1::PricingRules', type: :request do
  path '/api/partner/v1/pricing_rules' do
    post 'Creates a pricing rule' do
      tags 'Pricing Rules'
      consumes 'application/json'
      produces 'application/json'
      
      parameter name: :pricing_rule, in: :body, schema: {
        # Schema definition
      }

      response '201', 'pricing rule created' do
        let(:pricing_rule) { { name: 'Test Rule', rule_type: 'percentage' } }
        
        run_test! do |response|
          data = JSON.parse(response.body)
          expect(data['name']).to eq('Test Rule')
        end
      end

      response '422', 'invalid request' do
        let(:pricing_rule) { { name: '' } }
        run_test!
      end
    end
  end
end

3. Service Object Tests

Service objects MUST test:

  • Success cases with success? and result
  • Failure cases with failure? and errors
  • Side effects (database changes, cache updates)
  • APM instrumentation

Example:

# spec/services/audit/rule_archiver_spec.rb
RSpec.describe Audit::RuleArchiver do
  describe '#call' do
    let(:pricing_rule) { create(:pricing_rule) }
    let(:service) { described_class.new(pricing_rule) }

    it 'archives the rule successfully' do
      result = service.call
      
      expect(result).to be_success
      expect(result.result).to be_a(PricingRuleHistory)
      expect(pricing_rule.reload.status).to eq('archived')
    end

    context 'when archival fails' do
      before { allow(pricing_rule).to receive(:update!).and_raise(ActiveRecord::RecordInvalid) }

      it 'returns failure result' do
        result = service.call
        
        expect(result).to be_failure
        expect(result.errors).to be_present
      end
    end
  end
end

4. Factory Definitions

All models MUST have factory definitions.

Location: spec/factories/

Example:

# spec/factories/pricing_rules.rb
FactoryBot.define do
  factory :pricing_rule do
    name { Faker::Commerce.product_name }
    rule_type { :percentage }
    value { rand(5..50) }
    priority { rand(1..100) }
    status { :active }
    restaurant_id { rand(1..1000) }

    trait :inactive do
      status { :inactive }
    end

    trait :with_conditions do
      after(:create) do |rule|
        create(:date_time_condition, pricing_rule: rule)
      end
    end
  end
end

Test Running Commands

# Run all tests
bundle exec rspec spec/

# Run specific test file
bundle exec rspec spec/services/pricing/calculator_spec.rb

# Run specific test line
bundle exec rspec spec/services/pricing/calculator_spec.rb:15

# Run with documentation format
bundle exec rspec spec/ --format documentation

# Run with coverage report
COVERAGE=true bundle exec rspec spec/

Code Style & Linting

Rubocop Compliance

All code MUST pass Rubocop checks without violations.

Running Rubocop

# Check all files
bundle exec rubocop

# Check specific files
bundle exec rubocop app/services/pricing/calculator.rb

# Auto-fix safe violations
bundle exec rubocop -a

# Auto-fix all violations (use with caution)
bundle exec rubocop -A

Common Rules

File Structure

# Correct order:
# 1. Module/Class definition
# 2. Includes/Extends
# 3. Constants
# 4. Attributes (attr_reader, attr_accessor)
# 5. Class methods
# 6. Initialize
# 7. Public methods
# 8. Private methods

class PricingRule < ApplicationRecord
  include Cacheable
  
  STATUSES = %w[active inactive archived].freeze
  
  attr_reader :calculated_price
  
  def self.active_rules
    where(status: 'active')
  end
  
  def initialize(attributes = {})
    super
    @calculated_price = 0
  end
  
  def calculate_price(base_price)
    # Implementation
  end
  
  private
  
  def validate_price
    # Implementation
  end
end

Naming Conventions

  • Classes/Modules: PascalCase (PricingRule, DateTimeCondition)
  • Methods/Variables: snake_case (calculate_price, base_amount)
  • Constants: SCREAMING_SNAKE_CASE (MAX_PRICE, DEFAULT_DISCOUNT)
  • Boolean methods: Use ? suffix (active?, valid?)
  • Dangerous methods: Use ! suffix (save!, update!)

Line Length

  • Maximum 120 characters per line
  • Break long method chains with proper indentation
# Good
PricingRule
  .active
  .where(restaurant_id: restaurant_id)
  .order(priority: :desc)
  .limit(10)

# Bad
PricingRule.active.where(restaurant_id: restaurant_id).order(priority: :desc).limit(10)

RDoc Documentation Standards

RDoc Comment Format

All public classes, modules, and methods MUST have RDoc comments.

RDoc comments appear immediately before the code and use special tags to document parameters, return values, and examples.

Class Documentation

Format:

# Brief description of the class
#
# More detailed description explaining what this class does,
# its responsibilities, and how it fits into the system.
#
# @example
#   calculator = Pricing::Calculator.new(rule: rule, base_price: 100)
#   result = calculator.call
#   puts result.result # => calculated_price
#
# @see PricingRule for related pricing rule model
# @see DateTimeCondition for time-based condition details
class MyClass
  # Implementation
end

Method Documentation

Format:

# Brief one-line description of what the method does
#
# More detailed description of the method's behavior,
# including any important side effects or special cases.
#
# @param param_name [Type] description of the parameter
# @param other_param [String, Integer] accepts multiple types
# @return [ReturnType] description of the return value
# @raise [ExceptionType] description of when this exception is raised
# @note Important information about the method
# @since 1.0.0
#
# @example Basic usage
#   result = my_object.my_method("value", 10)
#   puts result # => some_output
#
# @example With error handling
#   begin
#     my_object.my_method(nil)
#   rescue ArgumentError => e
#     puts "Error: #{e.message}"
#   end
#
# @see #related_method for related functionality
def my_method(param_name, other_param)
  # Implementation
end

Common RDoc Tags

TagPurposeExample
@paramDocument parameters@param user_id [Integer] the user identifier
@returnDocument return type/value@return [Hash] pricing calculation result
@raiseDocument exceptions@raise [ArgumentError] if price is negative
@exampleProvide usage examples@example result = calculator.call
@noteImportant information@note This method performs database updates
@seeReference related code@see PricingRule#calculate
@sinceVersion when added@since 1.2.0
@deprecatedMark as deprecated@deprecated Use {#new_method} instead
@yieldsDocument blocks@yields [price] yields the calculated price

Service Object Documentation Example

module Pricing
  # Calculates the final price based on pricing rules and conditions
  #
  # This service encapsulates the complex logic for calculating prices,
  # considering multiple pricing rules, date/time conditions, and their
  # priorities. It supports both percentage and fixed-value pricing rules.
  #
  # The calculation process:
  # 1. Validates the base price
  # 2. Retrieves applicable pricing rules for the restaurant
  # 3. Filters rules by date/time conditions
  # 4. Sorts rules by priority
  # 5. Applies rules in sequence
  # 6. Returns the final calculated price
  #
  # @example Basic calculation
  #   result = Pricing::Calculator.new(
  #     base_price: 100.0,
  #     restaurant_id: 42,
  #     order_time: Time.current
  #   ).call
  #   
  #   if result.success?
  #     puts "Final price: #{result.result}"
  #   else
  #     puts "Calculation failed: #{result.errors.join(', ')}"
  #   end
  #
  # @example With custom context
  #   calculator = Pricing::Calculator.new(
  #     base_price: 150.0,
  #     restaurant_id: 42,
  #     order_time: Time.current,
  #     context: { user_type: 'premium', channel: 'mobile' }
  #   )
  #   result = calculator.call
  #
  # @see PricingRule for rule model details
  # @see DateTimeCondition for condition specifications
  # @see BaseService for parent class details
  class Calculator < BaseService
    include ApmInstrumented
    
    # Initialize the calculator with required parameters
    #
    # @param base_price [Float] the original price before any pricing rules are applied
    # @param restaurant_id [Integer] the restaurant to calculate pricing for
    # @param order_time [Time] the time to evaluate date/time conditions against (defaults to current time)
    # @param context [Hash] optional context hash for additional business logic
    # @raise [ArgumentError] if base_price is not a valid positive number
    def initialize(base_price:, restaurant_id:, order_time: Time.current, context: {})
      @base_price = base_price
      @restaurant_id = restaurant_id
      @order_time = order_time
      @context = context
    end
    
    # Execute the price calculation
    #
    # @return [ServiceResult] success result with calculated price or failure result with errors
    #
    # @example
    #   result = calculator.call
    #   if result.success?
    #     puts result.result # => 85.50
    #   end
    def call
      return failure_result('Invalid price') if @base_price <= 0
      
      calculated_price = perform_calculation
      success_result(calculated_price)
    rescue StandardError => e
      handle_error(e, 'Price calculation failed')
      failure_result(e.message)
    end
    
    private
    
    # Performs the actual price calculation logic
    #
    # @return [Float] the calculated final price
    # @note This is a private method and should not be called directly
    def perform_calculation
      # Implementation
    end
  end
end

Constant Documentation

class PricingRule < ApplicationRecord
  # Valid pricing rule types
  # @see #rule_type
  RULE_TYPES = %w[percentage fixed].freeze
  
  # Default priority value for new rules
  DEFAULT_PRIORITY = 100
  
  # Maximum percentage discount allowed
  MAX_DISCOUNT_PERCENTAGE = 100
end

Generate and View RDoc

# Generate RDoc documentation
bundle exec rails rdoc

# View documentation
open doc/index.html

Architecture Patterns

Service Object Pattern

All business logic MUST be encapsulated in service objects.

Location: app/services/

Structure:

module Pricing
  class Calculator < BaseService
    include ApmInstrumented
    
    def initialize(pricing_rule:, base_price:, context: {})
      @pricing_rule = pricing_rule
      @base_price = base_price
      @context = context
    end
    
    def call
      return failure_result('Invalid price') if @base_price <= 0
      
      calculated_price = perform_calculation
      
      success_result(calculated_price)
    rescue StandardError => e
      handle_error(e, 'Price calculation failed')
      failure_result(e.message)
    end
    
    private
    
    attr_reader :pricing_rule, :base_price, :context
    
    def perform_calculation
      # Calculation logic
    end
  end
end

Usage:

result = Pricing::Calculator.new(
  pricing_rule: rule,
  base_price: 100.0,
  context: { user_id: current_user.id }
).call

if result.success?
  render json: { price: result.result }, status: :ok
else
  render json: { errors: result.errors }, status: :unprocessable_entity
end

Repository Pattern

Database queries MUST be encapsulated in repository classes.

Location: app/repositories/

Example:

class PricingRuleRepository
  include Cacheable
  
  def find_active_rules(restaurant_id:, rule_type: nil)
    query = PricingRule.active.where(restaurant_id: restaurant_id)
    query = query.where(rule_type: rule_type) if rule_type.present?
    query.order(priority: :desc)
  end
  
  def find_with_conditions(rule_id)
    cache_fetch("pricing_rule:#{rule_id}:with_conditions", ttl: 5.minutes) do
      PricingRule
        .includes(:date_time_conditions)
        .find(rule_id)
    end
  end
end

Query Object Pattern

Complex queries MUST use query objects.

Location: app/queries/

Example:

class PricingRuleQuery
  def initialize(relation = PricingRule.all)
    @relation = relation
  end
  
  def active
    @relation.where(status: 'active')
  end
  
  def for_restaurant(restaurant_id)
    @relation.where(restaurant_id: restaurant_id)
  end
  
  def with_priority_above(priority)
    @relation.where('priority > ?', priority)
  end
  
  def ordered
    @relation.order(priority: :desc, created_at: :desc)
  end
end

Serializer Pattern (Alba Resources)

API responses MUST use Alba serializers.

Location: app/resources/

Example:

class PricingRuleResource
  include Alba::Resource
  
  attributes :id, :name, :rule_type, :value, :priority, :status
  
  attribute :created_at do |rule|
    rule.created_at.iso8601
  end
  
  many :date_time_conditions, resource: DateTimeConditionResource
  
  attribute :applied_discount, if: proc { |rule, params| params[:include_calculations] } do |rule|
    rule.calculate_discount
  end
end

API Documentation

Postman Collections

MUST update Postman collections when:

  • Adding new endpoints
  • Modifying request/response schemas
  • Changing authentication requirements
  • Updating query parameters or headers

Location: docs/postman/

Collections:

  1. Partner_API_v1.postman_collection.json - Partner-facing APIs
  2. Internal_API.postman_collection.json - Internal service-to-service APIs

Swagger Documentation

All API endpoints MUST have Swagger/RSpec documentation.

Generate Swagger docs:

bundle exec rake rswag:specs:swaggerize

Example:

path '/api/partner/v1/pricing_rules/{id}' do
  get 'Retrieves a pricing rule' do
    tags 'Pricing Rules'
    produces 'application/json'
    security [{ partner_auth: [] }]
    
    parameter name: :id, in: :path, type: :integer, required: true,
              description: 'Pricing rule ID'
    
    response '200', 'pricing rule found' do
      schema type: :object,
        properties: {
          id: { type: :integer },
          name: { type: :string },
          rule_type: { type: :string, enum: %w[percentage fixed] },
          value: { type: :number },
          status: { type: :string, enum: %w[active inactive archived] }
        },
        required: %w[id name rule_type value]
      
      run_test!
    end
    
    response '404', 'pricing rule not found' do
      run_test!
    end
  end
end

Git Workflow

Branch Naming

Format: <type>/<ticket-number>-<short-description>

Types:

  • feature/ - New features
  • fix/ - Bug fixes
  • refactor/ - Code refactoring
  • docs/ - Documentation updates
  • test/ - Test additions/updates
  • chore/ - Maintenance tasks

Examples:

  • feature/PE-123-add-time-based-pricing
  • fix/PE-456-null-pointer-error
  • refactor/PE-789-extract-calculator-service

Commit Messages

Format: <type>: <description>

Types:

  • feat: - New feature
  • fix: - Bug fix
  • refactor: - Code refactoring
  • test: - Test additions
  • docs: - Documentation
  • style: - Code style changes
  • chore: - Maintenance

Examples:

feat: add support for time-based pricing rules
fix: resolve null pointer error in price calculator
refactor: extract price calculation logic to service
test: add unit tests for DateTimeCondition model
docs: update API endpoint documentation

Pull Request Requirements

PR Template Checklist:

  • Tests added/updated and passing
  • Rubocop violations resolved
  • Documentation updated
  • Postman collection updated (if API changed)
  • Database migrations tested (up and down)
  • Performance impact considered
  • Security implications reviewed
  • Backward compatibility maintained

Performance Standards

Database Queries

Avoid N+1 queries:

# Bad
pricing_rules.each do |rule|
  rule.date_time_conditions.each { |condition| ... }
end

# Good
pricing_rules.includes(:date_time_conditions).each do |rule|
  rule.date_time_conditions.each { |condition| ... }
end

Caching Strategy

Use Redis caching for:

  • Frequently accessed data
  • Expensive computations
  • API responses (when appropriate)

Example:

def fetch_active_rules(restaurant_id)
  cache_key = "restaurant:#{restaurant_id}:active_rules"
  
  Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
    PricingRule.active.where(restaurant_id: restaurant_id).to_a
  end
end

APM Monitoring

All service objects MUST include APM instrumentation:

class MyService < BaseService
  include ApmInstrumented
  
  def call
    # Automatically tracked by APM
  end
end

Code Reusability & DRY Principle

Extract Common Logic

Identify repeated patterns and extract them into reusable methods or services.

Anti-Pattern: Duplicate Code

# Bad - duplicated validation logic
class PricingRulesController < BaseController
  def create
    return unless valid_price?(params[:price])
    # ...
  end
  
  def update
    return unless valid_price?(params[:price])
    # ...
  end
  
  private
  
  def valid_price?(price)
    price.present? && price.to_f > 0 && price.to_f <= 10000
  end
end

Pattern: Extract to Service

# Good - shared service
class PricingRulesController < BaseController
  def create
    result = Pricing::ValidatePrice.call(price: params[:price])
    return render_error(result.errors) unless result.success?
    # ...
  end
  
  def update
    result = Pricing::ValidatePrice.call(price: params[:price])
    return render_error(result.errors) unless result.success?
    # ...
  end
end

# Reusable service
module Pricing
  class ValidatePrice < BaseService
    # Validates that a price is within acceptable bounds
    #
    # @param price [Numeric, String] the price to validate
    # @return [ServiceResult] success or failure with validation errors
    def initialize(price:)
      @price = price
    end
    
    def call
      return failure_result('Price cannot be blank') if @price.blank?
      return failure_result('Price must be a valid number') unless numeric?(@price)
      
      price_float = @price.to_f
      return failure_result('Price must be greater than 0') if price_float <= 0
      return failure_result('Price cannot exceed 10000') if price_float > 10000
      
      success_result(price_float)
    end
    
    private
    
    def numeric?(value)
      Float(value, exception: false).present?
    end
  end
end

Anti-Pattern: Duplicated Queries

# Bad - similar queries in multiple places
class PricingRulesController < BaseController
  def active_rules
    @rules = PricingRule
      .where(restaurant_id: restaurant_id)
      .where(active: true)
      .order(priority: :desc)
  end
end

class PricingRulesService < BaseService
  def fetch_rules
    rules = PricingRule
      .where(restaurant_id: restaurant_id)
      .where(active: true)
      .order(priority: :desc)
  end
end

Pattern: Extract to Repository

# Good - centralized queries
class PricingRuleRepository
  # Fetches active pricing rules for a restaurant
  #
  # @param restaurant_id [Integer] the restaurant identifier
  # @param limit [Integer] maximum number of rules to return
  # @return [ActiveRecord::Relation] active pricing rules ordered by priority
  def active_rules(restaurant_id:, limit: nil)
    query = PricingRule
      .where(restaurant_id: restaurant_id)
      .where(active: true)
      .order(priority: :desc)
    
    query = query.limit(limit) if limit.present?
    query
  end
end

# Usage
class PricingRulesController < BaseController
  def active_rules
    repository = PricingRuleRepository.new
    @rules = repository.active_rules(restaurant_id: restaurant_id)
  end
end

class PricingRulesService < BaseService
  def fetch_rules
    repository = PricingRuleRepository.new
    @rules = repository.active_rules(restaurant_id: restaurant_id)
  end
end

Anti-Pattern: Duplicated Calculations

# Bad - logic repeated across methods
class OrderPricing
  def calculate_subtotal(items)
    items.sum { |item| item.price * item.quantity }
  end
  
  def calculate_total(items, discount)
    subtotal = items.sum { |item| item.price * item.quantity }
    subtotal - (subtotal * discount / 100)
  end
end

Pattern: Extract Calculation Methods

# Good - DRY calculation logic
class OrderPricing
  # Calculates the subtotal from order items
  #
  # @param items [Array<OrderItem>] collection of order items
  # @return [Float] the subtotal before discounts
  def calculate_subtotal(items)
    items.sum { |item| item.price * item.quantity }
  end
  
  # Calculates the total price with discount applied
  #
  # @param items [Array<OrderItem>] collection of order items
  # @param discount [Numeric] discount percentage (0-100)
  # @return [Float] the final total price
  def calculate_total(items, discount)
    subtotal = calculate_subtotal(items)
    apply_discount(subtotal, discount)
  end
  
  private
  
  # Applies a percentage discount to an amount
  #
  # @param amount [Float] the amount to discount
  # @param discount [Numeric] discount percentage
  # @return [Float] the amount after discount
  def apply_discount(amount, discount)
    amount - (amount * discount / 100)
  end
end

Shared Concerns

Use modules/concerns for shared behavior:

# app/models/concerns/timestampable.rb
# Provides automatic timestamp tracking for models
#
# Models including this concern will have automatic created_at and updated_at
# timestamps managed by Rails, plus soft delete support.
module Timestampable
  extend ActiveSupport::Concern
  
  included do
    validates :created_at, :updated_at, presence: true
  end
  
  # Returns whether this record was created today
  #
  # @return [Boolean] true if created_at is today's date
  def created_today?
    created_at.to_date == Date.current
  end
end

# Usage
class PricingRule < ApplicationRecord
  include Timestampable
end

Error Handling & Logging

Exception Handling

Always handle exceptions explicitly and log them appropriately.

# Bad - swallowing errors silently
def fetch_data
  result = external_api_call
  return nil if result.nil?
rescue
  nil
end

# Good - explicit error handling and logging
def fetch_data
  result = external_api_call
  return result unless result.nil?
rescue StandardError => e
  Rails.logger.error("Failed to fetch data: #{e.message}")
  Honeybadger.notify(e, context: { service: 'external_api' })
  raise # Re-raise after logging
end

Service Result Pattern

Use a consistent result pattern for operations:

# app/services/base_service.rb
# Base class for all service objects providing consistent result handling
#
# Service objects should inherit from this class and call success_result
# or failure_result to indicate the operation outcome.
class BaseService
  # Returns a successful result
  #
  # @param result [Object] the result data
  # @param meta [Hash] optional metadata to include in result
  # @return [ServiceResult] success result object
  def success_result(result = nil, meta: {})
    ServiceResult.new(success: true, result: result, meta: meta)
  end
  
  # Returns a failure result
  #
  # @param errors [String, Array<String>] error messages
  # @return [ServiceResult] failure result object
  def failure_result(errors)
    ServiceResult.new(success: false, errors: Array(errors))
  end
  
  # Handles errors and sends to APM if configured
  #
  # @param exception [StandardError] the exception to handle
  # @param message [String] additional context message
  # @return [void]
  def handle_error(exception, message = nil)
    Rails.logger.error("#{message}: #{exception.message}")
    Rails.logger.debug(exception.backtrace.join("\n")) if exception.backtrace
    
    # Send to APM monitoring if available
    Honeybadger.notify(exception, context: { service: self.class.name }) rescue nil
  end
end

class ServiceResult
  # Represents the result of a service operation
  #
  # @attr_reader success [Boolean] whether the operation succeeded
  # @attr_reader result [Object] the operation result (if successful)
  # @attr_reader errors [Array<String>] error messages (if failed)
  # @attr_reader meta [Hash] optional metadata
  attr_reader :success, :result, :errors, :meta
  
  # Initialize a service result
  #
  # @param success [Boolean] operation success status
  # @param result [Object] operation result
  # @param errors [Array<String>] error messages
  # @param meta [Hash] optional metadata
  def initialize(success:, result: nil, errors: [], meta: {})
    @success = success
    @result = result
    @errors = errors
    @meta = meta
  end
  
  # Check if operation was successful
  #
  # @return [Boolean] true if successful
  def success?
    @success == true
  end
  
  # Check if operation failed
  #
  # @return [Boolean] true if failed
  def failure?
    !success?
  end
end

Logging Standards

Use appropriate log levels:

class PricingCalculator
  # Logs should be meaningful and include context
  
  def calculate(rule, base_price)
    Rails.logger.debug("Starting calculation for rule #{rule.id}")
    
    if base_price <= 0
      Rails.logger.warn("Invalid base price: #{base_price} for rule #{rule.id}")
      return nil
    end
    
    result = perform_calculation(rule, base_price)
    
    Rails.logger.info("Calculation complete for rule #{rule.id}: #{result}")
    result
  rescue StandardError => e
    Rails.logger.error("Calculation failed: #{e.message} for rule #{rule.id}")
    raise
  end
end

Configuration Management

Environment Variables

All configuration should use environment variables, not hardcoded values.

# Bad - hardcoded values
class PricingCalculator
  MAX_DISCOUNT = 100
  CACHE_TTL = 3600
  
  def calculate
    # Uses hardcoded values
  end
end

# Good - environment-based configuration
class PricingCalculator
  # Maximum discount percentage allowed
  MAX_DISCOUNT = ENV.fetch('MAX_DISCOUNT_PERCENTAGE', '100').to_i
  
  # Cache TTL in seconds
  CACHE_TTL = ENV.fetch('PRICING_CACHE_TTL', '3600').to_i
  
  def calculate
    # Uses configuration from environment
  end
end

Configuration File Pattern

# config/settings.yml
pricing:
  max_discount_percentage: 100
  cache_ttl_seconds: 3600
  calculation_precision: 2

# app/models/settings.rb
# Application settings management
#
# Provides centralized access to configuration values
class Settings
  # Loads all settings from YAML file
  #
  # @return [Hash] all configuration settings
  def self.all
    @all ||= YAML.load_file(Rails.root.join('config/settings.yml'))
  end
  
  # Retrieves a setting value
  #
  # @param key [String, Symbol] the setting key with dot notation support
  # @return [Object] the setting value
  #
  # @example
  #   Settings.get('pricing.max_discount_percentage') # => 100
  def self.get(key)
    key.to_s.split('.').reduce(all) { |h, k| h[k] }
  end
end

Additional Best Practices

Security Requirements

Authentication

All partner endpoints MUST:

  • Validate access tokens
  • Check token expiration
  • Verify restaurant context
  • Log authentication attempts

Input Validation

All user inputs MUST:

  • Be validated and sanitized
  • Use strong parameters in controllers
  • Have database-level constraints
  • Include length and format checks

Example:

def pricing_rule_params
  params.require(:pricing_rule).permit(
    :name,
    :rule_type,
    :value,
    :priority,
    date_time_conditions_attributes: [
      :id,
      :days_of_week,
      :start_time,
      :end_time,
      :_destroy
    ]
  )
end

SQL Injection Prevention

Always use parameterized queries:

# Good
PricingRule.where("restaurant_id = ?", restaurant_id)
PricingRule.where(restaurant_id: restaurant_id)

# Bad - NEVER DO THIS
PricingRule.where("restaurant_id = #{restaurant_id}")

Environment Variables

Sensitive data MUST:

  • Be stored in environment variables
  • Never be committed to git
  • Be documented in .env.example
  • Use different values per environment

Documentation Requirements

Code Documentation

Classes and modules:

# Calculates pricing based on rules and conditions
#
# This service handles the complex logic of applying pricing rules
# considering date/time conditions, priority levels, and restaurant context.
#
# @example
#   calculator = Pricing::Calculator.new(rule: rule, base_price: 100)
#   result = calculator.call
#   result.result # => calculated_price
#
class Calculator < BaseService
  # Implementation
end

Complex methods:

# Applies time-based discounts to the base price
#
# @param base_price [Float] the original price before discounts
# @param time_range [Range] the time range for the discount
# @return [Float] the discounted price
def apply_time_discount(base_price, time_range)
  # Implementation
end

API Documentation

Update when:

  • Adding new endpoints
  • Modifying request/response schemas
  • Changing error responses
  • Adding query parameters

Files to update:

  • docs/004-API-ENDPOINTS.md
  • swagger/v1/swagger.yaml
  • Postman collections

README Updates

Update when:

  • Adding new features
  • Changing setup requirements
  • Modifying environment variables
  • Adding new dependencies

Database Standards

Migration Guidelines

Migrations MUST:

  • Be reversible (include down method or use reversible commands)
  • Include appropriate indexes
  • Have rollback strategies
  • Be tested in development first

Example:

class AddStatusToPricingRules < ActiveRecord::Migration[7.0]
  def change
    add_column :pricing_rules, :status, :string, default: 'active', null: false
    add_index :pricing_rules, :status
  end
end

Model Validations

All models MUST have:

  • Presence validations for required fields
  • Format validations for structured data
  • Uniqueness validations where needed
  • Custom validations for business rules

Example:

class PricingRule < ApplicationRecord
  validates :name, presence: true, length: { minimum: 3, maximum: 100 }
  validates :rule_type, presence: true, inclusion: { in: %w[percentage fixed] }
  validates :value, presence: true, numericality: { greater_than: 0 }
  validates :priority, numericality: { greater_than_or_equal_to: 0 }
  
  validate :value_within_bounds
  
  private
  
  def value_within_bounds
    return unless rule_type == 'percentage' && value > 100
    
    errors.add(:value, 'cannot exceed 100 for percentage rules')
  end
end

Additional Best Practices

Method Complexity

Methods should be focused and have low cyclomatic complexity.

# Bad - method doing too much
def process_order(order)
  if order.valid?
    if order.items.any?
      total = 0
      order.items.each do |item|
        if item.in_stock?
          total += item.price * item.quantity
          if total > 10000
            apply_bulk_discount(total)
          end
        else
          return error("Item #{item.id} out of stock")
        end
      end
      order.save!
      send_confirmation_email(order)
      return success(total)
    else
      return error("Order has no items")
    end
  else
    return error("Invalid order")
  end
end

# Good - single responsibility
def process_order(order)
  return failure_result("Invalid order") unless order.valid?
  
  items_result = validate_items(order)
  return items_result unless items_result.success?
  
  total = calculate_total(order)
  total = apply_bulk_discount(total) if qualifies_for_discount?(total)
  
  order.save!
  send_confirmation_email(order)
  
  success_result(total)
end

private

def validate_items(order)
  return failure_result("Order has no items") if order.items.empty?
  
  out_of_stock = order.items.reject(&:in_stock?)
  return failure_result("Items out of stock: #{out_of_stock.map(&:id).join(', ')}") if out_of_stock.any?
  
  success_result(order.items)
end

def calculate_total(order)
  order.items.sum { |item| item.price * item.quantity }
end

def qualifies_for_discount?(total)
  total > 10000
end

Guard Clauses

Use guard clauses to reduce nesting and improve readability.

# Bad - nested conditionals
def calculate_discount(user, order)
  if user.present?
    if order.present?
      if user.premium?
        if order.total > 100
          return order.total * 0.2
        else
          return order.total * 0.1
        end
      else
        if order.total > 50
          return order.total * 0.05
        end
      end
    end
  end
  0
end

# Good - guard clauses
def calculate_discount(user, order)
  return 0 unless user.present? && order.present?
  
  return order.total * 0.2 if user.premium? && order.total > 100
  return order.total * 0.1 if user.premium? && order.total <= 100
  return order.total * 0.05 if order.total > 50
  
  0
end

Meaningful Variable Names

Use clear, self-documenting variable names.

# Bad - unclear abbreviations
def calc(b, r, d)
  r_a = b * (r / 100.0)
  return (b - r_a) if d
  b - r_a * 0.5
end

# Good - descriptive names
def calculate_final_price(base_price, discount_percent, apply_immediately)
  discount_amount = base_price * (discount_percent / 100.0)
  return base_price - discount_amount if apply_immediately
  
  base_price - (discount_amount * 0.5)
end

Avoid Magic Numbers

Extract magic numbers to named constants with documentation.

# Bad - magic numbers scattered throughout
def apply_pricing_rules(price, rule_type)
  case rule_type
  when 1
    price * 0.9
  when 2
    price * 1.15
  when 3
    price - 50
  end
end

# Good - named constants with documentation
class PricingRules
  # Discount applied for regular customers
  REGULAR_DISCOUNT_MULTIPLIER = 0.9
  
  # Premium surcharge for special items
  PREMIUM_SURCHARGE_MULTIPLIER = 1.15
  
  # Fixed discount for bulk orders
  BULK_DISCOUNT_AMOUNT = 50
  
  RULE_TYPES = {
    regular: 1,
    premium: 2,
    bulk: 3,
  }.freeze
  
  def apply_pricing_rules(price, rule_type)
    case rule_type
    when RULE_TYPES[:regular]
      price * REGULAR_DISCOUNT_MULTIPLIER
    when RULE_TYPES[:premium]
      price * PREMIUM_SURCHARGE_MULTIPLIER
    when RULE_TYPES[:bulk]
      price - BULK_DISCOUNT_AMOUNT
    end
  end
end

Consistent Method Return Values

Methods should consistently return the same type.

# Bad - inconsistent return types
def fetch_user(id)
  user = User.find_by(id: id)
  return nil if user.blank?
  
  user
end

# Sometimes returns User, sometimes nil, sometimes false
user = fetch_user(123)
user.name if user  # Defensive programming needed

# Good - explicit with clear types
def fetch_user(id)
  User.find_by(id: id)  # Returns User or nil consistently
end

# Even better - service object with explicit result
class FetchUserService < BaseService
  def initialize(id:)
    @id = id
  end
  
  def call
    user = User.find_by(id: @id)
    return failure_result('User not found') if user.blank?
    
    success_result(user)
  end
end

Block & Iterator Conventions

Use appropriate iterators and block structures.

# Bad - overly verbose
active_users = []
users.each do |user|
  if user.active?
    active_users << user
  end
end

# Good - use select
active_users = users.select(&:active?)

# Bad - map then filter
prices = []
items.each { |item| prices << item.price }
valid_prices = prices.reject { |p| p <= 0 }

# Good - chain selectors
valid_prices = items.map(&:price).reject { |p| p <= 0 }

# Bad - each with side effects
result_prices = []
items.each { |item| result_prices << calculate_price(item) }

# Good - map for transformations
result_prices = items.map { |item| calculate_price(item) }

Type Hints and Duck Typing

Use type hints in documentation for clarity.

# Good - clear parameter and return types
class OrderProcessor
  # Processes an order for a customer
  #
  # @param order [Order] the order to process
  # @param customer [Customer, String] customer object or ID (coerced to ID)
  # @return [ServiceResult<Hash>] success with order data or failure with errors
  def process(order, customer)
    customer_id = extract_id(customer)
    
    # Implementation
    success_result(order.to_h)
  end
  
  private
  
  # Extracts an ID from various input types
  #
  # @param input [Integer, String, #id] accepts ID, string ID, or object with id method
  # @return [Integer] the extracted ID
  # @raise [TypeError] if input cannot be coerced to ID
  def extract_id(input)
    case input
    when Integer
      input
    when String
      input.to_i
    when ->(obj) { obj.respond_to?(:id) }
      input.id
    else
      raise TypeError, "Cannot extract ID from #{input.class}"
    end
  end
end

Proper Use of Private Methods

Mark methods as private/protected when they’re not part of the public API.

class PricingCalculator
  # Public API
  
  # Calculates the final price for an order
  #
  # @param order [Order] the order to calculate price for
  # @return [Float] the final calculated price
  def calculate(order)
    base_total = sum_item_prices(order.items)
    apply_rule_discounts(base_total, order.pricing_rules)
  end
  
  private
  
  # Sums up all item prices in an order
  #
  # @param items [Array<OrderItem>] the items to sum
  # @return [Float] the total of all item prices
  def sum_item_prices(items)
    items.sum(&:price)
  end
  
  # Applies pricing rules to reduce the total
  #
  # @param total [Float] the current total to apply rules to
  # @param rules [Array<PricingRule>] the rules to apply
  # @return [Float] the adjusted total
  def apply_rule_discounts(total, rules)
    rules.reduce(total) { |acc, rule| rule.apply(acc) }
  end
end

Naming Conventions

Use consistent naming patterns across the codebase:

# Query methods return boolean
is_valid?      # ✅ Good
valid?         # ✅ Also acceptable
validate?      # ❌ Confusing - sounds like a verb

# Predicates for collections
has_items?     # ✅ Good
items?         # ✅ Acceptable for "has any items"
include_item?  # ❌ Less clear

# Factory/builder methods
build_rule     # ✅ Good for unsaved objects
create_rule    # ✅ Good for saved objects
make_rule      # ❌ Vague

# Service method conventions
call           # ✅ Primary entry point
perform        # ✅ Alternative entry point
execute        # ❌ Less conventional in Rails

Comment Best Practices

Comments should explain WHY, not WHAT.

# Bad - comments just repeat the code
def apply_discount(total, percent)
  # Calculate discount amount
  discount = total * (percent / 100.0)
  
  # Subtract from total
  total - discount
end

# Good - comments explain the reasoning
def apply_discount(total, percent)
  # Percentage calculations need to be converted to decimal
  # to avoid integer division errors in Ruby
  discount = total * (percent / 100.0)
  
  # Never return negative totals, even with high discounts
  # This protects against business logic errors
  [total - discount, 0].max
end

# Good - complex logic with comments
def calculate_final_price(order)
  # Base calculation without rules
  subtotal = order.items.sum(&:price)
  
  # Apply time-based rules first as they have lowest priority
  # This ensures more specific rules can override if needed
  time_rules = order.pricing_rules.where(priority: 1..50)
  subtotal = apply_rules(subtotal, time_rules)
  
  # Apply volume-based rules with higher priority
  # These typically have more business impact
  volume_rules = order.pricing_rules.where(priority: 51..100)
  apply_rules(subtotal, volume_rules)
end

Additional Resources


Questions or Clarifications?

For questions about these standards, please:

  1. Check existing documentation
  2. Review similar code in the codebase
  3. Ask in team chat/standup
  4. Create a discussion in GitHub

Remember: These standards exist to maintain code quality, consistency, and reliability. Following them ensures a better developer experience for everyone on the team.