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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +2 -0
  5. data/.todo.reek +3 -0
  6. data/.travis.yml +7 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +92 -0
  10. data/Rakefile +6 -0
  11. data/bank_payments.gemspec +38 -0
  12. data/bank_specifications/teknisk_manual_swedbank.pdf +0 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/lib/bank_payments.rb +42 -0
  16. data/lib/bank_payments/beneficiary.rb +69 -0
  17. data/lib/bank_payments/spisu_record.rb +130 -0
  18. data/lib/bank_payments/swedbank_export/address_record.rb +40 -0
  19. data/lib/bank_payments/swedbank_export/bank_record.rb +28 -0
  20. data/lib/bank_payments/swedbank_export/credit_memo_record.rb +49 -0
  21. data/lib/bank_payments/swedbank_export/enums.rb +22 -0
  22. data/lib/bank_payments/swedbank_export/field_definition.rb +21 -0
  23. data/lib/bank_payments/swedbank_export/file.rb +26 -0
  24. data/lib/bank_payments/swedbank_export/money_record.rb +21 -0
  25. data/lib/bank_payments/swedbank_export/name_record.rb +15 -0
  26. data/lib/bank_payments/swedbank_export/opening_record.rb +30 -0
  27. data/lib/bank_payments/swedbank_export/payment_record.rb +20 -0
  28. data/lib/bank_payments/swedbank_export/reason_record.rb +16 -0
  29. data/lib/bank_payments/swedbank_export/reconciliation_record.rb +54 -0
  30. data/lib/bank_payments/swedbank_export/sequence.rb +148 -0
  31. data/lib/bank_payments/swedbank_import/account_record.rb +18 -0
  32. data/lib/bank_payments/swedbank_import/address_record.rb +35 -0
  33. data/lib/bank_payments/swedbank_import/amount_converter.rb +38 -0
  34. data/lib/bank_payments/swedbank_import/file.rb +35 -0
  35. data/lib/bank_payments/swedbank_import/money_record.rb +49 -0
  36. data/lib/bank_payments/swedbank_import/name_record.rb +20 -0
  37. data/lib/bank_payments/swedbank_import/opening_record.rb +25 -0
  38. data/lib/bank_payments/swedbank_import/reconciliation_record.rb +35 -0
  39. data/lib/bank_payments/swedbank_import/sequence.rb +42 -0
  40. data/lib/bank_payments/transaction.rb +38 -0
  41. data/lib/bank_payments/version.rb +3 -0
  42. data/lib/core_extensions/numeric/spisu.rb +15 -0
  43. 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,15 @@
1
+ module BankPayments
2
+ module SwedbankExport
3
+ class NameRecord < SpisuRecord
4
+
5
+ define_field :serial_number, '2:8:N'
6
+ define_field :name, '9:73:AN'
7
+
8
+ def initialize
9
+ super
10
+ self.type = '2'
11
+ end
12
+
13
+ end
14
+ end
15
+ 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,16 @@
1
+ module BankPayments
2
+ module SwedbankExport
3
+ class ReasonRecord < SpisuRecord
4
+
5
+ define_field :serial_number, '2:8:N'
6
+ define_field :code, '9:11:N'
7
+
8
+ def initialize
9
+ super
10
+ self.type = '7'
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+
@@ -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