Development Standards & Requirements
Table of Contents
- Overview
- Code Quality Requirements
- Testing Standards
- Code Style & Linting
- RDoc Documentation Standards
- Architecture Patterns
- Code Reusability & DRY Principle
- API Documentation
- Git Workflow
- Performance Standards
- Security Requirements
- Error Handling & Logging
- Configuration Management
- Documentation Requirements
- Additional Best Practices
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?andresult - Failure cases with
failure?anderrors - 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
| Tag | Purpose | Example |
|---|---|---|
@param | Document parameters | @param user_id [Integer] the user identifier |
@return | Document return type/value | @return [Hash] pricing calculation result |
@raise | Document exceptions | @raise [ArgumentError] if price is negative |
@example | Provide usage examples | @example result = calculator.call |
@note | Important information | @note This method performs database updates |
@see | Reference related code | @see PricingRule#calculate |
@since | Version when added | @since 1.2.0 |
@deprecated | Mark as deprecated | @deprecated Use {#new_method} instead |
@yields | Document 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:
Partner_API_v1.postman_collection.json- Partner-facing APIsInternal_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 featuresfix/- Bug fixesrefactor/- Code refactoringdocs/- Documentation updatestest/- Test additions/updateschore/- Maintenance tasks
Examples:
feature/PE-123-add-time-based-pricingfix/PE-456-null-pointer-errorrefactor/PE-789-extract-calculator-service
Commit Messages
Format: <type>: <description>
Types:
feat:- New featurefix:- Bug fixrefactor:- Code refactoringtest:- Test additionsdocs:- Documentationstyle:- Code style changeschore:- 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.mdswagger/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
downmethod 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:
- Check existing documentation
- Review similar code in the codebase
- Ask in team chat/standup
- 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.