sepa_rator 0.15.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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +117 -0
  4. data/lib/schema/pain.001.001.03.ch.02.xsd +1212 -0
  5. data/lib/schema/pain.001.001.03.xsd +921 -0
  6. data/lib/schema/pain.001.001.09.xsd +1114 -0
  7. data/lib/schema/pain.001.001.13.xsd +1251 -0
  8. data/lib/schema/pain.001.002.03.xsd +450 -0
  9. data/lib/schema/pain.001.003.03.xsd +474 -0
  10. data/lib/schema/pain.008.001.02.xsd +879 -0
  11. data/lib/schema/pain.008.001.08.xsd +1106 -0
  12. data/lib/schema/pain.008.001.12.xsd +1135 -0
  13. data/lib/schema/pain.008.002.02.xsd +597 -0
  14. data/lib/schema/pain.008.003.02.xsd +614 -0
  15. data/lib/sepa_rator/account/address.rb +71 -0
  16. data/lib/sepa_rator/account/contact_details.rb +70 -0
  17. data/lib/sepa_rator/account/creditor_account.rb +16 -0
  18. data/lib/sepa_rator/account/creditor_address.rb +5 -0
  19. data/lib/sepa_rator/account/debtor_account.rb +20 -0
  20. data/lib/sepa_rator/account/debtor_address.rb +5 -0
  21. data/lib/sepa_rator/account.rb +64 -0
  22. data/lib/sepa_rator/concerns/attribute_initializer.rb +40 -0
  23. data/lib/sepa_rator/concerns/regulatory_reporting_validator.rb +111 -0
  24. data/lib/sepa_rator/concerns/schema_validation.rb +41 -0
  25. data/lib/sepa_rator/concerns/xml_builder.rb +111 -0
  26. data/lib/sepa_rator/converter.rb +46 -0
  27. data/lib/sepa_rator/error.rb +15 -0
  28. data/lib/sepa_rator/message/credit_transfer.rb +221 -0
  29. data/lib/sepa_rator/message/direct_debit.rb +153 -0
  30. data/lib/sepa_rator/message.rb +284 -0
  31. data/lib/sepa_rator/transaction/credit_transfer_transaction.rb +178 -0
  32. data/lib/sepa_rator/transaction/direct_debit_transaction.rb +104 -0
  33. data/lib/sepa_rator/transaction.rb +114 -0
  34. data/lib/sepa_rator/validator.rb +99 -0
  35. data/lib/sepa_rator/version.rb +5 -0
  36. data/lib/sepa_rator.rb +27 -0
  37. metadata +128 -0
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SEPA
4
+ class CreditTransferTransaction < Transaction
5
+ include RegulatoryReportingValidator
6
+
7
+ attr_accessor :service_level,
8
+ :category_purpose,
9
+ :charge_bearer,
10
+ # PmtInf-level instruction for debtor agent (v09/v13, Max140Text)
11
+ :debtor_agent_instruction,
12
+ # Transaction-level instruction for debtor agent (Max140Text in v03/v09, InstrInf in v13)
13
+ :instruction_for_debtor_agent,
14
+ # ExternalDebtorAgentInstruction1Code (v13 only, 1-4 chars)
15
+ :instruction_for_debtor_agent_code,
16
+ # Array<Hash> of {code:, instruction_info:} for creditor agent instructions
17
+ :instructions_for_creditor_agent,
18
+ # Array<Hash> of {indicator:, authority:, details: [{type:, date:, country:, code:, amount:, information: []}]}
19
+ :regulatory_reportings,
20
+ # CreditTransferMandateData1 fields (v13 only)
21
+ :credit_transfer_mandate_id,
22
+ :credit_transfer_mandate_date_of_signature,
23
+ :credit_transfer_mandate_frequency,
24
+ :creditor_contact_details
25
+
26
+ CHARGE_BEARERS = %w[DEBT CRED SHAR SLEV].freeze
27
+ EPC_ONLY_SCHEMAS = %w[pain.001.002.03 pain.001.003.03].freeze
28
+ UETR_SCHEMAS = %w[pain.001.001.09 pain.001.001.13].freeze
29
+ PMTINF_INSTR_SCHEMAS = %w[pain.001.001.09 pain.001.001.13].freeze
30
+ MNDT_RLTD_INF_SCHEMAS = %w[pain.001.001.13].freeze
31
+ INSTRUCTION3_CODES = %w[CHQB HOLD PHOB TELB].freeze
32
+ FREQUENCY_CODES = %w[YEAR MNTH QURT MIAN WEEK DAIL ADHO INDA FRTN].freeze
33
+ REGULATORY_INDICATORS = RegulatoryReportingValidator::REGULATORY_INDICATORS
34
+ # EPC schemas (pain.001.002.03, pain.001.003.03) do not define these elements
35
+ INSTR_FOR_CDTR_AGT_SCHEMAS = %w[pain.001.001.03 pain.001.001.09 pain.001.001.13 pain.001.001.03.ch.02].freeze
36
+ TXN_INSTR_FOR_DBTR_AGT_SCHEMAS = %w[pain.001.001.03 pain.001.001.09 pain.001.001.13 pain.001.001.03.ch.02].freeze
37
+ REGULATORY_REPORTING_SCHEMAS = %w[pain.001.001.03 pain.001.001.09 pain.001.001.13 pain.001.001.03.ch.02].freeze
38
+
39
+ validates_inclusion_of :service_level, in: %w[SEPA URGP], allow_nil: true
40
+ validates_length_of :category_purpose, within: 1..4, allow_nil: true
41
+ validates_inclusion_of :charge_bearer, in: CHARGE_BEARERS, allow_nil: true
42
+ validates_address :creditor_address
43
+
44
+ convert :debtor_agent_instruction, :instruction_for_debtor_agent,
45
+ :credit_transfer_mandate_id, to: :text
46
+
47
+ validates_length_of :debtor_agent_instruction, within: 1..140, allow_nil: true
48
+ validates_length_of :instruction_for_debtor_agent, within: 1..140, allow_nil: true
49
+ validates_length_of :instruction_for_debtor_agent_code, within: 1..4, allow_nil: true
50
+ validates_length_of :credit_transfer_mandate_id, within: 1..35, allow_nil: true
51
+ validates_inclusion_of :credit_transfer_mandate_frequency, in: FREQUENCY_CODES, allow_nil: true
52
+
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
+ validate { |t| t.validate_requested_date_after(Date.today) }
59
+ validate :validate_instructions_for_creditor_agent
60
+ validate :validate_regulatory_reportings
61
+ validate :validate_credit_transfer_mandate_date_of_signature
62
+
63
+ def initialize(attributes = {})
64
+ super
65
+ self.service_level ||= 'SEPA' if currency == 'EUR'
66
+ end
67
+
68
+ def credit_transfer_mandate?
69
+ credit_transfer_mandate_id || credit_transfer_mandate_date_of_signature || credit_transfer_mandate_frequency
70
+ end
71
+
72
+ # Fields (uetr, bic) are already validated as nil-or-non-empty
73
+ # at add_transaction time, so a nil check is sufficient here.
74
+ def schema_compatible?(schema_name)
75
+ return false unless optional_fields_compatible?(schema_name)
76
+ return false unless instructions_for_creditor_agent_compatible?(schema_name)
77
+ return false unless regulatory_reportings_compatible?(schema_name)
78
+
79
+ case schema_name
80
+ when PAIN_001_001_03, PAIN_001_001_09, PAIN_001_001_13
81
+ iso_service_level_compatible?
82
+ when PAIN_001_002_03
83
+ bic && !bic.empty? && self.service_level == 'SEPA' && currency == 'EUR'
84
+ when PAIN_001_003_03
85
+ currency == 'EUR'
86
+ when PAIN_001_001_03_CH_02
87
+ currency == 'CHF'
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def optional_fields_compatible?(schema_name)
94
+ return false if charge_bearer && charge_bearer != 'SLEV' && EPC_ONLY_SCHEMAS.include?(schema_name)
95
+
96
+ schema_allows_field?(uetr, UETR_SCHEMAS, schema_name) &&
97
+ schema_allows_field?(agent_lei, LEI_SCHEMAS, schema_name) &&
98
+ schema_allows_field?(debtor_agent_instruction, PMTINF_INSTR_SCHEMAS, schema_name) &&
99
+ schema_allows_field?(credit_transfer_mandate?, MNDT_RLTD_INF_SCHEMAS, schema_name) &&
100
+ schema_allows_field?(instruction_for_debtor_agent_code, [PAIN_001_001_13], schema_name) &&
101
+ schema_allows_field?(instruction_for_debtor_agent, TXN_INSTR_FOR_DBTR_AGT_SCHEMAS, schema_name) &&
102
+ schema_allows_field?(regulatory_reportings&.any?, REGULATORY_REPORTING_SCHEMAS, schema_name)
103
+ end
104
+
105
+ # v13 RegulatoryReporting10 requires DbtCdtRptgInd (indicator) and supports type_proprietary.
106
+ # v03/v09 StructuredRegulatoryReporting3 uses plain-text Tp, so type_proprietary is incompatible.
107
+ def regulatory_reportings_compatible?(schema_name)
108
+ return true unless regulatory_reportings&.any?
109
+
110
+ regulatory_reportings.all? { |r| regulatory_reporting_schema_ok?(r, schema_name) }
111
+ end
112
+
113
+ def regulatory_reporting_schema_ok?(reporting, schema_name)
114
+ return false unless reporting.is_a?(Hash)
115
+ return false if schema_name == PAIN_001_001_13 && !reporting[:indicator]
116
+ return false if schema_name != PAIN_001_001_13 && type_proprietary?(reporting)
117
+
118
+ true
119
+ end
120
+
121
+ def type_proprietary?(reporting)
122
+ reporting[:details]&.any? { |d| d.is_a?(Hash) && d[:type_proprietary] }
123
+ end
124
+
125
+ def schema_allows_field?(value, allowed_schemas, schema_name)
126
+ !value || allowed_schemas.include?(schema_name)
127
+ end
128
+
129
+ def iso_service_level_compatible?
130
+ !self.service_level || self.service_level == 'URGP' || (self.service_level == 'SEPA' && currency == 'EUR')
131
+ end
132
+
133
+ def validate_instructions_for_creditor_agent
134
+ return unless instructions_for_creditor_agent
135
+
136
+ unless instructions_for_creditor_agent.is_a?(Array)
137
+ errors.add(:instructions_for_creditor_agent, 'must be an Array')
138
+ return
139
+ end
140
+
141
+ instructions_for_creditor_agent.each_with_index do |instr, i|
142
+ unless instr.is_a?(Hash) && (instr[:code] || instr[:instruction_info])
143
+ errors.add(:instructions_for_creditor_agent, "entry #{i} must have :code and/or :instruction_info")
144
+ next
145
+ end
146
+ if instr[:instruction_info]
147
+ len = instr[:instruction_info].to_s.length
148
+ errors.add(:instructions_for_creditor_agent, "entry #{i} instruction_info must be 1-140 characters") unless len.between?(1, 140)
149
+ end
150
+ end
151
+ end
152
+
153
+ def validate_credit_transfer_mandate_date_of_signature
154
+ return unless credit_transfer_mandate_date_of_signature
155
+ return if credit_transfer_mandate_date_of_signature.is_a?(Date)
156
+
157
+ errors.add(:credit_transfer_mandate_date_of_signature, 'is not a Date')
158
+ end
159
+
160
+ def instructions_for_creditor_agent_compatible?(schema_name)
161
+ return true unless instructions_for_creditor_agent&.any?
162
+ return false unless INSTR_FOR_CDTR_AGT_SCHEMAS.include?(schema_name)
163
+
164
+ instructions_for_creditor_agent.each do |instr|
165
+ code = instr[:code]
166
+ next unless code
167
+
168
+ case schema_name
169
+ when PAIN_001_001_03, PAIN_001_001_09, PAIN_001_001_03_CH_02
170
+ return false unless INSTRUCTION3_CODES.include?(code)
171
+ when PAIN_001_001_13
172
+ return false unless code.to_s.length.between?(1, 4)
173
+ end
174
+ end
175
+ true
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SEPA
4
+ class DirectDebitTransaction < Transaction
5
+ SEQUENCE_TYPES = %w[FRST OOFF RCUR FNAL RPRE].freeze
6
+ SEQUENCE_TYPES_V1 = %w[FRST OOFF RCUR FNAL].freeze
7
+ LOCAL_INSTRUMENTS = %w[CORE COR1 B2B].freeze
8
+
9
+ attr_accessor :mandate_id,
10
+ :mandate_date_of_signature,
11
+ :local_instrument,
12
+ :sequence_type,
13
+ :creditor_account,
14
+ :charge_bearer,
15
+ :original_mandate_id,
16
+ :original_debtor_account,
17
+ :same_mandate_new_debtor_agent,
18
+ :original_creditor_account,
19
+ :debtor_contact_details
20
+
21
+ CHARGE_BEARERS = %w[DEBT CRED SHAR SLEV].freeze
22
+
23
+ validates_with MandateIdentifierValidator, field_name: :mandate_id, message: 'is invalid'
24
+ validates_presence_of :mandate_date_of_signature
25
+ validates_inclusion_of :local_instrument, in: LOCAL_INSTRUMENTS
26
+ validates_inclusion_of :sequence_type, in: SEQUENCE_TYPES
27
+ 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
34
+ validate { |t| t.validate_requested_date_after(Date.today.next) }
35
+
36
+ validate do |t|
37
+ errors.add(:original_mandate_id, 'is invalid') if original_mandate_id && !original_mandate_id.to_s.match?(MandateIdentifierValidator::REGEX)
38
+
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)
45
+ end
46
+
47
+ if t.mandate_date_of_signature.is_a?(Date)
48
+ errors.add(:mandate_date_of_signature, 'is in the future') if t.mandate_date_of_signature > Date.today
49
+ else
50
+ errors.add(:mandate_date_of_signature, 'is not a Date')
51
+ end
52
+ end
53
+
54
+ def initialize(attributes = {})
55
+ super
56
+ self.local_instrument ||= 'CORE'
57
+ self.sequence_type ||= 'OOFF'
58
+
59
+ return unless local_instrument == 'COR1'
60
+
61
+ warn '[SEPA] COR1 local instrument is deprecated since November 2017. Use CORE instead.'
62
+ end
63
+
64
+ def amendment_informations?
65
+ original_mandate_id || original_debtor_account || same_mandate_new_debtor_agent || original_creditor_account
66
+ end
67
+
68
+ UETR_SCHEMAS = %w[pain.008.001.08 pain.008.001.12].freeze
69
+
70
+ # Fields (uetr, instruction_priority, bic) are already validated as nil-or-non-empty
71
+ # at add_transaction time, so a nil check is sufficient here.
72
+ def schema_compatible?(schema_name)
73
+ return false unless optional_fields_schema_compatible?(schema_name)
74
+
75
+ case schema_name
76
+ when PAIN_008_001_02
77
+ SEQUENCE_TYPES_V1.include?(sequence_type)
78
+ when PAIN_008_002_02
79
+ epc_v2_compatible?
80
+ when PAIN_008_003_02
81
+ epc_compatible? && currency == 'EUR' && SEQUENCE_TYPES_V1.include?(sequence_type)
82
+ when PAIN_008_001_08, PAIN_008_001_12
83
+ true
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def optional_fields_schema_compatible?(schema_name)
90
+ !(uetr && !UETR_SCHEMAS.include?(schema_name)) &&
91
+ !(agent_lei && !LEI_SCHEMAS.include?(schema_name)) &&
92
+ !(creditor_account&.agent_lei && !LEI_SCHEMAS.include?(schema_name))
93
+ end
94
+
95
+ def epc_v2_compatible?
96
+ epc_compatible? && bic && !bic.empty? && %w[CORE B2B].include?(local_instrument) &&
97
+ currency == 'EUR' && SEQUENCE_TYPES_V1.include?(sequence_type)
98
+ end
99
+
100
+ def epc_compatible?
101
+ !instruction_priority && (!charge_bearer || charge_bearer == 'SLEV')
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SEPA
4
+ class Transaction
5
+ include ActiveModel::Validations
6
+ include AttributeInitializer
7
+ extend Converter
8
+
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
+ # Convention SEPA: 1999-01-01 signifies "execute as soon as possible" (ASAP).
25
+ # When no specific date is requested, this sentinel value tells the bank
26
+ # to process the payment at the earliest opportunity.
27
+ DEFAULT_REQUESTED_DATE = Date.new(1999, 1, 1).freeze
28
+
29
+ attr_accessor :name,
30
+ :iban,
31
+ :bic,
32
+ :agent_lei,
33
+ :amount,
34
+ :instruction,
35
+ :reference,
36
+ :remittance_information,
37
+ :requested_date,
38
+ :batch_booking,
39
+ :currency,
40
+ :structured_remittance_information,
41
+ :structured_remittance_reference_type,
42
+ :structured_remittance_issuer,
43
+ :additional_remittance_information,
44
+ :uetr,
45
+ :instruction_priority,
46
+ :purpose_code,
47
+ :ultimate_debtor_name,
48
+ :ultimate_creditor_name
49
+
50
+ convert :name, :instruction, :reference, :remittance_information, :structured_remittance_information,
51
+ :structured_remittance_reference_type, :structured_remittance_issuer,
52
+ :purpose_code, :ultimate_debtor_name, :ultimate_creditor_name, to: :text
53
+ convert :amount, to: :decimal
54
+
55
+ validates_length_of :name, within: 1..70
56
+ validates_format_of :currency, with: /\A[A-Z]{3}\z/
57
+ validates_inclusion_of :instruction_priority, in: %w[HIGH NORM], allow_nil: true
58
+ validates_length_of :instruction, within: 1..35, allow_nil: true
59
+ validates_length_of :reference, within: 1..35, allow_nil: true
60
+ validates_length_of :remittance_information, within: 1..140, allow_nil: true
61
+ validates_length_of :structured_remittance_information, within: 1..35, allow_nil: true
62
+ validates_length_of :structured_remittance_reference_type, within: 1..4, allow_nil: true
63
+ validates_length_of :structured_remittance_issuer, within: 1..35, allow_nil: true
64
+ validates_length_of :purpose_code, within: 1..4, allow_nil: true
65
+ validates_length_of :ultimate_debtor_name, within: 1..70, allow_nil: true
66
+ validates_length_of :ultimate_creditor_name, within: 1..70, allow_nil: true
67
+ validates_numericality_of :amount, greater_than: 0, less_than_or_equal_to: 999_999_999.99
68
+ validates_presence_of :requested_date
69
+
70
+ 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
+ validates_format_of :uetr, with: UETR_REGEX, allow_nil: true
72
+ validates_inclusion_of :batch_booking, in: [true, false]
73
+ validates_with BICValidator, IBANValidator, message: 'is invalid'
74
+ validates_with LEIValidator, field_name: :agent_lei, message: 'is invalid'
75
+
76
+ validate do |t|
77
+ if t.remittance_information && (t.structured_remittance_information || t.additional_remittance_information)
78
+ t.errors.add(:base, 'remittance_information and structured remittance fields are mutually exclusive')
79
+ end
80
+
81
+ next unless t.additional_remittance_information
82
+
83
+ unless t.additional_remittance_information.is_a?(Array) && t.additional_remittance_information.length <= 3
84
+ t.errors.add(:additional_remittance_information, 'must be an Array with at most 3 items')
85
+ next
86
+ end
87
+
88
+ t.additional_remittance_information.each_with_index do |info, i|
89
+ t.errors.add(:additional_remittance_information, "entry #{i} exceeds 140 characters") if info.to_s.length > 140
90
+ end
91
+ end
92
+
93
+ def initialize(attributes = {})
94
+ super
95
+ self.requested_date ||= DEFAULT_REQUESTED_DATE
96
+ self.reference ||= 'NOTPROVIDED'
97
+ self.batch_booking = true if batch_booking.nil?
98
+ self.currency ||= 'EUR'
99
+ end
100
+
101
+ protected
102
+
103
+ # NOTE: This validation only checks that the date is not in the past.
104
+ # It does NOT validate against the TARGET2 business calendar (weekends, holidays).
105
+ # Callers should ensure the requested date falls on a TARGET2 business day.
106
+ def validate_requested_date_after(min_requested_date)
107
+ return unless requested_date.is_a?(Date)
108
+
109
+ return unless requested_date != DEFAULT_REQUESTED_DATE && requested_date < min_requested_date
110
+
111
+ errors.add(:requested_date, "must be greater or equal to #{min_requested_date}, or nil")
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SEPA
4
+ 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/
7
+
8
+ def validate(record)
9
+ field_name = options[:field_name] || :iban
10
+ value = record.public_send(field_name).to_s
11
+
12
+ return if IBANTools::IBAN.valid?(value) && value.match?(REGEX)
13
+
14
+ record.errors.add(field_name, :invalid, message: options[:message])
15
+ end
16
+ end
17
+
18
+ class BICValidator < ActiveModel::Validator
19
+ # AnyBICIdentifier (pain.001.001.03 and earlier schemas)
20
+ V03_REGEX = /\A[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}\z/
21
+ # BICFIDec2014Identifier (pain.001.001.09 / .13 and pain.008.001.08 / .12)
22
+ V09_REGEX = /\A[A-Z0-9]{4,4}[A-Z]{2,2}[A-Z0-9]{2,2}([A-Z0-9]{3,3}){0,1}\z/
23
+
24
+ REGEX = V03_REGEX
25
+
26
+ def validate(record)
27
+ field_name = options[:field_name] || :bic
28
+ value = record.public_send(field_name)
29
+
30
+ return unless value
31
+ return if value.to_s.match?(V03_REGEX) || value.to_s.match?(V09_REGEX)
32
+
33
+ record.errors.add(field_name, :invalid, message: options[:message])
34
+ end
35
+ end
36
+
37
+ class CreditorIdentifierValidator < ActiveModel::Validator
38
+ REGEX = %r{\A
39
+ [a-zA-Z]{2} # ISO country code
40
+ [0-9]{2} # Check digits
41
+ [A-Za-z0-9]{3} # Creditor business code
42
+ [A-Za-z0-9+?/:().,'-]{1,28} # National identifier
43
+ \z}x
44
+
45
+ def validate(record)
46
+ field_name = options[:field_name] || :creditor_identifier
47
+ value = record.public_send(field_name)
48
+
49
+ return if valid?(value)
50
+
51
+ record.errors.add(field_name, :invalid, message: options[:message])
52
+ end
53
+
54
+ def valid?(creditor_identifier)
55
+ return false unless creditor_identifier.to_s.match?(REGEX)
56
+
57
+ # In Germany, the identifier has to be exactly 18 chars long
58
+ return false if creditor_identifier[0..1].match?(/DE/i) && creditor_identifier.length != 18
59
+
60
+ # Verify mod-97 check digit (ISO 7064)
61
+ # Structure: CC DD BBB NNNN...
62
+ # CC = country code, DD = check digits, BBB = business code (skipped), N = national id
63
+ # Strip non-alphanumeric chars from national id before check (the spec allows +?/:().,'-
64
+ # but they are ignored for mod-97 computation)
65
+ 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
69
+ end
70
+ end
71
+
72
+ class MandateIdentifierValidator < ActiveModel::Validator
73
+ REGEX = %r{\A[A-Za-z0-9 +?/:().,'-]{1,35}\z}
74
+
75
+ def validate(record)
76
+ field_name = options[:field_name] || :mandate_id
77
+ value = record.public_send(field_name)
78
+
79
+ return if value.to_s.match?(REGEX)
80
+
81
+ record.errors.add(field_name, :invalid, message: options[:message])
82
+ end
83
+ end
84
+
85
+ class LEIValidator < ActiveModel::Validator
86
+ # LEIIdentifier (ISO 17442): 18 alphanumeric + 2 check digits
87
+ REGEX = /\A[A-Z0-9]{18}[0-9]{2}\z/
88
+
89
+ def validate(record)
90
+ field_name = options[:field_name] || :lei
91
+ value = record.public_send(field_name)
92
+
93
+ return unless value
94
+ return if value.to_s.match?(REGEX)
95
+
96
+ record.errors.add(field_name, :invalid, message: options[:message])
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SEPA
4
+ VERSION = '0.15.0'
5
+ end
data/lib/sepa_rator.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'bigdecimal'
5
+ require 'nokogiri'
6
+ require 'iban-tools'
7
+
8
+ require 'sepa_rator/error'
9
+ require 'sepa_rator/converter'
10
+ require 'sepa_rator/validator'
11
+ require 'sepa_rator/concerns/attribute_initializer'
12
+ require 'sepa_rator/concerns/schema_validation'
13
+ require 'sepa_rator/concerns/xml_builder'
14
+ require 'sepa_rator/concerns/regulatory_reporting_validator'
15
+ require 'sepa_rator/account'
16
+ require 'sepa_rator/account/debtor_account'
17
+ require 'sepa_rator/account/address'
18
+ require 'sepa_rator/account/contact_details'
19
+ require 'sepa_rator/account/debtor_address'
20
+ require 'sepa_rator/account/creditor_account'
21
+ require 'sepa_rator/account/creditor_address'
22
+ require 'sepa_rator/transaction'
23
+ require 'sepa_rator/transaction/direct_debit_transaction'
24
+ require 'sepa_rator/transaction/credit_transfer_transaction'
25
+ require 'sepa_rator/message'
26
+ require 'sepa_rator/message/direct_debit'
27
+ require 'sepa_rator/message/credit_transfer'
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sepa_rator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.15.0
5
+ platform: ruby
6
+ authors:
7
+ - Georg Leciejewski
8
+ - Georg Ledermann
9
+ - AdVitam
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 1980-01-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activemodel
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '7.0'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '9'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '7.0'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '9'
34
+ - !ruby/object:Gem::Dependency
35
+ name: iban-tools
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ - !ruby/object:Gem::Dependency
49
+ name: nokogiri
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.13'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '1.13'
62
+ description: Ruby gem for creating SEPA XML files (ISO 20022). Supports pain.001.001.03/.09/.13
63
+ and pain.008.001.02/.08/.12.
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - LICENSE.txt
69
+ - README.md
70
+ - lib/schema/pain.001.001.03.ch.02.xsd
71
+ - lib/schema/pain.001.001.03.xsd
72
+ - lib/schema/pain.001.001.09.xsd
73
+ - lib/schema/pain.001.001.13.xsd
74
+ - lib/schema/pain.001.002.03.xsd
75
+ - lib/schema/pain.001.003.03.xsd
76
+ - lib/schema/pain.008.001.02.xsd
77
+ - lib/schema/pain.008.001.08.xsd
78
+ - lib/schema/pain.008.001.12.xsd
79
+ - lib/schema/pain.008.002.02.xsd
80
+ - lib/schema/pain.008.003.02.xsd
81
+ - lib/sepa_rator.rb
82
+ - lib/sepa_rator/account.rb
83
+ - lib/sepa_rator/account/address.rb
84
+ - lib/sepa_rator/account/contact_details.rb
85
+ - lib/sepa_rator/account/creditor_account.rb
86
+ - lib/sepa_rator/account/creditor_address.rb
87
+ - lib/sepa_rator/account/debtor_account.rb
88
+ - lib/sepa_rator/account/debtor_address.rb
89
+ - lib/sepa_rator/concerns/attribute_initializer.rb
90
+ - lib/sepa_rator/concerns/regulatory_reporting_validator.rb
91
+ - lib/sepa_rator/concerns/schema_validation.rb
92
+ - lib/sepa_rator/concerns/xml_builder.rb
93
+ - lib/sepa_rator/converter.rb
94
+ - lib/sepa_rator/error.rb
95
+ - lib/sepa_rator/message.rb
96
+ - lib/sepa_rator/message/credit_transfer.rb
97
+ - lib/sepa_rator/message/direct_debit.rb
98
+ - lib/sepa_rator/transaction.rb
99
+ - lib/sepa_rator/transaction/credit_transfer_transaction.rb
100
+ - lib/sepa_rator/transaction/direct_debit_transaction.rb
101
+ - lib/sepa_rator/validator.rb
102
+ - lib/sepa_rator/version.rb
103
+ homepage: https://github.com/AdVitam/sepa_rator
104
+ licenses:
105
+ - MIT
106
+ metadata:
107
+ rubygems_mfa_required: 'true'
108
+ source_code_uri: https://github.com/AdVitam/sepa_rator
109
+ changelog_uri: https://github.com/AdVitam/sepa_rator/blob/master/CHANGELOG.md
110
+ bug_tracker_uri: https://github.com/AdVitam/sepa_rator/issues
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '3.2'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubygems_version: 3.6.9
126
+ specification_version: 4
127
+ summary: Ruby gem for creating SEPA XML files
128
+ test_files: []