@robosystems/client 0.2.48 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/extensions/InvestorClient.d.ts +91 -0
  2. package/extensions/InvestorClient.js +223 -0
  3. package/extensions/InvestorClient.ts +501 -0
  4. package/extensions/LedgerClient.d.ts +213 -165
  5. package/extensions/LedgerClient.js +315 -393
  6. package/extensions/LedgerClient.ts +887 -564
  7. package/extensions/ReportClient.d.ts +77 -115
  8. package/extensions/ReportClient.js +124 -211
  9. package/extensions/ReportClient.ts +298 -358
  10. package/extensions/config.js +1 -1
  11. package/extensions/config.ts +1 -1
  12. package/extensions/graphql/client.d.ts +46 -0
  13. package/extensions/graphql/client.js +75 -0
  14. package/extensions/graphql/client.ts +82 -0
  15. package/extensions/graphql/generated/graphql.d.ts +1786 -0
  16. package/extensions/graphql/generated/graphql.js +2671 -0
  17. package/extensions/graphql/generated/graphql.ts +4491 -0
  18. package/extensions/graphql/queries/investor/holdings.d.ts +8 -0
  19. package/extensions/graphql/queries/investor/holdings.js +36 -0
  20. package/extensions/graphql/queries/investor/holdings.ts +34 -0
  21. package/extensions/graphql/queries/investor/portfolio.d.ts +4 -0
  22. package/extensions/graphql/queries/investor/portfolio.js +21 -0
  23. package/extensions/graphql/queries/investor/portfolio.ts +19 -0
  24. package/extensions/graphql/queries/investor/portfolios.d.ts +4 -0
  25. package/extensions/graphql/queries/investor/portfolios.js +29 -0
  26. package/extensions/graphql/queries/investor/portfolios.ts +27 -0
  27. package/extensions/graphql/queries/investor/position.d.ts +4 -0
  28. package/extensions/graphql/queries/investor/position.js +33 -0
  29. package/extensions/graphql/queries/investor/position.ts +31 -0
  30. package/extensions/graphql/queries/investor/positions.d.ts +8 -0
  31. package/extensions/graphql/queries/investor/positions.js +57 -0
  32. package/extensions/graphql/queries/investor/positions.ts +55 -0
  33. package/extensions/graphql/queries/investor/securities.d.ts +8 -0
  34. package/extensions/graphql/queries/investor/securities.js +50 -0
  35. package/extensions/graphql/queries/investor/securities.ts +48 -0
  36. package/extensions/graphql/queries/investor/security.d.ts +4 -0
  37. package/extensions/graphql/queries/investor/security.js +26 -0
  38. package/extensions/graphql/queries/investor/security.ts +24 -0
  39. package/extensions/graphql/queries/ledger/accountRollups.d.ts +7 -0
  40. package/extensions/graphql/queries/ledger/accountRollups.js +36 -0
  41. package/extensions/graphql/queries/ledger/accountRollups.ts +34 -0
  42. package/extensions/graphql/queries/ledger/accountTree.d.ts +11 -0
  43. package/extensions/graphql/queries/ledger/accountTree.js +61 -0
  44. package/extensions/graphql/queries/ledger/accountTree.ts +59 -0
  45. package/extensions/graphql/queries/ledger/accounts.d.ts +8 -0
  46. package/extensions/graphql/queries/ledger/accounts.js +45 -0
  47. package/extensions/graphql/queries/ledger/accounts.ts +43 -0
  48. package/extensions/graphql/queries/ledger/closingBookStructures.d.ts +6 -0
  49. package/extensions/graphql/queries/ledger/closingBookStructures.js +27 -0
  50. package/extensions/graphql/queries/ledger/closingBookStructures.ts +25 -0
  51. package/extensions/graphql/queries/ledger/elements.d.ts +6 -0
  52. package/extensions/graphql/queries/ledger/elements.js +56 -0
  53. package/extensions/graphql/queries/ledger/elements.ts +54 -0
  54. package/extensions/graphql/queries/ledger/entities.d.ts +7 -0
  55. package/extensions/graphql/queries/ledger/entities.js +31 -0
  56. package/extensions/graphql/queries/ledger/entities.ts +29 -0
  57. package/extensions/graphql/queries/ledger/entity.d.ts +14 -0
  58. package/extensions/graphql/queries/ledger/entity.js +55 -0
  59. package/extensions/graphql/queries/ledger/entity.ts +54 -0
  60. package/extensions/graphql/queries/ledger/fiscalCalendar.d.ts +8 -0
  61. package/extensions/graphql/queries/ledger/fiscalCalendar.js +35 -0
  62. package/extensions/graphql/queries/ledger/fiscalCalendar.ts +33 -0
  63. package/extensions/graphql/queries/ledger/mappedTrialBalance.d.ts +9 -0
  64. package/extensions/graphql/queries/ledger/mappedTrialBalance.js +29 -0
  65. package/extensions/graphql/queries/ledger/mappedTrialBalance.ts +27 -0
  66. package/extensions/graphql/queries/ledger/mapping.d.ts +5 -0
  67. package/extensions/graphql/queries/ledger/mapping.js +35 -0
  68. package/extensions/graphql/queries/ledger/mapping.ts +33 -0
  69. package/extensions/graphql/queries/ledger/mappingCoverage.d.ts +5 -0
  70. package/extensions/graphql/queries/ledger/mappingCoverage.js +22 -0
  71. package/extensions/graphql/queries/ledger/mappingCoverage.ts +20 -0
  72. package/extensions/graphql/queries/ledger/mappings.d.ts +6 -0
  73. package/extensions/graphql/queries/ledger/mappings.js +23 -0
  74. package/extensions/graphql/queries/ledger/mappings.ts +21 -0
  75. package/extensions/graphql/queries/ledger/periodCloseStatus.d.ts +6 -0
  76. package/extensions/graphql/queries/ledger/periodCloseStatus.js +29 -0
  77. package/extensions/graphql/queries/ledger/periodCloseStatus.ts +27 -0
  78. package/extensions/graphql/queries/ledger/periodDrafts.d.ts +8 -0
  79. package/extensions/graphql/queries/ledger/periodDrafts.js +45 -0
  80. package/extensions/graphql/queries/ledger/periodDrafts.ts +43 -0
  81. package/extensions/graphql/queries/ledger/publishList.d.ts +5 -0
  82. package/extensions/graphql/queries/ledger/publishList.js +29 -0
  83. package/extensions/graphql/queries/ledger/publishList.ts +27 -0
  84. package/extensions/graphql/queries/ledger/publishLists.d.ts +5 -0
  85. package/extensions/graphql/queries/ledger/publishLists.js +29 -0
  86. package/extensions/graphql/queries/ledger/publishLists.ts +27 -0
  87. package/extensions/graphql/queries/ledger/report.d.ts +5 -0
  88. package/extensions/graphql/queries/ledger/report.js +40 -0
  89. package/extensions/graphql/queries/ledger/report.ts +38 -0
  90. package/extensions/graphql/queries/ledger/reportingTaxonomy.d.ts +6 -0
  91. package/extensions/graphql/queries/ledger/reportingTaxonomy.js +27 -0
  92. package/extensions/graphql/queries/ledger/reportingTaxonomy.ts +25 -0
  93. package/extensions/graphql/queries/ledger/reports.d.ts +8 -0
  94. package/extensions/graphql/queries/ledger/reports.js +45 -0
  95. package/extensions/graphql/queries/ledger/reports.ts +43 -0
  96. package/extensions/graphql/queries/ledger/scheduleFacts.d.ts +7 -0
  97. package/extensions/graphql/queries/ledger/scheduleFacts.js +24 -0
  98. package/extensions/graphql/queries/ledger/scheduleFacts.ts +22 -0
  99. package/extensions/graphql/queries/ledger/schedules.d.ts +6 -0
  100. package/extensions/graphql/queries/ledger/schedules.js +24 -0
  101. package/extensions/graphql/queries/ledger/schedules.ts +22 -0
  102. package/extensions/graphql/queries/ledger/statement.d.ts +9 -0
  103. package/extensions/graphql/queries/ledger/statement.js +43 -0
  104. package/extensions/graphql/queries/ledger/statement.ts +41 -0
  105. package/extensions/graphql/queries/ledger/structures.d.ts +5 -0
  106. package/extensions/graphql/queries/ledger/structures.js +22 -0
  107. package/extensions/graphql/queries/ledger/structures.ts +20 -0
  108. package/extensions/graphql/queries/ledger/summary.d.ts +7 -0
  109. package/extensions/graphql/queries/ledger/summary.js +25 -0
  110. package/extensions/graphql/queries/ledger/summary.ts +23 -0
  111. package/extensions/graphql/queries/ledger/taxonomies.d.ts +5 -0
  112. package/extensions/graphql/queries/ledger/taxonomies.js +28 -0
  113. package/extensions/graphql/queries/ledger/taxonomies.ts +26 -0
  114. package/extensions/graphql/queries/ledger/transaction.d.ts +6 -0
  115. package/extensions/graphql/queries/ledger/transaction.js +49 -0
  116. package/extensions/graphql/queries/ledger/transaction.ts +47 -0
  117. package/extensions/graphql/queries/ledger/transactions.d.ts +7 -0
  118. package/extensions/graphql/queries/ledger/transactions.js +49 -0
  119. package/extensions/graphql/queries/ledger/transactions.ts +47 -0
  120. package/extensions/graphql/queries/ledger/trialBalance.d.ts +7 -0
  121. package/extensions/graphql/queries/ledger/trialBalance.js +28 -0
  122. package/extensions/graphql/queries/ledger/trialBalance.ts +26 -0
  123. package/extensions/graphql/queries/ledger/unmappedElements.d.ts +8 -0
  124. package/extensions/graphql/queries/ledger/unmappedElements.js +29 -0
  125. package/extensions/graphql/queries/ledger/unmappedElements.ts +27 -0
  126. package/extensions/hooks.d.ts +1 -1
  127. package/extensions/index.d.ts +5 -1
  128. package/extensions/index.js +13 -1
  129. package/extensions/index.ts +14 -0
  130. package/index.ts +2 -2
  131. package/package.json +14 -5
  132. package/sdk/index.d.ts +2 -2
  133. package/sdk/index.js +38 -65
  134. package/sdk/index.ts +2 -2
  135. package/sdk/sdk.gen.d.ts +101 -220
  136. package/sdk/sdk.gen.js +251 -445
  137. package/sdk/sdk.gen.ts +216 -409
  138. package/sdk/types.gen.d.ts +4586 -7045
  139. package/sdk/types.gen.ts +4702 -7334
  140. package/sdk.gen.d.ts +101 -220
  141. package/sdk.gen.js +251 -445
  142. package/sdk.gen.ts +216 -409
  143. package/types.gen.d.ts +4586 -7045
  144. package/types.gen.ts +4702 -7334
  145. package/extensions/AgentClient.test.ts +0 -403
  146. package/extensions/LedgerClient.test.ts +0 -1138
  147. package/extensions/OperationClient.test.ts +0 -365
  148. package/extensions/QueryClient.test.ts +0 -432
  149. package/extensions/ReportClient.test.ts +0 -828
  150. package/extensions/SSEClient.test.ts +0 -375
  151. package/extensions/config.test.ts +0 -199
  152. package/extensions/hooks.test.ts +0 -373
  153. package/extensions/index.test.ts +0 -420
  154. package/sdk-extensions/AgentClient.d.ts +0 -82
  155. package/sdk-extensions/AgentClient.js +0 -218
  156. package/sdk-extensions/AgentClient.test.ts +0 -403
  157. package/sdk-extensions/AgentClient.ts +0 -321
  158. package/sdk-extensions/LedgerClient.d.ts +0 -242
  159. package/sdk-extensions/LedgerClient.js +0 -496
  160. package/sdk-extensions/LedgerClient.test.ts +0 -1138
  161. package/sdk-extensions/LedgerClient.ts +0 -800
  162. package/sdk-extensions/OperationClient.d.ts +0 -65
  163. package/sdk-extensions/OperationClient.js +0 -251
  164. package/sdk-extensions/OperationClient.test.ts +0 -365
  165. package/sdk-extensions/OperationClient.ts +0 -324
  166. package/sdk-extensions/QueryClient.d.ts +0 -52
  167. package/sdk-extensions/QueryClient.js +0 -313
  168. package/sdk-extensions/QueryClient.test.ts +0 -432
  169. package/sdk-extensions/QueryClient.ts +0 -420
  170. package/sdk-extensions/README.md +0 -901
  171. package/sdk-extensions/ReportClient.d.ts +0 -151
  172. package/sdk-extensions/ReportClient.js +0 -278
  173. package/sdk-extensions/ReportClient.test.ts +0 -828
  174. package/sdk-extensions/ReportClient.ts +0 -486
  175. package/sdk-extensions/SSEClient.d.ts +0 -62
  176. package/sdk-extensions/SSEClient.js +0 -154
  177. package/sdk-extensions/SSEClient.test.ts +0 -375
  178. package/sdk-extensions/SSEClient.ts +0 -210
  179. package/sdk-extensions/config.d.ts +0 -57
  180. package/sdk-extensions/config.js +0 -152
  181. package/sdk-extensions/config.test.ts +0 -199
  182. package/sdk-extensions/config.ts +0 -175
  183. package/sdk-extensions/hooks.d.ts +0 -110
  184. package/sdk-extensions/hooks.js +0 -384
  185. package/sdk-extensions/hooks.test.ts +0 -373
  186. package/sdk-extensions/hooks.ts +0 -459
  187. package/sdk-extensions/index.d.ts +0 -63
  188. package/sdk-extensions/index.js +0 -164
  189. package/sdk-extensions/index.test.ts +0 -420
  190. package/sdk-extensions/index.ts +0 -203
@@ -0,0 +1,501 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Investor Client for RoboSystems API
5
+ *
6
+ * High-level facade for the RoboInvestor domain: portfolios, securities,
7
+ * positions, and portfolio holdings aggregation. Follows the same
8
+ * hybrid transport pattern as `LedgerClient`:
9
+ *
10
+ * - **Reads** go through GraphQL at `/extensions/{graph_id}/graphql`.
11
+ * - **Writes** go through named operations at
12
+ * `/extensions/roboinvestor/{graph_id}/operations/{operation_name}`.
13
+ *
14
+ * Every write returns an `OperationEnvelope`; this facade unwraps
15
+ * `envelope.result` and returns a typed shape derived from the GraphQL
16
+ * codegen output (which is the source of truth for read payload types).
17
+ */
18
+
19
+ import type { TypedDocumentNode } from '@graphql-typed-document-node/core'
20
+ import { ClientError } from 'graphql-request'
21
+ import {
22
+ opCreatePortfolio,
23
+ opCreatePosition,
24
+ opCreateSecurity,
25
+ opDeletePortfolio,
26
+ opDeletePosition,
27
+ opDeleteSecurity,
28
+ opUpdatePortfolio,
29
+ opUpdatePosition,
30
+ opUpdateSecurity,
31
+ } from '../sdk.gen'
32
+ import type {
33
+ CreatePortfolioRequest,
34
+ CreatePositionRequest,
35
+ CreateSecurityRequest,
36
+ OperationEnvelope,
37
+ UpdatePortfolioOperation,
38
+ UpdatePositionOperation,
39
+ UpdateSecurityOperation,
40
+ } from '../types.gen'
41
+ import { GraphQLClientCache } from './graphql/client'
42
+ import {
43
+ GetInvestorHoldingsDocument,
44
+ GetInvestorPortfolioDocument,
45
+ GetInvestorPositionDocument,
46
+ GetInvestorSecurityDocument,
47
+ ListInvestorPortfoliosDocument,
48
+ ListInvestorPositionsDocument,
49
+ ListInvestorSecuritiesDocument,
50
+ type GetInvestorHoldingsQuery,
51
+ type GetInvestorPortfolioQuery,
52
+ type GetInvestorPositionQuery,
53
+ type GetInvestorSecurityQuery,
54
+ type ListInvestorPortfoliosQuery,
55
+ type ListInvestorPositionsQuery,
56
+ type ListInvestorSecuritiesQuery,
57
+ } from './graphql/generated/graphql'
58
+
59
+ // ── Friendly types derived from GraphQL codegen ────────────────────────
60
+
61
+ export type InvestorPortfolioList = NonNullable<ListInvestorPortfoliosQuery['portfolios']>
62
+ export type InvestorPortfolioSummary = InvestorPortfolioList['portfolios'][number]
63
+ export type InvestorPortfolio = NonNullable<GetInvestorPortfolioQuery['portfolio']>
64
+
65
+ export type InvestorSecurityList = NonNullable<ListInvestorSecuritiesQuery['securities']>
66
+ export type InvestorSecuritySummary = InvestorSecurityList['securities'][number]
67
+ export type InvestorSecurity = NonNullable<GetInvestorSecurityQuery['security']>
68
+
69
+ export type InvestorPositionList = NonNullable<ListInvestorPositionsQuery['positions']>
70
+ export type InvestorPositionSummary = InvestorPositionList['positions'][number]
71
+ export type InvestorPosition = NonNullable<GetInvestorPositionQuery['position']>
72
+
73
+ export type InvestorHoldings = NonNullable<GetInvestorHoldingsQuery['holdings']>
74
+ export type InvestorEntityHolding = InvestorHoldings['holdings'][number]
75
+ export type InvestorHeldSecurity = InvestorEntityHolding['securities'][number]
76
+
77
+ // ── Client ──────────────────────────────────────────────────────────────
78
+
79
+ interface InvestorClientConfig {
80
+ baseUrl: string
81
+ credentials?: 'include' | 'same-origin' | 'omit'
82
+ headers?: Record<string, string>
83
+ token?: string
84
+ }
85
+
86
+ export class InvestorClient {
87
+ private config: InvestorClientConfig
88
+ private gql: GraphQLClientCache
89
+
90
+ constructor(config: InvestorClientConfig) {
91
+ this.config = config
92
+ this.gql = new GraphQLClientCache(config)
93
+ }
94
+
95
+ // ── Portfolios ──────────────────────────────────────────────────────
96
+
97
+ /** List portfolios with pagination. */
98
+ async listPortfolios(
99
+ graphId: string,
100
+ options?: { limit?: number; offset?: number }
101
+ ): Promise<InvestorPortfolioList | null> {
102
+ return this.gqlQuery(
103
+ graphId,
104
+ ListInvestorPortfoliosDocument,
105
+ {
106
+ limit: options?.limit ?? 100,
107
+ offset: options?.offset ?? 0,
108
+ },
109
+ 'List portfolios',
110
+ (data) => data.portfolios
111
+ )
112
+ }
113
+
114
+ /** Get a single portfolio by id. Returns null if it doesn't exist. */
115
+ async getPortfolio(graphId: string, portfolioId: string): Promise<InvestorPortfolio | null> {
116
+ return this.gqlQuery(
117
+ graphId,
118
+ GetInvestorPortfolioDocument,
119
+ { portfolioId },
120
+ 'Get portfolio',
121
+ (data) => data.portfolio
122
+ )
123
+ }
124
+
125
+ /** Create a new portfolio. Returns the created portfolio. */
126
+ async createPortfolio(graphId: string, body: CreatePortfolioRequest): Promise<InvestorPortfolio> {
127
+ const envelope = await this.callOperation(
128
+ 'Create portfolio',
129
+ opCreatePortfolio({ path: { graph_id: graphId }, body })
130
+ )
131
+ return rawToInvestorPortfolio(envelope.result as unknown as RawPortfolioResponse)
132
+ }
133
+
134
+ /** Update a portfolio's metadata. Only provided fields are applied. */
135
+ async updatePortfolio(
136
+ graphId: string,
137
+ portfolioId: string,
138
+ updates: Omit<UpdatePortfolioOperation, 'portfolio_id'>
139
+ ): Promise<InvestorPortfolio> {
140
+ const envelope = await this.callOperation(
141
+ 'Update portfolio',
142
+ opUpdatePortfolio({
143
+ path: { graph_id: graphId },
144
+ body: {
145
+ ...updates,
146
+ portfolio_id: portfolioId,
147
+ } as UpdatePortfolioOperation,
148
+ })
149
+ )
150
+ return rawToInvestorPortfolio(envelope.result as unknown as RawPortfolioResponse)
151
+ }
152
+
153
+ /**
154
+ * Delete a portfolio. Fails with 409 if the portfolio still has active
155
+ * positions — dispose those first.
156
+ */
157
+ async deletePortfolio(graphId: string, portfolioId: string): Promise<{ deleted: boolean }> {
158
+ const envelope = await this.callOperation(
159
+ 'Delete portfolio',
160
+ opDeletePortfolio({
161
+ path: { graph_id: graphId },
162
+ body: { portfolio_id: portfolioId },
163
+ })
164
+ )
165
+ return (envelope.result ?? { deleted: true }) as { deleted: boolean }
166
+ }
167
+
168
+ // ── Securities ──────────────────────────────────────────────────────
169
+
170
+ /** List securities with pagination and filters. */
171
+ async listSecurities(
172
+ graphId: string,
173
+ options?: {
174
+ entityId?: string
175
+ securityType?: string
176
+ isActive?: boolean
177
+ limit?: number
178
+ offset?: number
179
+ }
180
+ ): Promise<InvestorSecurityList | null> {
181
+ return this.gqlQuery(
182
+ graphId,
183
+ ListInvestorSecuritiesDocument,
184
+ {
185
+ entityId: options?.entityId ?? null,
186
+ securityType: options?.securityType ?? null,
187
+ isActive: options?.isActive ?? null,
188
+ limit: options?.limit ?? 100,
189
+ offset: options?.offset ?? 0,
190
+ },
191
+ 'List securities',
192
+ (data) => data.securities
193
+ )
194
+ }
195
+
196
+ /** Get a single security by id. Returns null if it doesn't exist. */
197
+ async getSecurity(graphId: string, securityId: string): Promise<InvestorSecurity | null> {
198
+ return this.gqlQuery(
199
+ graphId,
200
+ GetInvestorSecurityDocument,
201
+ { securityId },
202
+ 'Get security',
203
+ (data) => data.security
204
+ )
205
+ }
206
+
207
+ /**
208
+ * Create a new security. Passing `source_graph_id` auto-links to the
209
+ * matching entity in the target graph.
210
+ */
211
+ async createSecurity(graphId: string, body: CreateSecurityRequest): Promise<InvestorSecurity> {
212
+ const envelope = await this.callOperation(
213
+ 'Create security',
214
+ opCreateSecurity({ path: { graph_id: graphId }, body })
215
+ )
216
+ return rawToInvestorSecurity(envelope.result as unknown as RawSecurityResponse)
217
+ }
218
+
219
+ /** Update a security's metadata. Only provided fields are applied. */
220
+ async updateSecurity(
221
+ graphId: string,
222
+ securityId: string,
223
+ updates: Omit<UpdateSecurityOperation, 'security_id'>
224
+ ): Promise<InvestorSecurity> {
225
+ const envelope = await this.callOperation(
226
+ 'Update security',
227
+ opUpdateSecurity({
228
+ path: { graph_id: graphId },
229
+ body: {
230
+ ...updates,
231
+ security_id: securityId,
232
+ } as UpdateSecurityOperation,
233
+ })
234
+ )
235
+ return rawToInvestorSecurity(envelope.result as unknown as RawSecurityResponse)
236
+ }
237
+
238
+ /** Soft-delete a security (sets is_active=false). */
239
+ async deleteSecurity(graphId: string, securityId: string): Promise<{ deleted: boolean }> {
240
+ const envelope = await this.callOperation(
241
+ 'Delete security',
242
+ opDeleteSecurity({
243
+ path: { graph_id: graphId },
244
+ body: { security_id: securityId },
245
+ })
246
+ )
247
+ return (envelope.result ?? { deleted: true }) as { deleted: boolean }
248
+ }
249
+
250
+ // ── Positions ───────────────────────────────────────────────────────
251
+
252
+ /** List positions with pagination and filters. */
253
+ async listPositions(
254
+ graphId: string,
255
+ options?: {
256
+ portfolioId?: string
257
+ securityId?: string
258
+ status?: string
259
+ limit?: number
260
+ offset?: number
261
+ }
262
+ ): Promise<InvestorPositionList | null> {
263
+ return this.gqlQuery(
264
+ graphId,
265
+ ListInvestorPositionsDocument,
266
+ {
267
+ portfolioId: options?.portfolioId ?? null,
268
+ securityId: options?.securityId ?? null,
269
+ status: options?.status ?? null,
270
+ limit: options?.limit ?? 100,
271
+ offset: options?.offset ?? 0,
272
+ },
273
+ 'List positions',
274
+ (data) => data.positions
275
+ )
276
+ }
277
+
278
+ /** Get a single position by id. Returns null if it doesn't exist. */
279
+ async getPosition(graphId: string, positionId: string): Promise<InvestorPosition | null> {
280
+ return this.gqlQuery(
281
+ graphId,
282
+ GetInvestorPositionDocument,
283
+ { positionId },
284
+ 'Get position',
285
+ (data) => data.position
286
+ )
287
+ }
288
+
289
+ /** Create a new position. */
290
+ async createPosition(graphId: string, body: CreatePositionRequest): Promise<InvestorPosition> {
291
+ const envelope = await this.callOperation(
292
+ 'Create position',
293
+ opCreatePosition({ path: { graph_id: graphId }, body })
294
+ )
295
+ return rawToInvestorPosition(envelope.result as unknown as RawPositionResponse)
296
+ }
297
+
298
+ /** Update a position. Only provided fields are applied. */
299
+ async updatePosition(
300
+ graphId: string,
301
+ positionId: string,
302
+ updates: Omit<UpdatePositionOperation, 'position_id'>
303
+ ): Promise<InvestorPosition> {
304
+ const envelope = await this.callOperation(
305
+ 'Update position',
306
+ opUpdatePosition({
307
+ path: { graph_id: graphId },
308
+ body: {
309
+ ...updates,
310
+ position_id: positionId,
311
+ } as UpdatePositionOperation,
312
+ })
313
+ )
314
+ return rawToInvestorPosition(envelope.result as unknown as RawPositionResponse)
315
+ }
316
+
317
+ /** Delete (dispose) a position. */
318
+ async deletePosition(graphId: string, positionId: string): Promise<{ deleted: boolean }> {
319
+ const envelope = await this.callOperation(
320
+ 'Delete position',
321
+ opDeletePosition({
322
+ path: { graph_id: graphId },
323
+ body: { position_id: positionId },
324
+ })
325
+ )
326
+ return (envelope.result ?? { deleted: true }) as { deleted: boolean }
327
+ }
328
+
329
+ // ── Holdings (aggregation) ─────────────────────────────────────────
330
+
331
+ /**
332
+ * Get portfolio holdings grouped by entity. Returns the full list of
333
+ * securities held and aggregate cost-basis / current-value for each
334
+ * entity in the portfolio.
335
+ */
336
+ async getHoldings(graphId: string, portfolioId: string): Promise<InvestorHoldings | null> {
337
+ return this.gqlQuery(
338
+ graphId,
339
+ GetInvestorHoldingsDocument,
340
+ { portfolioId },
341
+ 'Get holdings',
342
+ (data) => data.holdings
343
+ )
344
+ }
345
+
346
+ // ── Internal helpers ────────────────────────────────────────────────
347
+
348
+ private async gqlQuery<TData, TVars extends object, TResult>(
349
+ graphId: string,
350
+ document: TypedDocumentNode<TData, TVars>,
351
+ variables: TVars | undefined,
352
+ label: string,
353
+ pick: (data: TData) => TResult
354
+ ): Promise<TResult> {
355
+ try {
356
+ const client = this.gql.get(graphId)
357
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
358
+ const raw = client.request as (doc: unknown, vars?: unknown) => Promise<any>
359
+ const data = (await raw.call(client, document, variables)) as TData
360
+ return pick(data)
361
+ } catch (err) {
362
+ if (err instanceof ClientError) {
363
+ throw new Error(`${label} failed: ${JSON.stringify(err.response.errors ?? err.message)}`)
364
+ }
365
+ throw err
366
+ }
367
+ }
368
+
369
+ private async callOperation(
370
+ label: string,
371
+ call: Promise<{ data?: OperationEnvelope; error?: unknown }>
372
+ ): Promise<OperationEnvelope> {
373
+ const response = await call
374
+ if (response.error !== undefined) {
375
+ throw new Error(`${label} failed: ${JSON.stringify(response.error)}`)
376
+ }
377
+ if (response.data === undefined) {
378
+ throw new Error(`${label} failed: empty response`)
379
+ }
380
+ return response.data
381
+ }
382
+ }
383
+
384
+ // ── Module-private conversion helpers ─────────────────────────────────
385
+ //
386
+ // The RoboInvestor write operations (create/update portfolio, security,
387
+ // position) return Pydantic-serialized envelopes where `result` is in
388
+ // snake_case. The GraphQL-derived `InvestorPortfolio` / `InvestorSecurity`
389
+ // / `InvestorPosition` types are in camelCase (matching the read path).
390
+ //
391
+ // These helpers are the single place where we bridge the two naming
392
+ // conventions on the write path, mirroring the `rawFiscalCalendarToCamel`
393
+ // / `rawToClosingEntry` pattern in `LedgerClient`. Keeping the unsafe
394
+ // `as unknown as` cast localized to these helpers means a backend schema
395
+ // drift surfaces here rather than silently at every call site.
396
+
397
+ interface RawPortfolioResponse {
398
+ id: string
399
+ name: string
400
+ description: string | null
401
+ strategy: string | null
402
+ inception_date: string | null
403
+ base_currency: string
404
+ created_at: string
405
+ updated_at: string
406
+ }
407
+
408
+ interface RawSecurityResponse {
409
+ id: string
410
+ entity_id: string | null
411
+ entity_name: string | null
412
+ source_graph_id: string | null
413
+ name: string
414
+ security_type: string
415
+ security_subtype: string | null
416
+ terms: unknown
417
+ is_active: boolean
418
+ authorized_shares: number | null
419
+ outstanding_shares: number | null
420
+ created_at: string
421
+ updated_at: string
422
+ }
423
+
424
+ interface RawPositionResponse {
425
+ id: string
426
+ portfolio_id: string
427
+ security_id: string
428
+ security_name: string | null
429
+ entity_name: string | null
430
+ quantity: number
431
+ quantity_type: string
432
+ cost_basis: number
433
+ cost_basis_dollars: number
434
+ currency: string
435
+ current_value: number | null
436
+ current_value_dollars: number | null
437
+ valuation_date: string | null
438
+ valuation_source: string | null
439
+ acquisition_date: string | null
440
+ disposition_date: string | null
441
+ status: string
442
+ notes: string | null
443
+ created_at: string
444
+ updated_at: string
445
+ }
446
+
447
+ function rawToInvestorPortfolio(raw: RawPortfolioResponse): InvestorPortfolio {
448
+ return {
449
+ id: raw.id,
450
+ name: raw.name,
451
+ description: raw.description,
452
+ strategy: raw.strategy,
453
+ inceptionDate: raw.inception_date,
454
+ baseCurrency: raw.base_currency,
455
+ createdAt: raw.created_at,
456
+ updatedAt: raw.updated_at,
457
+ }
458
+ }
459
+
460
+ function rawToInvestorSecurity(raw: RawSecurityResponse): InvestorSecurity {
461
+ return {
462
+ id: raw.id,
463
+ entityId: raw.entity_id,
464
+ entityName: raw.entity_name,
465
+ sourceGraphId: raw.source_graph_id,
466
+ name: raw.name,
467
+ securityType: raw.security_type,
468
+ securitySubtype: raw.security_subtype,
469
+ terms: raw.terms,
470
+ isActive: raw.is_active,
471
+ authorizedShares: raw.authorized_shares,
472
+ outstandingShares: raw.outstanding_shares,
473
+ createdAt: raw.created_at,
474
+ updatedAt: raw.updated_at,
475
+ }
476
+ }
477
+
478
+ function rawToInvestorPosition(raw: RawPositionResponse): InvestorPosition {
479
+ return {
480
+ id: raw.id,
481
+ portfolioId: raw.portfolio_id,
482
+ securityId: raw.security_id,
483
+ securityName: raw.security_name,
484
+ entityName: raw.entity_name,
485
+ quantity: raw.quantity,
486
+ quantityType: raw.quantity_type,
487
+ costBasis: raw.cost_basis,
488
+ costBasisDollars: raw.cost_basis_dollars,
489
+ currency: raw.currency,
490
+ currentValue: raw.current_value,
491
+ currentValueDollars: raw.current_value_dollars,
492
+ valuationDate: raw.valuation_date,
493
+ valuationSource: raw.valuation_source,
494
+ acquisitionDate: raw.acquisition_date,
495
+ dispositionDate: raw.disposition_date,
496
+ status: raw.status,
497
+ notes: raw.notes,
498
+ createdAt: raw.created_at,
499
+ updatedAt: raw.updated_at,
500
+ }
501
+ }