sepa_king_codeur 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
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