main
ts 1,026 lines 36.1 KB
Raw
1 import { frontendConfig } from "../config";
2
3 export type Money = {
4 amount: number;
5 currency: string;
6 };
7
8 export type Portfolio = {
9 portfolioId: string;
10 name: string;
11 baseCurrency: string;
12 createdAt: string;
13 updatedAt: string;
14 provider?: string | null;
15 brokerConnectionId?: string | null;
16 lastBrokerSyncAt?: string | null;
17 lastBrokerSyncAttemptAt?: string | null;
18 lastBrokerSyncErrorCode?: string | null;
19 acquisitionSource?: "BROKER_API" | "MANUAL_CSV_IMPORT" | null;
20 sourceAccountReference?: string | null;
21 lastImportedAt?: string | null;
22 lastImportedFilename?: string | null;
23 };
24
25 export type PortfolioListItem = {
26 portfolioId: string;
27 name: string;
28 baseCurrency: string;
29 totalMarketValue: Money | null;
30 unrealizedProfitLoss: Money | null;
31 unrealizedProfitLossPercent: number | null;
32 positions: number;
33 updatedAt: string;
34 provider?: string | null;
35 brokerConnectionId?: string | null;
36 lastBrokerSyncAt?: string | null;
37 lastBrokerSyncAttemptAt?: string | null;
38 lastBrokerSyncErrorCode?: string | null;
39 valuationComplete: boolean;
40 acquisitionSource?: "BROKER_API" | "MANUAL_CSV_IMPORT" | null;
41 sourceAccountReference?: string | null;
42 lastImportedAt?: string | null;
43 lastImportedFilename?: string | null;
44 };
45
46 export type PortfolioDashboard = {
47 currencyTotals: Record<string, number>;
48 portfolios: PortfolioListItem[];
49 incompleteValuationPortfolioIds: string[];
50 };
51
52 export type SectorPerformanceStock = { globalInstrumentId: string; companyName: string; ticker: string; exchange: string; currency: string; latestPrice: number; referencePrice: number; performancePct: number | null; };
53 export type SectorPerformance = { region: "USA" | "EUROPE" | "INDIA"; sector: string; period: "DAY" | "WEEK" | "MONTH" | "YEAR"; asOf?: string | null; bestPerformers: SectorPerformanceStock[]; worstPerformers: SectorPerformanceStock[] };
54 export type MarketUniverseSector = { name: string; instrumentCount: number };
55 export type MarketUniverseSectors = { region: SectorPerformance["region"]; sectors: MarketUniverseSector[] };
56
57 export type ResearchInstrumentMatch = {
58 canonicalSymbol: string;
59 globalInstrumentId: string;
60 companyName: string;
61 symbol: string;
62 exchange: string;
63 mic?: string;
64 country: string;
65 currency?: string;
66 isin?: string;
67 sector?: string | null;
68 industry?: string | null;
69 assetType: string;
70 score?: number | null;
71 region: SectorPerformance["region"];
72 };
73 export type ResearchRequirementStatus = "READY_FRESH" | "READY_STALE" | "PARTIAL" | "MISSING"
74 | "CONFLICTING" | "UNSUPPORTED" | "REFRESHING" | "FAILED" | "NOT_APPLICABLE";
75 export type ResearchSupportedAction = "FIND_DATA" | "UPLOAD_EVIDENCE" | "RUN_PARTIAL_ANALYSIS";
76 export type ResearchReadinessRequirement = {
77 requirementId: string;
78 area: string;
79 areaWeightPct: number;
80 importance: "MANDATORY" | "IMPORTANT" | "SUPPORTING";
81 mandatory: boolean;
82 status: ResearchRequirementStatus;
83 applicability?: "APPLICABLE" | "PARTIALLY_APPLICABLE" | "NOT_APPLICABLE" | "UNKNOWN";
84 applicabilityReason?: string | null;
85 businessClassification?: string | null;
86 classificationSource?: string | null;
87 acquisitionObservation?: { outcome?: string | null; provider?: string | null; observed_at?: string | null; failure_reason?: string | null; history?: { outcome: string; provider: string; observed_at: string }[]; news_readiness?: string | null; coverage?: number | null; run_id?: string | null; } | null;
88 sourceProvider?: string | null;
89 sourceTier?: string | null;
90 sourceUrl?: string | null;
91 asOf?: string | null;
92 retrievedAt?: string | null;
93 ageSeconds?: number | null;
94 freshnessPolicy: {
95 policyId: string;
96 mode: string;
97 maximumAgeSeconds?: number | null;
98 scoringWindowDays?: number | null;
99 };
100 evidenceIds: string[];
101 coveredInputIds: string[];
102 missingInputIds: string[];
103 concreteRequirements: Array<{ inputId: string; importance: "MANDATORY" | "IMPORTANT" | "SUPPORTING"; covered: boolean; applicability?: string; applicabilityReason?: string | null }>;
104 coveragePct: number;
105 criticalCoveragePct: number;
106 missingReason?: string | null;
107 conflictReason?: string | null;
108 supportedActions: ResearchSupportedAction[];
109 };
110 export type ResearchReadiness = {
111 globalInstrumentId: string;
112 overallStatus: ResearchRequirementStatus;
113 overallCompletenessPct: number;
114 criticalCompletenessPct: number;
115 confidence: "HIGH" | "MEDIUM" | "LOW";
116 confidencePct: number;
117 generatedAt: string;
118 requirements: ResearchReadinessRequirement[];
119 analysisEligibility?: {
120 fullAnalysisAllowed: boolean;
121 partialAnalysisAllowed: boolean;
122 blockingRequirements: string[];
123 reason: string;
124 };
125 refreshState?: {
126 plannedRequirements: string[];
127 executedCapabilities: string[];
128 reusedSingleFlight: boolean;
129 failureReasons: Record<string, string>;
130 };
131 };
132 export type StockRuleEngineMetric = {
133 metric: string;
134 value: unknown;
135 unit?: string | null;
136 score: number;
137 rule: string;
138 configuredSubruleWeight: number;
139 appliedWeightPct: number;
140 source: string;
141 sourceUrl?: string | null;
142 asOf?: string | null;
143 evidenceReferences: string[];
144 };
145 export type StockRuleEngineAreaScore = {
146 area: string;
147 weight: number;
148 rawScore?: number | null;
149 weightedContribution: number;
150 status: "READY_FRESH" | "READY_STALE" | "PARTIAL" | "CONFLICTING" | "UNSCORABLE" | "UNSUPPORTED" | "NOT_APPLICABLE";
151 applicable: boolean;
152 metrics: StockRuleEngineMetric[];
153 positiveFactors: string[];
154 negativeFactors: string[];
155 evidenceReferences: string[];
156 sourceReferences: Array<{
157 sourceProvider?: string | null;
158 sourceTier?: string | null;
159 sourceUrl: string;
160 asOf?: string | null;
161 retrievedAt?: string | null;
162 evidenceReferences: string[];
163 }>;
164 missingInputs: string[];
165 };
166 export type StockRuleEngineAnalysis = {
167 ruleEngineVersion: string;
168 calculatedAt: string;
169 globalInstrumentId: string;
170 inputAsOf?: string | null;
171 inputFingerprint: string;
172 overallScore?: number | null;
173 qualityScore?: number | null;
174 opportunityScore?: number | null;
175 riskScore?: number | null;
176 confidenceScore: number;
177 confidence: "HIGH" | "MEDIUM" | "LOW";
178 decisionSignal: "STRONG_BUY" | "BUY" | "ACCUMULATE" | "HOLD" | "REDUCE" | "AVOID" | "EXIT_REVIEW" | "INSUFFICIENT_DATA";
179 partial: boolean;
180 cacheHit: boolean;
181 eligibility: NonNullable<ResearchReadiness["analysisEligibility"]>;
182 areaScores: StockRuleEngineAreaScore[];
183 riskOverrides: Array<{ code: string; severity: "HIGH" | "CRITICAL"; effect: "BLOCK_BUY"; evidenceIds: string[] }>;
184 missingInputs: string[];
185 evidenceReferences: string[];
186 };
187 export type ResearchWatchlist = {
188 watchlistId: string;
189 name: "IND-WATCHLIST" | "EU-WATCHLIST" | "USA-WATCHLIST" | string;
190 region: SectorPerformance["region"];
191 systemDefault: boolean;
192 instrumentCount: number;
193 createdAt: string;
194 updatedAt: string;
195 };
196 export type WatchlistResearchInstrument = {
197 globalInstrumentId: string;
198 companyName?: string | null;
199 ticker?: string | null;
200 exchange?: string | null;
201 country?: string | null;
202 currency?: string | null;
203 assetType?: string | null;
204 held: false;
205 sourcePeriod?: SectorPerformance["period"] | null;
206 sourcePerformancePct?: number | null;
207 addedAt?: string | null;
208 marketData?: WatchlistMarketData | null;
209 company: PortfolioResearchCompany;
210 };
211 export type WatchlistResearchPresentation = {
212 watchlist: ResearchWatchlist;
213 instruments: WatchlistResearchInstrument[];
214 };
215 export type MarketDataEnsureStatus = {
216 region: "INDIA";
217 universe: { status: "FRESH" | "STALE" | "REFRESH_STARTED" | "UNAVAILABLE"; lastUpdatedAt?: string | null };
218 historicalPrices: {
219 status: "FRESH" | "STALE" | "POPULATION_STARTED" | "RUNNING" | "UNAVAILABLE";
220 jobId?: string | null;
221 lastObservedAt?: string | null;
222 eligibleInstruments?: number;
223 coveredInstruments?: number;
224 };
225 };
226
227 export type Instrument = {
228 instrumentId: string;
229 globalInstrumentId?: string | null;
230 provider?: string | null;
231 providerInstrumentId?: string | null;
232 isin?: string | null;
233 ticker: string;
234 exchange: string;
235 mic?: string | null;
236 companyName: string;
237 assetType: string;
238 country?: string | null;
239 tradingCurrency: string;
240 sector?: string | null;
241 industry?: string | null;
242 brokerSymbol?: string | null;
243 brokerDescription?: string | null;
244 brokerExchange?: string | null;
245 canonicalSymbol?: string | null;
246 canonicalName?: string | null;
247 canonicalExchange?: string | null;
248 canonicalMic?: string | null;
249 securityType?: string | null;
250 providerMappings?: Array<{
251 provider: string;
252 providerSymbol?: string | null;
253 providerInstrumentId?: string | null;
254 exchange?: string | null;
255 currency?: string | null;
256 status: string;
257 resolutionSource?: string | null;
258 failureReason?: string | null;
259 confidence?: number | null;
260 }>;
261 };
262
263 export type Quote = {
264 bid?: Money;
265 ask?: Money;
266 last?: Money;
267 previousClose?: Money;
268 currency?: string;
269 timestamp?: string;
270 source?: string;
271 freshness?: "REAL_TIME" | "DELAYED" | "END_OF_DAY" | "STALE" | "MOCK" | "UNAVAILABLE";
272 marketStatus?: string;
273 sourceTimestamp?: string;
274 receivedAt?: string;
275 };
276
277 export type WatchlistSnapshot = {
278 providerTicker?: string | null;
279 exchange?: string | null;
280 currency?: string | null;
281 quoteType?: string | null;
282 price?: number | null;
283 priceCurrency?: string | null;
284 marketAsOf?: string | null;
285 retrievedAt?: string | null;
286 source?: string | null;
287 status?: string | null;
288 previousClose?: number | null;
289 bid?: number | null;
290 ask?: number | null;
291 volume?: number | null;
292 averageVolume10Day?: number | null;
293 averageVolume3Month?: number | null;
294 fiftyTwoWeekHigh?: number | null;
295 fiftyTwoWeekLow?: number | null;
296 marketCap?: number | null;
297 enterpriseValue?: number | null;
298 peRatio?: number | null;
299 forwardPe?: number | null;
300 pbRatio?: number | null;
301 psRatio?: number | null;
302 pegRatio?: number | null;
303 evEbitda?: number | null;
304 trailingEps?: number | null;
305 forwardEps?: number | null;
306 roe?: number | null;
307 roa?: number | null;
308 roce?: number | null;
309 netMargin?: number | null;
310 operatingMargin?: number | null;
311 debt?: number | null;
312 cash?: number | null;
313 debtEquity?: number | null;
314 revenueGrowth?: number | null;
315 earningsGrowth?: number | null;
316 freeCashFlow?: number | null;
317 operatingCashFlow?: number | null;
318 };
319
320 export type WatchlistMarketData = {
321 canonicalName?: string | null;
322 isin?: string | null;
323 providerMappings?: Instrument["providerMappings"];
324 snapshot?: WatchlistSnapshot | null;
325 quote?: Quote | null;
326 marketDataError?: string | null;
327 };
328
329 export type PortfolioPosition = {
330 positionId: string;
331 portfolioId: string;
332 instrument: Instrument;
333 quantity: number;
334 averageCost: Money;
335 currentPrice: Money | null;
336 importedPrice?: Money | null;
337 marketValue: Money | null;
338 costBasis: Money;
339 unrealizedProfitLoss: Money | null;
340 unrealizedProfitLossPercent: number | null;
341 brokerType: string;
342 sourceType: string;
343 displayName: string;
344 customDisplayName?: string | null;
345 dataFreshness: string;
346 lastUpdated: string;
347 quote?: Quote | null;
348 };
349
350 export type Allocation = {
351 country: Record<string, number>;
352 currency: Record<string, number>;
353 sector: Record<string, number>;
354 assetType: Record<string, number>;
355 broker: Record<string, number>;
356 };
357
358 export type PortfolioSummary = {
359 portfolioId: string;
360 baseCurrency: string;
361 totalMarketValue: Money | null;
362 totalCostBasis: Money;
363 unrealizedProfitLoss: Money | null;
364 unrealizedProfitLossPercent: number | null;
365 cash: Money;
366 positions: number;
367 allocation: Allocation;
368 };
369
370 export type PortfolioHistoryRange = "1D" | "5D" | "1W" | "1M" | "1Y" | "2Y" | "3Y" | "4Y" | "5Y" | "MAX";
371
372 export type PortfolioHistoryPoint = {
373 timestamp: string;
374 marketValue: Money;
375 investedCapital?: Money | null;
376 investedCapitalStatus: string;
377 cash: Money;
378 positionsMarketValue: Money;
379 unrealizedPnl: Money;
380 realizedPnl?: Money | null;
381 broker: string;
382 source: string;
383 dataFreshness: string;
384 };
385
386 export type PortfolioHistory = {
387 portfolioId: string;
388 baseCurrency: string;
389 range: PortfolioHistoryRange;
390 from?: string | null;
391 to: string;
392 investedCapitalStatus: string;
393 backfillAvailable: boolean;
394 points: PortfolioHistoryPoint[];
395 };
396
397 export type BrokerProviderInfo = {
398 brokerType: string;
399 displayName: string;
400 connectable: boolean;
401 unavailableReason?: string | null;
402 status: string;
403 providerStatus: string;
404 code: string;
405 message: string;
406 dataFreshness: string;
407 readOnly: boolean;
408 officialProviderSetupRequired: boolean;
409 consumerAuthMode: "BROKER_REDIRECT" | "PARTNER_OAUTH" | "INDIVIDUAL_API_CREDENTIALS" | "PARTNER_UNAVAILABLE" | "NONE";
410 individualApiSupported: boolean;
411 advancedIndividualMode: boolean;
412 manualImportSupported: boolean;
413 manualImportParserStatus: "SUPPORTED" | "UNCONFIGURED" | "UNAVAILABLE";
414 };
415
416 export type PortfolioImportPreview = {
417 portfolioName: string;
418 broker: string;
419 sourceType: "MANUAL_CSV_IMPORT";
420 rowsDetected: number;
421 validHoldings: number;
422 rejectedRows: number;
423 columnsMapped: string[];
424 issues: string[];
425 updatesExistingPortfolio: boolean;
426 parserSupported: boolean;
427 statementAt?: string | null;
428 currency: string;
429 holdings: Array<{ companyName: string; symbol: string; quantity: number; averageCost: number;
430 importedPrice: number; marketValue: number; unrealizedPnl: number }>;
431 };
432
433 export type PortfolioImportResult = {
434 portfolioId: string;
435 portfolioName: string;
436 broker: string;
437 sourceType: "MANUAL_CSV_IMPORT";
438 importedAt: string;
439 rowCount: number;
440 acceptedCount: number;
441 rejectedCount: number;
442 updatedExistingPortfolio: boolean;
443 };
444
445 export type BrokerAuthenticationAction = {
446 connectionId: string;
447 provider: string;
448 status: string;
449 action: "NONE" | "REDIRECT_REQUIRED" | "POPUP_REQUIRED" | "CALLBACK_PENDING"
450 | "CONSENT_REQUIRED" | "AUTHENTICATION_REQUIRED" | "PARTNER_AUTH_UNAVAILABLE"
451 | "UNAVAILABLE" | "UNSUPPORTED";
452 authenticationUrl?: string | null;
453 message: string;
454 };
455
456 export type BrokerAuthStatus = {
457 connectionId: string;
458 state: string;
459 authenticated: boolean;
460 };
461
462 export type BrokerCredentialStatus = {
463 connectionId: string;
464 provider: string;
465 configured: boolean;
466 requiredFields: string[];
467 message: string;
468 };
469
470 export type BrokerPortfolioSync = {
471 connectionId: string;
472 provider?: string | null;
473 status: string;
474 action: string;
475 message: string;
476 completedAt: string;
477 portfolios: PortfolioListItem[];
478 };
479
480 export type BrokerConnection = {
481 connectionId: string;
482 brokerType: string;
483 displayName: string;
484 status: string;
485 connectedAt?: string | null;
486 lastSuccessfulSyncAt?: string | null;
487 lastSyncAttemptAt?: string | null;
488 lastErrorCode?: string | null;
489 createdAt: string;
490 updatedAt: string;
491 capabilities: string[];
492 providerStatus: string;
493 dataFreshness: string;
494 readOnly: boolean;
495 };
496
497 export type ApiFailure = {
498 message: string;
499 correlationId?: string;
500 status?: number;
501 };
502
503 export type AuthenticatedUser = {
504 userId: string;
505 issuer: string;
506 subject: string;
507 email?: string | null;
508 displayName?: string | null;
509 roles: string[];
510 };
511
512 export type LoginResponse = {
513 accessToken: string;
514 tokenType: "Bearer";
515 expiresAt: string;
516 user: AuthenticatedUser;
517 };
518 export type RegistrationResponse = { userId: string; status: "EMAIL_VERIFICATION_PENDING" };
519
520 export type ResearchProfile = {
521 instrumentId: string;
522 companyId: string;
523 companyName: string;
524 aliases: string[];
525 isin?: string | null;
526 ticker: string;
527 exchange: string;
528 mic: string;
529 country: string;
530 currency: string;
531 };
532
533 export type ResearchEvent = {
534 eventId: string;
535 instrumentId: string;
536 companyId: string;
537 eventType: string;
538 eventDate?: string | null;
539 detectedAt: string;
540 title: string;
541 summary: string;
542 sourceDocumentId: string;
543 sourceUrl: string;
544 sourceType: string;
545 sourceClassification?: string;
546 reliability: string;
547 sourceMode: "DEMO" | "REAL";
548 confidence: number;
549 impact: string;
550 timeHorizon: string;
551 currency?: string | null;
552 monetaryValue?: number | null;
553 monetaryOriginal?: string | null;
554 percentageValue?: number | null;
555 percentageOriginal?: string | null;
556 customer?: string | null;
557 counterparty?: string | null;
558 location?: string | null;
559 capacityValue?: number | null;
560 capacityUnit?: string | null;
561 status: string;
562 rawEvidenceReference: string;
563 publishedAt?: string | null;
564 retrievedAt?: string | null;
565 supportingSources?: ResearchEvidenceSource[];
566 independenceKey?: string | null;
567 };
568
569 export type ResearchEvidenceSource = {
570 publisher?: string | null;
571 url: string;
572 sourceType: string;
573 publishedAt?: string | null;
574 retrievedAt: string;
575 reliability: string;
576 sourceMode: "DEMO" | "REAL";
577 documentId: string;
578 sourceName: string;
579 canonicalUrl: string;
580 independent: boolean;
581 };
582
583 export type ResearchDocument = {
584 documentId: string;
585 canonicalUrl: string;
586 originalUrl: string;
587 title?: string | null;
588 sourceType: string;
589 sourceClassification?: string;
590 sourceName: string;
591 publisher?: string | null;
592 publishedAt?: string | null;
593 retrievedAt: string;
594 language?: string | null;
595 contentType: string;
596 documentType: string;
597 contentHash: string;
598 instrumentId?: string | null;
599 companyId?: string | null;
600 country?: string | null;
601 exchange?: string | null;
602 status: string;
603 reliabilityLevel: string;
604 sourceMode: "DEMO" | "REAL";
605 freshness: string;
606 entityResolutionConfidence: number;
607 discoveredAt?: string | null;
608 discoveryProvider?: string | null;
609 sourceIndependenceKey?: string | null;
610 duplicateOfDocumentId?: string | null;
611 };
612
613 export type CatalystScore = {
614 instrumentId: string;
615 overallScore: number;
616 buckets: Record<string, number | null>;
617 categoryEvidence?: Record<
618 string,
619 {
620 category: string;
621 status: "POSITIVE_EVIDENCE" | "NEUTRAL_EVIDENCE" | "NEGATIVE_EVIDENCE" | "MIXED_EVIDENCE" | "NO_EVIDENCE";
622 score?: number | null;
623 eventCount: number;
624 sourceCount: number;
625 independentSourceCount?: number;
626 hasConflict?: boolean;
627 supportingEvents?: ResearchEvent[];
628 }
629 >;
630 aggregationRule?: string;
631 researchConfidence: number;
632 generatedAt: string;
633 };
634
635 export type ResearchSummary = {
636 profile: ResearchProfile;
637 catalystScore: CatalystScore;
638 recentEvents: ResearchEvent[];
639 documents: ResearchDocument[];
640 lastRefreshAt?: string | null;
641 dataFreshness: string;
642 demo: boolean;
643 sourceMix: Record<string, number>;
644 };
645
646 export type ProvenancedValue = {
647 value: unknown;
648 unit?: string | null;
649 asOfDate?: string | null;
650 period?: string | null;
651 sourceUrl: string;
652 sourceName: string;
653 sourceType?: string | null;
654 publishedAt?: string | null;
655 retrievedAt: string;
656 confidence?: number | null;
657 calculationBasis?: string | null;
658 };
659
660 export type QuarterlyResult = {
661 period: string; resultDate?: string | null;
662 documentTitle?: string | null; extractionStatus?: string | null; reportingBasis?: "CONSOLIDATED" | "STANDALONE" | null;
663 revenue?: ProvenancedValue | null; revenueYoYPercent?: ProvenancedValue | null; revenueQoQPercent?: ProvenancedValue | null;
664 ebitda?: ProvenancedValue | null; ebitdaMargin?: ProvenancedValue | null; ebitdaYoYPercent?: ProvenancedValue | null;
665 pat?: ProvenancedValue | null; patYoYPercent?: ProvenancedValue | null; patQoQPercent?: ProvenancedValue | null;
666 eps?: ProvenancedValue | null; sourceName: string; sourceUrl: string; sourceType: string;
667 debtOrBorrowings?: ProvenancedValue | null; exceptionalItems?: string | null; segmentInformation?: string | null;
668 managementCommentary?: string[]; yoySummary?: string | null;
669 nim?: ProvenancedValue | null; roa?: ProvenancedValue | null; roe?: ProvenancedValue | null;
670 grossNpa?: ProvenancedValue | null; netNpa?: ProvenancedValue | null; deposits?: ProvenancedValue | null;
671 advances?: ProvenancedValue | null; capitalAdequacy?: ProvenancedValue | null; creditCost?: ProvenancedValue | null;
672 publishedAt?: string | null; retrievedAt: string; confidence: number;
673 };
674
675 export type FinancialResultPeriod = {
676 period: string;
677 periodType: string;
678 reportingBasis?: string | null;
679 revenue?: ProvenancedValue | null;
680 operatingIncome?: ProvenancedValue | null;
681 ebit?: ProvenancedValue | null;
682 ebitda?: ProvenancedValue | null;
683 pat?: ProvenancedValue | null;
684 eps?: ProvenancedValue | null;
685 sourceName: string;
686 sourceUrl: string;
687 sourceType: string;
688 publishedAt?: string | null;
689 retrievedAt: string;
690 confidence: number;
691 };
692
693 export type FinancialStatementPeriod = {
694 period: string;
695 periodType: string;
696 reportingBasis?: string | null;
697 metrics: Record<string, ProvenancedValue>;
698 };
699
700 export type ShareholdingChange = {
701 category: string; current: ProvenancedValue; previous: ProvenancedValue;
702 currentPeriod: string; previousPeriod: string; changePercentagePoints: string; sourceDate?: string | null;
703 };
704
705 export type ShareholdingSnapshot = {
706 id: string; instrumentId: string; periodEnd: string; filingBasis?: string | null;
707 sourceProvider: string; sourceType: string; sourceUrl: string; publishedAt?: string | null;
708 retrievedAt: string; confidence: string | number; reliabilityLevel: string; sourceMode: string;
709 values: Array<{ category: string; percentage: string; metricBasis?: string | null; rawSourceLabel?: string | null; sourceLocator?: string | null; evidenceText?: string | null }>;
710 };
711
712 export type ValuationAssessment = {
713 state: "CHEAP" | "FAIR" | "EXPENSIVE" | "UNKNOWN"; reason: string;
714 currentPe?: ProvenancedValue | null; sectorPe?: ProvenancedValue | null; peerPe?: ProvenancedValue | null;
715 historicalPe?: ProvenancedValue | null; roe?: ProvenancedValue | null; roce?: ProvenancedValue | null;
716 };
717
718 export type StructuredMarketSnapshot = {
719 resolution: { providerTicker: string; companyName: string; exchange?: string | null; currency?: string | null; quoteType?: string | null; status: string };
720 status: string;
721 retrievedAt: string;
722 marketAsOf?: string | null;
723 sourceName: string;
724 sourceType: string;
725 sourceUrl: string;
726 facts: Record<string, ProvenancedValue>;
727 news: Array<{ headline: string; publisher: string; url: string; publishedAt?: string | null; retrievedAt: string }>;
728 acceptedFieldsCount: number;
729 safeErrorCode?: string | null;
730 };
731
732 export type EtfResearchProfile = {
733 instrumentId: string;
734 fundId: string;
735 fundName: string;
736 ticker: string;
737 exchange: string;
738 mic: string;
739 provider?: string | null;
740 providerInstrumentId?: string | null;
741 isin?: string | null;
742 currency?: string | null;
743 fundProvider?: string | null;
744 underlyingIndex?: string | null;
745 knownDomains: string[];
746 facts: Record<string, ProvenancedValue>;
747 };
748
749 export type PortfolioResearchCompany = {
750 instrumentId?: string | null;
751 companyId?: string | null;
752 companyName: string;
753 ticker?: string | null;
754 exchange?: string | null;
755 isin?: string | null;
756 provider?: string | null;
757 providerInstrumentId?: string | null;
758 assetType?: string | null;
759 status: string;
760 catalystScore?: number | null;
761 confidence?: number | null;
762 evidenceCoverage: Record<string, string>;
763 latestEvent?: ResearchEvent | null;
764 positiveEventsCount: number;
765 negativeEventsCount: number;
766 neutralEventsCount: number;
767 documentCount: number;
768 eventCount: number;
769 sourceCount: number;
770 lastRefresh?: string | null;
771 freshness: string;
772 mode: string;
773 missingCategories: string[];
774 etfProfile?: EtfResearchProfile | null;
775 currentPrice?: string | null;
776 entryZoneLow?: string | null;
777 entryZoneHigh?: string | null;
778 target1?: string | null;
779 target2?: string | null;
780 riskInvalidationLevel?: string | null;
781 potentialUpsidePct?: string | null;
782 potentialDownsidePct?: string | null;
783 riskRewardRatio?: string | null;
784 safeErrorCode?: string | null;
785 safeErrorMessage?: string | null;
786 latestQuarterlyResult?: QuarterlyResult | null;
787 financialResultHistory: FinancialResultPeriod[];
788 balanceSheetHistory: FinancialStatementPeriod[];
789 cashFlowHistory: FinancialStatementPeriod[];
790 quarterlyResultStatus?: string;
791 structuredMarket?: StructuredMarketSnapshot | null;
792 structuredProviderStatus?: string | null;
793 priceFreshness?: string;
794 shareholdingChanges: ShareholdingChange[];
795 shareholdingSnapshots?: ShareholdingSnapshot[];
796 shareholdingFreshness?: string;
797 ownershipIncreases: Array<"PROMOTER" | "FII_FPI" | "DII">;
798 valuation: ValuationAssessment;
799 currentQuarterCatalysts: ResearchEvent[];
800 durableCategoryEvidence?: CatalystScore["categoryEvidence"];
801 sourceDiversity: { sourcesFound: number; domainsFound: number; officialSources: number; exchangeSources: number; companySources: number; secondarySources: number };
802 };
803
804 export type PortfolioResearchSummary = {
805 portfolioId: string;
806 generatedAt: string;
807 companiesRequested: number;
808 companiesResolved: number;
809 companiesSucceeded: number;
810 companiesDegraded: number;
811 companiesFailed: number;
812 documentsCreated: number;
813 eventsCreated: number;
814 deduplicatedCount: number;
815 companies: PortfolioResearchCompany[];
816 totalCompanies: number; completed: number; partial: number; failed: number; unsupported: number; inProgress: number;
817 };
818
819 type ApiErrorBody = {
820 message?: string;
821 correlationId?: string;
822 };
823
824 function currentAuthenticatedApiToken(): string | null {
825 return typeof window === "undefined" ? null : window.localStorage.getItem("aip.accessToken");
826 }
827
828 export async function request<T>(path: string, init?: RequestInit): Promise<T> {
829 const token = currentAuthenticatedApiToken();
830 const response = await fetch(`${frontendConfig.apiBaseUrl}${path}`, {
831 ...init,
832 headers: {
833 ...(init?.body instanceof FormData ? {} : { "Content-Type": "application/json" }),
834 "X-Correlation-Id": crypto.randomUUID(),
835 ...(token ? { Authorization: `Bearer ${token}` } : {}),
836 ...init?.headers
837 }
838 });
839
840 if (!response.ok) {
841 let body: ApiErrorBody = {};
842 try {
843 body = (await response.json()) as ApiErrorBody;
844 } catch {
845 body = {};
846 }
847
848 const failure: ApiFailure = {
849 message: body.message ?? "The request could not be completed.",
850 correlationId: body.correlationId ?? response.headers.get("X-Correlation-Id") ?? undefined,
851 status: response.status
852 };
853 if (response.status === 401 && typeof window !== "undefined") {
854 window.localStorage.removeItem("aip.accessToken");
855 window.localStorage.removeItem("aip.user");
856 window.dispatchEvent(new Event("aip:unauthorized"));
857 }
858 throw failure;
859 }
860
861 if (response.status === 204) {
862 return undefined as T;
863 }
864
865 return (await response.json()) as T;
866 }
867
868 export const authApi = {
869 register: (payload: { email: string; password: string; firstName: string; lastName: string }) =>
870 request<RegistrationResponse>("/api/v1/auth/register", { method: "POST", body: JSON.stringify(payload) }),
871 login: (email: string, password: string) =>
872 request<LoginResponse>("/api/v1/auth/login", { method: "POST", body: JSON.stringify({ email, password }) }),
873 verifyEmail: (token: string) => request<{ userId: string; status: "ACTIVE" }>("/api/v1/auth/verify-email", { method: "POST", body: JSON.stringify({ token }) }),
874 resendVerification: (email: string) => request<{ message: string }>("/api/v1/auth/resend-verification", { method: "POST", body: JSON.stringify({ email }) }),
875 requestPasswordReset: (email: string) => request<{ message: string }>("/api/v1/auth/password-reset/request", { method: "POST", body: JSON.stringify({ email }) }),
876 confirmPasswordReset: (token: string, newPassword: string) => request<void>("/api/v1/auth/password-reset/confirm", { method: "POST", body: JSON.stringify({ token, newPassword }) }),
877 loginDev: (userKey: "user-a" | "user-b") =>
878 request<LoginResponse>("/api/v1/auth/dev/login", {
879 method: "POST",
880 body: JSON.stringify({ userKey })
881 })
882 };
883
884 export const portfolioApi = {
885 listPortfolios: () => request<PortfolioListItem[]>("/api/v1/portfolios"),
886 getDashboard: () => request<PortfolioDashboard>("/api/v1/portfolios/dashboard"),
887 ensureMarketData: (region: "INDIA") =>
888 request<MarketDataEnsureStatus>(`/api/v1/research/market-data/ensure?region=${region}`, { method: "POST" }),
889 getMarketUniverseSectors: (region: SectorPerformance["region"]) => {
890 const params = new URLSearchParams({ region });
891 return request<MarketUniverseSectors>(`/api/v1/research/market-universe/sectors?${params.toString()}`);
892 },
893 getSectorPerformance: (region: SectorPerformance["region"], sector: string, period: SectorPerformance["period"]) => {
894 // URLSearchParams represents spaces as '+'. This remains one decoded
895 // query value when the API gateway forwards the request.
896 const params = new URLSearchParams({ region, sector, period, limit: "5" });
897 return request<SectorPerformance>(`/api/v1/research/sector-performance?${params.toString()}`);
898 },
899 createPortfolio: (payload: { name: string; baseCurrency: string }) =>
900 request<Portfolio>("/api/v1/portfolios", {
901 method: "POST",
902 body: JSON.stringify(payload)
903 }),
904 getPortfolio: (portfolioId: string) => request<Portfolio>(`/api/v1/portfolios/${portfolioId}`),
905 getPositions: (portfolioId: string) =>
906 request<PortfolioPosition[]>(`/api/v1/portfolios/${portfolioId}/positions`),
907 refreshPrices: (portfolioId: string) =>
908 request<{ portfolioId: string; refreshed: number; failed: number; skipped: number; completedAt: string }>(
909 `/api/v1/portfolios/${portfolioId}/prices/refresh`, { method: "POST" }
910 ),
911 updateHoldingDisplayName: (portfolioId: string, positionId: string, customDisplayName: string | null) =>
912 request<PortfolioPosition>(`/api/v1/portfolios/${portfolioId}/positions/${positionId}/display-name`, {
913 method: "PUT",
914 body: JSON.stringify({ customDisplayName })
915 }),
916 getSummary: (portfolioId: string) =>
917 request<PortfolioSummary>(`/api/v1/portfolios/${portfolioId}/summary`),
918 getHistory: (portfolioId: string, range: PortfolioHistoryRange) =>
919 request<PortfolioHistory>(`/api/v1/portfolios/${portfolioId}/history?range=${encodeURIComponent(range)}`),
920 syncPortfolio: (portfolioId: string) =>
921 request<PortfolioSummary>(`/api/v1/portfolios/${portfolioId}/sync`, { method: "POST" }),
922 importBrokerConnection: (portfolioId: string, connectionId: string) =>
923 request<PortfolioSummary>(`/api/v1/portfolios/${portfolioId}/broker-connections/${connectionId}/import`, { method: "POST" }),
924 syncBrokerConnection: (connectionId: string) =>
925 request<BrokerPortfolioSync>(`/api/v1/portfolios/broker-connections/${connectionId}/sync`, { method: "POST" }),
926 previewImport: (brokerType: string, file: File, portfolioName?: string) => {
927 const body = new FormData();
928 body.append("file", file);
929 if (portfolioName?.trim()) body.append("portfolioName", portfolioName.trim());
930 return request<PortfolioImportPreview>(`/api/v1/portfolios/imports/${brokerType.toLowerCase()}/preview`, { method: "POST", body });
931 },
932 confirmImport: (brokerType: string, file: File, portfolioName?: string) => {
933 const body = new FormData();
934 body.append("file", file);
935 if (portfolioName?.trim()) body.append("portfolioName", portfolioName.trim());
936 return request<PortfolioImportResult>(`/api/v1/portfolios/imports/${brokerType.toLowerCase()}/confirm`, { method: "POST", body });
937 }
938 };
939
940 export const brokerApi = {
941 listBrokers: () => request<BrokerProviderInfo[]>("/api/v1/brokers"),
942 listConnections: () => request<BrokerConnection[]>("/api/v1/broker-connections"),
943 getConnectionStatus: (connectionId: string) =>
944 request<BrokerConnection>(`/api/v1/broker-connections/${connectionId}/status`),
945 getAuthStatus: (connectionId: string) =>
946 request<BrokerAuthStatus>(`/api/v1/broker-connections/${connectionId}/auth-status`),
947 connectMock: () => request<BrokerConnection>("/api/v1/broker-connections/mock", { method: "POST" }),
948 connectBroker: (brokerType: string) =>
949 request<BrokerConnection>(`/api/v1/broker-connections/${brokerType.toLowerCase()}/connect`, { method: "POST" }),
950 authenticationAction: (connectionId: string) =>
951 request<BrokerAuthenticationAction>(`/api/v1/broker-connections/${connectionId}/authentication-action`),
952 configureCredentials: (connectionId: string, clientKey: string, clientSecret: string) =>
953 request<BrokerCredentialStatus>(`/api/v1/broker-connections/${connectionId}/credentials`, {
954 method: "PUT", body: JSON.stringify({ clientKey, clientSecret })
955 }),
956 attachIciciSession: (connectionId: string, apiSession: string) =>
957 request<BrokerConnection>(`/api/v1/broker-connections/${connectionId}/icici-session`, {
958 method: "POST", body: JSON.stringify({ apiSession })
959 }),
960 attachHdfcRequestToken: (connectionId: string, requestToken: string) =>
961 request<BrokerConnection>(`/api/v1/broker-connections/${connectionId}/hdfc-request-token`, {
962 method: "POST", body: JSON.stringify({ requestToken })
963 }),
964 syncConnection: (connectionId: string) =>
965 request<BrokerConnection>(`/api/v1/broker-connections/${connectionId}/sync`, { method: "POST" }),
966 connectorLogin: (connectorId: string) =>
967 request<{ connectorId: string; loginUrl?: string | null; authStatus: string }>(`/api/v1/broker-connectors/${connectorId}/login`),
968 disconnectConnection: (connectionId: string) =>
969 request<void>(`/api/v1/broker-connections/${connectionId}`, { method: "DELETE" })
970 };
971
972 export const researchApi = {
973 listCompanies: () => request<ResearchProfile[]>("/api/v1/research/companies"),
974 getSummary: (instrumentId: string) => request<ResearchSummary>(`/api/v1/research/companies/${instrumentId}/summary`),
975 getCompanyPresentation: (instrumentId: string, region: SectorPerformance["region"]) => {
976 const params = new URLSearchParams({ region });
977 return request<PortfolioResearchCompany>(
978 `/api/v1/research/companies/${instrumentId}/presentation?${params.toString()}`
979 );
980 },
981 getEvents: (instrumentId: string, filters?: { eventType?: string; impact?: string; reliability?: string }) => {
982 const params = new URLSearchParams();
983 if (filters?.eventType) params.set("eventType", filters.eventType);
984 if (filters?.impact) params.set("impact", filters.impact);
985 if (filters?.reliability) params.set("reliability", filters.reliability);
986 const suffix = params.toString() ? `?${params}` : "";
987 return request<ResearchEvent[]>(`/api/v1/research/companies/${instrumentId}/events${suffix}`);
988 },
989 getDocuments: (instrumentId: string) =>
990 request<ResearchDocument[]>(`/api/v1/research/companies/${instrumentId}/documents`),
991 getReadiness: (globalInstrumentId: string) =>
992 request<ResearchReadiness>(`/api/v1/research/readiness/${globalInstrumentId}`),
993 ensureReadiness: (globalInstrumentId: string, requirements?: string[]) =>
994 request<ResearchReadiness>(`/api/v1/research/readiness/${globalInstrumentId}/ensure`, {
995 method: "POST",
996 body: JSON.stringify(requirements?.length ? { requirements } : {})
997 }),
998 analyze: (globalInstrumentId: string, allowPartial: boolean) =>
999 request<StockRuleEngineAnalysis>(`/api/v1/research/analysis/${globalInstrumentId}`, {
1000 method: "POST",
1001 body: JSON.stringify({ allowPartial })
1002 }),
1003 listWatchlists: () => request<ResearchWatchlist[]>("/api/v1/research/watchlists"),
1004 ensureDefaultWatchlist: (region: SectorPerformance["region"]) =>
1005 request<ResearchWatchlist>("/api/v1/research/watchlists/default/ensure", {
1006 method: "POST", body: JSON.stringify({ region })
1007 }),
1008 addWatchlistInstrument: (
1009 watchlistId: string,
1010 value: { globalInstrumentId: string; sourcePeriod?: SectorPerformance["period"] | null; sourcePerformancePct?: number | null }
1011 ) => request<{ globalInstrumentId: string }>(`/api/v1/research/watchlists/${watchlistId}/instruments`, {
1012 method: "POST", body: JSON.stringify(value)
1013 }),
1014 removeWatchlistInstrument: (watchlistId: string, globalInstrumentId: string) =>
1015 request<void>(`/api/v1/research/watchlists/${watchlistId}/instruments/${globalInstrumentId}`, { method: "DELETE" }),
1016 getWatchlistResearch: (watchlistId: string) =>
1017 request<WatchlistResearchPresentation>(`/api/v1/research/watchlists/${watchlistId}/research`),
1018 getPortfolioSummary: (portfolioId: string) =>
1019 request<PortfolioResearchSummary>(`/api/v1/research/portfolios/${portfolioId}/summary`),
1020 searchInstruments: (region: SectorPerformance["region"], query: string, limit = 20) => {
1021 const params = new URLSearchParams({ region, q: query, limit: String(limit) });
1022 return request<ResearchInstrumentMatch[]>(
1023 `/api/v1/research/instruments/search?${params.toString()}`
1024 ).then((results) => results.map((result) => ({ ...result, region })));
1025 },
1026 };