File: mt940.go

package info (click to toggle)
golang-github-mitch000001-go-hbci 0.4.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 2,468 kB
  • sloc: java: 1,092; makefile: 5
file content (333 lines) | stat: -rw-r--r-- 9,464 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
package swift

import (
	"bytes"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/mitch000001/go-hbci/domain"
	"github.com/pkg/errors"
)

// MT940 represents a S.W.I.F.T. Transaction Report
type MT940 struct {
	JobReference         *AlphaNumericTag
	Reference            *AlphaNumericTag
	Account              *AccountTag
	StatementNumber      *StatementNumberTag
	StartingBalance      *BalanceTag
	Transactions         []*TransactionSequence
	ClosingBalance       *BalanceTag
	CurrentValutaBalance *BalanceTag
	FutureValutaBalance  *BalanceTag
	CustomField          *CustomFieldTag
}

// AccountTransactions returns a slice of account transactions created from m
func (m *MT940) AccountTransactions() []domain.AccountTransaction {
	accountConnection := domain.AccountConnection{BankID: m.Account.BankID, AccountID: m.Account.AccountID, CountryCode: 280}
	var transactions []domain.AccountTransaction
	for _, transactionSequence := range m.Transactions {
		tr := transactionSequence.Transaction
		descr := transactionSequence.Description
		var amount float64
		if tr.DebitCreditIndicator == "D" {
			amount = -tr.Amount
		} else {
			amount = tr.Amount
		}
		transaction := domain.AccountTransaction{
			Account:     accountConnection,
			Amount:      domain.Amount{Amount: amount, Currency: m.StartingBalance.Currency},
			ValutaDate:  tr.ValutaDate.Time,
			BookingDate: tr.BookingDate.Time,
			AccountBalanceBefore: domain.Balance{
				Amount: domain.Amount{
					Amount:   m.StartingBalance.Amount,
					Currency: m.StartingBalance.Currency,
				},
				TransmissionDate: m.StartingBalance.BookingDate.Time,
			},
			AccountBalanceAfter: domain.Balance{
				Amount: domain.Amount{
					Amount:   m.ClosingBalance.Amount,
					Currency: m.ClosingBalance.Currency,
				},
				TransmissionDate: m.ClosingBalance.BookingDate.Time,
			},
		}
		if descr != nil {
			transaction.BookingText = descr.BookingText
			transaction.BankID = descr.BankID
			transaction.AccountID = descr.AccountID
			transaction.Name = descr.Name
			transaction.Purpose = strings.Join(descr.Purpose, " ")
			transaction.Purpose2 = strings.Join(descr.Purpose2, " ")
			transaction.TransactionID = descr.TransactionID
		}
		transactions = append(transactions, transaction)
	}
	return transactions
}

// AccountTag represents an account in S.W.I.F.T.
type AccountTag struct {
	Tag       string
	BankID    string
	AccountID string
}

// Unmarshal unmarshals value into a
func (a *AccountTag) Unmarshal(value []byte) error {
	elements, err := extractTagElements(value)
	if err != nil {
		return err
	}
	if len(elements) != 2 {
		return fmt.Errorf("%T: Malformed marshaled value", a)
	}
	a.Tag = string(elements[0])
	fields := bytes.Split(elements[1], []byte("/"))
	if len(fields) != 2 {
		return fmt.Errorf("%T: Malformed marshaled value", a)
	}
	a.BankID = string(fields[0])
	a.AccountID = string(fields[1])
	return nil
}

// StatementNumberTag represents a S.W.I.F.T. statement number
type StatementNumberTag struct {
	Tag         string
	Number      int
	SheetNumber int
}

// Unmarshal unmarshals value into s
func (s *StatementNumberTag) Unmarshal(value []byte) error {
	elements, err := extractTagElements(value)
	if err != nil {
		return err
	}
	if len(elements) != 2 {
		return fmt.Errorf("%T: Malformed marshaled value", s)
	}
	s.Tag = string(elements[0])
	var numBytes []byte
	if bytes.IndexByte(elements[1], '/') != -1 {
		buf := bytes.NewBuffer(elements[1])
		numBytes, err = buf.ReadBytes('/')
		if err != nil {
			return err
		}
		numBytes = numBytes[:len(numBytes)-1]
		sheetNum, err := strconv.Atoi(buf.String())
		if err != nil {
			return err
		}
		s.SheetNumber = sheetNum
	} else {
		numBytes = elements[1]
	}
	num, err := strconv.Atoi(string(numBytes))
	if err != nil {
		return err
	}
	s.Number = num
	return nil
}

// A BalanceTag represents a balance in S.W.I.F.T.
type BalanceTag struct {
	Tag                  string
	DebitCreditIndicator string
	BookingDate          domain.ShortDate
	Currency             string
	Amount               float64
}

// Balance returns the balance embodied in b
func (b *BalanceTag) Balance() domain.Balance {
	amount := b.Amount
	if b.DebitCreditIndicator == "D" {
		amount = -amount
	}
	return domain.Balance{
		Amount:           domain.Amount{Amount: amount, Currency: b.Currency},
		TransmissionDate: b.BookingDate.Time,
	}
}

// Unmarshal unmarshals value into b
func (b *BalanceTag) Unmarshal(value []byte) error {
	elements, err := extractTagElements(value)
	if err != nil {
		return err
	}
	if len(elements) != 2 {
		return fmt.Errorf("%T: Malformed marshaled value", b)
	}
	b.Tag = string(elements[0])
	buf := bytes.NewBuffer(elements[1])
	b.DebitCreditIndicator = string(buf.Next(1))
	dateBytes := buf.Next(6)
	date, err := parseDate(dateBytes, time.Now().Year())
	if err != nil {
		return errors.WithMessage(err, "unmarshal balance tag: parsing booking date")
	}
	b.BookingDate = domain.NewShortDate(date)
	b.Currency = string(buf.Next(3))
	amountString := strings.Replace(buf.String(), ",", ".", 1)
	amount, err := strconv.ParseFloat(amountString, 64)
	if err != nil {
		return errors.Wrap(err, "MT940 Balance tag: error unmarshaling amount")
	}
	b.Amount = amount
	return nil
}

// A TransactionSequence represents a transaction with an additional
// description in S.W.I.F.T.
type TransactionSequence struct {
	Transaction *TransactionTag
	Description *CustomFieldTag
}

// A TransactionTag represents a transaction in S.W.I.F.T.
type TransactionTag struct {
	Tag                   string
	ValutaDate            domain.ShortDate
	BookingDate           domain.ShortDate
	DebitCreditIndicator  string
	CurrencyKind          string
	Amount                float64
	BookingKey            string
	Reference             string
	BankReference         string
	AdditionalInformation string
}

// Unmarshal unmarshals value into t
func (t *TransactionTag) Unmarshal(value []byte) error {
	elements, err := extractTagElements(value)
	if err != nil {
		return err
	}
	if len(elements) != 2 {
		return fmt.Errorf("%T: Malformed marshaled value", t)
	}
	t.Tag = string(elements[0])
	buf := bytes.NewBuffer(elements[1])
	dateBytes := buf.Next(6)
	date, err := parseDate(dateBytes, time.Now().Year())
	if err != nil {
		return errors.WithMessage(err, "unmarshal transaction tag: parsing valuta date")
	}
	t.ValutaDate = domain.NewShortDate(date)
	r, _, err := buf.ReadRune()
	if err != nil {
		return err
	}
	if unicode.IsDigit(r) {
		buf.UnreadRune()
		dateBytes = buf.Next(4)
		date, err = parseDate(dateBytes, t.ValutaDate.Year())
		if err != nil {
			return errors.WithMessage(err, "unmarshal transaction tag: parsing booking date")
		}
		t.BookingDate = domain.NewShortDate(date)
		monthDiff := int(math.Abs(float64(t.ValutaDate.Month() - t.BookingDate.Month())))
		if monthDiff > 1 {
			t.BookingDate = domain.NewShortDate(t.BookingDate.AddDate(1, 0, 0))
		}
	}
	var runes []rune
	for {
		r, _, err := buf.ReadRune()
		if err != nil {
			return err
		}
		runes = append(runes, r)
		if unicode.IsDigit(r) {
			buf.UnreadRune()
			runes = runes[:len(runes)-1]
			if len(runes) == 3 {
				t.DebitCreditIndicator = string(runes[:2])
				t.CurrencyKind = string(runes[2:])
			} else if len(runes) == 2 {
				t.DebitCreditIndicator = string(runes[:1])
				t.CurrencyKind = string(runes[1:])
			} else if len(runes) == 1 {
				t.DebitCreditIndicator = string(runes)
			} else {
				return fmt.Errorf("%T: Malformed marshaled value", t)
			}
			break
		}
	}
	amountBytes, err := buf.ReadBytes('N')
	if err != nil {
		return err
	}
	amountBytes = bytes.Replace(amountBytes[:len(amountBytes)-1], []byte(","), []byte("."), 1)
	amount, err := strconv.ParseFloat(string(amountBytes), 64)
	if err != nil {
		return errors.Wrap(err, "MT940 Transaction tag: error unmarshaling amount")
	}
	t.Amount = amount
	t.BookingKey = string(buf.Next(3))
	remaining := buf.String()
	addInfSepIdx := strings.Index(remaining, "\r\n/")
	doubleSlashIdx := strings.Index(remaining, "//")

	if doubleSlashIdx != -1 && addInfSepIdx != -1 {
		t.Reference = remaining[:doubleSlashIdx]
		if doubleSlashIdx < addInfSepIdx {
			t.BankReference = remaining[doubleSlashIdx+2 : addInfSepIdx]
			t.AdditionalInformation = remaining[addInfSepIdx+3:]
		} else {
			// The only valid case in the FINTS30 documentation in the other
			// one, but the data we receive are sometimes formatted like that
			// :(
			t.BankReference = remaining[addInfSepIdx+3 : doubleSlashIdx]
			t.AdditionalInformation = remaining[doubleSlashIdx+2:]
		}
	} else {
		t.Reference = remaining
		if doubleSlashIdx != -1 {
			t.Reference = remaining[:doubleSlashIdx]
			t.BankReference = remaining[doubleSlashIdx+2:]
		}
		if addInfSepIdx != -1 {
			t.Reference = remaining[:addInfSepIdx]
			t.AdditionalInformation = remaining[addInfSepIdx+3:]
		}
	}
	return nil
}

func parseDate(value []byte, referenceYear int) (time.Time, error) {
	var offset int
	if len(value) == 6 {
		offset = 2
	} else {
		offset = 4
	}
	yearBegin := fmt.Sprintf("%d", referenceYear)[:offset]
	dateString := yearBegin + string(value)
	date, err := time.Parse("20060102", dateString)
	if err != nil {
		if strings.HasSuffix(dateString, "0229") {
			return time.Date(referenceYear, 2, 29, 0, 0, 0, 0, time.UTC), nil
		}
		if strings.HasSuffix(dateString, "0230") {
			return time.Date(referenceYear, 2, 30, 0, 0, 0, 0, time.UTC), nil
		}
		return time.Time{}, err
	}
	return date.Truncate(24 * time.Hour), nil
}