]*>\\s+${cellPattern.repeat(columns)}<\\/tr>`
+ return new RegExp(regexStr, 'g')
+}
+
function descriptionCleanup (s: string): string {
const substrToRemove = ['ISPLATA VISA', 'UPLATA VISA']
@@ -24,7 +35,12 @@ function descriptionCleanup (s: string): string {
return strWithoutGarbage.length > 0 ? strWithoutGarbage : decodedStr
}
-export function convertAccount (account: AccountDetails, data: string): Account {
+// '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)
const descriptionPattern = /([\w\s]+):<\/td>\s+ | \d+<\/td>/
@@ -44,22 +60,47 @@ export function convertAccount (account: AccountDetails, data: string): Account
}
}
-export function convertTransactions (account: AccountDetails, data: string): Transaction[] {
+export function convertCardTransactions (data: string): CardTransaction[] {
+ const transactions: CardTransaction[] = []
+
+ 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: {
+ // Amount sign is not true for unauthorized transaction so ignore it.
+ // We will restore the sign later according to a corresponding account transaction's sign
+ sum: Math.abs(parseSum(sum)),
+ instrument: currency
+ },
+ merchant: descriptionCleanup(stripLocationSuffix(desc))
+ }
+
+ transactions.push(transaction)
+ }
+
+ return transactions.reverse()
+}
+
+export function convertTransactions (accountId: string, data: string): Transaction[] {
const transactions: Transaction[] = []
- const regexp = / |
\s+]+>([\d.]+)<\/td>\s+ | ]+>([^<]+)<\/td>\s+ | ]+>(\w+)\s\d+<\/td>\s+ | ]+>\+?([-\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),
@@ -79,3 +120,18 @@ export function convertTransactions (account: AccountDetails, data: string): Tra
return transactions.reverse()
}
+
+export function convertExchangeRates (data: string): ExchangeRatesMap {
+ const exchangeRates: ExchangeRatesMap = new Map()
+
+ const cellContentPattern = '(?:)?([^<]+)(?: )?'
+ const matches = data.matchAll(tableLineRegexp(9, cellContentPattern))
+
+ for (const t of matches) {
+ // Currency code, Currency designation, Country, Unit, Buying rate, Middle rate, Selling rate, Buying rate (cash), Selling rate (cash)
+ const [, , currency, , , , middleRate] = t
+ exchangeRates.set(currency, parseFloat(middleRate.trim()))
+ }
+
+ return exchangeRates
+}
diff --git a/src/plugins/postal-savings-rs/fetchApi.ts b/src/plugins/postal-savings-rs/fetchApi.ts
index 4b7954c0..b0dd33cd 100644
--- a/src/plugins/postal-savings-rs/fetchApi.ts
+++ b/src/plugins/postal-savings-rs/fetchApi.ts
@@ -1,6 +1,8 @@
import { FetchResponse, fetch } from '../../common/network'
+import { toAtLeastTwoDigitsString } from '../../common/stringUtils'
import { InvalidLoginOrPasswordError } from '../../errors'
import { AccountDetails, Preferences } from './models'
+import moment from 'moment'
const baseUrl = 'https://hb.posted.co.rs/posted/en/'
@@ -104,3 +106,30 @@ export async function fetchAccountData (account: AccountDetails): Promise {
+ 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
+}
+
+export async function fetchExchangeRates (date: Date | null): Promise {
+ const formattedDate = date !== null ? moment(date).format('DD.MM.YYYY') : ''
+ const response = await fetchUrl('kursl.jsp', {
+ method: 'POST',
+ body: `DATUM=${formattedDate}&check1=on`
+ })
+
+ return response.body as string
+}
diff --git a/src/plugins/postal-savings-rs/index.ts b/src/plugins/postal-savings-rs/index.ts
index f83ce369..d88ab1b7 100644
--- a/src/plugins/postal-savings-rs/index.ts
+++ b/src/plugins/postal-savings-rs/index.ts
@@ -1,7 +1,8 @@
import { ScrapeFunc, Transaction, Account } from '../../types/zenmoney'
-import { fetchAllAccounts, fetchAuthorization, fetchAccountData } from './fetchApi'
+import { fetchAllAccounts, fetchAuthorization } from './fetchApi'
import { Preferences } from './models'
-import { convertAccount, convertTransactions } from './converters'
+import { accountDetailsToId } from './converters'
+import { fetchAccount, fetchTransactions } from './api'
export const scrape: ScrapeFunc = async ({ preferences, fromDate, toDate }) => {
toDate = toDate ?? new Date()
@@ -12,12 +13,14 @@ export const scrape: ScrapeFunc = async ({ preferences, fromDate, t
const accounts: Account[] = []
const transactions: Transaction[] = []
- for (const account of fetchedAccounts) {
- const rawAccountData = await fetchAccountData(account)
- accounts.push(convertAccount(account, rawAccountData))
- const filteredTransactions = convertTransactions(account, rawAccountData)
- .filter(transaction => transaction.date >= fromDate && (toDate == null || transaction.date <= toDate))
- transactions.push(...filteredTransactions)
+ for (const accountDetails of fetchedAccounts) {
+ if (ZenMoney.isAccountSkipped(accountDetailsToId(accountDetails))) {
+ continue
+ }
+
+ const account = await fetchAccount(accountDetails)
+ accounts.push(account)
+ transactions.push(...await fetchTransactions(account, fromDate, toDate))
}
return {
diff --git a/src/plugins/postal-savings-rs/models.ts b/src/plugins/postal-savings-rs/models.ts
index 3d5f6837..daff85a5 100644
--- a/src/plugins/postal-savings-rs/models.ts
+++ b/src/plugins/postal-savings-rs/models.ts
@@ -1,3 +1,5 @@
+import { AccountOrCard, Amount } from '../../types/zenmoney'
+
// Input preferences from schema in preferences.xml
export interface Preferences {
login: string
@@ -15,3 +17,18 @@ export interface AccountDetails {
id: number
type: AccountType
}
+
+export interface PSAccount extends AccountOrCard {
+ cardNumber: string | null
+ rawData: string
+}
+
+export interface CardTransaction {
+ date: Date
+ authorizationDate: Date | null
+ amount: Amount
+ accountSum?: number
+ merchant: string
+}
+
+export type ExchangeRatesMap = Map
|