@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,518 @@
1
+ import { useState, type ChangeEvent, type ReactElement } from 'react';
2
+ import type {
3
+ AmexSource,
4
+ CalSource,
5
+ DiscountSource,
6
+ IsracardSource,
7
+ MaxSource,
8
+ PoalimSource,
9
+ SourceConfig,
10
+ SourceType,
11
+ } from './source-types.js';
12
+
13
+ type FieldProps = {
14
+ label: string;
15
+ name: string;
16
+ value: string;
17
+ required?: boolean;
18
+ password?: boolean;
19
+ onChange(e: ChangeEvent<HTMLInputElement>): void;
20
+ };
21
+
22
+ function Field({ label, name, value, required, password, onChange }: FieldProps): ReactElement {
23
+ const [show, setShow] = useState(false);
24
+ return (
25
+ <div style={{ marginBottom: 8 }}>
26
+ <label htmlFor={name} style={{ display: 'block', marginBottom: 2 }}>
27
+ {label}
28
+ {required && ' *'}
29
+ </label>
30
+ <div style={{ display: 'flex', gap: 4 }}>
31
+ <input
32
+ id={name}
33
+ name={name}
34
+ type={password && !show ? 'password' : 'text'}
35
+ value={value}
36
+ required={required}
37
+ onChange={onChange}
38
+ style={{ flex: 1, padding: '4px 8px' }}
39
+ />
40
+ {password && (
41
+ <button
42
+ type="button"
43
+ onClick={() => setShow(s => !s)}
44
+ aria-label={show ? 'Hide' : 'Show'}
45
+ >
46
+ {show ? 'Hide' : 'Show'}
47
+ </button>
48
+ )}
49
+ </div>
50
+ </div>
51
+ );
52
+ }
53
+
54
+ type ListFieldProps = {
55
+ label: string;
56
+ name: string;
57
+ value: string; // comma-separated display value
58
+ hint?: string;
59
+ onChange(e: ChangeEvent<HTMLInputElement>): void;
60
+ };
61
+
62
+ function ListField({ label, name, value, hint, onChange }: ListFieldProps): ReactElement {
63
+ return (
64
+ <div style={{ marginBottom: 8 }}>
65
+ <label htmlFor={name} style={{ display: 'block', marginBottom: 2 }}>
66
+ {label}
67
+ </label>
68
+ <input
69
+ id={name}
70
+ name={name}
71
+ type="text"
72
+ value={value}
73
+ onChange={onChange}
74
+ placeholder="comma-separated, e.g. 123456,898989"
75
+ style={{ width: '100%', padding: '4px 8px', boxSizing: 'border-box' }}
76
+ />
77
+ {hint && <span style={{ fontSize: '0.8em', color: '#6b7280' }}>{hint}</span>}
78
+ </div>
79
+ );
80
+ }
81
+
82
+ function toList(csv: string): string[] | undefined {
83
+ const parts = csv
84
+ .split(',')
85
+ .map(s => s.trim())
86
+ .filter(Boolean);
87
+ const unique = [...new Set(parts)];
88
+ return unique.length > 0 ? unique : undefined;
89
+ }
90
+
91
+ function fromList(arr?: string[]): string {
92
+ return arr ? arr.join(', ') : '';
93
+ }
94
+
95
+ type PoalimFormProps = {
96
+ initial?: Partial<PoalimSource>;
97
+ onSave(data: Omit<PoalimSource, 'id' | 'type'>): void;
98
+ onCancel(): void;
99
+ };
100
+
101
+ export function PoalimForm({ initial = {}, onSave, onCancel }: PoalimFormProps): ReactElement {
102
+ const [fields, setFields] = useState({
103
+ nickname: initial.nickname ?? '',
104
+ userCode: initial.userCode ?? '',
105
+ password: initial.password ?? '',
106
+ isBusinessAccount: initial.options?.isBusinessAccount ?? false,
107
+ acceptedAccountNumbers: fromList(initial.options?.acceptedAccountNumbers),
108
+ acceptedBranchNumbers: fromList(initial.options?.acceptedBranchNumbers),
109
+ ignoredAccountNumbers: fromList(initial.options?.ignoredAccountNumbers),
110
+ ignoredBranchNumbers: fromList(initial.options?.ignoredBranchNumbers),
111
+ });
112
+
113
+ function set(e: ChangeEvent<HTMLInputElement>) {
114
+ setFields(f => ({ ...f, [e.target.name]: e.target.value }));
115
+ }
116
+
117
+ return (
118
+ <form
119
+ onSubmit={e => {
120
+ e.preventDefault();
121
+ const opts = {
122
+ isBusinessAccount: fields.isBusinessAccount || undefined,
123
+ acceptedAccountNumbers: toList(fields.acceptedAccountNumbers),
124
+ acceptedBranchNumbers: toList(fields.acceptedBranchNumbers),
125
+ ignoredAccountNumbers: toList(fields.ignoredAccountNumbers),
126
+ ignoredBranchNumbers: toList(fields.ignoredBranchNumbers),
127
+ };
128
+ const hasOpts = Object.values(opts).some(v => v !== undefined);
129
+ onSave({
130
+ nickname: fields.nickname || undefined,
131
+ userCode: fields.userCode,
132
+ password: fields.password,
133
+ options: hasOpts ? opts : undefined,
134
+ });
135
+ }}
136
+ >
137
+ <Field label="Nickname" name="nickname" value={fields.nickname} onChange={set} />
138
+ <Field label="User Code" name="userCode" value={fields.userCode} required onChange={set} />
139
+ <Field
140
+ label="Password"
141
+ name="password"
142
+ value={fields.password}
143
+ required
144
+ password
145
+ onChange={set}
146
+ />
147
+ <div style={{ marginBottom: 8 }}>
148
+ <label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
149
+ <input
150
+ type="checkbox"
151
+ checked={fields.isBusinessAccount}
152
+ onChange={e => setFields(f => ({ ...f, isBusinessAccount: e.target.checked }))}
153
+ />
154
+ Business account
155
+ </label>
156
+ </div>
157
+ <ListField
158
+ label="Accepted account numbers"
159
+ name="acceptedAccountNumbers"
160
+ value={fields.acceptedAccountNumbers}
161
+ onChange={set}
162
+ />
163
+ <ListField
164
+ label="Accepted branch numbers"
165
+ name="acceptedBranchNumbers"
166
+ value={fields.acceptedBranchNumbers}
167
+ onChange={set}
168
+ />
169
+ <ListField
170
+ label="Ignored account numbers"
171
+ name="ignoredAccountNumbers"
172
+ value={fields.ignoredAccountNumbers}
173
+ onChange={set}
174
+ />
175
+ <ListField
176
+ label="Ignored branch numbers"
177
+ name="ignoredBranchNumbers"
178
+ value={fields.ignoredBranchNumbers}
179
+ onChange={set}
180
+ />
181
+ <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
182
+ <button type="submit">Save</button>
183
+ <button type="button" onClick={onCancel}>
184
+ Cancel
185
+ </button>
186
+ </div>
187
+ </form>
188
+ );
189
+ }
190
+
191
+ type DiscountFormProps = {
192
+ initial?: Partial<DiscountSource>;
193
+ onSave(data: Omit<DiscountSource, 'id' | 'type'>): void;
194
+ onCancel(): void;
195
+ };
196
+
197
+ export function DiscountForm({ initial = {}, onSave, onCancel }: DiscountFormProps): ReactElement {
198
+ const [fields, setFields] = useState({
199
+ nickname: initial.nickname ?? '',
200
+ ID: initial.ID ?? '',
201
+ password: initial.password ?? '',
202
+ code: initial.code ?? '',
203
+ });
204
+
205
+ function set(e: ChangeEvent<HTMLInputElement>) {
206
+ setFields(f => ({ ...f, [e.target.name]: e.target.value }));
207
+ }
208
+
209
+ return (
210
+ <form
211
+ onSubmit={e => {
212
+ e.preventDefault();
213
+ onSave({
214
+ nickname: fields.nickname || undefined,
215
+ ID: fields.ID,
216
+ password: fields.password,
217
+ code: fields.code || undefined,
218
+ });
219
+ }}
220
+ >
221
+ <Field label="Nickname" name="nickname" value={fields.nickname} onChange={set} />
222
+ <Field label="ID" name="ID" value={fields.ID} required onChange={set} />
223
+ <Field
224
+ label="Password"
225
+ name="password"
226
+ value={fields.password}
227
+ required
228
+ password
229
+ onChange={set}
230
+ />
231
+ <Field label="Code" name="code" value={fields.code} onChange={set} />
232
+ <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
233
+ <button type="submit">Save</button>
234
+ <button type="button" onClick={onCancel}>
235
+ Cancel
236
+ </button>
237
+ </div>
238
+ </form>
239
+ );
240
+ }
241
+
242
+ type IsracardAmexFormProps = {
243
+ sourceType: 'isracard' | 'amex';
244
+ initial?: Partial<IsracardSource | AmexSource>;
245
+ onSave(data: Omit<IsracardSource | AmexSource, 'id' | 'type'>): void;
246
+ onCancel(): void;
247
+ };
248
+
249
+ export function IsracardAmexForm({
250
+ initial = {},
251
+ onSave,
252
+ onCancel,
253
+ }: IsracardAmexFormProps): ReactElement {
254
+ const [fields, setFields] = useState({
255
+ nickname: initial.nickname ?? '',
256
+ ownerId: initial.ownerId ?? '',
257
+ password: initial.password ?? '',
258
+ last6Digits: initial.last6Digits ?? '',
259
+ acceptedCardNumbers: fromList(initial.options?.acceptedCardNumbers),
260
+ ignoredCardNumbers: fromList(initial.options?.ignoredCardNumbers),
261
+ });
262
+
263
+ function set(e: ChangeEvent<HTMLInputElement>) {
264
+ setFields(f => ({ ...f, [e.target.name]: e.target.value }));
265
+ }
266
+
267
+ return (
268
+ <form
269
+ onSubmit={e => {
270
+ e.preventDefault();
271
+ const opts = {
272
+ acceptedCardNumbers: toList(fields.acceptedCardNumbers),
273
+ ignoredCardNumbers: toList(fields.ignoredCardNumbers),
274
+ };
275
+ const hasOpts = Object.values(opts).some(v => v !== undefined);
276
+ onSave({
277
+ nickname: fields.nickname || undefined,
278
+ ownerId: fields.ownerId,
279
+ password: fields.password,
280
+ last6Digits: fields.last6Digits,
281
+ options: hasOpts ? opts : undefined,
282
+ });
283
+ }}
284
+ >
285
+ <Field label="Nickname" name="nickname" value={fields.nickname} onChange={set} />
286
+ <Field label="Owner ID" name="ownerId" value={fields.ownerId} required onChange={set} />
287
+ <Field
288
+ label="Password"
289
+ name="password"
290
+ value={fields.password}
291
+ required
292
+ password
293
+ onChange={set}
294
+ />
295
+ <Field
296
+ label="Last 6 Digits"
297
+ name="last6Digits"
298
+ value={fields.last6Digits}
299
+ required
300
+ onChange={set}
301
+ />
302
+ <ListField
303
+ label="Accepted card numbers"
304
+ name="acceptedCardNumbers"
305
+ value={fields.acceptedCardNumbers}
306
+ onChange={set}
307
+ />
308
+ <ListField
309
+ label="Ignored card numbers"
310
+ name="ignoredCardNumbers"
311
+ value={fields.ignoredCardNumbers}
312
+ onChange={set}
313
+ />
314
+ <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
315
+ <button type="submit">Save</button>
316
+ <button type="button" onClick={onCancel}>
317
+ Cancel
318
+ </button>
319
+ </div>
320
+ </form>
321
+ );
322
+ }
323
+
324
+ type CalFormProps = {
325
+ initial?: Partial<CalSource>;
326
+ onSave(data: Omit<CalSource, 'id' | 'type'>): void;
327
+ onCancel(): void;
328
+ };
329
+
330
+ export function CalForm({ initial = {}, onSave, onCancel }: CalFormProps): ReactElement {
331
+ const [fields, setFields] = useState({
332
+ nickname: initial.nickname ?? '',
333
+ username: initial.username ?? '',
334
+ password: initial.password ?? '',
335
+ last4Digits: initial.last4Digits ?? '',
336
+ acceptedCardNumbers: fromList(initial.options?.acceptedCardNumbers),
337
+ ignoredCardNumbers: fromList(initial.options?.ignoredCardNumbers),
338
+ });
339
+
340
+ function set(e: ChangeEvent<HTMLInputElement>) {
341
+ setFields(f => ({ ...f, [e.target.name]: e.target.value }));
342
+ }
343
+
344
+ return (
345
+ <form
346
+ onSubmit={e => {
347
+ e.preventDefault();
348
+ const opts = {
349
+ acceptedCardNumbers: toList(fields.acceptedCardNumbers),
350
+ ignoredCardNumbers: toList(fields.ignoredCardNumbers),
351
+ };
352
+ const hasOpts = Object.values(opts).some(v => v !== undefined);
353
+ onSave({
354
+ nickname: fields.nickname || undefined,
355
+ username: fields.username,
356
+ password: fields.password,
357
+ last4Digits: fields.last4Digits,
358
+ options: hasOpts ? opts : undefined,
359
+ });
360
+ }}
361
+ >
362
+ <Field label="Nickname" name="nickname" value={fields.nickname} onChange={set} />
363
+ <Field label="Username" name="username" value={fields.username} required onChange={set} />
364
+ <Field
365
+ label="Password"
366
+ name="password"
367
+ value={fields.password}
368
+ required
369
+ password
370
+ onChange={set}
371
+ />
372
+ <Field
373
+ label="Last 4 Digits"
374
+ name="last4Digits"
375
+ value={fields.last4Digits}
376
+ required
377
+ onChange={set}
378
+ />
379
+ <ListField
380
+ label="Accepted card numbers"
381
+ name="acceptedCardNumbers"
382
+ value={fields.acceptedCardNumbers}
383
+ onChange={set}
384
+ />
385
+ <ListField
386
+ label="Ignored card numbers"
387
+ name="ignoredCardNumbers"
388
+ value={fields.ignoredCardNumbers}
389
+ onChange={set}
390
+ />
391
+ <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
392
+ <button type="submit">Save</button>
393
+ <button type="button" onClick={onCancel}>
394
+ Cancel
395
+ </button>
396
+ </div>
397
+ </form>
398
+ );
399
+ }
400
+
401
+ type MaxFormProps = {
402
+ initial?: Partial<MaxSource>;
403
+ onSave(data: Omit<MaxSource, 'id' | 'type'>): void;
404
+ onCancel(): void;
405
+ };
406
+
407
+ export function MaxForm({ initial = {}, onSave, onCancel }: MaxFormProps): ReactElement {
408
+ const [fields, setFields] = useState({
409
+ nickname: initial.nickname ?? '',
410
+ username: initial.username ?? '',
411
+ password: initial.password ?? '',
412
+ acceptedCardNumbers: fromList(initial.options?.acceptedCardNumbers),
413
+ ignoredCardNumbers: fromList(initial.options?.ignoredCardNumbers),
414
+ });
415
+
416
+ function set(e: ChangeEvent<HTMLInputElement>) {
417
+ setFields(f => ({ ...f, [e.target.name]: e.target.value }));
418
+ }
419
+
420
+ return (
421
+ <form
422
+ onSubmit={e => {
423
+ e.preventDefault();
424
+ const opts = {
425
+ acceptedCardNumbers: toList(fields.acceptedCardNumbers),
426
+ ignoredCardNumbers: toList(fields.ignoredCardNumbers),
427
+ };
428
+ const hasOpts = Object.values(opts).some(v => v !== undefined);
429
+ onSave({
430
+ nickname: fields.nickname || undefined,
431
+ username: fields.username,
432
+ password: fields.password,
433
+ options: hasOpts ? opts : undefined,
434
+ });
435
+ }}
436
+ >
437
+ <Field label="Nickname" name="nickname" value={fields.nickname} onChange={set} />
438
+ <Field label="Username" name="username" value={fields.username} required onChange={set} />
439
+ <Field
440
+ label="Password"
441
+ name="password"
442
+ value={fields.password}
443
+ required
444
+ password
445
+ onChange={set}
446
+ />
447
+ <ListField
448
+ label="Accepted card numbers"
449
+ name="acceptedCardNumbers"
450
+ value={fields.acceptedCardNumbers}
451
+ onChange={set}
452
+ />
453
+ <ListField
454
+ label="Ignored card numbers"
455
+ name="ignoredCardNumbers"
456
+ value={fields.ignoredCardNumbers}
457
+ onChange={set}
458
+ />
459
+ <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
460
+ <button type="submit">Save</button>
461
+ <button type="button" onClick={onCancel}>
462
+ Cancel
463
+ </button>
464
+ </div>
465
+ </form>
466
+ );
467
+ }
468
+
469
+ type SourceFormProps = {
470
+ sourceType: SourceType;
471
+ initial?: Partial<SourceConfig>;
472
+ onSave(data: Omit<SourceConfig, 'id' | 'type'>): void;
473
+ onCancel(): void;
474
+ };
475
+
476
+ export function SourceForm({
477
+ sourceType,
478
+ initial,
479
+ onSave,
480
+ onCancel,
481
+ }: SourceFormProps): ReactElement {
482
+ switch (sourceType) {
483
+ case 'poalim':
484
+ return (
485
+ <PoalimForm
486
+ initial={initial as Partial<PoalimSource>}
487
+ onSave={onSave}
488
+ onCancel={onCancel}
489
+ />
490
+ );
491
+ case 'discount':
492
+ return (
493
+ <DiscountForm
494
+ initial={initial as Partial<DiscountSource>}
495
+ onSave={onSave}
496
+ onCancel={onCancel}
497
+ />
498
+ );
499
+ case 'isracard':
500
+ case 'amex':
501
+ return (
502
+ <IsracardAmexForm
503
+ sourceType={sourceType}
504
+ initial={initial as Partial<IsracardSource>}
505
+ onSave={onSave}
506
+ onCancel={onCancel}
507
+ />
508
+ );
509
+ case 'cal':
510
+ return (
511
+ <CalForm initial={initial as Partial<CalSource>} onSave={onSave} onCancel={onCancel} />
512
+ );
513
+ case 'max':
514
+ return (
515
+ <MaxForm initial={initial as Partial<MaxSource>} onSave={onSave} onCancel={onCancel} />
516
+ );
517
+ }
518
+ }
@@ -0,0 +1,91 @@
1
+ import type { SourceType } from '../../../shared/source-types.js';
2
+
3
+ export type PoalimSource = {
4
+ id: string;
5
+ type: 'poalim';
6
+ nickname?: string;
7
+ userCode: string;
8
+ password: string;
9
+ options?: {
10
+ isBusinessAccount?: boolean;
11
+ acceptedAccountNumbers?: string[];
12
+ acceptedBranchNumbers?: string[];
13
+ ignoredAccountNumbers?: string[];
14
+ ignoredBranchNumbers?: string[];
15
+ };
16
+ };
17
+
18
+ export type DiscountSource = {
19
+ id: string;
20
+ type: 'discount';
21
+ nickname?: string;
22
+ ID: string;
23
+ password: string;
24
+ code?: string;
25
+ };
26
+
27
+ export type IsracardSource = {
28
+ id: string;
29
+ type: 'isracard';
30
+ nickname?: string;
31
+ ownerId: string;
32
+ password: string;
33
+ last6Digits: string;
34
+ options?: {
35
+ acceptedCardNumbers?: string[];
36
+ ignoredCardNumbers?: string[];
37
+ cardNumberMapping?: Record<string, string>;
38
+ };
39
+ };
40
+
41
+ export type AmexSource = {
42
+ id: string;
43
+ type: 'amex';
44
+ nickname?: string;
45
+ ownerId: string;
46
+ password: string;
47
+ last6Digits: string;
48
+ options?: {
49
+ acceptedCardNumbers?: string[];
50
+ ignoredCardNumbers?: string[];
51
+ cardNumberMapping?: Record<string, string>;
52
+ };
53
+ };
54
+
55
+ export type CalSource = {
56
+ id: string;
57
+ type: 'cal';
58
+ nickname?: string;
59
+ username: string;
60
+ password: string;
61
+ last4Digits: string;
62
+ options?: { acceptedCardNumbers?: string[]; ignoredCardNumbers?: string[] };
63
+ };
64
+
65
+ export type MaxSource = {
66
+ id: string;
67
+ type: 'max';
68
+ nickname?: string;
69
+ username: string;
70
+ password: string;
71
+ options?: { acceptedCardNumbers?: string[]; ignoredCardNumbers?: string[] };
72
+ };
73
+
74
+ export type SourceConfig =
75
+ | PoalimSource
76
+ | DiscountSource
77
+ | IsracardSource
78
+ | AmexSource
79
+ | CalSource
80
+ | MaxSource;
81
+
82
+ export type { SourceType } from '../../../shared/source-types.js';
83
+
84
+ export const SOURCE_LABELS: Record<SourceType, string> = {
85
+ poalim: 'Bank Hapoalim',
86
+ discount: 'Bank Discount',
87
+ isracard: 'Isracard',
88
+ amex: 'Amex',
89
+ cal: 'CAL',
90
+ max: 'Max',
91
+ };