@finatic/client 0.0.139 → 0.0.140

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 +278 -461
  2. package/dist/index.d.ts +55 -515
  3. package/dist/index.js +326 -456
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +327 -456
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/types/core/client/ApiClient.d.ts +12 -26
  8. package/dist/types/core/client/FinaticConnect.d.ts +20 -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 +1 -1
  14. package/package.json +7 -3
  15. package/src/core/client/ApiClient.ts +1721 -0
  16. package/src/core/client/FinaticConnect.ts +1476 -0
  17. package/src/core/portal/PortalUI.ts +300 -0
  18. package/src/index.d.ts +23 -0
  19. package/src/index.ts +87 -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 +330 -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,1721 @@
1
+ import { Order } from '../../types/api/orders';
2
+ import {
3
+ BrokerInfo,
4
+ BrokerAccount,
5
+ BrokerOrder,
6
+ BrokerPosition,
7
+ BrokerBalance,
8
+ BrokerDataOptions,
9
+ DisconnectCompanyResponse,
10
+ } from '../../types/api/broker';
11
+ import { BrokerOrderParams, BrokerExtras } from '../../types/api/broker';
12
+ import { CryptoOrderOptions, OptionsOrderOptions, OrderResponse } from '../../types/api/orders';
13
+ import { BrokerConnection } from '../../types/api/broker';
14
+ import {
15
+ OrdersFilter,
16
+ PositionsFilter,
17
+ AccountsFilter,
18
+ BalancesFilter,
19
+ } from '../../types/api/broker';
20
+ import { TradingContext } from '../../types/api/orders';
21
+ import { ApiPaginationInfo, PaginatedResult } from '../../types/common/pagination';
22
+ import { ApiResponse } from '../../types/api/core';
23
+ import { PortalUrlResponse } from '../../types/api/core';
24
+ import {
25
+ DeviceInfo,
26
+ SessionState,
27
+ TokenInfo,
28
+ SessionResponse,
29
+ SessionResponseData,
30
+ OtpRequestResponse,
31
+ OtpVerifyResponse,
32
+ SessionValidationResponse,
33
+ SessionAuthenticateResponse,
34
+ UserToken,
35
+ RefreshTokenRequest,
36
+ RefreshTokenResponse,
37
+ } from '../../types/api/auth';
38
+ import {
39
+ ApiError,
40
+ SessionError,
41
+ AuthenticationError,
42
+ AuthorizationError,
43
+ RateLimitError,
44
+ CompanyAccessError,
45
+ OrderError,
46
+ OrderValidationError,
47
+ TradingNotEnabledError,
48
+ } from '../../utils/errors';
49
+ // Supabase import removed - SDK no longer depends on Supabase
50
+
51
+ export class ApiClient {
52
+ private readonly baseUrl: string;
53
+ protected readonly deviceInfo?: DeviceInfo;
54
+ protected currentSessionState: SessionState | null = null;
55
+ protected currentSessionId: string | null = null;
56
+ private tradingContext: TradingContext = {};
57
+
58
+ // Session management (no Supabase needed)
59
+
60
+ // Session and company context
61
+ private companyId: string | null = null;
62
+ private csrfToken: string | null = null;
63
+
64
+ constructor(baseUrl: string, deviceInfo?: DeviceInfo) {
65
+ this.baseUrl = baseUrl;
66
+ this.deviceInfo = deviceInfo;
67
+ // Ensure baseUrl doesn't end with a slash
68
+ this.baseUrl = baseUrl.replace(/\/$/, '');
69
+ // Append /api/v1 if not already present
70
+ if (!this.baseUrl.includes('/api/v1')) {
71
+ this.baseUrl = `${this.baseUrl}/api/v1`;
72
+ }
73
+
74
+ // No Supabase initialization needed - SDK is clean
75
+ }
76
+
77
+ // Supabase initialization removed - SDK no longer depends on Supabase
78
+
79
+ /**
80
+ * Set session context (session ID, company ID, CSRF token)
81
+ */
82
+ setSessionContext(sessionId: string, companyId: string, csrfToken?: string): void {
83
+ this.currentSessionId = sessionId;
84
+ this.companyId = companyId;
85
+ this.csrfToken = csrfToken || null;
86
+ }
87
+
88
+ /**
89
+ * Get the current session ID
90
+ */
91
+ getCurrentSessionId(): string | null {
92
+ return this.currentSessionId;
93
+ }
94
+
95
+ /**
96
+ * Get the current company ID
97
+ */
98
+ getCurrentCompanyId(): string | null {
99
+ return this.companyId;
100
+ }
101
+
102
+ /**
103
+ * Get the current CSRF token
104
+ */
105
+ getCurrentCsrfToken(): string | null {
106
+ return this.csrfToken;
107
+ }
108
+
109
+ /**
110
+ * Get a valid access token (session-based auth - no tokens needed)
111
+ */
112
+ async getValidAccessToken(): Promise<string> {
113
+ // Session-based auth - return empty token as we use session headers
114
+ return '';
115
+ }
116
+
117
+ // Token expiration check removed - session-based auth doesn't use expiring tokens
118
+
119
+ // Supabase refresh method removed - SDK no longer uses Supabase tokens
120
+
121
+ /**
122
+ * Perform the actual Supabase session refresh
123
+ */
124
+ // Supabase refresh method removed - SDK no longer uses Supabase tokens
125
+
126
+ /**
127
+ * Clear session tokens (useful for logout)
128
+ */
129
+ clearTokens(): void {
130
+ // Session-based auth - no tokens to clear
131
+ }
132
+
133
+ /**
134
+ * Get current session info (for debugging/testing) - session-based auth
135
+ */
136
+ getTokenInfo(): { accessToken: string; refreshToken: string; expiresAt: number } | null {
137
+ // Session-based auth - no tokens to return
138
+ return null;
139
+ }
140
+
141
+ /**
142
+ * Make a request to the API.
143
+ */
144
+ protected async request<T>(
145
+ path: string,
146
+ options: {
147
+ method: string;
148
+ headers?: Record<string, string>;
149
+ body?: any;
150
+ params?: Record<string, string>;
151
+ }
152
+ ): Promise<T> {
153
+ // Ensure path starts with a slash
154
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
155
+ const url = new URL(`${this.baseUrl}${normalizedPath}`);
156
+
157
+ if (options.params) {
158
+ Object.entries(options.params).forEach(([key, value]) => {
159
+ url.searchParams.append(key, value);
160
+ });
161
+ }
162
+
163
+ // Get Supabase JWT token
164
+ const accessToken = await this.getValidAccessToken();
165
+
166
+ // Build comprehensive headers object with all available session data
167
+ const comprehensiveHeaders: Record<string, string> = {
168
+ 'Content-Type': 'application/json',
169
+ Authorization: `Bearer ${accessToken}`,
170
+ };
171
+
172
+ // Add device info if available
173
+ if (this.deviceInfo) {
174
+ comprehensiveHeaders['X-Device-Info'] = JSON.stringify({
175
+ ip_address: this.deviceInfo.ip_address || '',
176
+ user_agent: this.deviceInfo.user_agent || '',
177
+ fingerprint: this.deviceInfo.fingerprint || '',
178
+ });
179
+ }
180
+
181
+ // Add session headers if available (filter out empty values)
182
+ if (this.currentSessionId && this.currentSessionId.trim() !== '') {
183
+ comprehensiveHeaders['X-Session-ID'] = this.currentSessionId;
184
+ comprehensiveHeaders['Session-ID'] = this.currentSessionId;
185
+ }
186
+
187
+ if (this.companyId && this.companyId.trim() !== '') {
188
+ comprehensiveHeaders['X-Company-ID'] = this.companyId;
189
+ }
190
+
191
+ if (this.csrfToken && this.csrfToken.trim() !== '') {
192
+ comprehensiveHeaders['X-CSRF-Token'] = this.csrfToken;
193
+ }
194
+
195
+ // Add any additional headers from options (these will override defaults)
196
+ if (options.headers) {
197
+ Object.entries(options.headers).forEach(([key, value]) => {
198
+ if (value !== undefined && value !== null && value.trim() !== '') {
199
+ comprehensiveHeaders[key] = value;
200
+ }
201
+ });
202
+ }
203
+
204
+ // Safari-specific fix: Ensure all headers are explicitly set and not empty
205
+ // Safari can be strict about header formatting and empty values
206
+ const safariSafeHeaders: Record<string, string> = {};
207
+ Object.entries(comprehensiveHeaders).forEach(([key, value]) => {
208
+ if (value && value.trim() !== '') {
209
+ // Ensure header names are properly formatted for Safari
210
+ const normalizedKey = key.trim();
211
+ const normalizedValue = value.trim();
212
+ safariSafeHeaders[normalizedKey] = normalizedValue;
213
+ }
214
+ });
215
+
216
+ // Debug logging for development
217
+ if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
218
+ console.log('Request to:', url.toString());
219
+ console.log('Safari-safe headers:', safariSafeHeaders);
220
+ console.log('Browser:', navigator.userAgent);
221
+ console.log('Session ID:', this.currentSessionId);
222
+ console.log('Company ID:', this.companyId);
223
+ console.log('CSRF Token:', this.csrfToken);
224
+ }
225
+
226
+ const response = await fetch(url.toString(), {
227
+ method: options.method,
228
+ headers: safariSafeHeaders,
229
+ body: options.body ? JSON.stringify(options.body) : undefined,
230
+ });
231
+
232
+ // Debug logging for response
233
+ if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
234
+ console.log('Response status:', response.status);
235
+ console.log('Response headers:', Object.fromEntries(response.headers.entries()));
236
+ console.log('Request was made with headers:', safariSafeHeaders);
237
+ }
238
+
239
+ if (!response.ok) {
240
+ const error = await response.json();
241
+ throw this.handleError(response.status, error);
242
+ }
243
+
244
+ const data = await response.json();
245
+
246
+ // Check if the response has a success field and it's false
247
+ if (data && typeof data === 'object' && 'success' in data && data.success === false) {
248
+ // For order endpoints, provide more context
249
+ const isOrderEndpoint = path.includes('/brokers/orders');
250
+ if (isOrderEndpoint) {
251
+ // Add context that this is an order-related error
252
+ data._isOrderError = true;
253
+ }
254
+ throw this.handleError(data.status_code || 500, data);
255
+ }
256
+
257
+ // Check if the response has a status_code field indicating an error (4xx or 5xx)
258
+ if (data && typeof data === 'object' && 'status_code' in data && data.status_code >= 400) {
259
+ throw this.handleError(data.status_code, data);
260
+ }
261
+
262
+ // Check if the response has errors field with content
263
+ if (
264
+ data &&
265
+ typeof data === 'object' &&
266
+ 'errors' in data &&
267
+ data.errors &&
268
+ Array.isArray(data.errors) &&
269
+ data.errors.length > 0
270
+ ) {
271
+ throw this.handleError(data.status_code || 500, data);
272
+ }
273
+
274
+ return data;
275
+ }
276
+
277
+ /**
278
+ * Handle API errors. This method can be overridden by language-specific implementations.
279
+ */
280
+ protected handleError(status: number, error: any): ApiError {
281
+ // Extract message from the error object with multiple fallback options
282
+ let message = 'API request failed';
283
+
284
+ if (error && typeof error === 'object') {
285
+ // Try different possible message fields
286
+ message =
287
+ error.message ||
288
+ error.detail?.message ||
289
+ error.error?.message ||
290
+ error.errors?.[0]?.message ||
291
+ (typeof error.errors === 'string' ? error.errors : null) ||
292
+ 'API request failed';
293
+ }
294
+
295
+ // Check if this is an order-related error (either from order endpoints or order validation)
296
+ const isOrderError =
297
+ error._isOrderError ||
298
+ message.includes('ORDER_FAILED') ||
299
+ message.includes('AUTH_ERROR') ||
300
+ message.includes('not found or not available for trading') ||
301
+ message.includes('Symbol') ||
302
+ (error.errors &&
303
+ Array.isArray(error.errors) &&
304
+ error.errors.some(
305
+ (e: any) =>
306
+ e.category === 'INVALID_ORDER' ||
307
+ e.message?.includes('Symbol') ||
308
+ e.message?.includes('not available for trading')
309
+ ));
310
+
311
+ if (isOrderError) {
312
+ // Check if this is a validation error (400 status with specific validation messages)
313
+ const isValidationError =
314
+ status === 400 &&
315
+ (message.includes('not found or not available for trading') ||
316
+ message.includes('Symbol') ||
317
+ (error.errors &&
318
+ Array.isArray(error.errors) &&
319
+ error.errors.some(
320
+ (e: any) =>
321
+ e.category === 'INVALID_ORDER' ||
322
+ e.message?.includes('Symbol') ||
323
+ e.message?.includes('not available for trading')
324
+ )));
325
+
326
+ if (isValidationError) {
327
+ return new OrderValidationError(message, error);
328
+ }
329
+
330
+ // For order placement errors, provide more specific error messages
331
+ if (message.includes('ORDER_FAILED') || message.includes('AUTH_ERROR')) {
332
+ // Extract the specific error from the nested structure
333
+ const orderErrorMatch = message.match(/\[([^\]]+)\]/g);
334
+ if (orderErrorMatch && orderErrorMatch.length > 0) {
335
+ // Take the last error in the chain (most specific)
336
+ const specificError = orderErrorMatch[orderErrorMatch.length - 1].replace(/[[\]]/g, '');
337
+ message = `Order failed: ${specificError}`;
338
+ }
339
+ }
340
+
341
+ // Use OrderError for order-related failures
342
+ return new OrderError(message, error);
343
+ }
344
+
345
+ switch (status) {
346
+ case 400:
347
+ return new SessionError(message, error);
348
+ case 401:
349
+ return new AuthenticationError(
350
+ message || 'Unauthorized: Invalid or missing session token',
351
+ error
352
+ );
353
+ case 403:
354
+ if (error.detail?.code === 'NO_COMPANY_ACCESS') {
355
+ return new CompanyAccessError(
356
+ error.detail.message || 'No broker connections found for this company',
357
+ error.detail
358
+ );
359
+ }
360
+ if (error.detail?.code === 'TRADING_NOT_ENABLED') {
361
+ return new TradingNotEnabledError(
362
+ error.detail.message || 'Trading is not enabled for your company',
363
+ error.detail
364
+ );
365
+ }
366
+ return new AuthorizationError(
367
+ message || 'Forbidden: No access to the requested data',
368
+ error
369
+ );
370
+ case 404:
371
+ return new ApiError(
372
+ status,
373
+ message || 'Not found: The requested data does not exist',
374
+ error
375
+ );
376
+ case 429:
377
+ return new RateLimitError(message || 'Rate limit exceeded', error);
378
+ case 500:
379
+ return new ApiError(status, message || 'Internal server error', error);
380
+ default:
381
+ return new ApiError(status, message || 'API request failed', error);
382
+ }
383
+ }
384
+
385
+ // Session Management
386
+ async startSession(token: string, userId?: string): Promise<SessionResponse> {
387
+ const response = await this.request<SessionResponse>('/session/start', {
388
+ method: 'POST',
389
+ headers: {
390
+ 'Content-Type': 'application/json',
391
+ 'One-Time-Token': token,
392
+ 'X-Device-Info': JSON.stringify({
393
+ ip_address: this.deviceInfo?.ip_address || '',
394
+ user_agent: this.deviceInfo?.user_agent || '',
395
+ fingerprint: this.deviceInfo?.fingerprint || '',
396
+ }),
397
+ },
398
+ body: {
399
+ user_id: userId,
400
+ },
401
+ });
402
+
403
+ // Store session ID and set state to ACTIVE
404
+ this.currentSessionId = response.data.session_id;
405
+ this.currentSessionState = SessionState.ACTIVE;
406
+
407
+ return response;
408
+ }
409
+
410
+ // OTP Flow
411
+ async requestOtp(sessionId: string, email: string): Promise<OtpRequestResponse> {
412
+ return this.request<OtpRequestResponse>('/auth/otp/request', {
413
+ method: 'POST',
414
+ headers: {
415
+ 'Content-Type': 'application/json',
416
+ 'X-Session-ID': sessionId,
417
+ 'X-Device-Info': JSON.stringify({
418
+ ip_address: this.deviceInfo?.ip_address || '',
419
+ user_agent: this.deviceInfo?.user_agent || '',
420
+ fingerprint: this.deviceInfo?.fingerprint || '',
421
+ }),
422
+ },
423
+ body: {
424
+ email,
425
+ },
426
+ });
427
+ }
428
+
429
+ async verifyOtp(sessionId: string, otp: string): Promise<OtpVerifyResponse> {
430
+ const response = await this.request<OtpVerifyResponse>('/auth/otp/verify', {
431
+ method: 'POST',
432
+ headers: {
433
+ 'Content-Type': 'application/json',
434
+ 'X-Session-ID': sessionId,
435
+ 'X-Device-Info': JSON.stringify({
436
+ ip_address: this.deviceInfo?.ip_address || '',
437
+ user_agent: this.deviceInfo?.user_agent || '',
438
+ fingerprint: this.deviceInfo?.fingerprint || '',
439
+ }),
440
+ },
441
+ body: {
442
+ otp,
443
+ },
444
+ });
445
+
446
+ // OTP verification successful - tokens are handled by Supabase client
447
+ if (response.success && response.data) {
448
+ // Note: Supabase JWT will be handled by the Supabase client
449
+ // The backend now creates/retrieves Supabase users and returns session info
450
+ }
451
+
452
+ return response;
453
+ }
454
+
455
+ // Direct Authentication
456
+ async authenticateDirectly(
457
+ sessionId: string,
458
+ userId: string
459
+ ): Promise<SessionAuthenticateResponse> {
460
+ // Ensure session is active before authenticating
461
+ if (this.currentSessionState !== SessionState.ACTIVE) {
462
+ throw new SessionError('Session must be in ACTIVE state to authenticate');
463
+ }
464
+
465
+ const response = await this.request<SessionAuthenticateResponse>('/session/authenticate', {
466
+ method: 'POST',
467
+ headers: {
468
+ 'Content-Type': 'application/json',
469
+ 'Session-ID': sessionId,
470
+ 'X-Session-ID': sessionId,
471
+ 'X-Device-Info': JSON.stringify({
472
+ ip_address: this.deviceInfo?.ip_address || '',
473
+ user_agent: this.deviceInfo?.user_agent || '',
474
+ fingerprint: this.deviceInfo?.fingerprint || '',
475
+ }),
476
+ },
477
+ body: {
478
+ session_id: sessionId,
479
+ user_id: userId,
480
+ },
481
+ });
482
+
483
+ // Store tokens after successful direct authentication
484
+ if (response.success && response.data) {
485
+ // Session-based auth - no token storage needed
486
+ }
487
+
488
+ return response;
489
+ }
490
+
491
+ // Portal Management
492
+ /**
493
+ * Get the portal URL for an active session
494
+ * @param sessionId The session identifier
495
+ * @returns Portal URL response
496
+ * @throws SessionError if session is not in ACTIVE state
497
+ */
498
+ public async getPortalUrl(sessionId: string): Promise<PortalUrlResponse> {
499
+ if (this.currentSessionState !== SessionState.ACTIVE) {
500
+ throw new SessionError('Session must be in ACTIVE state to get portal URL');
501
+ }
502
+
503
+ return this.request<PortalUrlResponse>('/session/portal', {
504
+ method: 'GET',
505
+ headers: {
506
+ 'Content-Type': 'application/json',
507
+ 'Session-ID': sessionId,
508
+ 'X-Session-ID': sessionId,
509
+ 'X-Device-Info': JSON.stringify({
510
+ ip_address: this.deviceInfo?.ip_address || '',
511
+ user_agent: this.deviceInfo?.user_agent || '',
512
+ fingerprint: this.deviceInfo?.fingerprint || '',
513
+ }),
514
+ },
515
+ });
516
+ }
517
+
518
+ async completePortalSession(sessionId: string): Promise<PortalUrlResponse> {
519
+ return this.request<PortalUrlResponse>(`/portal/${sessionId}/complete`, {
520
+ method: 'POST',
521
+ headers: {
522
+ 'Content-Type': 'application/json',
523
+ },
524
+ });
525
+ }
526
+
527
+ // Portfolio Management
528
+
529
+ async getOrders(): Promise<{ data: Order[] }> {
530
+ const accessToken = await this.getValidAccessToken();
531
+ return this.request<{ data: Order[] }>('/brokers/data/orders', {
532
+ method: 'GET',
533
+ headers: {
534
+ Authorization: `Bearer ${accessToken}`,
535
+ },
536
+ });
537
+ }
538
+
539
+ // Enhanced Trading Methods with Session Management
540
+ async placeBrokerOrder(
541
+ params: Partial<BrokerOrderParams> & {
542
+ symbol: string;
543
+ orderQty: number;
544
+ action: 'Buy' | 'Sell';
545
+ orderType: 'Market' | 'Limit' | 'Stop' | 'StopLimit';
546
+ assetType: 'equity' | 'equity_option' | 'crypto' | 'forex' | 'future' | 'future_option';
547
+ },
548
+ extras: BrokerExtras = {},
549
+ connection_id?: string
550
+ ): Promise<OrderResponse> {
551
+ const accessToken = await this.getValidAccessToken();
552
+
553
+ // Get broker and account from context or params
554
+ const broker = params.broker || this.tradingContext.broker;
555
+ const accountNumber = params.accountNumber || this.tradingContext.accountNumber;
556
+
557
+ if (!broker) {
558
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
559
+ }
560
+
561
+ if (!accountNumber) {
562
+ throw new Error('Account not set. Call setAccount() or pass accountNumber parameter.');
563
+ }
564
+
565
+ // Merge context with provided parameters
566
+ const fullParams: BrokerOrderParams = {
567
+ broker:
568
+ ((params.broker || this.tradingContext.broker) as
569
+ | 'robinhood'
570
+ | 'tasty_trade'
571
+ | 'ninja_trader') ||
572
+ (() => {
573
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
574
+ })(),
575
+ accountNumber:
576
+ params.accountNumber ||
577
+ this.tradingContext.accountNumber ||
578
+ (() => {
579
+ throw new Error('Account not set. Call setAccount() or pass accountNumber parameter.');
580
+ })(),
581
+ symbol: params.symbol,
582
+ orderQty: params.orderQty,
583
+ action: params.action,
584
+ orderType: params.orderType,
585
+ assetType: params.assetType,
586
+ timeInForce: params.timeInForce || 'day',
587
+ price: params.price,
588
+ stopPrice: params.stopPrice,
589
+ order_id: params.order_id,
590
+ };
591
+
592
+ // Build request body with camelCase parameter names
593
+ const requestBody = this.buildOrderRequestBody(fullParams, extras);
594
+
595
+ // Add query parameters if connection_id is provided
596
+ const queryParams: Record<string, string> = {};
597
+ if (connection_id) {
598
+ queryParams.connection_id = connection_id;
599
+ }
600
+
601
+ return this.request<OrderResponse>('/brokers/orders', {
602
+ method: 'POST',
603
+ headers: {
604
+ 'Content-Type': 'application/json',
605
+ Authorization: `Bearer ${accessToken}`,
606
+ 'Session-ID': this.currentSessionId || '',
607
+ 'X-Session-ID': this.currentSessionId || '',
608
+ 'X-Device-Info': JSON.stringify(this.deviceInfo),
609
+ },
610
+ body: requestBody,
611
+ params: queryParams,
612
+ });
613
+ }
614
+
615
+ async cancelBrokerOrder(
616
+ orderId: string,
617
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
618
+ extras: any = {},
619
+ connection_id?: string
620
+ ): Promise<OrderResponse> {
621
+ const accessToken = await this.getValidAccessToken();
622
+
623
+ const selectedBroker = broker || this.tradingContext.broker;
624
+ if (!selectedBroker) {
625
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
626
+ }
627
+
628
+ const accountNumber = this.tradingContext.accountNumber;
629
+
630
+ // Build query parameters as required by API documentation
631
+ const queryParams: Record<string, string> = {};
632
+
633
+ // Add optional parameters if available
634
+ if (accountNumber) {
635
+ queryParams.account_number = accountNumber.toString();
636
+ }
637
+ if (connection_id) {
638
+ queryParams.connection_id = connection_id;
639
+ }
640
+
641
+ // Build optional request body if extras are provided
642
+ let body: any = undefined;
643
+ if (Object.keys(extras).length > 0) {
644
+ body = {
645
+ broker: selectedBroker,
646
+ order: {
647
+ order_id: orderId,
648
+ account_number: accountNumber,
649
+ ...extras,
650
+ },
651
+ };
652
+ }
653
+
654
+ return this.request<OrderResponse>(`/brokers/orders/${orderId}`, {
655
+ method: 'DELETE',
656
+ headers: {
657
+ 'Content-Type': 'application/json',
658
+ 'Session-ID': this.currentSessionId || '',
659
+ 'X-Session-ID': this.currentSessionId || '',
660
+ 'X-Device-Info': JSON.stringify(this.deviceInfo),
661
+ },
662
+ body,
663
+ params: queryParams,
664
+ });
665
+ }
666
+
667
+ async modifyBrokerOrder(
668
+ orderId: string,
669
+ params: Partial<BrokerOrderParams>,
670
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
671
+ extras: any = {},
672
+ connection_id?: string
673
+ ): Promise<OrderResponse> {
674
+ const accessToken = await this.getValidAccessToken();
675
+
676
+ const selectedBroker = broker || this.tradingContext.broker;
677
+ if (!selectedBroker) {
678
+ throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
679
+ }
680
+
681
+ // Build request body with camelCase parameter names and include broker
682
+ const requestBody = this.buildModifyRequestBody(params, extras, selectedBroker);
683
+
684
+ // Add query parameters if connection_id is provided
685
+ const queryParams: Record<string, string> = {};
686
+ if (connection_id) {
687
+ queryParams.connection_id = connection_id;
688
+ }
689
+
690
+ return this.request<OrderResponse>(`/brokers/orders/${orderId}`, {
691
+ method: 'PATCH',
692
+ headers: {
693
+ 'Content-Type': 'application/json',
694
+ 'Session-ID': this.currentSessionId || '',
695
+ 'X-Session-ID': this.currentSessionId || '',
696
+ 'X-Device-Info': JSON.stringify(this.deviceInfo),
697
+ },
698
+ body: requestBody,
699
+ params: queryParams,
700
+ });
701
+ }
702
+
703
+ // Context management methods
704
+ setBroker(broker: 'robinhood' | 'tasty_trade' | 'ninja_trader'): void {
705
+ this.tradingContext.broker = broker;
706
+ // Clear account when broker changes
707
+ this.tradingContext.accountNumber = undefined;
708
+ this.tradingContext.accountId = undefined;
709
+ }
710
+
711
+ setAccount(accountNumber: string, accountId?: string): void {
712
+ this.tradingContext.accountNumber = accountNumber;
713
+ this.tradingContext.accountId = accountId;
714
+ }
715
+
716
+
717
+ // Stock convenience methods
718
+ async placeStockMarketOrder(
719
+ symbol: string,
720
+ orderQty: number,
721
+ action: 'Buy' | 'Sell',
722
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
723
+ accountNumber?: string,
724
+ extras: BrokerExtras = {},
725
+ connection_id?: string
726
+ ): Promise<OrderResponse> {
727
+ return this.placeBrokerOrder(
728
+ {
729
+ symbol,
730
+ orderQty,
731
+ action,
732
+ orderType: 'Market',
733
+ assetType: 'equity',
734
+ timeInForce: 'day',
735
+ broker,
736
+ accountNumber,
737
+ },
738
+ extras,
739
+ connection_id
740
+ );
741
+ }
742
+
743
+ async placeStockLimitOrder(
744
+ symbol: string,
745
+ orderQty: number,
746
+ action: 'Buy' | 'Sell',
747
+ price: number,
748
+ timeInForce: 'day' | 'gtc' = 'gtc',
749
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
750
+ accountNumber?: string,
751
+ extras: BrokerExtras = {},
752
+ connection_id?: string
753
+ ): Promise<OrderResponse> {
754
+ return this.placeBrokerOrder(
755
+ {
756
+ symbol,
757
+ orderQty,
758
+ action,
759
+ orderType: 'Limit',
760
+ assetType: 'equity',
761
+ price,
762
+ timeInForce,
763
+ broker,
764
+ accountNumber,
765
+ },
766
+ extras,
767
+ connection_id
768
+ );
769
+ }
770
+
771
+ async placeStockStopOrder(
772
+ symbol: string,
773
+ orderQty: number,
774
+ action: 'Buy' | 'Sell',
775
+ stopPrice: number,
776
+ timeInForce: 'day' | 'gtc' = 'day',
777
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
778
+ accountNumber?: string,
779
+ extras: BrokerExtras = {},
780
+ connection_id?: string
781
+ ): Promise<OrderResponse> {
782
+ return this.placeBrokerOrder(
783
+ {
784
+ symbol,
785
+ orderQty,
786
+ action,
787
+ orderType: 'Stop',
788
+ assetType: 'equity',
789
+ stopPrice,
790
+ timeInForce,
791
+ broker,
792
+ accountNumber,
793
+ },
794
+ extras,
795
+ connection_id
796
+ );
797
+ }
798
+
799
+ // Crypto convenience methods
800
+ async placeCryptoMarketOrder(
801
+ symbol: string,
802
+ orderQty: number,
803
+ action: 'Buy' | 'Sell',
804
+ options: CryptoOrderOptions = {},
805
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
806
+ accountNumber?: string,
807
+ extras: BrokerExtras = {}
808
+ ): Promise<OrderResponse> {
809
+ return this.placeBrokerOrder(
810
+ {
811
+ symbol,
812
+ orderQty,
813
+ action,
814
+ orderType: 'Market',
815
+ assetType: 'crypto',
816
+ timeInForce: 'day',
817
+ broker,
818
+ accountNumber,
819
+ ...options,
820
+ },
821
+ extras
822
+ );
823
+ }
824
+
825
+ async placeCryptoLimitOrder(
826
+ symbol: string,
827
+ orderQty: number,
828
+ action: 'Buy' | 'Sell',
829
+ price: number,
830
+ timeInForce: 'day' | 'gtc' = 'gtc',
831
+ options: CryptoOrderOptions = {},
832
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
833
+ accountNumber?: string,
834
+ extras: BrokerExtras = {}
835
+ ): Promise<OrderResponse> {
836
+ return this.placeBrokerOrder(
837
+ {
838
+ symbol,
839
+ orderQty,
840
+ action,
841
+ orderType: 'Limit',
842
+ assetType: 'crypto',
843
+ price,
844
+ timeInForce,
845
+ broker,
846
+ accountNumber,
847
+ ...options,
848
+ },
849
+ extras
850
+ );
851
+ }
852
+
853
+ // Options convenience methods
854
+ async placeOptionsMarketOrder(
855
+ symbol: string,
856
+ orderQty: number,
857
+ action: 'Buy' | 'Sell',
858
+ options: OptionsOrderOptions,
859
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
860
+ accountNumber?: string,
861
+ extras: BrokerExtras = {}
862
+ ): Promise<OrderResponse> {
863
+ return this.placeBrokerOrder(
864
+ {
865
+ symbol,
866
+ orderQty,
867
+ action,
868
+ orderType: 'Market',
869
+ assetType: 'equity_option',
870
+ timeInForce: 'day',
871
+ broker,
872
+ accountNumber,
873
+ ...options,
874
+ },
875
+ extras
876
+ );
877
+ }
878
+
879
+ async placeOptionsLimitOrder(
880
+ symbol: string,
881
+ orderQty: number,
882
+ action: 'Buy' | 'Sell',
883
+ price: number,
884
+ options: OptionsOrderOptions,
885
+ timeInForce: 'day' | 'gtc' = 'gtc',
886
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
887
+ accountNumber?: string,
888
+ extras: BrokerExtras = {}
889
+ ): Promise<OrderResponse> {
890
+ return this.placeBrokerOrder(
891
+ {
892
+ symbol,
893
+ orderQty,
894
+ action,
895
+ orderType: 'Limit',
896
+ assetType: 'equity_option',
897
+ price,
898
+ timeInForce,
899
+ broker,
900
+ accountNumber,
901
+ ...options,
902
+ },
903
+ extras
904
+ );
905
+ }
906
+
907
+ // Futures convenience methods
908
+ async placeFuturesMarketOrder(
909
+ symbol: string,
910
+ orderQty: number,
911
+ action: 'Buy' | 'Sell',
912
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
913
+ accountNumber?: string,
914
+ extras: BrokerExtras = {}
915
+ ): Promise<OrderResponse> {
916
+ return this.placeBrokerOrder(
917
+ {
918
+ symbol,
919
+ orderQty,
920
+ action,
921
+ orderType: 'Market',
922
+ assetType: 'future',
923
+ timeInForce: 'day',
924
+ broker,
925
+ accountNumber,
926
+ },
927
+ extras
928
+ );
929
+ }
930
+
931
+ async placeFuturesLimitOrder(
932
+ symbol: string,
933
+ orderQty: number,
934
+ action: 'Buy' | 'Sell',
935
+ price: number,
936
+ timeInForce: 'day' | 'gtc' = 'gtc',
937
+ broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
938
+ accountNumber?: string,
939
+ extras: BrokerExtras = {}
940
+ ): Promise<OrderResponse> {
941
+ return this.placeBrokerOrder(
942
+ {
943
+ symbol,
944
+ orderQty,
945
+ action,
946
+ orderType: 'Limit',
947
+ assetType: 'future',
948
+ price,
949
+ timeInForce,
950
+ broker,
951
+ accountNumber,
952
+ },
953
+ extras
954
+ );
955
+ }
956
+
957
+ private buildOrderRequestBody(params: BrokerOrderParams, extras: BrokerExtras = {}) {
958
+ const baseOrder: any = {
959
+ order_id: params.order_id,
960
+ orderType: params.orderType,
961
+ assetType: params.assetType,
962
+ action: params.action,
963
+ timeInForce: params.timeInForce,
964
+ accountNumber: params.accountNumber,
965
+ symbol: params.symbol,
966
+ orderQty: params.orderQty,
967
+ };
968
+
969
+ if (params.price !== undefined) baseOrder.price = params.price;
970
+ if (params.stopPrice !== undefined) baseOrder.stopPrice = params.stopPrice;
971
+
972
+ // Apply broker-specific defaults – map camelCase extras property keys to snake_case before merging
973
+ const brokerExtras = this.applyBrokerDefaults(params.broker, extras);
974
+
975
+ return {
976
+ broker: params.broker,
977
+ order: {
978
+ ...baseOrder,
979
+ ...brokerExtras,
980
+ },
981
+ };
982
+ }
983
+
984
+ private buildModifyRequestBody(params: Partial<BrokerOrderParams>, extras: any, broker: string) {
985
+ const order: any = {};
986
+
987
+ if (params.order_id !== undefined) order.order_id = params.order_id;
988
+ if (params.orderType !== undefined) order.orderType = params.orderType;
989
+ if (params.assetType !== undefined) order.assetType = params.assetType;
990
+ if (params.action !== undefined) order.action = params.action;
991
+ if (params.timeInForce !== undefined) order.timeInForce = params.timeInForce;
992
+ if (params.accountNumber !== undefined) order.accountNumber = params.accountNumber;
993
+ if (params.symbol !== undefined) order.symbol = params.symbol;
994
+ if (params.orderQty !== undefined) order.orderQty = params.orderQty;
995
+ if (params.price !== undefined) order.price = params.price;
996
+ if (params.stopPrice !== undefined) order.stopPrice = params.stopPrice;
997
+
998
+ // Apply broker-specific defaults (handles snake_case conversion)
999
+ const brokerExtras = this.applyBrokerDefaults(broker, extras);
1000
+
1001
+ return {
1002
+ broker,
1003
+ order: {
1004
+ ...order,
1005
+ ...brokerExtras,
1006
+ },
1007
+ };
1008
+ }
1009
+
1010
+ private applyBrokerDefaults(broker: string, extras: any): any {
1011
+ // If the caller provided a broker-scoped extras object (e.g. { ninjaTrader: { ... } })
1012
+ // pull the nested object for easier processing.
1013
+ if (extras && typeof extras === 'object') {
1014
+ const scoped =
1015
+ broker === 'robinhood'
1016
+ ? extras.robinhood
1017
+ : broker === 'ninja_trader'
1018
+ ? extras.ninjaTrader
1019
+ : broker === 'tasty_trade'
1020
+ ? extras.tastyTrade
1021
+ : undefined;
1022
+ if (scoped) {
1023
+ extras = scoped;
1024
+ }
1025
+ }
1026
+
1027
+ switch (broker) {
1028
+ case 'robinhood':
1029
+ return {
1030
+ ...extras,
1031
+ extendedHours: extras?.extendedHours ?? extras?.extended_hours ?? true,
1032
+ marketHours: extras?.marketHours ?? extras?.market_hours ?? 'regular_hours',
1033
+ trailType: extras?.trailType ?? extras?.trail_type ?? 'percentage',
1034
+ };
1035
+ case 'ninja_trader':
1036
+ return {
1037
+ ...extras,
1038
+ accountSpec: extras?.accountSpec ?? extras?.account_spec ?? '',
1039
+ isAutomated: extras?.isAutomated ?? extras?.is_automated ?? true,
1040
+ };
1041
+ case 'tasty_trade':
1042
+ return {
1043
+ ...extras,
1044
+ automatedSource: extras?.automatedSource ?? extras?.automated_source ?? true,
1045
+ };
1046
+ default:
1047
+ return extras;
1048
+ }
1049
+ }
1050
+
1051
+ async getUserToken(sessionId: string): Promise<UserToken> {
1052
+ const accessToken = await this.getValidAccessToken();
1053
+ return this.request<UserToken>(`/session/${sessionId}/user`, {
1054
+ method: 'GET',
1055
+ headers: {
1056
+ Authorization: `Bearer ${accessToken}`,
1057
+ },
1058
+ });
1059
+ }
1060
+
1061
+ /**
1062
+ * Get the current session state
1063
+ */
1064
+ getCurrentSessionState(): SessionState | null {
1065
+ return this.currentSessionState;
1066
+ }
1067
+
1068
+ /**
1069
+ * Refresh the current session to extend its lifetime
1070
+ * Note: This now uses Supabase session refresh instead of custom endpoint
1071
+ */
1072
+ async refreshSession(): Promise<{
1073
+ success: boolean;
1074
+ response_data: {
1075
+ session_id: string;
1076
+ company_id: string;
1077
+ status: string;
1078
+ expires_at: string;
1079
+ user_id: string;
1080
+ auto_login: boolean;
1081
+ };
1082
+ message: string;
1083
+ status_code: number;
1084
+ }> {
1085
+ if (!this.currentSessionId || !this.companyId) {
1086
+ throw new SessionError('No active session to refresh');
1087
+ }
1088
+
1089
+ // Session-based auth - no token refresh needed
1090
+
1091
+ // Return session info in expected format
1092
+ return {
1093
+ success: true,
1094
+ response_data: {
1095
+ session_id: this.currentSessionId,
1096
+ company_id: this.companyId,
1097
+ status: 'active',
1098
+ expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour from now
1099
+ user_id: '', // Session-based auth - user_id comes from session
1100
+ auto_login: false,
1101
+ },
1102
+ message: 'Session refreshed successfully',
1103
+ status_code: 200,
1104
+ };
1105
+ }
1106
+
1107
+ // Broker Data Management
1108
+ async getBrokerList(): Promise<{
1109
+ _id: string;
1110
+ response_data: BrokerInfo[];
1111
+ message: string;
1112
+ status_code: number;
1113
+ warnings: null;
1114
+ errors: null;
1115
+ }> {
1116
+ // Public endpoint - no auth required
1117
+ return this.request<{
1118
+ _id: string;
1119
+ response_data: BrokerInfo[];
1120
+ message: string;
1121
+ status_code: number;
1122
+ warnings: null;
1123
+ errors: null;
1124
+ }>('/brokers/', {
1125
+ method: 'GET',
1126
+ });
1127
+ }
1128
+
1129
+ async getBrokerAccounts(options?: BrokerDataOptions): Promise<{
1130
+ _id: string;
1131
+ response_data: BrokerAccount[];
1132
+ message: string;
1133
+ status_code: number;
1134
+ warnings: null;
1135
+ errors: null;
1136
+ }> {
1137
+ const accessToken = await this.getValidAccessToken();
1138
+ const params: Record<string, string> = {};
1139
+
1140
+ if (options?.broker_name) {
1141
+ params.broker_id = options.broker_name;
1142
+ }
1143
+ if (options?.account_id) {
1144
+ params.account_id = options.account_id;
1145
+ }
1146
+ if (options?.symbol) {
1147
+ params.symbol = options.symbol;
1148
+ }
1149
+
1150
+ return this.request<{
1151
+ _id: string;
1152
+ response_data: BrokerAccount[];
1153
+ message: string;
1154
+ status_code: number;
1155
+ warnings: null;
1156
+ errors: null;
1157
+ }>('/brokers/data/accounts', {
1158
+ method: 'GET',
1159
+ headers: {
1160
+ Authorization: `Bearer ${accessToken}`,
1161
+ },
1162
+ params,
1163
+ });
1164
+ }
1165
+
1166
+ async getBrokerOrders(options?: BrokerDataOptions): Promise<{
1167
+ _id: string;
1168
+ response_data: BrokerOrder[];
1169
+ message: string;
1170
+ status_code: number;
1171
+ warnings: null;
1172
+ errors: null;
1173
+ }> {
1174
+ const accessToken = await this.getValidAccessToken();
1175
+ const params: Record<string, string> = {};
1176
+
1177
+ if (options?.broker_name) {
1178
+ params.broker_id = options.broker_name;
1179
+ }
1180
+ if (options?.account_id) {
1181
+ params.account_id = options.account_id;
1182
+ }
1183
+ if (options?.symbol) {
1184
+ params.symbol = options.symbol;
1185
+ }
1186
+
1187
+ return this.request<{
1188
+ _id: string;
1189
+ response_data: BrokerOrder[];
1190
+ message: string;
1191
+ status_code: number;
1192
+ warnings: null;
1193
+ errors: null;
1194
+ }>('/brokers/data/orders', {
1195
+ method: 'GET',
1196
+ headers: {
1197
+ Authorization: `Bearer ${accessToken}`,
1198
+ },
1199
+ params,
1200
+ });
1201
+ }
1202
+
1203
+ async getBrokerPositions(options?: BrokerDataOptions): Promise<{
1204
+ _id: string;
1205
+ response_data: BrokerPosition[];
1206
+ message: string;
1207
+ status_code: number;
1208
+ warnings: null;
1209
+ errors: null;
1210
+ }> {
1211
+ const accessToken = await this.getValidAccessToken();
1212
+ const params: Record<string, string> = {};
1213
+
1214
+ if (options?.broker_name) {
1215
+ params.broker_id = options.broker_name;
1216
+ }
1217
+ if (options?.account_id) {
1218
+ params.account_id = options.account_id;
1219
+ }
1220
+ if (options?.symbol) {
1221
+ params.symbol = options.symbol;
1222
+ }
1223
+
1224
+ return this.request<{
1225
+ _id: string;
1226
+ response_data: BrokerPosition[];
1227
+ message: string;
1228
+ status_code: number;
1229
+ warnings: null;
1230
+ errors: null;
1231
+ }>('/brokers/data/positions', {
1232
+ method: 'GET',
1233
+ headers: {
1234
+ Authorization: `Bearer ${accessToken}`,
1235
+ },
1236
+ params,
1237
+ });
1238
+ }
1239
+
1240
+ async getBrokerBalances(options?: BrokerDataOptions): Promise<{
1241
+ _id: string;
1242
+ response_data: BrokerBalance[];
1243
+ message: string;
1244
+ status_code: number;
1245
+ warnings: null;
1246
+ errors: null;
1247
+ }> {
1248
+ const accessToken = await this.getValidAccessToken();
1249
+ const params: Record<string, string> = {};
1250
+
1251
+ if (options?.broker_name) {
1252
+ params.broker_id = options.broker_name;
1253
+ }
1254
+ if (options?.account_id) {
1255
+ params.account_id = options.account_id;
1256
+ }
1257
+ if (options?.symbol) {
1258
+ params.symbol = options.symbol;
1259
+ }
1260
+
1261
+ return this.request<{
1262
+ _id: string;
1263
+ response_data: BrokerBalance[];
1264
+ message: string;
1265
+ status_code: number;
1266
+ warnings: null;
1267
+ errors: null;
1268
+ }>('/brokers/data/balances', {
1269
+ method: 'GET',
1270
+ headers: {
1271
+ Authorization: `Bearer ${accessToken}`,
1272
+ },
1273
+ params,
1274
+ });
1275
+ }
1276
+
1277
+ async getBrokerConnections(): Promise<{
1278
+ _id: string;
1279
+ response_data: BrokerConnection[];
1280
+ message: string;
1281
+ status_code: number;
1282
+ warnings: null;
1283
+ errors: null;
1284
+ }> {
1285
+ const accessToken = await this.getValidAccessToken();
1286
+ return this.request<{
1287
+ _id: string;
1288
+ response_data: BrokerConnection[];
1289
+ message: string;
1290
+ status_code: number;
1291
+ warnings: null;
1292
+ errors: null;
1293
+ }>('/brokers/connections', {
1294
+ method: 'GET',
1295
+ headers: {
1296
+ Authorization: `Bearer ${accessToken}`,
1297
+ },
1298
+ });
1299
+ }
1300
+
1301
+ async getBalances(filters?: any): Promise<{
1302
+ _id: string;
1303
+ response_data: any[];
1304
+ message: string;
1305
+ status_code: number;
1306
+ warnings: null;
1307
+ errors: null;
1308
+ }> {
1309
+ const accessToken = await this.getValidAccessToken();
1310
+ const params = new URLSearchParams();
1311
+ if (filters) {
1312
+ Object.entries(filters).forEach(([key, value]) => {
1313
+ if (value !== undefined && value !== null) {
1314
+ params.append(key, String(value));
1315
+ }
1316
+ });
1317
+ }
1318
+
1319
+ const queryString = params.toString();
1320
+ const url = queryString ? `/brokers/data/balances?${queryString}` : '/brokers/data/balances';
1321
+
1322
+ return this.request<{
1323
+ _id: string;
1324
+ response_data: any[];
1325
+ message: string;
1326
+ status_code: number;
1327
+ warnings: null;
1328
+ errors: null;
1329
+ }>(url, {
1330
+ method: 'GET',
1331
+ headers: {
1332
+ Authorization: `Bearer ${accessToken}`,
1333
+ },
1334
+ });
1335
+ }
1336
+
1337
+ // Page-based pagination methods
1338
+ async getBrokerOrdersPage(
1339
+ page: number = 1,
1340
+ perPage: number = 100,
1341
+ filters?: OrdersFilter
1342
+ ): Promise<PaginatedResult<BrokerOrder[]>> {
1343
+ const accessToken = await this.getValidAccessToken();
1344
+ const offset = (page - 1) * perPage;
1345
+ const params: Record<string, string> = {
1346
+ limit: perPage.toString(),
1347
+ offset: offset.toString(),
1348
+ };
1349
+
1350
+ // Add filter parameters
1351
+ if (filters) {
1352
+ if (filters.broker_id) params.broker_id = filters.broker_id;
1353
+ if (filters.connection_id) params.connection_id = filters.connection_id;
1354
+ if (filters.account_id) params.account_id = filters.account_id;
1355
+ if (filters.symbol) params.symbol = filters.symbol;
1356
+ if (filters.status) params.status = filters.status;
1357
+ if (filters.side) params.side = filters.side;
1358
+ if (filters.asset_type) params.asset_type = filters.asset_type;
1359
+ if (filters.created_after) params.created_after = filters.created_after;
1360
+ if (filters.created_before) params.created_before = filters.created_before;
1361
+ }
1362
+
1363
+ const response = await this.request<ApiResponse<BrokerOrder[]>>('/brokers/data/orders', {
1364
+ method: 'GET',
1365
+ headers: {
1366
+ Authorization: `Bearer ${accessToken}`,
1367
+ },
1368
+ params,
1369
+ });
1370
+
1371
+ // Create navigation callback for pagination
1372
+ const navigationCallback = async (
1373
+ newOffset: number,
1374
+ newLimit: number
1375
+ ): Promise<PaginatedResult<BrokerOrder[]>> => {
1376
+ const newParams: Record<string, string> = {
1377
+ limit: newLimit.toString(),
1378
+ offset: newOffset.toString(),
1379
+ };
1380
+
1381
+ // Add filter parameters
1382
+ if (filters) {
1383
+ if (filters.broker_id) newParams.broker_id = filters.broker_id;
1384
+ if (filters.connection_id) newParams.connection_id = filters.connection_id;
1385
+ if (filters.account_id) newParams.account_id = filters.account_id;
1386
+ if (filters.symbol) newParams.symbol = filters.symbol;
1387
+ if (filters.status) newParams.status = filters.status;
1388
+ if (filters.side) newParams.side = filters.side;
1389
+ if (filters.asset_type) newParams.asset_type = filters.asset_type;
1390
+ if (filters.created_after) newParams.created_after = filters.created_after;
1391
+ if (filters.created_before) newParams.created_before = filters.created_before;
1392
+ }
1393
+
1394
+ const newResponse = await this.request<ApiResponse<BrokerOrder[]>>('/brokers/data/orders', {
1395
+ method: 'GET',
1396
+ headers: {
1397
+ Authorization: `Bearer ${accessToken}`,
1398
+ },
1399
+ params: newParams,
1400
+ });
1401
+
1402
+ return new PaginatedResult(
1403
+ newResponse.response_data,
1404
+ newResponse.pagination || {
1405
+ has_more: false,
1406
+ next_offset: newOffset,
1407
+ current_offset: newOffset,
1408
+ limit: newLimit,
1409
+ },
1410
+ navigationCallback
1411
+ );
1412
+ };
1413
+
1414
+ return new PaginatedResult(
1415
+ response.response_data,
1416
+ response.pagination || {
1417
+ has_more: false,
1418
+ next_offset: offset,
1419
+ current_offset: offset,
1420
+ limit: perPage,
1421
+ },
1422
+ navigationCallback
1423
+ );
1424
+ }
1425
+
1426
+ async getBrokerAccountsPage(
1427
+ page: number = 1,
1428
+ perPage: number = 100,
1429
+ filters?: AccountsFilter
1430
+ ): Promise<PaginatedResult<BrokerAccount[]>> {
1431
+ const accessToken = await this.getValidAccessToken();
1432
+ const offset = (page - 1) * perPage;
1433
+ const params: Record<string, string> = {
1434
+ limit: perPage.toString(),
1435
+ offset: offset.toString(),
1436
+ };
1437
+
1438
+ // Add filter parameters
1439
+ if (filters) {
1440
+ if (filters.broker_id) params.broker_id = filters.broker_id;
1441
+ if (filters.connection_id) params.connection_id = filters.connection_id;
1442
+ if (filters.account_type) params.account_type = filters.account_type;
1443
+ if (filters.status) params.status = filters.status;
1444
+ if (filters.currency) params.currency = filters.currency;
1445
+ }
1446
+
1447
+ const response = await this.request<ApiResponse<BrokerAccount[]>>('/brokers/data/accounts', {
1448
+ method: 'GET',
1449
+ headers: {
1450
+ Authorization: `Bearer ${accessToken}`,
1451
+ },
1452
+ params,
1453
+ });
1454
+
1455
+ // Create navigation callback for pagination
1456
+ const navigationCallback = async (
1457
+ newOffset: number,
1458
+ newLimit: number
1459
+ ): Promise<PaginatedResult<BrokerAccount[]>> => {
1460
+ const newParams: Record<string, string> = {
1461
+ limit: newLimit.toString(),
1462
+ offset: newOffset.toString(),
1463
+ };
1464
+
1465
+ // Add filter parameters
1466
+ if (filters) {
1467
+ if (filters.broker_id) newParams.broker_id = filters.broker_id;
1468
+ if (filters.connection_id) newParams.connection_id = filters.connection_id;
1469
+ if (filters.account_type) newParams.account_type = filters.account_type;
1470
+ if (filters.status) newParams.status = filters.status;
1471
+ if (filters.currency) newParams.currency = filters.currency;
1472
+ }
1473
+
1474
+ const newResponse = await this.request<ApiResponse<BrokerAccount[]>>(
1475
+ '/brokers/data/accounts',
1476
+ {
1477
+ method: 'GET',
1478
+ headers: {
1479
+ Authorization: `Bearer ${accessToken}`,
1480
+ },
1481
+ params: newParams,
1482
+ }
1483
+ );
1484
+
1485
+ return new PaginatedResult(
1486
+ newResponse.response_data,
1487
+ newResponse.pagination || {
1488
+ has_more: false,
1489
+ next_offset: newOffset,
1490
+ current_offset: newOffset,
1491
+ limit: newLimit,
1492
+ },
1493
+ navigationCallback
1494
+ );
1495
+ };
1496
+
1497
+ return new PaginatedResult(
1498
+ response.response_data,
1499
+ response.pagination || {
1500
+ has_more: false,
1501
+ next_offset: offset,
1502
+ current_offset: offset,
1503
+ limit: perPage,
1504
+ },
1505
+ navigationCallback
1506
+ );
1507
+ }
1508
+
1509
+ async getBrokerPositionsPage(
1510
+ page: number = 1,
1511
+ perPage: number = 100,
1512
+ filters?: PositionsFilter
1513
+ ): Promise<PaginatedResult<BrokerPosition[]>> {
1514
+ const accessToken = await this.getValidAccessToken();
1515
+ const offset = (page - 1) * perPage;
1516
+ const params: Record<string, string> = {
1517
+ limit: perPage.toString(),
1518
+ offset: offset.toString(),
1519
+ };
1520
+
1521
+ // Add filter parameters
1522
+ if (filters) {
1523
+ if (filters.broker_id) params.broker_id = filters.broker_id;
1524
+ if (filters.account_id) params.account_id = filters.account_id;
1525
+ if (filters.symbol) params.symbol = filters.symbol;
1526
+ if (filters.position_status) params.position_status = filters.position_status;
1527
+ if (filters.side) params.side = filters.side;
1528
+ }
1529
+
1530
+ const response = await this.request<ApiResponse<BrokerPosition[]>>('/brokers/data/positions', {
1531
+ method: 'GET',
1532
+ headers: {
1533
+ Authorization: `Bearer ${accessToken}`,
1534
+ },
1535
+ params,
1536
+ });
1537
+
1538
+ // Create navigation callback for pagination
1539
+ const navigationCallback = async (
1540
+ newOffset: number,
1541
+ newLimit: number
1542
+ ): Promise<PaginatedResult<BrokerPosition[]>> => {
1543
+ const newParams: Record<string, string> = {
1544
+ limit: newLimit.toString(),
1545
+ offset: newOffset.toString(),
1546
+ };
1547
+
1548
+ // Add filter parameters
1549
+ if (filters) {
1550
+ if (filters.broker_id) newParams.broker_id = filters.broker_id;
1551
+ if (filters.account_id) newParams.account_id = filters.account_id;
1552
+ if (filters.symbol) newParams.symbol = filters.symbol;
1553
+ if (filters.position_status) newParams.position_status = filters.position_status;
1554
+ if (filters.side) newParams.side = filters.side;
1555
+ }
1556
+
1557
+ const newResponse = await this.request<ApiResponse<BrokerPosition[]>>(
1558
+ '/brokers/data/positions',
1559
+ {
1560
+ method: 'GET',
1561
+ headers: {
1562
+ Authorization: `Bearer ${accessToken}`,
1563
+ },
1564
+ params: newParams,
1565
+ }
1566
+ );
1567
+
1568
+ return new PaginatedResult(
1569
+ newResponse.response_data,
1570
+ newResponse.pagination || {
1571
+ has_more: false,
1572
+ next_offset: newOffset,
1573
+ current_offset: newOffset,
1574
+ limit: newLimit,
1575
+ },
1576
+ navigationCallback
1577
+ );
1578
+ };
1579
+
1580
+ return new PaginatedResult(
1581
+ response.response_data,
1582
+ response.pagination || {
1583
+ has_more: false,
1584
+ next_offset: offset,
1585
+ current_offset: offset,
1586
+ limit: perPage,
1587
+ },
1588
+ navigationCallback
1589
+ );
1590
+ }
1591
+
1592
+ async getBrokerBalancesPage(
1593
+ page: number = 1,
1594
+ perPage: number = 100,
1595
+ filters?: BalancesFilter
1596
+ ): Promise<PaginatedResult<BrokerBalance[]>> {
1597
+ const accessToken = await this.getValidAccessToken();
1598
+ const offset = (page - 1) * perPage;
1599
+ const params: Record<string, string> = {
1600
+ limit: perPage.toString(),
1601
+ offset: offset.toString(),
1602
+ };
1603
+
1604
+ // Add filter parameters
1605
+ if (filters) {
1606
+ if (filters.broker_id) params.broker_id = filters.broker_id;
1607
+ if (filters.connection_id) params.connection_id = filters.connection_id;
1608
+ if (filters.account_id) params.account_id = filters.account_id;
1609
+ if (filters.is_end_of_day_snapshot !== undefined)
1610
+ params.is_end_of_day_snapshot = filters.is_end_of_day_snapshot.toString();
1611
+ if (filters.balance_created_after)
1612
+ params.balance_created_after = filters.balance_created_after;
1613
+ if (filters.balance_created_before)
1614
+ params.balance_created_before = filters.balance_created_before;
1615
+ if (filters.with_metadata !== undefined)
1616
+ params.with_metadata = filters.with_metadata.toString();
1617
+ }
1618
+
1619
+ const response = await this.request<ApiResponse<BrokerBalance[]>>('/brokers/data/balances', {
1620
+ method: 'GET',
1621
+ headers: {
1622
+ Authorization: `Bearer ${accessToken}`,
1623
+ },
1624
+ params,
1625
+ });
1626
+
1627
+ // Create navigation callback for pagination
1628
+ const navigationCallback = async (
1629
+ newOffset: number,
1630
+ newLimit: number
1631
+ ): Promise<PaginatedResult<BrokerBalance[]>> => {
1632
+ const newParams: Record<string, string> = {
1633
+ limit: newLimit.toString(),
1634
+ offset: newOffset.toString(),
1635
+ };
1636
+
1637
+ // Add filter parameters
1638
+ if (filters) {
1639
+ if (filters.broker_id) newParams.broker_id = filters.broker_id;
1640
+ if (filters.connection_id) newParams.connection_id = filters.connection_id;
1641
+ if (filters.account_id) newParams.account_id = filters.account_id;
1642
+ if (filters.is_end_of_day_snapshot !== undefined)
1643
+ newParams.is_end_of_day_snapshot = filters.is_end_of_day_snapshot.toString();
1644
+ if (filters.balance_created_after)
1645
+ newParams.balance_created_after = filters.balance_created_after;
1646
+ if (filters.balance_created_before)
1647
+ newParams.balance_created_before = filters.balance_created_before;
1648
+ if (filters.with_metadata !== undefined)
1649
+ newParams.with_metadata = filters.with_metadata.toString();
1650
+ }
1651
+
1652
+ const newResponse = await this.request<ApiResponse<BrokerBalance[]>>(
1653
+ '/brokers/data/balances',
1654
+ {
1655
+ method: 'GET',
1656
+ headers: {
1657
+ Authorization: `Bearer ${accessToken}`,
1658
+ },
1659
+ params: newParams,
1660
+ }
1661
+ );
1662
+
1663
+ return new PaginatedResult(
1664
+ newResponse.response_data,
1665
+ newResponse.pagination || {
1666
+ has_more: false,
1667
+ next_offset: newOffset,
1668
+ current_offset: newOffset,
1669
+ limit: newLimit,
1670
+ },
1671
+ navigationCallback
1672
+ );
1673
+ };
1674
+
1675
+ return new PaginatedResult(
1676
+ response.response_data,
1677
+ response.pagination || {
1678
+ has_more: false,
1679
+ next_offset: offset,
1680
+ current_offset: offset,
1681
+ limit: perPage,
1682
+ },
1683
+ navigationCallback
1684
+ );
1685
+ }
1686
+
1687
+ // Navigation methods
1688
+ async getNextPage<T>(
1689
+ previousResult: PaginatedResult<T>,
1690
+ fetchFunction: (offset: number, limit: number) => Promise<PaginatedResult<T>>
1691
+ ): Promise<PaginatedResult<T> | null> {
1692
+ if (!previousResult.hasNext) {
1693
+ return null;
1694
+ }
1695
+
1696
+ return fetchFunction(previousResult.metadata.nextOffset, previousResult.metadata.limit);
1697
+ }
1698
+
1699
+ /**
1700
+ * Check if this is a mock client
1701
+ * @returns false for real API client
1702
+ */
1703
+ isMockClient(): boolean {
1704
+ return false;
1705
+ }
1706
+
1707
+ /**
1708
+ * Disconnect a company from a broker connection
1709
+ * @param connectionId - The connection ID to disconnect
1710
+ * @returns Promise with disconnect response
1711
+ */
1712
+ async disconnectCompany(connectionId: string): Promise<DisconnectCompanyResponse> {
1713
+ const accessToken = await this.getValidAccessToken();
1714
+ return this.request<DisconnectCompanyResponse>(`/brokers/disconnect/${connectionId}`, {
1715
+ method: 'DELETE',
1716
+ headers: {
1717
+ Authorization: `Bearer ${accessToken}`,
1718
+ },
1719
+ });
1720
+ }
1721
+ }