Skip to content

Commit 737163b

Browse files
committed
fix: p24 parser (#107)
1 parent 61bac17 commit 737163b

2 files changed

Lines changed: 147 additions & 3 deletions

File tree

pkg/importers/privat24.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ var (
3737
incomeTransferRegex = simpleExpenseRegex
3838
internalTransferToRegex = regexp.MustCompile(`(\d+.?\d+)([A-Z]{3}) (Переказ на свою карт[^ ]+ (?:(\d+\*\*\d+) )?(.*))$`)
3939
internalTransferFromRegex = regexp.MustCompile(`(\d+.?\d+)([A-Z]{3}) (Переказ зі своєї карт[^ ]+ (\*?\d+\*?\*?\d+) ?(.*)?)$`)
40+
newFormatHeaderRegex = regexp.MustCompile(`^\[\d+/\d+/\d+ \d+:\d+ [AP]M\] PrivatBank: `)
4041
)
4142

4243
type Privat24 struct {
@@ -68,9 +69,16 @@ func (p *Privat24) ExtractMessages(
6869
line := strings.TrimSpace(r)
6970

7071
if line == "\n" || line == "" {
72+
if builder.Len() != 0 {
73+
messages = append(messages, builder.String())
74+
builder.Reset()
75+
}
76+
continue // end of message
77+
}
78+
79+
if newFormatHeaderRegex.MatchString(line) && builder.Len() != 0 {
7180
messages = append(messages, builder.String())
7281
builder.Reset()
73-
continue // end of message
7482
}
7583

7684
builder.WriteString(line)
@@ -95,7 +103,16 @@ func (p *Privat24) Parse(
95103
for _, message := range messages {
96104
lines := toLines(message)
97105

98-
header := lines[0] // is header in format PrivatBank, [10/1/2025 9:50 AM]
106+
header := lines[0]
107+
dataLines := lines[1:]
108+
109+
// new format: [2/16/2026 4:16 AM] PrivatBank: 22.23EUR Description
110+
if strings.HasPrefix(header, "[") && strings.Contains(header, "] PrivatBank: ") {
111+
endIdx := strings.Index(header, "] PrivatBank: ")
112+
dataLine := header[endIdx+len("] PrivatBank: "):]
113+
header = header[:endIdx+1]
114+
dataLines = append([]string{dataLine}, dataLines...)
115+
}
99116

100117
createdAt, err := p.ParseHeaderDate(header)
101118
if err != nil {
@@ -105,7 +122,7 @@ func (p *Privat24) Parse(
105122
}
106123

107124
records = append(records, &Record{
108-
Data: []byte(strings.Join(lines[1:], "\n")),
125+
Data: []byte(strings.Join(dataLines, "\n")),
109126
Message: &Message{
110127
CreatedAt: createdAt,
111128
},

pkg/importers/privat24_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ func TestParseHeaderDate_Success(t *testing.T) {
4444
header: "PrivatBank, [12/31/2025 11:59 PM]",
4545
wantTime: time.Date(2025, 12, 31, 23, 59, 0, 0, time.UTC),
4646
},
47+
{
48+
name: "new format header - date only bracket",
49+
header: "[2/16/2026 4:16 AM]",
50+
wantTime: time.Date(2026, 2, 16, 4, 16, 0, 0, time.UTC),
51+
},
4752
}
4853

4954
for _, c := range cases {
@@ -1508,3 +1513,125 @@ func TestImportExpenseWithDoubleConversion(t *testing.T) {
15081513
assert.EqualValues(t, uahAccount.ID, expense.Expense.SourceAccountId)
15091514
assert.EqualValues(t, expenseAccount.ID, expense.Expense.DestinationAccountId)
15101515
}
1516+
1517+
func TestImportNewFormatExpense(t *testing.T) {
1518+
p := importers.NewPrivat24(importers.NewBaseParser(nil, nil, nil))
1519+
1520+
uahAccount := &database.Account{
1521+
ID: 1,
1522+
Currency: "UAH",
1523+
AccountNumber: "4*03",
1524+
Type: v1.AccountType_ACCOUNT_TYPE_ASSET,
1525+
}
1526+
expenseAccount := &database.Account{
1527+
ID: 2,
1528+
Currency: "EUR",
1529+
Type: v1.AccountType_ACCOUNT_TYPE_EXPENSE,
1530+
Name: "_default_expense",
1531+
Flags: database.AccountFlagIsDefault,
1532+
AccountNumber: "_default_expense_eur",
1533+
}
1534+
1535+
data := []byte(`[3/10/2026 8:30 AM] PrivatBank: 50.00EUR Комуналка та Інтернет. Test Merchant
1536+
4*03 09:30
1537+
Бал. 10000.00UAH
1538+
Курс 40.0000 UAH/EUR
1539+
Кред. ліміт 50000.0UAH`)
1540+
1541+
result, err := p.Parse(context.TODO(), &importers.ParseRequest{
1542+
ImportRequest: importers.ImportRequest{
1543+
Data: []string{string(data)},
1544+
Accounts: []*database.Account{uahAccount, expenseAccount},
1545+
},
1546+
})
1547+
1548+
assert.NoError(t, err)
1549+
assert.NotNil(t, result)
1550+
assert.Len(t, result.CreateRequests, 1)
1551+
1552+
req := result.CreateRequests[0]
1553+
assert.Equal(t, "Комуналка та Інтернет. Test Merchant", req.Title)
1554+
expense := req.Transaction.(*transactionsv1.CreateTransactionRequest_Expense)
1555+
assert.EqualValues(t, "EUR", expense.Expense.DestinationCurrency)
1556+
assert.EqualValues(t, uahAccount.ID, expense.Expense.SourceAccountId)
1557+
assert.EqualValues(t, expenseAccount.ID, expense.Expense.DestinationAccountId)
1558+
}
1559+
1560+
func TestImportNewFormatMultipleMessages(t *testing.T) {
1561+
p := importers.NewPrivat24(importers.NewBaseParser(nil, nil, nil))
1562+
1563+
uahAccount := &database.Account{
1564+
ID: 1,
1565+
Currency: "UAH",
1566+
AccountNumber: "4*03",
1567+
Type: v1.AccountType_ACCOUNT_TYPE_ASSET,
1568+
}
1569+
expenseAccount := &database.Account{
1570+
ID: 2,
1571+
Currency: "UAH",
1572+
Type: v1.AccountType_ACCOUNT_TYPE_EXPENSE,
1573+
Name: "_default_expense",
1574+
Flags: database.AccountFlagIsDefault,
1575+
AccountNumber: "_default_expense_uah",
1576+
}
1577+
1578+
data := []byte(`[3/10/2026 8:30 AM] PrivatBank: 50.00UAH Комуналка та Інтернет. Test Merchant
1579+
4*03 09:30
1580+
Бал. 10000.00UAH
1581+
Кред. ліміт 50000.0UAH
1582+
[3/10/2026 2:00 PM] PrivatBank: 100.00UAH Ресторани, кафе, бари. Test Cafe
1583+
4*03 15:00
1584+
Бал. 9000.00UAH
1585+
Кред. ліміт 50000.0UAH
1586+
[3/11/2026 10:00 AM] PrivatBank: 25.00UAH Авто. Test Auto Service
1587+
4*03 11:00
1588+
Бал. 8500.00UAH
1589+
Кред. ліміт 50000.0UAH`)
1590+
1591+
result, err := p.Parse(context.TODO(), &importers.ParseRequest{
1592+
ImportRequest: importers.ImportRequest{
1593+
Data: []string{string(data)},
1594+
Accounts: []*database.Account{uahAccount, expenseAccount},
1595+
},
1596+
})
1597+
1598+
assert.NoError(t, err)
1599+
assert.NotNil(t, result)
1600+
assert.Len(t, result.CreateRequests, 3)
1601+
}
1602+
1603+
func TestImportNewFormatWithCashback(t *testing.T) {
1604+
p := importers.NewPrivat24(importers.NewBaseParser(nil, nil, nil))
1605+
1606+
uahAccount := &database.Account{
1607+
ID: 1,
1608+
Currency: "UAH",
1609+
AccountNumber: "4*03",
1610+
Type: v1.AccountType_ACCOUNT_TYPE_ASSET,
1611+
}
1612+
expenseAccount := &database.Account{
1613+
ID: 2,
1614+
Currency: "UAH",
1615+
Type: v1.AccountType_ACCOUNT_TYPE_EXPENSE,
1616+
Name: "_default_expense",
1617+
Flags: database.AccountFlagIsDefault,
1618+
AccountNumber: "_default_expense_uah",
1619+
}
1620+
1621+
data := []byte(`[3/10/2026 5:48 PM] PrivatBank: 200.00UAH Розваги. Test Game Store
1622+
4*03 18:48
1623+
Кешбек 30.00UAH
1624+
Бал. 10000.00UAH
1625+
Кред. ліміт 50000.0UAH`)
1626+
1627+
result, err := p.Parse(context.TODO(), &importers.ParseRequest{
1628+
ImportRequest: importers.ImportRequest{
1629+
Data: []string{string(data)},
1630+
Accounts: []*database.Account{uahAccount, expenseAccount},
1631+
},
1632+
})
1633+
1634+
assert.NoError(t, err)
1635+
assert.NotNil(t, result)
1636+
assert.Len(t, result.CreateRequests, 1)
1637+
}

0 commit comments

Comments
 (0)