sepa_king_extended 0.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +30 -0
  5. data/CONTRIBUTING.md +38 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +267 -0
  9. data/Rakefile +6 -0
  10. data/gemfiles/Gemfile-activemodel-3.0.x +5 -0
  11. data/gemfiles/Gemfile-activemodel-3.1.x +5 -0
  12. data/gemfiles/Gemfile-activemodel-3.2.x +5 -0
  13. data/gemfiles/Gemfile-activemodel-4.0.x +5 -0
  14. data/gemfiles/Gemfile-activemodel-4.1.x +5 -0
  15. data/gemfiles/Gemfile-activemodel-4.2.x +5 -0
  16. data/gemfiles/Gemfile-activemodel-5.0.x +5 -0
  17. data/gemfiles/Gemfile-activemodel-5.1.x +5 -0
  18. data/lib/schema/pain.001.001.03.xsd +921 -0
  19. data/lib/schema/pain.001.002.03.xsd +450 -0
  20. data/lib/schema/pain.001.003.03.xsd +474 -0
  21. data/lib/schema/pain.008.001.02.xsd +879 -0
  22. data/lib/schema/pain.008.002.02.xsd +597 -0
  23. data/lib/schema/pain.008.003.02.xsd +614 -0
  24. data/lib/sepa_king/account/creditor_account.rb +8 -0
  25. data/lib/sepa_king/account/debtor_account.rb +5 -0
  26. data/lib/sepa_king/account.rb +19 -0
  27. data/lib/sepa_king/converter.rb +51 -0
  28. data/lib/sepa_king/message/credit_transfer.rb +99 -0
  29. data/lib/sepa_king/message/direct_debit.rb +151 -0
  30. data/lib/sepa_king/message.rb +135 -0
  31. data/lib/sepa_king/transaction/credit_transfer_transaction.rb +30 -0
  32. data/lib/sepa_king/transaction/direct_debit_transaction.rb +45 -0
  33. data/lib/sepa_king/transaction.rb +44 -0
  34. data/lib/sepa_king/validator.rb +68 -0
  35. data/lib/sepa_king/version.rb +3 -0
  36. data/lib/sepa_king.rb +16 -0
  37. data/sepa_king_extended.gemspec +32 -0
  38. data/spec/account_spec.rb +42 -0
  39. data/spec/converter_spec.rb +74 -0
  40. data/spec/credit_transfer_spec.rb +364 -0
  41. data/spec/credit_transfer_transaction_spec.rb +58 -0
  42. data/spec/creditor_account_spec.rb +23 -0
  43. data/spec/debtor_account_spec.rb +12 -0
  44. data/spec/direct_debit_spec.rb +469 -0
  45. data/spec/direct_debit_transaction_spec.rb +69 -0
  46. data/spec/examples/pain.001.001.03.xml +89 -0
  47. data/spec/examples/pain.001.002.03.xml +89 -0
  48. data/spec/examples/pain.001.003.03.xml +89 -0
  49. data/spec/examples/pain.008.002.02.xml +134 -0
  50. data/spec/examples/pain.008.003.02.xml +134 -0
  51. data/spec/message_spec.rb +88 -0
  52. data/spec/spec_helper.rb +33 -0
  53. data/spec/support/active_model.rb +30 -0
  54. data/spec/support/custom_matcher.rb +60 -0
  55. data/spec/support/factories.rb +24 -0
  56. data/spec/support/validations.rb +27 -0
  57. data/spec/transaction_spec.rb +88 -0
  58. data/spec/validation_spec.rb +25 -0
  59. data/spec/validator_spec.rb +99 -0
  60. metadata +254 -0
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+ module SEPA
3
+ module Converter
4
+ def convert(*attributes, options)
5
+ include InstanceMethods
6
+
7
+ method_name = "convert_#{options[:to]}"
8
+ raise ArgumentError.new("Converter '#{options[:to]}' does not exist!") unless InstanceMethods.method_defined?(method_name)
9
+
10
+ attributes.each do |attribute|
11
+ define_method "#{attribute}=" do |value|
12
+ instance_variable_set("@#{attribute}", send(method_name, value))
13
+ end
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def convert_text(value)
19
+ return unless value
20
+
21
+ value.to_s.
22
+ # Replace some special characters described as "Best practices" in Chapter 6.2 of this document:
23
+ # http://www.europeanpaymentscouncil.eu/index.cfm/knowledge-bank/epc-documents/sepa-requirements-for-an-extended-character-set-unicode-subset-best-practices/
24
+ gsub('€','E').
25
+ gsub('@','(at)').
26
+ gsub('_','-').
27
+
28
+ # Replace linebreaks by spaces
29
+ gsub(/\n+/,' ').
30
+
31
+ # Remove all invalid characters
32
+ gsub(/[^a-zA-Z0-9ÄÖÜäöüß&*$%\ \'\:\?\,\-\(\+\.\)\/]/, '').
33
+
34
+ # Remove leading and trailing spaces
35
+ strip
36
+ end
37
+
38
+ def convert_decimal(value)
39
+ return unless value
40
+ value = begin
41
+ BigDecimal(value.to_s)
42
+ rescue ArgumentError
43
+ end
44
+
45
+ if value && value.finite? && value > 0
46
+ value.round(2)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,99 @@
1
+ # encoding: utf-8
2
+
3
+ module SEPA
4
+ class CreditTransfer < Message
5
+ self.account_class = DebtorAccount
6
+ self.transaction_class = CreditTransferTransaction
7
+ self.xml_main_tag = 'CstmrCdtTrfInitn'
8
+ self.known_schemas = [ PAIN_001_003_03, PAIN_001_002_03, PAIN_001_001_03 ]
9
+
10
+ private
11
+ # Find groups of transactions which share the same values of some attributes
12
+ def transaction_group(transaction)
13
+ { requested_date: transaction.requested_date,
14
+ batch_booking: transaction.batch_booking,
15
+ service_level: transaction.service_level
16
+ }
17
+ end
18
+
19
+ def build_payment_informations(builder)
20
+ # Build a PmtInf block for every group of transactions
21
+ grouped_transactions.each do |group, transactions|
22
+ # All transactions with the same requested_date are placed into the same PmtInf block
23
+ builder.PmtInf do
24
+ builder.PmtInfId(payment_information_identification(group))
25
+ builder.PmtMtd('TRF')
26
+ builder.BtchBookg(group[:batch_booking])
27
+ builder.NbOfTxs(transactions.length)
28
+ builder.CtrlSum('%.2f' % amount_total(transactions))
29
+ unless group[:service_level].blank?
30
+ builder.PmtTpInf do
31
+ builder.SvcLvl do
32
+ builder.Cd(group[:service_level])
33
+ end
34
+ end
35
+ end
36
+ builder.ReqdExctnDt(group[:requested_date].iso8601)
37
+ builder.Dbtr do
38
+ builder.Nm(account.name)
39
+ end
40
+ builder.DbtrAcct do
41
+ builder.Id do
42
+ builder.IBAN(account.iban)
43
+ end
44
+ end
45
+ builder.DbtrAgt do
46
+ builder.FinInstnId do
47
+ if account.bic
48
+ builder.BIC(account.bic)
49
+ else
50
+ builder.Othr do
51
+ builder.Id('NOTPROVIDED')
52
+ end
53
+ end
54
+ end
55
+ end
56
+ builder.ChrgBr('SLEV')
57
+
58
+ transactions.each do |transaction|
59
+ build_transaction(builder, transaction)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def build_transaction(builder, transaction)
66
+ builder.CdtTrfTxInf do
67
+ builder.PmtId do
68
+ if transaction.instruction.present?
69
+ builder.InstrId(transaction.instruction)
70
+ end
71
+ builder.EndToEndId(transaction.reference)
72
+ end
73
+ builder.Amt do
74
+ builder.InstdAmt('%.2f' % transaction.amount, Ccy: transaction.currency)
75
+ end
76
+ if transaction.bic
77
+ builder.CdtrAgt do
78
+ builder.FinInstnId do
79
+ builder.BIC(transaction.bic)
80
+ end
81
+ end
82
+ end
83
+ builder.Cdtr do
84
+ builder.Nm(transaction.name)
85
+ end
86
+ builder.CdtrAcct do
87
+ builder.Id do
88
+ builder.IBAN(transaction.iban)
89
+ end
90
+ end
91
+ if transaction.remittance_information
92
+ builder.RmtInf do
93
+ builder.Ustrd(transaction.remittance_information)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,151 @@
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
+ return unless transaction.original_debtor_account || transaction.same_mandate_new_debtor_agent
88
+ builder.AmdmntInd(true)
89
+ builder.AmdmntInfDtls do
90
+ if transaction.original_debtor_account
91
+ builder.OrgnlDbtrAcct do
92
+ builder.Id do
93
+ builder.IBAN(transaction.original_debtor_account)
94
+ end
95
+ end
96
+ else
97
+ builder.OrgnlDbtrAgt do
98
+ builder.FinInstnId do
99
+ builder.Othr do
100
+ builder.Id('SMNDA')
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def build_transaction(builder, transaction)
109
+ builder.DrctDbtTxInf do
110
+ builder.PmtId do
111
+ if transaction.instruction.present?
112
+ builder.InstrId(transaction.instruction)
113
+ end
114
+ builder.EndToEndId(transaction.reference)
115
+ end
116
+ builder.InstdAmt('%.2f' % transaction.amount, Ccy: transaction.currency)
117
+ builder.DrctDbtTx do
118
+ builder.MndtRltdInf do
119
+ builder.MndtId(transaction.mandate_id)
120
+ builder.DtOfSgntr(transaction.mandate_date_of_signature.iso8601)
121
+ build_amendment_informations(builder, transaction)
122
+ end
123
+ end
124
+ builder.DbtrAgt do
125
+ builder.FinInstnId do
126
+ if transaction.bic
127
+ builder.BIC(transaction.bic)
128
+ else
129
+ builder.Othr do
130
+ builder.Id('NOTPROVIDED')
131
+ end
132
+ end
133
+ end
134
+ end
135
+ builder.Dbtr do
136
+ builder.Nm(transaction.name)
137
+ end
138
+ builder.DbtrAcct do
139
+ builder.Id do
140
+ builder.IBAN(transaction.iban)
141
+ end
142
+ end
143
+ if transaction.remittance_information
144
+ builder.RmtInf do
145
+ builder.Ustrd(transaction.remittance_information)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,135 @@
1
+ # encoding: utf-8
2
+
3
+ module SEPA
4
+ PAIN_008_001_02 = 'pain.008.001.02'
5
+ PAIN_008_002_02 = 'pain.008.002.02'
6
+ PAIN_008_003_02 = 'pain.008.003.02'
7
+ PAIN_001_001_03 = 'pain.001.001.03'
8
+ PAIN_001_002_03 = 'pain.001.002.03'
9
+ PAIN_001_003_03 = 'pain.001.003.03'
10
+
11
+ class Message
12
+ include ActiveModel::Validations
13
+
14
+ attr_reader :account, :grouped_transactions
15
+
16
+ validates_presence_of :transactions
17
+ validate do |record|
18
+ record.errors.add(:account, record.account.errors.full_messages) unless record.account.valid?
19
+ end
20
+
21
+ class_attribute :account_class, :transaction_class, :xml_main_tag, :known_schemas
22
+
23
+ def initialize(account_options={})
24
+ @grouped_transactions = {}
25
+ @account = account_class.new(account_options)
26
+ end
27
+
28
+ def add_transaction(options)
29
+ transaction = transaction_class.new(options)
30
+ raise ArgumentError.new(transaction.errors.full_messages.join("\n")) unless transaction.valid?
31
+ @grouped_transactions[transaction_group(transaction)] ||= []
32
+ @grouped_transactions[transaction_group(transaction)] << transaction
33
+ end
34
+
35
+ def transactions
36
+ grouped_transactions.values.flatten
37
+ end
38
+
39
+ # @return [String] xml
40
+ def to_xml(schema_name=self.known_schemas.first)
41
+ raise RuntimeError.new(errors.full_messages.join("\n")) unless valid?
42
+ raise RuntimeError.new("Incompatible with schema #{schema_name}!") unless schema_compatible?(schema_name)
43
+
44
+ builder = Builder::XmlMarkup.new indent: 2
45
+ builder.instruct!
46
+ builder.Document(xml_schema(schema_name)) do
47
+ builder.__send__(xml_main_tag) do
48
+ build_group_header(builder)
49
+ build_payment_informations(builder)
50
+ end
51
+ end
52
+ end
53
+
54
+ def amount_total(selected_transactions=transactions)
55
+ selected_transactions.inject(0) { |sum, t| sum + t.amount }
56
+ end
57
+
58
+ def schema_compatible?(schema_name)
59
+ raise ArgumentError.new("Schema #{schema_name} is unknown!") unless self.known_schemas.include?(schema_name)
60
+
61
+ case schema_name
62
+ when PAIN_001_002_03, PAIN_008_002_02, PAIN_001_001_03
63
+ account.bic.present? && transactions.all? { |t| t.schema_compatible?(schema_name) }
64
+ when PAIN_001_003_03, PAIN_008_003_02, PAIN_008_001_02
65
+ transactions.all? { |t| t.schema_compatible?(schema_name) }
66
+ end
67
+ end
68
+
69
+ # Set unique identifer for the message
70
+ def message_identification=(value)
71
+ raise ArgumentError.new('mesage_identification must be a string!') unless value.is_a?(String)
72
+
73
+ regex = /\A([A-Za-z0-9]|[\+|\?|\/|\-|\:|\(|\)|\.|\,|\'|\ ]){1,35}\z/
74
+ raise ArgumentError.new("mesage_identification does not match #{regex}!") unless value.match(regex)
75
+
76
+ @message_identification = value
77
+ end
78
+
79
+ # Get unique identifer for the message (with fallback to a random string)
80
+ def message_identification
81
+ @message_identification ||= "SEPA-KING/#{SecureRandom.hex(11)}"
82
+ end
83
+
84
+ # Returns the id of the batch to which the given transaction belongs
85
+ # Identified based upon the reference of the transaction
86
+ def batch_id(transaction_reference)
87
+ grouped_transactions.each do |group, transactions|
88
+ if transactions.select { |transaction| transaction.reference == transaction_reference }.any?
89
+ return payment_information_identification(group)
90
+ end
91
+ end
92
+ end
93
+
94
+ def batches
95
+ grouped_transactions.keys.collect { |group| payment_information_identification(group) }
96
+ end
97
+
98
+ private
99
+ # @return {Hash<Symbol=>String>} xml schema information used in output xml
100
+ def xml_schema(schema_name)
101
+ { :xmlns => "urn:iso:std:iso:20022:tech:xsd:#{schema_name}",
102
+ :'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
103
+ :'xsi:schemaLocation' => "urn:iso:std:iso:20022:tech:xsd:#{schema_name} #{schema_name}.xsd" }
104
+ end
105
+
106
+ def build_group_header(builder)
107
+ builder.GrpHdr do
108
+ builder.MsgId(message_identification)
109
+ builder.CreDtTm(Time.now.iso8601)
110
+ builder.NbOfTxs(transactions.length)
111
+ builder.CtrlSum('%.2f' % amount_total)
112
+ builder.InitgPty do
113
+ builder.Nm(account.name)
114
+ builder.Id do
115
+ builder.OrgId do
116
+ builder.Othr do
117
+ builder.Id(account.creditor_identifier)
118
+ end
119
+ end
120
+ end if account.respond_to? :creditor_identifier
121
+ end
122
+ end
123
+ end
124
+
125
+ # Unique and consecutive identifier (used for the <PmntInf> blocks)
126
+ def payment_information_identification(group)
127
+ "#{message_identification}/#{grouped_transactions.keys.index(group)+1}"
128
+ end
129
+
130
+ # Returns a key to determine the group to which the transaction belongs
131
+ def transaction_group(transaction)
132
+ transaction
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+ module SEPA
3
+ class CreditTransferTransaction < Transaction
4
+ attr_accessor :service_level
5
+
6
+ validates_inclusion_of :service_level, :in => ['SEPA', 'URGP', '']
7
+
8
+ validate { |t| t.validate_requested_date_after(Date.today) }
9
+
10
+ def initialize(attributes = {})
11
+ super
12
+ if self.currency == 'EUR'
13
+ self.service_level ||= 'SEPA'
14
+ end
15
+ end
16
+
17
+ def schema_compatible?(schema_name)
18
+ case schema_name
19
+ when PAIN_001_001_03
20
+ if self.currency == 'EUR'
21
+ self.service_level == 'SEPA'
22
+ end
23
+ when PAIN_001_002_03
24
+ self.bic.present? && self.service_level == 'SEPA' && self.currency == 'EUR'
25
+ when PAIN_001_003_03
26
+ self.currency == 'EUR'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
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, :mandate_date_of_signature, :local_instrument, :sequence_type, :creditor_account, :original_debtor_account, :same_mandate_new_debtor_agent
8
+
9
+ validates_with MandateIdentifierValidator, field_name: :mandate_id, message: "%{value} is invalid"
10
+ validates_presence_of :mandate_date_of_signature
11
+ validates_inclusion_of :local_instrument, in: LOCAL_INSTRUMENTS
12
+ validates_inclusion_of :sequence_type, in: SEQUENCE_TYPES
13
+
14
+ validate { |t| t.validate_requested_date_after(Date.today.next) }
15
+
16
+ validate do |t|
17
+ if creditor_account
18
+ errors.add(:creditor_account, 'is not correct') unless creditor_account.valid?
19
+ end
20
+
21
+ if t.mandate_date_of_signature.is_a?(Date)
22
+ errors.add(:mandate_date_of_signature, 'is in the future') if t.mandate_date_of_signature > Date.today
23
+ else
24
+ errors.add(:mandate_date_of_signature, 'is not a Date')
25
+ end
26
+ end
27
+
28
+ def initialize(attributes = {})
29
+ super
30
+ self.local_instrument ||= 'CORE'
31
+ self.sequence_type ||= 'OOFF'
32
+ end
33
+
34
+ def schema_compatible?(schema_name)
35
+ case schema_name
36
+ when PAIN_008_002_02
37
+ self.bic.present? && %w(CORE B2B).include?(self.local_instrument) && self.currency == 'EUR'
38
+ when PAIN_008_003_02
39
+ self.currency == 'EUR'
40
+ when PAIN_008_001_02
41
+ true
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,44 @@
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, :iban, :bic, :amount, :instruction, :reference, :remittance_information, :requested_date, :batch_booking, :currency
10
+ convert :name, :instruction, :reference, :remittance_information, to: :text
11
+ convert :amount, to: :decimal
12
+
13
+ validates_length_of :name, within: 1..70
14
+ validates_length_of :currency, is: 3
15
+ validates_length_of :instruction, within: 1..35, allow_nil: true
16
+ validates_length_of :reference, within: 1..35, allow_nil: true
17
+ validates_length_of :remittance_information, within: 1..140, allow_nil: true
18
+ validates_numericality_of :amount, greater_than: 0
19
+ validates_presence_of :requested_date
20
+ validates_inclusion_of :batch_booking, :in => [true, false]
21
+ validates_with BICValidator, IBANValidator, message: "%{value} is invalid"
22
+
23
+ def initialize(attributes = {})
24
+ attributes.each do |name, value|
25
+ send("#{name}=", value)
26
+ end
27
+
28
+ self.requested_date ||= DEFAULT_REQUESTED_DATE
29
+ self.reference ||= 'NOTPROVIDED'
30
+ self.batch_booking = true if self.batch_booking.nil?
31
+ self.currency ||= 'EUR'
32
+ end
33
+
34
+ protected
35
+
36
+ def validate_requested_date_after(min_requested_date)
37
+ return unless requested_date.is_a?(Date)
38
+
39
+ if requested_date != DEFAULT_REQUESTED_DATE && requested_date < min_requested_date
40
+ errors.add(:requested_date, "must be greater or equal to #{min_requested_date}, or nil")
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+ module SEPA
3
+ class IBANValidator < ActiveModel::Validator
4
+ # IBAN2007Identifier (taken from schema)
5
+ REGEX = /\A[A-Z]{2,2}[0-9]{2,2}[a-zA-Z0-9]{1,30}\z/
6
+
7
+ def validate(record)
8
+ field_name = options[:field_name] || :iban
9
+ value = record.send(field_name).to_s
10
+
11
+ unless IBANTools::IBAN.valid?(value) && value.match(REGEX)
12
+ record.errors.add(field_name, :invalid, message: options[:message])
13
+ end
14
+ end
15
+ end
16
+
17
+ class BICValidator < ActiveModel::Validator
18
+ # AnyBICIdentifier (taken from schema)
19
+ REGEX = /\A[A-Z]{6,6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3,3}){0,1}\z/
20
+
21
+ def validate(record)
22
+ field_name = options[:field_name] || :bic
23
+ value = record.send(field_name)
24
+
25
+ if value
26
+ unless value.to_s.match(REGEX)
27
+ record.errors.add(field_name, :invalid, message: options[:message])
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ class CreditorIdentifierValidator < ActiveModel::Validator
34
+ REGEX = /\A[a-zA-Z]{2,2}[0-9]{2,2}([A-Za-z0-9]|[\+|\?|\/|\-|\:|\(|\)|\.|,|']){3,3}([A-Za-z0-9]|[\+|\?|\/|\-|:|\(|\)|\.|,|']){1,28}\z/
35
+
36
+ def validate(record)
37
+ field_name = options[:field_name] || :creditor_identifier
38
+ value = record.send(field_name)
39
+
40
+ unless valid?(value)
41
+ record.errors.add(field_name, :invalid, message: options[:message])
42
+ end
43
+ end
44
+
45
+ def valid?(creditor_identifier)
46
+ if ok = creditor_identifier.to_s.match(REGEX)
47
+ # In Germany, the identifier has to be exactly 18 chars long
48
+ if creditor_identifier[0..1].match(/DE/i)
49
+ ok = creditor_identifier.length == 18
50
+ end
51
+ end
52
+ ok
53
+ end
54
+ end
55
+
56
+ class MandateIdentifierValidator < ActiveModel::Validator
57
+ REGEX = /\A([A-Za-z0-9]|[\+|\?|\/|\-|\:|\(|\)|\.|\,|\']){1,35}\z/
58
+
59
+ def validate(record)
60
+ field_name = options[:field_name] || :mandate_id
61
+ value = record.send(field_name)
62
+
63
+ unless value.to_s.match(REGEX)
64
+ record.errors.add(field_name, :invalid, message: options[:message])
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module SEPA
2
+ VERSION = '0.10.1'
3
+ end
data/lib/sepa_king.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'active_model'
2
+ require 'bigdecimal'
3
+ require 'builder'
4
+ require 'iban-tools'
5
+
6
+ require 'sepa_king/converter'
7
+ require 'sepa_king/validator'
8
+ require 'sepa_king/account'
9
+ require 'sepa_king/account/debtor_account'
10
+ require 'sepa_king/account/creditor_account'
11
+ require 'sepa_king/transaction'
12
+ require 'sepa_king/transaction/direct_debit_transaction'
13
+ require 'sepa_king/transaction/credit_transfer_transaction'
14
+ require 'sepa_king/message'
15
+ require 'sepa_king/message/direct_debit'
16
+ require 'sepa_king/message/credit_transfer'
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sepa_king/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'sepa_king_extended'
8
+ s.version = SEPA::VERSION
9
+ s.authors = ['Georg Leciejewski', 'Georg Ledermann', 'Chris Okamoto']
10
+ s.email = ['gl@salesking.eu', 'mail@georg-ledermann.de', 'christiane.okamoto@gmail.com']
11
+ s.description = 'Accepts payment type 3 - for payments in CHF and not SEPA in pain.0001.0001.03 (ISO 20022)'
12
+ s.summary = 'Ruby gem for creating SEPA XML files'
13
+ s.homepage = 'https://github.com/chrisokamoto/sepa_king_extended'
14
+
15
+ s.files = `git ls-files`.split($/)
16
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+ s.require_paths = ['lib']
19
+
20
+ s.required_ruby_version = '>= 2.0.0'
21
+
22
+ s.add_runtime_dependency 'activemodel', '>= 3.0.0'
23
+ s.add_runtime_dependency 'builder'
24
+ s.add_runtime_dependency 'iban-tools'
25
+
26
+ s.add_development_dependency 'bundler'
27
+ s.add_development_dependency 'rspec'
28
+ s.add_development_dependency 'coveralls'
29
+ s.add_development_dependency 'simplecov'
30
+ s.add_development_dependency 'rake'
31
+ s.add_development_dependency 'nokogiri', RUBY_VERSION < '2.1.0' ? '~> 1.6.0' : '~> 1'
32
+ end