bank_payments 0.5.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/.coveralls.yml +1 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.todo.reek +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +92 -0
- data/Rakefile +6 -0
- data/bank_payments.gemspec +38 -0
- data/bank_specifications/teknisk_manual_swedbank.pdf +0 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/bank_payments.rb +42 -0
- data/lib/bank_payments/beneficiary.rb +69 -0
- data/lib/bank_payments/spisu_record.rb +130 -0
- data/lib/bank_payments/swedbank_export/address_record.rb +40 -0
- data/lib/bank_payments/swedbank_export/bank_record.rb +28 -0
- data/lib/bank_payments/swedbank_export/credit_memo_record.rb +49 -0
- data/lib/bank_payments/swedbank_export/enums.rb +22 -0
- data/lib/bank_payments/swedbank_export/field_definition.rb +21 -0
- data/lib/bank_payments/swedbank_export/file.rb +26 -0
- data/lib/bank_payments/swedbank_export/money_record.rb +21 -0
- data/lib/bank_payments/swedbank_export/name_record.rb +15 -0
- data/lib/bank_payments/swedbank_export/opening_record.rb +30 -0
- data/lib/bank_payments/swedbank_export/payment_record.rb +20 -0
- data/lib/bank_payments/swedbank_export/reason_record.rb +16 -0
- data/lib/bank_payments/swedbank_export/reconciliation_record.rb +54 -0
- data/lib/bank_payments/swedbank_export/sequence.rb +148 -0
- data/lib/bank_payments/swedbank_import/account_record.rb +18 -0
- data/lib/bank_payments/swedbank_import/address_record.rb +35 -0
- data/lib/bank_payments/swedbank_import/amount_converter.rb +38 -0
- data/lib/bank_payments/swedbank_import/file.rb +35 -0
- data/lib/bank_payments/swedbank_import/money_record.rb +49 -0
- data/lib/bank_payments/swedbank_import/name_record.rb +20 -0
- data/lib/bank_payments/swedbank_import/opening_record.rb +25 -0
- data/lib/bank_payments/swedbank_import/reconciliation_record.rb +35 -0
- data/lib/bank_payments/swedbank_import/sequence.rb +42 -0
- data/lib/bank_payments/transaction.rb +38 -0
- data/lib/bank_payments/version.rb +3 -0
- data/lib/core_extensions/numeric/spisu.rb +15 -0
- metadata +169 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module BankPayments
|
2
|
+
module SwedbankExport
|
3
|
+
|
4
|
+
# Used to describe the destination of the transaction being
|
5
|
+
# done as well as some basic transactional information such as
|
6
|
+
#
|
7
|
+
# * Which party/parties should be responsible for the transactional costs
|
8
|
+
# * What kind of Swedbank account that should be used for the transaction
|
9
|
+
#
|
10
|
+
# @author Michael Litton
|
11
|
+
class AddressRecord < SpisuRecord
|
12
|
+
|
13
|
+
define_field :serial_number, '2:8:N'
|
14
|
+
define_field :address, '9:73:AN'
|
15
|
+
define_field :country_code, '75:76:AN'
|
16
|
+
|
17
|
+
# Account type to use
|
18
|
+
# '0': Inlåningskonto (SEK-konto)
|
19
|
+
# '1': Valutakonto
|
20
|
+
define_field :account_type, '74:74:N'
|
21
|
+
|
22
|
+
# Transaction cost responsibility
|
23
|
+
# '2': The beneficiary is responsible for their transaction costs
|
24
|
+
# '0': The payer stands for all transaction costs
|
25
|
+
define_field :cost_carrier, '78:78:N'
|
26
|
+
|
27
|
+
# Priority code
|
28
|
+
# '0': Normal
|
29
|
+
# '1': Express
|
30
|
+
# '2': Check
|
31
|
+
define_field :priority, '80:80:N'
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
super
|
35
|
+
self.type = '3'
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module BankPayments
|
2
|
+
module SwedbankExport
|
3
|
+
|
4
|
+
# Describes the beneficiaries / payees bank. All fields are required except
|
5
|
+
# for the name which is only required is the payment is made outside of EU.
|
6
|
+
# This is something that any implementors needs to validate on their own.
|
7
|
+
#
|
8
|
+
# @author Michael Litton
|
9
|
+
class BankRecord < SpisuRecord
|
10
|
+
|
11
|
+
define_field :serial_number, '2:8:N'
|
12
|
+
|
13
|
+
# Usually BIC (Bank Identification Code)
|
14
|
+
define_field :bank_id, '9:20:AN'
|
15
|
+
|
16
|
+
# IBAN (Internactional Bank Account Number)
|
17
|
+
define_field :account, '21:50:AN'
|
18
|
+
|
19
|
+
# Leave this blank if the payment is made within EU
|
20
|
+
define_field :name, '51:80:AN'
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
super
|
24
|
+
self.type = '4'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module BankPayments::SwedbankExport
|
2
|
+
|
3
|
+
# Describes a credit to be made. See the parent class for additional information.
|
4
|
+
# The only special thing about this class is how to interpret the date.
|
5
|
+
#
|
6
|
+
# When you set the date it is to interpreted as a soft "expiry date" for the
|
7
|
+
# credit itself. When the date passes the bank will still use it, if possible,
|
8
|
+
# but it will appear of a special list at the bank: 'Utestående kreditfakturor'
|
9
|
+
#
|
10
|
+
# @author Michael Litton
|
11
|
+
class CreditMemoRecord < MoneyRecord
|
12
|
+
|
13
|
+
DIGIT_MAP = {
|
14
|
+
'0' => '-',
|
15
|
+
'1' => 'J',
|
16
|
+
'2' => 'K',
|
17
|
+
'3' => 'L',
|
18
|
+
'4' => 'M',
|
19
|
+
'5' => 'N',
|
20
|
+
'6' => 'O',
|
21
|
+
'7' => 'P',
|
22
|
+
'8' => 'Q',
|
23
|
+
'9' => 'R'
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
super
|
28
|
+
self.type = '5'
|
29
|
+
end
|
30
|
+
|
31
|
+
def amount_sek=(amount)
|
32
|
+
super change_last_digit(amount.abs.spisu_format)
|
33
|
+
end
|
34
|
+
|
35
|
+
def amount_foreign=(amount)
|
36
|
+
super change_last_digit(amount.abs.spisu_format)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Set a special value in positions 44 and 78 (the last digit). This is
|
42
|
+
# how the bank determines that this is a credit memo
|
43
|
+
def change_last_digit(digits)
|
44
|
+
digits[0..-2] + DIGIT_MAP[digits[-1]]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module BankPayments::SwedbankExport
|
2
|
+
|
3
|
+
# Lookup module for account types as described in the specification from
|
4
|
+
# the bank making calling code a little bit easier to understand.
|
5
|
+
module AccountType
|
6
|
+
DEPOSIT_ACCOUNT = 0
|
7
|
+
CURRENCY_ACCOUNT = 1
|
8
|
+
end
|
9
|
+
|
10
|
+
# @see AccountType
|
11
|
+
module Priority
|
12
|
+
NORMAL = 0
|
13
|
+
EXPRESS = 1
|
14
|
+
CHECK = 2
|
15
|
+
end
|
16
|
+
|
17
|
+
# @see AccountType
|
18
|
+
module CostResponsibility
|
19
|
+
PAYER = 0
|
20
|
+
OWN_EXPENSES = 2
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module BankPayments::SwedbankExport
|
2
|
+
|
3
|
+
# Defines a field in the the Swedbank SPISU format
|
4
|
+
#
|
5
|
+
# @author Michael Litton
|
6
|
+
class FieldDefinition
|
7
|
+
attr_reader :name, :start, :stop, :type
|
8
|
+
|
9
|
+
def initialize(name, definition)
|
10
|
+
@name = name
|
11
|
+
unformatted_def = definition.split(':')
|
12
|
+
@start, @stop, @type = unformatted_def.each_with_index.map do |value,idx|
|
13
|
+
idx < 2 ? value.to_i : value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def length
|
18
|
+
@stop - @start + 1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module BankPayments::SwedbankExport
|
2
|
+
|
3
|
+
# Contains international payments to be made to foreign beneficiaries. The file
|
4
|
+
# contains one or more sequnces containing payments.
|
5
|
+
#
|
6
|
+
# @author Michael Litton
|
7
|
+
class File
|
8
|
+
attr_accessor :file_name
|
9
|
+
|
10
|
+
def initialize(file_name)
|
11
|
+
@file_name = file_name
|
12
|
+
@sequences = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Adds a sequence to the file
|
16
|
+
def <<(sequence)
|
17
|
+
@sequences << sequence
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_file_data
|
21
|
+
sequence_data = @sequences.map { |sequence| sequence.to_file_data }
|
22
|
+
|
23
|
+
sequence_data.join("\n").encode('iso-8859-1')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module BankPayments
|
2
|
+
module SwedbankExport
|
3
|
+
|
4
|
+
# There can be two types of monetary records in a transaction. Payments and
|
5
|
+
# credits sent over to the bank. This is the parent class that describes both.
|
6
|
+
# There is a date present which means different things depending on which
|
7
|
+
# implementation that is being used.
|
8
|
+
#
|
9
|
+
# @author Michael Litton
|
10
|
+
class MoneyRecord < SpisuRecord
|
11
|
+
|
12
|
+
define_field :serial_number, '2:8:N'
|
13
|
+
define_field :reference_msg, '9:33:AN'
|
14
|
+
define_field :amount_sek, '34:44:N'
|
15
|
+
define_field :amount_foreign, '66:78:N'
|
16
|
+
define_field :currency_code, '55:57:AN'
|
17
|
+
define_field :date, '58:63:N'
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module BankPayments
|
2
|
+
module SwedbankExport
|
3
|
+
|
4
|
+
|
5
|
+
# The first record in a sequence which contains information about payer.
|
6
|
+
#
|
7
|
+
# @author Michael Litton
|
8
|
+
class OpeningRecord < SpisuRecord
|
9
|
+
|
10
|
+
define_field :account, '2:9:N'
|
11
|
+
define_field :creation_date, '10:15:N'
|
12
|
+
define_field :name, '16:37:AN'
|
13
|
+
define_field :address, '38:72:AN'
|
14
|
+
define_field :pay_date, '73:78:N'
|
15
|
+
define_field :layout, '79:79:N'
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
super
|
19
|
+
self.type = '0'
|
20
|
+
set_spisu_layout
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def set_spisu_layout
|
26
|
+
self.layout = '2'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module BankPayments::SwedbankExport
|
2
|
+
class PaymentRecord < MoneyRecord
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
super
|
6
|
+
self.type = '6'
|
7
|
+
end
|
8
|
+
|
9
|
+
def amount_sek=(amount)
|
10
|
+
super amount.spisu_format
|
11
|
+
end
|
12
|
+
|
13
|
+
def amount_foreign=(amount)
|
14
|
+
super amount.spisu_format
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module BankPayments
|
2
|
+
module SwedbankExport
|
3
|
+
class ReconciliationRecord < SpisuRecord
|
4
|
+
|
5
|
+
define_field :account, '2:9:N'
|
6
|
+
define_field :sum_amount_sek, '10:21:N'
|
7
|
+
define_field :sum_amount_foreign, '64:78:N'
|
8
|
+
define_field :total_beneficiaries, '32:43:N'
|
9
|
+
define_field :total_records, '44:55:N'
|
10
|
+
|
11
|
+
DIGIT_MAP = {
|
12
|
+
'0' => '-',
|
13
|
+
'1' => 'J',
|
14
|
+
'2' => 'K',
|
15
|
+
'3' => 'L',
|
16
|
+
'4' => 'M',
|
17
|
+
'5' => 'N',
|
18
|
+
'6' => 'O',
|
19
|
+
'7' => 'P',
|
20
|
+
'8' => 'Q',
|
21
|
+
'9' => 'R'
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
super
|
26
|
+
self.type = '9'
|
27
|
+
end
|
28
|
+
|
29
|
+
def sum_amount_sek=(amount)
|
30
|
+
super reformat_sums(amount)
|
31
|
+
end
|
32
|
+
|
33
|
+
def sum_amount_foreign=(amount)
|
34
|
+
super reformat_sums(amount)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def reformat_sums(amount)
|
40
|
+
if amount >= 0
|
41
|
+
amount.spisu_format
|
42
|
+
else
|
43
|
+
change_last_digit(amount.abs.spisu_format)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set a special value in positions 44 and 78 (the last digit). This is
|
48
|
+
# how the bank determines that this is a credit memo
|
49
|
+
def change_last_digit(digits)
|
50
|
+
digits[0..-2] + DIGIT_MAP[digits[-1]]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module BankPayments::SwedbankExport
|
2
|
+
|
3
|
+
# An export file may contain multiple sequences. A sequence starts with an
|
4
|
+
# {BankPayments::SwedbankExport::OpeningRecord} and ends with an
|
5
|
+
# {BankPayments::SwedbankExport::ReconciliationRecord}. Each sequence will
|
6
|
+
# consist of at least the following:
|
7
|
+
#
|
8
|
+
# * {BankPayments::SwedbankExport::OpeningRecord}
|
9
|
+
# * {BankPayments::SwedbankExport::NameRecord}
|
10
|
+
# * {BankPayments::SwedbankExport::AddressRecord}
|
11
|
+
# * {BankPayments::SwedbankExport::BankRecord}
|
12
|
+
# * {BankPayments::SwedbankExport::MoneyRecord} (Payment or Credit Memo)
|
13
|
+
# * {BankPayments::SwedbankExport::ReasonRecord}
|
14
|
+
# * {BankPayments::SwedbankExport::ReconciliationRecord}
|
15
|
+
#
|
16
|
+
# In particular you need to supply the following per beneficiary/payee:
|
17
|
+
#
|
18
|
+
# * {BankPayments::SwedbankExport::NameRecord}
|
19
|
+
# * {BankPayments::SwedbankExport::AddressRecord}
|
20
|
+
# * {BankPayments::SwedbankExport::BankRecord}
|
21
|
+
#
|
22
|
+
# which describes where the payment should be made. Then you will supply
|
23
|
+
# a money record (payment or credit memo) together with it reason to to
|
24
|
+
# describe a payment. All of these records will have a unique serial number
|
25
|
+
# for the beneficiary.
|
26
|
+
#
|
27
|
+
# According to the documentation you can group money records together with the
|
28
|
+
# beneficiary given that the have the correct serial number.
|
29
|
+
#
|
30
|
+
# * {BankPayments::SwedbankExport::MoneyRecord} (Payment or Credit Memo)
|
31
|
+
# * {BankPayments::SwedbankExport::ReasonRecord}
|
32
|
+
#
|
33
|
+
# @author Michael Litton
|
34
|
+
class Sequence
|
35
|
+
|
36
|
+
# Intializes the required records for a sequence given the
|
37
|
+
# sends account, name and adress
|
38
|
+
def initialize(account, name, address, payment_date = nil)
|
39
|
+
@initial_records = []
|
40
|
+
|
41
|
+
@initial_records << OpeningRecord.new do |o_record|
|
42
|
+
o_record.account = account
|
43
|
+
o_record.name = name
|
44
|
+
o_record.address = address
|
45
|
+
end
|
46
|
+
|
47
|
+
@initial_records << ReconciliationRecord.new do |r_record|
|
48
|
+
r_record.account = account
|
49
|
+
r_record.sum_amount_sek = 0
|
50
|
+
r_record.sum_amount_foreign = 0
|
51
|
+
r_record.total_beneficiaries = 0
|
52
|
+
r_record.total_records = 2
|
53
|
+
end
|
54
|
+
|
55
|
+
@payment_date = payment_date
|
56
|
+
|
57
|
+
# TODO Make this thread safe using a mutex?
|
58
|
+
@beneficiaries = []
|
59
|
+
end
|
60
|
+
|
61
|
+
def records
|
62
|
+
all_records = []
|
63
|
+
all_records << @initial_records.first
|
64
|
+
|
65
|
+
@beneficiaries.each_with_index do |entry,index|
|
66
|
+
destination_records = entry[:beneficiary].to_spisu_records
|
67
|
+
moneytary_records = entry[:transactions].map(&:to_spisu_records)
|
68
|
+
|
69
|
+
[destination_records + moneytary_records].flatten.each do|record|
|
70
|
+
record.serial_number = index + 1
|
71
|
+
end
|
72
|
+
|
73
|
+
all_records << destination_records << moneytary_records
|
74
|
+
end
|
75
|
+
|
76
|
+
reconciliation = @initial_records.last
|
77
|
+
reconciliation.sum_amount_sek = sum_sek_transactions
|
78
|
+
reconciliation.sum_amount_foreign = sum_foreign_transactions
|
79
|
+
reconciliation.total_beneficiaries = @beneficiaries.size
|
80
|
+
reconciliation.total_records = all_records.flatten.size + 1
|
81
|
+
|
82
|
+
all_records << reconciliation
|
83
|
+
|
84
|
+
all_records.flatten
|
85
|
+
end
|
86
|
+
|
87
|
+
# Adds a transaction to an existing beneficiary or creates a
|
88
|
+
# new one.
|
89
|
+
def add_transaction(beneficiary, transaction)
|
90
|
+
find_or_create_beneficiary(beneficiary) do |entry|
|
91
|
+
entry[:transactions] << transaction
|
92
|
+
entry[:transactions].sort_by!(&:amount_sek)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Goes through the sequence and validates that in corresponds to the
|
97
|
+
# rules in the specification
|
98
|
+
def valid?
|
99
|
+
all_requried_types_present?
|
100
|
+
end
|
101
|
+
|
102
|
+
def to_file_data
|
103
|
+
records.join("\n")
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def sum_sek_transactions
|
109
|
+
sum_field(:amount_sek)
|
110
|
+
end
|
111
|
+
|
112
|
+
def sum_foreign_transactions
|
113
|
+
sum_field(:amount_foreign)
|
114
|
+
end
|
115
|
+
|
116
|
+
def sum_field(field)
|
117
|
+
tnxs = @beneficiaries.map { |e| e[:transactions] }.flatten
|
118
|
+
tnxs.inject(0.0) { |sum,e| sum += e.send(field) }
|
119
|
+
end
|
120
|
+
|
121
|
+
def find_or_create_beneficiary(beneficiary, &block)
|
122
|
+
entry = @beneficiaries.find { |entry| entry[:beneficiary] == beneficiary }
|
123
|
+
|
124
|
+
if entry.nil?
|
125
|
+
entry = { beneficiary: beneficiary, transactions: [] }
|
126
|
+
@beneficiaries << entry
|
127
|
+
end
|
128
|
+
|
129
|
+
yield entry
|
130
|
+
end
|
131
|
+
|
132
|
+
def all_requried_types_present?
|
133
|
+
all_types = records.map do |record|
|
134
|
+
if record.is_a?(MoneyRecord)
|
135
|
+
'MoneyRecord'
|
136
|
+
else
|
137
|
+
record.class.name.split('::').last
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
required_types = %w(OpeningRecord NameRecord AddressRecord BankRecord
|
142
|
+
MoneyRecord ReasonRecord ReconciliationRecord)
|
143
|
+
|
144
|
+
(required_types - all_types.uniq).size == 0
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|