@finatic/client 0.0.139 → 0.0.141

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 (39) hide show
  1. package/README.md +335 -446
  2. package/dist/index.d.ts +272 -515
  3. package/dist/index.js +531 -449
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +532 -449
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/types/core/client/ApiClient.d.ts +81 -27
  8. package/dist/types/core/client/FinaticConnect.d.ts +53 -103
  9. package/dist/types/index.d.ts +1 -2
  10. package/dist/types/mocks/MockApiClient.d.ts +2 -4
  11. package/dist/types/mocks/utils.d.ts +0 -5
  12. package/dist/types/types/api/auth.d.ts +12 -30
  13. package/dist/types/types/api/broker.d.ts +117 -1
  14. package/package.json +7 -3
  15. package/src/core/client/ApiClient.ts +1978 -0
  16. package/src/core/client/FinaticConnect.ts +1557 -0
  17. package/src/core/portal/PortalUI.ts +300 -0
  18. package/src/index.d.ts +23 -0
  19. package/src/index.ts +99 -0
  20. package/src/mocks/MockApiClient.ts +1032 -0
  21. package/src/mocks/MockDataProvider.ts +986 -0
  22. package/src/mocks/MockFactory.ts +97 -0
  23. package/src/mocks/utils.ts +133 -0
  24. package/src/themes/portalPresets.ts +1307 -0
  25. package/src/types/api/auth.ts +112 -0
  26. package/src/types/api/broker.ts +461 -0
  27. package/src/types/api/core.ts +53 -0
  28. package/src/types/api/errors.ts +35 -0
  29. package/src/types/api/orders.ts +45 -0
  30. package/src/types/api/portfolio.ts +59 -0
  31. package/src/types/common/pagination.ts +138 -0
  32. package/src/types/connect.ts +56 -0
  33. package/src/types/index.ts +25 -0
  34. package/src/types/portal.ts +214 -0
  35. package/src/types/ui/theme.ts +105 -0
  36. package/src/utils/brokerUtils.ts +85 -0
  37. package/src/utils/errors.ts +104 -0
  38. package/src/utils/events.ts +54 -0
  39. package/src/utils/themeUtils.ts +146 -0
@@ -0,0 +1,1032 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ // Import types from the main API types
4
+ import {
5
+ SessionResponse,
6
+ OtpRequestResponse,
7
+ OtpVerifyResponse,
8
+ PortalUrlResponse,
9
+ SessionValidationResponse,
10
+ SessionAuthenticateResponse,
11
+ UserToken,
12
+ Order,
13
+ BrokerInfo,
14
+ BrokerAccount,
15
+ BrokerOrder,
16
+ BrokerPosition,
17
+ BrokerBalance,
18
+ BrokerConnection,
19
+ BrokerDataOptions,
20
+ BrokerOrderParams,
21
+ BrokerExtras,
22
+ CryptoOrderOptions,
23
+ OptionsOrderOptions,
24
+ OrderResponse,
25
+ TradingContext,
26
+ RefreshTokenRequest,
27
+ RefreshTokenResponse,
28
+ OrdersFilter,
29
+ PositionsFilter,
30
+ AccountsFilter,
31
+ BalancesFilter,
32
+ BrokerDataOrder,
33
+ BrokerDataPosition,
34
+ BrokerDataAccount,
35
+ DisconnectCompanyResponse,
36
+ } from '../types';
37
+ import { PaginatedResult } from '../types';
38
+ import { DeviceInfo, SessionState, TokenInfo } from '../types/api/auth';
39
+ import {
40
+ ApiError,
41
+ SessionError,
42
+ AuthenticationError,
43
+ AuthorizationError,
44
+ RateLimitError,
45
+ CompanyAccessError,
46
+ OrderError,
47
+ OrderValidationError,
48
+ } from '../utils/errors';
49
+ import { MockDataProvider, MockConfig } from './MockDataProvider';
50
+
51
+ /**
52
+ * Mock API Client that implements the same interface as the real ApiClient
53
+ * but returns mock data instead of making HTTP requests
54
+ */
55
+ export class MockApiClient {
56
+ private readonly baseUrl: string;
57
+ protected readonly deviceInfo?: DeviceInfo;
58
+ protected currentSessionState: SessionState | null = null;
59
+ protected currentSessionId: string | null = null;
60
+ private tradingContext: TradingContext = {};
61
+
62
+ // Token management
63
+ private tokenInfo: TokenInfo | null = null;
64
+ private refreshPromise: Promise<TokenInfo> | null = null;
65
+ private readonly REFRESH_BUFFER_MINUTES = 5;
66
+
67
+ // Session and company context
68
+ private companyId: string | null = null;
69
+ private csrfToken: string | null = null;
70
+
71
+ // Mock data provider
72
+ private mockDataProvider: MockDataProvider;
73
+ private readonly mockApiOnly: boolean;
74
+
75
+ constructor(baseUrl: string, deviceInfo?: DeviceInfo, mockConfig?: MockConfig) {
76
+ this.baseUrl = baseUrl;
77
+ this.deviceInfo = deviceInfo;
78
+ this.mockApiOnly = mockConfig?.mockApiOnly || false;
79
+ this.mockDataProvider = new MockDataProvider(mockConfig);
80
+
81
+ // Log that mocks are being used
82
+ if (this.mockApiOnly) {
83
+ console.log('🔧 Finatic SDK: Using MOCK API Client (API only - real portal)');
84
+ } else {
85
+ console.log('🔧 Finatic SDK: Using MOCK API Client');
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Store tokens after successful authentication
91
+ */
92
+ setTokens(accessToken: string, refreshToken: string, expiresAt: string, userId?: string): void {
93
+ this.tokenInfo = {
94
+ accessToken,
95
+ refreshToken,
96
+ expiresAt,
97
+ userId,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Get the current access token, refreshing if necessary
103
+ */
104
+ async getValidAccessToken(): Promise<string> {
105
+ if (!this.tokenInfo) {
106
+ throw new AuthenticationError('No tokens available. Please authenticate first.');
107
+ }
108
+
109
+ // Check if token is expired or about to expire
110
+ if (this.isTokenExpired()) {
111
+ await this.refreshTokens();
112
+ }
113
+
114
+ return this.tokenInfo.accessToken;
115
+ }
116
+
117
+ /**
118
+ * Check if the current token is expired or about to expire
119
+ */
120
+ private isTokenExpired(): boolean {
121
+ if (!this.tokenInfo) return true;
122
+
123
+ const expiryTime = new Date(this.tokenInfo.expiresAt).getTime();
124
+ const currentTime = Date.now();
125
+ const bufferTime = this.REFRESH_BUFFER_MINUTES * 60 * 1000;
126
+
127
+ return currentTime >= expiryTime - bufferTime;
128
+ }
129
+
130
+ /**
131
+ * Refresh the access token using the refresh token
132
+ */
133
+ private async refreshTokens(): Promise<void> {
134
+ if (!this.tokenInfo) {
135
+ throw new AuthenticationError('No refresh token available.');
136
+ }
137
+
138
+ // If a refresh is already in progress, wait for it
139
+ if (this.refreshPromise) {
140
+ await this.refreshPromise;
141
+ return;
142
+ }
143
+
144
+ // Start a new refresh
145
+ this.refreshPromise = this.performTokenRefresh();
146
+
147
+ try {
148
+ await this.refreshPromise;
149
+ } finally {
150
+ this.refreshPromise = null;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Perform the actual token refresh request
156
+ */
157
+ private async performTokenRefresh(): Promise<TokenInfo> {
158
+ if (!this.tokenInfo) {
159
+ throw new AuthenticationError('No refresh token available.');
160
+ }
161
+
162
+ try {
163
+ const response = await this.mockDataProvider.mockRefreshToken(this.tokenInfo.refreshToken);
164
+
165
+ // Update stored tokens
166
+ this.tokenInfo = {
167
+ accessToken: response.response_data.access_token,
168
+ refreshToken: response.response_data.refresh_token,
169
+ expiresAt: response.response_data.expires_at,
170
+ userId: this.tokenInfo.userId,
171
+ };
172
+
173
+ return this.tokenInfo;
174
+ } catch (error) {
175
+ // Clear tokens on refresh failure
176
+ this.tokenInfo = null;
177
+ throw new AuthenticationError(
178
+ 'Token refresh failed. Please re-authenticate.',
179
+ error as Record<string, any>
180
+ );
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Clear stored tokens (useful for logout)
186
+ */
187
+ clearTokens(): void {
188
+ this.tokenInfo = null;
189
+ this.refreshPromise = null;
190
+ }
191
+
192
+ /**
193
+ * Get current token info (for debugging/testing)
194
+ */
195
+ getTokenInfo(): TokenInfo | null {
196
+ return this.tokenInfo ? { ...this.tokenInfo } : null;
197
+ }
198
+
199
+ /**
200
+ * Set session context (session ID, company ID, CSRF token)
201
+ */
202
+ setSessionContext(sessionId: string, companyId: string, csrfToken?: string): void {
203
+ this.currentSessionId = sessionId;
204
+ this.companyId = companyId;
205
+ this.csrfToken = csrfToken || null;
206
+ }
207
+
208
+ /**
209
+ * Get the current session ID
210
+ */
211
+ getCurrentSessionId(): string | null {
212
+ return this.currentSessionId;
213
+ }
214
+
215
+ /**
216
+ * Get the current company ID
217
+ */
218
+ getCurrentCompanyId(): string | null {
219
+ return this.companyId;
220
+ }
221
+
222
+ /**
223
+ * Get the current CSRF token
224
+ */
225
+ getCurrentCsrfToken(): string | null {
226
+ return this.csrfToken;
227
+ }
228
+
229
+ // Session Management
230
+ async startSession(token: string, userId?: string): Promise<SessionResponse> {
231
+ const response = await this.mockDataProvider.mockStartSession(token, userId);
232
+
233
+ // Store session ID and set state to ACTIVE
234
+ this.currentSessionId = response.data.session_id;
235
+ this.currentSessionState = SessionState.ACTIVE;
236
+
237
+ return response;
238
+ }
239
+
240
+ // OTP Flow
241
+ async requestOtp(sessionId: string, email: string): Promise<OtpRequestResponse> {
242
+ return this.mockDataProvider.mockRequestOtp(sessionId, email);
243
+ }
244
+
245
+ async verifyOtp(sessionId: string, otp: string): Promise<OtpVerifyResponse> {
246
+ const response = await this.mockDataProvider.mockVerifyOtp(sessionId, otp);
247
+
248
+ // Store tokens after successful OTP verification
249
+ if (response.success && response.data) {
250
+ const expiresAt = new Date(Date.now() + response.data.expires_in * 1000).toISOString();
251
+ this.setTokens(
252
+ response.data.access_token,
253
+ response.data.refresh_token,
254
+ expiresAt,
255
+ response.data.user_id
256
+ );
257
+ }
258
+
259
+ return response;
260
+ }
261
+
262
+ // Direct Authentication
263
+ async authenticateDirectly(
264
+ sessionId: string,
265
+ userId: string
266
+ ): Promise<SessionAuthenticateResponse> {
267
+ // Ensure session is active before authenticating
268
+ if (this.currentSessionState !== SessionState.ACTIVE) {
269
+ throw new SessionError('Session must be in ACTIVE state to authenticate');
270
+ }
271
+
272
+ const response = await this.mockDataProvider.mockAuthenticateDirectly(sessionId, userId);
273
+
274
+ // Store tokens after successful direct authentication
275
+ if (response.success && response.data) {
276
+ // For direct auth, we don't get expires_in, so we'll set a default 1-hour expiry
277
+ const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); // 1 hour
278
+ this.setTokens(response.data.access_token, response.data.refresh_token, expiresAt, userId);
279
+ }
280
+
281
+ return response;
282
+ }
283
+
284
+ // Portal Management
285
+ async getPortalUrl(sessionId: string): Promise<PortalUrlResponse> {
286
+ if (this.currentSessionState !== SessionState.ACTIVE) {
287
+ throw new SessionError('Session must be in ACTIVE state to get portal URL');
288
+ }
289
+
290
+ // If in mockApiOnly mode, return the real portal URL
291
+ if (this.mockApiOnly) {
292
+ return {
293
+ success: true,
294
+ message: 'Portal URL retrieved successfully',
295
+ data: {
296
+ portal_url: 'http://localhost:5173/companies',
297
+ },
298
+ };
299
+ }
300
+
301
+ return this.mockDataProvider.mockGetPortalUrl(sessionId);
302
+ }
303
+
304
+ async validatePortalSession(
305
+ sessionId: string,
306
+ signature: string
307
+ ): Promise<SessionValidationResponse> {
308
+ return this.mockDataProvider.mockValidatePortalSession(sessionId, signature);
309
+ }
310
+
311
+ async completePortalSession(sessionId: string): Promise<PortalUrlResponse> {
312
+ return this.mockDataProvider.mockCompletePortalSession(sessionId);
313
+ }
314
+
315
+ // Portfolio Management
316
+
317
+ async getOrders(filter?: OrdersFilter): Promise<{ data: Order[] }> {
318
+ const accessToken = await this.getValidAccessToken();
319
+ return this.mockDataProvider.mockGetOrders(filter);
320
+ }
321
+
322
+ async placeOrder(order: BrokerOrderParams): Promise<void> {
323
+ const accessToken = await this.getValidAccessToken();
324
+ await this.mockDataProvider.mockPlaceOrder(order);
325
+ }
326
+
327
+ // Enhanced Trading Methods with Session Management
328
+ async placeBrokerOrder(
329
+ params: Partial<BrokerOrderParams> & {
330
+ symbol: string;
331
+ orderQty: number;
332
+ action: 'Buy' | 'Sell';
333
+ orderType: 'Market' | 'Limit' | 'Stop' | 'StopLimit';
334
+ assetType: 'equity' | 'equity_option' | 'crypto' | 'forex' | 'future' | 'future_option';
335
+ },
336
+ extras: BrokerExtras = {},
337
+ connection_id?: string
338
+ ): Promise<OrderResponse> {
339
+ const accessToken = await this.getValidAccessToken();
340
+
341
+ // Debug logging
342
+ console.log('MockApiClient.placeBrokerOrder Debug:', {
343
+ params,
344
+ tradingContext: this.tradingContext,
345
+ paramsBroker: params.broker,
346
+ contextBroker: this.tradingContext.broker,
347
+ paramsAccountNumber: params.accountNumber,
348
+ contextAccountNumber: this.tradingContext.accountNumber,
349
+ });
350
+
351
+ const fullParams: BrokerOrderParams = {
352
+ broker:
353
+ ((params.broker || this.tradingContext.broker) as
354
+ | 'robinhood'
355
+ | 'tasty_trade'
356
+ | 'ninja_trader') ||
357
+ (() => {
358
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
359
+ })(),
360
+ accountNumber:
361
+ params.accountNumber ||
362
+ this.tradingContext.accountNumber ||
363
+ (() => {
364
+ throw new Error('Account not set. Call setAccount() or pass accountNumber parameter.');
365
+ })(),
366
+ symbol: params.symbol,
367
+ orderQty: params.orderQty,
368
+ action: params.action,
369
+ orderType: params.orderType,
370
+ assetType: params.assetType,
371
+ timeInForce: params.timeInForce || 'day',
372
+ price: params.price,
373
+ stopPrice: params.stopPrice,
374
+ order_id: params.order_id,
375
+ };
376
+
377
+ console.log('MockApiClient.placeBrokerOrder Debug - Final params:', fullParams);
378
+ return this.mockDataProvider.mockPlaceOrder(fullParams);
379
+ }
380
+
381
+ async cancelBrokerOrder(
382
+ orderId: string,
383
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
384
+ extras: any = {},
385
+ connection_id?: string
386
+ ): Promise<OrderResponse> {
387
+ // Mock successful cancellation
388
+ return {
389
+ success: true,
390
+ response_data: {
391
+ orderId: orderId,
392
+ status: 'cancelled',
393
+ broker: broker || this.tradingContext.broker,
394
+ },
395
+ message: 'Order cancelled successfully',
396
+ status_code: 200,
397
+ };
398
+ }
399
+
400
+ async modifyBrokerOrder(
401
+ orderId: string,
402
+ params: Partial<BrokerOrderParams>,
403
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
404
+ extras: any = {},
405
+ connection_id?: string
406
+ ): Promise<OrderResponse> {
407
+ // Mock successful modification
408
+ return {
409
+ success: true,
410
+ response_data: {
411
+ orderId: orderId,
412
+ status: 'modified',
413
+ broker: broker || this.tradingContext.broker,
414
+ ...params,
415
+ },
416
+ message: 'Order modified successfully',
417
+ status_code: 200,
418
+ };
419
+ }
420
+
421
+ // Context management methods
422
+ setBroker(broker: 'robinhood' | 'tasty_trade' | 'ninja_trader'): void {
423
+ this.tradingContext.broker = broker;
424
+ // Clear account when broker changes
425
+ this.tradingContext.accountNumber = undefined;
426
+ this.tradingContext.accountId = undefined;
427
+ }
428
+
429
+ setAccount(accountNumber: string, accountId?: string): void {
430
+ console.log('MockApiClient.setAccount Debug:', {
431
+ accountNumber,
432
+ accountId,
433
+ previousContext: { ...this.tradingContext },
434
+ });
435
+ this.tradingContext.accountNumber = accountNumber;
436
+ this.tradingContext.accountId = accountId;
437
+ console.log('MockApiClient.setAccount Debug - Updated context:', this.tradingContext);
438
+ }
439
+
440
+
441
+ // Stock convenience methods
442
+ async placeStockMarketOrder(
443
+ symbol: string,
444
+ orderQty: number,
445
+ action: 'Buy' | 'Sell',
446
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
447
+ accountNumber?: string,
448
+ extras: BrokerExtras = {}
449
+ ): Promise<OrderResponse> {
450
+ return this.placeBrokerOrder(
451
+ {
452
+ broker,
453
+ accountNumber,
454
+ symbol,
455
+ orderQty,
456
+ action,
457
+ orderType: 'Market',
458
+ assetType: 'equity',
459
+ timeInForce: 'day',
460
+ },
461
+ extras
462
+ );
463
+ }
464
+
465
+ async placeStockLimitOrder(
466
+ symbol: string,
467
+ orderQty: number,
468
+ action: 'Buy' | 'Sell',
469
+ price: number,
470
+ timeInForce: 'day' | 'gtc' = 'gtc',
471
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
472
+ accountNumber?: string,
473
+ extras: BrokerExtras = {}
474
+ ): Promise<OrderResponse> {
475
+ return this.placeBrokerOrder(
476
+ {
477
+ broker,
478
+ accountNumber,
479
+ symbol,
480
+ orderQty,
481
+ action,
482
+ orderType: 'Limit',
483
+ assetType: 'equity',
484
+ timeInForce,
485
+ price,
486
+ },
487
+ extras
488
+ );
489
+ }
490
+
491
+ async placeStockStopOrder(
492
+ symbol: string,
493
+ orderQty: number,
494
+ action: 'Buy' | 'Sell',
495
+ stopPrice: number,
496
+ timeInForce: 'day' | 'gtc' = 'day',
497
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
498
+ accountNumber?: string,
499
+ extras: BrokerExtras = {}
500
+ ): Promise<OrderResponse> {
501
+ return this.placeBrokerOrder(
502
+ {
503
+ broker,
504
+ accountNumber,
505
+ symbol,
506
+ orderQty,
507
+ action,
508
+ orderType: 'Stop',
509
+ assetType: 'equity',
510
+ timeInForce,
511
+ stopPrice,
512
+ },
513
+ extras
514
+ );
515
+ }
516
+
517
+ // Crypto convenience methods
518
+ async placeCryptoMarketOrder(
519
+ symbol: string,
520
+ orderQty: number,
521
+ action: 'Buy' | 'Sell',
522
+ options: CryptoOrderOptions = {},
523
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
524
+ accountNumber?: string,
525
+ extras: BrokerExtras = {}
526
+ ): Promise<OrderResponse> {
527
+ const orderParams: Partial<BrokerOrderParams> & {
528
+ symbol: string;
529
+ orderQty: number;
530
+ action: 'Buy' | 'Sell';
531
+ orderType: 'Market';
532
+ assetType: 'crypto';
533
+ } = {
534
+ broker,
535
+ accountNumber,
536
+ symbol,
537
+ orderQty: options.quantity || orderQty,
538
+ action,
539
+ orderType: 'Market',
540
+ assetType: 'crypto',
541
+ timeInForce: 'gtc', // Crypto typically uses GTC
542
+ };
543
+
544
+ return this.placeBrokerOrder(orderParams, extras);
545
+ }
546
+
547
+ async placeCryptoLimitOrder(
548
+ symbol: string,
549
+ orderQty: number,
550
+ action: 'Buy' | 'Sell',
551
+ price: number,
552
+ timeInForce: 'day' | 'gtc' = 'gtc',
553
+ options: CryptoOrderOptions = {},
554
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
555
+ accountNumber?: string,
556
+ extras: BrokerExtras = {}
557
+ ): Promise<OrderResponse> {
558
+ const orderParams: Partial<BrokerOrderParams> & {
559
+ symbol: string;
560
+ orderQty: number;
561
+ action: 'Buy' | 'Sell';
562
+ orderType: 'Limit';
563
+ assetType: 'crypto';
564
+ } = {
565
+ broker,
566
+ accountNumber,
567
+ symbol,
568
+ orderQty: options.quantity || orderQty,
569
+ action,
570
+ orderType: 'Limit',
571
+ assetType: 'crypto',
572
+ timeInForce,
573
+ price,
574
+ };
575
+
576
+ return this.placeBrokerOrder(orderParams, extras);
577
+ }
578
+
579
+ // Options convenience methods
580
+ async placeOptionsMarketOrder(
581
+ symbol: string,
582
+ orderQty: number,
583
+ action: 'Buy' | 'Sell',
584
+ options: OptionsOrderOptions,
585
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
586
+ accountNumber?: string,
587
+ extras: BrokerExtras = {}
588
+ ): Promise<OrderResponse> {
589
+ const orderParams: Partial<BrokerOrderParams> & {
590
+ symbol: string;
591
+ orderQty: number;
592
+ action: 'Buy' | 'Sell';
593
+ orderType: 'Market';
594
+ assetType: 'equity_option';
595
+ } = {
596
+ broker,
597
+ accountNumber,
598
+ symbol,
599
+ orderQty,
600
+ action,
601
+ orderType: 'Market',
602
+ assetType: 'equity_option',
603
+ timeInForce: 'day',
604
+ };
605
+
606
+ return this.placeBrokerOrder(orderParams, extras);
607
+ }
608
+
609
+ async placeOptionsLimitOrder(
610
+ symbol: string,
611
+ orderQty: number,
612
+ action: 'Buy' | 'Sell',
613
+ price: number,
614
+ options: OptionsOrderOptions,
615
+ timeInForce: 'day' | 'gtc' = 'gtc',
616
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
617
+ accountNumber?: string,
618
+ extras: BrokerExtras = {}
619
+ ): Promise<OrderResponse> {
620
+ const orderParams: Partial<BrokerOrderParams> & {
621
+ symbol: string;
622
+ orderQty: number;
623
+ action: 'Buy' | 'Sell';
624
+ orderType: 'Limit';
625
+ assetType: 'equity_option';
626
+ } = {
627
+ broker,
628
+ accountNumber,
629
+ symbol,
630
+ orderQty,
631
+ action,
632
+ orderType: 'Limit',
633
+ assetType: 'equity_option',
634
+ timeInForce,
635
+ price,
636
+ };
637
+
638
+ return this.placeBrokerOrder(orderParams, extras);
639
+ }
640
+
641
+ // Futures convenience methods
642
+ async placeFuturesMarketOrder(
643
+ symbol: string,
644
+ orderQty: number,
645
+ action: 'Buy' | 'Sell',
646
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
647
+ accountNumber?: string,
648
+ extras: BrokerExtras = {}
649
+ ): Promise<OrderResponse> {
650
+ return this.placeBrokerOrder(
651
+ {
652
+ broker,
653
+ accountNumber,
654
+ symbol,
655
+ orderQty,
656
+ action,
657
+ orderType: 'Market',
658
+ assetType: 'future',
659
+ timeInForce: 'day',
660
+ },
661
+ extras
662
+ );
663
+ }
664
+
665
+ async placeFuturesLimitOrder(
666
+ symbol: string,
667
+ orderQty: number,
668
+ action: 'Buy' | 'Sell',
669
+ price: number,
670
+ timeInForce: 'day' | 'gtc' = 'gtc',
671
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
672
+ accountNumber?: string,
673
+ extras: BrokerExtras = {}
674
+ ): Promise<OrderResponse> {
675
+ return this.placeBrokerOrder(
676
+ {
677
+ broker,
678
+ accountNumber,
679
+ symbol,
680
+ orderQty,
681
+ action,
682
+ orderType: 'Limit',
683
+ assetType: 'future',
684
+ timeInForce,
685
+ price,
686
+ },
687
+ extras
688
+ );
689
+ }
690
+
691
+ async getUserToken(sessionId: string): Promise<UserToken> {
692
+ const token = this.mockDataProvider.getUserToken(sessionId);
693
+ if (!token) {
694
+ throw new AuthenticationError('User token not found');
695
+ }
696
+ return token;
697
+ }
698
+
699
+ getCurrentSessionState(): SessionState | null {
700
+ return this.currentSessionState;
701
+ }
702
+
703
+ // Broker Data Management
704
+ async getBrokerList(): Promise<{
705
+ _id: string;
706
+ response_data: BrokerInfo[];
707
+ message: string;
708
+ status_code: number;
709
+ warnings: null;
710
+ errors: null;
711
+ }> {
712
+ // Public in mock mode as well - no auth required
713
+ return this.mockDataProvider.mockGetBrokerList();
714
+ }
715
+
716
+ async getBrokerAccounts(options?: BrokerDataOptions): Promise<{
717
+ _id: string;
718
+ response_data: BrokerAccount[];
719
+ message: string;
720
+ status_code: number;
721
+ warnings: null;
722
+ errors: null;
723
+ }> {
724
+ const accessToken = await this.getValidAccessToken();
725
+ return this.mockDataProvider.mockGetBrokerAccounts();
726
+ }
727
+
728
+ async getBrokerOrders(options?: BrokerDataOptions): Promise<{
729
+ _id: string;
730
+ response_data: BrokerOrder[];
731
+ message: string;
732
+ status_code: number;
733
+ warnings: null;
734
+ errors: null;
735
+ }> {
736
+ const accessToken = await this.getValidAccessToken();
737
+ // Return empty orders for now - keeping original interface
738
+ return {
739
+ _id: uuidv4(),
740
+ response_data: [],
741
+ message: 'Broker orders retrieved successfully',
742
+ status_code: 200,
743
+ warnings: null,
744
+ errors: null,
745
+ };
746
+ }
747
+
748
+ async getBrokerPositions(options?: BrokerDataOptions): Promise<{
749
+ _id: string;
750
+ response_data: BrokerPosition[];
751
+ message: string;
752
+ status_code: number;
753
+ warnings: null;
754
+ errors: null;
755
+ }> {
756
+ const accessToken = await this.getValidAccessToken();
757
+ // Return empty positions for now - keeping original interface
758
+ return {
759
+ _id: uuidv4(),
760
+ response_data: [],
761
+ message: 'Broker positions retrieved successfully',
762
+ status_code: 200,
763
+ warnings: null,
764
+ errors: null,
765
+ };
766
+ }
767
+
768
+ // New broker data methods with filtering support
769
+ async getBrokerOrdersWithFilter(filter?: OrdersFilter): Promise<{ data: BrokerDataOrder[] }> {
770
+ return this.mockDataProvider.mockGetBrokerOrders(filter);
771
+ }
772
+
773
+ async getBrokerPositionsWithFilter(
774
+ filter?: PositionsFilter
775
+ ): Promise<{ data: BrokerDataPosition[] }> {
776
+ return this.mockDataProvider.mockGetBrokerPositions(filter);
777
+ }
778
+
779
+ async getBrokerBalancesWithFilter(filter?: BalancesFilter): Promise<{ data: BrokerBalance[] }> {
780
+ return this.mockDataProvider.mockGetBrokerBalances(filter);
781
+ }
782
+
783
+ async getBrokerDataAccountsWithFilter(
784
+ filter?: AccountsFilter
785
+ ): Promise<{ data: BrokerAccount[] }> {
786
+ return this.mockDataProvider.mockGetBrokerDataAccounts(filter);
787
+ }
788
+
789
+ // Page-based pagination methods
790
+ async getBrokerOrdersPage(
791
+ page: number = 1,
792
+ perPage: number = 100,
793
+ filters?: OrdersFilter
794
+ ): Promise<PaginatedResult<BrokerDataOrder[]>> {
795
+ const mockOrders = await this.mockDataProvider.mockGetBrokerOrders(filters);
796
+ const orders = mockOrders.data;
797
+
798
+ // Simulate pagination
799
+ const startIndex = (page - 1) * perPage;
800
+ const endIndex = startIndex + perPage;
801
+ const paginatedOrders = orders.slice(startIndex, endIndex);
802
+
803
+ const hasMore = endIndex < orders.length;
804
+ const nextOffset = hasMore ? endIndex : startIndex;
805
+
806
+ // Create navigation callback for mock pagination
807
+ const navigationCallback = async (
808
+ newOffset: number,
809
+ newLimit: number
810
+ ): Promise<PaginatedResult<BrokerDataOrder[]>> => {
811
+ const newStartIndex = newOffset;
812
+ const newEndIndex = newStartIndex + newLimit;
813
+ const newPaginatedOrders = orders.slice(newStartIndex, newEndIndex);
814
+ const newHasMore = newEndIndex < orders.length;
815
+ const newNextOffset = newHasMore ? newEndIndex : newStartIndex;
816
+
817
+ return new PaginatedResult(
818
+ newPaginatedOrders,
819
+ {
820
+ has_more: newHasMore,
821
+ next_offset: newNextOffset,
822
+ current_offset: newStartIndex,
823
+ limit: newLimit,
824
+ },
825
+ navigationCallback
826
+ );
827
+ };
828
+
829
+ return new PaginatedResult(
830
+ paginatedOrders,
831
+ {
832
+ has_more: hasMore,
833
+ next_offset: nextOffset,
834
+ current_offset: startIndex,
835
+ limit: perPage,
836
+ },
837
+ navigationCallback
838
+ );
839
+ }
840
+
841
+ async getBrokerAccountsPage(
842
+ page: number = 1,
843
+ perPage: number = 100,
844
+ filters?: AccountsFilter
845
+ ): Promise<PaginatedResult<BrokerAccount[]>> {
846
+ const mockAccounts = await this.mockDataProvider.mockGetBrokerDataAccounts(filters);
847
+ const accounts = mockAccounts.data;
848
+
849
+ // Simulate pagination
850
+ const startIndex = (page - 1) * perPage;
851
+ const endIndex = startIndex + perPage;
852
+ const paginatedAccounts = accounts.slice(startIndex, endIndex);
853
+
854
+ const hasMore = endIndex < accounts.length;
855
+ const nextOffset = hasMore ? endIndex : startIndex;
856
+
857
+ // Create navigation callback for mock pagination
858
+ const navigationCallback = async (
859
+ newOffset: number,
860
+ newLimit: number
861
+ ): Promise<PaginatedResult<BrokerAccount[]>> => {
862
+ const newStartIndex = newOffset;
863
+ const newEndIndex = newStartIndex + newLimit;
864
+ const newPaginatedAccounts = accounts.slice(newStartIndex, newEndIndex);
865
+ const newHasMore = newEndIndex < accounts.length;
866
+ const newNextOffset = newHasMore ? newEndIndex : newStartIndex;
867
+
868
+ return new PaginatedResult(
869
+ newPaginatedAccounts,
870
+ {
871
+ has_more: newHasMore,
872
+ next_offset: newNextOffset,
873
+ current_offset: newStartIndex,
874
+ limit: newLimit,
875
+ },
876
+ navigationCallback
877
+ );
878
+ };
879
+
880
+ return new PaginatedResult(
881
+ paginatedAccounts,
882
+ {
883
+ has_more: hasMore,
884
+ next_offset: nextOffset,
885
+ current_offset: startIndex,
886
+ limit: perPage,
887
+ },
888
+ navigationCallback
889
+ );
890
+ }
891
+
892
+ async getBrokerPositionsPage(
893
+ page: number = 1,
894
+ perPage: number = 100,
895
+ filters?: PositionsFilter
896
+ ): Promise<PaginatedResult<BrokerDataPosition[]>> {
897
+ const mockPositions = await this.mockDataProvider.mockGetBrokerPositions(filters);
898
+ const positions = mockPositions.data;
899
+
900
+ // Simulate pagination
901
+ const startIndex = (page - 1) * perPage;
902
+ const endIndex = startIndex + perPage;
903
+ const paginatedPositions = positions.slice(startIndex, endIndex);
904
+
905
+ const hasMore = endIndex < positions.length;
906
+ const nextOffset = hasMore ? endIndex : startIndex;
907
+
908
+ // Create navigation callback for mock pagination
909
+ const navigationCallback = async (
910
+ newOffset: number,
911
+ newLimit: number
912
+ ): Promise<PaginatedResult<BrokerDataPosition[]>> => {
913
+ const newStartIndex = newOffset;
914
+ const newEndIndex = newStartIndex + newLimit;
915
+ const newPaginatedPositions = positions.slice(newStartIndex, newEndIndex);
916
+ const newHasMore = newEndIndex < positions.length;
917
+ const newNextOffset = newHasMore ? newEndIndex : newStartIndex;
918
+
919
+ return new PaginatedResult(
920
+ newPaginatedPositions,
921
+ {
922
+ has_more: newHasMore,
923
+ next_offset: newNextOffset,
924
+ current_offset: newStartIndex,
925
+ limit: newLimit,
926
+ },
927
+ navigationCallback
928
+ );
929
+ };
930
+
931
+ return new PaginatedResult(
932
+ paginatedPositions,
933
+ {
934
+ has_more: hasMore,
935
+ next_offset: nextOffset,
936
+ current_offset: startIndex,
937
+ limit: perPage,
938
+ },
939
+ navigationCallback
940
+ );
941
+ }
942
+
943
+ async getBrokerBalancesPage(
944
+ page: number = 1,
945
+ perPage: number = 100,
946
+ filters?: BalancesFilter
947
+ ): Promise<PaginatedResult<BrokerBalance[]>> {
948
+ const mockBalances = await this.mockDataProvider.mockGetBrokerBalances(filters);
949
+ const balances = mockBalances.data;
950
+
951
+ // Simulate pagination
952
+ const startIndex = (page - 1) * perPage;
953
+ const endIndex = startIndex + perPage;
954
+ const paginatedBalances = balances.slice(startIndex, endIndex);
955
+
956
+ const hasMore = endIndex < balances.length;
957
+ const nextOffset = hasMore ? endIndex : startIndex;
958
+
959
+ // Create navigation callback for mock pagination
960
+ const navigationCallback = async (
961
+ newOffset: number,
962
+ newLimit: number
963
+ ): Promise<PaginatedResult<BrokerBalance[]>> => {
964
+ const newStartIndex = newOffset;
965
+ const newEndIndex = newStartIndex + newLimit;
966
+ const newPaginatedBalances = balances.slice(newStartIndex, newEndIndex);
967
+ const newHasMore = newEndIndex < balances.length;
968
+ const newNextOffset = newHasMore ? newEndIndex : newStartIndex;
969
+
970
+ return new PaginatedResult(
971
+ newPaginatedBalances,
972
+ {
973
+ has_more: newHasMore,
974
+ next_offset: newNextOffset,
975
+ current_offset: newStartIndex,
976
+ limit: newLimit,
977
+ },
978
+ navigationCallback
979
+ );
980
+ };
981
+
982
+ return new PaginatedResult(
983
+ paginatedBalances,
984
+ {
985
+ has_more: hasMore,
986
+ next_offset: nextOffset,
987
+ current_offset: startIndex,
988
+ limit: perPage,
989
+ },
990
+ navigationCallback
991
+ );
992
+ }
993
+
994
+ async getBrokerConnections(): Promise<{
995
+ _id: string;
996
+ response_data: BrokerConnection[];
997
+ message: string;
998
+ status_code: number;
999
+ warnings: null;
1000
+ errors: null;
1001
+ }> {
1002
+ const accessToken = await this.getValidAccessToken();
1003
+ return this.mockDataProvider.mockGetBrokerConnections();
1004
+ }
1005
+
1006
+ /**
1007
+ * Mock disconnect company method
1008
+ * @param connectionId - The connection ID to disconnect
1009
+ * @returns Promise with mock disconnect response
1010
+ */
1011
+ async disconnectCompany(connectionId: string): Promise<DisconnectCompanyResponse> {
1012
+ const accessToken = await this.getValidAccessToken();
1013
+ return this.mockDataProvider.mockDisconnectCompany(connectionId);
1014
+ }
1015
+
1016
+ // Utility methods for mock system
1017
+ getMockDataProvider(): MockDataProvider {
1018
+ return this.mockDataProvider;
1019
+ }
1020
+
1021
+ clearMockData(): void {
1022
+ this.mockDataProvider.clearData();
1023
+ }
1024
+
1025
+ /**
1026
+ * Check if this is a mock client
1027
+ * @returns true if this is a mock client
1028
+ */
1029
+ isMockClient(): boolean {
1030
+ return true;
1031
+ }
1032
+ }