sepa_king_extended 0.10.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 (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