Skip to content

Commit d732d99

Browse files
authored
[ffin-kz] Fix moving between accounts (#954)
1 parent d966cc7 commit d732d99

File tree

9 files changed

+490
-51
lines changed

9 files changed

+490
-51
lines changed

src/plugins/ffin-kz/__tests__/api/parseDepositPdfStatement.test.ts

Lines changed: 53 additions & 36 deletions
Large diffs are not rendered by default.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { parseSinglePdfString, splitCardStatements } from '../../api'
2+
3+
describe('splitCardStatements', () => {
4+
it('splits one PDF text with multiple card statements and keeps transactions under correct accounts', () => {
5+
const combinedPdfText = `
6+
Фридом Банк Казахстан
7+
Выписка по карте First Card
8+
Номер счётаВалютаОстаток
9+
KZ111111111111111111 KZT 19,455.00
10+
11+
Дата Сумма Валюта Операция Детали
12+
01.01.2026 -100.00 ₸ KZT Перевод Получатель:
13+
14+
Выписка по карте Second Card
15+
Номер счётаВалютаОстаток
16+
KZ222222222222222222 KZT 5,000.00
17+
18+
Дата Сумма Валюта Операция Детали
19+
02.01.2026 +200.00 ₸ KZT Пополнение PEREVOD BCC.KZ
20+
`.trim()
21+
22+
const parts = splitCardStatements(combinedPdfText)
23+
expect(parts).toHaveLength(2)
24+
25+
const first = parseSinglePdfString(parts[0], 'uid-1')
26+
const second = parseSinglePdfString(parts[1], 'uid-2')
27+
28+
expect(first.account.id).toBe('KZ111111111111111111')
29+
expect(first.transactions).toHaveLength(1)
30+
expect(first.transactions[0].statementUid).toBe('uid-1')
31+
expect(first.transactions[0].date).toBe('2026-01-01T00:00:00.000')
32+
33+
expect(second.account.id).toBe('KZ222222222222222222')
34+
expect(second.transactions).toHaveLength(1)
35+
expect(second.transactions[0].statementUid).toBe('uid-2')
36+
expect(second.transactions[0].date).toBe('2026-01-02T00:00:00.000')
37+
})
38+
})

src/plugins/ffin-kz/__tests__/converters/transactions/convertPdfStatementTransaction.test.ts

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,262 @@ describe('convertPdfStatementTransaction', () => {
136136
expect(tx.comment).toEqual('Выплата процентов по вкладу')
137137
expect(tx.merchant).toBeNull()
138138
})
139+
140+
it('builds transfer from deposit to card using deposit sync id from origin string', () => {
141+
const rawTransaction = {
142+
hold: false,
143+
date: '2025-12-12T00:00:00.000Z',
144+
originalAmount: '+70000.00 KZT',
145+
amount: '+70000.00',
146+
description: 'Другое Плательщик: Иванов Иван Получатель: Иванов Иван Назначение: Выплата вклада по Договору No KZ00000B000000002KZT от 01.02.2025',
147+
statementUid: 'deposit-transfer-uid',
148+
originString: '12.12.2025 +70,000.00 ₸ KZT Другое Плательщик: Иванов Иван Получатель: Иванов Иван Назначение: Выплата вклада по Договору No KZ00000B000000002KZT от 01.02.2025'
149+
}
150+
const account = { id: 'KZCARD0001', instrument: 'KZT' }
151+
const currencyRates = {}
152+
153+
const converted = convertPdfStatementTransaction(rawTransaction as any, account, currencyRates)
154+
if (converted == null) {
155+
throw new Error('Expected conversion result')
156+
}
157+
const tx = converted.transaction
158+
expect(tx.movements).toHaveLength(2)
159+
expect(tx.movements[0]).toEqual(expect.objectContaining({
160+
account: { id: 'KZCARD0001' },
161+
sum: 70000
162+
}))
163+
expect(tx.movements[1]).toEqual(expect.objectContaining({
164+
account: {
165+
type: AccountType.deposit,
166+
instrument: 'KZT',
167+
company: null,
168+
syncIds: ['KZ00000B000000002']
169+
},
170+
sum: -70000
171+
}))
172+
})
173+
174+
it('removes processing marker from merchant title', () => {
175+
const rawTransaction = {
176+
hold: false,
177+
date: '2025-12-09T00:00:00.000',
178+
originalAmount: '-1820.00 KZT',
179+
amount: '-1820.00',
180+
description: 'Покупка в обработке YANDEX.GO ALMATY KZ',
181+
statementUid: 'processing-marker-uid',
182+
originString: '09.12.2025 -1,820.00 ₸ KZT Сумма в обработке Покупка в обработке YANDEX.GO ALMATY KZ'
183+
}
184+
const account = { id: 'KZCARD0001', instrument: 'KZT' }
185+
const currencyRates = {}
186+
187+
const converted = convertPdfStatementTransaction(rawTransaction as any, account, currencyRates)
188+
if (converted == null) {
189+
throw new Error('Expected conversion result')
190+
}
191+
const tx = converted.transaction
192+
expect(tx.merchant).toEqual({
193+
title: 'YANDEX.GO',
194+
city: 'Almaty',
195+
country: 'Kazakhstan',
196+
mcc: null,
197+
location: null,
198+
category: null
199+
})
200+
expect(tx.hold).toBe(true)
201+
})
202+
203+
it('treats deposit payout by contract as transfer from deposit to card', () => {
204+
const rawTransaction = {
205+
hold: false,
206+
date: '2025-12-20T00:00:00.000Z',
207+
originalAmount: '+70000.00 KZT',
208+
amount: '+70000.00',
209+
description: 'Выплата вклада по Договору №\nKZ00000B000000002KZT от 01.02.2025.\nВкладчик: Тестовый Тест',
210+
statementUid: 'deposit-payout-uid',
211+
originString: '20.12.2025 +70,000.00 ₸ KZT Выплата вклада по Договору №\nKZ00000B000000002KZT от 01.02.2025.\nВкладчик: Тестовый Тест'
212+
}
213+
const account = { id: 'KZCARD0001', instrument: 'KZT' }
214+
const currencyRates = {}
215+
216+
const converted = convertPdfStatementTransaction(rawTransaction as any, account, currencyRates)
217+
if (converted == null) {
218+
throw new Error('Expected conversion result')
219+
}
220+
const tx = converted.transaction
221+
expect(tx.movements).toHaveLength(2)
222+
expect(tx.movements[0]).toEqual(expect.objectContaining({
223+
account: { id: 'KZCARD0001' },
224+
sum: 70000
225+
}))
226+
expect(tx.movements[1]).toEqual(expect.objectContaining({
227+
account: {
228+
type: AccountType.deposit,
229+
instrument: 'KZT',
230+
company: null,
231+
syncIds: ['KZ00000B000000002']
232+
},
233+
sum: -70000
234+
}))
235+
})
236+
237+
it('uses ccard counterpart for transfer when source account is deposit statement account', () => {
238+
const rawTransaction = {
239+
hold: false,
240+
date: '2025-12-20T00:00:00.000Z',
241+
originalAmount: '-70000.00 KZT',
242+
amount: '-70000.00',
243+
description: 'Выплата вклада по Договору № KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест',
244+
statementUid: 'deposit-source-uid',
245+
originString: '20.12.2025 -70,000.00 ₸ KZT Другое Выплата вклада по Договору № KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест'
246+
}
247+
const account = { id: 'KZ00000B000000002KZT', instrument: 'KZT', type: AccountType.deposit }
248+
const currencyRates = {}
249+
250+
const converted = convertPdfStatementTransaction(rawTransaction as any, account as any, currencyRates)
251+
if (converted == null) {
252+
throw new Error('Expected conversion result')
253+
}
254+
const tx = converted.transaction
255+
expect(tx.movements).toHaveLength(2)
256+
expect(tx.movements[0]).toEqual(expect.objectContaining({
257+
account: { id: 'KZ00000B000000002KZT' },
258+
sum: -70000
259+
}))
260+
expect(tx.movements[1]).toEqual(expect.objectContaining({
261+
account: {
262+
type: AccountType.ccard,
263+
instrument: 'KZT',
264+
company: null,
265+
syncIds: null
266+
},
267+
sum: 70000
268+
}))
269+
expect(tx.comment).toBeNull()
270+
})
271+
272+
it('does not set merchant or comment for contract reference transfer text', () => {
273+
const rawTransaction = {
274+
hold: false,
275+
date: '2026-01-10T00:00:00.000Z',
276+
originalAmount: '-30000.00 KZT',
277+
amount: '-30000.00',
278+
description: 'No KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест',
279+
statementUid: 'contract-ref-uid',
280+
originString: '10.01.2026 -30,000.00 ₸ KZT Другое No KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест'
281+
}
282+
const account = { id: 'KZ00000B000000002KZT', instrument: 'KZT', type: AccountType.deposit }
283+
const currencyRates = {}
284+
285+
const converted = convertPdfStatementTransaction(rawTransaction as any, account as any, currencyRates)
286+
if (converted == null) {
287+
throw new Error('Expected conversion result')
288+
}
289+
const tx = converted.transaction
290+
expect(tx.comment).toBeNull()
291+
expect(tx.merchant).toBeNull()
292+
})
293+
294+
it('parses card top-up from deposit reference as transfer with deposit sync id', () => {
295+
const rawTransaction = {
296+
hold: false,
297+
date: '2026-01-15T00:00:00.000Z',
298+
originalAmount: '+35000.00 KZT',
299+
amount: '+35000.00',
300+
description: 'Другое KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест',
301+
statementUid: 'card-topup-from-deposit-uid',
302+
originString: '15.01.2026 +35,000.00 ₸ KZT Другое KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест'
303+
}
304+
const account = { id: 'KZCARD0001', instrument: 'KZT', type: AccountType.ccard }
305+
const currencyRates = {}
306+
307+
const converted = convertPdfStatementTransaction(rawTransaction as any, account as any, currencyRates)
308+
if (converted == null) {
309+
throw new Error('Expected conversion result')
310+
}
311+
const tx = converted.transaction
312+
expect(tx.movements).toHaveLength(2)
313+
expect(tx.movements[0]).toEqual(expect.objectContaining({
314+
account: { id: 'KZCARD0001' },
315+
sum: 35000
316+
}))
317+
expect(tx.movements[1]).toEqual(expect.objectContaining({
318+
account: {
319+
type: AccountType.deposit,
320+
instrument: 'KZT',
321+
company: null,
322+
syncIds: ['KZ00000B000000002']
323+
},
324+
sum: -35000
325+
}))
326+
expect(tx.comment).toBeNull()
327+
expect(tx.merchant).toBeNull()
328+
})
329+
330+
it('does not treat ccard as deposit source even if account id ends with currency', () => {
331+
const rawTransaction = {
332+
hold: false,
333+
date: '2026-01-17T00:00:00.000Z',
334+
originalAmount: '+10000.00 KZT',
335+
amount: '+10000.00',
336+
description: 'Другое KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест',
337+
statementUid: 'ccard-id-format-uid',
338+
originString: '17.01.2026 +10,000.00 ₸ KZT Другое KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест'
339+
}
340+
const account = { id: 'KZ43551B829618809KZT', instrument: 'KZT', type: AccountType.ccard }
341+
const currencyRates = {}
342+
343+
const converted = convertPdfStatementTransaction(rawTransaction as any, account as any, currencyRates)
344+
if (converted == null) {
345+
throw new Error('Expected conversion result')
346+
}
347+
const tx = converted.transaction
348+
expect(tx.movements).toHaveLength(2)
349+
expect(tx.movements[0]).toEqual(expect.objectContaining({
350+
account: { id: 'KZ43551B829618809KZT' },
351+
sum: 10000
352+
}))
353+
expect(tx.movements[1]).toEqual(expect.objectContaining({
354+
account: {
355+
type: AccountType.deposit,
356+
instrument: 'KZT',
357+
company: null,
358+
syncIds: ['KZ00000B000000002']
359+
},
360+
sum: -10000
361+
}))
362+
})
363+
364+
it('normalizes wrong negative sign for payout transfer on card statement to income', () => {
365+
const rawTransaction = {
366+
hold: false,
367+
date: '2026-01-16T00:00:00.000Z',
368+
originalAmount: '-35000.00 KZT',
369+
amount: '-35000.00',
370+
description: 'Другое KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест',
371+
statementUid: 'card-topup-wrong-sign-uid',
372+
originString: '16.01.2026 -35,000.00 ₸ KZT Другое Выплата вклада по Договору No KZ00000B000000002KZT от 01.02.2025. Вкладчик: Тестовый Тест'
373+
}
374+
const account = { id: 'KZCARD0001', instrument: 'KZT', type: AccountType.ccard }
375+
const currencyRates = {}
376+
377+
const converted = convertPdfStatementTransaction(rawTransaction as any, account as any, currencyRates)
378+
if (converted == null) {
379+
throw new Error('Expected conversion result')
380+
}
381+
const tx = converted.transaction
382+
expect(tx.movements).toHaveLength(2)
383+
expect(tx.movements[0]).toEqual(expect.objectContaining({
384+
account: { id: 'KZCARD0001' },
385+
sum: 35000
386+
}))
387+
expect(tx.movements[1]).toEqual(expect.objectContaining({
388+
account: {
389+
type: AccountType.deposit,
390+
instrument: 'KZT',
391+
company: null,
392+
syncIds: ['KZ00000B000000002']
393+
},
394+
sum: -35000
395+
}))
396+
})
139397
})

src/plugins/ffin-kz/__tests__/converters/transactions/skippedTransaction.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('convertTransaction', () => {
1313
originString: '09.05.2025 -0.00 ₸ KZT Другое'
1414
},
1515
{
16-
id: 'KZ60551Z329356713',
16+
id: 'KZ00000Z000000003',
1717
instrument: 'KZT'
1818
},
1919
null

src/plugins/ffin-kz/__tests__/index/dropOffsettingSingles.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('dropOffsettingSinglesSameAccount', () => {
66
const date = new Date('2025-11-28T10:00:00.000Z')
77
const baseMovement = (sum: number): Movement => ({
88
id: sum > 0 ? 'plus' : 'minus',
9-
account: { id: 'KZ987654312KZT' },
9+
account: { id: 'KZ000000000000777KZT' },
1010
invoice: null,
1111
sum,
1212
fee: 0

src/plugins/ffin-kz/__tests__/index/dropPlaceholderTransfers.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('dropPlaceholderTransfers', () => {
1212
movements: [
1313
{
1414
id: 'real',
15-
account: { id: 'KZ987654312KZT' },
15+
account: { id: 'ACC-DEPOSIT-RAW' },
1616
invoice: null,
1717
sum: 40000,
1818
fee: 0
@@ -55,4 +55,33 @@ describe('dropPlaceholderTransfers', () => {
5555
const merchant = kept[0].merchant as any
5656
expect(merchant?.title ?? merchant?.fullTitle).toBe('keep')
5757
})
58+
59+
it('keeps valid transfer from deposit statement to ccard placeholder', () => {
60+
const date = new Date('2026-02-01T19:00:00.000Z')
61+
const depositToCardTransfer: Transaction = {
62+
hold: false,
63+
date,
64+
comment: null,
65+
merchant: null,
66+
movements: [
67+
{
68+
id: 'from-deposit',
69+
account: { id: 'KZ00000B000000002KZT' },
70+
invoice: null,
71+
sum: -70000,
72+
fee: 0
73+
},
74+
{
75+
id: null,
76+
account: { type: AccountType.ccard, instrument: 'KZT', company: null, syncIds: null },
77+
invoice: null,
78+
sum: 70000,
79+
fee: 0
80+
}
81+
]
82+
}
83+
84+
const kept = dropPlaceholderTransfers([depositToCardTransfer])
85+
expect(kept).toHaveLength(1)
86+
})
5887
})

0 commit comments

Comments
 (0)