diff --git a/src/plugins/altabanka-rs/ZenmoneyManifest.xml b/src/plugins/altabanka-rs/ZenmoneyManifest.xml new file mode 100755 index 00000000..eed2908d --- /dev/null +++ b/src/plugins/altabanka-rs/ZenmoneyManifest.xml @@ -0,0 +1,14 @@ + + + altabanka-rs + 1.0 + 1 + 15639 + true + false + + index.js + preferences.xml + + + diff --git a/src/plugins/altabanka-rs/__tests__/api/__mocks__/accounts.html b/src/plugins/altabanka-rs/__tests__/api/__mocks__/accounts.html new file mode 100644 index 00000000..c7383c5f --- /dev/null +++ b/src/plugins/altabanka-rs/__tests__/api/__mocks__/accounts.html @@ -0,0 +1,1140 @@ + + + + + + + + Altabanka Srbija + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ profile-img +
+ +
+
+ + +
+
+ +
+ +
+ + + + + + + + +
+
+ cover + cover +
+
+ + +
+
+
+ + + +
    +
  • slider
  • +
  • list
  • +
+ + + + + +
+
+
+ +
+ +
+
+ + + + + + + + + + + + +
+ +
+
+ + + + + + + + + diff --git a/src/plugins/altabanka-rs/__tests__/api/__mocks__/login.html b/src/plugins/altabanka-rs/__tests__/api/__mocks__/login.html new file mode 100644 index 00000000..629d5710 --- /dev/null +++ b/src/plugins/altabanka-rs/__tests__/api/__mocks__/login.html @@ -0,0 +1,23 @@ +
+
+ + + +
+
diff --git a/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-get.html b/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-get.html new file mode 100644 index 00000000..0ccf8050 --- /dev/null +++ b/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-get.html @@ -0,0 +1,1381 @@ + + + + + + + + Altabanka Srbija + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ profile-img +
+ +
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + +
+
+ + + +
+ + +
+ + + + + +
+
+ + + +
+ + + +
+

+ Lista transakcija za period 13.12.2022 - + 13.12.2024 +

+ +
+
+
+

+ Datum + +

+
+
+

+ Platilac/Primalac + +

+
+
+

+ Opis + +

+
+
+

+ Iznos + +

+
+
+ +
+
+
+ C8
+

11.10.2024

+
+
+

+ + Za: CITY 86 BEOGRAD + +
+ + +

+
+
+

+ Kartica: LP ANDREY SERAPIONOV PR + BE +

+
+
+

5,000.00 RSD

+
+
+
+
+
+ C8
+

11.10.2024

+
+
+

+ + Za: CITY 86 BEOGRAD + +
+ + +

+
+
+

+ Kartica: BLVCK SUGAR DOO BE +

+
+
+

850.00 RSD

+
+
+
+
+
+ HT
+

11.10.2024

+
+
+

+ + Za: HVALA TIPS POS V BE... + +
+ + +

+
+
+

+ Kartica: SELECTIVE CENTAR KNEZ + BE +

+
+
+

2,150.00 RSD

+
+
+
+
+
+ PV
+

09.10.2024

+
+
+

+ + Za: PP VIDIN KAPIJA A1... + +
+ + +

+
+
+

+ PP VIDIN KAPIJA A1 BEOGRAD +

+
+
+

450.00 RSD

+
+
+
+
+
+ 2-
+

09.10.2024

+
+
+

+ + Za: 213 - MAXI 139 BE... + +
+ + +

+
+
+

+ Kartica: LILLY APOTEKA 269 BE +

+
+
+

1,305.95 RSD

+
+
+
+
+
+ HG
+

08.10.2024

+
+
+

+ + Za: H&M GALERIJA RS 0520... + +
+ + +

+
+
+

+ H&M GALERIJA RS 0520 + BEOGRAD +

+
+
+

1,117.00 RSD

+
+
+
+
+
+ HG
+

08.10.2024

+
+
+

+ + Za: H&M GALERIJA RS 0520... + +
+ + +

+
+
+

+ H&M GALERIJA RS 0520 + BEOGRAD +

+
+
+

6,081.00 RSD

+
+
+
+
+
+ DD
+

08.10.2024

+
+
+

+ + Za: DON DON LULU 23 BE... + +
+ + +

+
+
+

+ DON DON LULU 23 BEOGRAD SAVSKI +

+
+
+

393.00 RSD

+
+
+
+
+
+ PC
+

08.10.2024

+
+
+

+ + Za: PAYSPOT CORNER 12... + +
+ + +

+
+
+

+ PAYSPOT CORNER 12 BEOGRAD + SAVSKI +

+
+
+

1,999.00 RSD

+
+
+
+
+
+ BP
+

08.10.2024

+
+
+

+ + Za: BENU PHARMACIES 393... + +
+ + +

+
+
+

+ Kartica: SUSHIRRITO BW BE +

+
+
+

2,230.00 RSD

+
+
+
+ + + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + + + + + +
+
+ + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-post-1.html b/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-post-1.html new file mode 100644 index 00000000..473245e5 --- /dev/null +++ b/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-post-1.html @@ -0,0 +1,1129 @@ + + + + + + + + Altabanka Srbija + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ profile-img +
+ +
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + +
+
+ + + + + +
+
+ + + +
+ + + +
+

+ Transaction history for period 01.12.2022 - 01.12.2024 +

+ +
+
+
+

+ Date + +

+
+
+

+ From/To + +

+
+
+

+ Description + +

+
+
+

+ Amount + +

+
+
+ +
+
+
+ C8
+

11.10.2024

+
+
+

+ + To: CITY 86 BEOGRAD + +
+ + +

+
+
+

+ Kartica: LP ANDREY SERAPIONOV PR BE +

+
+
+

5,000.00 RSD

+
+
+
+
+
+ C8
+

11.10.2024

+
+
+

+ + To: CITY 86 BEOGRAD + +
+ + +

+
+
+

+ Kartica: BLVCK SUGAR DOO BE +

+
+
+

850.00 RSD

+
+
+
+
+
+ HT
+

11.10.2024

+
+
+

+ + To: HVALA TIPS POS V BE... + +
+ + +

+
+
+

+ Kartica: SELECTIVE CENTAR KNEZ BE +

+
+
+

2,150.00 RSD

+
+
+
+
+
+ PV
+

09.10.2024

+
+
+

+ + To: PP VIDIN KAPIJA A1... + +
+ + +

+
+
+

+ PP VIDIN KAPIJA A1 BEOGRAD +

+
+
+

450.00 RSD

+
+
+
+
+
+ 2-
+

09.10.2024

+
+
+

+ + To: 213 - MAXI 139 BE... + +
+ + +

+
+
+

+ Kartica: LILLY APOTEKA 269 BE +

+
+
+

1,305.95 RSD

+
+
+
+
+
+ HG
+

08.10.2024

+
+
+

+ + To: H&M GALERIJA RS 0520... + +
+ + +

+
+
+

+ H&M GALERIJA RS 0520 BEOGRAD +

+
+
+

1,117.00 RSD

+
+
+
+
+
+ HG
+

08.10.2024

+
+
+

+ + To: H&M GALERIJA RS 0520... + +
+ + +

+
+
+

+ H&M GALERIJA RS 0520 BEOGRAD +

+
+
+

6,081.00 RSD

+
+
+
+
+
+ DD
+

08.10.2024

+
+
+

+ + To: DON DON LULU 23 BE... + +
+ + +

+
+
+

+ DON DON LULU 23 BEOGRAD SAVSKI +

+
+
+

393.00 RSD

+
+
+
+
+
+ PC
+

08.10.2024

+
+
+

+ + To: PAYSPOT CORNER 12... + +
+ + +

+
+
+

+ PAYSPOT CORNER 12 BEOGRAD SAVSKI +

+
+
+

1,999.00 RSD

+
+
+
+
+
+ BP
+

08.10.2024

+
+
+

+ + To: BENU PHARMACIES 393... + +
+ + +

+
+
+

+ Kartica: SUSHIRRITO BW BE +

+
+
+

2,230.00 RSD

+
+
+
+ + + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-post-2.html b/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-post-2.html new file mode 100644 index 00000000..844261d9 --- /dev/null +++ b/src/plugins/altabanka-rs/__tests__/api/__mocks__/transactions-post-2.html @@ -0,0 +1,1382 @@ + + + + + + + + Altabanka Srbija + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ profile-img +
+ +
+
+ + +
+
+ +
+ +
+ + + + + + + + + + + +
+
+ + + + + +
+
+ + + +
+ + + +
+

+ Transaction history for period 01.12.2022 - 01.12.2024 +

+ +
+
+
+

+ Date + +

+
+
+

+ From/To + +

+
+
+

+ Description + +

+
+
+

+ Amount + +

+
+
+ +
+
+
+ K0
+

21.10.2024

+
+
+

+ + To: K-ETA 02156634... + +
+ + +

+
+
+

+ K-ETA 0215663441 +

+
+
+

7.14 EUR

+
+
+
+
+
+ MA
+

21.10.2024

+
+
+

+ + To: MOBILNA APP ONEA PO... + +
+ + +

+
+
+

+ MOBILNA APP ONEA PODGORICA +

+
+
+

4.00 EUR

+
+
+
+
+
+ V1
+

18.10.2024

+
+
+

+ + To: VOLI 1 PODGORIC... + +
+ + +

+
+
+

+ VOLI 1 PODGORICA +

+
+
+

8.33 EUR

+
+
+
+
+
+ MP
+

18.10.2024

+
+
+

+ + To: MARIEN PLATZ PODG... + +
+ + +

+
+
+

+ MARIEN PLATZ PODGORICA +

+
+
+

71.62 EUR

+
+
+
+
+
+ TB
+

14.10.2024

+
+
+

+ + To: THE BRITISH COUNCIL... + +
+ + +

+
+
+

+ THE BRITISH COUNCIL MANCHESTER 23500.00 RSD + Kurs:117.0177 +

+
+
+

201.46 EUR

+
+
+
+
+
+ AI
+

01.10.2024

+
+
+

+ + To: APPLE.COM/BILL IT... + +
+ + +

+
+
+

+ APPLE.COM/BILL ITUNES.COM +

+
+
+

98.99 EUR

+
+
+
+
+
+ AS
+

26.09.2024

+
+
+

+ + To: AIR SERBIA A.D BEOGRAD... + +
+ + +

+
+
+

+ AIR SERBIA A.D BEOGRAD NOVI BEOGRAD 36992.00 + RSD Kurs:117.0936 +

+
+
+

315.92 EUR

+
+
+
+
+
+ CC
+

26.09.2024

+
+
+

+ + To: CHIP CARD AD BEOGRAD... + +
+ + +

+
+
+

+ CHIP CARD AD BEOGRAD KOPAONIK 600.00 RSD + Kurs:117.0936 +

+
+
+

5.12 EUR

+
+
+
+
+
+ AS
+

26.09.2024

+
+
+

+ + To: AIR SERBIA A.D BEOGRAD... + +
+ + +

+
+
+

+ AIR SERBIA A.D BEOGRAD NOVI BEOGRAD 63147.00 + RSD Kurs:117.0936 +

+
+
+

539.29 EUR

+
+
+
+
+
+ AS
+

25.09.2024

+
+
+

+ + To: AIR SERBIA A.D BEOGRAD... + +
+ + +

+
+
+

+ AIR SERBIA A.D BEOGRAD NOVI BEOGRAD 20455.00 + RSD Kurs:117.0917 +

+
+
+

167.16 EUR

+
+
+
+
+
+ A8
+

25.09.2024

+
+
+

+ + To: APPLE.COM/US 800-... + +
+ + +

+
+
+

+ APPLE.COM/US 800-676-2775 99.00 USD + Kurs:1.1195 +

+
+
+

89.33 EUR

+
+
+
+
+
+ PR
+

05.09.2024

+
+
+

+ + To: PAVLOV ROMAN + +
+ 190-0001000164050-10 + +

+
+
+

+ Prodaja deviza +

+
+
+

10.00 EUR

+
+
+
+
+
+ CF
+

30.08.2024

+
+
+

+ + To: CC_NEW FITNESS BE... + +
+ + +

+
+
+

+ CC_NEW FITNESS BEOGRAD 3320.00 RSD + Kurs:117.0222 +

+
+
+

28.37 EUR

+
+
+
+
+
+ CF
+

31.07.2024

+
+
+

+ + To: CC_NEW FITNESS BE... + +
+ + +

+
+
+

+ CC_NEW FITNESS BEOGRAD 3320.00 RSD + Kurs:117.0553 +

+
+
+

28.36 EUR

+
+
+
+
+
+ CS
+

25.07.2024

+
+
+

+ + To: CINEPLEXX SRB DO NO... + +
+ + +

+
+
+

+ CINEPLEXX SRB DO NOVI BEOGRAD 3210.00 RSD + Kurs:117.0680 +

+
+
+

19.86 EUR

+
+
+
+
+
+ PR
+

08.05.2024

+
+
+

+ + To: PAVLOV ROMAN + +
+ 190-0001000164050-10 + +

+
+
+

+ Prodaja deviza +

+
+
+

10.00 EUR

+
+
+
+
+
+
+

14.04.2024

+
+
+

+ + From: + +
+ + +

+
+
+

+ ROMAN PAVLOV +

+
+
+

3,800.00 EUR

+
+
+
+
+
+ FC
+

25.03.2024

+
+
+

+ + To: FAVORIT CGI 6 PETR... + +
+ + +

+
+
+

+ FAVORIT CGI 6 PETROVAC NA M 1000.00 RSD + Kurs:117.2024 +

+
+
+

8.53 EUR

+
+
+
+
+
+
+

08.02.2024

+
+
+

+ + From: + +
+ + +

+
+
+

+ ROMAN PAVLOV +

+
+
+

500.00 EUR

+
+
+
+ + + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/altabanka-rs/__tests__/api/parsers.test.ts b/src/plugins/altabanka-rs/__tests__/api/parsers.test.ts new file mode 100644 index 00000000..42828dfc --- /dev/null +++ b/src/plugins/altabanka-rs/__tests__/api/parsers.test.ts @@ -0,0 +1,351 @@ +import fs from 'fs' +import path from 'path' +import { + parseAccountInfo, + parseLoginResult, + parseRequestVerificationToken, + parseTransactions +} from '../../parsers' + +describe('parsers', () => { + it('should parse login result', async () => { + const mockBody = fs.readFileSync( + path.resolve(__dirname, './__mocks__/login.html'), + 'utf8' + ) + + const response = parseLoginResult(mockBody) + + expect(response).toBe(true) + expect(parseLoginResult('some other text')).toBe(false) + }) + + it('should parse accounts', async () => { + const mockBody = fs.readFileSync( + path.resolve(__dirname, './__mocks__/accounts.html'), + 'utf8' + ) + + const response = parseAccountInfo(mockBody) + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "accountNumber": "0001000512876", + "balance": 249233.58, + "cardNumber": "0xC4952DBB2BB24ECAFF9655505605F0F0", + "currency": "RSD", + "id": "0001000512876-0xC4952DBB2BB24ECAFF9655505605F0F0", + "name": "4242XXXXXXXX4061", + }, + Object { + "accountNumber": "0001000512876", + "balance": 249233.58, + "cardNumber": "0x3A7A78AB70965C9C2323270A293FA54F", + "currency": "RSD", + "id": "0001000512876-0x3A7A78AB70965C9C2323270A293FA54F", + "name": "9891XXXXXXXX6625", + }, + Object { + "accountNumber": "0001000512876", + "balance": 249233.58, + "cardNumber": "", + "currency": "RSD", + "id": "0001000512876", + "name": "Tekući račun", + }, + Object { + "accountNumber": "0001000512877", + "balance": 2979.07, + "cardNumber": "", + "currency": "EUR", + "id": "0001000512877", + "name": "Štedni račun", + }, + ] + `) + }) + + it('should parse verification token', async () => { + const mockBody = fs.readFileSync( + path.resolve(__dirname, './__mocks__/transactions-get.html'), + 'utf8' + ) + + const response = parseRequestVerificationToken(mockBody) + + expect(response).toMatchInlineSnapshot( + '"gRMs7qlFJfglZXJ6LWt9Rrf7uFmYKVrlUJAw9kZJvP3i_2TyXQT8g09XpquVXaLZEXbWPapfQN8LTUDqEcvgUkBoHok94w-t8dlg88U-nFY1"' + ) + }) + + it('should parse card transactions', async () => { + const mockBody = fs.readFileSync( + path.resolve(__dirname, './__mocks__/transactions-post-1.html'), + 'utf8' + ) + + const response = parseTransactions(mockBody) + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "address": "LP ANDREY SERAPIONOV PR BE", + "amount": -5000, + "currency": "RSD", + "date": 2024-10-10T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROQiAnv9eKjzzUz%2BKkcY17oVfGcac1cpGXH2hbQafqYVaR0zItwhlKuOVk0qzP4RfUc8R0cB5b%2Brb", + }, + Object { + "address": "BLVCK SUGAR DOO BE", + "amount": -850, + "currency": "RSD", + "date": 2024-10-10T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROQiAnv9eKjzzX9TfebVLqb1omJLFzKwNFK63CgLt%2BCwFb6mv2B5sE6ZvvPwhZFnucOH9zKee0pDC", + }, + Object { + "address": "SELECTIVE CENTAR KNEZ BE", + "amount": -2150, + "currency": "RSD", + "date": 2024-10-10T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROQiAnv9eKjzzAwZGs4gh3RVQsAS279lTUOIDuDdF0ZB3O%2FFsxX6VBmGsn%2BxHeaSsNZzqSpOlhn%2FS", + }, + Object { + "address": "PP VIDIN KAPIJA A1 BEOGRAD", + "amount": -450, + "currency": "RSD", + "date": 2024-10-08T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROW%2BDYw0B37YdNg3%2B3cjmm40EKzx4BEDfrMQelRZkazIjUpYF2dR4LyoTrZHdVbqdc8vUq%2BjISqxR", + }, + Object { + "address": "LILLY APOTEKA 269 BE", + "amount": -1305.95, + "currency": "RSD", + "date": 2024-10-08T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7RORKfHQXYp29vjWAesZBAPA2vq3eEsdM2vDZeC%2BSqNxG%2FKcf5TopUo9Uqi9YNOHp8yE%2BgMPDeOUL1", + }, + Object { + "address": "H&M GALERIJA RS 0520 BEOGRAD", + "amount": -1117, + "currency": "RSD", + "date": 2024-10-07T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROSByFhq1mSAHUCkr8H4Y8NzC7nAThSgF3KfisQBq%2Bhhu%2BBj8Y0KOoXigSWwxeyxN1zkoico0tVDR", + }, + Object { + "address": "H&M GALERIJA RS 0520 BEOGRAD", + "amount": -6081, + "currency": "RSD", + "date": 2024-10-07T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROSByFhq1mSAHmFaIW1okYp15M6gW5mu9%2FuCwDxxqK7K%2Bg4q8MGJen3Diyg98rhcN3OyAdthi7QbW", + }, + Object { + "address": "DON DON LULU 23 BEOGRAD SAVSKI", + "amount": -393, + "currency": "RSD", + "date": 2024-10-07T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROSByFhq1mSAHuzaP7%2BB9LHcKdGwn2lfDqVzZZVr0qPI5lHa5tVIXpnpKbKpix82xRkmcrlxxivnP", + }, + Object { + "address": "PAYSPOT CORNER 12 BEOGRAD SAVSKI", + "amount": -1999, + "currency": "RSD", + "date": 2024-10-07T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROSByFhq1mSAHnhPD4SwQ5%2BdkzpMrXR3Apu2VJrKwBnVSLhz5T4gyYUsq6Il6maqcGyrre7qV0h0P", + }, + Object { + "address": "SUSHIRRITO BW BE", + "amount": -2230, + "currency": "RSD", + "date": 2024-10-07T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROXCpqc%2FgQY3tMC0oRtmp3FZhztLjcXl7DfINqELg3RYn1YqUZvSoj70jtihxumnx74OHMnl%2BUmGr", + }, + ] + `) + }) + + it('should parse account transactions', async () => { + const mockBody = fs.readFileSync( + path.resolve(__dirname, './__mocks__/transactions-post-2.html'), + 'utf8' + ) + + const response = parseTransactions(mockBody) + + expect(response).toMatchInlineSnapshot(` + Array [ + Object { + "address": "K-ETA 0215663441", + "amount": -7.14, + "currency": "EUR", + "date": 2024-10-20T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROexS%2BMuFAgyqGLsF4zn6wmg2%2Bls9%2B8iDIMN9DKrpLesjKGAnTGDgwRa0CmWUudJmfv5%2FFnM8dI9l", + }, + Object { + "address": "MOBILNA APP ONEA PODGORICA", + "amount": -4, + "currency": "EUR", + "date": 2024-10-20T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROexS%2BMuFAgyqD%2BWpkTldkqwcd41njf%2FDJdlqZFMz0HEfwAnn4IB%2BwvQzycvw%2F2jCJBwfJmv9ymvj", + }, + Object { + "address": "VOLI 1 PODGORICA", + "amount": -8.33, + "currency": "EUR", + "date": 2024-10-17T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7RObwJBTpX2tfccY5WOXPWm5g6XirWQBvUYcxyBq3X9tIh66iV7eS%2Bpk2DCLfxYS%2Bo0cELG8OPzvg%2B", + }, + Object { + "address": "MARIEN PLATZ PODGORICA", + "amount": -71.62, + "currency": "EUR", + "date": 2024-10-17T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7RObwJBTpX2tfcSXO15NgJyAT4Dd%2BqRWWlV4BYbsQZs1moUnIC%2FOtHwGEri%2Fp3hFnlSvvxPAIiz0xS", + }, + Object { + "address": "THE BRITISH COUNCIL MANCHESTER 23500.00 RSD + Kurs:117.0177", + "amount": -201.46, + "currency": "EUR", + "date": 2024-10-13T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROVItA%2F1GrciWf6yiBQboMNZhIFXhNjwOBNY877Ypq%2BQq6T2grgK7sjST6dR6hDkQI0EEjwe0TTRR", + }, + Object { + "address": "APPLE.COM/BILL ITUNES.COM", + "amount": -98.99, + "currency": "EUR", + "date": 2024-09-30T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROeNQ5v0%2BZB%2FLp92ZxoYpjTWuMELQqBegH1PuKDjHm2e3rwj%2ByG5VDg1OZFrSGlg6t3MXgb5D%2BolT", + }, + Object { + "address": "AIR SERBIA A.D BEOGRAD NOVI BEOGRAD 36992.00 + RSD Kurs:117.0936", + "amount": -315.92, + "currency": "EUR", + "date": 2024-09-25T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROeDyeYpiaLJvewDfWrvlOG64LBWMkfmOlNGcepDegFz3t636MfLGsPS0ErAC54B8idWx8cH7zokZ", + }, + Object { + "address": "CHIP CARD AD BEOGRAD KOPAONIK 600.00 RSD + Kurs:117.0936", + "amount": -5.12, + "currency": "EUR", + "date": 2024-09-25T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROeDyeYpiaLJv%2Fl7BwqxRalCcAa6g2Nv2IYQlD9jr3%2FHvORIjAFf2IjISI8X6jMij7vajo%2Fm8CIvZ", + }, + Object { + "address": "AIR SERBIA A.D BEOGRAD NOVI BEOGRAD 63147.00 + RSD Kurs:117.0936", + "amount": -539.29, + "currency": "EUR", + "date": 2024-09-25T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROeDyeYpiaLJvnp1c2%2FCl5OZ3DPyOExYV3GVElhraeyusFlT8JkDbp7y1NN8JPA2%2Bjc8OONpKh4K6", + }, + Object { + "address": "AIR SERBIA A.D BEOGRAD NOVI BEOGRAD 20455.00 + RSD Kurs:117.0917", + "amount": -167.16, + "currency": "EUR", + "date": 2024-09-24T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROV3Qa3XqC9Rhi5rpkiLRsN0oWg85cW3c%2FD9kPnaO%2FTLMOOBqFL3fy6%2FeIhaFdxeNXotNK41IhZvL", + }, + Object { + "address": "APPLE.COM/US 800-676-2775 99.00 USD + Kurs:1.1195", + "amount": -89.33, + "currency": "EUR", + "date": 2024-09-24T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROV3Qa3XqC9RhUhavsPPoyv9nShr%2Bj7yuZ%2B2P2MRKxi%2FRHsjWSj78bzYS1DZyLjl0EpifURazx0Me", + }, + Object { + "address": "Prodaja deviza", + "amount": -10, + "currency": "EUR", + "date": 2024-09-04T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGyJ6XvrOBRhDLeeN%2BxJ0%2Fw4XMM8TTJEEZ53CZCjtJj68Ido9BFHwz3TUOdSJ2hA7SM%3D", + }, + Object { + "address": "CC_NEW FITNESS BEOGRAD 3320.00 RSD + Kurs:117.0222", + "amount": -28.37, + "currency": "EUR", + "date": 2024-08-29T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROWW8SMindbi9qyWTYzL6T0%2BiBI2QHnsg8kphdlLWyI599KGB4zJBkqCIlE8Aib%2FR4vGAm63DMjYt", + }, + Object { + "address": "CC_NEW FITNESS BEOGRAD 3320.00 RSD + Kurs:117.0553", + "amount": -28.36, + "currency": "EUR", + "date": 2024-07-30T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROSMX4igovJ%2FmtS2Jk10Pkqrt9MrwJ14QtjyPZld6fvZDhvXC7DqjMPkfMP5I1%2BJMELw7YSv%2B8Nra", + }, + Object { + "address": "CINEPLEXX SRB DO NOVI BEOGRAD 3210.00 RSD + Kurs:117.0680", + "amount": -19.86, + "currency": "EUR", + "date": 2024-07-24T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROT4IailIraT5c5%2FAjyJOcSAH6hRg11RtwQ58MITi7ZmmRSpk02HvNxWLyDmAjwGk8B29oj2UWfNh", + }, + Object { + "address": "Prodaja deviza", + "amount": -10, + "currency": "EUR", + "date": 2024-05-07T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGyJ6XvrOBRhDLHO4MP7xsK4%2BKHdPBOyRoi8Iavw0zjReAOpKRwd35zVLkElh0b4KVs%3D", + }, + Object { + "address": "ROMAN PAVLOV", + "amount": 3800, + "currency": "EUR", + "date": 2024-04-13T22:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGydLNdInhh39gudFQ5POsk76CNhd%2B9wvSjblV5pYsUGEJFr3dEnMpxBfDNIPvv3S7A%3D", + }, + Object { + "address": "FAVORIT CGI 6 PETROVAC NA M 1000.00 RSD + Kurs:117.2024", + "amount": -8.53, + "currency": "EUR", + "date": 2024-03-24T23:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGywu4UtfJ7ROSiVJjLYHqrGk4nRX9QuDcJrEiES1A3OI8EyW%2BKToW2JJdZbajx4yTPRCXLQlotlm2%2FP0PBZ8AqL", + }, + Object { + "address": "ROMAN PAVLOV", + "amount": 500, + "currency": "EUR", + "date": 2024-02-07T23:00:00.000Z, + "description": "", + "id": "id_%2Bh5Bxj5lBGydLNdInhh39oT8MEW0Tovt7fQH97F17VRFy%2Bhp83GjBFWKCZW4nBSOdTuweS8HjHs%3D", + }, + ] + `) + }) +}) diff --git a/src/plugins/altabanka-rs/__tests__/converters/accounts/account.test.ts b/src/plugins/altabanka-rs/__tests__/converters/accounts/account.test.ts new file mode 100644 index 00000000..5b737d07 --- /dev/null +++ b/src/plugins/altabanka-rs/__tests__/converters/accounts/account.test.ts @@ -0,0 +1,34 @@ +import { AccountInfo } from '../../../types' +import { convertAccounts } from '../../../converters' + +describe('convertAccounts', () => { + it('should convert accounts', () => { + const accounts: AccountInfo[] = [ + { + id: 'GTqs0fbZAP4rVJNP3KU2GaTRZEG9aJ5qodGQBp6Rek8', + balance: 6471.0, + name: 'ACCOUNT NAME', + currency: 'TRY', + cardNumber: '0011', + accountNumber: '0022' + } + ] + + expect(convertAccounts(accounts)).toMatchInlineSnapshot(` + Array [ + Object { + "available": 6471, + "balance": 6471, + "creditLimit": 0, + "id": "GTqs0fbZAP4rVJNP3KU2GaTRZEG9aJ5qodGQBp6Rek8", + "instrument": "TRY", + "syncIds": Array [ + "GTqs0fbZAP4rVJNP3KU2GaTRZEG9aJ5qodGQBp6Rek8", + ], + "title": "ACCOUNT NAME", + "type": "ccard", + }, + ] + `) + }) +}) diff --git a/src/plugins/altabanka-rs/__tests__/converters/transactions/outcome.test.ts b/src/plugins/altabanka-rs/__tests__/converters/transactions/outcome.test.ts new file mode 100644 index 00000000..b1e31f01 --- /dev/null +++ b/src/plugins/altabanka-rs/__tests__/converters/transactions/outcome.test.ts @@ -0,0 +1,99 @@ +import { convertTransaction } from '../../../converters' +import { AccountInfo, AccountTransaction } from '../../../types' + +describe('convertTransaction', () => { + it('should convert basic transaction', () => { + const accountTransaction: AccountTransaction = { + date: new Date('2022-12-23T00:01:48.0000000+03:00'), + address: 'Harcama - AVKAN ECZANESI ANTALYA TR (POS - ****9753)', + amount: -40.36, + currency: 'TRY', + id: 'transaction_123', + description: 'Description' + } + + const account: AccountInfo = { + id: 'B7C94FAC', + currency: 'RUB', + cardNumber: '0011', + accountNumber: '0022', + name: 'Name', + balance: 100 + } + + expect(convertTransaction(accountTransaction, account)) + .toMatchInlineSnapshot(` + Object { + "comment": "Description", + "date": 2022-12-22T21:01:48.000Z, + "hold": false, + "merchant": Object { + "fullTitle": "Harcama - AVKAN ECZANESI ANTALYA TR (POS - ****9753)", + "location": null, + "mcc": null, + }, + "movements": Array [ + Object { + "account": Object { + "id": "B7C94FAC", + }, + "fee": 0, + "id": "transaction_123", + "invoice": Object { + "instrument": "TRY", + "sum": -40.36, + }, + "sum": -40.36, + }, + ], + } + `) + }) + + it('should convert transaction with card in another currency', () => { + const account: AccountInfo = { + id: 'B7C94FAC', + currency: 'RUB', + cardNumber: '0011', + accountNumber: '0022', + name: 'Name', + balance: 100 + } + + const cardTransaction: AccountTransaction = { + date: new Date('2022-12-23T00:01:49.0000000+03:00'), + address: 'Address', + amount: -2, + currency: 'USD', + id: 'transaction_123', + description: 'Description' + } + + expect(convertTransaction(cardTransaction, account)).toMatchInlineSnapshot(` + Object { + "comment": "Description", + "date": 2022-12-22T21:01:49.000Z, + "hold": false, + "merchant": Object { + "fullTitle": "Address", + "location": null, + "mcc": null, + }, + "movements": Array [ + Object { + "account": Object { + "id": "B7C94FAC", + }, + "fee": 0, + "id": "transaction_123", + "invoice": Object { + "instrument": "USD", + "sum": -2, + }, + "sum": -2, + }, + ], + } + `) + }) +}) diff --git a/src/plugins/altabanka-rs/api.ts b/src/plugins/altabanka-rs/api.ts new file mode 100644 index 00000000..5aa3377b --- /dev/null +++ b/src/plugins/altabanka-rs/api.ts @@ -0,0 +1,155 @@ +import { fetch, FetchResponse } from '../../common/network' +import { sanitize } from '../../common/sanitize' +import { InvalidPreferencesError } from '../../errors' +import { parseAccountInfo, parseLoginResult, parseRequestVerificationToken, parseTransactions } from './parsers' +import { AccountInfo, AccountTransaction, Preferences } from './types' +import moment from 'moment' +// @ts-expect-error no types for package +import * as qs from 'querystring-browser' + +export class AltaBankaApi { + private readonly baseUrl: string + + constructor (options: {baseUrl: string}) { + this.baseUrl = options.baseUrl + } + + public async login ({ login, password }: Preferences, isInBackground: boolean): Promise { + const formData = { + Username_ID: login, + Password_ID: password, + ActiveLoginMethod: 'usernamepassword', + workitemid: 'v2bdQSMgtKTpqU8qDDR9iQ==', + 'X-Requested-With': 'XMLHttpRequest' + } + + const response = await fetch( + this.baseUrl + 'Identity/Login', + { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + body: qs.stringify(formData), + sanitizeRequestLog: true + }) as FetchResponse & {body: string} + + if (!parseLoginResult(response.body)) { + console.error('login failed') + throw new InvalidPreferencesError() + } + + console.info('login successful') + } + + public async fetchAccounts (): Promise { + const response = await fetch( + this.baseUrl + 'Home/Accounts', + undefined + ) as FetchResponse & {body: string} + + const accountsInfo = parseAccountInfo(response.body) + + console.debug('fetchAccounts', sanitize(accountsInfo, false)) + + return accountsInfo + } + + public async fetchVerificationToken (): Promise { + const response = await fetch( + this.baseUrl + 'AccountData/Transactions/List', + undefined + ) as FetchResponse & {body: string} + + const token = parseRequestVerificationToken(response.body) + + console.debug('fetchVerificationToken', sanitize(token, true)) + + return token + } + + public async fetchTransactions ( + accountId: string, + token: string, + page: number, + fromDate: Date, + toDate?: Date + ): Promise { + const formData: Record = { + Accounts_ID: accountId, + __RequestVerificationToken: token, + DateFrom_ID: moment(fromDate).format('DD/MM/YYYY'), + SortOrder_ID: 'asc', + PageSize: '100', + PageNumber: page.toString() + } + if (toDate) { + formData.DateTo_ID = moment(toDate).format('DD/MM/YYYY') + } + + const response = await fetch( + this.baseUrl + 'AccountData/Transactions/List', + { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + body: qs.stringify(formData) + } + ) as FetchResponse & {body: string} + + const transactions = parseTransactions(response.body) + + console.debug('fetchTransactions', sanitize(transactions, false)) + + return transactions + } + + public async fetchCardTransactions ( + accountId: string, + cardNumber: string, + token: string, + page: number, + fromDate: Date, + toDate?: Date + ): Promise { + const formData: Record = { + Accounts_ID: accountId, + __RequestVerificationToken: token, + DateFrom_ID: moment(fromDate).format('DD/MM/YYYY'), + SortOrder_ID: 'asc', + PageSize: '100', + PageNumber: page.toString(), + Statuses_ID: 'p' + } + + if (toDate) { + formData.DateTo_ID = moment(toDate).format('DD/MM/YYYY') + } + + if (cardNumber !== '') { + formData.Cards_ID = cardNumber + } + + const response = await fetch( + this.baseUrl + 'AccountData/Transactions/CardTransactionsList', + { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + body: qs.stringify(formData) + } + ) as FetchResponse & {body: string} + + const transactions = parseTransactions(response.body) + + console.debug('fetchCardTransactions', sanitize(transactions, false)) + + return transactions + } +} + +export const altaBankaApi = new AltaBankaApi({ + baseUrl: 'https://altabanka.24x7.rs/altaonline/' +}) diff --git a/src/plugins/altabanka-rs/converters.ts b/src/plugins/altabanka-rs/converters.ts new file mode 100644 index 00000000..6d38c196 --- /dev/null +++ b/src/plugins/altabanka-rs/converters.ts @@ -0,0 +1,50 @@ +import { AccountOrCard, AccountType, Transaction } from '../../types/zenmoney' +import { AccountInfo, AccountTransaction } from './types' + +export function convertAccounts (apiAccounts: AccountInfo[]): AccountOrCard[] { + return apiAccounts.map(apiAccount => { + return { + id: apiAccount.id, + type: AccountType.ccard, + title: apiAccount.name, + instrument: apiAccount.currency, + balance: apiAccount.balance, + available: apiAccount.balance, + creditLimit: 0, + syncIds: [apiAccount.id] + } + }) +} + +export function convertTransaction (accountTransaction: AccountTransaction, account: AccountInfo, hold = false): Transaction { + let invoice = null + + if (accountTransaction.currency !== account.currency) { + invoice = { + sum: accountTransaction.amount, + instrument: accountTransaction.currency + } + } + + const merchant = { + fullTitle: accountTransaction.address, + mcc: null, + location: null + } + + return { + hold, + date: accountTransaction.date, + movements: [ + { + id: accountTransaction.id, + account: { id: account.id }, + sum: accountTransaction.amount, + fee: 0, + invoice + } + ], + merchant, + comment: accountTransaction.description + } +} diff --git a/src/plugins/altabanka-rs/index.ts b/src/plugins/altabanka-rs/index.ts new file mode 100644 index 00000000..fa88f60e --- /dev/null +++ b/src/plugins/altabanka-rs/index.ts @@ -0,0 +1,57 @@ +import { Account, ScrapeFunc, Transaction } from '../../types/zenmoney' +import { Preferences } from './types' +import { convertAccounts, convertTransaction } from './converters' +import { altaBankaApi } from './api' + +export const scrape: ScrapeFunc = async ({ preferences, fromDate, toDate, isInBackground }) => { + await altaBankaApi.login(preferences, isInBackground) + + const apiAccounts = await altaBankaApi.fetchAccounts() + const convertedAccounts: Account[] = convertAccounts(apiAccounts.filter(a => a.cardNumber === '')) + const transactions: Transaction[] = [] + + const token = await altaBankaApi.fetchVerificationToken() + + await Promise.all(apiAccounts.map(async (account) => { + if (ZenMoney.isAccountSkipped(account.accountNumber)) { + return + } + + let page = 1 + if (account.cardNumber === '') { + while (page < 100) { + const apiTransactions = await altaBankaApi.fetchTransactions(account.id, token, page, fromDate, toDate) + + if (apiTransactions.length === 0) { + break + } + + page++ + + transactions.push(...apiTransactions.map(t => convertTransaction(t, account))) + } + } + + if (account.cardNumber !== '') { + account.id = account.accountNumber + page = 1 + + while (page < 100) { + const apiCardTransactions = await altaBankaApi.fetchCardTransactions(account.accountNumber, account.cardNumber, token, page, fromDate, toDate) + + if (apiCardTransactions.length === 0) { + break + } + + page++ + + transactions.push(...apiCardTransactions.map(t => convertTransaction(t, account, true))) + } + } + })) + + return { + accounts: convertedAccounts, + transactions + } +} diff --git a/src/plugins/altabanka-rs/parsers.ts b/src/plugins/altabanka-rs/parsers.ts new file mode 100644 index 00000000..28202902 --- /dev/null +++ b/src/plugins/altabanka-rs/parsers.ts @@ -0,0 +1,71 @@ +import cheerio from 'cheerio' +import { AccountInfo, AccountTransaction } from './types' +import moment from 'moment' + +const exchangeRateRegex = /\d+\.\d+ \w{3} Kurs:.+/ + +export function parseLoginResult (body: string): boolean { + return body.includes('location.href = action;') +} + +export function parseAccountInfo (body: string): AccountInfo[] { + const html = cheerio.load(body) + const accountsHtml = html('#account-slider').find('.slide') + + return accountsHtml.toArray().map(accountHtml => { + const html = cheerio.load(accountHtml) + + return { + id: (accountHtml as cheerio.TagElement).attribs['data-accountnumber'].trim(), + cardNumber: (accountHtml as cheerio.TagElement).attribs['data-cardno'].trim(), + accountNumber: (accountHtml as cheerio.TagElement).attribs['data-accno'].trim(), + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + name: html('.acc-name').text().trim() || html('.acc-nr').text().trim(), + currency: html('.main-balance option').text().trim(), + balance: parseFloat(html('.main-balance option').data('amount')?.replace('.', '')?.replace(',', '.')) + } + }) +} + +export function parseRequestVerificationToken (body: string): string { + const html = cheerio.load(body) + const token = html('input[name="__RequestVerificationToken"]').val() + + return token +} + +export function parseTransactions (body: string): AccountTransaction[] { + const html = cheerio.load(body) + const transactionsHtml = html('#transaction-view-content').find('.pageable-content') + + return transactionsHtml.toArray().map(transactionHtml => { + const html = cheerio.load(transactionHtml) + + const transactionHref = (transactionHtml as cheerio.TagElement).attribs['data-href'].trim() + const queryParams = transactionHref.split('?')[1] + const params = queryParams.split('&').reduce>((acc, param) => { + const [key, value] = param.split('=') + acc[key] = value + return acc + }, {}) + + const [dateHtml, , descriptionHtml, amountHtml] = html('div').children('div').toArray() + + const direction = cheerio.load(dateHtml)('div.tag').hasClass('up') ? -1 : 1 + const date = moment(cheerio.load(dateHtml)('p').text()?.trim(), 'DD.MM.YYYY').toDate() + let address = cheerio.load(descriptionHtml)('span').text()?.trim().replace(/ {2}/g, '').replace('Kartica: ', '') + const [amount, currency] = cheerio.load(amountHtml)('p').text().trim().split(' ') ?? [] + + const description = address?.match(exchangeRateRegex)?.[0] ?? '' + address = address?.replace(description ?? '', '').trim() + + return { + id: 'id_' + params.q, + date, + address, + amount: direction * Number(amount.replace(/,/g, '')), + currency, + description + } + }) +} diff --git a/src/plugins/altabanka-rs/preferences.xml b/src/plugins/altabanka-rs/preferences.xml new file mode 100755 index 00000000..c3dc3a9e --- /dev/null +++ b/src/plugins/altabanka-rs/preferences.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/src/plugins/altabanka-rs/types.ts b/src/plugins/altabanka-rs/types.ts new file mode 100644 index 00000000..bef0d8fa --- /dev/null +++ b/src/plugins/altabanka-rs/types.ts @@ -0,0 +1,22 @@ +export interface Preferences { + login: string + password: string +} + +export interface AccountInfo { + id: string + cardNumber: string + accountNumber: string + name: string + currency: string + balance: number +} + +export interface AccountTransaction { + id: string + date: Date + address: string + amount: number + currency: string + description: string +}