sepa_rator 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a58a4b048d1554e4d422e8a7f0d3a054e01c9f280c026bbcba373d826b72b23
4
- data.tar.gz: db691fda7be2fb62d6d42b172708956ac12b17c46614a09c24ac4fd035dbfd20
3
+ metadata.gz: 83ba8734459eaf10832606aa2c4c894cad0fde36f221a88e4664d55ec929d1d6
4
+ data.tar.gz: 54c0ad9c0f54bcfa88b2912cd1f93b5fd2574d15581574dd5bd2760941adef10
5
5
  SHA512:
6
- metadata.gz: 9c3f27235b35022711b6115b025eb5b50694f6832d486238610144b676adf672917f1a354e2cf6a11dff98c5570a764cd27e1713cbc53e208a56c08b665bf702
7
- data.tar.gz: 2f29c643d85508b1159d630a169f3248450b36556ca4bfd84032d1efc6325d68ab9956a7d36dbbbb063a83515aaa8fd16064ed2f9b2c461fd4c3966aa5a341df
6
+ metadata.gz: 2570e0b6dc02a36accc1d6caeeeb19b064f6da7395f43a1d13af1629de401f67b94d07fcbd45dd82b5e6bc473860ede0c8a6e41f811eca5e66f1fda15398444c
7
+ data.tar.gz: eb2e7d4a6b6809f4a4eeec006de9816773407845c3a74a002fe082d7fb4190052389ea007b7357bf65651638d83c9cbafbc15135faba6decf145de16d0f48558
@@ -2,8 +2,7 @@
2
2
 
3
3
  module SEPA
4
4
  class Address
5
- include ActiveModel::Validations
6
- include AttributeInitializer
5
+ include ActiveModel::Model
7
6
  extend Converter
8
7
 
9
8
  # PostalAddress6 fields (all schemas)
@@ -2,8 +2,7 @@
2
2
 
3
3
  module SEPA
4
4
  class ContactDetails
5
- include ActiveModel::Validations
6
- include AttributeInitializer
5
+ include ActiveModel::Model
7
6
  extend Converter
8
7
 
9
8
  NAME_PREFIXES = %w[DOCT MADM MISS MIST MIKS].freeze
@@ -2,8 +2,7 @@
2
2
 
3
3
  module SEPA
4
4
  class Account
5
- include ActiveModel::Validations
6
- include AttributeInitializer
5
+ include ActiveModel::Model
7
6
  extend Converter
8
7
 
9
8
  attr_accessor :name, :iban, :bic, :address, :agent_lei, :contact_details
@@ -11,28 +10,10 @@ module SEPA
11
10
  convert :name, to: :text
12
11
 
13
12
  validates_length_of :name, within: 1..70
14
- validates_with BICValidator, IBANValidator, message: 'is invalid'
13
+ validates_with BICValidator, message: 'is invalid'
14
+ validates_with IBANValidator
15
15
  validates_with LEIValidator, field_name: :agent_lei, message: 'is invalid'
16
-
17
- validate do |record|
18
- next unless record.address
19
-
20
- unless record.address.valid?
21
- record.address.errors.each do |error|
22
- record.errors.add(:address, error.full_message)
23
- end
24
- end
25
- end
26
-
27
- validate do |record|
28
- next unless record.contact_details
29
-
30
- unless record.contact_details.valid?
31
- record.contact_details.errors.each do |error|
32
- record.errors.add(:contact_details, error.full_message)
33
- end
34
- end
35
- end
16
+ validates :address, :contact_details, nested_model: true, allow_nil: true
36
17
 
37
18
  def initiating_party_id(builder, schema_name); end
38
19
 
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+
3
5
  module SEPA
4
6
  module SchemaValidation
7
+ extend ActiveSupport::Concern
8
+
5
9
  SCHEMA_DIR = File.expand_path('../../schema', __dir__).freeze
6
10
  SCHEMA_CACHE_MUTEX = Mutex.new
7
11
 
8
- def self.included(base)
9
- base.extend ClassMethods
10
- end
11
-
12
- module ClassMethods
12
+ class_methods do
13
13
  def schema_cache
14
14
  @schema_cache ||= {}
15
15
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Validates nested ActiveModel objects and propagates their errors.
4
+ # Defined at root level so ActiveModel's const_get lookup finds it from any namespace.
5
+ # Usage: validates :address, :contact_details, nested_model: true
6
+ class NestedModelValidator < ActiveModel::EachValidator
7
+ def validate_each(record, attribute, value)
8
+ return unless value
9
+ return if value.valid?
10
+
11
+ value.errors.each { |error| record.errors.add(attribute, error.full_message) }
12
+ end
13
+ end
@@ -21,7 +21,8 @@ module SEPA
21
21
  :credit_transfer_mandate_id,
22
22
  :credit_transfer_mandate_date_of_signature,
23
23
  :credit_transfer_mandate_frequency,
24
- :creditor_contact_details
24
+ :creditor_contact_details,
25
+ :creditor_address
25
26
 
26
27
  CHARGE_BEARERS = %w[DEBT CRED SHAR SLEV].freeze
27
28
  EPC_ONLY_SCHEMAS = %w[pain.001.002.03 pain.001.003.03].freeze
@@ -39,7 +40,7 @@ module SEPA
39
40
  validates_inclusion_of :service_level, in: %w[SEPA URGP], allow_nil: true
40
41
  validates_length_of :category_purpose, within: 1..4, allow_nil: true
41
42
  validates_inclusion_of :charge_bearer, in: CHARGE_BEARERS, allow_nil: true
42
- validates_address :creditor_address
43
+ validates :creditor_address, :creditor_contact_details, nested_model: true, allow_nil: true
43
44
 
44
45
  convert :debtor_agent_instruction, :instruction_for_debtor_agent,
45
46
  :credit_transfer_mandate_id, to: :text
@@ -50,11 +51,6 @@ module SEPA
50
51
  validates_length_of :credit_transfer_mandate_id, within: 1..35, allow_nil: true
51
52
  validates_inclusion_of :credit_transfer_mandate_frequency, in: FREQUENCY_CODES, allow_nil: true
52
53
 
53
- validate do |t|
54
- next unless t.creditor_contact_details && !t.creditor_contact_details.valid?
55
-
56
- t.creditor_contact_details.errors.each { |error| t.errors.add(:creditor_contact_details, error.full_message) }
57
- end
58
54
  validate { |t| t.validate_requested_date_after(Date.today) }
59
55
  validate :validate_instructions_for_creditor_agent
60
56
  validate :validate_regulatory_reportings
@@ -16,7 +16,8 @@ module SEPA
16
16
  :original_debtor_account,
17
17
  :same_mandate_new_debtor_agent,
18
18
  :original_creditor_account,
19
- :debtor_contact_details
19
+ :debtor_contact_details,
20
+ :debtor_address
20
21
 
21
22
  CHARGE_BEARERS = %w[DEBT CRED SHAR SLEV].freeze
22
23
 
@@ -25,23 +26,14 @@ module SEPA
25
26
  validates_inclusion_of :local_instrument, in: LOCAL_INSTRUMENTS
26
27
  validates_inclusion_of :sequence_type, in: SEQUENCE_TYPES
27
28
  validates_inclusion_of :charge_bearer, in: CHARGE_BEARERS, allow_nil: true
28
- validates_address :debtor_address
29
- validate do |t|
30
- next unless t.debtor_contact_details && !t.debtor_contact_details.valid?
31
-
32
- t.debtor_contact_details.errors.each { |error| t.errors.add(:debtor_contact_details, error.full_message) }
33
- end
29
+ validates :debtor_address, :debtor_contact_details, :creditor_account, nested_model: true, allow_nil: true
34
30
  validate { |t| t.validate_requested_date_after(Date.today.next) }
35
31
 
36
32
  validate do |t|
37
33
  errors.add(:original_mandate_id, 'is invalid') if original_mandate_id && !original_mandate_id.to_s.match?(MandateIdentifierValidator::REGEX)
38
34
 
39
- errors.add(:creditor_account, 'is not correct') if creditor_account && !creditor_account.valid?
40
-
41
- if original_debtor_account && !original_debtor_account.to_s.empty?
42
- iban_str = original_debtor_account.to_s
43
- errors.add(:original_debtor_account, 'is not a valid IBAN') unless
44
- IBANTools::IBAN.valid?(iban_str) && iban_str.match?(IBANValidator::REGEX)
35
+ if original_debtor_account && !original_debtor_account.to_s.empty? && !IBANValidator.valid_iban?(original_debtor_account)
36
+ errors.add(:original_debtor_account, 'is not a valid IBAN')
45
37
  end
46
38
 
47
39
  if t.mandate_date_of_signature.is_a?(Date)
@@ -2,25 +2,9 @@
2
2
 
3
3
  module SEPA
4
4
  class Transaction
5
- include ActiveModel::Validations
6
- include AttributeInitializer
5
+ include ActiveModel::Model
7
6
  extend Converter
8
7
 
9
- # DSL to declare and validate address fields on subclasses (ISP-compliant).
10
- # Each subclass declares only the address it actually uses.
11
- def self.validates_address(*fields)
12
- fields.each do |field|
13
- attr_accessor field
14
-
15
- validate do |t|
16
- address = t.public_send(field)
17
- next unless address && !address.valid?
18
-
19
- address.errors.each { |error| t.errors.add(field, error.full_message) }
20
- end
21
- end
22
- end
23
-
24
8
  # Convention SEPA: 1999-01-01 signifies "execute as soon as possible" (ASAP).
25
9
  # When no specific date is requested, this sentinel value tells the bank
26
10
  # to process the payment at the earliest opportunity.
@@ -70,7 +54,8 @@ module SEPA
70
54
  UETR_REGEX = /\A[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}\z/
71
55
  validates_format_of :uetr, with: UETR_REGEX, allow_nil: true
72
56
  validates_inclusion_of :batch_booking, in: [true, false]
73
- validates_with BICValidator, IBANValidator, message: 'is invalid'
57
+ validates_with BICValidator, message: 'is invalid'
58
+ validates_with IBANValidator
74
59
  validates_with LEIValidator, field_name: :agent_lei, message: 'is invalid'
75
60
 
76
61
  validate do |t|
@@ -1,17 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SEPA
4
+ # ISO 7064 Mod 97-10 checksum used by IBAN, LEI, and Creditor Identifier
5
+ def self.mod97_valid?(alphanumeric_string)
6
+ numeric = alphanumeric_string.gsub(/[A-Z]/i) { |c| c.upcase.ord - 55 }
7
+ numeric.to_i % 97 == 1
8
+ end
9
+
4
10
  class IBANValidator < ActiveModel::Validator
5
- # IBAN2007Identifier (taken from schema)
6
- REGEX = /\A[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}\z/
11
+ def self.valid_iban?(value)
12
+ iban = Ibandit::IBAN.new(value.to_s)
13
+ iban.valid? && value.to_s == iban.iban
14
+ end
7
15
 
8
16
  def validate(record)
9
17
  field_name = options[:field_name] || :iban
10
18
  value = record.public_send(field_name).to_s
11
19
 
12
- return if IBANTools::IBAN.valid?(value) && value.match?(REGEX)
20
+ iban = Ibandit::IBAN.new(value)
21
+ unless iban.valid?
22
+ record.errors.add(field_name, :invalid, message: options[:message] || iban_error_message(iban))
23
+ return
24
+ end
25
+ return if value == iban.iban
13
26
 
14
- record.errors.add(field_name, :invalid, message: options[:message])
27
+ record.errors.add(field_name, :invalid,
28
+ message: options[:message] || 'is invalid (must be uppercase with no spaces)')
29
+ end
30
+
31
+ private
32
+
33
+ def iban_error_message(iban)
34
+ details = iban.errors.map { |key, msg| "#{key} #{msg}" }.join(', ')
35
+ details.empty? ? 'is invalid' : "is invalid (#{details})"
15
36
  end
16
37
  end
17
38
 
@@ -63,9 +84,7 @@ module SEPA
63
84
  # Strip non-alphanumeric chars from national id before check (the spec allows +?/:().,'-
64
85
  # but they are ignored for mod-97 computation)
65
86
  check_base = creditor_identifier[0..3] + creditor_identifier[7..].gsub(/[^A-Za-z0-9]/, '')
66
- rearranged = check_base[4..] + check_base[0..3]
67
- numeric = rearranged.gsub(/[A-Z]/i) { |c| c.upcase.ord - 55 }
68
- numeric.to_i % 97 == 1
87
+ SEPA.mod97_valid?(check_base[4..] + check_base[0..3])
69
88
  end
70
89
  end
71
90
 
@@ -91,9 +110,17 @@ module SEPA
91
110
  value = record.public_send(field_name)
92
111
 
93
112
  return unless value
94
- return if value.to_s.match?(REGEX)
113
+ return if valid_lei?(value.to_s)
95
114
 
96
115
  record.errors.add(field_name, :invalid, message: options[:message])
97
116
  end
117
+
118
+ private
119
+
120
+ def valid_lei?(value)
121
+ return false unless value.match?(REGEX)
122
+
123
+ SEPA.mod97_valid?(value)
124
+ end
98
125
  end
99
126
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SEPA
4
- VERSION = '0.15.0'
4
+ VERSION = '0.16.0'
5
5
  end
data/lib/sepa_rator.rb CHANGED
@@ -3,12 +3,12 @@
3
3
  require 'active_model'
4
4
  require 'bigdecimal'
5
5
  require 'nokogiri'
6
- require 'iban-tools'
6
+ require 'ibandit'
7
7
 
8
8
  require 'sepa_rator/error'
9
9
  require 'sepa_rator/converter'
10
10
  require 'sepa_rator/validator'
11
- require 'sepa_rator/concerns/attribute_initializer'
11
+ require 'sepa_rator/nested_model_validator'
12
12
  require 'sepa_rator/concerns/schema_validation'
13
13
  require 'sepa_rator/concerns/xml_builder'
14
14
  require 'sepa_rator/concerns/regulatory_reporting_validator'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sepa_rator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Georg Leciejewski
@@ -32,19 +32,19 @@ dependencies:
32
32
  - !ruby/object:Gem::Version
33
33
  version: '9'
34
34
  - !ruby/object:Gem::Dependency
35
- name: iban-tools
35
+ name: ibandit
36
36
  requirement: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '1.0'
41
41
  type: :runtime
42
42
  prerelease: false
43
43
  version_requirements: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '1.0'
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: nokogiri
50
50
  requirement: !ruby/object:Gem::Requirement
@@ -86,7 +86,6 @@ files:
86
86
  - lib/sepa_rator/account/creditor_address.rb
87
87
  - lib/sepa_rator/account/debtor_account.rb
88
88
  - lib/sepa_rator/account/debtor_address.rb
89
- - lib/sepa_rator/concerns/attribute_initializer.rb
90
89
  - lib/sepa_rator/concerns/regulatory_reporting_validator.rb
91
90
  - lib/sepa_rator/concerns/schema_validation.rb
92
91
  - lib/sepa_rator/concerns/xml_builder.rb
@@ -95,6 +94,7 @@ files:
95
94
  - lib/sepa_rator/message.rb
96
95
  - lib/sepa_rator/message/credit_transfer.rb
97
96
  - lib/sepa_rator/message/direct_debit.rb
97
+ - lib/sepa_rator/nested_model_validator.rb
98
98
  - lib/sepa_rator/transaction.rb
99
99
  - lib/sepa_rator/transaction/credit_transfer_transaction.rb
100
100
  - lib/sepa_rator/transaction/direct_debit_transaction.rb
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SEPA
4
- module AttributeInitializer
5
- def self.included(base)
6
- base.include ActiveModel::AttributeAssignment
7
- base.extend ClassMethods
8
- end
9
-
10
- module ClassMethods
11
- def permitted_attributes
12
- @permitted_attributes ||= begin
13
- own_setters = instance_methods(false)
14
- .select { |m| m.to_s.end_with?('=') }
15
- .to_set { |m| m.to_s.chomp('=') }
16
-
17
- if superclass.respond_to?(:permitted_attributes)
18
- own_setters | superclass.permitted_attributes
19
- else
20
- own_setters
21
- end
22
- end.freeze
23
- end
24
- end
25
-
26
- def initialize(attributes = {})
27
- assign_attributes(attributes)
28
- rescue ActiveModel::UnknownAttributeError => e
29
- raise ArgumentError, "Unknown attribute: #{e.attribute}"
30
- end
31
-
32
- private
33
-
34
- def _assign_attribute(key, value)
35
- raise ArgumentError, "Unknown attribute: #{key}" unless self.class.permitted_attributes.include?(key.to_s)
36
-
37
- public_send("#{key}=", value)
38
- end
39
- end
40
- end