@finatic/client 0.0.131

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 (47) hide show
  1. package/README.md +489 -0
  2. package/dist/index.d.ts +2037 -0
  3. package/dist/index.js +4815 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +4787 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/types/client/ApiClient.d.ts +234 -0
  8. package/dist/types/client/FinaticConnect.d.ts +307 -0
  9. package/dist/types/index.d.ts +17 -0
  10. package/dist/types/mocks/MockApiClient.d.ts +228 -0
  11. package/dist/types/mocks/MockDataProvider.d.ts +132 -0
  12. package/dist/types/mocks/MockFactory.d.ts +53 -0
  13. package/dist/types/mocks/index.d.ts +5 -0
  14. package/dist/types/mocks/utils.d.ts +29 -0
  15. package/dist/types/portal/PortalUI.d.ts +38 -0
  16. package/dist/types/security/ApiSecurity.d.ts +24 -0
  17. package/dist/types/security/RuntimeSecurity.d.ts +28 -0
  18. package/dist/types/security/SecurityUtils.d.ts +21 -0
  19. package/dist/types/security/index.d.ts +2 -0
  20. package/dist/types/services/AnalyticsService.d.ts +18 -0
  21. package/dist/types/services/ApiClient.d.ts +121 -0
  22. package/dist/types/services/PortalService.d.ts +24 -0
  23. package/dist/types/services/TradingService.d.ts +55 -0
  24. package/dist/types/services/api.d.ts +23 -0
  25. package/dist/types/services/auth.d.ts +9 -0
  26. package/dist/types/services/index.d.ts +4 -0
  27. package/dist/types/services/portfolio.d.ts +10 -0
  28. package/dist/types/services/trading.d.ts +10 -0
  29. package/dist/types/shared/index.d.ts +2 -0
  30. package/dist/types/shared/themes/index.d.ts +2 -0
  31. package/dist/types/shared/themes/portalPresets.d.ts +8 -0
  32. package/dist/types/shared/themes/presets.d.ts +3 -0
  33. package/dist/types/shared/themes/system.d.ts +2 -0
  34. package/dist/types/shared/types/index.d.ts +110 -0
  35. package/dist/types/types/api.d.ts +486 -0
  36. package/dist/types/types/config.d.ts +12 -0
  37. package/dist/types/types/connect.d.ts +51 -0
  38. package/dist/types/types/errors.d.ts +47 -0
  39. package/dist/types/types/portal.d.ts +75 -0
  40. package/dist/types/types/security.d.ts +35 -0
  41. package/dist/types/types/shared.d.ts +50 -0
  42. package/dist/types/types/theme.d.ts +101 -0
  43. package/dist/types/types.d.ts +157 -0
  44. package/dist/types/utils/errors.d.ts +42 -0
  45. package/dist/types/utils/events.d.ts +12 -0
  46. package/dist/types/utils/themeUtils.d.ts +34 -0
  47. package/package.json +56 -0
package/dist/index.js ADDED
@@ -0,0 +1,4815 @@
1
+ 'use strict';
2
+
3
+ var uuid = require('uuid');
4
+
5
+ /**
6
+ * API-related types for Finatic Connect SDK
7
+ */
8
+ var SessionState;
9
+ (function (SessionState) {
10
+ SessionState["PENDING"] = "PENDING";
11
+ SessionState["AUTHENTICATING"] = "AUTHENTICATING";
12
+ SessionState["ACTIVE"] = "ACTIVE";
13
+ SessionState["COMPLETED"] = "COMPLETED";
14
+ SessionState["EXPIRED"] = "EXPIRED";
15
+ })(SessionState || (SessionState = {}));
16
+
17
+ /**
18
+ * Core types for Finatic Connect SDK
19
+ */
20
+ class PaginatedResult {
21
+ constructor(data, paginationInfo, navigationCallback) {
22
+ this.data = data;
23
+ this.metadata = {
24
+ hasMore: paginationInfo.has_more,
25
+ nextOffset: paginationInfo.next_offset,
26
+ currentOffset: paginationInfo.current_offset,
27
+ limit: paginationInfo.limit,
28
+ currentPage: Math.floor(paginationInfo.current_offset / paginationInfo.limit) + 1,
29
+ hasNext: paginationInfo.has_more,
30
+ hasPrevious: paginationInfo.current_offset > 0,
31
+ };
32
+ this.navigationCallback = navigationCallback;
33
+ }
34
+ get hasNext() {
35
+ return this.metadata.hasNext;
36
+ }
37
+ get hasPrevious() {
38
+ return this.metadata.hasPrevious;
39
+ }
40
+ get currentPage() {
41
+ return this.metadata.currentPage;
42
+ }
43
+ /**
44
+ * Navigate to the next page
45
+ * @returns Promise<PaginatedResult<T> | null> - Next page result or null if no next page
46
+ */
47
+ async nextPage() {
48
+ if (!this.hasNext || !this.navigationCallback) {
49
+ return null;
50
+ }
51
+ try {
52
+ return await this.navigationCallback(this.metadata.nextOffset, this.metadata.limit);
53
+ }
54
+ catch (error) {
55
+ console.error('Error navigating to next page:', error);
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Navigate to the previous page
61
+ * @returns Promise<PaginatedResult<T> | null> - Previous page result or null if no previous page
62
+ */
63
+ async previousPage() {
64
+ if (!this.hasPrevious || !this.navigationCallback) {
65
+ return null;
66
+ }
67
+ const previousOffset = Math.max(0, this.metadata.currentOffset - this.metadata.limit);
68
+ try {
69
+ return await this.navigationCallback(previousOffset, this.metadata.limit);
70
+ }
71
+ catch (error) {
72
+ console.error('Error navigating to previous page:', error);
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Navigate to a specific page
78
+ * @param pageNumber - The page number to navigate to (1-based)
79
+ * @returns Promise<PaginatedResult<T> | null> - Page result or null if page doesn't exist
80
+ */
81
+ async goToPage(pageNumber) {
82
+ if (pageNumber < 1 || !this.navigationCallback) {
83
+ return null;
84
+ }
85
+ const targetOffset = (pageNumber - 1) * this.metadata.limit;
86
+ try {
87
+ return await this.navigationCallback(targetOffset, this.metadata.limit);
88
+ }
89
+ catch (error) {
90
+ console.error('Error navigating to page:', pageNumber, error);
91
+ return null;
92
+ }
93
+ }
94
+ /**
95
+ * Get the first page
96
+ * @returns Promise<PaginatedResult<T> | null> - First page result or null if error
97
+ */
98
+ async firstPage() {
99
+ return this.goToPage(1);
100
+ }
101
+ /**
102
+ * Get the last page (this is a best effort - we don't know the total count)
103
+ * @returns Promise<PaginatedResult<T> | null> - Last page result or null if error
104
+ */
105
+ async lastPage() {
106
+ if (!this.navigationCallback) {
107
+ return null;
108
+ }
109
+ const findLast = async (page) => {
110
+ const next = await page.nextPage();
111
+ if (next) {
112
+ return findLast(next);
113
+ }
114
+ return page;
115
+ };
116
+ return findLast(this);
117
+ }
118
+ /**
119
+ * Get pagination info as a string
120
+ * @returns string - Human readable pagination info
121
+ */
122
+ getPaginationInfo() {
123
+ return `Page ${this.currentPage} (${this.metadata.currentOffset + 1}-${this.metadata.currentOffset + this.metadata.limit}) - ${this.hasNext ? 'Has more' : 'Last page'}`;
124
+ }
125
+ }
126
+
127
+ class BaseError extends Error {
128
+ constructor(message, code = 'UNKNOWN_ERROR') {
129
+ super(message);
130
+ this.code = code;
131
+ this.name = this.constructor.name;
132
+ }
133
+ }
134
+ class ApiError extends BaseError {
135
+ constructor(status, message, details) {
136
+ super(message, `API_ERROR_${status}`);
137
+ this.status = status;
138
+ this.details = details;
139
+ this.name = 'ApiError';
140
+ }
141
+ }
142
+ class SessionError extends ApiError {
143
+ constructor(message, details) {
144
+ super(400, message, details);
145
+ this.name = 'SessionError';
146
+ }
147
+ }
148
+ class AuthenticationError extends ApiError {
149
+ constructor(message, details) {
150
+ super(401, message, details);
151
+ this.name = 'AuthenticationError';
152
+ }
153
+ }
154
+ class AuthorizationError extends ApiError {
155
+ constructor(message, details) {
156
+ super(403, message, details);
157
+ this.name = 'AuthorizationError';
158
+ }
159
+ }
160
+ class RateLimitError extends ApiError {
161
+ constructor(message, details) {
162
+ super(429, message, details);
163
+ this.name = 'RateLimitError';
164
+ }
165
+ }
166
+ class TokenError extends BaseError {
167
+ constructor(message) {
168
+ super(message, 'TOKEN_ERROR');
169
+ this.name = 'TokenError';
170
+ }
171
+ }
172
+ class ValidationError extends BaseError {
173
+ constructor(message) {
174
+ super(message, 'VALIDATION_ERROR');
175
+ this.name = 'ValidationError';
176
+ }
177
+ }
178
+ class NetworkError extends BaseError {
179
+ constructor(message) {
180
+ super(message, 'NETWORK_ERROR');
181
+ this.name = 'NetworkError';
182
+ }
183
+ }
184
+ class SecurityError extends BaseError {
185
+ constructor(message) {
186
+ super(message, 'SECURITY_ERROR');
187
+ this.name = 'SecurityError';
188
+ }
189
+ }
190
+ class CompanyAccessError extends ApiError {
191
+ constructor(message, details) {
192
+ super(403, message, details);
193
+ this.name = 'CompanyAccessError';
194
+ }
195
+ }
196
+ class OrderError extends ApiError {
197
+ constructor(message, details) {
198
+ super(500, message, details);
199
+ this.name = 'OrderError';
200
+ }
201
+ }
202
+ class OrderValidationError extends ApiError {
203
+ constructor(message, details) {
204
+ super(400, message, details);
205
+ this.name = 'OrderValidationError';
206
+ }
207
+ }
208
+
209
+ class ApiClient {
210
+ constructor(baseUrl, deviceInfo) {
211
+ this.currentSessionState = null;
212
+ this.currentSessionId = null;
213
+ this.tradingContext = {};
214
+ // Token management
215
+ this.tokenInfo = null;
216
+ this.refreshPromise = null;
217
+ this.REFRESH_BUFFER_MINUTES = 5; // Refresh token 5 minutes before expiry
218
+ // Session and company context
219
+ this.companyId = null;
220
+ this.csrfToken = null;
221
+ this.baseUrl = baseUrl;
222
+ this.deviceInfo = deviceInfo;
223
+ // Ensure baseUrl doesn't end with a slash
224
+ this.baseUrl = baseUrl.replace(/\/$/, '');
225
+ // Append /api/v1 if not already present
226
+ if (!this.baseUrl.includes('/api/v1')) {
227
+ this.baseUrl = `${this.baseUrl}/api/v1`;
228
+ }
229
+ }
230
+ /**
231
+ * Set session context (session ID, company ID, CSRF token)
232
+ */
233
+ setSessionContext(sessionId, companyId, csrfToken) {
234
+ this.currentSessionId = sessionId;
235
+ this.companyId = companyId;
236
+ this.csrfToken = csrfToken || null;
237
+ }
238
+ /**
239
+ * Get the current session ID
240
+ */
241
+ getCurrentSessionId() {
242
+ return this.currentSessionId;
243
+ }
244
+ /**
245
+ * Get the current company ID
246
+ */
247
+ getCurrentCompanyId() {
248
+ return this.companyId;
249
+ }
250
+ /**
251
+ * Get the current CSRF token
252
+ */
253
+ getCurrentCsrfToken() {
254
+ return this.csrfToken;
255
+ }
256
+ /**
257
+ * Store tokens after successful authentication
258
+ */
259
+ setTokens(accessToken, refreshToken, expiresAt, userId) {
260
+ this.tokenInfo = {
261
+ accessToken,
262
+ refreshToken,
263
+ expiresAt,
264
+ userId,
265
+ };
266
+ }
267
+ /**
268
+ * Get the current access token, refreshing if necessary
269
+ */
270
+ async getValidAccessToken() {
271
+ if (!this.tokenInfo) {
272
+ throw new AuthenticationError('No tokens available. Please authenticate first.');
273
+ }
274
+ // Check if token is expired or about to expire
275
+ if (this.isTokenExpired()) {
276
+ await this.refreshTokens();
277
+ }
278
+ return this.tokenInfo.accessToken;
279
+ }
280
+ /**
281
+ * Check if the current token is expired or about to expire
282
+ */
283
+ isTokenExpired() {
284
+ if (!this.tokenInfo)
285
+ return true;
286
+ const expiryTime = new Date(this.tokenInfo.expiresAt).getTime();
287
+ const currentTime = Date.now();
288
+ const bufferTime = this.REFRESH_BUFFER_MINUTES * 60 * 1000; // 5 minutes in milliseconds
289
+ return currentTime >= expiryTime - bufferTime;
290
+ }
291
+ /**
292
+ * Refresh the access token using the refresh token
293
+ */
294
+ async refreshTokens() {
295
+ if (!this.tokenInfo) {
296
+ throw new AuthenticationError('No refresh token available.');
297
+ }
298
+ // If a refresh is already in progress, wait for it
299
+ if (this.refreshPromise) {
300
+ await this.refreshPromise;
301
+ return;
302
+ }
303
+ // Start a new refresh
304
+ this.refreshPromise = this.performTokenRefresh();
305
+ try {
306
+ await this.refreshPromise;
307
+ }
308
+ finally {
309
+ this.refreshPromise = null;
310
+ }
311
+ }
312
+ /**
313
+ * Perform the actual token refresh request
314
+ */
315
+ async performTokenRefresh() {
316
+ if (!this.tokenInfo) {
317
+ throw new AuthenticationError('No refresh token available.');
318
+ }
319
+ try {
320
+ const response = await this.request('/company/auth/refresh', {
321
+ method: 'POST',
322
+ headers: {
323
+ 'Content-Type': 'application/json',
324
+ },
325
+ body: {
326
+ refresh_token: this.tokenInfo.refreshToken,
327
+ },
328
+ });
329
+ // Update stored tokens
330
+ this.tokenInfo = {
331
+ accessToken: response.response_data.access_token,
332
+ refreshToken: response.response_data.refresh_token,
333
+ expiresAt: response.response_data.expires_at,
334
+ userId: this.tokenInfo.userId,
335
+ };
336
+ return this.tokenInfo;
337
+ }
338
+ catch (error) {
339
+ // Clear tokens on refresh failure
340
+ this.tokenInfo = null;
341
+ throw new AuthenticationError('Token refresh failed. Please re-authenticate.', error);
342
+ }
343
+ }
344
+ /**
345
+ * Clear stored tokens (useful for logout)
346
+ */
347
+ clearTokens() {
348
+ this.tokenInfo = null;
349
+ this.refreshPromise = null;
350
+ }
351
+ /**
352
+ * Get current token info (for debugging/testing)
353
+ */
354
+ getTokenInfo() {
355
+ return this.tokenInfo ? { ...this.tokenInfo } : null;
356
+ }
357
+ /**
358
+ * Make a request to the API.
359
+ */
360
+ async request(path, options) {
361
+ // Ensure path starts with a slash
362
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
363
+ const url = new URL(`${this.baseUrl}${normalizedPath}`);
364
+ if (options.params) {
365
+ Object.entries(options.params).forEach(([key, value]) => {
366
+ url.searchParams.append(key, value);
367
+ });
368
+ }
369
+ const response = await fetch(url.toString(), {
370
+ method: options.method,
371
+ headers: options.headers,
372
+ body: options.body ? JSON.stringify(options.body) : undefined,
373
+ });
374
+ if (!response.ok) {
375
+ const error = await response.json();
376
+ throw this.handleError(response.status, error);
377
+ }
378
+ const data = await response.json();
379
+ // Check if the response has a success field and it's false
380
+ if (data && typeof data === 'object' && 'success' in data && data.success === false) {
381
+ // For order endpoints, provide more context
382
+ const isOrderEndpoint = path.includes('/brokers/orders');
383
+ if (isOrderEndpoint) {
384
+ // Add context that this is an order-related error
385
+ data._isOrderError = true;
386
+ }
387
+ throw this.handleError(data.status_code || 500, data);
388
+ }
389
+ // Check if the response has a status_code field indicating an error (4xx or 5xx)
390
+ if (data && typeof data === 'object' && 'status_code' in data && data.status_code >= 400) {
391
+ throw this.handleError(data.status_code, data);
392
+ }
393
+ // Check if the response has errors field with content
394
+ if (data &&
395
+ typeof data === 'object' &&
396
+ 'errors' in data &&
397
+ data.errors &&
398
+ Array.isArray(data.errors) &&
399
+ data.errors.length > 0) {
400
+ throw this.handleError(data.status_code || 500, data);
401
+ }
402
+ return data;
403
+ }
404
+ /**
405
+ * Handle API errors. This method can be overridden by language-specific implementations.
406
+ */
407
+ handleError(status, error) {
408
+ // Extract message from the error object with multiple fallback options
409
+ let message = 'API request failed';
410
+ if (error && typeof error === 'object') {
411
+ // Try different possible message fields
412
+ message =
413
+ error.message ||
414
+ error.detail?.message ||
415
+ error.error?.message ||
416
+ error.errors?.[0]?.message ||
417
+ (typeof error.errors === 'string' ? error.errors : null) ||
418
+ 'API request failed';
419
+ }
420
+ // Check if this is an order-related error (either from order endpoints or order validation)
421
+ const isOrderError = error._isOrderError ||
422
+ message.includes('ORDER_FAILED') ||
423
+ message.includes('AUTH_ERROR') ||
424
+ message.includes('not found or not available for trading') ||
425
+ message.includes('Symbol') ||
426
+ (error.errors &&
427
+ Array.isArray(error.errors) &&
428
+ error.errors.some((e) => e.category === 'INVALID_ORDER' ||
429
+ e.message?.includes('Symbol') ||
430
+ e.message?.includes('not available for trading')));
431
+ if (isOrderError) {
432
+ // Check if this is a validation error (400 status with specific validation messages)
433
+ const isValidationError = status === 400 &&
434
+ (message.includes('not found or not available for trading') ||
435
+ message.includes('Symbol') ||
436
+ (error.errors &&
437
+ Array.isArray(error.errors) &&
438
+ error.errors.some((e) => e.category === 'INVALID_ORDER' ||
439
+ e.message?.includes('Symbol') ||
440
+ e.message?.includes('not available for trading'))));
441
+ if (isValidationError) {
442
+ return new OrderValidationError(message, error);
443
+ }
444
+ // For order placement errors, provide more specific error messages
445
+ if (message.includes('ORDER_FAILED') || message.includes('AUTH_ERROR')) {
446
+ // Extract the specific error from the nested structure
447
+ const orderErrorMatch = message.match(/\[([^\]]+)\]/g);
448
+ if (orderErrorMatch && orderErrorMatch.length > 0) {
449
+ // Take the last error in the chain (most specific)
450
+ const specificError = orderErrorMatch[orderErrorMatch.length - 1].replace(/[[\]]/g, '');
451
+ message = `Order failed: ${specificError}`;
452
+ }
453
+ }
454
+ // Use OrderError for order-related failures
455
+ return new OrderError(message, error);
456
+ }
457
+ switch (status) {
458
+ case 400:
459
+ return new SessionError(message, error);
460
+ case 401:
461
+ return new AuthenticationError(message || 'Unauthorized: Invalid or missing session token', error);
462
+ case 403:
463
+ if (error.detail?.code === 'NO_COMPANY_ACCESS') {
464
+ return new CompanyAccessError(error.detail.message || 'No broker connections found for this company', error.detail);
465
+ }
466
+ return new AuthorizationError(message || 'Forbidden: No access to the requested data', error);
467
+ case 404:
468
+ return new ApiError(status, message || 'Not found: The requested data does not exist', error);
469
+ case 429:
470
+ return new RateLimitError(message || 'Rate limit exceeded', error);
471
+ case 500:
472
+ return new ApiError(status, message || 'Internal server error', error);
473
+ default:
474
+ return new ApiError(status, message || 'API request failed', error);
475
+ }
476
+ }
477
+ // Session Management
478
+ async startSession(token, userId) {
479
+ const response = await this.request('/auth/session/start', {
480
+ method: 'POST',
481
+ headers: {
482
+ 'Content-Type': 'application/json',
483
+ 'One-Time-Token': token,
484
+ 'X-Device-Info': JSON.stringify({
485
+ ip_address: this.deviceInfo?.ip_address || '',
486
+ user_agent: this.deviceInfo?.user_agent || '',
487
+ fingerprint: this.deviceInfo?.fingerprint || '',
488
+ }),
489
+ },
490
+ body: {
491
+ user_id: userId,
492
+ },
493
+ });
494
+ // Store session ID and set state to ACTIVE
495
+ this.currentSessionId = response.data.session_id;
496
+ this.currentSessionState = SessionState.ACTIVE;
497
+ return response;
498
+ }
499
+ // OTP Flow
500
+ async requestOtp(sessionId, email) {
501
+ return this.request('/auth/otp/request', {
502
+ method: 'POST',
503
+ headers: {
504
+ 'Content-Type': 'application/json',
505
+ 'X-Session-ID': sessionId,
506
+ 'X-Device-Info': JSON.stringify({
507
+ ip_address: this.deviceInfo?.ip_address || '',
508
+ user_agent: this.deviceInfo?.user_agent || '',
509
+ fingerprint: this.deviceInfo?.fingerprint || '',
510
+ }),
511
+ },
512
+ body: {
513
+ email,
514
+ },
515
+ });
516
+ }
517
+ async verifyOtp(sessionId, otp) {
518
+ const response = await this.request('/auth/otp/verify', {
519
+ method: 'POST',
520
+ headers: {
521
+ 'Content-Type': 'application/json',
522
+ 'X-Session-ID': sessionId,
523
+ 'X-Device-Info': JSON.stringify({
524
+ ip_address: this.deviceInfo?.ip_address || '',
525
+ user_agent: this.deviceInfo?.user_agent || '',
526
+ fingerprint: this.deviceInfo?.fingerprint || '',
527
+ }),
528
+ },
529
+ body: {
530
+ otp,
531
+ },
532
+ });
533
+ // Store tokens after successful OTP verification
534
+ if (response.success && response.data) {
535
+ const expiresAt = new Date(Date.now() + response.data.expires_in * 1000).toISOString();
536
+ this.setTokens(response.data.access_token, response.data.refresh_token, expiresAt, response.data.user_id);
537
+ }
538
+ return response;
539
+ }
540
+ // Direct Authentication
541
+ async authenticateDirectly(sessionId, userId) {
542
+ // Ensure session is active before authenticating
543
+ if (this.currentSessionState !== SessionState.ACTIVE) {
544
+ throw new SessionError('Session must be in ACTIVE state to authenticate');
545
+ }
546
+ const response = await this.request('/auth/session/authenticate', {
547
+ method: 'POST',
548
+ headers: {
549
+ 'Content-Type': 'application/json',
550
+ 'Session-ID': sessionId,
551
+ 'X-Session-ID': sessionId,
552
+ 'X-Device-Info': JSON.stringify({
553
+ ip_address: this.deviceInfo?.ip_address || '',
554
+ user_agent: this.deviceInfo?.user_agent || '',
555
+ fingerprint: this.deviceInfo?.fingerprint || '',
556
+ }),
557
+ },
558
+ body: {
559
+ session_id: sessionId,
560
+ user_id: userId,
561
+ },
562
+ });
563
+ // Store tokens after successful direct authentication
564
+ if (response.success && response.data) {
565
+ // For direct auth, we don't get expires_in, so we'll set a default 1-hour expiry
566
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour
567
+ this.setTokens(response.data.access_token, response.data.refresh_token, expiresAt, userId);
568
+ }
569
+ return response;
570
+ }
571
+ // Portal Management
572
+ /**
573
+ * Get the portal URL for an active session
574
+ * @param sessionId The session identifier
575
+ * @returns Portal URL response
576
+ * @throws SessionError if session is not in ACTIVE state
577
+ */
578
+ async getPortalUrl(sessionId) {
579
+ if (this.currentSessionState !== SessionState.ACTIVE) {
580
+ throw new SessionError('Session must be in ACTIVE state to get portal URL');
581
+ }
582
+ return this.request('/auth/session/portal', {
583
+ method: 'GET',
584
+ headers: {
585
+ 'Content-Type': 'application/json',
586
+ 'Session-ID': sessionId,
587
+ 'X-Session-ID': sessionId,
588
+ 'X-Device-Info': JSON.stringify({
589
+ ip_address: this.deviceInfo?.ip_address || '',
590
+ user_agent: this.deviceInfo?.user_agent || '',
591
+ fingerprint: this.deviceInfo?.fingerprint || '',
592
+ }),
593
+ },
594
+ });
595
+ }
596
+ async validatePortalSession(sessionId, signature) {
597
+ return this.request('/portal/validate', {
598
+ method: 'GET',
599
+ headers: {
600
+ 'Content-Type': 'application/json',
601
+ 'X-Session-ID': sessionId,
602
+ 'X-Device-Info': JSON.stringify({
603
+ ip_address: this.deviceInfo?.ip_address || '',
604
+ user_agent: this.deviceInfo?.user_agent || '',
605
+ fingerprint: this.deviceInfo?.fingerprint || '',
606
+ }),
607
+ },
608
+ params: {
609
+ signature,
610
+ },
611
+ });
612
+ }
613
+ async completePortalSession(sessionId) {
614
+ return this.request(`/portal/${sessionId}/complete`, {
615
+ method: 'POST',
616
+ headers: {
617
+ 'Content-Type': 'application/json',
618
+ },
619
+ });
620
+ }
621
+ // Portfolio Management
622
+ async getHoldings(accessToken) {
623
+ return this.request('/portfolio/holdings', {
624
+ method: 'GET',
625
+ headers: {
626
+ 'Content-Type': 'application/json',
627
+ Authorization: `Bearer ${accessToken}`,
628
+ },
629
+ });
630
+ }
631
+ async getOrders(accessToken) {
632
+ return this.request('/orders/', {
633
+ method: 'GET',
634
+ headers: {
635
+ 'Content-Type': 'application/json',
636
+ Authorization: `Bearer ${accessToken}`,
637
+ },
638
+ });
639
+ }
640
+ async getPortfolio(accessToken) {
641
+ const response = await this.request('/portfolio/', {
642
+ method: 'GET',
643
+ headers: {
644
+ Authorization: `Bearer ${accessToken}`,
645
+ 'Content-Type': 'application/json',
646
+ },
647
+ });
648
+ return response;
649
+ }
650
+ async placeOrder(accessToken, order) {
651
+ await this.request('/orders/', {
652
+ method: 'POST',
653
+ headers: {
654
+ Authorization: `Bearer ${accessToken}`,
655
+ 'Content-Type': 'application/json',
656
+ },
657
+ body: order,
658
+ });
659
+ }
660
+ // New methods with automatic token management
661
+ async getHoldingsAuto() {
662
+ const accessToken = await this.getValidAccessToken();
663
+ return this.getHoldings(accessToken);
664
+ }
665
+ async getOrdersAuto() {
666
+ const accessToken = await this.getValidAccessToken();
667
+ return this.getOrders(accessToken);
668
+ }
669
+ async getPortfolioAuto() {
670
+ const accessToken = await this.getValidAccessToken();
671
+ return this.getPortfolio(accessToken);
672
+ }
673
+ async placeOrderAuto(order) {
674
+ const accessToken = await this.getValidAccessToken();
675
+ return this.placeOrder(accessToken, order);
676
+ }
677
+ // Enhanced Trading Methods with Session Management
678
+ async placeBrokerOrder(accessToken, params, extras = {}) {
679
+ // Merge context with provided parameters
680
+ const fullParams = {
681
+ broker: (params.broker || this.tradingContext.broker) ||
682
+ (() => {
683
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
684
+ })(),
685
+ accountNumber: params.accountNumber ||
686
+ this.tradingContext.accountNumber ||
687
+ (() => {
688
+ throw new Error('Account not set. Call setAccount() or pass accountNumber parameter.');
689
+ })(),
690
+ symbol: params.symbol,
691
+ orderQty: params.orderQty,
692
+ action: params.action,
693
+ orderType: params.orderType,
694
+ assetType: params.assetType,
695
+ timeInForce: params.timeInForce || 'day',
696
+ price: params.price,
697
+ stopPrice: params.stopPrice,
698
+ };
699
+ // Build request body with snake_case parameter names
700
+ const requestBody = this.buildOrderRequestBody(fullParams, extras);
701
+ return this.request('/brokers/orders', {
702
+ method: 'POST',
703
+ headers: {
704
+ 'Content-Type': 'application/json',
705
+ Authorization: `Bearer ${accessToken}`,
706
+ 'Session-ID': this.currentSessionId || '',
707
+ 'X-Session-ID': this.currentSessionId || '',
708
+ 'X-Device-Info': JSON.stringify(this.deviceInfo),
709
+ },
710
+ body: requestBody,
711
+ });
712
+ }
713
+ async cancelBrokerOrder(orderId, broker, extras = {}) {
714
+ const targetBroker = broker ||
715
+ this.tradingContext.broker ||
716
+ (() => {
717
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
718
+ })();
719
+ // Build optional body for extras if provided
720
+ const body = Object.keys(extras).length > 0 ? { order: extras } : undefined;
721
+ return this.request(`/brokers/orders/${targetBroker}/${orderId}`, {
722
+ method: 'DELETE',
723
+ headers: {
724
+ 'Content-Type': 'application/json',
725
+ 'Session-ID': this.currentSessionId || '',
726
+ 'X-Session-ID': this.currentSessionId || '',
727
+ 'X-Device-Info': JSON.stringify(this.deviceInfo),
728
+ },
729
+ body,
730
+ });
731
+ }
732
+ async modifyBrokerOrder(orderId, params, broker, extras = {}) {
733
+ const targetBroker = broker ||
734
+ this.tradingContext.broker ||
735
+ (() => {
736
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
737
+ })();
738
+ // Build request body with snake_case parameter names and include broker
739
+ const requestBody = this.buildModifyRequestBody(params, extras, targetBroker);
740
+ return this.request(`/brokers/orders/${orderId}`, {
741
+ method: 'PATCH',
742
+ headers: {
743
+ 'Content-Type': 'application/json',
744
+ 'Session-ID': this.currentSessionId || '',
745
+ 'X-Session-ID': this.currentSessionId || '',
746
+ 'X-Device-Info': JSON.stringify(this.deviceInfo),
747
+ },
748
+ body: requestBody,
749
+ });
750
+ }
751
+ // Context management methods
752
+ setBroker(broker) {
753
+ this.tradingContext.broker = broker;
754
+ // Clear account when broker changes
755
+ this.tradingContext.accountNumber = undefined;
756
+ this.tradingContext.accountId = undefined;
757
+ }
758
+ setAccount(accountNumber, accountId) {
759
+ this.tradingContext.accountNumber = accountNumber;
760
+ this.tradingContext.accountId = accountId;
761
+ }
762
+ getTradingContext() {
763
+ return { ...this.tradingContext };
764
+ }
765
+ clearTradingContext() {
766
+ this.tradingContext = {};
767
+ }
768
+ // Stock convenience methods
769
+ async placeStockMarketOrder(accessToken, symbol, orderQty, action, broker, accountNumber, extras = {}) {
770
+ return this.placeBrokerOrder(accessToken, {
771
+ broker,
772
+ accountNumber,
773
+ symbol,
774
+ orderQty,
775
+ action,
776
+ orderType: 'Market',
777
+ assetType: 'Stock',
778
+ timeInForce: 'day',
779
+ }, extras);
780
+ }
781
+ async placeStockLimitOrder(accessToken, symbol, orderQty, action, price, timeInForce = 'gtc', broker, accountNumber, extras = {}) {
782
+ return this.placeBrokerOrder(accessToken, {
783
+ broker,
784
+ accountNumber,
785
+ symbol,
786
+ orderQty,
787
+ action,
788
+ orderType: 'Limit',
789
+ assetType: 'Stock',
790
+ timeInForce,
791
+ price,
792
+ }, extras);
793
+ }
794
+ async placeStockStopOrder(accessToken, symbol, orderQty, action, stopPrice, timeInForce = 'day', broker, accountNumber, extras = {}) {
795
+ return this.placeBrokerOrder(accessToken, {
796
+ broker,
797
+ accountNumber,
798
+ symbol,
799
+ orderQty,
800
+ action,
801
+ orderType: 'Stop',
802
+ assetType: 'Stock',
803
+ timeInForce,
804
+ stopPrice,
805
+ }, extras);
806
+ }
807
+ // Crypto convenience methods
808
+ async placeCryptoMarketOrder(accessToken, symbol, orderQty, action, options = {}, broker, accountNumber, extras = {}) {
809
+ const orderParams = {
810
+ broker,
811
+ accountNumber,
812
+ symbol,
813
+ orderQty: options.quantity || orderQty,
814
+ action,
815
+ orderType: 'Market',
816
+ assetType: 'Crypto',
817
+ timeInForce: 'gtc', // Crypto typically uses GTC
818
+ };
819
+ // Add notional if provided
820
+ if (options.notional) {
821
+ orderParams.notional = options.notional;
822
+ }
823
+ return this.placeBrokerOrder(accessToken, orderParams, extras);
824
+ }
825
+ async placeCryptoLimitOrder(accessToken, symbol, orderQty, action, price, timeInForce = 'gtc', options = {}, broker, accountNumber, extras = {}) {
826
+ const orderParams = {
827
+ broker,
828
+ accountNumber,
829
+ symbol,
830
+ orderQty: options.quantity || orderQty,
831
+ action,
832
+ orderType: 'Limit',
833
+ assetType: 'Crypto',
834
+ timeInForce,
835
+ price,
836
+ };
837
+ // Add notional if provided
838
+ if (options.notional) {
839
+ orderParams.notional = options.notional;
840
+ }
841
+ return this.placeBrokerOrder(accessToken, orderParams, extras);
842
+ }
843
+ // Options convenience methods
844
+ async placeOptionsMarketOrder(accessToken, symbol, orderQty, action, options, broker, accountNumber, extras = {}) {
845
+ const orderParams = {
846
+ broker,
847
+ accountNumber,
848
+ symbol,
849
+ orderQty,
850
+ action,
851
+ orderType: 'Market',
852
+ assetType: 'Option',
853
+ timeInForce: 'day',
854
+ };
855
+ // Add options-specific parameters to extras
856
+ const targetBroker = broker || this.tradingContext.broker;
857
+ const optionsExtras = {
858
+ ...extras,
859
+ ...(targetBroker && {
860
+ [targetBroker]: {
861
+ ...extras[targetBroker],
862
+ strikePrice: options.strikePrice,
863
+ expirationDate: options.expirationDate,
864
+ optionType: options.optionType,
865
+ contractSize: options.contractSize || 100,
866
+ },
867
+ }),
868
+ };
869
+ return this.placeBrokerOrder(accessToken, orderParams, optionsExtras);
870
+ }
871
+ async placeOptionsLimitOrder(accessToken, symbol, orderQty, action, price, options, timeInForce = 'gtc', broker, accountNumber, extras = {}) {
872
+ const orderParams = {
873
+ broker,
874
+ accountNumber,
875
+ symbol,
876
+ orderQty,
877
+ action,
878
+ orderType: 'Limit',
879
+ assetType: 'Option',
880
+ timeInForce,
881
+ price,
882
+ };
883
+ // Add options-specific parameters to extras
884
+ const targetBroker = broker || this.tradingContext.broker;
885
+ const optionsExtras = {
886
+ ...extras,
887
+ ...(targetBroker && {
888
+ [targetBroker]: {
889
+ ...extras[targetBroker],
890
+ strikePrice: options.strikePrice,
891
+ expirationDate: options.expirationDate,
892
+ optionType: options.optionType,
893
+ contractSize: options.contractSize || 100,
894
+ },
895
+ }),
896
+ };
897
+ return this.placeBrokerOrder(accessToken, orderParams, optionsExtras);
898
+ }
899
+ // Futures convenience methods
900
+ async placeFuturesMarketOrder(accessToken, symbol, orderQty, action, broker, accountNumber, extras = {}) {
901
+ return this.placeBrokerOrder(accessToken, {
902
+ broker,
903
+ accountNumber,
904
+ symbol,
905
+ orderQty,
906
+ action,
907
+ orderType: 'Market',
908
+ assetType: 'Futures',
909
+ timeInForce: 'day',
910
+ }, extras);
911
+ }
912
+ async placeFuturesLimitOrder(accessToken, symbol, orderQty, action, price, timeInForce = 'gtc', broker, accountNumber, extras = {}) {
913
+ return this.placeBrokerOrder(accessToken, {
914
+ broker,
915
+ accountNumber,
916
+ symbol,
917
+ orderQty,
918
+ action,
919
+ orderType: 'Limit',
920
+ assetType: 'Futures',
921
+ timeInForce,
922
+ price,
923
+ }, extras);
924
+ }
925
+ buildOrderRequestBody(params, extras = {}) {
926
+ const baseOrder = {
927
+ order_type: params.orderType,
928
+ asset_type: params.assetType,
929
+ action: params.action,
930
+ time_in_force: params.timeInForce.toLowerCase(),
931
+ account_number: params.accountNumber,
932
+ symbol: params.symbol,
933
+ order_qty: params.orderQty,
934
+ };
935
+ if (params.price !== undefined)
936
+ baseOrder.price = params.price;
937
+ if (params.stopPrice !== undefined)
938
+ baseOrder.stop_price = params.stopPrice;
939
+ // Apply broker-specific defaults
940
+ const brokerExtras = this.applyBrokerDefaults(params.broker, extras[params.broker] || {});
941
+ return {
942
+ broker: params.broker,
943
+ ...baseOrder,
944
+ ...brokerExtras,
945
+ };
946
+ }
947
+ buildModifyRequestBody(params, extras, broker) {
948
+ const requestBody = {
949
+ broker: broker,
950
+ };
951
+ if (params.orderType !== undefined)
952
+ requestBody.order_type = params.orderType;
953
+ if (params.assetType !== undefined)
954
+ requestBody.asset_type = params.assetType;
955
+ if (params.action !== undefined)
956
+ requestBody.action = params.action;
957
+ if (params.timeInForce !== undefined)
958
+ requestBody.time_in_force = params.timeInForce.toLowerCase();
959
+ if (params.accountNumber !== undefined)
960
+ requestBody.account_number = params.accountNumber;
961
+ if (params.symbol !== undefined)
962
+ requestBody.symbol = params.symbol;
963
+ if (params.orderQty !== undefined)
964
+ requestBody.order_qty = params.orderQty;
965
+ if (params.price !== undefined)
966
+ requestBody.price = params.price;
967
+ if (params.stopPrice !== undefined)
968
+ requestBody.stop_price = params.stopPrice;
969
+ // Apply broker-specific defaults
970
+ const brokerExtras = this.applyBrokerDefaults(broker, extras[broker] || {});
971
+ return {
972
+ ...requestBody,
973
+ ...brokerExtras,
974
+ };
975
+ }
976
+ applyBrokerDefaults(broker, extras) {
977
+ switch (broker) {
978
+ case 'robinhood':
979
+ return {
980
+ extendedHours: extras.extendedHours ?? true,
981
+ marketHours: extras.marketHours ?? 'regular_hours',
982
+ trailType: extras.trailType ?? 'percentage',
983
+ ...extras,
984
+ };
985
+ case 'ninja_trader':
986
+ return {
987
+ accountSpec: extras.accountSpec ?? '',
988
+ isAutomated: extras.isAutomated ?? true,
989
+ ...extras,
990
+ };
991
+ case 'tasty_trade':
992
+ return {
993
+ automatedSource: extras.automatedSource ?? true,
994
+ ...extras,
995
+ };
996
+ default:
997
+ return extras;
998
+ }
999
+ }
1000
+ async revokeToken(accessToken) {
1001
+ return this.request('/auth/token/revoke', {
1002
+ method: 'POST',
1003
+ headers: {
1004
+ 'Content-Type': 'application/json',
1005
+ Authorization: `Bearer ${accessToken}`,
1006
+ },
1007
+ });
1008
+ }
1009
+ async getUserToken(userId) {
1010
+ return this.request('/auth/token', {
1011
+ method: 'POST',
1012
+ headers: {
1013
+ 'Content-Type': 'application/json',
1014
+ },
1015
+ body: { userId },
1016
+ });
1017
+ }
1018
+ getCurrentSessionState() {
1019
+ return this.currentSessionState;
1020
+ }
1021
+ // Broker Data Management
1022
+ async getBrokerList(accessToken) {
1023
+ return this.request('/brokers/', {
1024
+ method: 'GET',
1025
+ headers: {
1026
+ 'Content-Type': 'application/json',
1027
+ Authorization: `Bearer ${accessToken}`,
1028
+ 'Session-ID': this.currentSessionId || '',
1029
+ 'X-Session-ID': this.currentSessionId || '',
1030
+ 'X-Device-Info': JSON.stringify({
1031
+ ip_address: this.deviceInfo?.ip_address || '',
1032
+ user_agent: this.deviceInfo?.user_agent || '',
1033
+ fingerprint: this.deviceInfo?.fingerprint || '',
1034
+ }),
1035
+ },
1036
+ });
1037
+ }
1038
+ async getBrokerAccounts(accessToken, options) {
1039
+ const params = {};
1040
+ if (options?.broker_name)
1041
+ params.broker_name = options.broker_name;
1042
+ if (options?.account_id)
1043
+ params.account_id = options.account_id;
1044
+ return this.request('/brokers/data/accounts', {
1045
+ method: 'GET',
1046
+ headers: {
1047
+ 'Content-Type': 'application/json',
1048
+ Authorization: `Bearer ${accessToken}`,
1049
+ 'Session-ID': this.currentSessionId || '',
1050
+ 'X-Session-ID': this.currentSessionId || '',
1051
+ 'X-Device-Info': JSON.stringify({
1052
+ ip_address: this.deviceInfo?.ip_address || '',
1053
+ user_agent: this.deviceInfo?.user_agent || '',
1054
+ fingerprint: this.deviceInfo?.fingerprint || '',
1055
+ }),
1056
+ },
1057
+ params,
1058
+ });
1059
+ }
1060
+ async getBrokerOrders(accessToken, options) {
1061
+ const params = {};
1062
+ if (options?.broker_name)
1063
+ params.broker_name = options.broker_name;
1064
+ if (options?.account_id)
1065
+ params.account_id = options.account_id;
1066
+ if (options?.symbol)
1067
+ params.symbol = options.symbol;
1068
+ return this.request('/brokers/data/orders', {
1069
+ method: 'GET',
1070
+ headers: {
1071
+ 'Content-Type': 'application/json',
1072
+ Authorization: `Bearer ${accessToken}`,
1073
+ 'Session-ID': this.currentSessionId || '',
1074
+ 'X-Session-ID': this.currentSessionId || '',
1075
+ 'X-Device-Info': JSON.stringify({
1076
+ ip_address: this.deviceInfo?.ip_address || '',
1077
+ user_agent: this.deviceInfo?.user_agent || '',
1078
+ fingerprint: this.deviceInfo?.fingerprint || '',
1079
+ }),
1080
+ },
1081
+ params,
1082
+ });
1083
+ }
1084
+ async getBrokerPositions(accessToken, options) {
1085
+ const queryParams = {};
1086
+ if (options?.broker_name)
1087
+ queryParams.broker_name = options.broker_name;
1088
+ if (options?.account_id)
1089
+ queryParams.account_id = options.account_id;
1090
+ if (options?.symbol)
1091
+ queryParams.symbol = options.symbol;
1092
+ return this.request('/brokers/data/positions', {
1093
+ method: 'GET',
1094
+ headers: {
1095
+ Authorization: `Bearer ${accessToken}`,
1096
+ 'Content-Type': 'application/json',
1097
+ 'Session-ID': this.currentSessionId || '',
1098
+ 'X-Session-ID': this.currentSessionId || '',
1099
+ 'X-Device-Info': JSON.stringify({
1100
+ ip_address: this.deviceInfo?.ip_address || '',
1101
+ user_agent: this.deviceInfo?.user_agent || '',
1102
+ fingerprint: this.deviceInfo?.fingerprint || '',
1103
+ }),
1104
+ },
1105
+ params: queryParams,
1106
+ });
1107
+ }
1108
+ async getBrokerConnections(accessToken) {
1109
+ return this.request('/brokers/connections', {
1110
+ method: 'GET',
1111
+ headers: {
1112
+ Authorization: `Bearer ${accessToken}`,
1113
+ 'Content-Type': 'application/json',
1114
+ },
1115
+ });
1116
+ }
1117
+ // Automatic token management versions of broker methods
1118
+ async getBrokerListAuto() {
1119
+ const accessToken = await this.getValidAccessToken();
1120
+ return this.getBrokerList(accessToken);
1121
+ }
1122
+ async getBrokerAccountsAuto(options) {
1123
+ const accessToken = await this.getValidAccessToken();
1124
+ return this.getBrokerAccounts(accessToken, options);
1125
+ }
1126
+ async getBrokerOrdersAuto(options) {
1127
+ const accessToken = await this.getValidAccessToken();
1128
+ return this.getBrokerOrders(accessToken, options);
1129
+ }
1130
+ async getBrokerPositionsAuto(options) {
1131
+ const accessToken = await this.getValidAccessToken();
1132
+ return this.getBrokerPositions(accessToken, options);
1133
+ }
1134
+ async getBrokerConnectionsAuto() {
1135
+ const accessToken = await this.getValidAccessToken();
1136
+ return this.getBrokerConnections(accessToken);
1137
+ }
1138
+ // Automatic token management versions of trading methods
1139
+ async placeBrokerOrderAuto(params, extras = {}) {
1140
+ const accessToken = await this.getValidAccessToken();
1141
+ return this.placeBrokerOrder(accessToken, params, extras);
1142
+ }
1143
+ async placeStockMarketOrderAuto(symbol, orderQty, action, broker, accountNumber, extras = {}) {
1144
+ const accessToken = await this.getValidAccessToken();
1145
+ return this.placeStockMarketOrder(accessToken, symbol, orderQty, action, broker, accountNumber, extras);
1146
+ }
1147
+ async placeStockLimitOrderAuto(symbol, orderQty, action, price, timeInForce = 'gtc', broker, accountNumber, extras = {}) {
1148
+ const accessToken = await this.getValidAccessToken();
1149
+ return this.placeStockLimitOrder(accessToken, symbol, orderQty, action, price, timeInForce, broker, accountNumber, extras);
1150
+ }
1151
+ async placeStockStopOrderAuto(symbol, orderQty, action, stopPrice, timeInForce = 'day', broker, accountNumber, extras = {}) {
1152
+ const accessToken = await this.getValidAccessToken();
1153
+ return this.placeStockStopOrder(accessToken, symbol, orderQty, action, stopPrice, timeInForce, broker, accountNumber, extras);
1154
+ }
1155
+ // Page-based pagination methods
1156
+ async getBrokerOrdersPage(page = 1, perPage = 100, filters) {
1157
+ const accessToken = await this.getValidAccessToken();
1158
+ const offset = (page - 1) * perPage;
1159
+ const params = {
1160
+ limit: perPage.toString(),
1161
+ offset: offset.toString(),
1162
+ };
1163
+ // Add filter parameters
1164
+ if (filters) {
1165
+ if (filters.broker_id)
1166
+ params.broker_id = filters.broker_id;
1167
+ if (filters.connection_id)
1168
+ params.connection_id = filters.connection_id;
1169
+ if (filters.account_id)
1170
+ params.account_id = filters.account_id;
1171
+ if (filters.symbol)
1172
+ params.symbol = filters.symbol;
1173
+ if (filters.status)
1174
+ params.status = filters.status;
1175
+ if (filters.side)
1176
+ params.side = filters.side;
1177
+ if (filters.asset_type)
1178
+ params.asset_type = filters.asset_type;
1179
+ if (filters.created_after)
1180
+ params.created_after = filters.created_after;
1181
+ if (filters.created_before)
1182
+ params.created_before = filters.created_before;
1183
+ }
1184
+ const response = await this.request('/brokers/data/orders', {
1185
+ method: 'GET',
1186
+ headers: {
1187
+ 'Content-Type': 'application/json',
1188
+ Authorization: `Bearer ${accessToken}`,
1189
+ 'X-Company-ID': this.companyId || '',
1190
+ 'X-Session-ID': this.currentSessionId || '',
1191
+ 'X-CSRF-Token': this.csrfToken || '',
1192
+ 'X-Device-Info': JSON.stringify({
1193
+ ip_address: this.deviceInfo?.ip_address || '',
1194
+ user_agent: this.deviceInfo?.user_agent || '',
1195
+ fingerprint: this.deviceInfo?.fingerprint || '',
1196
+ }),
1197
+ },
1198
+ params,
1199
+ });
1200
+ return new PaginatedResult(response.response_data, response.pagination || {
1201
+ has_more: false,
1202
+ next_offset: offset,
1203
+ current_offset: offset,
1204
+ limit: perPage,
1205
+ });
1206
+ }
1207
+ async getBrokerAccountsPage(page = 1, perPage = 100, filters) {
1208
+ const accessToken = await this.getValidAccessToken();
1209
+ const offset = (page - 1) * perPage;
1210
+ const params = {
1211
+ limit: perPage.toString(),
1212
+ offset: offset.toString(),
1213
+ };
1214
+ // Add filter parameters
1215
+ if (filters) {
1216
+ if (filters.broker_id)
1217
+ params.broker_id = filters.broker_id;
1218
+ if (filters.connection_id)
1219
+ params.connection_id = filters.connection_id;
1220
+ if (filters.account_type)
1221
+ params.account_type = filters.account_type;
1222
+ if (filters.status)
1223
+ params.status = filters.status;
1224
+ if (filters.currency)
1225
+ params.currency = filters.currency;
1226
+ }
1227
+ const response = await this.request('/brokers/data/accounts', {
1228
+ method: 'GET',
1229
+ headers: {
1230
+ 'Content-Type': 'application/json',
1231
+ Authorization: `Bearer ${accessToken}`,
1232
+ 'X-Company-ID': this.companyId || '',
1233
+ 'X-Session-ID': this.currentSessionId || '',
1234
+ 'X-CSRF-Token': this.csrfToken || '',
1235
+ 'X-Device-Info': JSON.stringify({
1236
+ ip_address: this.deviceInfo?.ip_address || '',
1237
+ user_agent: this.deviceInfo?.user_agent || '',
1238
+ fingerprint: this.deviceInfo?.fingerprint || '',
1239
+ }),
1240
+ },
1241
+ params,
1242
+ });
1243
+ return new PaginatedResult(response.response_data, response.pagination || {
1244
+ has_more: false,
1245
+ next_offset: offset,
1246
+ current_offset: offset,
1247
+ limit: perPage,
1248
+ });
1249
+ }
1250
+ async getBrokerPositionsPage(page = 1, perPage = 100, filters) {
1251
+ const accessToken = await this.getValidAccessToken();
1252
+ const offset = (page - 1) * perPage;
1253
+ const params = {
1254
+ limit: perPage.toString(),
1255
+ offset: offset.toString(),
1256
+ };
1257
+ // Add filter parameters
1258
+ if (filters) {
1259
+ if (filters.broker_id)
1260
+ params.broker_id = filters.broker_id;
1261
+ if (filters.account_id)
1262
+ params.account_id = filters.account_id;
1263
+ if (filters.symbol)
1264
+ params.symbol = filters.symbol;
1265
+ if (filters.position_status)
1266
+ params.position_status = filters.position_status;
1267
+ if (filters.side)
1268
+ params.side = filters.side;
1269
+ }
1270
+ const response = await this.request('/brokers/data/positions', {
1271
+ method: 'GET',
1272
+ headers: {
1273
+ 'Content-Type': 'application/json',
1274
+ Authorization: `Bearer ${accessToken}`,
1275
+ 'X-Company-ID': this.companyId || '',
1276
+ 'X-Session-ID': this.currentSessionId || '',
1277
+ 'X-CSRF-Token': this.csrfToken || '',
1278
+ 'X-Device-Info': JSON.stringify({
1279
+ ip_address: this.deviceInfo?.ip_address || '',
1280
+ user_agent: this.deviceInfo?.user_agent || '',
1281
+ fingerprint: this.deviceInfo?.fingerprint || '',
1282
+ }),
1283
+ },
1284
+ params,
1285
+ });
1286
+ return new PaginatedResult(response.response_data, response.pagination || {
1287
+ has_more: false,
1288
+ next_offset: offset,
1289
+ current_offset: offset,
1290
+ limit: perPage,
1291
+ });
1292
+ }
1293
+ // Navigation methods
1294
+ async getNextPage(previousResult, fetchFunction) {
1295
+ if (!previousResult.hasNext) {
1296
+ return null;
1297
+ }
1298
+ return fetchFunction(previousResult.metadata.nextOffset, previousResult.metadata.limit);
1299
+ }
1300
+ /**
1301
+ * Check if this is a mock client
1302
+ * @returns false for real API client
1303
+ */
1304
+ isMockClient() {
1305
+ return false;
1306
+ }
1307
+ }
1308
+
1309
+ class EventEmitter {
1310
+ constructor() {
1311
+ this.events = new Map();
1312
+ }
1313
+ on(event, callback) {
1314
+ if (!this.events.has(event)) {
1315
+ this.events.set(event, new Set());
1316
+ }
1317
+ this.events.get(event).add(callback);
1318
+ }
1319
+ off(event, callback) {
1320
+ if (this.events.has(event)) {
1321
+ this.events.get(event).delete(callback);
1322
+ }
1323
+ }
1324
+ once(event, callback) {
1325
+ const onceCallback = (...args) => {
1326
+ callback(...args);
1327
+ this.off(event, onceCallback);
1328
+ };
1329
+ this.on(event, onceCallback);
1330
+ }
1331
+ emit(event, ...args) {
1332
+ if (this.events.has(event)) {
1333
+ this.events.get(event).forEach(callback => {
1334
+ try {
1335
+ callback(...args);
1336
+ }
1337
+ catch (error) {
1338
+ console.error(`Error in event handler for ${event}:`, error);
1339
+ }
1340
+ });
1341
+ }
1342
+ }
1343
+ removeAllListeners(event) {
1344
+ if (event) {
1345
+ this.events.delete(event);
1346
+ }
1347
+ else {
1348
+ this.events.clear();
1349
+ }
1350
+ }
1351
+ listenerCount(event) {
1352
+ return this.events.get(event)?.size || 0;
1353
+ }
1354
+ listeners(event) {
1355
+ return Array.from(this.events.get(event) || []);
1356
+ }
1357
+ }
1358
+
1359
+ class PortalUI {
1360
+ constructor(portalUrl) {
1361
+ this.iframe = null;
1362
+ this.container = null;
1363
+ this.messageHandler = null;
1364
+ this.sessionId = null;
1365
+ this.portalOrigin = null;
1366
+ this.userToken = null;
1367
+ this.originalBodyStyle = null;
1368
+ this.createContainer();
1369
+ }
1370
+ createContainer() {
1371
+ console.debug('[PortalUI] Creating container and iframe');
1372
+ this.container = document.createElement('div');
1373
+ this.container.style.cssText = `
1374
+ position: fixed;
1375
+ top: 0;
1376
+ left: 0;
1377
+ width: 100%;
1378
+ height: 100%;
1379
+ background: rgba(0, 0, 0, 0.5);
1380
+ display: none;
1381
+ z-index: 9999;
1382
+ `;
1383
+ this.iframe = document.createElement('iframe');
1384
+ // Let the portal handle its own styling - only set essential attributes
1385
+ this.iframe.style.cssText = `
1386
+ position: absolute;
1387
+ top: 50%;
1388
+ left: 50%;
1389
+ transform: translate(-50%, -50%);
1390
+ width: 90%;
1391
+ max-width: 500px;
1392
+ height: 90%;
1393
+ max-height: 600px;
1394
+ border: none;
1395
+ border-radius: 24px;
1396
+ `;
1397
+ // Set security headers
1398
+ this.iframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-popups allow-same-origin');
1399
+ this.iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
1400
+ this.iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture');
1401
+ // Add CSP meta tag to allow Google Fonts and other required resources
1402
+ const meta = document.createElement('meta');
1403
+ meta.setAttribute('http-equiv', 'Content-Security-Policy');
1404
+ meta.setAttribute('content', `
1405
+ default-src 'self' https:;
1406
+ style-src 'self' 'unsafe-inline' https: https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net;
1407
+ font-src 'self' https://fonts.gstatic.com;
1408
+ img-src 'self' data: https:;
1409
+ script-src 'self' 'unsafe-inline' 'unsafe-eval' https:;
1410
+ connect-src 'self' https:;
1411
+ frame-src 'self' https:;
1412
+ `
1413
+ .replace(/\s+/g, ' ')
1414
+ .trim());
1415
+ this.iframe.contentDocument?.head.appendChild(meta);
1416
+ this.container.appendChild(this.iframe);
1417
+ document.body.appendChild(this.container);
1418
+ console.debug('[PortalUI] Container and iframe created successfully');
1419
+ }
1420
+ /**
1421
+ * Lock background scrolling by setting overflow: hidden on body
1422
+ */
1423
+ lockScroll() {
1424
+ if (typeof document !== 'undefined' && document.body) {
1425
+ // Store original body style to restore later
1426
+ this.originalBodyStyle = document.body.style.cssText;
1427
+ // Add overflow: hidden to prevent scrolling
1428
+ document.body.style.overflow = 'hidden';
1429
+ document.body.style.position = 'fixed';
1430
+ document.body.style.width = '100%';
1431
+ document.body.style.top = `-${window.scrollY}px`;
1432
+ console.debug('[PortalUI] Background scroll locked');
1433
+ }
1434
+ }
1435
+ /**
1436
+ * Unlock background scrolling by restoring original body style
1437
+ */
1438
+ unlockScroll() {
1439
+ if (typeof document !== 'undefined' && document.body && this.originalBodyStyle !== null) {
1440
+ // Restore original body style
1441
+ document.body.style.cssText = this.originalBodyStyle;
1442
+ // Restore scroll position
1443
+ const scrollY = document.body.style.top;
1444
+ document.body.style.position = '';
1445
+ document.body.style.top = '';
1446
+ window.scrollTo(0, parseInt(scrollY || '0') * -1);
1447
+ this.originalBodyStyle = null;
1448
+ console.debug('[PortalUI] Background scroll unlocked');
1449
+ }
1450
+ }
1451
+ show(url, sessionId, options = {}) {
1452
+ if (!this.iframe || !this.container) {
1453
+ this.createContainer();
1454
+ }
1455
+ // Set portalOrigin to the actual portal URL's origin
1456
+ try {
1457
+ this.portalOrigin = new URL(url).origin;
1458
+ }
1459
+ catch (e) {
1460
+ this.portalOrigin = null;
1461
+ }
1462
+ this.sessionId = sessionId;
1463
+ this.options = options;
1464
+ this.container.style.display = 'block';
1465
+ this.iframe.src = url;
1466
+ // Lock background scrolling
1467
+ this.lockScroll();
1468
+ // Set up message handler
1469
+ this.messageHandler = this.handleMessage.bind(this);
1470
+ window.addEventListener('message', this.messageHandler);
1471
+ }
1472
+ hide() {
1473
+ if (this.container) {
1474
+ this.container.style.display = 'none';
1475
+ }
1476
+ if (this.iframe) {
1477
+ this.iframe.src = '';
1478
+ }
1479
+ if (this.messageHandler) {
1480
+ window.removeEventListener('message', this.messageHandler);
1481
+ this.messageHandler = null;
1482
+ }
1483
+ this.sessionId = null;
1484
+ // Unlock background scrolling
1485
+ this.unlockScroll();
1486
+ }
1487
+ handleMessage(event) {
1488
+ // Verify origin matches the portal URL
1489
+ if (!this.portalOrigin || event.origin !== this.portalOrigin) {
1490
+ console.warn('[PortalUI] Received message from unauthorized origin:', event.origin, 'Expected:', this.portalOrigin);
1491
+ return;
1492
+ }
1493
+ const { type, userId, access_token, refresh_token, error, height, data } = event.data;
1494
+ console.log('[PortalUI] Received message:', event.data);
1495
+ switch (type) {
1496
+ case 'portal-success': {
1497
+ // Handle both direct userId and data.userId formats
1498
+ const successUserId = userId || (data && data.userId);
1499
+ const successAccessToken = access_token || (data && data.access_token);
1500
+ const successRefreshToken = refresh_token || (data && data.refresh_token);
1501
+ this.handlePortalSuccess(successUserId, successAccessToken, successRefreshToken);
1502
+ break;
1503
+ }
1504
+ case 'portal-error': {
1505
+ // Handle both direct error and data.message formats
1506
+ const errorMessage = error || (data && data.message);
1507
+ this.handlePortalError(errorMessage);
1508
+ break;
1509
+ }
1510
+ case 'portal-close':
1511
+ this.handlePortalClose();
1512
+ break;
1513
+ case 'event':
1514
+ this.handleGenericEvent(data);
1515
+ break;
1516
+ case 'resize':
1517
+ this.handleResize(height);
1518
+ break;
1519
+ // Legacy support for old message types
1520
+ case 'success':
1521
+ this.handleSuccess(userId, access_token, refresh_token);
1522
+ break;
1523
+ case 'error':
1524
+ this.handleError(error);
1525
+ break;
1526
+ case 'close':
1527
+ this.handleClose();
1528
+ break;
1529
+ default:
1530
+ console.warn('[PortalUI] Received unhandled message type:', type);
1531
+ }
1532
+ }
1533
+ handlePortalSuccess(userId, accessToken, refreshToken) {
1534
+ if (!userId) {
1535
+ console.error('[PortalUI] Missing userId in portal-success message');
1536
+ return;
1537
+ }
1538
+ console.log('[PortalUI] Portal success - User connected:', userId);
1539
+ // If tokens are provided, store them internally
1540
+ if (accessToken && refreshToken) {
1541
+ const userToken = {
1542
+ accessToken: accessToken,
1543
+ refreshToken: refreshToken,
1544
+ expiresIn: 3600, // Default to 1 hour
1545
+ user_id: userId,
1546
+ tokenType: 'Bearer',
1547
+ scope: 'api:access',
1548
+ };
1549
+ this.userToken = userToken;
1550
+ console.log('[PortalUI] Portal authentication successful');
1551
+ }
1552
+ else {
1553
+ console.warn('[PortalUI] No tokens received from portal');
1554
+ }
1555
+ // Pass userId to parent (SDK will handle tokens internally)
1556
+ this.options?.onSuccess?.(userId);
1557
+ }
1558
+ handlePortalError(error) {
1559
+ console.error('[PortalUI] Portal error:', error);
1560
+ this.options?.onError?.(new Error(error || 'Unknown portal error'));
1561
+ }
1562
+ handlePortalClose() {
1563
+ console.log('[PortalUI] Portal closed by user');
1564
+ this.options?.onClose?.();
1565
+ this.hide();
1566
+ }
1567
+ handleGenericEvent(data) {
1568
+ if (!data || !data.type) {
1569
+ console.warn('[PortalUI] Invalid event data:', data);
1570
+ return;
1571
+ }
1572
+ console.log('[PortalUI] Generic event received:', data.type, data.data);
1573
+ // Emit the event to be handled by the SDK
1574
+ // This will be implemented in FinaticConnect
1575
+ if (this.options?.onEvent) {
1576
+ this.options.onEvent(data.type, data.data);
1577
+ }
1578
+ }
1579
+ handleSuccess(userId, access_token, refresh_token) {
1580
+ if (!userId || !access_token || !refresh_token) {
1581
+ console.error('[PortalUI] Missing required fields in success message');
1582
+ return;
1583
+ }
1584
+ // Convert portal tokens to UserToken format
1585
+ const userToken = {
1586
+ accessToken: access_token,
1587
+ refreshToken: refresh_token,
1588
+ expiresIn: 3600, // Default to 1 hour
1589
+ user_id: userId,
1590
+ tokenType: 'Bearer',
1591
+ scope: 'api:access',
1592
+ };
1593
+ // Store tokens internally
1594
+ this.userToken = userToken;
1595
+ // Pass userId to parent
1596
+ this.options?.onSuccess?.(userId);
1597
+ }
1598
+ handleError(error) {
1599
+ console.error('[PortalUI] Received error:', error);
1600
+ this.options?.onError?.(new Error(error || 'Unknown error'));
1601
+ }
1602
+ handleClose() {
1603
+ console.log('[PortalUI] Received close message');
1604
+ this.options?.onClose?.();
1605
+ this.hide();
1606
+ }
1607
+ handleResize(height) {
1608
+ if (height && this.iframe) {
1609
+ console.log('[PortalUI] Received resize message:', height);
1610
+ this.iframe.style.height = `${height}px`;
1611
+ }
1612
+ }
1613
+ getTokens() {
1614
+ return this.userToken;
1615
+ }
1616
+ }
1617
+
1618
+ /**
1619
+ * Mock data provider for Finatic API endpoints
1620
+ */
1621
+ class MockDataProvider {
1622
+ constructor(config = {}) {
1623
+ this.sessionData = new Map();
1624
+ this.userTokens = new Map();
1625
+ this.config = {
1626
+ delay: config.delay || this.getRandomDelay(50, 200),
1627
+ scenario: config.scenario || 'success',
1628
+ customData: config.customData || {},
1629
+ };
1630
+ }
1631
+ /**
1632
+ * Get a random delay between min and max milliseconds
1633
+ */
1634
+ getRandomDelay(min, max) {
1635
+ return Math.floor(Math.random() * (max - min + 1)) + min;
1636
+ }
1637
+ /**
1638
+ * Simulate network delay
1639
+ */
1640
+ async simulateDelay() {
1641
+ const delay = this.config.delay || this.getRandomDelay(50, 200);
1642
+ await new Promise(resolve => setTimeout(resolve, delay));
1643
+ }
1644
+ /**
1645
+ * Generate a realistic session ID
1646
+ */
1647
+ generateSessionId() {
1648
+ return `session_${uuid.v4().replace(/-/g, '')}`;
1649
+ }
1650
+ /**
1651
+ * Generate a realistic user ID
1652
+ */
1653
+ generateUserId() {
1654
+ return `user_${uuid.v4().replace(/-/g, '').substring(0, 8)}`;
1655
+ }
1656
+ /**
1657
+ * Generate a realistic company ID
1658
+ */
1659
+ generateCompanyId() {
1660
+ return `company_${uuid.v4().replace(/-/g, '').substring(0, 8)}`;
1661
+ }
1662
+ /**
1663
+ * Generate mock tokens
1664
+ */
1665
+ generateTokens(userId) {
1666
+ const accessToken = `mock_access_${uuid.v4().replace(/-/g, '')}`;
1667
+ const refreshToken = `mock_refresh_${uuid.v4().replace(/-/g, '')}`;
1668
+ return {
1669
+ accessToken,
1670
+ refreshToken,
1671
+ expiresIn: 3600, // 1 hour
1672
+ user_id: userId,
1673
+ tokenType: 'Bearer',
1674
+ scope: 'read write',
1675
+ };
1676
+ }
1677
+ // Authentication & Session Management Mocks
1678
+ async mockStartSession(token, userId) {
1679
+ await this.simulateDelay();
1680
+ const sessionId = this.generateSessionId();
1681
+ const companyId = this.generateCompanyId();
1682
+ const actualUserId = userId || this.generateUserId();
1683
+ const sessionData = {
1684
+ session_id: sessionId,
1685
+ state: SessionState.ACTIVE,
1686
+ company_id: companyId,
1687
+ status: 'active',
1688
+ expires_at: new Date(Date.now() + 30 * 60 * 1000).toISOString(), // 30 minutes
1689
+ user_id: actualUserId,
1690
+ auto_login: false,
1691
+ };
1692
+ this.sessionData.set(sessionId, sessionData);
1693
+ return {
1694
+ data: sessionData,
1695
+ message: 'Session started successfully',
1696
+ };
1697
+ }
1698
+ async mockRequestOtp(sessionId, email) {
1699
+ await this.simulateDelay();
1700
+ return {
1701
+ success: true,
1702
+ message: 'OTP sent successfully',
1703
+ data: true,
1704
+ };
1705
+ }
1706
+ async mockVerifyOtp(sessionId, otp) {
1707
+ await this.simulateDelay();
1708
+ const userId = this.generateUserId();
1709
+ const tokens = this.generateTokens(userId);
1710
+ this.userTokens.set(userId, tokens);
1711
+ return {
1712
+ success: true,
1713
+ message: 'OTP verified successfully',
1714
+ data: {
1715
+ access_token: tokens.accessToken,
1716
+ refresh_token: tokens.refreshToken,
1717
+ user_id: userId,
1718
+ expires_in: tokens.expiresIn,
1719
+ scope: tokens.scope,
1720
+ token_type: tokens.tokenType,
1721
+ },
1722
+ };
1723
+ }
1724
+ async mockAuthenticateDirectly(sessionId, userId) {
1725
+ await this.simulateDelay();
1726
+ const tokens = this.generateTokens(userId);
1727
+ this.userTokens.set(userId, tokens);
1728
+ return {
1729
+ success: true,
1730
+ message: 'Authentication successful',
1731
+ data: {
1732
+ access_token: tokens.accessToken,
1733
+ refresh_token: tokens.refreshToken,
1734
+ },
1735
+ };
1736
+ }
1737
+ async mockGetPortalUrl(sessionId) {
1738
+ await this.simulateDelay();
1739
+ const scenario = this.getScenario();
1740
+ if (scenario === 'error') {
1741
+ return {
1742
+ success: false,
1743
+ message: 'Failed to retrieve portal URL (mock error)',
1744
+ data: { portal_url: '' },
1745
+ };
1746
+ }
1747
+ if (scenario === 'network_error') {
1748
+ throw new Error('Network error (mock)');
1749
+ }
1750
+ if (scenario === 'rate_limit') {
1751
+ return {
1752
+ success: false,
1753
+ message: 'Rate limit exceeded (mock)',
1754
+ data: { portal_url: '' },
1755
+ };
1756
+ }
1757
+ if (scenario === 'auth_failure') {
1758
+ return {
1759
+ success: false,
1760
+ message: 'Authentication failed (mock)',
1761
+ data: { portal_url: '' },
1762
+ };
1763
+ }
1764
+ // Default: success
1765
+ return {
1766
+ success: true,
1767
+ message: 'Portal URL retrieved successfully',
1768
+ data: {
1769
+ portal_url: 'http://localhost:3000/mock-portal',
1770
+ },
1771
+ };
1772
+ }
1773
+ async mockValidatePortalSession(sessionId, signature) {
1774
+ await this.simulateDelay();
1775
+ return {
1776
+ valid: true,
1777
+ company_id: this.generateCompanyId(),
1778
+ status: 'active',
1779
+ };
1780
+ }
1781
+ async mockCompletePortalSession(sessionId) {
1782
+ await this.simulateDelay();
1783
+ return {
1784
+ success: true,
1785
+ message: 'Portal session completed successfully',
1786
+ data: {
1787
+ portal_url: `https://portal.finatic.dev/complete/${sessionId}`,
1788
+ },
1789
+ };
1790
+ }
1791
+ async mockRefreshToken(refreshToken) {
1792
+ await this.simulateDelay();
1793
+ const newAccessToken = `mock_access_${uuid.v4().replace(/-/g, '')}`;
1794
+ const newRefreshToken = `mock_refresh_${uuid.v4().replace(/-/g, '')}`;
1795
+ return {
1796
+ success: true,
1797
+ response_data: {
1798
+ access_token: newAccessToken,
1799
+ refresh_token: newRefreshToken,
1800
+ expires_at: new Date(Date.now() + 3600 * 1000).toISOString(),
1801
+ company_id: this.generateCompanyId(),
1802
+ company_name: 'Mock Company',
1803
+ email_verified: true,
1804
+ },
1805
+ message: 'Token refreshed successfully',
1806
+ };
1807
+ }
1808
+ // Broker Management Mocks
1809
+ async mockGetBrokerList() {
1810
+ await this.simulateDelay();
1811
+ const brokers = [
1812
+ {
1813
+ id: 'robinhood',
1814
+ name: 'robinhood',
1815
+ display_name: 'Robinhood',
1816
+ description: 'Commission-free stock and options trading',
1817
+ website: 'https://robinhood.com',
1818
+ features: ['stocks', 'options', 'crypto', 'fractional_shares'],
1819
+ auth_type: 'oauth',
1820
+ logo_path: '/logos/robinhood.png',
1821
+ is_active: true,
1822
+ },
1823
+ {
1824
+ id: 'tasty_trade',
1825
+ name: 'tasty_trade',
1826
+ display_name: 'TastyTrade',
1827
+ description: 'Options and futures trading platform',
1828
+ website: 'https://tastytrade.com',
1829
+ features: ['options', 'futures', 'stocks'],
1830
+ auth_type: 'oauth',
1831
+ logo_path: '/logos/tastytrade.png',
1832
+ is_active: true,
1833
+ },
1834
+ {
1835
+ id: 'ninja_trader',
1836
+ name: 'ninja_trader',
1837
+ display_name: 'NinjaTrader',
1838
+ description: 'Advanced futures and forex trading platform',
1839
+ website: 'https://ninjatrader.com',
1840
+ features: ['futures', 'forex', 'options'],
1841
+ auth_type: 'api_key',
1842
+ logo_path: '/logos/ninjatrader.png',
1843
+ is_active: true,
1844
+ },
1845
+ ];
1846
+ return {
1847
+ _id: uuid.v4(),
1848
+ response_data: brokers,
1849
+ message: 'Broker list retrieved successfully',
1850
+ status_code: 200,
1851
+ warnings: null,
1852
+ errors: null,
1853
+ };
1854
+ }
1855
+ async mockGetBrokerAccounts() {
1856
+ await this.simulateDelay();
1857
+ const accounts = [
1858
+ {
1859
+ id: uuid.v4(),
1860
+ user_broker_connection_id: uuid.v4(),
1861
+ broker_provided_account_id: '123456789',
1862
+ account_name: 'Individual Account',
1863
+ account_type: 'individual',
1864
+ currency: 'USD',
1865
+ cash_balance: 15000.5,
1866
+ buying_power: 45000.0,
1867
+ status: 'active',
1868
+ created_at: new Date().toISOString(),
1869
+ updated_at: new Date().toISOString(),
1870
+ last_synced_at: new Date().toISOString(),
1871
+ },
1872
+ {
1873
+ id: uuid.v4(),
1874
+ user_broker_connection_id: uuid.v4(),
1875
+ broker_provided_account_id: '987654321',
1876
+ account_name: 'IRA Account',
1877
+ account_type: 'ira',
1878
+ currency: 'USD',
1879
+ cash_balance: 25000.75,
1880
+ buying_power: 75000.0,
1881
+ status: 'active',
1882
+ created_at: new Date().toISOString(),
1883
+ updated_at: new Date().toISOString(),
1884
+ last_synced_at: new Date().toISOString(),
1885
+ },
1886
+ ];
1887
+ return {
1888
+ _id: uuid.v4(),
1889
+ response_data: accounts,
1890
+ message: 'Broker accounts retrieved successfully',
1891
+ status_code: 200,
1892
+ warnings: null,
1893
+ errors: null,
1894
+ };
1895
+ }
1896
+ async mockGetBrokerConnections() {
1897
+ await this.simulateDelay();
1898
+ const connections = [
1899
+ {
1900
+ id: uuid.v4(),
1901
+ broker_id: 'robinhood',
1902
+ user_id: this.generateUserId(),
1903
+ company_id: this.generateCompanyId(),
1904
+ status: 'connected',
1905
+ connected_at: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days ago
1906
+ last_synced_at: new Date().toISOString(),
1907
+ permissions: {
1908
+ read: true,
1909
+ write: true,
1910
+ },
1911
+ metadata: {
1912
+ nickname: 'My Robinhood',
1913
+ },
1914
+ needs_reauth: false,
1915
+ },
1916
+ {
1917
+ id: uuid.v4(),
1918
+ broker_id: 'tasty_trade',
1919
+ user_id: this.generateUserId(),
1920
+ company_id: this.generateCompanyId(),
1921
+ status: 'connected',
1922
+ connected_at: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
1923
+ last_synced_at: new Date().toISOString(),
1924
+ permissions: {
1925
+ read: true,
1926
+ write: false,
1927
+ },
1928
+ metadata: {
1929
+ nickname: 'Tasty Options',
1930
+ },
1931
+ needs_reauth: false,
1932
+ },
1933
+ ];
1934
+ return {
1935
+ _id: uuid.v4(),
1936
+ response_data: connections,
1937
+ message: 'Broker connections retrieved successfully',
1938
+ status_code: 200,
1939
+ warnings: null,
1940
+ errors: null,
1941
+ };
1942
+ }
1943
+ // Portfolio & Trading Mocks
1944
+ async mockGetHoldings() {
1945
+ await this.simulateDelay();
1946
+ const holdings = [
1947
+ {
1948
+ symbol: 'AAPL',
1949
+ quantity: 100,
1950
+ averagePrice: 150.25,
1951
+ currentPrice: 175.5,
1952
+ marketValue: 17550.0,
1953
+ unrealizedPnL: 2525.0,
1954
+ realizedPnL: 0,
1955
+ costBasis: 15025.0,
1956
+ currency: 'USD',
1957
+ },
1958
+ {
1959
+ symbol: 'TSLA',
1960
+ quantity: 50,
1961
+ averagePrice: 200.0,
1962
+ currentPrice: 220.75,
1963
+ marketValue: 11037.5,
1964
+ unrealizedPnL: 1037.5,
1965
+ realizedPnL: 0,
1966
+ costBasis: 10000.0,
1967
+ currency: 'USD',
1968
+ },
1969
+ {
1970
+ symbol: 'MSFT',
1971
+ quantity: 75,
1972
+ averagePrice: 300.0,
1973
+ currentPrice: 325.25,
1974
+ marketValue: 24393.75,
1975
+ unrealizedPnL: 1893.75,
1976
+ realizedPnL: 0,
1977
+ costBasis: 22500.0,
1978
+ currency: 'USD',
1979
+ },
1980
+ ];
1981
+ return { data: holdings };
1982
+ }
1983
+ async mockGetPortfolio() {
1984
+ await this.simulateDelay();
1985
+ const portfolio = {
1986
+ id: uuid.v4(),
1987
+ name: 'Main Portfolio',
1988
+ type: 'individual',
1989
+ status: 'active',
1990
+ cash: 15000.5,
1991
+ buyingPower: 45000.0,
1992
+ equity: 52981.25,
1993
+ longMarketValue: 37981.25,
1994
+ shortMarketValue: 0,
1995
+ initialMargin: 0,
1996
+ maintenanceMargin: 0,
1997
+ lastEquity: 52000.0,
1998
+ positions: [
1999
+ {
2000
+ symbol: 'AAPL',
2001
+ quantity: 100,
2002
+ averagePrice: 150.25,
2003
+ currentPrice: 175.5,
2004
+ marketValue: 17550.0,
2005
+ unrealizedPnL: 2525.0,
2006
+ realizedPnL: 0,
2007
+ costBasis: 15025.0,
2008
+ currency: 'USD',
2009
+ },
2010
+ {
2011
+ symbol: 'TSLA',
2012
+ quantity: 50,
2013
+ averagePrice: 200.0,
2014
+ currentPrice: 220.75,
2015
+ marketValue: 11037.5,
2016
+ unrealizedPnL: 1037.5,
2017
+ realizedPnL: 0,
2018
+ costBasis: 10000.0,
2019
+ currency: 'USD',
2020
+ },
2021
+ {
2022
+ symbol: 'MSFT',
2023
+ quantity: 75,
2024
+ averagePrice: 300.0,
2025
+ currentPrice: 325.25,
2026
+ marketValue: 24393.75,
2027
+ unrealizedPnL: 1893.75,
2028
+ realizedPnL: 0,
2029
+ costBasis: 22500.0,
2030
+ currency: 'USD',
2031
+ },
2032
+ ],
2033
+ performance: {
2034
+ totalReturn: 0.089,
2035
+ dailyReturn: 0.002,
2036
+ weeklyReturn: 0.015,
2037
+ monthlyReturn: 0.045,
2038
+ yearlyReturn: 0.089,
2039
+ maxDrawdown: -0.05,
2040
+ sharpeRatio: 1.2,
2041
+ beta: 0.95,
2042
+ alpha: 0.02,
2043
+ },
2044
+ };
2045
+ return { data: portfolio };
2046
+ }
2047
+ async mockGetOrders(filter) {
2048
+ await this.simulateDelay();
2049
+ const mockOrders = [
2050
+ {
2051
+ symbol: 'AAPL',
2052
+ side: 'buy',
2053
+ quantity: 100,
2054
+ type_: 'market',
2055
+ timeInForce: 'day',
2056
+ },
2057
+ {
2058
+ symbol: 'TSLA',
2059
+ side: 'sell',
2060
+ quantity: 50,
2061
+ type_: 'limit',
2062
+ price: 250.0,
2063
+ timeInForce: 'day',
2064
+ },
2065
+ ];
2066
+ // Apply filters if provided
2067
+ let filteredOrders = mockOrders;
2068
+ if (filter) {
2069
+ filteredOrders = this.applyOrderFilters(mockOrders, filter);
2070
+ }
2071
+ return { data: filteredOrders };
2072
+ }
2073
+ async mockGetBrokerOrders(filter) {
2074
+ await this.simulateDelay();
2075
+ // Determine how many orders to generate based on limit parameter
2076
+ const limit = filter?.limit || 100;
2077
+ const maxLimit = Math.min(limit, 1000); // Cap at 1000
2078
+ const count = Math.max(1, maxLimit); // At least 1
2079
+ // Generate diverse mock orders based on requested count
2080
+ const mockOrders = this.generateMockOrders(count);
2081
+ // Apply filters if provided
2082
+ let filteredOrders = mockOrders;
2083
+ if (filter) {
2084
+ filteredOrders = this.applyBrokerOrderFilters(mockOrders, filter);
2085
+ }
2086
+ return {
2087
+ data: filteredOrders,
2088
+ };
2089
+ }
2090
+ async mockGetBrokerPositions(filter) {
2091
+ await this.simulateDelay();
2092
+ // Determine how many positions to generate based on limit parameter
2093
+ const limit = filter?.limit || 100;
2094
+ const maxLimit = Math.min(limit, 1000); // Cap at 1000
2095
+ const count = Math.max(1, maxLimit); // At least 1
2096
+ // Generate diverse mock positions based on requested count
2097
+ const mockPositions = this.generateMockPositions(count);
2098
+ // Apply filters if provided
2099
+ let filteredPositions = mockPositions;
2100
+ if (filter) {
2101
+ filteredPositions = this.applyBrokerPositionFilters(mockPositions, filter);
2102
+ }
2103
+ return {
2104
+ data: filteredPositions,
2105
+ };
2106
+ }
2107
+ async mockGetBrokerDataAccounts(filter) {
2108
+ await this.simulateDelay();
2109
+ // Determine how many accounts to generate based on limit parameter
2110
+ const limit = filter?.limit || 100;
2111
+ const maxLimit = Math.min(limit, 1000); // Cap at 1000
2112
+ const count = Math.max(1, maxLimit); // At least 1
2113
+ // Generate diverse mock accounts based on requested count
2114
+ const mockAccounts = this.generateMockAccounts(count);
2115
+ // Apply filters if provided
2116
+ let filteredAccounts = mockAccounts;
2117
+ if (filter) {
2118
+ filteredAccounts = this.applyBrokerAccountFilters(mockAccounts, filter);
2119
+ }
2120
+ return {
2121
+ data: filteredAccounts,
2122
+ };
2123
+ }
2124
+ async mockPlaceOrder(order) {
2125
+ await this.simulateDelay();
2126
+ return {
2127
+ success: true,
2128
+ response_data: {
2129
+ orderId: uuid.v4(),
2130
+ status: 'submitted',
2131
+ broker: 'robinhood',
2132
+ accountNumber: '123456789',
2133
+ },
2134
+ message: 'Order placed successfully',
2135
+ status_code: 200,
2136
+ };
2137
+ }
2138
+ // Utility methods
2139
+ /**
2140
+ * Get stored session data
2141
+ */
2142
+ getSessionData(sessionId) {
2143
+ return this.sessionData.get(sessionId);
2144
+ }
2145
+ /**
2146
+ * Get stored user token
2147
+ */
2148
+ getUserToken(userId) {
2149
+ return this.userTokens.get(userId);
2150
+ }
2151
+ /**
2152
+ * Clear all stored data
2153
+ */
2154
+ clearData() {
2155
+ this.sessionData.clear();
2156
+ this.userTokens.clear();
2157
+ }
2158
+ /**
2159
+ * Update configuration
2160
+ */
2161
+ updateConfig(config) {
2162
+ this.config = { ...this.config, ...config };
2163
+ }
2164
+ setScenario(scenario) {
2165
+ this.config.scenario = scenario;
2166
+ }
2167
+ getScenario() {
2168
+ return this.config.scenario || 'success';
2169
+ }
2170
+ // Helper methods to apply filters
2171
+ applyOrderFilters(orders, filter) {
2172
+ return orders.filter(order => {
2173
+ if (filter.symbol && order.symbol !== filter.symbol)
2174
+ return false;
2175
+ if (filter.side && order.side !== filter.side)
2176
+ return false;
2177
+ return true;
2178
+ });
2179
+ }
2180
+ applyBrokerOrderFilters(orders, filter) {
2181
+ return orders.filter(order => {
2182
+ if (filter.broker_id && order.broker_id !== filter.broker_id)
2183
+ return false;
2184
+ if (filter.connection_id && order.connection_id !== filter.connection_id)
2185
+ return false;
2186
+ if (filter.account_id && order.account_id !== filter.account_id)
2187
+ return false;
2188
+ if (filter.symbol && order.symbol !== filter.symbol)
2189
+ return false;
2190
+ if (filter.status && order.status !== filter.status)
2191
+ return false;
2192
+ if (filter.side && order.side !== filter.side)
2193
+ return false;
2194
+ if (filter.asset_type && order.asset_type !== filter.asset_type)
2195
+ return false;
2196
+ if (filter.created_after && new Date(order.created_at) < new Date(filter.created_after))
2197
+ return false;
2198
+ if (filter.created_before && new Date(order.created_at) > new Date(filter.created_before))
2199
+ return false;
2200
+ return true;
2201
+ });
2202
+ }
2203
+ applyBrokerPositionFilters(positions, filter) {
2204
+ return positions.filter(position => {
2205
+ if (filter.broker_id && position.broker_id !== filter.broker_id)
2206
+ return false;
2207
+ if (filter.connection_id && position.connection_id !== filter.connection_id)
2208
+ return false;
2209
+ if (filter.account_id && position.account_id !== filter.account_id)
2210
+ return false;
2211
+ if (filter.symbol && position.symbol !== filter.symbol)
2212
+ return false;
2213
+ if (filter.side && position.side !== filter.side)
2214
+ return false;
2215
+ if (filter.asset_type && position.asset_type !== filter.asset_type)
2216
+ return false;
2217
+ if (filter.position_status && position.position_status !== filter.position_status)
2218
+ return false;
2219
+ if (filter.updated_after && new Date(position.updated_at) < new Date(filter.updated_after))
2220
+ return false;
2221
+ if (filter.updated_before && new Date(position.updated_at) > new Date(filter.updated_before))
2222
+ return false;
2223
+ return true;
2224
+ });
2225
+ }
2226
+ applyBrokerAccountFilters(accounts, filter) {
2227
+ return accounts.filter(account => {
2228
+ if (filter.broker_id && account.broker_id !== filter.broker_id)
2229
+ return false;
2230
+ if (filter.connection_id && account.connection_id !== filter.connection_id)
2231
+ return false;
2232
+ if (filter.account_type && account.account_type !== filter.account_type)
2233
+ return false;
2234
+ if (filter.status && account.status !== filter.status)
2235
+ return false;
2236
+ if (filter.currency && account.currency !== filter.currency)
2237
+ return false;
2238
+ return true;
2239
+ });
2240
+ }
2241
+ /**
2242
+ * Generate mock orders with diverse data
2243
+ */
2244
+ generateMockOrders(count) {
2245
+ const orders = [];
2246
+ const symbols = [
2247
+ 'AAPL',
2248
+ 'TSLA',
2249
+ 'MSFT',
2250
+ 'GOOGL',
2251
+ 'AMZN',
2252
+ 'META',
2253
+ 'NVDA',
2254
+ 'NFLX',
2255
+ 'SPY',
2256
+ 'QQQ',
2257
+ 'IWM',
2258
+ 'VTI',
2259
+ 'BTC',
2260
+ 'ETH',
2261
+ 'ADA',
2262
+ ];
2263
+ const brokers = ['robinhood', 'alpaca', 'tasty_trade', 'ninja_trader'];
2264
+ const orderTypes = ['market', 'limit', 'stop', 'stop_limit'];
2265
+ const sides = ['buy', 'sell'];
2266
+ const statuses = ['filled', 'pending', 'cancelled', 'rejected', 'partially_filled'];
2267
+ const assetTypes = ['stock', 'option', 'crypto', 'future'];
2268
+ for (let i = 0; i < count; i++) {
2269
+ const symbol = symbols[Math.floor(Math.random() * symbols.length)];
2270
+ const broker = brokers[Math.floor(Math.random() * brokers.length)];
2271
+ const orderType = orderTypes[Math.floor(Math.random() * orderTypes.length)];
2272
+ const side = sides[Math.floor(Math.random() * sides.length)];
2273
+ const status = statuses[Math.floor(Math.random() * statuses.length)];
2274
+ const assetType = assetTypes[Math.floor(Math.random() * assetTypes.length)];
2275
+ const quantity = Math.floor(Math.random() * 1000) + 1;
2276
+ const price = Math.random() * 500 + 10;
2277
+ const isFilled = status === 'filled' || status === 'partially_filled';
2278
+ const filledQuantity = isFilled ? Math.floor(quantity * (0.5 + Math.random() * 0.5)) : 0;
2279
+ const filledAvgPrice = isFilled ? price * (0.95 + Math.random() * 0.1) : 0;
2280
+ orders.push({
2281
+ id: uuid.v4(),
2282
+ broker_id: broker,
2283
+ connection_id: uuid.v4(),
2284
+ account_id: `account_${Math.floor(Math.random() * 1000000)}`,
2285
+ order_id: `order_${String(i + 1).padStart(6, '0')}`,
2286
+ symbol,
2287
+ order_type: orderType,
2288
+ side,
2289
+ quantity,
2290
+ price,
2291
+ status,
2292
+ asset_type: assetType,
2293
+ created_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), // Random date within last 30 days
2294
+ updated_at: new Date().toISOString(),
2295
+ filled_at: isFilled ? new Date().toISOString() : undefined,
2296
+ filled_quantity: filledQuantity,
2297
+ filled_avg_price: filledAvgPrice,
2298
+ });
2299
+ }
2300
+ return orders;
2301
+ }
2302
+ /**
2303
+ * Generate mock positions with diverse data
2304
+ */
2305
+ generateMockPositions(count) {
2306
+ const positions = [];
2307
+ const symbols = [
2308
+ 'AAPL',
2309
+ 'TSLA',
2310
+ 'MSFT',
2311
+ 'GOOGL',
2312
+ 'AMZN',
2313
+ 'META',
2314
+ 'NVDA',
2315
+ 'NFLX',
2316
+ 'SPY',
2317
+ 'QQQ',
2318
+ 'IWM',
2319
+ 'VTI',
2320
+ 'BTC',
2321
+ 'ETH',
2322
+ 'ADA',
2323
+ ];
2324
+ const brokers = ['robinhood', 'alpaca', 'tasty_trade', 'ninja_trader'];
2325
+ const sides = ['long', 'short'];
2326
+ const assetTypes = ['stock', 'option', 'crypto', 'future'];
2327
+ const positionStatuses = ['open', 'closed'];
2328
+ for (let i = 0; i < count; i++) {
2329
+ const symbol = symbols[Math.floor(Math.random() * symbols.length)];
2330
+ const broker = brokers[Math.floor(Math.random() * brokers.length)];
2331
+ const side = sides[Math.floor(Math.random() * sides.length)];
2332
+ const assetType = assetTypes[Math.floor(Math.random() * assetTypes.length)];
2333
+ const positionStatus = positionStatuses[Math.floor(Math.random() * positionStatuses.length)];
2334
+ const quantity = Math.floor(Math.random() * 1000) + 1;
2335
+ const averagePrice = Math.random() * 500 + 10;
2336
+ const currentPrice = averagePrice * (0.8 + Math.random() * 0.4); // ±20% variation
2337
+ const marketValue = quantity * currentPrice;
2338
+ const costBasis = quantity * averagePrice;
2339
+ const unrealizedGainLoss = marketValue - costBasis;
2340
+ const unrealizedGainLossPercent = (unrealizedGainLoss / costBasis) * 100;
2341
+ positions.push({
2342
+ id: uuid.v4(),
2343
+ broker_id: broker,
2344
+ connection_id: uuid.v4(),
2345
+ account_id: `account_${Math.floor(Math.random() * 1000000)}`,
2346
+ symbol,
2347
+ asset_type: assetType,
2348
+ side,
2349
+ quantity,
2350
+ average_price: averagePrice,
2351
+ market_value: marketValue,
2352
+ cost_basis: costBasis,
2353
+ unrealized_gain_loss: unrealizedGainLoss,
2354
+ unrealized_gain_loss_percent: unrealizedGainLossPercent,
2355
+ current_price: currentPrice,
2356
+ last_price: currentPrice,
2357
+ last_price_updated_at: new Date().toISOString(),
2358
+ position_status: positionStatus,
2359
+ created_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(), // Random date within last 30 days
2360
+ updated_at: new Date().toISOString(),
2361
+ });
2362
+ }
2363
+ return positions;
2364
+ }
2365
+ /**
2366
+ * Generate mock accounts with diverse data
2367
+ */
2368
+ generateMockAccounts(count) {
2369
+ const accounts = [];
2370
+ const brokers = ['robinhood', 'alpaca', 'tasty_trade', 'ninja_trader'];
2371
+ const accountTypes = ['margin', 'cash', 'crypto_wallet', 'live', 'sim'];
2372
+ const statuses = ['active', 'inactive'];
2373
+ const currencies = ['USD', 'EUR', 'GBP', 'CAD'];
2374
+ const accountNames = [
2375
+ 'Individual Account',
2376
+ 'Paper Trading',
2377
+ 'Retirement Account',
2378
+ 'Crypto Wallet',
2379
+ 'Margin Account',
2380
+ 'Cash Account',
2381
+ 'Trading Account',
2382
+ 'Investment Account',
2383
+ ];
2384
+ for (let i = 0; i < count; i++) {
2385
+ const broker = brokers[Math.floor(Math.random() * brokers.length)];
2386
+ const accountType = accountTypes[Math.floor(Math.random() * accountTypes.length)];
2387
+ const status = statuses[Math.floor(Math.random() * statuses.length)];
2388
+ const currency = currencies[Math.floor(Math.random() * currencies.length)];
2389
+ const accountName = accountNames[Math.floor(Math.random() * accountNames.length)];
2390
+ const cashBalance = Math.random() * 100000 + 1000;
2391
+ const buyingPower = cashBalance * (1 + Math.random() * 2); // 1-3x cash balance
2392
+ const equity = buyingPower * (0.8 + Math.random() * 0.4); // ±20% variation
2393
+ accounts.push({
2394
+ id: uuid.v4(),
2395
+ broker_id: broker,
2396
+ connection_id: uuid.v4(),
2397
+ account_id: `account_${Math.floor(Math.random() * 1000000)}`,
2398
+ account_name: `${accountName} ${i + 1}`,
2399
+ account_type: accountType,
2400
+ status,
2401
+ currency,
2402
+ cash_balance: cashBalance,
2403
+ buying_power: buyingPower,
2404
+ equity,
2405
+ created_at: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString(), // Random date within last year
2406
+ updated_at: new Date().toISOString(),
2407
+ last_synced_at: new Date().toISOString(),
2408
+ });
2409
+ }
2410
+ return accounts;
2411
+ }
2412
+ }
2413
+
2414
+ /**
2415
+ * Mock API Client that implements the same interface as the real ApiClient
2416
+ * but returns mock data instead of making HTTP requests
2417
+ */
2418
+ class MockApiClient {
2419
+ constructor(baseUrl, deviceInfo, mockConfig) {
2420
+ this.currentSessionState = null;
2421
+ this.currentSessionId = null;
2422
+ this.tradingContext = {};
2423
+ // Token management
2424
+ this.tokenInfo = null;
2425
+ this.refreshPromise = null;
2426
+ this.REFRESH_BUFFER_MINUTES = 5;
2427
+ // Session and company context
2428
+ this.companyId = null;
2429
+ this.csrfToken = null;
2430
+ this.baseUrl = baseUrl;
2431
+ this.deviceInfo = deviceInfo;
2432
+ this.mockApiOnly = mockConfig?.mockApiOnly || false;
2433
+ this.mockDataProvider = new MockDataProvider(mockConfig);
2434
+ // Log that mocks are being used
2435
+ if (this.mockApiOnly) {
2436
+ console.log('🔧 Finatic SDK: Using MOCK API Client (API only - real portal)');
2437
+ }
2438
+ else {
2439
+ console.log('🔧 Finatic SDK: Using MOCK API Client');
2440
+ }
2441
+ }
2442
+ /**
2443
+ * Store tokens after successful authentication
2444
+ */
2445
+ setTokens(accessToken, refreshToken, expiresAt, userId) {
2446
+ this.tokenInfo = {
2447
+ accessToken,
2448
+ refreshToken,
2449
+ expiresAt,
2450
+ userId,
2451
+ };
2452
+ }
2453
+ /**
2454
+ * Get the current access token, refreshing if necessary
2455
+ */
2456
+ async getValidAccessToken() {
2457
+ if (!this.tokenInfo) {
2458
+ throw new AuthenticationError('No tokens available. Please authenticate first.');
2459
+ }
2460
+ // Check if token is expired or about to expire
2461
+ if (this.isTokenExpired()) {
2462
+ await this.refreshTokens();
2463
+ }
2464
+ return this.tokenInfo.accessToken;
2465
+ }
2466
+ /**
2467
+ * Check if the current token is expired or about to expire
2468
+ */
2469
+ isTokenExpired() {
2470
+ if (!this.tokenInfo)
2471
+ return true;
2472
+ const expiryTime = new Date(this.tokenInfo.expiresAt).getTime();
2473
+ const currentTime = Date.now();
2474
+ const bufferTime = this.REFRESH_BUFFER_MINUTES * 60 * 1000;
2475
+ return currentTime >= expiryTime - bufferTime;
2476
+ }
2477
+ /**
2478
+ * Refresh the access token using the refresh token
2479
+ */
2480
+ async refreshTokens() {
2481
+ if (!this.tokenInfo) {
2482
+ throw new AuthenticationError('No refresh token available.');
2483
+ }
2484
+ // If a refresh is already in progress, wait for it
2485
+ if (this.refreshPromise) {
2486
+ await this.refreshPromise;
2487
+ return;
2488
+ }
2489
+ // Start a new refresh
2490
+ this.refreshPromise = this.performTokenRefresh();
2491
+ try {
2492
+ await this.refreshPromise;
2493
+ }
2494
+ finally {
2495
+ this.refreshPromise = null;
2496
+ }
2497
+ }
2498
+ /**
2499
+ * Perform the actual token refresh request
2500
+ */
2501
+ async performTokenRefresh() {
2502
+ if (!this.tokenInfo) {
2503
+ throw new AuthenticationError('No refresh token available.');
2504
+ }
2505
+ try {
2506
+ const response = await this.mockDataProvider.mockRefreshToken(this.tokenInfo.refreshToken);
2507
+ // Update stored tokens
2508
+ this.tokenInfo = {
2509
+ accessToken: response.response_data.access_token,
2510
+ refreshToken: response.response_data.refresh_token,
2511
+ expiresAt: response.response_data.expires_at,
2512
+ userId: this.tokenInfo.userId,
2513
+ };
2514
+ return this.tokenInfo;
2515
+ }
2516
+ catch (error) {
2517
+ // Clear tokens on refresh failure
2518
+ this.tokenInfo = null;
2519
+ throw new AuthenticationError('Token refresh failed. Please re-authenticate.', error);
2520
+ }
2521
+ }
2522
+ /**
2523
+ * Clear stored tokens (useful for logout)
2524
+ */
2525
+ clearTokens() {
2526
+ this.tokenInfo = null;
2527
+ this.refreshPromise = null;
2528
+ }
2529
+ /**
2530
+ * Get current token info (for debugging/testing)
2531
+ */
2532
+ getTokenInfo() {
2533
+ return this.tokenInfo ? { ...this.tokenInfo } : null;
2534
+ }
2535
+ /**
2536
+ * Set session context (session ID, company ID, CSRF token)
2537
+ */
2538
+ setSessionContext(sessionId, companyId, csrfToken) {
2539
+ this.currentSessionId = sessionId;
2540
+ this.companyId = companyId;
2541
+ this.csrfToken = csrfToken || null;
2542
+ }
2543
+ /**
2544
+ * Get the current session ID
2545
+ */
2546
+ getCurrentSessionId() {
2547
+ return this.currentSessionId;
2548
+ }
2549
+ /**
2550
+ * Get the current company ID
2551
+ */
2552
+ getCurrentCompanyId() {
2553
+ return this.companyId;
2554
+ }
2555
+ /**
2556
+ * Get the current CSRF token
2557
+ */
2558
+ getCurrentCsrfToken() {
2559
+ return this.csrfToken;
2560
+ }
2561
+ // Session Management
2562
+ async startSession(token, userId) {
2563
+ const response = await this.mockDataProvider.mockStartSession(token, userId);
2564
+ // Store session ID and set state to ACTIVE
2565
+ this.currentSessionId = response.data.session_id;
2566
+ this.currentSessionState = SessionState.ACTIVE;
2567
+ return response;
2568
+ }
2569
+ // OTP Flow
2570
+ async requestOtp(sessionId, email) {
2571
+ return this.mockDataProvider.mockRequestOtp(sessionId, email);
2572
+ }
2573
+ async verifyOtp(sessionId, otp) {
2574
+ const response = await this.mockDataProvider.mockVerifyOtp(sessionId, otp);
2575
+ // Store tokens after successful OTP verification
2576
+ if (response.success && response.data) {
2577
+ const expiresAt = new Date(Date.now() + response.data.expires_in * 1000).toISOString();
2578
+ this.setTokens(response.data.access_token, response.data.refresh_token, expiresAt, response.data.user_id);
2579
+ }
2580
+ return response;
2581
+ }
2582
+ // Direct Authentication
2583
+ async authenticateDirectly(sessionId, userId) {
2584
+ // Ensure session is active before authenticating
2585
+ if (this.currentSessionState !== SessionState.ACTIVE) {
2586
+ throw new SessionError('Session must be in ACTIVE state to authenticate');
2587
+ }
2588
+ const response = await this.mockDataProvider.mockAuthenticateDirectly(sessionId, userId);
2589
+ // Store tokens after successful direct authentication
2590
+ if (response.success && response.data) {
2591
+ // For direct auth, we don't get expires_in, so we'll set a default 1-hour expiry
2592
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour
2593
+ this.setTokens(response.data.access_token, response.data.refresh_token, expiresAt, userId);
2594
+ }
2595
+ return response;
2596
+ }
2597
+ // Portal Management
2598
+ async getPortalUrl(sessionId) {
2599
+ if (this.currentSessionState !== SessionState.ACTIVE) {
2600
+ throw new SessionError('Session must be in ACTIVE state to get portal URL');
2601
+ }
2602
+ // If in mockApiOnly mode, return the real portal URL
2603
+ if (this.mockApiOnly) {
2604
+ return {
2605
+ success: true,
2606
+ message: 'Portal URL retrieved successfully',
2607
+ data: {
2608
+ portal_url: 'http://localhost:5173/companies',
2609
+ },
2610
+ };
2611
+ }
2612
+ return this.mockDataProvider.mockGetPortalUrl(sessionId);
2613
+ }
2614
+ async validatePortalSession(sessionId, signature) {
2615
+ return this.mockDataProvider.mockValidatePortalSession(sessionId, signature);
2616
+ }
2617
+ async completePortalSession(sessionId) {
2618
+ return this.mockDataProvider.mockCompletePortalSession(sessionId);
2619
+ }
2620
+ // Portfolio Management
2621
+ async getHoldings(accessToken) {
2622
+ return this.mockDataProvider.mockGetHoldings();
2623
+ }
2624
+ async getOrders(accessToken, filter) {
2625
+ return this.mockDataProvider.mockGetOrders(filter);
2626
+ }
2627
+ async getPortfolio(accessToken) {
2628
+ return this.mockDataProvider.mockGetPortfolio();
2629
+ }
2630
+ async placeOrder(accessToken, order) {
2631
+ await this.mockDataProvider.mockPlaceOrder(order);
2632
+ }
2633
+ // New methods with automatic token management
2634
+ async getHoldingsAuto() {
2635
+ const accessToken = await this.getValidAccessToken();
2636
+ return this.getHoldings(accessToken);
2637
+ }
2638
+ async getOrdersAuto() {
2639
+ const accessToken = await this.getValidAccessToken();
2640
+ return this.getOrders(accessToken);
2641
+ }
2642
+ async getPortfolioAuto() {
2643
+ const accessToken = await this.getValidAccessToken();
2644
+ return this.getPortfolio(accessToken);
2645
+ }
2646
+ async placeOrderAuto(order) {
2647
+ const accessToken = await this.getValidAccessToken();
2648
+ return this.placeOrder(accessToken, order);
2649
+ }
2650
+ // Enhanced Trading Methods with Session Management
2651
+ async placeBrokerOrder(accessToken, params, extras = {}) {
2652
+ // Merge context with provided parameters
2653
+ const fullParams = {
2654
+ broker: (params.broker || this.tradingContext.broker) ||
2655
+ (() => {
2656
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
2657
+ })(),
2658
+ accountNumber: params.accountNumber ||
2659
+ this.tradingContext.accountNumber ||
2660
+ (() => {
2661
+ throw new Error('Account not set. Call setAccount() or pass accountNumber parameter.');
2662
+ })(),
2663
+ symbol: params.symbol,
2664
+ orderQty: params.orderQty,
2665
+ action: params.action,
2666
+ orderType: params.orderType,
2667
+ assetType: params.assetType,
2668
+ timeInForce: params.timeInForce || 'day',
2669
+ price: params.price,
2670
+ stopPrice: params.stopPrice,
2671
+ };
2672
+ // Convert timeInForce to the Order interface format
2673
+ const timeInForce = fullParams.timeInForce === 'gtd' ? 'gtc' : fullParams.timeInForce;
2674
+ return this.mockDataProvider.mockPlaceOrder({
2675
+ symbol: fullParams.symbol,
2676
+ side: fullParams.action.toLowerCase(),
2677
+ quantity: fullParams.orderQty,
2678
+ type_: fullParams.orderType.toLowerCase(),
2679
+ timeInForce: timeInForce,
2680
+ price: fullParams.price,
2681
+ stopPrice: fullParams.stopPrice,
2682
+ });
2683
+ }
2684
+ async cancelBrokerOrder(orderId, broker, extras = {}) {
2685
+ return {
2686
+ success: true,
2687
+ response_data: {
2688
+ orderId,
2689
+ status: 'cancelled',
2690
+ broker: broker || this.tradingContext.broker || 'robinhood',
2691
+ accountNumber: this.tradingContext.accountNumber || '123456789',
2692
+ },
2693
+ message: 'Order cancelled successfully',
2694
+ status_code: 200,
2695
+ };
2696
+ }
2697
+ async modifyBrokerOrder(orderId, params, broker, extras = {}) {
2698
+ return {
2699
+ success: true,
2700
+ response_data: {
2701
+ orderId,
2702
+ status: 'modified',
2703
+ broker: broker || this.tradingContext.broker || 'robinhood',
2704
+ accountNumber: this.tradingContext.accountNumber || '123456789',
2705
+ },
2706
+ message: 'Order modified successfully',
2707
+ status_code: 200,
2708
+ };
2709
+ }
2710
+ // Context management methods
2711
+ setBroker(broker) {
2712
+ this.tradingContext.broker = broker;
2713
+ // Clear account when broker changes
2714
+ this.tradingContext.accountNumber = undefined;
2715
+ this.tradingContext.accountId = undefined;
2716
+ }
2717
+ setAccount(accountNumber, accountId) {
2718
+ this.tradingContext.accountNumber = accountNumber;
2719
+ this.tradingContext.accountId = accountId;
2720
+ }
2721
+ getTradingContext() {
2722
+ return { ...this.tradingContext };
2723
+ }
2724
+ clearTradingContext() {
2725
+ this.tradingContext = {};
2726
+ }
2727
+ // Stock convenience methods
2728
+ async placeStockMarketOrder(accessToken, symbol, orderQty, action, broker, accountNumber, extras = {}) {
2729
+ return this.placeBrokerOrder(accessToken, {
2730
+ broker,
2731
+ accountNumber,
2732
+ symbol,
2733
+ orderQty,
2734
+ action,
2735
+ orderType: 'Market',
2736
+ assetType: 'Stock',
2737
+ timeInForce: 'day',
2738
+ }, extras);
2739
+ }
2740
+ async placeStockLimitOrder(accessToken, symbol, orderQty, action, price, timeInForce = 'gtc', broker, accountNumber, extras = {}) {
2741
+ return this.placeBrokerOrder(accessToken, {
2742
+ broker,
2743
+ accountNumber,
2744
+ symbol,
2745
+ orderQty,
2746
+ action,
2747
+ orderType: 'Limit',
2748
+ assetType: 'Stock',
2749
+ timeInForce,
2750
+ price,
2751
+ }, extras);
2752
+ }
2753
+ async placeStockStopOrder(accessToken, symbol, orderQty, action, stopPrice, timeInForce = 'day', broker, accountNumber, extras = {}) {
2754
+ return this.placeBrokerOrder(accessToken, {
2755
+ broker,
2756
+ accountNumber,
2757
+ symbol,
2758
+ orderQty,
2759
+ action,
2760
+ orderType: 'Stop',
2761
+ assetType: 'Stock',
2762
+ timeInForce,
2763
+ stopPrice,
2764
+ }, extras);
2765
+ }
2766
+ // Crypto convenience methods
2767
+ async placeCryptoMarketOrder(accessToken, symbol, orderQty, action, options = {}, broker, accountNumber, extras = {}) {
2768
+ const orderParams = {
2769
+ broker,
2770
+ accountNumber,
2771
+ symbol,
2772
+ orderQty: options.quantity || orderQty,
2773
+ action,
2774
+ orderType: 'Market',
2775
+ assetType: 'Crypto',
2776
+ timeInForce: 'gtc', // Crypto typically uses GTC
2777
+ };
2778
+ return this.placeBrokerOrder(accessToken, orderParams, extras);
2779
+ }
2780
+ async placeCryptoLimitOrder(accessToken, symbol, orderQty, action, price, timeInForce = 'gtc', options = {}, broker, accountNumber, extras = {}) {
2781
+ const orderParams = {
2782
+ broker,
2783
+ accountNumber,
2784
+ symbol,
2785
+ orderQty: options.quantity || orderQty,
2786
+ action,
2787
+ orderType: 'Limit',
2788
+ assetType: 'Crypto',
2789
+ timeInForce,
2790
+ price,
2791
+ };
2792
+ return this.placeBrokerOrder(accessToken, orderParams, extras);
2793
+ }
2794
+ // Options convenience methods
2795
+ async placeOptionsMarketOrder(accessToken, symbol, orderQty, action, options, broker, accountNumber, extras = {}) {
2796
+ const orderParams = {
2797
+ broker,
2798
+ accountNumber,
2799
+ symbol,
2800
+ orderQty,
2801
+ action,
2802
+ orderType: 'Market',
2803
+ assetType: 'Option',
2804
+ timeInForce: 'day',
2805
+ };
2806
+ return this.placeBrokerOrder(accessToken, orderParams, extras);
2807
+ }
2808
+ async placeOptionsLimitOrder(accessToken, symbol, orderQty, action, price, options, timeInForce = 'gtc', broker, accountNumber, extras = {}) {
2809
+ const orderParams = {
2810
+ broker,
2811
+ accountNumber,
2812
+ symbol,
2813
+ orderQty,
2814
+ action,
2815
+ orderType: 'Limit',
2816
+ assetType: 'Option',
2817
+ timeInForce,
2818
+ price,
2819
+ };
2820
+ return this.placeBrokerOrder(accessToken, orderParams, extras);
2821
+ }
2822
+ // Futures convenience methods
2823
+ async placeFuturesMarketOrder(accessToken, symbol, orderQty, action, broker, accountNumber, extras = {}) {
2824
+ return this.placeBrokerOrder(accessToken, {
2825
+ broker,
2826
+ accountNumber,
2827
+ symbol,
2828
+ orderQty,
2829
+ action,
2830
+ orderType: 'Market',
2831
+ assetType: 'Futures',
2832
+ timeInForce: 'day',
2833
+ }, extras);
2834
+ }
2835
+ async placeFuturesLimitOrder(accessToken, symbol, orderQty, action, price, timeInForce = 'gtc', broker, accountNumber, extras = {}) {
2836
+ return this.placeBrokerOrder(accessToken, {
2837
+ broker,
2838
+ accountNumber,
2839
+ symbol,
2840
+ orderQty,
2841
+ action,
2842
+ orderType: 'Limit',
2843
+ assetType: 'Futures',
2844
+ timeInForce,
2845
+ price,
2846
+ }, extras);
2847
+ }
2848
+ async revokeToken(accessToken) {
2849
+ // Clear tokens on revoke
2850
+ this.clearTokens();
2851
+ }
2852
+ async getUserToken(userId) {
2853
+ const token = this.mockDataProvider.getUserToken(userId);
2854
+ if (!token) {
2855
+ throw new AuthenticationError('User token not found');
2856
+ }
2857
+ return token;
2858
+ }
2859
+ getCurrentSessionState() {
2860
+ return this.currentSessionState;
2861
+ }
2862
+ // Broker Data Management
2863
+ async getBrokerList(accessToken) {
2864
+ return this.mockDataProvider.mockGetBrokerList();
2865
+ }
2866
+ async getBrokerAccounts(accessToken, options) {
2867
+ return this.mockDataProvider.mockGetBrokerAccounts();
2868
+ }
2869
+ async getBrokerOrders(accessToken, options) {
2870
+ // Return empty orders for now - keeping original interface
2871
+ return {
2872
+ _id: uuid.v4(),
2873
+ response_data: [],
2874
+ message: 'Broker orders retrieved successfully',
2875
+ status_code: 200,
2876
+ warnings: null,
2877
+ errors: null,
2878
+ };
2879
+ }
2880
+ async getBrokerPositions(accessToken, options) {
2881
+ // Return empty positions for now - keeping original interface
2882
+ return {
2883
+ _id: uuid.v4(),
2884
+ response_data: [],
2885
+ message: 'Broker positions retrieved successfully',
2886
+ status_code: 200,
2887
+ warnings: null,
2888
+ errors: null,
2889
+ };
2890
+ }
2891
+ // New broker data methods with filtering support
2892
+ async getBrokerOrdersWithFilter(filter) {
2893
+ return this.mockDataProvider.mockGetBrokerOrders(filter);
2894
+ }
2895
+ async getBrokerPositionsWithFilter(filter) {
2896
+ return this.mockDataProvider.mockGetBrokerPositions(filter);
2897
+ }
2898
+ async getBrokerDataAccountsWithFilter(filter) {
2899
+ return this.mockDataProvider.mockGetBrokerDataAccounts(filter);
2900
+ }
2901
+ // Page-based pagination methods
2902
+ async getBrokerOrdersPage(page = 1, perPage = 100, filters) {
2903
+ const mockOrders = await this.mockDataProvider.mockGetBrokerOrders(filters);
2904
+ const orders = mockOrders.data;
2905
+ // Simulate pagination
2906
+ const startIndex = (page - 1) * perPage;
2907
+ const endIndex = startIndex + perPage;
2908
+ const paginatedOrders = orders.slice(startIndex, endIndex);
2909
+ const hasMore = endIndex < orders.length;
2910
+ const nextOffset = hasMore ? endIndex : startIndex;
2911
+ // Create navigation callback for mock pagination
2912
+ const navigationCallback = async (newOffset, newLimit) => {
2913
+ const newStartIndex = newOffset;
2914
+ const newEndIndex = newStartIndex + newLimit;
2915
+ const newPaginatedOrders = orders.slice(newStartIndex, newEndIndex);
2916
+ const newHasMore = newEndIndex < orders.length;
2917
+ const newNextOffset = newHasMore ? newEndIndex : newStartIndex;
2918
+ return new PaginatedResult(newPaginatedOrders, {
2919
+ has_more: newHasMore,
2920
+ next_offset: newNextOffset,
2921
+ current_offset: newStartIndex,
2922
+ limit: newLimit,
2923
+ }, navigationCallback);
2924
+ };
2925
+ return new PaginatedResult(paginatedOrders, {
2926
+ has_more: hasMore,
2927
+ next_offset: nextOffset,
2928
+ current_offset: startIndex,
2929
+ limit: perPage,
2930
+ }, navigationCallback);
2931
+ }
2932
+ async getBrokerAccountsPage(page = 1, perPage = 100, filters) {
2933
+ const mockAccounts = await this.mockDataProvider.mockGetBrokerDataAccounts(filters);
2934
+ const accounts = mockAccounts.data;
2935
+ // Simulate pagination
2936
+ const startIndex = (page - 1) * perPage;
2937
+ const endIndex = startIndex + perPage;
2938
+ const paginatedAccounts = accounts.slice(startIndex, endIndex);
2939
+ const hasMore = endIndex < accounts.length;
2940
+ const nextOffset = hasMore ? endIndex : startIndex;
2941
+ // Create navigation callback for mock pagination
2942
+ const navigationCallback = async (newOffset, newLimit) => {
2943
+ const newStartIndex = newOffset;
2944
+ const newEndIndex = newStartIndex + newLimit;
2945
+ const newPaginatedAccounts = accounts.slice(newStartIndex, newEndIndex);
2946
+ const newHasMore = newEndIndex < accounts.length;
2947
+ const newNextOffset = newHasMore ? newEndIndex : newStartIndex;
2948
+ return new PaginatedResult(newPaginatedAccounts, {
2949
+ has_more: newHasMore,
2950
+ next_offset: newNextOffset,
2951
+ current_offset: newStartIndex,
2952
+ limit: newLimit,
2953
+ }, navigationCallback);
2954
+ };
2955
+ return new PaginatedResult(paginatedAccounts, {
2956
+ has_more: hasMore,
2957
+ next_offset: nextOffset,
2958
+ current_offset: startIndex,
2959
+ limit: perPage,
2960
+ }, navigationCallback);
2961
+ }
2962
+ async getBrokerPositionsPage(page = 1, perPage = 100, filters) {
2963
+ const mockPositions = await this.mockDataProvider.mockGetBrokerPositions(filters);
2964
+ const positions = mockPositions.data;
2965
+ // Simulate pagination
2966
+ const startIndex = (page - 1) * perPage;
2967
+ const endIndex = startIndex + perPage;
2968
+ const paginatedPositions = positions.slice(startIndex, endIndex);
2969
+ const hasMore = endIndex < positions.length;
2970
+ const nextOffset = hasMore ? endIndex : startIndex;
2971
+ // Create navigation callback for mock pagination
2972
+ const navigationCallback = async (newOffset, newLimit) => {
2973
+ const newStartIndex = newOffset;
2974
+ const newEndIndex = newStartIndex + newLimit;
2975
+ const newPaginatedPositions = positions.slice(newStartIndex, newEndIndex);
2976
+ const newHasMore = newEndIndex < positions.length;
2977
+ const newNextOffset = newHasMore ? newEndIndex : newStartIndex;
2978
+ return new PaginatedResult(newPaginatedPositions, {
2979
+ has_more: newHasMore,
2980
+ next_offset: newNextOffset,
2981
+ current_offset: newStartIndex,
2982
+ limit: newLimit,
2983
+ }, navigationCallback);
2984
+ };
2985
+ return new PaginatedResult(paginatedPositions, {
2986
+ has_more: hasMore,
2987
+ next_offset: nextOffset,
2988
+ current_offset: startIndex,
2989
+ limit: perPage,
2990
+ }, navigationCallback);
2991
+ }
2992
+ async getBrokerConnections(accessToken) {
2993
+ return this.mockDataProvider.mockGetBrokerConnections();
2994
+ }
2995
+ // Automatic token management versions of broker methods
2996
+ async getBrokerListAuto() {
2997
+ const accessToken = await this.getValidAccessToken();
2998
+ return this.getBrokerList(accessToken);
2999
+ }
3000
+ async getBrokerAccountsAuto(options) {
3001
+ const accessToken = await this.getValidAccessToken();
3002
+ return this.getBrokerAccounts(accessToken, options);
3003
+ }
3004
+ async getBrokerOrdersAuto(options) {
3005
+ const accessToken = await this.getValidAccessToken();
3006
+ return this.getBrokerOrders(accessToken, options);
3007
+ }
3008
+ async getBrokerPositionsAuto(options) {
3009
+ const accessToken = await this.getValidAccessToken();
3010
+ return this.getBrokerPositions(accessToken, options);
3011
+ }
3012
+ async getBrokerConnectionsAuto() {
3013
+ const accessToken = await this.getValidAccessToken();
3014
+ return this.getBrokerConnections(accessToken);
3015
+ }
3016
+ // Automatic token management versions of trading methods
3017
+ async placeBrokerOrderAuto(params, extras = {}) {
3018
+ const accessToken = await this.getValidAccessToken();
3019
+ return this.placeBrokerOrder(accessToken, params, extras);
3020
+ }
3021
+ async placeStockMarketOrderAuto(symbol, orderQty, action, broker, accountNumber, extras = {}) {
3022
+ const accessToken = await this.getValidAccessToken();
3023
+ return this.placeStockMarketOrder(accessToken, symbol, orderQty, action, broker, accountNumber, extras);
3024
+ }
3025
+ async placeStockLimitOrderAuto(symbol, orderQty, action, price, timeInForce = 'gtc', broker, accountNumber, extras = {}) {
3026
+ const accessToken = await this.getValidAccessToken();
3027
+ return this.placeStockLimitOrder(accessToken, symbol, orderQty, action, price, timeInForce, broker, accountNumber, extras);
3028
+ }
3029
+ async placeStockStopOrderAuto(symbol, orderQty, action, stopPrice, timeInForce = 'day', broker, accountNumber, extras = {}) {
3030
+ const accessToken = await this.getValidAccessToken();
3031
+ return this.placeStockStopOrder(accessToken, symbol, orderQty, action, stopPrice, timeInForce, broker, accountNumber, extras);
3032
+ }
3033
+ // Utility methods for mock system
3034
+ getMockDataProvider() {
3035
+ return this.mockDataProvider;
3036
+ }
3037
+ clearMockData() {
3038
+ this.mockDataProvider.clearData();
3039
+ }
3040
+ /**
3041
+ * Check if this is a mock client
3042
+ * @returns true if this is a mock client
3043
+ */
3044
+ isMockClient() {
3045
+ return true;
3046
+ }
3047
+ }
3048
+
3049
+ /**
3050
+ * Utility functions for mock system environment detection
3051
+ */
3052
+ /**
3053
+ * Check if mocks should be used based on environment variables
3054
+ * Supports both browser and Node.js environments
3055
+ */
3056
+ function shouldUseMocks() {
3057
+ // Check Node.js environment
3058
+ if (typeof process !== 'undefined' && process?.env) {
3059
+ return (process.env.FINATIC_USE_MOCKS === 'true' ||
3060
+ process.env.NEXT_PUBLIC_FINATIC_USE_MOCKS === 'true');
3061
+ }
3062
+ // Check browser environment
3063
+ if (typeof window !== 'undefined') {
3064
+ // Check global variable
3065
+ if (window.FINATIC_USE_MOCKS === 'true') {
3066
+ return true;
3067
+ }
3068
+ // Check Next.js public environment variables
3069
+ if (window.NEXT_PUBLIC_FINATIC_USE_MOCKS === 'true') {
3070
+ return true;
3071
+ }
3072
+ // Check localStorage
3073
+ try {
3074
+ return localStorage.getItem('FINATIC_USE_MOCKS') === 'true';
3075
+ }
3076
+ catch (error) {
3077
+ // localStorage might not be available
3078
+ return false;
3079
+ }
3080
+ }
3081
+ return false;
3082
+ }
3083
+ /**
3084
+ * Check if only API should be mocked (but portal should use real URL)
3085
+ */
3086
+ function shouldMockApiOnly() {
3087
+ // Check Node.js environment
3088
+ if (typeof process !== 'undefined' && process?.env) {
3089
+ return (process.env.FINATIC_MOCK_API_ONLY === 'true' ||
3090
+ process.env.NEXT_PUBLIC_FINATIC_MOCK_API_ONLY === 'true');
3091
+ }
3092
+ // Check browser environment
3093
+ if (typeof window !== 'undefined') {
3094
+ // Check global variable
3095
+ if (window.FINATIC_MOCK_API_ONLY === 'true') {
3096
+ return true;
3097
+ }
3098
+ // Check Next.js public environment variables
3099
+ if (window.NEXT_PUBLIC_FINATIC_MOCK_API_ONLY === 'true') {
3100
+ return true;
3101
+ }
3102
+ // Check localStorage
3103
+ try {
3104
+ return localStorage.getItem('FINATIC_MOCK_API_ONLY') === 'true';
3105
+ }
3106
+ catch (error) {
3107
+ // localStorage might not be available
3108
+ return false;
3109
+ }
3110
+ }
3111
+ return false;
3112
+ }
3113
+ /**
3114
+ * Get mock configuration from environment
3115
+ */
3116
+ function getMockConfig() {
3117
+ const enabled = shouldUseMocks();
3118
+ const mockApiOnly = shouldMockApiOnly();
3119
+ let delay;
3120
+ // Check for custom delay in Node.js
3121
+ if (typeof process !== 'undefined' && process?.env?.FINATIC_MOCK_DELAY) {
3122
+ delay = parseInt(process.env.FINATIC_MOCK_DELAY, 10);
3123
+ }
3124
+ // Check for custom delay in browser
3125
+ if (typeof window !== 'undefined') {
3126
+ try {
3127
+ const storedDelay = localStorage.getItem('FINATIC_MOCK_DELAY');
3128
+ if (storedDelay) {
3129
+ delay = parseInt(storedDelay, 10);
3130
+ }
3131
+ }
3132
+ catch (error) {
3133
+ // localStorage might not be available
3134
+ }
3135
+ }
3136
+ return { enabled, delay, mockApiOnly };
3137
+ }
3138
+
3139
+ /**
3140
+ * Factory class for creating API clients (real or mock)
3141
+ */
3142
+ class MockFactory {
3143
+ /**
3144
+ * Create an API client based on environment configuration
3145
+ * @param baseUrl - The base URL for the API
3146
+ * @param deviceInfo - Optional device information
3147
+ * @param mockConfig - Optional mock configuration (only used if mocks are enabled)
3148
+ * @returns ApiClient or MockApiClient instance
3149
+ */
3150
+ static createApiClient(baseUrl, deviceInfo, mockConfig) {
3151
+ const useMocks = shouldUseMocks();
3152
+ const mockApiOnly = shouldMockApiOnly();
3153
+ if (useMocks || mockApiOnly) {
3154
+ // Merge environment config with provided config
3155
+ const envConfig = getMockConfig();
3156
+ const finalConfig = {
3157
+ delay: mockConfig?.delay || envConfig.delay,
3158
+ scenario: mockConfig?.scenario || 'success',
3159
+ customData: mockConfig?.customData || {},
3160
+ mockApiOnly: mockApiOnly, // Pass this flag to the mock client
3161
+ };
3162
+ return new MockApiClient(baseUrl, deviceInfo, finalConfig);
3163
+ }
3164
+ else {
3165
+ return new ApiClient(baseUrl, deviceInfo);
3166
+ }
3167
+ }
3168
+ /**
3169
+ * Force create a mock API client regardless of environment settings
3170
+ * @param baseUrl - The base URL for the API
3171
+ * @param deviceInfo - Optional device information
3172
+ * @param mockConfig - Optional mock configuration
3173
+ * @returns MockApiClient instance
3174
+ */
3175
+ static createMockApiClient(baseUrl, deviceInfo, mockConfig) {
3176
+ return new MockApiClient(baseUrl, deviceInfo, mockConfig);
3177
+ }
3178
+ /**
3179
+ * Force create a real API client regardless of environment settings
3180
+ * @param baseUrl - The base URL for the API
3181
+ * @param deviceInfo - Optional device information
3182
+ * @returns ApiClient instance
3183
+ */
3184
+ static createRealApiClient(baseUrl, deviceInfo) {
3185
+ return new ApiClient(baseUrl, deviceInfo);
3186
+ }
3187
+ /**
3188
+ * Check if mocks are currently enabled
3189
+ * @returns boolean indicating if mocks are enabled
3190
+ */
3191
+ static isMockMode() {
3192
+ return shouldUseMocks();
3193
+ }
3194
+ /**
3195
+ * Get current mock configuration
3196
+ * @returns Mock configuration object
3197
+ */
3198
+ static getMockConfig() {
3199
+ return getMockConfig();
3200
+ }
3201
+ }
3202
+
3203
+ // Dark theme (default Finatic theme)
3204
+ const darkTheme = {
3205
+ mode: 'dark',
3206
+ colors: {
3207
+ background: {
3208
+ primary: '#000000',
3209
+ secondary: '#1a1a1a',
3210
+ tertiary: '#2a2a2a',
3211
+ accent: 'rgba(0, 255, 255, 0.1)',
3212
+ glass: 'rgba(255, 255, 255, 0.05)',
3213
+ },
3214
+ status: {
3215
+ connected: '#00FFFF',
3216
+ disconnected: '#EF4444',
3217
+ warning: '#F59E0B',
3218
+ pending: '#8B5CF6',
3219
+ error: '#EF4444',
3220
+ success: '#00FFFF',
3221
+ },
3222
+ text: {
3223
+ primary: '#FFFFFF',
3224
+ secondary: '#CBD5E1',
3225
+ muted: '#94A3B8',
3226
+ inverse: '#000000',
3227
+ },
3228
+ border: {
3229
+ primary: 'rgba(0, 255, 255, 0.2)',
3230
+ secondary: 'rgba(255, 255, 255, 0.1)',
3231
+ hover: 'rgba(0, 255, 255, 0.4)',
3232
+ focus: 'rgba(0, 255, 255, 0.6)',
3233
+ accent: '#00FFFF',
3234
+ },
3235
+ input: {
3236
+ background: '#1a1a1a',
3237
+ border: 'rgba(0, 255, 255, 0.2)',
3238
+ borderFocus: '#00FFFF',
3239
+ text: '#FFFFFF',
3240
+ placeholder: '#94A3B8',
3241
+ },
3242
+ button: {
3243
+ primary: {
3244
+ background: '#00FFFF',
3245
+ text: '#000000',
3246
+ hover: '#00E6E6',
3247
+ active: '#00CCCC',
3248
+ },
3249
+ secondary: {
3250
+ background: 'transparent',
3251
+ text: '#00FFFF',
3252
+ border: '#00FFFF',
3253
+ hover: 'rgba(0, 255, 255, 0.1)',
3254
+ active: 'rgba(0, 255, 255, 0.2)',
3255
+ },
3256
+ },
3257
+ },
3258
+ branding: {
3259
+ primaryColor: '#00FFFF',
3260
+ },
3261
+ };
3262
+ // Light theme with cyan accents
3263
+ const lightTheme = {
3264
+ mode: 'light',
3265
+ colors: {
3266
+ background: {
3267
+ primary: '#FFFFFF',
3268
+ secondary: '#F8FAFC',
3269
+ tertiary: '#F1F5F9',
3270
+ accent: 'rgba(0, 255, 255, 0.1)',
3271
+ glass: 'rgba(0, 0, 0, 0.05)',
3272
+ },
3273
+ status: {
3274
+ connected: '#00FFFF',
3275
+ disconnected: '#EF4444',
3276
+ warning: '#F59E0B',
3277
+ pending: '#8B5CF6',
3278
+ error: '#EF4444',
3279
+ success: '#00FFFF',
3280
+ },
3281
+ text: {
3282
+ primary: '#1E293B',
3283
+ secondary: '#64748B',
3284
+ muted: '#94A3B8',
3285
+ inverse: '#FFFFFF',
3286
+ },
3287
+ border: {
3288
+ primary: 'rgba(0, 255, 255, 0.2)',
3289
+ secondary: 'rgba(0, 0, 0, 0.1)',
3290
+ hover: 'rgba(0, 255, 255, 0.4)',
3291
+ focus: 'rgba(0, 255, 255, 0.6)',
3292
+ accent: '#00FFFF',
3293
+ },
3294
+ input: {
3295
+ background: '#FFFFFF',
3296
+ border: 'rgba(0, 255, 255, 0.2)',
3297
+ borderFocus: '#00FFFF',
3298
+ text: '#1E293B',
3299
+ placeholder: '#94A3B8',
3300
+ },
3301
+ button: {
3302
+ primary: {
3303
+ background: '#00FFFF',
3304
+ text: '#000000',
3305
+ hover: '#00E6E6',
3306
+ active: '#00CCCC',
3307
+ },
3308
+ secondary: {
3309
+ background: 'transparent',
3310
+ text: '#00FFFF',
3311
+ border: '#00FFFF',
3312
+ hover: 'rgba(0, 255, 255, 0.1)',
3313
+ active: 'rgba(0, 255, 255, 0.2)',
3314
+ },
3315
+ },
3316
+ },
3317
+ branding: {
3318
+ primaryColor: '#00FFFF',
3319
+ },
3320
+ };
3321
+ // Corporate blue theme
3322
+ const corporateBlueTheme = {
3323
+ mode: 'dark',
3324
+ colors: {
3325
+ background: {
3326
+ primary: '#1E293B',
3327
+ secondary: '#334155',
3328
+ tertiary: '#475569',
3329
+ accent: 'rgba(59, 130, 246, 0.1)',
3330
+ glass: 'rgba(255, 255, 255, 0.05)',
3331
+ },
3332
+ status: {
3333
+ connected: '#3B82F6',
3334
+ disconnected: '#EF4444',
3335
+ warning: '#F59E0B',
3336
+ pending: '#8B5CF6',
3337
+ error: '#EF4444',
3338
+ success: '#10B981',
3339
+ },
3340
+ text: {
3341
+ primary: '#F8FAFC',
3342
+ secondary: '#CBD5E1',
3343
+ muted: '#94A3B8',
3344
+ inverse: '#1E293B',
3345
+ },
3346
+ border: {
3347
+ primary: 'rgba(59, 130, 246, 0.2)',
3348
+ secondary: 'rgba(255, 255, 255, 0.1)',
3349
+ hover: 'rgba(59, 130, 246, 0.4)',
3350
+ focus: 'rgba(59, 130, 246, 0.6)',
3351
+ accent: '#3B82F6',
3352
+ },
3353
+ input: {
3354
+ background: '#334155',
3355
+ border: 'rgba(59, 130, 246, 0.2)',
3356
+ borderFocus: '#3B82F6',
3357
+ text: '#F8FAFC',
3358
+ placeholder: '#94A3B8',
3359
+ },
3360
+ button: {
3361
+ primary: {
3362
+ background: '#3B82F6',
3363
+ text: '#FFFFFF',
3364
+ hover: '#2563EB',
3365
+ active: '#1D4ED8',
3366
+ },
3367
+ secondary: {
3368
+ background: 'transparent',
3369
+ text: '#3B82F6',
3370
+ border: '#3B82F6',
3371
+ hover: 'rgba(59, 130, 246, 0.1)',
3372
+ active: 'rgba(59, 130, 246, 0.2)',
3373
+ },
3374
+ },
3375
+ },
3376
+ branding: {
3377
+ primaryColor: '#3B82F6',
3378
+ },
3379
+ };
3380
+ // Purple theme
3381
+ const purpleTheme = {
3382
+ mode: 'dark',
3383
+ colors: {
3384
+ background: {
3385
+ primary: '#1a1a1a',
3386
+ secondary: '#2a2a2a',
3387
+ tertiary: '#3a3a3a',
3388
+ accent: 'rgba(168, 85, 247, 0.1)',
3389
+ glass: 'rgba(255, 255, 255, 0.05)',
3390
+ },
3391
+ status: {
3392
+ connected: '#A855F7',
3393
+ disconnected: '#EF4444',
3394
+ warning: '#F59E0B',
3395
+ pending: '#8B5CF6',
3396
+ error: '#EF4444',
3397
+ success: '#10B981',
3398
+ },
3399
+ text: {
3400
+ primary: '#F8FAFC',
3401
+ secondary: '#CBD5E1',
3402
+ muted: '#94A3B8',
3403
+ inverse: '#1a1a1a',
3404
+ },
3405
+ border: {
3406
+ primary: 'rgba(168, 85, 247, 0.2)',
3407
+ secondary: 'rgba(255, 255, 255, 0.1)',
3408
+ hover: 'rgba(168, 85, 247, 0.4)',
3409
+ focus: 'rgba(168, 85, 247, 0.6)',
3410
+ accent: '#A855F7',
3411
+ },
3412
+ input: {
3413
+ background: '#334155',
3414
+ border: 'rgba(168, 85, 247, 0.2)',
3415
+ borderFocus: '#A855F7',
3416
+ text: '#F8FAFC',
3417
+ placeholder: '#94A3B8',
3418
+ },
3419
+ button: {
3420
+ primary: {
3421
+ background: '#A855F7',
3422
+ text: '#FFFFFF',
3423
+ hover: '#9333EA',
3424
+ active: '#7C3AED',
3425
+ },
3426
+ secondary: {
3427
+ background: 'transparent',
3428
+ text: '#A855F7',
3429
+ border: '#A855F7',
3430
+ hover: 'rgba(168, 85, 247, 0.1)',
3431
+ active: 'rgba(168, 85, 247, 0.2)',
3432
+ },
3433
+ },
3434
+ },
3435
+ branding: {
3436
+ primaryColor: '#A855F7',
3437
+ },
3438
+ };
3439
+ // Green theme
3440
+ const greenTheme = {
3441
+ mode: 'dark',
3442
+ colors: {
3443
+ background: {
3444
+ primary: '#1a1a1a',
3445
+ secondary: '#2a2a2a',
3446
+ tertiary: '#3a3a3a',
3447
+ accent: 'rgba(34, 197, 94, 0.1)',
3448
+ glass: 'rgba(255, 255, 255, 0.05)',
3449
+ },
3450
+ status: {
3451
+ connected: '#22C55E',
3452
+ disconnected: '#EF4444',
3453
+ warning: '#F59E0B',
3454
+ pending: '#8B5CF6',
3455
+ error: '#EF4444',
3456
+ success: '#22C55E',
3457
+ },
3458
+ text: {
3459
+ primary: '#F8FAFC',
3460
+ secondary: '#CBD5E1',
3461
+ muted: '#94A3B8',
3462
+ inverse: '#1a1a1a',
3463
+ },
3464
+ border: {
3465
+ primary: 'rgba(34, 197, 94, 0.2)',
3466
+ secondary: 'rgba(255, 255, 255, 0.1)',
3467
+ hover: 'rgba(34, 197, 94, 0.4)',
3468
+ focus: 'rgba(34, 197, 94, 0.6)',
3469
+ accent: '#22C55E',
3470
+ },
3471
+ input: {
3472
+ background: '#334155',
3473
+ border: 'rgba(34, 197, 94, 0.2)',
3474
+ borderFocus: '#22C55E',
3475
+ text: '#F8FAFC',
3476
+ placeholder: '#94A3B8',
3477
+ },
3478
+ button: {
3479
+ primary: {
3480
+ background: '#22C55E',
3481
+ text: '#FFFFFF',
3482
+ hover: '#16A34A',
3483
+ active: '#15803D',
3484
+ },
3485
+ secondary: {
3486
+ background: 'transparent',
3487
+ text: '#22C55E',
3488
+ border: '#22C55E',
3489
+ hover: 'rgba(34, 197, 94, 0.1)',
3490
+ active: 'rgba(34, 197, 94, 0.2)',
3491
+ },
3492
+ },
3493
+ },
3494
+ branding: {
3495
+ primaryColor: '#22C55E',
3496
+ },
3497
+ };
3498
+ // Orange theme
3499
+ const orangeTheme = {
3500
+ mode: 'dark',
3501
+ colors: {
3502
+ background: {
3503
+ primary: '#1a1a1a',
3504
+ secondary: '#2a2a2a',
3505
+ tertiary: '#3a3a3a',
3506
+ accent: 'rgba(249, 115, 22, 0.1)',
3507
+ glass: 'rgba(255, 255, 255, 0.05)',
3508
+ },
3509
+ status: {
3510
+ connected: '#F97316',
3511
+ disconnected: '#EF4444',
3512
+ warning: '#F59E0B',
3513
+ pending: '#8B5CF6',
3514
+ error: '#EF4444',
3515
+ success: '#10B981',
3516
+ },
3517
+ text: {
3518
+ primary: '#F8FAFC',
3519
+ secondary: '#CBD5E1',
3520
+ muted: '#94A3B8',
3521
+ inverse: '#1a1a1a',
3522
+ },
3523
+ border: {
3524
+ primary: 'rgba(249, 115, 22, 0.2)',
3525
+ secondary: 'rgba(255, 255, 255, 0.1)',
3526
+ hover: 'rgba(249, 115, 22, 0.4)',
3527
+ focus: 'rgba(249, 115, 22, 0.6)',
3528
+ accent: '#F97316',
3529
+ },
3530
+ input: {
3531
+ background: '#334155',
3532
+ border: 'rgba(249, 115, 22, 0.2)',
3533
+ borderFocus: '#F97316',
3534
+ text: '#F8FAFC',
3535
+ placeholder: '#94A3B8',
3536
+ },
3537
+ button: {
3538
+ primary: {
3539
+ background: '#F97316',
3540
+ text: '#FFFFFF',
3541
+ hover: '#EA580C',
3542
+ active: '#DC2626',
3543
+ },
3544
+ secondary: {
3545
+ background: 'transparent',
3546
+ text: '#F97316',
3547
+ border: '#F97316',
3548
+ hover: 'rgba(249, 115, 22, 0.1)',
3549
+ active: 'rgba(249, 115, 22, 0.2)',
3550
+ },
3551
+ },
3552
+ },
3553
+ branding: {
3554
+ primaryColor: '#F97316',
3555
+ },
3556
+ };
3557
+ // Theme preset mapping
3558
+ const portalThemePresets = {
3559
+ dark: darkTheme,
3560
+ light: lightTheme,
3561
+ corporateBlue: corporateBlueTheme,
3562
+ purple: purpleTheme,
3563
+ green: greenTheme,
3564
+ orange: orangeTheme,
3565
+ };
3566
+
3567
+ /**
3568
+ * Generate a portal URL with theme parameters
3569
+ * @param baseUrl The base portal URL
3570
+ * @param theme The theme configuration
3571
+ * @returns The portal URL with theme parameters
3572
+ */
3573
+ function generatePortalThemeURL(baseUrl, theme) {
3574
+ if (!theme) {
3575
+ return baseUrl;
3576
+ }
3577
+ try {
3578
+ const url = new URL(baseUrl);
3579
+ if (theme.preset) {
3580
+ // Use preset theme
3581
+ url.searchParams.set('theme', theme.preset);
3582
+ }
3583
+ else if (theme.custom) {
3584
+ // Use custom theme
3585
+ const encodedTheme = btoa(JSON.stringify(theme.custom));
3586
+ url.searchParams.set('theme', 'custom');
3587
+ url.searchParams.set('themeObject', encodedTheme);
3588
+ }
3589
+ return url.toString();
3590
+ }
3591
+ catch (error) {
3592
+ console.error('Failed to generate theme URL:', error);
3593
+ return baseUrl;
3594
+ }
3595
+ }
3596
+ /**
3597
+ * Generate a portal URL with theme parameters, appending to existing query params
3598
+ * @param baseUrl The base portal URL (may already have query parameters)
3599
+ * @param theme The theme configuration
3600
+ * @returns The portal URL with theme parameters appended
3601
+ */
3602
+ function appendThemeToURL(baseUrl, theme) {
3603
+ if (!theme) {
3604
+ return baseUrl;
3605
+ }
3606
+ try {
3607
+ const url = new URL(baseUrl);
3608
+ if (theme.preset) {
3609
+ // Use preset theme
3610
+ url.searchParams.set('theme', theme.preset);
3611
+ }
3612
+ else if (theme.custom) {
3613
+ // Use custom theme
3614
+ const encodedTheme = btoa(JSON.stringify(theme.custom));
3615
+ url.searchParams.set('theme', 'custom');
3616
+ url.searchParams.set('themeObject', encodedTheme);
3617
+ }
3618
+ return url.toString();
3619
+ }
3620
+ catch (error) {
3621
+ console.error('Failed to append theme to URL:', error);
3622
+ return baseUrl;
3623
+ }
3624
+ }
3625
+ /**
3626
+ * Get a theme configuration by preset name
3627
+ * @param preset The preset theme name
3628
+ * @returns The theme configuration or undefined if not found
3629
+ */
3630
+ function getThemePreset(preset) {
3631
+ return portalThemePresets[preset];
3632
+ }
3633
+ /**
3634
+ * Validate a custom theme configuration
3635
+ * @param theme The theme configuration to validate
3636
+ * @returns True if valid, false otherwise
3637
+ */
3638
+ function validateCustomTheme(theme) {
3639
+ try {
3640
+ // Check required properties
3641
+ if (!theme.mode || !['dark', 'light', 'auto'].includes(theme.mode)) {
3642
+ return false;
3643
+ }
3644
+ if (!theme.colors) {
3645
+ return false;
3646
+ }
3647
+ // Check required color sections
3648
+ const requiredSections = ['background', 'status', 'text', 'border', 'input', 'button'];
3649
+ for (const section of requiredSections) {
3650
+ if (!theme.colors[section]) {
3651
+ return false;
3652
+ }
3653
+ }
3654
+ return true;
3655
+ }
3656
+ catch (error) {
3657
+ console.error('Theme validation error:', error);
3658
+ return false;
3659
+ }
3660
+ }
3661
+ /**
3662
+ * Create a custom theme from a preset with modifications
3663
+ * @param preset The base preset theme
3664
+ * @param modifications Partial theme modifications
3665
+ * @returns The modified theme configuration
3666
+ */
3667
+ function createCustomThemeFromPreset(preset, modifications) {
3668
+ const baseTheme = getThemePreset(preset);
3669
+ if (!baseTheme) {
3670
+ console.error(`Preset theme '${preset}' not found`);
3671
+ return null;
3672
+ }
3673
+ return {
3674
+ ...baseTheme,
3675
+ ...modifications,
3676
+ };
3677
+ }
3678
+
3679
+ class FinaticConnect extends EventEmitter {
3680
+ constructor(options, deviceInfo) {
3681
+ super();
3682
+ this.userToken = null;
3683
+ this.sessionId = null;
3684
+ this.BROKER_LIST_CACHE_KEY = 'finatic_broker_list_cache';
3685
+ this.BROKER_LIST_CACHE_VERSION = '1.0';
3686
+ this.BROKER_LIST_CACHE_DURATION = 1000 * 60 * 60 * 24; // 24 hours in milliseconds
3687
+ this.currentSessionState = null;
3688
+ this.options = options;
3689
+ this.baseUrl = options.baseUrl || 'http://localhost:8000';
3690
+ this.apiClient = MockFactory.createApiClient(this.baseUrl, deviceInfo);
3691
+ this.portalUI = new PortalUI(this.baseUrl);
3692
+ this.deviceInfo = deviceInfo;
3693
+ // Extract company ID from token
3694
+ try {
3695
+ // Validate token exists
3696
+ if (!options.token) {
3697
+ throw new Error('Token is required but not provided');
3698
+ }
3699
+ // Check if token is in JWT format (contains dots)
3700
+ if (options.token.includes('.')) {
3701
+ const tokenParts = options.token.split('.');
3702
+ if (tokenParts.length === 3) {
3703
+ const payload = JSON.parse(atob(tokenParts[1]));
3704
+ this.companyId = payload.company_id;
3705
+ }
3706
+ else {
3707
+ throw new Error('Invalid JWT token format');
3708
+ }
3709
+ }
3710
+ else {
3711
+ // Handle UUID format token
3712
+ // For UUID tokens, we'll get the company_id from the session start response
3713
+ this.companyId = ''; // Will be set after session start
3714
+ }
3715
+ }
3716
+ catch (error) {
3717
+ if (error instanceof Error) {
3718
+ throw new Error('Failed to parse token: ' + error.message);
3719
+ }
3720
+ else {
3721
+ throw new Error('Failed to parse token: Unknown error');
3722
+ }
3723
+ }
3724
+ // Set up event listeners for callbacks
3725
+ if (this.options.onSuccess) {
3726
+ this.on('success', this.options.onSuccess);
3727
+ }
3728
+ if (this.options.onError) {
3729
+ this.on('error', this.options.onError);
3730
+ }
3731
+ if (this.options.onClose) {
3732
+ this.on('close', this.options.onClose);
3733
+ }
3734
+ // Register automatic session cleanup
3735
+ this.registerSessionCleanup();
3736
+ }
3737
+ handleTokens(tokens) {
3738
+ if (!tokens.access_token || !tokens.refresh_token) {
3739
+ return;
3740
+ }
3741
+ // Keep existing user_id or use empty string as fallback
3742
+ const userId = this.userToken?.user_id || '';
3743
+ this.userToken = {
3744
+ accessToken: tokens.access_token,
3745
+ refreshToken: tokens.refresh_token,
3746
+ expiresIn: 3600, // Default to 1 hour if not provided
3747
+ user_id: userId,
3748
+ tokenType: 'Bearer',
3749
+ scope: 'api:access',
3750
+ };
3751
+ // Store tokens in ApiClient for automatic refresh
3752
+ const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); // 1 hour from now
3753
+ this.apiClient.setTokens(tokens.access_token, tokens.refresh_token, expiresAt, userId);
3754
+ }
3755
+ /**
3756
+ * Check if the user is authenticated
3757
+ * @returns True if the user has a valid access token
3758
+ */
3759
+ isAuthenticated() {
3760
+ return this.isAuthed();
3761
+ }
3762
+ /**
3763
+ * Check if the user is fully authenticated (has userId, access token, and refresh token)
3764
+ * @returns True if the user is fully authenticated and ready for API calls
3765
+ */
3766
+ isAuthed() {
3767
+ return !!(this.userToken?.accessToken &&
3768
+ this.userToken?.refreshToken &&
3769
+ this.userToken?.user_id);
3770
+ }
3771
+ /**
3772
+ * Get user's orders with pagination and optional filtering
3773
+ * @param params - Query parameters including page, perPage, and filters
3774
+ * @returns Promise with paginated result that supports navigation
3775
+ */
3776
+ async getOrders(params) {
3777
+ if (!this.isAuthenticated()) {
3778
+ throw new AuthenticationError('User is not authenticated');
3779
+ }
3780
+ const page = params?.page || 1;
3781
+ const perPage = params?.perPage || 100;
3782
+ const filter = params?.filter;
3783
+ return this.getOrdersPage(page, perPage, filter);
3784
+ }
3785
+ /**
3786
+ * Get user's positions with pagination and optional filtering
3787
+ * @param params - Query parameters including page, perPage, and filters
3788
+ * @returns Promise with paginated result that supports navigation
3789
+ */
3790
+ async getPositions(params) {
3791
+ if (!this.isAuthenticated()) {
3792
+ throw new AuthenticationError('User is not authenticated');
3793
+ }
3794
+ const page = params?.page || 1;
3795
+ const perPage = params?.perPage || 100;
3796
+ const filter = params?.filter;
3797
+ return this.getPositionsPage(page, perPage, filter);
3798
+ }
3799
+ /**
3800
+ * Get user's accounts with pagination and optional filtering
3801
+ * @param params - Query parameters including page, perPage, and filters
3802
+ * @returns Promise with paginated result that supports navigation
3803
+ */
3804
+ async getAccounts(params) {
3805
+ if (!this.isAuthenticated()) {
3806
+ throw new AuthenticationError('User is not authenticated');
3807
+ }
3808
+ const page = params?.page || 1;
3809
+ const perPage = params?.perPage || 100;
3810
+ const filter = params?.filter;
3811
+ return this.getAccountsPage(page, perPage, filter);
3812
+ }
3813
+ /**
3814
+ * Revoke the current user's access
3815
+ */
3816
+ async revokeToken() {
3817
+ if (!this.userToken) {
3818
+ return;
3819
+ }
3820
+ try {
3821
+ await this.apiClient.revokeToken(this.userToken.accessToken);
3822
+ this.userToken = null;
3823
+ }
3824
+ catch (error) {
3825
+ this.emit('error', error);
3826
+ throw error;
3827
+ }
3828
+ }
3829
+ /**
3830
+ * Initialize the Finatic Connect SDK
3831
+ * @param token - The portal token from your backend
3832
+ * @param userId - Optional: The user ID if you have it from a previous session
3833
+ * @param options - Optional configuration including baseUrl
3834
+ * @returns FinaticConnect instance
3835
+ */
3836
+ static async init(token, userId, options) {
3837
+ if (!FinaticConnect.instance) {
3838
+ const connectOptions = {
3839
+ token,
3840
+ baseUrl: options?.baseUrl || 'http://localhost:8000',
3841
+ onSuccess: undefined,
3842
+ onError: undefined,
3843
+ onClose: undefined,
3844
+ };
3845
+ // Generate device info
3846
+ const deviceInfo = {
3847
+ ip_address: '', // Will be set by the server
3848
+ user_agent: navigator.userAgent,
3849
+ fingerprint: btoa([
3850
+ navigator.userAgent,
3851
+ navigator.language,
3852
+ new Date().getTimezoneOffset(),
3853
+ screen.width,
3854
+ screen.height,
3855
+ navigator.hardwareConcurrency,
3856
+ // @ts-expect-error - deviceMemory is not in the Navigator type but exists in modern browsers
3857
+ navigator.deviceMemory || 'unknown',
3858
+ ].join('|')),
3859
+ };
3860
+ FinaticConnect.instance = new FinaticConnect(connectOptions, deviceInfo);
3861
+ // Start session and get session data
3862
+ const normalizedUserId = userId || undefined; // Convert null to undefined
3863
+ const startResponse = await FinaticConnect.instance.apiClient.startSession(token, normalizedUserId);
3864
+ FinaticConnect.instance.sessionId = startResponse.data.session_id;
3865
+ FinaticConnect.instance.companyId = startResponse.data.company_id || '';
3866
+ // Set session context in API client
3867
+ if (FinaticConnect.instance.apiClient &&
3868
+ typeof FinaticConnect.instance.apiClient.setSessionContext === 'function') {
3869
+ FinaticConnect.instance.apiClient.setSessionContext(FinaticConnect.instance.sessionId, FinaticConnect.instance.companyId, startResponse.data.csrf_token // If available in response
3870
+ );
3871
+ }
3872
+ // If userId is provided, authenticate directly
3873
+ if (normalizedUserId) {
3874
+ try {
3875
+ const authResponse = await FinaticConnect.instance.apiClient.authenticateDirectly(startResponse.data.session_id, normalizedUserId);
3876
+ // Convert API response to UserToken format
3877
+ const userToken = {
3878
+ accessToken: authResponse.data.access_token,
3879
+ refreshToken: authResponse.data.refresh_token,
3880
+ expiresIn: 3600, // Default to 1 hour
3881
+ user_id: normalizedUserId,
3882
+ tokenType: 'Bearer',
3883
+ scope: 'api:access',
3884
+ };
3885
+ // Set the tokens in both FinaticConnect and ApiClient
3886
+ FinaticConnect.instance.userToken = userToken;
3887
+ // Set tokens in ApiClient for automatic token management
3888
+ const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); // 1 hour from now
3889
+ FinaticConnect.instance.apiClient.setTokens(authResponse.data.access_token, authResponse.data.refresh_token, expiresAt, normalizedUserId);
3890
+ // Emit success event
3891
+ FinaticConnect.instance.emit('success', normalizedUserId);
3892
+ }
3893
+ catch (error) {
3894
+ FinaticConnect.instance.emit('error', error);
3895
+ throw error;
3896
+ }
3897
+ }
3898
+ }
3899
+ return FinaticConnect.instance;
3900
+ }
3901
+ /**
3902
+ * Initialize the SDK with a user ID
3903
+ * @param userId - The user ID from a previous session
3904
+ */
3905
+ async setUserId(userId) {
3906
+ await this.initializeWithUser(userId);
3907
+ }
3908
+ async initializeWithUser(userId) {
3909
+ try {
3910
+ this.userToken = await this.apiClient.getUserToken(userId);
3911
+ // Set tokens in ApiClient for automatic token management
3912
+ if (this.userToken) {
3913
+ const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); // 1 hour from now
3914
+ this.apiClient.setTokens(this.userToken.accessToken, this.userToken.refreshToken, expiresAt, userId);
3915
+ }
3916
+ this.emit('success', userId);
3917
+ }
3918
+ catch (error) {
3919
+ this.emit('error', error);
3920
+ }
3921
+ }
3922
+ /**
3923
+ * Handle company access error by opening the portal
3924
+ * @param error The company access error
3925
+ * @param options Optional configuration for the portal
3926
+ */
3927
+ async handleCompanyAccessError(error, options) {
3928
+ // Emit a specific event for company access errors
3929
+ this.emit('companyAccessError', error);
3930
+ // Open the portal to allow the user to connect a broker
3931
+ await this.openPortal(options);
3932
+ }
3933
+ /**
3934
+ * Open the portal for user authentication
3935
+ * @param options Optional configuration for the portal
3936
+ */
3937
+ async openPortal(options) {
3938
+ try {
3939
+ if (!this.sessionId) {
3940
+ throw new SessionError('Session not initialized');
3941
+ }
3942
+ // Ensure session is active
3943
+ const sessionState = this.apiClient.getCurrentSessionState();
3944
+ if (sessionState !== SessionState.ACTIVE) {
3945
+ // If not active, try to start a new session
3946
+ const startResponse = await this.apiClient.startSession(this.options.token);
3947
+ this.sessionId = startResponse.data.session_id;
3948
+ this.companyId = startResponse.data.company_id || '';
3949
+ // Set session context in API client
3950
+ if (this.apiClient && typeof this.apiClient.setSessionContext === 'function') {
3951
+ this.apiClient.setSessionContext(this.sessionId, this.companyId, startResponse.data.csrf_token // If available in response
3952
+ );
3953
+ }
3954
+ // Wait for session to become active
3955
+ const maxAttempts = 5;
3956
+ let attempts = 0;
3957
+ while (attempts < maxAttempts) {
3958
+ const sessionResponse = await this.apiClient.validatePortalSession(this.sessionId, '');
3959
+ if (sessionResponse.status === 'active') {
3960
+ break;
3961
+ }
3962
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second between attempts
3963
+ attempts++;
3964
+ }
3965
+ if (attempts === maxAttempts) {
3966
+ throw new SessionError('Session failed to become active');
3967
+ }
3968
+ }
3969
+ // Get portal URL
3970
+ const portalResponse = await this.apiClient.getPortalUrl(this.sessionId);
3971
+ if (!portalResponse.data.portal_url) {
3972
+ throw new Error('Failed to get portal URL');
3973
+ }
3974
+ // Apply theme to portal URL if provided
3975
+ const themedPortalUrl = appendThemeToURL(portalResponse.data.portal_url, options?.theme);
3976
+ // Create portal UI if not exists
3977
+ if (!this.portalUI) {
3978
+ this.portalUI = new PortalUI(this.baseUrl);
3979
+ }
3980
+ // Show portal
3981
+ this.portalUI.show(themedPortalUrl, this.sessionId || '', {
3982
+ onSuccess: async (userId) => {
3983
+ try {
3984
+ if (!this.sessionId) {
3985
+ throw new SessionError('Session not initialized');
3986
+ }
3987
+ // Get tokens from portal UI
3988
+ const userToken = this.portalUI.getTokens();
3989
+ if (!userToken) {
3990
+ throw new Error('No tokens received from portal');
3991
+ }
3992
+ // Set the tokens internally
3993
+ this.userToken = userToken;
3994
+ // Set tokens in ApiClient for automatic token management
3995
+ const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString(); // 1 hour from now
3996
+ this.apiClient.setTokens(userToken.accessToken, userToken.refreshToken, expiresAt, userId);
3997
+ // Emit portal success event
3998
+ this.emit('portal:success', userId);
3999
+ // Emit legacy success event
4000
+ this.emit('success', userId);
4001
+ options?.onSuccess?.(userId);
4002
+ }
4003
+ catch (error) {
4004
+ if (error instanceof CompanyAccessError) {
4005
+ // Handle company access error by opening the portal
4006
+ await this.handleCompanyAccessError(error, options);
4007
+ }
4008
+ else {
4009
+ this.emit('error', error);
4010
+ options?.onError?.(error);
4011
+ }
4012
+ }
4013
+ },
4014
+ onError: (error) => {
4015
+ // Emit portal error event
4016
+ this.emit('portal:error', error);
4017
+ // Emit legacy error event
4018
+ this.emit('error', error);
4019
+ options?.onError?.(error);
4020
+ },
4021
+ onClose: () => {
4022
+ // Emit portal close event
4023
+ this.emit('portal:close');
4024
+ // Emit legacy close event
4025
+ this.emit('close');
4026
+ options?.onClose?.();
4027
+ },
4028
+ onEvent: (type, data) => {
4029
+ console.log('[FinaticConnect] Portal event received:', type, data);
4030
+ // Emit generic event
4031
+ this.emit('event', type, data);
4032
+ // Call the event callback
4033
+ options?.onEvent?.(type, data);
4034
+ },
4035
+ });
4036
+ }
4037
+ catch (error) {
4038
+ if (error instanceof CompanyAccessError) {
4039
+ // Handle company access error by opening the portal
4040
+ await this.handleCompanyAccessError(error, options);
4041
+ }
4042
+ else {
4043
+ this.emit('error', error);
4044
+ options?.onError?.(error);
4045
+ }
4046
+ }
4047
+ }
4048
+ /**
4049
+ * Close the Finatic Connect Portal
4050
+ */
4051
+ closePortal() {
4052
+ this.portalUI.hide();
4053
+ this.emit('close');
4054
+ }
4055
+ /**
4056
+ * Initialize a new session
4057
+ * @param oneTimeToken - The one-time token from initSession
4058
+ */
4059
+ async startSession(oneTimeToken) {
4060
+ try {
4061
+ const response = await this.apiClient.startSession(oneTimeToken);
4062
+ this.sessionId = response.data.session_id;
4063
+ this.currentSessionState = response.data.state;
4064
+ // Set session context in API client
4065
+ if (this.apiClient && typeof this.apiClient.setSessionContext === 'function') {
4066
+ this.apiClient.setSessionContext(this.sessionId, this.companyId, response.data.csrf_token // If available in response
4067
+ );
4068
+ }
4069
+ // For non-direct auth, we need to wait for the session to be ACTIVE
4070
+ if (response.data.state === SessionState.PENDING) {
4071
+ // Wait for session to become active
4072
+ const maxAttempts = 5;
4073
+ let attempts = 0;
4074
+ while (attempts < maxAttempts) {
4075
+ const sessionResponse = await this.apiClient.validatePortalSession(this.sessionId, '');
4076
+ if (sessionResponse.status === 'active') {
4077
+ this.currentSessionState = SessionState.ACTIVE;
4078
+ break;
4079
+ }
4080
+ await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second between attempts
4081
+ attempts++;
4082
+ }
4083
+ if (attempts === maxAttempts) {
4084
+ throw new SessionError('Session failed to become active');
4085
+ }
4086
+ }
4087
+ }
4088
+ catch (error) {
4089
+ if (error instanceof SessionError) {
4090
+ throw new AuthenticationError('Failed to start session', error.details);
4091
+ }
4092
+ throw error;
4093
+ }
4094
+ }
4095
+ /**
4096
+ * Place a new order using the broker order API
4097
+ * @param order - Order details with broker context
4098
+ */
4099
+ async placeOrder(order) {
4100
+ if (!this.userToken) {
4101
+ throw new Error('Not initialized with user');
4102
+ }
4103
+ try {
4104
+ // Convert order format to match broker API
4105
+ const brokerOrder = {
4106
+ symbol: order.symbol,
4107
+ orderQty: order.quantity,
4108
+ action: order.side === 'buy' ? 'Buy' : 'Sell',
4109
+ orderType: order.orderType === 'market'
4110
+ ? 'Market'
4111
+ : order.orderType === 'limit'
4112
+ ? 'Limit'
4113
+ : order.orderType === 'stop'
4114
+ ? 'Stop'
4115
+ : 'TrailingStop',
4116
+ assetType: order.assetType || 'Stock',
4117
+ timeInForce: order.timeInForce,
4118
+ price: order.price,
4119
+ stopPrice: order.stopPrice,
4120
+ broker: order.broker,
4121
+ accountNumber: order.accountNumber,
4122
+ };
4123
+ return await this.apiClient.placeBrokerOrder(this.userToken.accessToken, brokerOrder);
4124
+ }
4125
+ catch (error) {
4126
+ this.emit('error', error);
4127
+ throw error;
4128
+ }
4129
+ }
4130
+ /**
4131
+ * Cancel a broker order
4132
+ * @param orderId - The order ID to cancel
4133
+ * @param broker - Optional broker override
4134
+ */
4135
+ async cancelOrder(orderId, broker) {
4136
+ if (!this.userToken) {
4137
+ throw new Error('Not initialized with user');
4138
+ }
4139
+ try {
4140
+ return await this.apiClient.cancelBrokerOrder(orderId, broker);
4141
+ }
4142
+ catch (error) {
4143
+ this.emit('error', error);
4144
+ throw error;
4145
+ }
4146
+ }
4147
+ /**
4148
+ * Modify a broker order
4149
+ * @param orderId - The order ID to modify
4150
+ * @param modifications - The modifications to apply
4151
+ * @param broker - Optional broker override
4152
+ */
4153
+ async modifyOrder(orderId, modifications, broker) {
4154
+ if (!this.userToken) {
4155
+ throw new Error('Not initialized with user');
4156
+ }
4157
+ try {
4158
+ // Convert modifications to broker format
4159
+ const brokerModifications = {};
4160
+ if (modifications.symbol)
4161
+ brokerModifications.symbol = modifications.symbol;
4162
+ if (modifications.quantity)
4163
+ brokerModifications.orderQty = modifications.quantity;
4164
+ if (modifications.price)
4165
+ brokerModifications.price = modifications.price;
4166
+ if (modifications.stopPrice)
4167
+ brokerModifications.stopPrice = modifications.stopPrice;
4168
+ if (modifications.timeInForce)
4169
+ brokerModifications.timeInForce = modifications.timeInForce;
4170
+ return await this.apiClient.modifyBrokerOrder(orderId, brokerModifications, broker);
4171
+ }
4172
+ catch (error) {
4173
+ this.emit('error', error);
4174
+ throw error;
4175
+ }
4176
+ }
4177
+ /**
4178
+ * Set the broker context for trading
4179
+ * @param broker - The broker to use for trading
4180
+ */
4181
+ setBroker(broker) {
4182
+ this.apiClient.setBroker(broker);
4183
+ }
4184
+ /**
4185
+ * Set the account context for trading
4186
+ * @param accountNumber - The account number to use for trading
4187
+ * @param accountId - Optional account ID
4188
+ */
4189
+ setAccount(accountNumber, accountId) {
4190
+ this.apiClient.setAccount(accountNumber, accountId);
4191
+ }
4192
+ /**
4193
+ * Get the current trading context
4194
+ */
4195
+ getTradingContext() {
4196
+ return this.apiClient.getTradingContext();
4197
+ }
4198
+ /**
4199
+ * Clear the trading context
4200
+ */
4201
+ clearTradingContext() {
4202
+ this.apiClient.clearTradingContext();
4203
+ }
4204
+ /**
4205
+ * Place a stock market order (convenience method)
4206
+ */
4207
+ async placeStockMarketOrder(symbol, quantity, side, broker, accountNumber) {
4208
+ if (!this.userToken) {
4209
+ throw new Error('Not initialized with user');
4210
+ }
4211
+ try {
4212
+ return await this.apiClient.placeStockMarketOrder(this.userToken.accessToken, symbol, quantity, side === 'buy' ? 'Buy' : 'Sell', broker, accountNumber);
4213
+ }
4214
+ catch (error) {
4215
+ this.emit('error', error);
4216
+ throw error;
4217
+ }
4218
+ }
4219
+ /**
4220
+ * Place a stock limit order (convenience method)
4221
+ */
4222
+ async placeStockLimitOrder(symbol, quantity, side, price, timeInForce = 'gtc', broker, accountNumber) {
4223
+ if (!this.userToken) {
4224
+ throw new Error('Not initialized with user');
4225
+ }
4226
+ try {
4227
+ return await this.apiClient.placeStockLimitOrder(this.userToken.accessToken, symbol, quantity, side === 'buy' ? 'Buy' : 'Sell', price, timeInForce, broker, accountNumber);
4228
+ }
4229
+ catch (error) {
4230
+ this.emit('error', error);
4231
+ throw error;
4232
+ }
4233
+ }
4234
+ /**
4235
+ * Get the current user ID
4236
+ * @returns The current user ID or undefined if not authenticated
4237
+ * @throws AuthenticationError if user is not authenticated
4238
+ */
4239
+ getUserId() {
4240
+ if (!this.isAuthenticated()) {
4241
+ return null;
4242
+ }
4243
+ if (!this.userToken?.user_id) {
4244
+ return null;
4245
+ }
4246
+ return this.userToken.user_id;
4247
+ }
4248
+ /**
4249
+ * Get list of supported brokers
4250
+ * @returns Promise with array of broker information
4251
+ */
4252
+ async getBrokerList() {
4253
+ if (!this.isAuthenticated()) {
4254
+ throw new AuthenticationError('Not authenticated');
4255
+ }
4256
+ const response = await this.apiClient.getBrokerListAuto();
4257
+ const baseUrl = this.baseUrl.replace('/api/v1', ''); // Remove /api/v1 to get the base URL
4258
+ // Transform the broker list to include full logo URLs
4259
+ return response.response_data.map((broker) => ({
4260
+ ...broker,
4261
+ logo_path: broker.logo_path ? `${baseUrl}${broker.logo_path}` : '',
4262
+ }));
4263
+ }
4264
+ /**
4265
+ * Get broker connections
4266
+ * @returns Promise with array of broker connections
4267
+ * @throws AuthenticationError if user is not authenticated
4268
+ */
4269
+ async getBrokerConnections() {
4270
+ if (!this.isAuthenticated()) {
4271
+ throw new AuthenticationError('User is not authenticated. Please connect a broker first.');
4272
+ }
4273
+ if (!this.userToken?.user_id) {
4274
+ throw new AuthenticationError('No user ID available. Please connect a broker first.');
4275
+ }
4276
+ const response = await this.apiClient.getBrokerConnectionsAuto();
4277
+ if (response.status_code !== 200) {
4278
+ throw new Error(response.message || 'Failed to retrieve broker connections');
4279
+ }
4280
+ return response.response_data;
4281
+ }
4282
+ // Abstract convenience methods
4283
+ /**
4284
+ * Get only open positions
4285
+ * @returns Promise with array of open positions
4286
+ */
4287
+ async getOpenPositions() {
4288
+ return this.getAllPositions({ position_status: 'open' });
4289
+ }
4290
+ /**
4291
+ * Get only filled orders
4292
+ * @returns Promise with array of filled orders
4293
+ */
4294
+ async getFilledOrders() {
4295
+ return this.getAllOrders({ status: 'filled' });
4296
+ }
4297
+ /**
4298
+ * Get only pending orders
4299
+ * @returns Promise with array of pending orders
4300
+ */
4301
+ async getPendingOrders() {
4302
+ return this.getAllOrders({ status: 'pending' });
4303
+ }
4304
+ /**
4305
+ * Get only active accounts
4306
+ * @returns Promise with array of active accounts
4307
+ */
4308
+ async getActiveAccounts() {
4309
+ return this.getAllAccounts({ status: 'active' });
4310
+ }
4311
+ /**
4312
+ * Get orders for a specific symbol
4313
+ * @param symbol - The symbol to filter by
4314
+ * @returns Promise with array of orders for the symbol
4315
+ */
4316
+ async getOrdersBySymbol(symbol) {
4317
+ return this.getAllOrders({ symbol });
4318
+ }
4319
+ /**
4320
+ * Get positions for a specific symbol
4321
+ * @param symbol - The symbol to filter by
4322
+ * @returns Promise with array of positions for the symbol
4323
+ */
4324
+ async getPositionsBySymbol(symbol) {
4325
+ return this.getAllPositions({ symbol });
4326
+ }
4327
+ /**
4328
+ * Get orders for a specific broker
4329
+ * @param brokerId - The broker ID to filter by
4330
+ * @returns Promise with array of orders for the broker
4331
+ */
4332
+ async getOrdersByBroker(brokerId) {
4333
+ return this.getAllOrders({ broker_id: brokerId });
4334
+ }
4335
+ /**
4336
+ * Get positions for a specific broker
4337
+ * @param brokerId - The broker ID to filter by
4338
+ * @returns Promise with array of positions for the broker
4339
+ */
4340
+ async getPositionsByBroker(brokerId) {
4341
+ return this.getAllPositions({ broker_id: brokerId });
4342
+ }
4343
+ // Pagination methods
4344
+ /**
4345
+ * Get a specific page of orders with pagination metadata
4346
+ * @param page - Page number (default: 1)
4347
+ * @param perPage - Items per page (default: 100)
4348
+ * @param filter - Optional filter parameters
4349
+ * @returns Promise with paginated orders result
4350
+ */
4351
+ async getOrdersPage(page = 1, perPage = 100, filter) {
4352
+ if (!this.isAuthenticated()) {
4353
+ throw new AuthenticationError('User is not authenticated');
4354
+ }
4355
+ return this.apiClient.getBrokerOrdersPage(page, perPage, filter);
4356
+ }
4357
+ /**
4358
+ * Get a specific page of positions with pagination metadata
4359
+ * @param page - Page number (default: 1)
4360
+ * @param perPage - Items per page (default: 100)
4361
+ * @param filter - Optional filter parameters
4362
+ * @returns Promise with paginated positions result
4363
+ */
4364
+ async getPositionsPage(page = 1, perPage = 100, filter) {
4365
+ if (!this.isAuthenticated()) {
4366
+ throw new AuthenticationError('User is not authenticated');
4367
+ }
4368
+ return this.apiClient.getBrokerPositionsPage(page, perPage, filter);
4369
+ }
4370
+ /**
4371
+ * Get a specific page of accounts with pagination metadata
4372
+ * @param page - Page number (default: 1)
4373
+ * @param perPage - Items per page (default: 100)
4374
+ * @param filter - Optional filter parameters
4375
+ * @returns Promise with paginated accounts result
4376
+ */
4377
+ async getAccountsPage(page = 1, perPage = 100, filter) {
4378
+ if (!this.isAuthenticated()) {
4379
+ throw new AuthenticationError('User is not authenticated');
4380
+ }
4381
+ return this.apiClient.getBrokerAccountsPage(page, perPage, filter);
4382
+ }
4383
+ /**
4384
+ * Get the next page of orders
4385
+ * @param previousResult - The previous paginated result
4386
+ * @returns Promise with next page of orders or null if no more pages
4387
+ */
4388
+ async getNextOrdersPage(previousResult) {
4389
+ if (!this.isAuthenticated()) {
4390
+ throw new AuthenticationError('User is not authenticated');
4391
+ }
4392
+ return this.apiClient.getNextPage(previousResult, (offset, limit) => {
4393
+ const page = Math.floor(offset / limit) + 1;
4394
+ return this.apiClient.getBrokerOrdersPage(page, limit);
4395
+ });
4396
+ }
4397
+ /**
4398
+ * Get the next page of positions
4399
+ * @param previousResult - The previous paginated result
4400
+ * @returns Promise with next page of positions or null if no more pages
4401
+ */
4402
+ async getNextPositionsPage(previousResult) {
4403
+ if (!this.isAuthenticated()) {
4404
+ throw new AuthenticationError('User is not authenticated');
4405
+ }
4406
+ return this.apiClient.getNextPage(previousResult, (offset, limit) => {
4407
+ const page = Math.floor(offset / limit) + 1;
4408
+ return this.apiClient.getBrokerPositionsPage(page, limit);
4409
+ });
4410
+ }
4411
+ /**
4412
+ * Get the next page of accounts
4413
+ * @param previousResult - The previous paginated result
4414
+ * @returns Promise with next page of accounts or null if no more pages
4415
+ */
4416
+ async getNextAccountsPage(previousResult) {
4417
+ if (!this.isAuthenticated()) {
4418
+ throw new AuthenticationError('User is not authenticated');
4419
+ }
4420
+ return this.apiClient.getNextPage(previousResult, (offset, limit) => {
4421
+ const page = Math.floor(offset / limit) + 1;
4422
+ return this.apiClient.getBrokerAccountsPage(page, limit);
4423
+ });
4424
+ }
4425
+ /**
4426
+ * Get all orders across all pages (convenience method)
4427
+ * @param filter - Optional filter parameters
4428
+ * @returns Promise with all orders
4429
+ */
4430
+ async getAllOrders(filter) {
4431
+ if (!this.isAuthenticated()) {
4432
+ throw new AuthenticationError('User is not authenticated');
4433
+ }
4434
+ const allData = [];
4435
+ let currentResult = await this.getOrdersPage(1, 100, filter);
4436
+ while (currentResult) {
4437
+ allData.push(...currentResult.data);
4438
+ if (!currentResult.hasNext)
4439
+ break;
4440
+ const nextResult = await this.getNextOrdersPage(currentResult);
4441
+ if (!nextResult)
4442
+ break;
4443
+ currentResult = nextResult;
4444
+ }
4445
+ return allData;
4446
+ }
4447
+ /**
4448
+ * Get all positions across all pages (convenience method)
4449
+ * @param filter - Optional filter parameters
4450
+ * @returns Promise with all positions
4451
+ */
4452
+ async getAllPositions(filter) {
4453
+ if (!this.isAuthenticated()) {
4454
+ throw new AuthenticationError('User is not authenticated');
4455
+ }
4456
+ const allData = [];
4457
+ let currentResult = await this.getPositionsPage(1, 100, filter);
4458
+ while (currentResult) {
4459
+ allData.push(...currentResult.data);
4460
+ if (!currentResult.hasNext)
4461
+ break;
4462
+ const nextResult = await this.getNextPositionsPage(currentResult);
4463
+ if (!nextResult)
4464
+ break;
4465
+ currentResult = nextResult;
4466
+ }
4467
+ return allData;
4468
+ }
4469
+ /**
4470
+ * Get all accounts across all pages (convenience method)
4471
+ * @param filter - Optional filter parameters
4472
+ * @returns Promise with all accounts
4473
+ */
4474
+ async getAllAccounts(filter) {
4475
+ if (!this.isAuthenticated()) {
4476
+ throw new AuthenticationError('User is not authenticated');
4477
+ }
4478
+ const allData = [];
4479
+ let currentResult = await this.getAccountsPage(1, 100, filter);
4480
+ while (currentResult) {
4481
+ allData.push(...currentResult.data);
4482
+ if (!currentResult.hasNext)
4483
+ break;
4484
+ const nextResult = await this.getNextAccountsPage(currentResult);
4485
+ if (!nextResult)
4486
+ break;
4487
+ currentResult = nextResult;
4488
+ }
4489
+ return allData;
4490
+ }
4491
+ /**
4492
+ * Register automatic session cleanup on page unload/visibility change
4493
+ */
4494
+ registerSessionCleanup() {
4495
+ // Cleanup on page unload (refresh, close tab, navigate away)
4496
+ window.addEventListener('beforeunload', this.handleSessionCleanup.bind(this));
4497
+ // Cleanup on page visibility change (mobile browsers, app switching)
4498
+ document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
4499
+ }
4500
+ /**
4501
+ * Handle session cleanup when page is unloading
4502
+ */
4503
+ async handleSessionCleanup() {
4504
+ if (this.sessionId) {
4505
+ await this.completeSession(this.sessionId);
4506
+ }
4507
+ }
4508
+ /**
4509
+ * Handle visibility change (mobile browsers)
4510
+ */
4511
+ async handleVisibilityChange() {
4512
+ if (document.visibilityState === 'hidden' && this.sessionId) {
4513
+ await this.completeSession(this.sessionId);
4514
+ }
4515
+ }
4516
+ /**
4517
+ * Complete the session by calling the API
4518
+ * @param sessionId - The session ID to complete
4519
+ */
4520
+ async completeSession(sessionId) {
4521
+ try {
4522
+ // Check if we're in mock mode (check if apiClient is a mock client)
4523
+ const isMockMode = this.apiClient &&
4524
+ typeof this.apiClient.isMockClient === 'function' &&
4525
+ this.apiClient.isMockClient();
4526
+ if (isMockMode) {
4527
+ // Mock the completion response
4528
+ console.log('[FinaticConnect] Mock session completion for session:', sessionId);
4529
+ return;
4530
+ }
4531
+ // Real API call
4532
+ const response = await fetch(`${this.baseUrl}/portal/${sessionId}/complete`, {
4533
+ method: 'POST',
4534
+ headers: {
4535
+ Authorization: `Bearer ${this.userToken?.accessToken || ''}`,
4536
+ 'Content-Type': 'application/json',
4537
+ },
4538
+ });
4539
+ if (response.ok) {
4540
+ console.log('[FinaticConnect] Session completed successfully');
4541
+ }
4542
+ else {
4543
+ console.warn('[FinaticConnect] Failed to complete session:', response.status);
4544
+ }
4545
+ }
4546
+ catch (error) {
4547
+ // Silent failure - don't throw errors during cleanup
4548
+ console.warn('[FinaticConnect] Session cleanup failed:', error);
4549
+ }
4550
+ }
4551
+ }
4552
+ FinaticConnect.instance = null;
4553
+
4554
+ class CoreTradingService {
4555
+ constructor(apiClient) {
4556
+ this.apiClient = apiClient;
4557
+ }
4558
+ async getAccounts(filter) {
4559
+ return this.getAllAccounts(filter);
4560
+ }
4561
+ async getOrders(filter) {
4562
+ return this.getAllOrders(filter);
4563
+ }
4564
+ async getPositions(filter) {
4565
+ return this.getAllPositions(filter);
4566
+ }
4567
+ async placeOrder(order) {
4568
+ const response = await this.apiClient.placeOrder(order);
4569
+ return response.data.orderId;
4570
+ }
4571
+ async cancelOrder(orderId) {
4572
+ const response = await this.apiClient.cancelOrder(orderId);
4573
+ return response.data.success;
4574
+ }
4575
+ async placeOptionsOrder(order) {
4576
+ const response = await this.apiClient.placeOptionsOrder(order);
4577
+ return response.data.orderId;
4578
+ }
4579
+ // Pagination methods
4580
+ async getAccountsPage(page = 1, perPage = 100, filter) {
4581
+ return this.apiClient.getBrokerAccountsPage(page, perPage, filter);
4582
+ }
4583
+ async getOrdersPage(page = 1, perPage = 100, filter) {
4584
+ return this.apiClient.getBrokerOrdersPage(page, perPage, filter);
4585
+ }
4586
+ async getPositionsPage(page = 1, perPage = 100, filter) {
4587
+ return this.apiClient.getBrokerPositionsPage(page, perPage, filter);
4588
+ }
4589
+ // Navigation methods
4590
+ async getNextAccountsPage(previousResult) {
4591
+ return this.apiClient.getNextPage(previousResult, (offset, limit) => this.apiClient.getBrokerAccountsPage(1, limit, { offset }));
4592
+ }
4593
+ async getNextOrdersPage(previousResult) {
4594
+ return this.apiClient.getNextPage(previousResult, (offset, limit) => this.apiClient.getBrokerOrdersPage(1, limit, { offset }));
4595
+ }
4596
+ async getNextPositionsPage(previousResult) {
4597
+ return this.apiClient.getNextPage(previousResult, (offset, limit) => this.apiClient.getBrokerPositionsPage(1, limit, { offset }));
4598
+ }
4599
+ // Get all pages convenience methods
4600
+ async getAllAccounts(filter) {
4601
+ const allData = [];
4602
+ let currentResult = await this.getAccountsPage(1, 100, filter);
4603
+ while (currentResult) {
4604
+ allData.push(...currentResult.data);
4605
+ if (!currentResult.hasNext)
4606
+ break;
4607
+ const nextResult = await this.getNextAccountsPage(currentResult);
4608
+ if (!nextResult)
4609
+ break;
4610
+ currentResult = nextResult;
4611
+ }
4612
+ return allData;
4613
+ }
4614
+ async getAllOrders(filter) {
4615
+ const allData = [];
4616
+ let currentResult = await this.getOrdersPage(1, 100, filter);
4617
+ while (currentResult) {
4618
+ allData.push(...currentResult.data);
4619
+ if (!currentResult.hasNext)
4620
+ break;
4621
+ const nextResult = await this.getNextOrdersPage(currentResult);
4622
+ if (!nextResult)
4623
+ break;
4624
+ currentResult = nextResult;
4625
+ }
4626
+ return allData;
4627
+ }
4628
+ async getAllPositions(filter) {
4629
+ const allData = [];
4630
+ let currentResult = await this.getPositionsPage(1, 100, filter);
4631
+ while (currentResult) {
4632
+ allData.push(...currentResult.data);
4633
+ if (!currentResult.hasNext)
4634
+ break;
4635
+ const nextResult = await this.getNextPositionsPage(currentResult);
4636
+ if (!nextResult)
4637
+ break;
4638
+ currentResult = nextResult;
4639
+ }
4640
+ return allData;
4641
+ }
4642
+ // Abstract convenience methods
4643
+ async getOpenPositions() {
4644
+ return this.getPositions({ position_status: 'open' });
4645
+ }
4646
+ async getFilledOrders() {
4647
+ return this.getOrders({ status: 'filled' });
4648
+ }
4649
+ async getPendingOrders() {
4650
+ return this.getOrders({ status: 'pending' });
4651
+ }
4652
+ async getActiveAccounts() {
4653
+ return this.getAccounts({ status: 'active' });
4654
+ }
4655
+ async getOrdersBySymbol(symbol) {
4656
+ return this.getOrders({ symbol });
4657
+ }
4658
+ async getPositionsBySymbol(symbol) {
4659
+ return this.getPositions({ symbol });
4660
+ }
4661
+ async getOrdersByBroker(brokerId) {
4662
+ return this.getOrders({ broker_id: brokerId });
4663
+ }
4664
+ async getPositionsByBroker(brokerId) {
4665
+ return this.getPositions({ broker_id: brokerId });
4666
+ }
4667
+ }
4668
+
4669
+ class CoreAnalyticsService {
4670
+ constructor(apiClient) {
4671
+ this.apiClient = apiClient;
4672
+ }
4673
+ async getPerformance() {
4674
+ const response = await this.apiClient.getPerformance();
4675
+ return response.data.performance;
4676
+ }
4677
+ async getDailyHistory() {
4678
+ const response = await this.apiClient.getDailyHistory();
4679
+ return response.data.history;
4680
+ }
4681
+ async getWeeklySnapshots() {
4682
+ const response = await this.apiClient.getWeeklySnapshots();
4683
+ return response.data.snapshots;
4684
+ }
4685
+ async getPortfolioDeltas() {
4686
+ const response = await this.apiClient.getPortfolioDeltas();
4687
+ return response.data.deltas;
4688
+ }
4689
+ async getUserLogs(userId) {
4690
+ const response = await this.apiClient.getUserLogs(userId);
4691
+ return response.data.logs;
4692
+ }
4693
+ }
4694
+
4695
+ class CorePortalService {
4696
+ constructor(apiClient) {
4697
+ this.apiClient = apiClient;
4698
+ this.iframe = null;
4699
+ }
4700
+ async createPortal(config) {
4701
+ // Create iframe
4702
+ this.iframe = document.createElement('iframe');
4703
+ this.iframe.style.display = 'none';
4704
+ document.body.appendChild(this.iframe);
4705
+ // Get portal content from API
4706
+ const response = await this.apiClient.getPortalContent();
4707
+ const content = response.data.content;
4708
+ // Set iframe content
4709
+ if (this.iframe.contentWindow) {
4710
+ this.iframe.contentWindow.document.open();
4711
+ this.iframe.contentWindow.document.write(content);
4712
+ this.iframe.contentWindow.document.close();
4713
+ }
4714
+ // Position and style iframe
4715
+ this.positionIframe(config);
4716
+ this.styleIframe(config);
4717
+ // Show iframe
4718
+ this.iframe.style.display = 'block';
4719
+ }
4720
+ async closePortal() {
4721
+ if (this.iframe) {
4722
+ this.iframe.remove();
4723
+ this.iframe = null;
4724
+ }
4725
+ }
4726
+ async updateTheme(theme) {
4727
+ if (this.iframe?.contentWindow) {
4728
+ this.iframe.contentWindow.postMessage({ type: 'update_theme', theme }, '*');
4729
+ }
4730
+ }
4731
+ async getBrokerAccounts() {
4732
+ const response = await this.apiClient.getBrokerAccounts();
4733
+ return response.data.accounts;
4734
+ }
4735
+ async linkBrokerAccount(brokerId) {
4736
+ await this.apiClient.connectBroker(brokerId);
4737
+ }
4738
+ async unlinkBrokerAccount(brokerId) {
4739
+ await this.apiClient.disconnectBroker(brokerId);
4740
+ }
4741
+ positionIframe(config) {
4742
+ if (!this.iframe)
4743
+ return;
4744
+ const position = config.position || 'center';
4745
+ const width = config.width || '600px';
4746
+ const height = config.height || '800px';
4747
+ this.iframe.style.width = width;
4748
+ this.iframe.style.height = height;
4749
+ switch (position) {
4750
+ case 'center':
4751
+ this.iframe.style.position = 'fixed';
4752
+ this.iframe.style.top = '50%';
4753
+ this.iframe.style.left = '50%';
4754
+ this.iframe.style.transform = 'translate(-50%, -50%)';
4755
+ break;
4756
+ case 'top':
4757
+ this.iframe.style.position = 'fixed';
4758
+ this.iframe.style.top = '0';
4759
+ this.iframe.style.left = '50%';
4760
+ this.iframe.style.transform = 'translateX(-50%)';
4761
+ break;
4762
+ case 'bottom':
4763
+ this.iframe.style.position = 'fixed';
4764
+ this.iframe.style.bottom = '0';
4765
+ this.iframe.style.left = '50%';
4766
+ this.iframe.style.transform = 'translateX(-50%)';
4767
+ break;
4768
+ // Note: 'left' and 'right' positions removed from PortalConfig interface
4769
+ // Default to center if invalid position is provided
4770
+ default:
4771
+ this.iframe.style.position = 'fixed';
4772
+ this.iframe.style.top = '50%';
4773
+ this.iframe.style.left = '50%';
4774
+ this.iframe.style.transform = 'translate(-50%, -50%)';
4775
+ break;
4776
+ }
4777
+ }
4778
+ styleIframe(config) {
4779
+ if (!this.iframe)
4780
+ return;
4781
+ // Apply z-index if specified
4782
+ if (config.zIndex) {
4783
+ this.iframe.style.zIndex = config.zIndex.toString();
4784
+ }
4785
+ }
4786
+ }
4787
+
4788
+ exports.ApiClient = ApiClient;
4789
+ exports.ApiError = ApiError;
4790
+ exports.AuthenticationError = AuthenticationError;
4791
+ exports.AuthorizationError = AuthorizationError;
4792
+ exports.BaseError = BaseError;
4793
+ exports.CompanyAccessError = CompanyAccessError;
4794
+ exports.CoreAnalyticsService = CoreAnalyticsService;
4795
+ exports.CorePortalService = CorePortalService;
4796
+ exports.CoreTradingService = CoreTradingService;
4797
+ exports.EventEmitter = EventEmitter;
4798
+ exports.FinaticConnect = FinaticConnect;
4799
+ exports.MockFactory = MockFactory;
4800
+ exports.NetworkError = NetworkError;
4801
+ exports.OrderError = OrderError;
4802
+ exports.OrderValidationError = OrderValidationError;
4803
+ exports.PaginatedResult = PaginatedResult;
4804
+ exports.RateLimitError = RateLimitError;
4805
+ exports.SecurityError = SecurityError;
4806
+ exports.SessionError = SessionError;
4807
+ exports.TokenError = TokenError;
4808
+ exports.ValidationError = ValidationError;
4809
+ exports.appendThemeToURL = appendThemeToURL;
4810
+ exports.createCustomThemeFromPreset = createCustomThemeFromPreset;
4811
+ exports.generatePortalThemeURL = generatePortalThemeURL;
4812
+ exports.getThemePreset = getThemePreset;
4813
+ exports.portalThemePresets = portalThemePresets;
4814
+ exports.validateCustomTheme = validateCustomTheme;
4815
+ //# sourceMappingURL=index.js.map