model-expert
Skill2 starsChanged 35 days agoMITView on GitHub
Expert at writing Rails 8 models with proper validations, associations, scopes, callbacks, and concerns. Use when user asks to create models, add associations, implement business logic in models, or work with ActiveRecord patterns. Specializes in multi-tenancy, soft deletion, HIPAA compliance, and NanoID primary keys.
In this repository75 other files it gives its agents.
- acceptance-criteriaSkill
- architect-guideSkill
- architect-reviewSkill
- audit-fixerSkill
- avo-admin-expertSkill
- backend-services-expertSkill
- bug-hunt-allSkill
- bug-hunt-fixSkill
- bug-huntSkill
- build-feature-remoteSkill
- build-featureSkill
- codeSkill
- component-testSkill
- controller-expertSkill
- create-qa-documentSkill
- debugSkill
- devops-expertSkill
- execute-qaSkill
- feature-qaSkill
- fix-bug-indexSkill
- fix-honeybadgerSkill
- fix-spec-bugsSkill
- fix-system-testSkill
- frontend-expertSkill
- frontend-reviewSkill
- full-qa-fixSkill
- full-qaSkill
- full-reviewSkill
- green-ciSkill
- group-issuesSkill
- honeybadger-auditSkill
- ideateSkill
- impact-assessmentSkill
- improveSkill
- merge-mainSkill
- model-testSkill
- native-bug-hunt-fixSkill
- native-bug-huntSkill
- page-speedSkill
- plan-questionsSkill
- planSkill
- plan-system-testsSkill
- policy-testSkill
- prdSkill
- prd-worktreeSkill
- product-questionSkill
- product-question-tldrSkill
- qa-branchSkill
- qa-testerSkill
- refactor-sweepSkill
- resolve-honeybadgerSkill
- resolve-issueSkill
- resolve-issuesSkill
- review-acceptance-criteriaSkill
- review-fixesSkill
- review-railsSkill
- rspec-test-expertSkill
- scale-reviewSkill
- service-testSkill
- settle-pr-reviewSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
17 rules. About 0 are common, 11 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈11
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 33
Every model should follow this consistent structure:
- Very specific: line 253
Every tenant-scoped model must include acts_as_tenant.
- Very specific: line 275
Cross-tenant queries: use ActsAsTenant.without_tenant:
- Very specific: line 284
Always include organization_id column and index:
- Distinctive: line 297
Use for models that should be recoverable after deletion.
- Distinctive: line 580
Don't change existing values: - Only add new ones at the end
- Very specific: line 821
Be explicit about conditions: - Always add if: or unless:
- Distinctive: line 1314
When creating or modifying models:
- Distinctive: line 1319
Write migration first: - Ensure database schema is correct
- Distinctive: line 1320
Implement model: - Follow the structure template above
- Distinctive: line 1323
Verify multi-tenancy: - Ensure proper organization scoping
- Very specific: line 1330
Skipping multi-tenancy: - Always include acts_as_tenant
- Distinctive: line 1331
Missing soft delete: - Use SoftDeletable for recoverable data
- Distinctive: line 1332
N+1 queries: - Use eager loading and scoped associations
- Very specific: line 1333
Validation bypasses: - Never use save(validate: false) in production code
- Distinctive: line 1334
Direct SQL: - Use ActiveRecord query methods
- Distinctive: line 1335
Callback abuse: - Keep callbacks simple, use services for complex logic
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Model Expert
You are an expert Rails backend engineer specializing in writing clean, well-structured ActiveRecord models for Rails 8 applications. You follow established patterns, ensure data integrity, and write models that are easy to understand and maintain.
Core Principles
- Follow established patterns - Match existing conventions in the codebase
- Keep models focused - Single responsibility, delegate complex logic to services
- Validate at the right level - Database constraints + model validations
- Security first - Multi-tenancy, authorization, and HIPAA compliance
- Performance matters - Proper indexing, eager loading, N+1 prevention
- Test everything - Validations, associations, callbacks, and business logic
Technology Stack Context
This is a Rails 8 application with:
- Rails 8.0.2 with Ruby 3.3.5
- PostgreSQL with NanoID primary keys (12 character strings)
- Multi-tenancy: ActsAsTenant with Organization as tenant
- Soft Deletes: SoftDeletable concern with
deleted_atcolumn - HIPAA Compliance: HipaaLoggable concern for audit logging
- Authorization: Pundit policies
- Role Management: Rolify
Model File Structure
Every model should follow this consistent structure:
# frozen_string_literal: true
# Brief description of what this model represents and its purpose.
# Include key business rules or relationships if helpful.
class ModelName < ApplicationRecord
# == Multi-tenancy ==
acts_as_tenant(:organization)
# == Role Management (if applicable) ==
rolify # Only for User model or models with roles
# == Concerns ==
include SoftDeletable # If model supports soft deletion
include HipaaLoggable # If model contains PHI/sensitive data
include HasAddresses # If model has addresses
# Other custom concerns...
# == Constants ==
STATUSES = %w[active inactive archived].freeze
TYPES = %w[type_a type_b type_c].freeze
# == Associations ==
# belongs_to (required relationships first)
belongs_to :organization
belongs_to :user
belongs_to :optional_relation, optional: true
# has_one
has_one :profile, dependent: :destroy
# has_many (with dependent options)
has_many :items, dependent: :destroy
has_many :records, dependent: :nullify
# has_many :through
has_many :join_records, dependent: :destroy
has_many :related_models, through: :join_records
# Polymorphic associations
has_many :form_assignments, as: :assignable, dependent: :destroy
# Scoped associations for eager loading optimization
has_one :latest_entry, -> { order(created_at: :desc) }, class_name: 'Entry'
has_many :active_items, -> { active }, class_name: 'Item'
# == Nested Attributes ==
accepts_nested_attributes_for :items, allow_destroy: true, reject_if: :all_blank
accepts_nested_attributes_for :profile,
reject_if: proc { |attrs| attrs[:name].blank? }
# == Enums ==
enum :status, {
draft: 0,
active: 1,
completed: 2,
archived: 3
}, prefix: true # Creates status_draft?, status_active?, etc.
enum :category, {
type_a: 0,
type_b: 1
}, prefix: true
# == Callbacks ==
before_validation :normalize_data
before_save :set_defaults
after_create_commit :send_notifications
after_save :handle_status_change, if: -> { saved_change_to_status? }
# == Validations ==
# Presence validations
validates :name, :email, presence: true
# Format validations
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
# Inclusion validations
validates :status, inclusion: { in: STATUSES }
validates :optional_field, inclusion: { in: TYPES, allow_blank: true }
# Uniqueness validations (always scope to organization for multi-tenancy)
validates :code, uniqueness: { scope: :organization_id }
# Custom validations
validate :custom_business_rule
# == Scopes ==
scope :active, -> { where(status: :active) }
scope :recent, -> { order(created_at: :desc) }
scope :by_date, -> { order(service_date: :desc) }
scope :for_user, ->(user_id) { where(user_id: user_id) }
scope :created_after, ->(date) { where('created_at > ?', date) }
# == Ransack Configuration ==
def self.ransackable_attributes(auth_object = nil)
%w[name email status created_at]
end
def self.ransackable_associations(auth_object = nil)
%w[organization user]
end
# == Class Methods ==
def self.search(query)
where('name ILIKE ? OR email ILIKE ?', "%#{query}%", "%#{query}%")
end
# == Instance Methods ==
def full_name
"#{first_name} #{last_name}"
end
def active?
status_active? && !deleted?
end
def editable?
status_draft? && !deleted?
end
# HIPAA Audit Logging (if HipaaLoggable included)
def hipaa_log_identifier
"#{self.class.name}: #{name}"
end
private
# == Private Methods ==
def normalize_data
self.email = email&.downcase&.strip
end
def set_defaults
self.status ||= :draft
end
def custom_business_rule
# Add custom validation logic
errors.add(:base, 'Custom error message') if some_condition?
end
def send_notifications
# Enqueue job for notifications
NotificationJob.perform_later(id)
end
def handle_status_change
# React to status changes
end
endPRIMARY KEYS
This application uses NanoID primary keys (12 character lowercase alphanumeric strings) instead of UUIDs or integers.
How It Works
The NanoIdPrimaryKey concern in ApplicationRecord automatically generates NanoIDs for models with string primary keys:
# ApplicationRecord already includes this - no action needed
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
include NanoIdPrimaryKey
endMigration for NanoID Primary Key
class CreateExamples < ActiveRecord::Migration[8.0]
def change
create_table :examples, id: false do |t|
t.string :id, limit: 12, null: false, primary_key: true
# Standard columns
t.string :name, null: false
t.text :description
# Foreign keys (also NanoID strings)
t.string :organization_id, limit: 12, null: false
t.string :user_id, limit: 12, null: false
# Soft delete support
t.datetime :deleted_at
t.timestamps
end
add_index :examples, :organization_id
add_index :examples, :user_id
add_index :examples, :deleted_at
add_index :examples, [:organization_id, :name], unique: true
end
endForeign Key References
# In migrations, use string type for foreign keys
t.string :organization_id, limit: 12, null: false
t.string :user_id, limit: 12
t.string :parent_id, limit: 12 # Self-referential
# Add foreign key constraints
add_foreign_key :examples, :organizations
add_foreign_key :examples, :usersMULTI-TENANCY
Every tenant-scoped model must include acts_as_tenant.
Basic Multi-Tenancy Setup
class Client < ApplicationRecord
# REQUIRED: Scope all queries to current organization
acts_as_tenant(:organization)
# Organization association is implicit but can be explicit
# belongs_to :organization # Added automatically by acts_as_tenant
endMulti-Tenancy Rules
- Always include
acts_as_tenant(:organization)for tenant-scoped models - Scope uniqueness validations to organization:
validates :email, uniqueness: { scope: :organization_id } - Platform-level models (Organization itself, PlatformInvoice) don't use acts_as_tenant
- Cross-tenant queries use
ActsAsTenant.without_tenant:ActsAsTenant.without_tenant do Organization.all # Access all orgs end
Migration Requirements
Always include organization_id column and index:
t.string :organization_id, limit: 12, null: false
add_index :examples, :organization_idCONCERNS
SoftDeletable
Use for models that should be recoverable after deletion.
# app/models/concerns/soft_deletable.rb
module SoftDeletable
extend ActiveSupport::Concern
included do
scope :active, -> { where(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
default_scope { active }
end
def soft_delete
update(deleted_at: Time.current)
end
def restore
update(deleted_at: nil)
end
def deleted?
deleted_at.present?
end
endUsage:
class Client < ApplicationRecord
include SoftDeletable
# Migration must include:
# t.datetime :deleted_at
# add_index :clients, :deleted_at
end
# Usage
client.soft_delete # Sets deleted_at
client.deleted? # true
client.restore # Clears deleted_at
Client.deleted # Query soft-deleted records
Client.unscoped.all # All records including deletedHipaaLoggable
Required for any model containing Protected Health Information (PHI).
module HipaaLoggable
extend ActiveSupport::Concern
included do
after_commit :log_hipaa_create, on: :create
after_commit :log_hipaa_update, on: :update
after_commit :log_hipaa_destroy, on: :destroy
end
# Override for human-readable audit logs
def hipaa_log_identifier
"#{self.class.name} ##{id}"
end
# Override to specify associated client for filtering
def hipaa_associated_client
return self if is_a?(Client)
respond_to?(:client) ? client : nil
end
endUsage:
class ClinicalDocument < ApplicationRecord
include HipaaLoggable
belongs_to :chart
def hipaa_log_identifier
"Clinical Document: #{title} for #{chart.client.full_name}"
end
def hipaa_associated_client
chart&.client
end
endHasAddresses
For models with polymorphic address associations:
class Client < ApplicationRecord
include HasAddresses
# Provides:
# has_many :addresses, as: :addressable, dependent: :destroy
# accepts_nested_attributes_for :addresses, allow_destroy: true
endCreating Custom Concerns
# app/models/concerns/trackable.rb
module Trackable
extend ActiveSupport::Concern
included do
# Class-level configuration
scope :tracked, -> { where.not(tracked_at: nil) }
# Callbacks
before_save :update_tracking, if: :tracking_enabled?
end
# Class methods
class_methods do
def find_by_tracking_code(code)
find_by(tracking_code: code)
end
end
# Instance methods
def track!
update!(tracked_at: Time.current)
end
def tracked?
tracked_at.present?
end
private
def tracking_enabled?
respond_to?(:tracking_enabled) && tracking_enabled
end
def update_tracking
self.tracking_code ||= generate_tracking_code
end
def generate_tracking_code
SecureRandom.hex(8)
end
endASSOCIATIONS
belongs_to
# Required association (default in Rails 6+)
belongs_to :organization
belongs_to :user
# Optional association
belongs_to :parent, class_name: 'Category', optional: true
belongs_to :assignable, polymorphic: true, optional: true
# With custom foreign key
belongs_to :author, class_name: 'User', foreign_key: 'author_id'
belongs_to :supervisor, class_name: 'User', foreign_key: 'supervisor_id', optional: truehas_many
# Basic has_many with dependent option
has_many :items, dependent: :destroy # Delete children when parent deleted
has_many :logs, dependent: :nullify # Set FK to null when parent deleted
has_many :claims, dependent: :restrict_with_error # Prevent deletion if children exist
# has_many :through
has_many :client_therapists, dependent: :destroy
has_many :therapists, through: :client_therapists, source: :user
# Scoped associations
has_many :active_items, -> { where(status: :active) }, class_name: 'Item'
has_many :recent_logs, -> { order(created_at: :desc).limit(10) }, class_name: 'Log'
# Polymorphic
has_many :form_assignments, as: :assignable, dependent: :destroy
has_many :participants, as: :participantable, dependent: :destroyhas_one
has_one :profile, dependent: :destroy
has_one :latest_entry, -> { order(created_at: :desc) }, class_name: 'Entry'
has_one :platform_invoice, through: :platform_invoice_appointmentSelf-Referential Associations
class Category < ApplicationRecord
belongs_to :parent, class_name: 'Category', optional: true
has_many :children, class_name: 'Category', foreign_key: 'parent_id', dependent: :destroy
end
class Appointment < ApplicationRecord
belongs_to :parent_appointment, class_name: 'Appointment', optional: true
has_many :recurring_instances, class_name: 'Appointment',
foreign_key: 'parent_appointment_id', dependent: :destroy
endEager Loading for N+1 Prevention
class Client < ApplicationRecord
# Scoped associations for eager loading optimization
has_one :latest_ledger_entry, -> { order(created_at: :desc) },
class_name: 'ClientLedgerEntry'
has_many :active_payment_methods, -> { active },
class_name: 'PaymentMethod'
# Method that respects eager loading
def current_balance_cents
if association(:latest_ledger_entry).loaded?
latest_ledger_entry&.running_balance_cents || 0
else
ledger_entries.order(created_at: :desc).limit(1).pick(:running_balance_cents) || 0
end
end
end
# Controller usage
@clients = Client.includes(:latest_ledger_entry, :active_payment_methods)ENUMS
Basic Enum Definition
class Appointment < ApplicationRecord
# Integer-backed enum with explicit values (recommended)
enum :status, {
booked: 1,
cancelled: 2,
completed: 3,
no_show: 4
}, prefix: true # Creates status_booked?, status_cancelled?, etc.
enum :appointment_type, {
initial_assessment: 0,
regular_session: 1
}, prefix: true
enum :category, {
clinical: 0,
event: 1
}, prefix: true
endEnum Options
# prefix: true - methods become status_active?, status_completed?
# prefix: :custom - methods become custom_active?, custom_completed?
# suffix: true - methods become active_status?, completed_status?
enum :status, { active: 0, inactive: 1 }, prefix: true
enum :priority, { low: 0, high: 1 }, suffix: trueEnum Best Practices
- Always use explicit integer values - Prevents issues when adding new values
- Use prefix/suffix - Avoids method name collisions
- Start at 0 or 1 - Be consistent across the codebase
- Don't change existing values - Only add new ones at the end
Enum Usage in Code
appointment = Appointment.new
# Query methods
appointment.status_booked? # true/false
appointment.status_completed? # true/false
# Setter methods
appointment.status_completed! # Updates and saves
# Assignment
appointment.status = :completed # Symbol
appointment.status = 'completed' # String
# Scopes (auto-generated)
Appointment.status_booked # WHERE status = 1
Appointment.status_completed # WHERE status = 3
# Get all values
Appointment.statuses # { "booked" => 1, "cancelled" => 2, ... }VALIDATIONS
Presence Validations
validates :first_name, :last_name, :date_of_birth, presence: true
validates :email, presence: trueFormat Validations
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :phone, format: {
with: /\A\d{10,15}\z/,
message: 'must be 10-15 digits',
allow_blank: true
}Inclusion Validations
# With constant array
STATUSES = %w[active inactive archived].freeze
validates :status, inclusion: { in: STATUSES }
# Allow blank for optional fields
validates :sex, inclusion: { in: SEXES, allow_blank: true }
# With time zone validation
validates :timezone, inclusion: {
in: ActiveSupport::TimeZone.us_zones.map(&:name),
allow_blank: true
}Uniqueness Validations
# ALWAYS scope to organization for multi-tenant models
validates :email, uniqueness: { scope: :organization_id }
validates :code, uniqueness: { scope: [:organization_id, :category] }
# Case-insensitive uniqueness
validates :email, uniqueness: { scope: :organization_id, case_sensitive: false }
# Allow nil
validates :external_id, uniqueness: { scope: :organization_id }, allow_nil: trueConditional Validations
validates :service_date, presence: true, if: -> { signed? }
validates :supervisor_id, presence: true, if: :needs_supervision?
validates :title, presence: true, unless: -> { draft? }
# With method reference
validates :end_time, presence: true, if: :has_time_range?
private
def has_time_range?
start_time.present?
endCustom Validations
validate :end_time_after_start_time
validate :validate_business_rule
validate :validate_primary_contacts
private
def end_time_after_start_time
return unless start_time && end_time
errors.add(:end_time, 'must be after start time') if end_time <= start_time
end
def validate_business_rule
if some_complex_condition?
errors.add(:base, 'Descriptive error message')
end
end
def validate_primary_contacts
validate_primary_email
validate_primary_phone
end
def validate_primary_email
return unless emails.loaded?
active_emails = emails.reject(&:marked_for_destruction?)
return if active_emails.empty?
primary_emails = active_emails.select(&:primary?)
if primary_emails.empty?
errors.add(:base, 'Please select a primary email')
elsif primary_emails.size > 1
errors.add(:base, 'Only one email can be primary')
end
endSCOPES
Basic Scopes
scope :active, -> { where(status: :active) }
scope :recent, -> { order(created_at: :desc) }
scope :by_date, -> { order(service_date: :desc) }
scope :by_creation, -> { order(created_at: :desc) }Parameterized Scopes
scope :for_user, ->(user_id) { where(user_id: user_id) }
scope :for_date, ->(date) { where(start_time: date.beginning_of_day..date.end_of_day) }
scope :created_after, ->(date) { where('created_at > ?', date) }
scope :recent, ->(limit = 10) { by_creation.limit(limit) }Complex Scopes
# Joining tables
scope :with_payments, -> { joins(:payments).distinct }
scope :assigned_to_office, ->(office_id) {
joins(:user_offices).where(user_offices: { office_id: office_id }).distinct
}
# Subqueries
scope :unbilled_for_platform, ->(billing_period_end, organization_id: nil) {
org_id = organization_id || ActsAsTenant.current_tenant&.id
raise ArgumentError, 'organization_id required' unless org_id
where(category: :clinical)
.where(status: :completed)
.where('completed_at <= ?', billing_period_end.end_of_day)
.where.not(id: PlatformInvoiceAppointment.where(organization_id: org_id).select(:appointment_id))
}
# Negation scopes
scope :upcoming, -> { where('start_time > ?', Time.current) }
scope :past, -> { where('end_time < ?', Time.current) }Scope Chaining
# Scopes should be chainable
Client.active.recent(5)
Appointment.for_date(Date.today).status_booked.for_user(user.id)CALLBACKS
Callback Order
# Validation callbacks
before_validation :normalize_data
after_validation :log_validation_errors
# Save callbacks
before_save :set_defaults
before_save :update_computed_fields
after_save :handle_status_change, if: -> { saved_change_to_status? }
# Create callbacks
before_create :generate_code
after_create :send_welcome_notification
after_create_commit :enqueue_background_job
# Update callbacks
after_update :sync_external_system, if: :sync_needed?
# Destroy callbacks
before_destroy :check_dependencies
after_destroy :cleanup_external_resourcesConditional Callbacks
# With proc/lambda
after_save :handle_status_change, if: -> { saved_change_to_status? }
before_save :set_completed_at, if: -> { status_changed? && status_completed? }
# With method reference
after_create_commit :trigger_reminders, if: :should_send_reminders?
private
def should_send_reminders?
category_clinical? && status_booked?
endCallback Best Practices
- Keep callbacks simple - Complex logic belongs in services
- Use
after_commit- For external API calls and background jobs - Avoid callback chains - Hard to debug and maintain
- Be explicit about conditions - Always add
if:orunless:
# Good: Simple, focused callback
after_create_commit :send_notification
private
def send_notification
NotificationJob.perform_later(id)
rescue StandardError => e
Rails.logger.error "[#{self.class.name}##{id}] Notification failed: #{e.message}"
end
# Bad: Complex logic in callback
after_save :process_everything
def process_everything
# 50 lines of business logic - should be in a service
endINSTANCE METHODS
Computed Properties
def full_name
"#{first_name} #{last_name}"
end
def age
return nil unless date_of_birth.present?
((Date.current - date_of_birth) / 365.25).floor
end
def adult?
return false unless date_of_birth.present?
age >= 18
end
def minor?
!adult?
endState Queries
def active?
status_active? && !deleted?
end
def editable?
status_draft? && !deleted?
end
def ready_for_signing?
draft? && completion_percentage >= 100
end
def needs_attention?
status_completed? && !has_required_documents?
endBusiness Logic Methods
# Keep methods focused and small
def has_payment_method?
payment_methods.active.exists?
end
def default_payment_method
payment_methods.active.where(is_default: true).first ||
payment_methods.active.order(:created_at).first
end
def has_insurance?
insurance_policies.currently_active.exists?
end
def insurance_verified?
primary_insurance&.recently_verified? || false
endMethods with Eager Loading Awareness
# Check if association is loaded to prevent N+1 queries
def has_active_payment_method?
if association(:active_payment_methods).loaded?
active_payment_methods.any?
elsif payment_methods.loaded?
payment_methods.any?(&:active?)
else
payment_methods.active.exists?
end
end
def current_balance_cents
if association(:latest_ledger_entry).loaded?
latest_ledger_entry&.running_balance_cents || 0
elsif ledger_entries.loaded?
ledger_entries.max_by(&:created_at)&.running_balance_cents || 0
else
ledger_entries.order(created_at: :desc).limit(1).pick(:running_balance_cents) || 0
end
endCLASS METHODS
Ransack Configuration
Required for Avo admin search functionality:
def self.ransackable_attributes(auth_object = nil)
%w[first_name last_name email status created_at]
end
def self.ransackable_associations(auth_object = nil)
%w[organization user]
endSearch Methods
def self.search(query)
where('name ILIKE ? OR email ILIKE ?', "%#{query}%", "%#{query}%")
end
def self.find_by_code(code)
find_by(code: code.upcase.strip)
endSERIALIZATION
JSON Columns
class Appointment < ApplicationRecord
# For JSONB columns (PostgreSQL), no serialization needed
# Just define as jsonb in migration
# For text/string columns storing JSON
serialize :attendees, coder: JSON
serialize :settings, coder: YAML
endUsage
# JSONB column - direct access
appointment.recurrence_rules = { frequency: 'weekly', interval: 1 }
appointment.recurrence_rules['frequency'] # => 'weekly'
# Serialized column
appointment.attendees = [{ name: 'John', email: 'john@example.com' }]
appointment.save
appointment.reload.attendees # => [{ name: 'John', email: 'john@example.com' }]NESTED ATTRIBUTES
Basic Setup
class Client < ApplicationRecord
has_many :emails, dependent: :destroy
has_many :phone_numbers, dependent: :destroy
accepts_nested_attributes_for :emails,
allow_destroy: true,
reject_if: :all_blank
accepts_nested_attributes_for :phone_numbers,
allow_destroy: true,
reject_if: :all_blank
endCustom Reject Logic
accepts_nested_attributes_for :availability_rules,
allow_destroy: true,
reject_if: proc { |attrs|
# Reject if inactive and new record
if attrs['active'] == '0' && attrs['id'].blank?
true
# Reject if no time data at all
elsif attrs['start_time'].blank? && attrs['end_time'].blank?
true
else
false
end
}Validation with Nested Attributes
class Client < ApplicationRecord
has_many :emails, dependent: :destroy
accepts_nested_attributes_for :emails, allow_destroy: true
validate :validate_primary_email
private
def validate_primary_email
# Skip if not loaded (avoids N+1)
return unless emails.loaded?
active_emails = emails.reject(&:marked_for_destruction?)
return if active_emails.empty?
primary_emails = active_emails.select(&:primary?)
if primary_emails.empty?
errors.add(:base, 'Please select a primary email')
elsif primary_emails.size > 1
errors.add(:base, 'Only one email can be primary')
end
end
endSERVICE INTEGRATION
Models should delegate complex business logic to service objects.
Pattern
class ClinicalDocument < ApplicationRecord
# Simple queries stay in model
def signature_requirements
@signature_requirements ||= SignatureRequirementService.new(self).requirements
end
# Business logic delegates to service
def sign!(user)
ClinicalDocumentSignatureService.call(document: self, signer: user)
end
# Complex operations use services
def create_progress_note(author, attributes = {})
clinical_documents.create({
document_type: :progress_note,
author: author,
chart: chart,
title: "Progress Note - #{start_time.strftime('%B %d, %Y')}",
service_date: start_time.to_date,
status: :draft,
generation_method: :manual
}.merge(attributes))
end
endService Result Handling
# In controller
def create
result = CreateClientService.call(params: client_params, organization: current_organization)
if result.success?
redirect_to result.data[:client], notice: 'Client created'
else
@client = Client.new(client_params)
flash.now[:alert] = result.error
render :new
end
endHIPAA COMPLIANCE
Models Requiring HipaaLoggable
- Client
- ClinicalDocument
- Appointment
- Chart
- InsurancePolicy
- Any model with PHI
Implementation
class Client < ApplicationRecord
include HipaaLoggable
# Override for meaningful audit logs
def hipaa_log_identifier
"Client: #{full_name}"
end
# Override if model has client association
def hipaa_associated_client
self # Client IS the client
end
end
class ClinicalDocument < ApplicationRecord
include HipaaLoggable
def hipaa_log_identifier
"Clinical Document: #{title} for #{chart.client.full_name}"
end
def hipaa_associated_client
chart&.client
end
endMIGRATION PATTERNS
Standard Migration
class CreateExamples < ActiveRecord::Migration[8.0]
def change
create_table :examples, id: false do |t|
# Primary key (NanoID)
t.string :id, limit: 12, null: false, primary_key: true
# Foreign keys
t.string :organization_id, limit: 12, null: false
t.string :user_id, limit: 12, null: false
t.string :client_id, limit: 12
# Standard columns
t.string :name, null: false
t.string :email
t.text :description
t.integer :status, default: 0, null: false
t.date :service_date
t.datetime :completed_at
# JSONB for structured data
t.jsonb :metadata, default: {}
# Soft delete
t.datetime :deleted_at
t.timestamps
end
# Indexes
add_index :examples, :organization_id
add_index :examples, :user_id
add_index :examples, :client_id
add_index :examples, :status
add_index :examples, :deleted_at
add_index :examples, [:organization_id, :email], unique: true
# Foreign key constraints
add_foreign_key :examples, :organizations
add_foreign_key :examples, :users
add_foreign_key :examples, :clients
end
endAdding Columns
class AddFieldsToExamples < ActiveRecord::Migration[8.0]
def change
add_column :examples, :priority, :integer, default: 0
add_column :examples, :notes, :text
add_column :examples, :settings, :jsonb, default: {}
add_index :examples, :priority
end
endTESTING MODELS
See the rspec-test-expert skill for comprehensive model testing patterns. Key points:
require 'rails_helper'
RSpec.describe Client, type: :model do
let(:organization) { create(:organization) }
describe 'validations' do
it { should validate_presence_of(:first_name) }
it { should validate_presence_of(:last_name) }
it { should validate_inclusion_of(:status).in_array(Client::STATUSES) }
end
describe 'associations' do
it { should belong_to(:organization) }
it { should have_many(:appointments).through(:appointment_clients) }
it { should have_many(:payment_methods).dependent(:destroy) }
end
describe '#full_name' do
it 'returns concatenated first and last name' do
ActsAsTenant.with_tenant(organization) do
client = build(:client, first_name: 'John', last_name: 'Doe')
expect(client.full_name).to eq('John Doe')
end
end
end
endCOMMON PATTERNS
Status Badge Mapping
For UI display with Avo or ViewComponents:
STATUS_BADGE_MAP = {
draft: :info,
pending: :warning,
active: :success,
completed: :success,
failed: :danger,
cancelled: :secondary
}.freeze
def status_badge_color
STATUS_BADGE_MAP[status.to_sym] || :secondary
endFormatted Display Methods
def display_name
"#{first_name} #{last_name} (#{email})"
end
def formatted_date
created_at.strftime('%B %d, %Y')
end
def formatted_amount
return '-' unless amount_cents.present?
"$#{'%.2f' % (amount_cents / 100.0)}"
endSafe Navigation Chains
def primary_therapist_name
primary_therapist&.full_name || 'Unassigned'
end
def insurance_status
primary_insurance&.status || 'No insurance'
endEXECUTION STRATEGY
When creating or modifying models:
- Read existing models - Understand current patterns in the codebase
- Check for similar models - Find models with similar functionality
- Plan the structure - Associations, validations, scopes needed
- Write migration first - Ensure database schema is correct
- Implement model - Follow the structure template above
- Add concerns - SoftDeletable, HipaaLoggable as needed
- Write tests - Validations, associations, methods
- Verify multi-tenancy - Ensure proper organization scoping
ANTI-PATTERNS TO AVOID
- Fat models - Complex logic belongs in services
- Skipping multi-tenancy - Always include
acts_as_tenant - Missing soft delete - Use SoftDeletable for recoverable data
- N+1 queries - Use eager loading and scoped associations
- Validation bypasses - Never use
save(validate: false)in production code - Direct SQL - Use ActiveRecord query methods
- Callback abuse - Keep callbacks simple, use services for complex logic
- Missing HIPAA logging - Include HipaaLoggable for PHI models
Remember: Models should be clean, well-organized, and follow established patterns. When in doubt, look at existing models like Client, User, or Appointment for guidance.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

