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")