sepa_king_extended 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +30 -0
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +267 -0
- data/Rakefile +6 -0
- data/gemfiles/Gemfile-activemodel-3.0.x +5 -0
- data/gemfiles/Gemfile-activemodel-3.1.x +5 -0
- data/gemfiles/Gemfile-activemodel-3.2.x +5 -0
- data/gemfiles/Gemfile-activemodel-4.0.x +5 -0
- data/gemfiles/Gemfile-activemodel-4.1.x +5 -0
- data/gemfiles/Gemfile-activemodel-4.2.x +5 -0
- data/gemfiles/Gemfile-activemodel-5.0.x +5 -0
- data/gemfiles/Gemfile-activemodel-5.1.x +5 -0
- data/lib/schema/pain.001.001.03.xsd +921 -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.002.02.xsd +597 -0
- data/lib/schema/pain.008.003.02.xsd +614 -0
- data/lib/sepa_king/account/creditor_account.rb +8 -0
- data/lib/sepa_king/account/debtor_account.rb +5 -0
- data/lib/sepa_king/account.rb +19 -0
- data/lib/sepa_king/converter.rb +51 -0
- data/lib/sepa_king/message/credit_transfer.rb +99 -0
- data/lib/sepa_king/message/direct_debit.rb +151 -0
- data/lib/sepa_king/message.rb +135 -0
- data/lib/sepa_king/transaction/credit_transfer_transaction.rb +30 -0
- data/lib/sepa_king/transaction/direct_debit_transaction.rb +45 -0
- data/lib/sepa_king/transaction.rb +44 -0
- data/lib/sepa_king/validator.rb +68 -0
- data/lib/sepa_king/version.rb +3 -0
- data/lib/sepa_king.rb +16 -0
- data/sepa_king_extended.gemspec +32 -0
- data/spec/account_spec.rb +42 -0
- data/spec/converter_spec.rb +74 -0
- data/spec/credit_transfer_spec.rb +364 -0
- data/spec/credit_transfer_transaction_spec.rb +58 -0
- data/spec/creditor_account_spec.rb +23 -0
- data/spec/debtor_account_spec.rb +12 -0
- data/spec/direct_debit_spec.rb +469 -0
- data/spec/direct_debit_transaction_spec.rb +69 -0
- data/spec/examples/pain.001.001.03.xml +89 -0
- data/spec/examples/pain.001.002.03.xml +89 -0
- data/spec/examples/pain.001.003.03.xml +89 -0
- data/spec/examples/pain.008.002.02.xml +134 -0
- data/spec/examples/pain.008.003.02.xml +134 -0
- data/spec/message_spec.rb +88 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support/active_model.rb +30 -0
- data/spec/support/custom_matcher.rb +60 -0
- data/spec/support/factories.rb +24 -0
- data/spec/support/validations.rb +27 -0
- data/spec/transaction_spec.rb +88 -0
- data/spec/validation_spec.rb +25 -0
- data/spec/validator_spec.rb +99 -0
- 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
|
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
|