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.
- 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
|