Pular para o conteúdo principal

Validações Personalizadas

Além das validações inline nos campos (presence: true, uniqueness: true), o Nerdify permite criar validações customizadas usando o padrão Rails/Mongoid. Isso é útil para regras de negócio complexas que envolvem múltiplos campos ou condições dinâmicas.


Sintaxe Básica

Validação com Método

class Customer
include Nerdify::Model
orm :mongoid

# Chama o método de validação
validate :check_cpf_format

private

def check_cpf_format
return if cpf.blank?

unless cpf.match?(/^\d{3}\.\d{3}\.\d{3}-\d{2}$/)
errors.add(:cpf, "formato inválido")
end
end
end

Validações Condicionais

Usando Lambda

Execute a validação apenas quando a condição for verdadeira:

validate :check_stock_availability, if: -> { status_changed? && status == "pending" }

def check_stock_availability
cart_items.each do |item|
if item.quantity > item.available_stock
errors.add(:base, "Estoque insuficiente para #{item.name}")
end
end
end

Usando Método

validate :check_customer_presence, if: :has_services?

def has_services?
cart_items.any? { |item| item.sale_item_type == "SaleService" }
end

def check_customer_presence
errors.add(:customer_id, :blank) unless customer.present?
end

Múltiplas Condições

validate :validate_discount, if: -> {
!skip_callbacks &&
discount.present? &&
discount > 0
}

def validate_discount
max_discount = pdv_settings&.max_discount.to_f
if discount.to_f > max_discount
errors.add(:discount, "não pode exceder #{max_discount}%")
end
end

Validações em Concerns

Para organização, coloque validações em concerns dedicados:

# app/models/concerns/sale/validations.rb
module Sale::Validations
extend ActiveSupport::Concern

included do
attr_accessor :toast_error # Para mensagens amigáveis

validate :cart_items_presence, if: -> { status == "pending" }
validate :customer_required_for_services, if: -> { status == "pending" }
validate :stock_availability, if: -> { status_changed? && status == "pending" }
end

private

def cart_items_presence
errors.add(:cart_items, :presence) unless cart_items.exists?
end

def customer_required_for_services
if cart_items.where(:sale_item_type.in => %w[SaleService SalePackage]).exists?
errors.add(:customer_id, :blank) unless customer.present?
end
end

def stock_availability
items_without_stock = []

cart_items.each do |item|
next unless item.sale_product?

if item.quantity > item.available_stock
items_without_stock << {
name: item.name,
available: item.available_stock,
requested: item.quantity
}
end
end

if items_without_stock.present?
messages = items_without_stock.map { |i|
"#{i[:name]} (disponível: #{i[:available]})"
}
self.toast_error = "Estoque insuficiente: #{messages.join(', ')}"
errors.add(:base, toast_error)
end
end
end

Validações Cruzadas

Validando Campos Relacionados

validate :validate_total_matches_sale

def validate_total_matches_sale
return unless sale.present?

if total != sale.total
errors.add(:total, "não confere com o total da venda")
end
end

Validando Relacionamentos

validate :validate_cashier_status, if: -> { status == "paid" }

def validate_cashier_status
errors.add(:cashier_id, :blank) unless cashier.present?

if cashier.present? && cashier.status != "open"
errors.add(:cashier_id, "caixa não está aberto")
end
end

Validações com Uniqueness Personalizada

validate :unique_checkout_for_sale, if: -> { new_record? && sale_id.present? }

def unique_checkout_for_sale
if Checkout.where(sale_id: sale_id).exists?
errors.add(:sale_id, "já possui um checkout")
end
end

Validações de Itens Aninhados

Valide cada item de uma coleção:

validate :validate_cart_items_pets

def validate_cart_items_pets
invalid_items = []

cart_items.each do |item|
if item.sale_item.request_pet? && item.pet_id.blank?
invalid_items << item.name
end
end

if invalid_items.present?
errors.add(:base, "Vincule um pet aos itens: #{invalid_items.join(', ')}")
end
end

Padrão com before_validation

Combine before_validation com validate para preparar dados:

before_validation :normalize_phone
validate :check_phone_format

def normalize_phone
self.phone = phone.gsub(/\D/, '') if phone.present?
end

def check_phone_format
return if phone.blank?

unless phone.length.between?(10, 11)
errors.add(:phone, "deve ter 10 ou 11 dígitos")
end
end

Mensagens de Erro

Usando Símbolos (I18n)

errors.add(:field, :blank)      # Busca tradução automática
errors.add(:field, :invalid) # "não é válido"
errors.add(:field, :taken) # "já está em uso"

Usando Strings

errors.add(:discount, "não pode exceder o limite configurado")
errors.add(:base, "Existem pendências que impedem esta operação")

Mensagens para Toast

Use um atributo auxiliar para mensagens amigáveis:

attr_accessor :toast_error

validate :check_something

def check_something
if problem?
self.toast_error = "Mensagem amigável para o usuário"
errors.add(:base, toast_error)
end
end

Interrompendo Operações

Para impedir que o registro seja salvo, use throw(:abort):

before_destroy :prevent_if_has_sales

def prevent_if_has_sales
if sales.exists?
errors.add(:base, "Não é possível excluir: existem vendas vinculadas")
throw(:abort)
end
end

Exemplo Completo

# app/models/concerns/checkout/validations.rb
module Checkout::Validations
extend ActiveSupport::Concern

included do
validate :validate_cashier_presence, if: -> { status == "paid" }
validate :validate_total_with_sale, if: -> { sale.present? }
validate :validate_total_paid, if: -> { status_changed? && status == "paid" }
validate :unique_checkout_for_sale, if: -> { new_record? && sale_id.present? }
end

private

def validate_cashier_presence
errors.add(:cashier_id, :blank) unless cashier.present?
errors.add(:cashier_id, :invalid) if cashier.present? && cashier.status != "open"
end

def validate_total_with_sale
errors.add(:total, :invalid) if total != sale.total
end

def validate_total_paid
errors.add(:paid_value, :invalid) if total != paid_value
end

def unique_checkout_for_sale
errors.add(:sale_id, :uniqueness) if Checkout.where(sale_id: sale_id).exists?
end
end

Boas Práticas

Faça

  • Organize validações complexas em concerns dedicados
  • Use condições if: para evitar validações desnecessárias
  • Forneça mensagens de erro claras e acionáveis
  • Valide dados no momento correto do ciclo de vida

Evite

  • Validações que fazem muitas queries ao banco
  • Lógica de negócio complexa dentro das validações
  • Mensagens de erro genéricas como "inválido"
  • Validar campos que não foram alterados