@striderlabs/mcp-chase 1.0.0
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.
- package/README.md +100 -0
- package/dist/auth.js +72 -0
- package/dist/browser.js +394 -0
- package/dist/index.js +398 -0
- package/package.json +39 -0
- package/server.json +20 -0
- package/src/auth.ts +83 -0
- package/src/browser.ts +530 -0
- package/src/index.ts +460 -0
- package/tsconfig.json +15 -0
package/src/browser.ts
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chase Bank Browser Automation
|
|
3
|
+
*
|
|
4
|
+
* Patchright-based stealth automation for Chase banking operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { chromium, Browser, BrowserContext, Page } from "patchright";
|
|
8
|
+
import { saveCookies, loadCookies, AuthState } from "./auth.js";
|
|
9
|
+
|
|
10
|
+
const CHASE_BASE_URL = "https://www.chase.com";
|
|
11
|
+
const DEFAULT_TIMEOUT = 60000;
|
|
12
|
+
|
|
13
|
+
// Singleton browser instance
|
|
14
|
+
let browser: Browser | null = null;
|
|
15
|
+
let context: BrowserContext | null = null;
|
|
16
|
+
let page: Page | null = null;
|
|
17
|
+
|
|
18
|
+
export interface Account {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
type: "checking" | "savings" | "credit" | "investment" | "loan";
|
|
22
|
+
balance: number;
|
|
23
|
+
availableBalance?: number;
|
|
24
|
+
accountNumber?: string;
|
|
25
|
+
lastFour?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Transaction {
|
|
29
|
+
id: string;
|
|
30
|
+
date: string;
|
|
31
|
+
description: string;
|
|
32
|
+
amount: number;
|
|
33
|
+
type: "credit" | "debit";
|
|
34
|
+
category?: string;
|
|
35
|
+
pending: boolean;
|
|
36
|
+
merchant?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Bill {
|
|
40
|
+
id: string;
|
|
41
|
+
payee: string;
|
|
42
|
+
amount: number;
|
|
43
|
+
dueDate: string;
|
|
44
|
+
status: "scheduled" | "paid" | "overdue";
|
|
45
|
+
autopay: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Transfer {
|
|
49
|
+
id: string;
|
|
50
|
+
fromAccount: string;
|
|
51
|
+
toAccount: string;
|
|
52
|
+
amount: number;
|
|
53
|
+
date: string;
|
|
54
|
+
status: "pending" | "completed" | "failed";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Statement {
|
|
58
|
+
id: string;
|
|
59
|
+
accountId: string;
|
|
60
|
+
period: string;
|
|
61
|
+
date: string;
|
|
62
|
+
downloadUrl?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RewardsInfo {
|
|
66
|
+
pointsBalance: number;
|
|
67
|
+
cashBackBalance: number;
|
|
68
|
+
pendingRewards: number;
|
|
69
|
+
tier?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initialize browser with stealth settings
|
|
74
|
+
*/
|
|
75
|
+
async function initBrowser(): Promise<void> {
|
|
76
|
+
if (browser) return;
|
|
77
|
+
|
|
78
|
+
browser = await chromium.launch({
|
|
79
|
+
headless: true,
|
|
80
|
+
args: [
|
|
81
|
+
"--disable-blink-features=AutomationControlled",
|
|
82
|
+
"--no-sandbox",
|
|
83
|
+
"--disable-setuid-sandbox",
|
|
84
|
+
"--disable-web-security",
|
|
85
|
+
"--disable-features=IsolateOrigins,site-per-process",
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
context = await browser.newContext({
|
|
90
|
+
userAgent:
|
|
91
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
|
92
|
+
viewport: { width: 1280, height: 800 },
|
|
93
|
+
locale: "en-US",
|
|
94
|
+
timezoneId: "America/Los_Angeles",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Load saved cookies
|
|
98
|
+
await loadCookies(context);
|
|
99
|
+
|
|
100
|
+
page = await context.newPage();
|
|
101
|
+
page.setDefaultTimeout(DEFAULT_TIMEOUT);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the current page, initializing if needed
|
|
106
|
+
*/
|
|
107
|
+
async function getPage(): Promise<Page> {
|
|
108
|
+
await initBrowser();
|
|
109
|
+
if (!page) throw new Error("Failed to initialize browser");
|
|
110
|
+
return page;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Clean up browser resources
|
|
115
|
+
*/
|
|
116
|
+
export async function cleanup(): Promise<void> {
|
|
117
|
+
if (context) {
|
|
118
|
+
await saveCookies(context);
|
|
119
|
+
}
|
|
120
|
+
if (browser) {
|
|
121
|
+
await browser.close();
|
|
122
|
+
browser = null;
|
|
123
|
+
context = null;
|
|
124
|
+
page = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get Chase login URL and instructions
|
|
130
|
+
*/
|
|
131
|
+
export async function getLoginUrl(): Promise<{ url: string; instructions: string }> {
|
|
132
|
+
return {
|
|
133
|
+
url: "https://secure.chase.com/web/auth/dashboard",
|
|
134
|
+
instructions:
|
|
135
|
+
"Please log in to Chase in a browser, then export your cookies to ~/.strider/chase/cookies.json. Use a browser extension like 'Cookie-Editor' to export cookies in JSON format. Note: Chase uses MFA, so you may need to re-export cookies after each session.",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check authentication status
|
|
141
|
+
*/
|
|
142
|
+
export async function checkAuth(): Promise<AuthState> {
|
|
143
|
+
try {
|
|
144
|
+
const p = await getPage();
|
|
145
|
+
await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
|
|
146
|
+
|
|
147
|
+
// Check if we're on the dashboard or redirected to login
|
|
148
|
+
const currentUrl = p.url();
|
|
149
|
+
|
|
150
|
+
if (currentUrl.includes("/auth/logon") || currentUrl.includes("/login")) {
|
|
151
|
+
return { isLoggedIn: false };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try to get account holder name
|
|
155
|
+
const holderName = await p.$eval(
|
|
156
|
+
'.mds-header-user-name, .account-holder-name, [data-testid="user-greeting"]',
|
|
157
|
+
(el) => el.textContent?.trim() || "",
|
|
158
|
+
).catch(() => "");
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
isLoggedIn: true,
|
|
162
|
+
accountHolder: holderName || undefined,
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return { isLoggedIn: false };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get all accounts
|
|
171
|
+
*/
|
|
172
|
+
export async function getAccounts(): Promise<{ success: boolean; accounts?: Account[]; error?: string }> {
|
|
173
|
+
try {
|
|
174
|
+
const p = await getPage();
|
|
175
|
+
await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
|
|
176
|
+
|
|
177
|
+
await p.waitForTimeout(2000);
|
|
178
|
+
|
|
179
|
+
const accounts = await p.$$eval(
|
|
180
|
+
'.account-tile, .account-card, [data-testid="account-tile"]',
|
|
181
|
+
(elements) =>
|
|
182
|
+
elements.map((el, index) => {
|
|
183
|
+
const nameEl = el.querySelector('.account-name, .tile-header, h3');
|
|
184
|
+
const balanceEl = el.querySelector('.account-balance, .balance, [data-testid="balance"]');
|
|
185
|
+
const lastFourEl = el.querySelector('.account-last-four, .masked-number');
|
|
186
|
+
const typeEl = el.querySelector('.account-type');
|
|
187
|
+
|
|
188
|
+
const name = nameEl?.textContent?.trim() || `Account ${index + 1}`;
|
|
189
|
+
const balanceText = balanceEl?.textContent?.trim() || '0';
|
|
190
|
+
const balance = parseFloat(balanceText.replace(/[$,]/g, '')) || 0;
|
|
191
|
+
const lastFour = lastFourEl?.textContent?.trim().replace(/[^0-9]/g, '').slice(-4) || undefined;
|
|
192
|
+
|
|
193
|
+
let type: 'checking' | 'savings' | 'credit' | 'investment' | 'loan' = 'checking';
|
|
194
|
+
const typeText = (typeEl?.textContent || name).toLowerCase();
|
|
195
|
+
if (typeText.includes('saving')) type = 'savings';
|
|
196
|
+
else if (typeText.includes('credit') || typeText.includes('card')) type = 'credit';
|
|
197
|
+
else if (typeText.includes('invest') || typeText.includes('brokerage')) type = 'investment';
|
|
198
|
+
else if (typeText.includes('loan') || typeText.includes('mortgage') || typeText.includes('auto')) type = 'loan';
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: el.getAttribute('data-account-id') || `account-${index}`,
|
|
202
|
+
name,
|
|
203
|
+
type,
|
|
204
|
+
balance,
|
|
205
|
+
lastFour,
|
|
206
|
+
};
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return { success: true, accounts };
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: error instanceof Error ? error.message : "Failed to get accounts",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get account transactions
|
|
221
|
+
*/
|
|
222
|
+
export async function getTransactions(
|
|
223
|
+
accountId: string,
|
|
224
|
+
limit: number = 25
|
|
225
|
+
): Promise<{ success: boolean; transactions?: Transaction[]; error?: string }> {
|
|
226
|
+
try {
|
|
227
|
+
const p = await getPage();
|
|
228
|
+
|
|
229
|
+
// Navigate to account details
|
|
230
|
+
await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
|
|
231
|
+
await p.waitForTimeout(1000);
|
|
232
|
+
|
|
233
|
+
// Click on the account
|
|
234
|
+
const accountTile = await p.$(`[data-account-id="${accountId}"], .account-tile:nth-child(${parseInt(accountId.replace('account-', '')) + 1})`);
|
|
235
|
+
if (accountTile) {
|
|
236
|
+
await accountTile.click();
|
|
237
|
+
await p.waitForNavigation({ waitUntil: "networkidle" }).catch(() => {});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await p.waitForTimeout(2000);
|
|
241
|
+
|
|
242
|
+
const transactions = await p.$$eval(
|
|
243
|
+
'.transaction-row, .activity-row, [data-testid="transaction"]',
|
|
244
|
+
(elements, maxItems) =>
|
|
245
|
+
elements.slice(0, maxItems).map((el, index) => {
|
|
246
|
+
const dateEl = el.querySelector('.transaction-date, .date, td:first-child');
|
|
247
|
+
const descEl = el.querySelector('.transaction-description, .description, .merchant-name');
|
|
248
|
+
const amountEl = el.querySelector('.transaction-amount, .amount');
|
|
249
|
+
const pendingEl = el.querySelector('.pending, .pending-badge');
|
|
250
|
+
const categoryEl = el.querySelector('.category, .transaction-category');
|
|
251
|
+
|
|
252
|
+
const amountText = amountEl?.textContent?.trim() || '0';
|
|
253
|
+
const amount = Math.abs(parseFloat(amountText.replace(/[$,]/g, ''))) || 0;
|
|
254
|
+
const isCredit = amountText.includes('+') || el.classList.contains('credit');
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
id: el.getAttribute('data-transaction-id') || `txn-${index}`,
|
|
258
|
+
date: dateEl?.textContent?.trim() || '',
|
|
259
|
+
description: descEl?.textContent?.trim() || 'Unknown',
|
|
260
|
+
amount,
|
|
261
|
+
type: isCredit ? 'credit' : 'debit' as 'credit' | 'debit',
|
|
262
|
+
category: categoryEl?.textContent?.trim() || undefined,
|
|
263
|
+
pending: !!pendingEl,
|
|
264
|
+
};
|
|
265
|
+
}),
|
|
266
|
+
limit
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
return { success: true, transactions };
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
error: error instanceof Error ? error.message : "Failed to get transactions",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get account balance
|
|
280
|
+
*/
|
|
281
|
+
export async function getBalance(accountId: string): Promise<{ success: boolean; balance?: number; availableBalance?: number; error?: string }> {
|
|
282
|
+
try {
|
|
283
|
+
const accountsResult = await getAccounts();
|
|
284
|
+
if (!accountsResult.success || !accountsResult.accounts) {
|
|
285
|
+
return { success: false, error: "Failed to get accounts" };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const account = accountsResult.accounts.find(a => a.id === accountId);
|
|
289
|
+
if (!account) {
|
|
290
|
+
return { success: false, error: `Account not found: ${accountId}` };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
success: true,
|
|
295
|
+
balance: account.balance,
|
|
296
|
+
availableBalance: account.availableBalance || account.balance,
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
error: error instanceof Error ? error.message : "Failed to get balance",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get bills/payees
|
|
308
|
+
*/
|
|
309
|
+
export async function getBills(): Promise<{ success: boolean; bills?: Bill[]; error?: string }> {
|
|
310
|
+
try {
|
|
311
|
+
const p = await getPage();
|
|
312
|
+
await p.goto(`${CHASE_BASE_URL}/web/auth/billpay`, { waitUntil: "networkidle" });
|
|
313
|
+
|
|
314
|
+
await p.waitForTimeout(2000);
|
|
315
|
+
|
|
316
|
+
const bills = await p.$$eval(
|
|
317
|
+
'.payee-row, .bill-row, [data-testid="payee"]',
|
|
318
|
+
(elements) =>
|
|
319
|
+
elements.map((el, index) => {
|
|
320
|
+
const payeeEl = el.querySelector('.payee-name, .name');
|
|
321
|
+
const amountEl = el.querySelector('.amount, .payment-amount');
|
|
322
|
+
const dueDateEl = el.querySelector('.due-date, .date');
|
|
323
|
+
const statusEl = el.querySelector('.status, .payment-status');
|
|
324
|
+
const autopayEl = el.querySelector('.autopay, .auto-pay-enabled');
|
|
325
|
+
|
|
326
|
+
const amountText = amountEl?.textContent?.trim() || '0';
|
|
327
|
+
const amount = parseFloat(amountText.replace(/[$,]/g, '')) || 0;
|
|
328
|
+
|
|
329
|
+
let status: 'scheduled' | 'paid' | 'overdue' = 'scheduled';
|
|
330
|
+
const statusText = statusEl?.textContent?.toLowerCase() || '';
|
|
331
|
+
if (statusText.includes('paid')) status = 'paid';
|
|
332
|
+
else if (statusText.includes('overdue') || statusText.includes('past due')) status = 'overdue';
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
id: el.getAttribute('data-payee-id') || `bill-${index}`,
|
|
336
|
+
payee: payeeEl?.textContent?.trim() || 'Unknown Payee',
|
|
337
|
+
amount,
|
|
338
|
+
dueDate: dueDateEl?.textContent?.trim() || '',
|
|
339
|
+
status,
|
|
340
|
+
autopay: !!autopayEl,
|
|
341
|
+
};
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return { success: true, bills };
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
error: error instanceof Error ? error.message : "Failed to get bills",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get transfer history
|
|
356
|
+
*/
|
|
357
|
+
export async function getTransfers(): Promise<{ success: boolean; transfers?: Transfer[]; error?: string }> {
|
|
358
|
+
try {
|
|
359
|
+
const p = await getPage();
|
|
360
|
+
await p.goto(`${CHASE_BASE_URL}/web/auth/transfer`, { waitUntil: "networkidle" });
|
|
361
|
+
|
|
362
|
+
await p.waitForTimeout(2000);
|
|
363
|
+
|
|
364
|
+
const transfers = await p.$$eval(
|
|
365
|
+
'.transfer-row, .activity-row, [data-testid="transfer"]',
|
|
366
|
+
(elements) =>
|
|
367
|
+
elements.slice(0, 20).map((el, index) => {
|
|
368
|
+
const fromEl = el.querySelector('.from-account, .source');
|
|
369
|
+
const toEl = el.querySelector('.to-account, .destination');
|
|
370
|
+
const amountEl = el.querySelector('.amount');
|
|
371
|
+
const dateEl = el.querySelector('.date, .transfer-date');
|
|
372
|
+
const statusEl = el.querySelector('.status');
|
|
373
|
+
|
|
374
|
+
const amountText = amountEl?.textContent?.trim() || '0';
|
|
375
|
+
const amount = parseFloat(amountText.replace(/[$,]/g, '')) || 0;
|
|
376
|
+
|
|
377
|
+
let status: 'pending' | 'completed' | 'failed' = 'completed';
|
|
378
|
+
const statusText = statusEl?.textContent?.toLowerCase() || '';
|
|
379
|
+
if (statusText.includes('pending')) status = 'pending';
|
|
380
|
+
else if (statusText.includes('failed') || statusText.includes('cancelled')) status = 'failed';
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
id: el.getAttribute('data-transfer-id') || `transfer-${index}`,
|
|
384
|
+
fromAccount: fromEl?.textContent?.trim() || 'Unknown',
|
|
385
|
+
toAccount: toEl?.textContent?.trim() || 'Unknown',
|
|
386
|
+
amount,
|
|
387
|
+
date: dateEl?.textContent?.trim() || '',
|
|
388
|
+
status,
|
|
389
|
+
};
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
return { success: true, transfers };
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return {
|
|
396
|
+
success: false,
|
|
397
|
+
error: error instanceof Error ? error.message : "Failed to get transfers",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get statements
|
|
404
|
+
*/
|
|
405
|
+
export async function getStatements(accountId: string): Promise<{ success: boolean; statements?: Statement[]; error?: string }> {
|
|
406
|
+
try {
|
|
407
|
+
const p = await getPage();
|
|
408
|
+
|
|
409
|
+
// Navigate to account and then statements
|
|
410
|
+
await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
|
|
411
|
+
await p.waitForTimeout(1000);
|
|
412
|
+
|
|
413
|
+
// This would need to navigate to the specific account's statements section
|
|
414
|
+
// For now, return a message about navigation
|
|
415
|
+
|
|
416
|
+
const statements = await p.$$eval(
|
|
417
|
+
'.statement-row, [data-testid="statement"]',
|
|
418
|
+
(elements, acctId) =>
|
|
419
|
+
elements.slice(0, 12).map((el, index) => {
|
|
420
|
+
const periodEl = el.querySelector('.period, .statement-period');
|
|
421
|
+
const dateEl = el.querySelector('.date, .statement-date');
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
id: el.getAttribute('data-statement-id') || `stmt-${index}`,
|
|
425
|
+
accountId: acctId,
|
|
426
|
+
period: periodEl?.textContent?.trim() || '',
|
|
427
|
+
date: dateEl?.textContent?.trim() || '',
|
|
428
|
+
};
|
|
429
|
+
}),
|
|
430
|
+
accountId
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
return { success: true, statements };
|
|
434
|
+
} catch (error) {
|
|
435
|
+
return {
|
|
436
|
+
success: false,
|
|
437
|
+
error: error instanceof Error ? error.message : "Failed to get statements",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Get rewards/points info
|
|
444
|
+
*/
|
|
445
|
+
export async function getRewards(): Promise<{ success: boolean; rewards?: RewardsInfo; error?: string }> {
|
|
446
|
+
try {
|
|
447
|
+
const p = await getPage();
|
|
448
|
+
await p.goto(`${CHASE_BASE_URL}/web/auth/dashboard`, { waitUntil: "networkidle" });
|
|
449
|
+
|
|
450
|
+
await p.waitForTimeout(2000);
|
|
451
|
+
|
|
452
|
+
const rewards = await p.evaluate(() => {
|
|
453
|
+
const pointsEl = document.querySelector('.points-balance, .ultimate-rewards, [data-testid="points"]');
|
|
454
|
+
const cashBackEl = document.querySelector('.cash-back-balance, [data-testid="cashback"]');
|
|
455
|
+
const tierEl = document.querySelector('.rewards-tier, .membership-level');
|
|
456
|
+
|
|
457
|
+
const pointsText = pointsEl?.textContent?.trim() || '0';
|
|
458
|
+
const cashBackText = cashBackEl?.textContent?.trim() || '0';
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
pointsBalance: parseInt(pointsText.replace(/[^0-9]/g, '')) || 0,
|
|
462
|
+
cashBackBalance: parseFloat(cashBackText.replace(/[$,]/g, '')) || 0,
|
|
463
|
+
pendingRewards: 0,
|
|
464
|
+
tier: tierEl?.textContent?.trim() || undefined,
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return { success: true, rewards };
|
|
469
|
+
} catch (error) {
|
|
470
|
+
return {
|
|
471
|
+
success: false,
|
|
472
|
+
error: error instanceof Error ? error.message : "Failed to get rewards",
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Initiate a transfer (preview only - does not execute)
|
|
479
|
+
*/
|
|
480
|
+
export async function initiateTransfer(
|
|
481
|
+
fromAccountId: string,
|
|
482
|
+
toAccountId: string,
|
|
483
|
+
amount: number
|
|
484
|
+
): Promise<{ success: boolean; preview?: object; error?: string }> {
|
|
485
|
+
// For security, this only provides a preview and instructions
|
|
486
|
+
// The actual transfer would need to be confirmed by the user
|
|
487
|
+
return {
|
|
488
|
+
success: true,
|
|
489
|
+
preview: {
|
|
490
|
+
fromAccountId,
|
|
491
|
+
toAccountId,
|
|
492
|
+
amount,
|
|
493
|
+
message: "For security reasons, transfers must be completed manually in Chase. Navigate to Transfers in your Chase account to complete this transfer.",
|
|
494
|
+
instructions: [
|
|
495
|
+
"1. Log in to Chase.com or Chase mobile app",
|
|
496
|
+
"2. Go to 'Pay & Transfer' > 'Transfers'",
|
|
497
|
+
"3. Select your from and to accounts",
|
|
498
|
+
`4. Enter amount: $${amount.toFixed(2)}`,
|
|
499
|
+
"5. Review and confirm the transfer"
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Pay a bill (preview only - does not execute)
|
|
507
|
+
*/
|
|
508
|
+
export async function payBill(
|
|
509
|
+
payeeId: string,
|
|
510
|
+
amount: number,
|
|
511
|
+
date?: string
|
|
512
|
+
): Promise<{ success: boolean; preview?: object; error?: string }> {
|
|
513
|
+
// For security, this only provides a preview and instructions
|
|
514
|
+
return {
|
|
515
|
+
success: true,
|
|
516
|
+
preview: {
|
|
517
|
+
payeeId,
|
|
518
|
+
amount,
|
|
519
|
+
scheduledDate: date || "Immediately",
|
|
520
|
+
message: "For security reasons, bill payments must be completed manually in Chase.",
|
|
521
|
+
instructions: [
|
|
522
|
+
"1. Log in to Chase.com or Chase mobile app",
|
|
523
|
+
"2. Go to 'Pay & Transfer' > 'Pay Bills'",
|
|
524
|
+
"3. Select the payee",
|
|
525
|
+
`4. Enter amount: $${amount.toFixed(2)}`,
|
|
526
|
+
"5. Choose payment date and confirm"
|
|
527
|
+
]
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|