Skip to content

Commit

Permalink
[postal-savings-rs] Add card transactions fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
osipxd committed Apr 22, 2024
1 parent d414f5a commit b434910
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { convertCardTransactions } from '../../../converters'

const input = `
<tr>
<td align="left">05.04.2024</td>
<td align="right">-3.282,63</td>
<td align="left">RSD</td>
<td align="left">09.04.2024</td>
<td align="left">Wolt doo </td>
<td align="right">4870........1234</td>
</tr>
<tr>
<td align="left">06.04.2024</td>
<td align="right">-370,00</td>
<td align="left">RSD</td>
<td align="left">09.04.2024</td>
<td align="left">LP NOVI CAJ VLADIMIR NOVI </td>
<td align="right">4870........1234</td>
</tr>
<tr>
<td align="left">07.04.2024</td>
<td align="right">500,00</td>
<td align="left">EUR</td>
<td align="left">10.04.2024</td>
<td align="left">REFUND U INOSTRANSTVU </td>
<td align="right">4870........1234</td>
</tr>
<tr>
<td align="left">08.04.2024</td>
<td align="right">-210,00</td>
<td align="left">RSD</td>
<td align="left">10.04.2024</td>
<td align="left">TISHLER TOBACCO AND COFFENOVI </td>
<td align="right">4870........1234</td>
</tr>
<tr>
<td align="left">12.04.2024</td>
<td align="right">20,00</td>
<td align="left">USD</td>
<td align="left"> </td>
<td align="left">CHATGPT SUBSCRIPTION +1415</td>
<td align="right">4870........1234</td>
</tr>
<tr>
<td align="left">13.04.2024</td>
<td align="right">10.000,00</td>
<td align="left">RSD</td>
<td align="left"> </td>
<td align="left">ATMBPS KAFE RANDEVU NOVI </td>
<td align="right">4870........1234</td>
</tr>
<tr>
<td align="left">13.04.2024</td>
<td align="right">250,00</td>
<td align="left">EUR</td>
<td align="left"> </td>
<td align="left">REFUND U INOSTRANSTVU </td>
<td align="right">4870........1234</td>
</tr>
`

describe('convertCardTransactions', () => {
it('converts card transactions', () => {
expect(convertCardTransactions(input)).toEqual([
{
date: new Date('2024-04-13T00:00:00.000'),
authorizationDate: null,
amount: {
sum: 250.00,
instrument: 'EUR'
},
merchant: 'REFUND U INOSTRANSTVU'
},
{
date: new Date('2024-04-13T00:00:00.000'),
authorizationDate: null,
amount: {
sum: -10_000.00,
instrument: 'RSD'
},
merchant: 'ATMBPS KAFE RANDEVU'
},
{
date: new Date('2024-04-12T00:00:00.000'),
authorizationDate: null,
amount: {
sum: -20.00,
instrument: 'USD'
},
merchant: 'CHATGPT SUBSCRIPTION'
},
{
date: new Date('2024-04-08T00:00:00.000'),
authorizationDate: new Date('2024-04-10T00:00:00.000'),
amount: {
sum: -210.00,
instrument: 'RSD'
},
merchant: 'TISHLER TOBACCO AND COFFE'
},
{
date: new Date('2024-04-07T00:00:00.000'),
authorizationDate: new Date('2024-04-10T00:00:00.000'),
amount: {
sum: 500.00,
instrument: 'EUR'
},
merchant: 'REFUND U INOSTRANSTVU'
},
{
date: new Date('2024-04-06T00:00:00.000'),
authorizationDate: new Date('2024-04-09T00:00:00.000'),
amount: {
sum: -370.00,
instrument: 'RSD'
},
merchant: 'LP NOVI CAJ VLADIMIR'
},
{
date: new Date('2024-04-05T00:00:00.000'),
authorizationDate: new Date('2024-04-09T00:00:00.000'),
amount: {
sum: -3282.63,
instrument: 'RSD'
},
merchant: 'Wolt doo'
}
])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { convertTransactions } from '../../../converters'

describe('payment', () => {
it('payment', () => {
expect(convertTransactions({
id: 123456789,
type: 5
}, '<tr>\n' +
expect(convertTransactions(
'1234567895',
'<tr>\n' +
'<td align="right" width="80" nowrap>05.12.2023</td>\n' +
'<td align="left" width="130" nowrap>ISPLATA VISA&nbsp;NICEFOODS</td>\n' +
'<td align="right" width="100" nowrap>EUR 978</td>\n' +
Expand Down
19 changes: 17 additions & 2 deletions src/plugins/postal-savings-rs/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { accountDetailsToId, convertAccount } from './converters'
import { accountDetailsToId, convertAccount, convertCardTransactions, convertTransactions } from './converters'
import { AccountDetails, PBAccount } from './models'
import { fetchAccountData } from './fetchApi'
import { fetchAccountData, fetchCardTransactions } from './fetchApi'
import { Transaction } from '../../types/zenmoney'

function accountCardKey (accountId: string): string {
return `account/${accountId}/card`
Expand Down Expand Up @@ -31,3 +32,17 @@ async function readCardNumber (prompt: string): Promise<string | null> {
const cardNumber = (await ZenMoney.readLine(prompt, { inputType: 'number' }) ?? '').replace(/\D/g, '')
return cardNumber === '' || cardNumber === '0' ? null : cardNumber
}

export async function fetchTransactions (account: PBAccount, fromDate: Date, toDate: Date): Promise<Transaction[]> {
const accountTransactions = convertTransactions(account.id, account.rawData)

if (account.cardNumber !== null) {
const cardTransactions = await fetchCardTransactions(account.cardNumber, fromDate, toDate)

// TODO: Remove after debug
console.log(cardTransactions)
console.log(convertCardTransactions(cardTransactions))
}

return accountTransactions.filter(transaction => transaction.date >= fromDate && transaction.date <= toDate)
}
58 changes: 52 additions & 6 deletions src/plugins/postal-savings-rs/converters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AccountOrCard, AccountType, Transaction } from '../../types/zenmoney'
import { AccountDetails } from './models'
import { AccountDetails, CardTransaction } from './models'
import moment from 'moment'

export function accountDetailsToId (account: AccountDetails): string {
Expand All @@ -10,6 +10,16 @@ function parseSum (s: string): number {
return parseFloat(s.replace(' ', '').replace('.', '').replace(',', '.'))
}

function parseDate (s: string): Date {
return moment(s, 'DD.MM.YYYY').toDate()
}

function tableLineRegexp (columns: number): RegExp {
const cellPattern = '<td[^>]*>([^<]+)<\\/td>\\s+'
const regexStr = `<tr>\\s+${cellPattern.repeat(columns)}<\\/tr>`
return new RegExp(regexStr, 'g')
}

function descriptionCleanup (s: string): string {
const substrToRemove = ['ISPLATA VISA', 'UPLATA VISA']

Expand All @@ -24,6 +34,11 @@ function descriptionCleanup (s: string): string {
return strWithoutGarbage.length > 0 ? strWithoutGarbage : decodedStr
}

// 'TRGOCENTAR DOO BEOGR' -> 'TRGOCENTAR DOO'
function stripLocationSuffix (line: string): string {
return line.slice(0, -5).trim()
}

export function convertAccount (account: AccountDetails, data: string): AccountOrCard {
const id = accountDetailsToId(account)

Expand All @@ -44,22 +59,53 @@ export function convertAccount (account: AccountDetails, data: string): AccountO
}
}

export function convertTransactions (account: AccountDetails, data: string): Transaction[] {
export function convertCardTransactions (data: string): CardTransaction[] {
const transactions: CardTransaction[] = []

// For some reason all transactions without authorization have positive sign, so make it negative.
const fixAmount = (transaction: CardTransaction) => {
if (!transaction.authorizationDate && transaction.amount.sum > 0 && !transaction.merchant.startsWith('REFUND ')) {
transaction.amount.sum *= -1
}
}

const matches = data.matchAll(tableLineRegexp(6))

for (const t of matches) {
const [, date, sum, currency, authorizationDate, desc] = t

const transaction: CardTransaction = {
date: parseDate(date),
authorizationDate: (authorizationDate.trim() !== '') ? parseDate(authorizationDate) : null,
amount: {
sum: parseSum(sum),
instrument: currency
},
merchant: descriptionCleanup(stripLocationSuffix(desc))
}
fixAmount(transaction)

transactions.push(transaction)
}

return transactions.reverse()
}

export function convertTransactions (accountId: string, data: string): Transaction[] {
const transactions: Transaction[] = []

const regexp = /<tr>\s+<td[^>]+>([\d.]+)<\/td>\s+<td[^>]+>([^<]+)<\/td>\s+<td[^>]+>(\w+)\s\d+<\/td>\s+<td[^>]+>\+?([-\d,.]+)<\/td>/g
const matches = data.matchAll(regexp)
const matches = data.matchAll(tableLineRegexp(6))

for (const t of matches) {
const [, date, desc, , sum] = t

transactions.push({
hold: false,
date: moment(date, 'DD.MM.YYYY').toDate(),
date: parseDate(date),
movements: [
{
id: null,
account: { id: accountDetailsToId(account) },
account: { id: accountId },
// TODO: parse from /kartizv.jsp
invoice: null,
sum: parseSum(sum),
Expand Down
18 changes: 18 additions & 0 deletions src/plugins/postal-savings-rs/fetchApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FetchResponse, fetch } from '../../common/network'
import { toAtLeastTwoDigitsString } from '../../common/stringUtils'
import { InvalidLoginOrPasswordError } from '../../errors'
import { AccountDetails, Preferences } from './models'

Expand Down Expand Up @@ -104,3 +105,20 @@ export async function fetchAccountData (account: AccountDetails): Promise<string

return response.body as string
}

export async function fetchCardTransactions (cardNumber: string, fromDate: Date, toDate: Date): Promise<string> {
fromDate = fromDate.getFullYear() >= toDate.getFullYear() ? fromDate : new Date(toDate.getFullYear(), 0, 1)

const fromDay = toAtLeastTwoDigitsString(fromDate.getDate())
const fromMonth = toAtLeastTwoDigitsString(fromDate.getMonth() + 1)
const toDay = toAtLeastTwoDigitsString(toDate.getDate())
const toMonth = toAtLeastTwoDigitsString(toDate.getMonth() + 1)
const year = toDate.getFullYear()

const response = await fetchUrl('karttrn.jsp', {
method: 'POST',
body: `KOM=K3&H1=${cardNumber}&IRADIO=I2&oddan=${fromDay}&odmes=${fromMonth}&dodan=${toDay}&domes=${toMonth}&god=${year}`
})

return response.body as string
}
8 changes: 3 additions & 5 deletions src/plugins/postal-savings-rs/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ScrapeFunc, Transaction, Account } from '../../types/zenmoney'
import { fetchAllAccounts, fetchAuthorization } from './fetchApi'
import { Preferences } from './models'
import { accountDetailsToId, convertTransactions } from './converters'
import { fetchAccount } from './api'
import { accountDetailsToId } from './converters'
import { fetchAccount, fetchTransactions } from './api'

export const scrape: ScrapeFunc<Preferences> = async ({ preferences, fromDate, toDate }) => {
toDate = toDate ?? new Date()
Expand All @@ -17,10 +17,8 @@ export const scrape: ScrapeFunc<Preferences> = async ({ preferences, fromDate, t
if (ZenMoney.isAccountSkipped(accountDetailsToId(accountDetails))) continue

const account = await fetchAccount(accountDetails)
const filteredTransactions = convertTransactions(accountDetails, account.rawData)
.filter(transaction => transaction.date >= fromDate && transaction.date <= toDate)
accounts.push(account)
transactions.push(...filteredTransactions)
transactions.push(...await fetchTransactions(account, fromDate, toDate))
}

return {
Expand Down
9 changes: 8 additions & 1 deletion src/plugins/postal-savings-rs/models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AccountOrCard } from '../../types/zenmoney'
import { AccountOrCard, Amount } from '../../types/zenmoney'

// Input preferences from schema in preferences.xml
export interface Preferences {
Expand All @@ -22,3 +22,10 @@ export interface PBAccount extends AccountOrCard {
cardNumber: string | null
rawData: string
}

export interface CardTransaction {
date: Date
authorizationDate: Date | null
amount: Amount
merchant: string
}

0 comments on commit b434910

Please sign in to comment.