@robosystems/client 0.2.49 → 0.3.1

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