@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.
- package/README.md +489 -0
- package/dist/index.d.ts +2037 -0
- package/dist/index.js +4815 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4787 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types/client/ApiClient.d.ts +234 -0
- package/dist/types/client/FinaticConnect.d.ts +307 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/mocks/MockApiClient.d.ts +228 -0
- package/dist/types/mocks/MockDataProvider.d.ts +132 -0
- package/dist/types/mocks/MockFactory.d.ts +53 -0
- package/dist/types/mocks/index.d.ts +5 -0
- package/dist/types/mocks/utils.d.ts +29 -0
- package/dist/types/portal/PortalUI.d.ts +38 -0
- package/dist/types/security/ApiSecurity.d.ts +24 -0
- package/dist/types/security/RuntimeSecurity.d.ts +28 -0
- package/dist/types/security/SecurityUtils.d.ts +21 -0
- package/dist/types/security/index.d.ts +2 -0
- package/dist/types/services/AnalyticsService.d.ts +18 -0
- package/dist/types/services/ApiClient.d.ts +121 -0
- package/dist/types/services/PortalService.d.ts +24 -0
- package/dist/types/services/TradingService.d.ts +55 -0
- package/dist/types/services/api.d.ts +23 -0
- package/dist/types/services/auth.d.ts +9 -0
- package/dist/types/services/index.d.ts +4 -0
- package/dist/types/services/portfolio.d.ts +10 -0
- package/dist/types/services/trading.d.ts +10 -0
- package/dist/types/shared/index.d.ts +2 -0
- package/dist/types/shared/themes/index.d.ts +2 -0
- package/dist/types/shared/themes/portalPresets.d.ts +8 -0
- package/dist/types/shared/themes/presets.d.ts +3 -0
- package/dist/types/shared/themes/system.d.ts +2 -0
- package/dist/types/shared/types/index.d.ts +110 -0
- package/dist/types/types/api.d.ts +486 -0
- package/dist/types/types/config.d.ts +12 -0
- package/dist/types/types/connect.d.ts +51 -0
- package/dist/types/types/errors.d.ts +47 -0
- package/dist/types/types/portal.d.ts +75 -0
- package/dist/types/types/security.d.ts +35 -0
- package/dist/types/types/shared.d.ts +50 -0
- package/dist/types/types/theme.d.ts +101 -0
- package/dist/types/types.d.ts +157 -0
- package/dist/types/utils/errors.d.ts +42 -0
- package/dist/types/utils/events.d.ts +12 -0
- package/dist/types/utils/themeUtils.d.ts +34 -0
- 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
|