@accounter/scraper-app 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +90 -0
  2. package/docs/plan.md +76 -0
  3. package/index.html +12 -0
  4. package/package.json +40 -0
  5. package/src/env.template +2 -0
  6. package/src/server/__tests__/accounts-routes.test.ts +133 -0
  7. package/src/server/__tests__/check-accounts.test.ts +305 -0
  8. package/src/server/__tests__/filter-payload.test.ts +193 -0
  9. package/src/server/__tests__/graphql-client.integration.test.ts +98 -0
  10. package/src/server/__tests__/graphql-client.test.ts +508 -0
  11. package/src/server/__tests__/healthz.test.ts +22 -0
  12. package/src/server/__tests__/history.test.ts +111 -0
  13. package/src/server/__tests__/otp-manager.test.ts +132 -0
  14. package/src/server/__tests__/scrape-runner.test.ts +144 -0
  15. package/src/server/__tests__/settings-routes.test.ts +117 -0
  16. package/src/server/__tests__/sources-routes.test.ts +149 -0
  17. package/src/server/__tests__/validate-payload.test.ts +193 -0
  18. package/src/server/__tests__/vault-routes.test.ts +174 -0
  19. package/src/server/__tests__/vault.test.ts +33 -0
  20. package/src/server/__tests__/websocket.test.ts +151 -0
  21. package/src/server/account-discovery.ts +49 -0
  22. package/src/server/accounts-routes.ts +74 -0
  23. package/src/server/check-accounts.ts +79 -0
  24. package/src/server/filter-payload.ts +145 -0
  25. package/src/server/graphql/client.ts +103 -0
  26. package/src/server/graphql/mutations.ts +518 -0
  27. package/src/server/history-routes.ts +11 -0
  28. package/src/server/history.ts +53 -0
  29. package/src/server/index.ts +40 -0
  30. package/src/server/otp-manager.ts +63 -0
  31. package/src/server/payload-schemas/amex.schema.ts +2 -0
  32. package/src/server/payload-schemas/cal.schema.ts +27 -0
  33. package/src/server/payload-schemas/currency-rates.schema.ts +11 -0
  34. package/src/server/payload-schemas/discount.schema.ts +26 -0
  35. package/src/server/payload-schemas/isracard.schema.ts +58 -0
  36. package/src/server/payload-schemas/max.schema.ts +27 -0
  37. package/src/server/payload-schemas/poalim-foreign.schema.ts +30 -0
  38. package/src/server/payload-schemas/poalim-ils.schema.ts +31 -0
  39. package/src/server/payload-schemas/poalim-swift.schema.ts +21 -0
  40. package/src/server/scrape-runner.ts +165 -0
  41. package/src/server/scrapers/__tests__/amex.test.ts +142 -0
  42. package/src/server/scrapers/__tests__/cal.test.ts +135 -0
  43. package/src/server/scrapers/__tests__/currency-rates.test.ts +105 -0
  44. package/src/server/scrapers/__tests__/discount.test.ts +160 -0
  45. package/src/server/scrapers/__tests__/isracard.test.ts +142 -0
  46. package/src/server/scrapers/__tests__/max.test.ts +115 -0
  47. package/src/server/scrapers/__tests__/poalim.test.ts +154 -0
  48. package/src/server/scrapers/amex.ts +63 -0
  49. package/src/server/scrapers/cal.ts +56 -0
  50. package/src/server/scrapers/currency-rates.ts +64 -0
  51. package/src/server/scrapers/discount.ts +62 -0
  52. package/src/server/scrapers/isracard.ts +68 -0
  53. package/src/server/scrapers/max.ts +32 -0
  54. package/src/server/scrapers/poalim.ts +103 -0
  55. package/src/server/settings-routes.ts +27 -0
  56. package/src/server/sources-routes.ts +182 -0
  57. package/src/server/validate-payload.ts +74 -0
  58. package/src/server/vault-routes.ts +99 -0
  59. package/src/server/vault-store.ts +42 -0
  60. package/src/server/vault.ts +216 -0
  61. package/src/server/websocket.ts +454 -0
  62. package/src/shared/source-types.ts +10 -0
  63. package/src/shared/types.ts +20 -0
  64. package/src/shared/ws-protocol.ts +177 -0
  65. package/src/test-setup.ts +6 -0
  66. package/src/ui/__tests__/accounts-tab.test.tsx +134 -0
  67. package/src/ui/__tests__/config.test.tsx +99 -0
  68. package/src/ui/__tests__/history.test.tsx +94 -0
  69. package/src/ui/__tests__/run.test.tsx +195 -0
  70. package/src/ui/__tests__/settings-tab.test.tsx +79 -0
  71. package/src/ui/__tests__/sources-tab.test.tsx +139 -0
  72. package/src/ui/__tests__/vault-setup.test.tsx +105 -0
  73. package/src/ui/__tests__/vault-unlock.test.tsx +78 -0
  74. package/src/ui/app.tsx +109 -0
  75. package/src/ui/components/error-boundary.tsx +54 -0
  76. package/src/ui/components/otp-modal.tsx +82 -0
  77. package/src/ui/components/skeleton.tsx +58 -0
  78. package/src/ui/components/task-row.tsx +241 -0
  79. package/src/ui/contexts/vault-context.tsx +77 -0
  80. package/src/ui/lib/api.ts +117 -0
  81. package/src/ui/lib/ws.ts +137 -0
  82. package/src/ui/main.tsx +9 -0
  83. package/src/ui/screens/config/accounts-tab.tsx +185 -0
  84. package/src/ui/screens/config/config.tsx +163 -0
  85. package/src/ui/screens/config/settings-tab.tsx +167 -0
  86. package/src/ui/screens/config/source-forms.tsx +518 -0
  87. package/src/ui/screens/config/source-types.ts +91 -0
  88. package/src/ui/screens/config/sources-tab.tsx +176 -0
  89. package/src/ui/screens/history.tsx +234 -0
  90. package/src/ui/screens/run.tsx +266 -0
  91. package/src/ui/screens/vault-setup.tsx +120 -0
  92. package/src/ui/screens/vault-unlock.tsx +38 -0
  93. package/tsconfig.json +15 -0
  94. package/tsup.config.ts +10 -0
  95. package/vite.config.ts +24 -0
  96. package/vitest.config.ts +7 -0
@@ -0,0 +1,193 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { filterPayload } from '../filter-payload.js';
3
+ import type { CalPayload } from '../payload-schemas/cal.schema.js';
4
+ import type { MaxPayload } from '../payload-schemas/max.schema.js';
5
+ import type { PoalimIlsPayload } from '../payload-schemas/poalim-ils.schema.js';
6
+ import type { IsracardCardsTransactionsList } from '@accounter/modern-poalim-scraper';
7
+
8
+ // ── Helpers ────────────────────────────────────────────────────────────────────
9
+
10
+ function makeIsracardPayload(cards: string[]): IsracardCardsTransactionsList {
11
+ const bean: Record<string, unknown> = {};
12
+ bean.cardNumberList = cards.map(c => `card ${c}`);
13
+ cards.forEach((card, i) => {
14
+ bean[`Index${i}`] = {
15
+ '@AllCards': card,
16
+ CurrentCardTransactions: [
17
+ {
18
+ '@cardTransactions': card,
19
+ txnIsrael: [{ cardIndex: `${i}`, supplierName: 'Shop', dealSum: '100', fullPurchaseDate: '2024-01-01', purchaseDate: '2024-01-01', voucherNumber: '1', voucherNumberRatz: '1' }],
20
+ txnAbroad: [],
21
+ },
22
+ ],
23
+ };
24
+ });
25
+ return {
26
+ Header: { Status: 'OK', Message: null },
27
+ CardsTransactionsListBean: bean as IsracardCardsTransactionsList['CardsTransactionsListBean'],
28
+ };
29
+ }
30
+
31
+ function getCardsInPayload(payload: IsracardCardsTransactionsList): string[] {
32
+ const bean = payload.CardsTransactionsListBean;
33
+ return Object.keys(bean)
34
+ .filter(k => /^Index\d+$/.test(k))
35
+ .flatMap(k => {
36
+ const idx = bean[k] as { CurrentCardTransactions: Array<{ '@cardTransactions': string }> };
37
+ return idx.CurrentCardTransactions.map(c => c['@cardTransactions']);
38
+ });
39
+ }
40
+
41
+ function makeCalPayload(cards: string[]): CalPayload {
42
+ return cards.map(card => ({
43
+ card,
44
+ month: '2024-01',
45
+ transactions: [{ trnIntId: `${card}-1`, merchantName: 'Shop', trnPurchaseDate: '2024-01-01', trnAmt: 100, trnCurrencySymbol: 'ILS', trnType: 'normal', debCrdDate: '2024-01-05', amtBeforeConvAndIndex: 100, debCrdCurrencySymbol: 'ILS' }],
46
+ }));
47
+ }
48
+
49
+ function makeMaxPayload(accounts: string[]): MaxPayload {
50
+ return accounts.map(accountNumber => ({
51
+ accountNumber,
52
+ txns: [{ cardIndex: 1, categoryId: 1, merchantName: 'Shop', originalAmount: 100, originalCurrency: 'ILS', purchaseDate: '2024-01-01', uid: `${accountNumber}-uid`, planName: 'regular', planTypeId: 1 }],
53
+ }));
54
+ }
55
+
56
+ function makePoalimIlsPayload(accountNumber: number, branchNumber: number): PoalimIlsPayload {
57
+ return {
58
+ transactions: [{ activityDescription: 'Transfer', activityTypeCode: 1, eventAmount: 100, eventDate: 20240101, serialNumber: 1, transactionType: 'REGULAR', currentBalance: 5000, referenceNumber: 1001 }],
59
+ retrievalTransactionData: { accountNumber, branchNumber, bankNumber: 12 },
60
+ };
61
+ }
62
+
63
+ const isracardCreds = (accepted?: string[], ignored?: string[]) => ({
64
+ id: 'a', ownerId: '123', password: 'pw', last6Digits: '123456',
65
+ options: { acceptedCardNumbers: accepted, ignoredCardNumbers: ignored },
66
+ });
67
+
68
+ const calCreds = (accepted?: string[], ignored?: string[]) => ({
69
+ id: 'a', username: 'u', password: 'pw', last4Digits: '1234',
70
+ options: { acceptedCardNumbers: accepted, ignoredCardNumbers: ignored },
71
+ });
72
+
73
+ const maxCreds = (accepted?: string[], ignored?: string[]) => ({
74
+ id: 'a', username: 'u', password: 'pw',
75
+ options: { acceptedCardNumbers: accepted, ignoredCardNumbers: ignored },
76
+ });
77
+
78
+ const poalimCreds = (acceptedAcc?: string[], ignoredAcc?: string[], acceptedBranch?: string[], ignoredBranch?: string[]) => ({
79
+ id: 'a', userCode: 'u', password: 'pw',
80
+ options: { acceptedAccountNumbers: acceptedAcc, ignoredAccountNumbers: ignoredAcc, acceptedBranchNumbers: acceptedBranch, ignoredBranchNumbers: ignoredBranch },
81
+ });
82
+
83
+ // ── Isracard ───────────────────────────────────────────────────────────────────
84
+
85
+ describe('filterPayload — isracard', () => {
86
+ it('keeps only accepted card', () => {
87
+ const payload = makeIsracardPayload(['1234', '5678']);
88
+ const result = filterPayload('isracard', payload, isracardCreds(['1234']));
89
+ expect(getCardsInPayload(result as IsracardCardsTransactionsList)).toEqual(['1234']);
90
+ });
91
+
92
+ it('excludes ignored card when no accepted list', () => {
93
+ const payload = makeIsracardPayload(['1234', '5678']);
94
+ const result = filterPayload('isracard', payload, isracardCreds(undefined, ['1234']));
95
+ expect(getCardsInPayload(result as IsracardCardsTransactionsList)).toEqual(['5678']);
96
+ });
97
+
98
+ it('accepted minus ignored — only 1234 survives', () => {
99
+ const payload = makeIsracardPayload(['1234', '5678']);
100
+ const result = filterPayload('isracard', payload, isracardCreds(['1234', '5678'], ['5678']));
101
+ expect(getCardsInPayload(result as IsracardCardsTransactionsList)).toEqual(['1234']);
102
+ });
103
+
104
+ it('empty accepted + empty ignored → all cards kept', () => {
105
+ const payload = makeIsracardPayload(['1234', '5678']);
106
+ const result = filterPayload('isracard', payload, isracardCreds());
107
+ expect(getCardsInPayload(result as IsracardCardsTransactionsList)).toHaveLength(2);
108
+ });
109
+ });
110
+
111
+ // ── Cal ────────────────────────────────────────────────────────────────────────
112
+
113
+ describe('filterPayload — cal', () => {
114
+ it('keeps only accepted card', () => {
115
+ const payload = makeCalPayload(['1234', '5678']);
116
+ const result = filterPayload('cal', payload, calCreds(['1234'])) as CalPayload;
117
+ expect(result.map(e => e.card)).toEqual(['1234']);
118
+ });
119
+
120
+ it('excludes ignored card', () => {
121
+ const payload = makeCalPayload(['1234', '5678']);
122
+ const result = filterPayload('cal', payload, calCreds(undefined, ['1234'])) as CalPayload;
123
+ expect(result.map(e => e.card)).toEqual(['5678']);
124
+ });
125
+
126
+ it('accepted minus ignored', () => {
127
+ const payload = makeCalPayload(['1234', '5678']);
128
+ const result = filterPayload('cal', payload, calCreds(['1234', '5678'], ['5678'])) as CalPayload;
129
+ expect(result.map(e => e.card)).toEqual(['1234']);
130
+ });
131
+
132
+ it('empty lists → all kept', () => {
133
+ const payload = makeCalPayload(['1234', '5678']);
134
+ const result = filterPayload('cal', payload, calCreds()) as CalPayload;
135
+ expect(result).toHaveLength(2);
136
+ });
137
+ });
138
+
139
+ // ── Max ────────────────────────────────────────────────────────────────────────
140
+
141
+ describe('filterPayload — max', () => {
142
+ it('keeps only accepted account', () => {
143
+ const payload = makeMaxPayload(['ACC1', 'ACC2']);
144
+ const result = filterPayload('max', payload, maxCreds(['ACC1'])) as MaxPayload;
145
+ expect(result.map(e => e.accountNumber)).toEqual(['ACC1']);
146
+ });
147
+
148
+ it('excludes ignored account', () => {
149
+ const payload = makeMaxPayload(['ACC1', 'ACC2']);
150
+ const result = filterPayload('max', payload, maxCreds(undefined, ['ACC1'])) as MaxPayload;
151
+ expect(result.map(e => e.accountNumber)).toEqual(['ACC2']);
152
+ });
153
+
154
+ it('empty lists → all kept', () => {
155
+ const payload = makeMaxPayload(['ACC1', 'ACC2']);
156
+ const result = filterPayload('max', payload, maxCreds()) as MaxPayload;
157
+ expect(result).toHaveLength(2);
158
+ });
159
+ });
160
+
161
+ // ── Poalim ILS ────────────────────────────────────────────────────────────────
162
+
163
+ describe('filterPayload — poalim', () => {
164
+ it('keeps transactions when account is in accepted list', () => {
165
+ const payload = makePoalimIlsPayload(100000, 600);
166
+ const result = filterPayload('poalim', payload, poalimCreds(['100000'], undefined, ['600'])) as PoalimIlsPayload;
167
+ expect(result.transactions).toHaveLength(1);
168
+ });
169
+
170
+ it('zeros transactions when account not in accepted list', () => {
171
+ const payload = makePoalimIlsPayload(100000, 600);
172
+ const result = filterPayload('poalim', payload, poalimCreds(['999999'])) as PoalimIlsPayload;
173
+ expect(result.transactions).toHaveLength(0);
174
+ });
175
+
176
+ it('zeros transactions when account is in ignored list', () => {
177
+ const payload = makePoalimIlsPayload(100000, 600);
178
+ const result = filterPayload('poalim', payload, poalimCreds(undefined, ['100000'])) as PoalimIlsPayload;
179
+ expect(result.transactions).toHaveLength(0);
180
+ });
181
+
182
+ it('zeros transactions when branch not in accepted list', () => {
183
+ const payload = makePoalimIlsPayload(100000, 600);
184
+ const result = filterPayload('poalim', payload, poalimCreds(undefined, undefined, ['999'])) as PoalimIlsPayload;
185
+ expect(result.transactions).toHaveLength(0);
186
+ });
187
+
188
+ it('empty lists → all transactions kept', () => {
189
+ const payload = makePoalimIlsPayload(100000, 600);
190
+ const result = filterPayload('poalim', payload, poalimCreds()) as PoalimIlsPayload;
191
+ expect(result.transactions).toHaveLength(1);
192
+ });
193
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Integration test for the GraphQL upload client against a live Accounter server.
3
+ *
4
+ * Prerequisites:
5
+ * - Local Accounter server running (yarn server:dev or similar)
6
+ * - ACCOUNTER_SERVER_URL set in .env (default: http://localhost:4000/graphql)
7
+ * - SCRAPER_API_KEY set in .env with a key that has the "scraper" role
8
+ *
9
+ * Skipped automatically when SCRAPER_API_KEY is not set.
10
+ */
11
+
12
+ import { config } from 'dotenv';
13
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
14
+ import { createUploadClient } from '../graphql/client.js';
15
+ import type { UploadResult } from '../graphql/mutations.js';
16
+
17
+ config({ path: ['../../.env', '.env'] });
18
+
19
+ const SERVER_URL = process.env['ACCOUNTER_SERVER_URL'] ?? 'http://localhost:4000/graphql';
20
+ const API_KEY = process.env['SCRAPER_API_KEY'] ?? '';
21
+
22
+ const FIXTURE_TX = {
23
+ transactions: [
24
+ {
25
+ activityDescription: 'Integration test deposit',
26
+ activityTypeCode: 1,
27
+ eventAmount: 1,
28
+ eventDate: 20991231,
29
+ formattedEventDate: '31/12/2099',
30
+ serialNumber: 9999999,
31
+ transactionType: 'REGULAR' as const,
32
+ currentBalance: 0,
33
+ referenceNumber: 9999999,
34
+ referenceCatenatedNumber: 0,
35
+ valueDate: '2099-12-31',
36
+ formattedValueDate: '31/12/2099',
37
+ eventActivityTypeCode: 1,
38
+ internalLinkCode: 0,
39
+ originalEventCreateDate: 0,
40
+ dataGroupCode: 0,
41
+ expandedEventDate: '20991231',
42
+ executingBranchNumber: 600,
43
+ eventId: '9999999',
44
+ differentDateIndication: 'N',
45
+ tableNumber: 0,
46
+ recordNumber: 0,
47
+ contraBankNumber: 0,
48
+ contraBranchNumber: 0,
49
+ contraAccountNumber: 0,
50
+ contraAccountTypeCode: 0,
51
+ marketingOfferContext: false,
52
+ commentExistenceSwitch: false,
53
+ fieldDescDisplaySwitch: false,
54
+ },
55
+ ],
56
+ retrievalTransactionData: {
57
+ accountNumber: 999999,
58
+ branchNumber: 600,
59
+ bankNumber: 12,
60
+ },
61
+ };
62
+
63
+ describe.skipIf(!API_KEY)('GraphQL upload client — live server integration', () => {
64
+ let client: ReturnType<typeof createUploadClient>;
65
+
66
+ beforeAll(async () => {
67
+ client = createUploadClient(SERVER_URL, API_KEY);
68
+
69
+ // Verify the server is reachable before running tests
70
+ const res = await fetch(SERVER_URL.replace('/graphql', '/healthz')).catch(() => null);
71
+ if (!res || !res.ok) {
72
+ throw new Error(
73
+ `Accounter server not reachable at ${SERVER_URL}. Start it with yarn server:dev.`,
74
+ );
75
+ }
76
+ });
77
+
78
+ afterAll(async () => {
79
+ // Clean up the fixture row we inserted so re-running tests always starts fresh.
80
+ // We do this by inserting the same row again — it will be skipped (ON CONFLICT),
81
+ // which confirms idempotency. Actual cleanup would require a direct DB query.
82
+ });
83
+
84
+ it('uploadPoalimIlsTransactions: inserts 1 on first call', async () => {
85
+ const result: UploadResult = await client.uploadPoalimIls(FIXTURE_TX);
86
+ expect(result.inserted).toBe(1);
87
+ expect(result.skipped).toBe(0);
88
+ expect(result.insertedIds).toHaveLength(1);
89
+ expect(typeof result.insertedIds[0]).toBe('string');
90
+ });
91
+
92
+ it('uploadPoalimIlsTransactions: skips 1 on second call (ON CONFLICT)', async () => {
93
+ const result: UploadResult = await client.uploadPoalimIls(FIXTURE_TX);
94
+ expect(result.inserted).toBe(0);
95
+ expect(result.skipped).toBe(1);
96
+ expect(result.insertedIds).toHaveLength(0);
97
+ });
98
+ });