sepa_king_codeur 0.12.1

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +37 -0
  5. data/CONTRIBUTING.md +38 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +297 -0
  9. data/Rakefile +6 -0
  10. data/gemfiles/Gemfile-activemodel-3.1.x +5 -0
  11. data/gemfiles/Gemfile-activemodel-3.2.x +5 -0
  12. data/gemfiles/Gemfile-activemodel-4.0.x +5 -0
  13. data/gemfiles/Gemfile-activemodel-4.1.x +5 -0
  14. data/gemfiles/Gemfile-activemodel-4.2.x +5 -0
  15. data/gemfiles/Gemfile-activemodel-5.0.x +5 -0
  16. data/gemfiles/Gemfile-activemodel-5.1.x +5 -0
  17. data/gemfiles/Gemfile-activemodel-5.2.x +5 -0
  18. data/gemfiles/Gemfile-activemodel-6.0.x +5 -0
  19. data/lib/schema/pain.001.001.03.ch.02.xsd +1212 -0
  20. data/lib/schema/pain.001.001.03.xsd +921 -0
  21. data/lib/schema/pain.001.002.03.xsd +450 -0
  22. data/lib/schema/pain.001.003.03.xsd +474 -0
  23. data/lib/schema/pain.008.001.02.xsd +879 -0
  24. data/lib/schema/pain.008.002.02.xsd +597 -0
  25. data/lib/schema/pain.008.003.02.xsd +614 -0
  26. data/lib/sepa_king.rb +19 -0
  27. data/lib/sepa_king/account.rb +19 -0
  28. data/lib/sepa_king/account/creditor_account.rb +8 -0
  29. data/lib/sepa_king/account/creditor_address.rb +37 -0
  30. data/lib/sepa_king/account/debtor_account.rb +5 -0
  31. data/lib/sepa_king/account/debtor_address.rb +37 -0
  32. data/lib/sepa_king/converter.rb +51 -0
  33. data/lib/sepa_king/error.rb +4 -0
  34. data/lib/sepa_king/message.rb +169 -0
  35. data/lib/sepa_king/message/credit_transfer.rb +137 -0
  36. data/lib/sepa_king/message/direct_debit.rb +207 -0
  37. data/lib/sepa_king/transaction.rb +56 -0
  38. data/lib/sepa_king/transaction/credit_transfer_transaction.rb +31 -0
  39. data/lib/sepa_king/transaction/direct_debit_transaction.rb +56 -0
  40. data/lib/sepa_king/validator.rb +57 -0
  41. data/lib/sepa_king/version.rb +3 -0
  42. data/sepa_king.gemspec +33 -0
  43. data/spec/account_spec.rb +42 -0
  44. data/spec/converter_spec.rb +74 -0
  45. data/spec/credit_transfer_spec.rb +520 -0
  46. data/spec/credit_transfer_transaction_spec.rb +74 -0
  47. data/spec/creditor_account_spec.rb +23 -0
  48. data/spec/debtor_account_spec.rb +12 -0
  49. data/spec/debtor_address_spec.rb +12 -0
  50. data/spec/direct_debit_spec.rb +657 -0
  51. data/spec/direct_debit_transaction_spec.rb +69 -0
  52. data/spec/examples/pain.001.001.03.ch.02.xml +172 -0
  53. data/spec/examples/pain.001.001.03.xml +89 -0
  54. data/spec/examples/pain.001.002.03.xml +89 -0
  55. data/spec/examples/pain.001.003.03.xml +89 -0
  56. data/spec/examples/pain.008.002.02.xml +134 -0
  57. data/spec/examples/pain.008.003.02.xml +134 -0
  58. data/spec/message_spec.rb +128 -0
  59. data/spec/spec_helper.rb +33 -0
  60. data/spec/support/active_model.rb +30 -0
  61. data/spec/support/custom_matcher.rb +60 -0
  62. data/spec/support/factories.rb +24 -0
  63. data/spec/support/validations.rb +27 -0
  64. data/spec/transaction_spec.rb +134 -0
  65. data/spec/validation_spec.rb +25 -0
  66. data/spec/validator_spec.rb +99 -0
  67. metadata +250 -0
@@ -0,0 +1,207 @@
1
+ # encoding: utf-8
2
+
3
+ module SEPA
4
+ class DirectDebit < Message
5
+ self.account_class = CreditorAccount
6
+ self.transaction_class = DirectDebitTransaction
7
+ self.xml_main_tag = 'CstmrDrctDbtInitn'
8
+ self.known_schemas = [ PAIN_008_003_02, PAIN_008_002_02, PAIN_008_001_02 ]
9
+
10
+ validate do |record|
11
+ if record.transactions.map(&:local_instrument).uniq.size > 1
12
+ errors.add(:base, 'CORE, COR1 AND B2B must not be mixed in one message!')
13
+ end
14
+ end
15
+
16
+ private
17
+ # Find groups of transactions which share the same values of some attributes
18
+ def transaction_group(transaction)
19
+ { requested_date: transaction.requested_date,
20
+ local_instrument: transaction.local_instrument,
21
+ sequence_type: transaction.sequence_type,
22
+ batch_booking: transaction.batch_booking,
23
+ account: transaction.creditor_account || account
24
+ }
25
+ end
26
+
27
+ def build_payment_informations(builder)
28
+ # Build a PmtInf block for every group of transactions
29
+ grouped_transactions.each do |group, transactions|
30
+ builder.PmtInf do
31
+ builder.PmtInfId(payment_information_identification(group))
32
+ builder.PmtMtd('DD')
33
+ builder.BtchBookg(group[:batch_booking])
34
+ builder.NbOfTxs(transactions.length)
35
+ builder.CtrlSum('%.2f' % amount_total(transactions))
36
+ builder.PmtTpInf do
37
+ builder.SvcLvl do
38
+ builder.Cd('SEPA')
39
+ end
40
+ builder.LclInstrm do
41
+ builder.Cd(group[:local_instrument])
42
+ end
43
+ builder.SeqTp(group[:sequence_type])
44
+ end
45
+ builder.ReqdColltnDt(group[:requested_date].iso8601)
46
+ builder.Cdtr do
47
+ builder.Nm(group[:account].name)
48
+ end
49
+ builder.CdtrAcct do
50
+ builder.Id do
51
+ builder.IBAN(group[:account].iban)
52
+ end
53
+ end
54
+ builder.CdtrAgt do
55
+ builder.FinInstnId do
56
+ if group[:account].bic
57
+ builder.BIC(group[:account].bic)
58
+ else
59
+ builder.Othr do
60
+ builder.Id('NOTPROVIDED')
61
+ end
62
+ end
63
+ end
64
+ end
65
+ builder.ChrgBr('SLEV')
66
+ builder.CdtrSchmeId do
67
+ builder.Id do
68
+ builder.PrvtId do
69
+ builder.Othr do
70
+ builder.Id(group[:account].creditor_identifier)
71
+ builder.SchmeNm do
72
+ builder.Prtry('SEPA')
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ transactions.each do |transaction|
80
+ build_transaction(builder, transaction)
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def build_amendment_informations(builder, transaction)
87
+ builder.AmdmntInd(true)
88
+ builder.AmdmntInfDtls do
89
+ if transaction.original_debtor_account
90
+ builder.OrgnlDbtrAcct do
91
+ builder.Id do
92
+ builder.IBAN(transaction.original_debtor_account)
93
+ end
94
+ end
95
+ elsif transaction.same_mandate_new_debtor_agent
96
+ builder.OrgnlDbtrAgt do
97
+ builder.FinInstnId do
98
+ builder.Othr do
99
+ builder.Id('SMNDA')
100
+ end
101
+ end
102
+ end
103
+ end
104
+ if transaction.original_creditor_account
105
+ builder.OrgnlCdtrSchmeId do
106
+ if transaction.original_creditor_account.name
107
+ builder.Nm(transaction.original_creditor_account.name)
108
+ end
109
+ if transaction.original_creditor_account.creditor_identifier
110
+ builder.Id do
111
+ builder.PrvtId do
112
+ builder.Othr do
113
+ builder.Id(transaction.original_creditor_account.creditor_identifier)
114
+ builder.SchmeNm do
115
+ builder.Prtry('SEPA')
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ def build_transaction(builder, transaction)
127
+ builder.DrctDbtTxInf do
128
+ builder.PmtId do
129
+ if transaction.instruction.present?
130
+ builder.InstrId(transaction.instruction)
131
+ end
132
+ builder.EndToEndId(transaction.reference)
133
+ end
134
+ builder.InstdAmt('%.2f' % transaction.amount, Ccy: transaction.currency)
135
+ builder.DrctDbtTx do
136
+ builder.MndtRltdInf do
137
+ builder.MndtId(transaction.mandate_id)
138
+ builder.DtOfSgntr(transaction.mandate_date_of_signature.iso8601)
139
+ build_amendment_informations(builder, transaction) if transaction.amendment_informations?
140
+ end
141
+ end
142
+ builder.DbtrAgt do
143
+ builder.FinInstnId do
144
+ if transaction.bic
145
+ builder.BIC(transaction.bic)
146
+ else
147
+ builder.Othr do
148
+ builder.Id('NOTPROVIDED')
149
+ end
150
+ end
151
+ end
152
+ end
153
+ builder.Dbtr do
154
+ builder.Nm(transaction.name)
155
+ if transaction.debtor_address
156
+ builder.PstlAdr do
157
+ # Only set the fields that are actually provided.
158
+ # StrtNm, BldgNb, PstCd, TwnNm provide a structured address
159
+ # separated into its individual fields.
160
+ # AdrLine provides the address in free format text.
161
+ # Both are currently allowed and the actual preference depends on the bank.
162
+ # Also the fields that are required legally may vary depending on the country
163
+ # or change over time.
164
+ if transaction.debtor_address.street_name
165
+ builder.StrtNm transaction.debtor_address.street_name
166
+ end
167
+
168
+ if transaction.debtor_address.building_number
169
+ builder.BldgNb transaction.debtor_address.building_number
170
+ end
171
+
172
+ if transaction.debtor_address.post_code
173
+ builder.PstCd transaction.debtor_address.post_code
174
+ end
175
+
176
+ if transaction.debtor_address.town_name
177
+ builder.TwnNm transaction.debtor_address.town_name
178
+ end
179
+
180
+ if transaction.debtor_address.country_code
181
+ builder.Ctry transaction.debtor_address.country_code
182
+ end
183
+
184
+ if transaction.debtor_address.address_line1
185
+ builder.AdrLine transaction.debtor_address.address_line1
186
+ end
187
+
188
+ if transaction.debtor_address.address_line2
189
+ builder.AdrLine transaction.debtor_address.address_line2
190
+ end
191
+ end
192
+ end
193
+ end
194
+ builder.DbtrAcct do
195
+ builder.Id do
196
+ builder.IBAN(transaction.iban)
197
+ end
198
+ end
199
+ if transaction.remittance_information
200
+ builder.RmtInf do
201
+ builder.Ustrd(transaction.remittance_information)
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+ module SEPA
3
+ class Transaction
4
+ include ActiveModel::Validations
5
+ extend Converter
6
+
7
+ DEFAULT_REQUESTED_DATE = Date.new(1999, 1, 1).freeze
8
+
9
+ attr_accessor :name,
10
+ :iban,
11
+ :bic,
12
+ :amount,
13
+ :instruction,
14
+ :reference,
15
+ :remittance_information,
16
+ :requested_date,
17
+ :batch_booking,
18
+ :currency,
19
+ :debtor_address,
20
+ :creditor_address
21
+
22
+ convert :name, :instruction, :reference, :remittance_information, to: :text
23
+ convert :amount, to: :decimal
24
+
25
+ validates_length_of :name, within: 1..70
26
+ validates_length_of :currency, is: 3
27
+ validates_length_of :instruction, within: 1..35, allow_nil: true
28
+ validates_length_of :reference, within: 1..35, allow_nil: true
29
+ validates_length_of :remittance_information, within: 1..140, allow_nil: true
30
+ validates_numericality_of :amount, greater_than: 0
31
+ validates_presence_of :requested_date
32
+ validates_inclusion_of :batch_booking, :in => [true, false]
33
+ validates_with BICValidator, IBANValidator, message: "%{value} is invalid"
34
+
35
+ def initialize(attributes = {})
36
+ attributes.each do |name, value|
37
+ send("#{name}=", value)
38
+ end
39
+
40
+ self.requested_date ||= DEFAULT_REQUESTED_DATE
41
+ self.reference ||= 'NOTPROVIDED'
42
+ self.batch_booking = true if self.batch_booking.nil?
43
+ self.currency ||= 'EUR'
44
+ end
45
+
46
+ protected
47
+
48
+ def validate_requested_date_after(min_requested_date)
49
+ return unless requested_date.is_a?(Date)
50
+
51
+ if requested_date != DEFAULT_REQUESTED_DATE && requested_date < min_requested_date
52
+ errors.add(:requested_date, "must be greater or equal to #{min_requested_date}, or nil")
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ module SEPA
3
+ class CreditTransferTransaction < Transaction
4
+ attr_accessor :service_level,
5
+ :creditor_address,
6
+ :category_purpose
7
+
8
+ validates_inclusion_of :service_level, :in => %w(SEPA URGP), :allow_nil => true
9
+ validates_length_of :category_purpose, within: 1..4, allow_nil: true
10
+
11
+ validate { |t| t.validate_requested_date_after(Date.today) }
12
+
13
+ def initialize(attributes = {})
14
+ super
15
+ self.service_level ||= 'SEPA' if self.currency == 'EUR'
16
+ end
17
+
18
+ def schema_compatible?(schema_name)
19
+ case schema_name
20
+ when PAIN_001_001_03
21
+ !self.service_level || (self.service_level == 'SEPA' && self.currency == 'EUR')
22
+ when PAIN_001_002_03
23
+ self.bic.present? && self.service_level == 'SEPA' && self.currency == 'EUR'
24
+ when PAIN_001_003_03
25
+ self.currency == 'EUR'
26
+ when PAIN_001_001_03_CH_02
27
+ self.currency == 'CHF'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: utf-8
2
+ module SEPA
3
+ class DirectDebitTransaction < Transaction
4
+ SEQUENCE_TYPES = %w(FRST OOFF RCUR FNAL)
5
+ LOCAL_INSTRUMENTS = %w(CORE COR1 B2B)
6
+
7
+ attr_accessor :mandate_id,
8
+ :mandate_date_of_signature,
9
+ :local_instrument,
10
+ :sequence_type,
11
+ :creditor_account,
12
+ :original_debtor_account,
13
+ :same_mandate_new_debtor_agent,
14
+ :original_creditor_account,
15
+ :debtor_address
16
+
17
+ validates_with MandateIdentifierValidator, field_name: :mandate_id, message: "%{value} is invalid"
18
+ validates_presence_of :mandate_date_of_signature
19
+ validates_inclusion_of :local_instrument, in: LOCAL_INSTRUMENTS
20
+ validates_inclusion_of :sequence_type, in: SEQUENCE_TYPES
21
+ validate { |t| t.validate_requested_date_after(Date.today.next) }
22
+
23
+ validate do |t|
24
+ if creditor_account
25
+ errors.add(:creditor_account, 'is not correct') unless creditor_account.valid?
26
+ end
27
+
28
+ if t.mandate_date_of_signature.is_a?(Date)
29
+ errors.add(:mandate_date_of_signature, 'is in the future') if t.mandate_date_of_signature > Date.today
30
+ else
31
+ errors.add(:mandate_date_of_signature, 'is not a Date')
32
+ end
33
+ end
34
+
35
+ def initialize(attributes = {})
36
+ super
37
+ self.local_instrument ||= 'CORE'
38
+ self.sequence_type ||= 'OOFF'
39
+ end
40
+
41
+ def amendment_informations?
42
+ original_debtor_account || same_mandate_new_debtor_agent || original_creditor_account
43
+ end
44
+
45
+ def schema_compatible?(schema_name)
46
+ case schema_name
47
+ when PAIN_008_002_02
48
+ self.bic.present? && %w(CORE B2B).include?(self.local_instrument) && self.currency == 'EUR'
49
+ when PAIN_008_003_02
50
+ self.currency == 'EUR'
51
+ when PAIN_008_001_02
52
+ true
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,57 @@
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/.freeze
7
+
8
+ def validate(record)
9
+ field_name = options[:field_name] || :iban
10
+ value = record.send(field_name).to_s
11
+
12
+ record.errors.add(field_name, :invalid, message: options[:message]) unless value.match(REGEX)
13
+ end
14
+ end
15
+
16
+ class BICValidator < ActiveModel::Validator
17
+ # AnyBICIdentifier (taken from schema)
18
+ REGEX = /\A[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}\z/.freeze
19
+
20
+ def validate(record)
21
+ field_name = options[:field_name] || :bic
22
+ value = record.send(field_name)
23
+
24
+ record.errors.add(field_name, :invalid, message: options[:message]) if value && !value.to_s.match(REGEX)
25
+ end
26
+ end
27
+
28
+ class CreditorIdentifierValidator < ActiveModel::Validator
29
+ REGEX = %r{\A[a-zA-Z]{2,2}[0-9]{2,2}([A-Za-z0-9]|[+|?|/|\-|:|(|)|.|,|']){3,3}([A-Za-z0-9]|[+|?|/|\-|:|(|)|.|,|']){1,28}\z}.freeze
30
+
31
+ def validate(record)
32
+ field_name = options[:field_name] || :creditor_identifier
33
+ value = record.send(field_name)
34
+
35
+ record.errors.add(field_name, :invalid, message: options[:message]) unless valid?(value)
36
+ end
37
+
38
+ def valid?(creditor_identifier)
39
+ if ok = creditor_identifier.to_s.match(REGEX) && creditor_identifier[0..1].match(/DE/i)
40
+ # In Germany, the identifier has to be exactly 18 chars long
41
+ ok = creditor_identifier.length == 18
42
+ end
43
+ ok
44
+ end
45
+ end
46
+
47
+ class MandateIdentifierValidator < ActiveModel::Validator
48
+ REGEX = %r{\A([A-Za-z0-9]|[+|?|/|\-|:|(|)|.|,|']){1,35}\z}.freeze
49
+
50
+ def validate(record)
51
+ field_name = options[:field_name] || :mandate_id
52
+ value = record.send(field_name)
53
+
54
+ record.errors.add(field_name, :invalid, message: options[:message]) unless value.to_s.match(REGEX)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module SEPA
2
+ VERSION = '0.12.1'
3
+ end
data/sepa_king.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'sepa_king/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'sepa_king_codeur'
7
+ s.version = SEPA::VERSION
8
+ s.authors = ['Georg Leciejewski', 'Georg Ledermann', 'Codeur']
9
+ s.email = ['gl@salesking.eu', 'mail@georg-ledermann.de', 'dev@codeur.com']
10
+ s.description = 'Implemention of pain.001.002.03 and pain.008.002.02 (ISO 20022)'
11
+ s.summary = 'Ruby gem for creating SEPA XML files'
12
+ s.homepage = 'http://github.com/codeur/sepa_king'
13
+ s.license = 'MIT'
14
+
15
+ s.files = `git ls-files`.split($/)
16
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
17
+ s.require_paths = ['lib']
18
+
19
+ s.metadata['homepage_uri'] = s.homepage
20
+ s.metadata['source_code_uri'] = 'https://github.com/codeur/sepa_king'
21
+
22
+ s.required_ruby_version = '>= 2.2'
23
+
24
+ s.add_runtime_dependency 'activemodel', '>= 3.1'
25
+ s.add_runtime_dependency 'iban-tools'
26
+ s.add_runtime_dependency 'nokogiri'
27
+
28
+ s.add_development_dependency 'bundler'
29
+ s.add_development_dependency 'coveralls'
30
+ s.add_development_dependency 'rake'
31
+ s.add_development_dependency 'rspec'
32
+ s.add_development_dependency 'simplecov'
33
+ end