# Validates as EU VAT number. Checks the value supplied against the picture # for the appropriate country. # Constants representing the ISO country codes for the EU member states # and the RegExp pictures of their VAT numbers. Derived from the UK HMRC # guide "EC Country Codes and customer VAT Number formats" module EU MEMBER_STATES_COUNTRY_CODES = %w(AT BE BG CY CZ DE DK EE EL ES EU FI FR GB HU IE IT LT LU LV MT NL PL PT RO SE SI SK).freeze MEMBER_STATES_VAT_PICTURES = { "AT" => /\AU\d{8}\Z/, "BE" => /\A0\d{9}\Z/, "BG" => /\A\d{9,10}\Z/, "CY" => /\A\d{8}[A-Z]\Z/, "CZ" => /\A\d{8,10}\Z/, "DE" => /\A\d{9}\Z/, "DK" => /\A(\d\d ?){4}\Z/, "EE" => /\A\d{9}\Z/, "EL" => /\A\d{9}\Z/, "ES" => /\A[A-Z\d]\d{7}[A-Z\d]\Z/, "EU" => /\A\d{9}\Z/, "FI" => /\A\d{8}\Z/, "FR" => /\A[A-HJ-NP-Z\d]{2} ?\d{9}\Z/, "GB" => /\AGD\d{3}\Z|\AHA\d{3}\Z|\A\d{3} ?\d{4} ?\d\d( ?\d{3})?\Z/, "HU" => /\A\d{8}\Z/, "IE" => /\A[7-9][A-Z\d\+\*]\d{5}[A-W]\Z/, "IT" => /\A\d{11}\Z/, "LT" => /\A\d{9}\Z|\A\d{12}\Z/, "LU" => /\A\d{8}\Z/, "LV" => /\A\d{11}\Z/, "MT" => /\A\d{8}\Z/, "NL" => /\A\d{9}B\d\d\Z/, "PL" => /\A\d{10}\Z/, "PT" => /\A\d{9}\Z/, "RO" => /\A\d{2,10}\Z/, "SE" => /\A\d{12}\Z/, "SI" => /\A\d{8}\Z/, "SK" => /\A\d{10}\Z/ }.freeze end module ActiveRecord module Validations module ClassMethods # Validates whether the value of the specified attribute is in # the correct form by matching it against the VAT picture for the # appropriate country. # # class Person < ActiveRecord::Base # validates_as_eu_vat_number :identifier, :with => :country_code # validates_as_eu_vat_number :full_vat_number # end # # # Configuration options: # * message - A custom error message (default is: "is invalid") # * with - Either a symbol referencing a field with the country code, or the code itself as a String. # * on Specifies when this validation is active (default is :save, other options :create, :update) # * allow_nil - Skip validation if attribute is nil. # * allow_blank - Skip validation if attribute is blank. # * if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The # method, proc or string should return or evaluate to a true or false value. # * unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). The # method, proc or string should return or evaluate to a true or false value. def validates_as_eu_vat_number(*attr_names) configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } configuration.update(attr_names.extract_options!) validates_each(attr_names, configuration) do |record, attr_name, value| value = value.to_s.dup country_reference = configuration[:with] if country_reference eu_country_code = record.send(country_reference) if country_reference.is_a?(Symbol) eu_country_code = country_reference if country_reference.is_a?(String) end eu_country_code ||= value.slice!(0..1) eu_country_code.upcase! unless EU::MEMBER_STATES_COUNTRY_CODES.include?(eu_country_code) && value =~ EU::MEMBER_STATES_VAT_PICTURES[eu_country_code] record.errors.add(attr_name, configuration[:message]) end end end end end end