Module: Einvoicing::Validators::FR

Defined in:
lib/einvoicing/validators/fr.rb

Overview

French invoice validator. Checks SIREN, SIRET, TVA (VAT) number format and Luhn checksum, plus mandatory invoice fields for French B2B compliance.

Examples:

errors = Einvoicing::Validators::FR.validate(invoice)
errors.empty? # => true if valid

Einvoicing::Validators::FR.validate!(invoice) # raises on failure

Constant Summary collapse

SIREN_RE =
/\A\d{9}\z/
SIRET_RE =
/\A\d{14}\z/
VAT_RE =

FR VAT: "FR" + 2 alphanumeric chars + 9-digit SIREN

/\AFR[A-Z0-9]{2}\d{9}\z/
INV_NUM_RE =
/\A[\w\-\/]{1,35}\z/
IBAN_RE =

IBAN: country code (2 alpha) + 2 check digits + BBAN (11-30 alphanumeric) = 15-34 total

/\A[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}\z/
BIC_RE =

BIC: 4 alpha (institution) + 2 alpha (country) + 2 alphanumeric (location) + optional 3 alphanumeric (branch)

/\A[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?\z/

Class Method Summary collapse

Class Method Details

.valid_bic?(bic) ⇒ Boolean

Validate a BIC (ISO 9362) — 8 or 11 chars.

Parameters:

  • bic (String)

Returns:

  • (Boolean)


92
93
94
# File 'lib/einvoicing/validators/fr.rb', line 92

def self.valid_bic?(bic)
  bic.to_s.match?(BIC_RE)
end

.valid_iban?(iban) ⇒ Boolean

Validate an IBAN (ISO 13616 format + mod-97 checksum).

Parameters:

  • iban (String)

Returns:

  • (Boolean)


79
80
81
82
83
84
85
86
87
# File 'lib/einvoicing/validators/fr.rb', line 79

def self.valid_iban?(iban)
  str = iban.to_s.gsub(/\s/, "").upcase
  return false unless str.match?(IBAN_RE)

  # Move first 4 chars to end, replace each letter with its numeric value (A=10..Z=35)
  rearranged = str[4..] + str[0..3]
  numeric = rearranged.chars.map { |c| c =~ /[A-Z]/ ? (c.ord - 55).to_s : c }.join
  numeric.to_i % 97 == 1
end

.valid_invoice_number?(number) ⇒ Boolean

Validate an invoice number format (alphanumeric, dashes, slashes, 1-35 chars).

Parameters:

  • number (String)

Returns:

  • (Boolean)


72
73
74
# File 'lib/einvoicing/validators/fr.rb', line 72

def self.valid_invoice_number?(number)
  number.to_s.match?(INV_NUM_RE)
end

.valid_siren?(siren) ⇒ Boolean

Validate a single SIREN number.

Parameters:

  • siren (String)

Returns:

  • (Boolean)


47
48
49
50
51
# File 'lib/einvoicing/validators/fr.rb', line 47

def self.valid_siren?(siren)
  return false unless siren.to_s.match?(SIREN_RE)

  Base.luhn_valid?(siren.to_s)
end

.valid_siret?(siret) ⇒ Boolean

Validate a single SIRET number.

Parameters:

  • siret (String)

Returns:

  • (Boolean)


56
57
58
59
60
# File 'lib/einvoicing/validators/fr.rb', line 56

def self.valid_siret?(siret)
  return false unless siret.to_s.match?(SIRET_RE)

  Base.luhn_valid?(siret.to_s)
end

.valid_vat_number?(vat) ⇒ Boolean

Validate a French VAT number format.

Parameters:

  • vat (String)

    e.g. "FR12123456789"

Returns:

  • (Boolean)


65
66
67
# File 'lib/einvoicing/validators/fr.rb', line 65

def self.valid_vat_number?(vat)
  vat.to_s.match?(VAT_RE)
end

.validate(invoice) ⇒ Array<Hash>

Returns list of error hashes ({ field:, error:, message: }); empty if valid.

Parameters:

Returns:

  • (Array<Hash>)

    list of error hashes ({ field:, error:, message: }); empty if valid



27
28
29
30
31
32
33
34
# File 'lib/einvoicing/validators/fr.rb', line 27

def self.validate(invoice)
  [
    *validate_invoice_fields(invoice),
    *validate_party(invoice.seller, :seller),
    *validate_party(invoice.buyer,  :buyer),
    *validate_lines(invoice.lines)
  ]
end

.validate!(invoice) ⇒ Object

Raises:



37
38
39
40
41
42
# File 'lib/einvoicing/validators/fr.rb', line 37

def self.validate!(invoice)
  errors = validate(invoice)
  raise ValidationError, errors.map { |e| e[:message] }.join("; ") unless errors.empty?

  true
end