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.
- checksums.yaml +7 -0
- data/LICENSE.txt +23 -0
- data/README.md +117 -0
- data/lib/schema/pain.001.001.03.ch.02.xsd +1212 -0
- data/lib/schema/pain.001.001.03.xsd +921 -0
- data/lib/schema/pain.001.001.09.xsd +1114 -0
- data/lib/schema/pain.001.001.13.xsd +1251 -0
- data/lib/schema/pain.001.002.03.xsd +450 -0
- data/lib/schema/pain.001.003.03.xsd +474 -0
- data/lib/schema/pain.008.001.02.xsd +879 -0
- data/lib/schema/pain.008.001.08.xsd +1106 -0
- data/lib/schema/pain.008.001.12.xsd +1135 -0
- data/lib/schema/pain.008.002.02.xsd +597 -0
- data/lib/schema/pain.008.003.02.xsd +614 -0
- data/lib/sepa_rator/account/address.rb +71 -0
- data/lib/sepa_rator/account/contact_details.rb +70 -0
- data/lib/sepa_rator/account/creditor_account.rb +16 -0
- data/lib/sepa_rator/account/creditor_address.rb +5 -0
- data/lib/sepa_rator/account/debtor_account.rb +20 -0
- data/lib/sepa_rator/account/debtor_address.rb +5 -0
- data/lib/sepa_rator/account.rb +64 -0
- data/lib/sepa_rator/concerns/attribute_initializer.rb +40 -0
- data/lib/sepa_rator/concerns/regulatory_reporting_validator.rb +111 -0
- data/lib/sepa_rator/concerns/schema_validation.rb +41 -0
- data/lib/sepa_rator/concerns/xml_builder.rb +111 -0
- data/lib/sepa_rator/converter.rb +46 -0
- data/lib/sepa_rator/error.rb +15 -0
- data/lib/sepa_rator/message/credit_transfer.rb +221 -0
- data/lib/sepa_rator/message/direct_debit.rb +153 -0
- data/lib/sepa_rator/message.rb +284 -0
- data/lib/sepa_rator/transaction/credit_transfer_transaction.rb +178 -0
- data/lib/sepa_rator/transaction/direct_debit_transaction.rb +104 -0
- data/lib/sepa_rator/transaction.rb +114 -0
- data/lib/sepa_rator/validator.rb +99 -0
- data/lib/sepa_rator/version.rb +5 -0
- data/lib/sepa_rator.rb +27 -0
- 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
|
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: []
|