@casual-simulation/aux-records 3.8.2-alpha.19511653187 → 3.10.3-alpha.20787554310

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 (239) hide show
  1. package/AIChatInterface.d.ts +7 -43
  2. package/AIChatInterface.js +8 -6
  3. package/AIChatInterface.js.map +1 -1
  4. package/AIController.js +44 -49
  5. package/AIController.js.map +1 -1
  6. package/AIOpenAIRealtimeInterface.d.ts +1 -1
  7. package/AnthropicAIChatInterface.js +4 -4
  8. package/AnthropicAIChatInterface.js.map +1 -1
  9. package/AuthController.d.ts +78 -10
  10. package/AuthController.js +230 -166
  11. package/AuthController.js.map +1 -1
  12. package/AuthStore.d.ts +317 -4
  13. package/BigIntPatch.d.ts +1 -0
  14. package/BigIntPatch.js +24 -0
  15. package/BigIntPatch.js.map +1 -0
  16. package/BlockadeLabsGenerateSkyboxInterface.js +4 -4
  17. package/BlockadeLabsGenerateSkyboxInterface.js.map +1 -1
  18. package/CachingConfigStore.d.ts +8 -1
  19. package/CachingConfigStore.js +50 -0
  20. package/CachingConfigStore.js.map +1 -1
  21. package/ComIdConfig.d.ts +132 -86
  22. package/ComIdConfig.js +5 -5
  23. package/ComIdConfig.js.map +1 -1
  24. package/ConfigurationStore.d.ts +1393 -3
  25. package/ConfigurationStore.js +49 -0
  26. package/ConfigurationStore.js.map +1 -1
  27. package/DataRecordsController.js +28 -28
  28. package/DataRecordsController.js.map +1 -1
  29. package/EventRecordsController.js +9 -9
  30. package/EventRecordsController.js.map +1 -1
  31. package/FileRecordsController.js +18 -17
  32. package/FileRecordsController.js.map +1 -1
  33. package/GoogleAIChatInterface.js +4 -4
  34. package/GoogleAIChatInterface.js.map +1 -1
  35. package/HttpTestUtils.d.ts +48 -0
  36. package/HttpTestUtils.js +256 -0
  37. package/HttpTestUtils.js.map +1 -0
  38. package/LivekitController.js +2 -2
  39. package/LivekitController.js.map +1 -1
  40. package/LoomController.js +3 -4
  41. package/LoomController.js.map +1 -1
  42. package/MemoryStore.d.ts +60 -7
  43. package/MemoryStore.js +427 -59
  44. package/MemoryStore.js.map +1 -1
  45. package/MetricsStore.d.ts +11 -0
  46. package/ModerationConfiguration.d.ts +11 -85
  47. package/ModerationConfiguration.js +17 -17
  48. package/ModerationConfiguration.js.map +1 -1
  49. package/ModerationController.js +9 -11
  50. package/ModerationController.js.map +1 -1
  51. package/OpenAIChatInterface.js +8 -13
  52. package/OpenAIChatInterface.js.map +1 -1
  53. package/OpenAIImageInterface.js +4 -5
  54. package/OpenAIImageInterface.js.map +1 -1
  55. package/PolicyController.js +66 -73
  56. package/PolicyController.js.map +1 -1
  57. package/PolicyStore.d.ts +59 -33
  58. package/PolicyStore.js +35 -1
  59. package/PolicyStore.js.map +1 -1
  60. package/PrivoClient.d.ts +3 -1
  61. package/PrivoClient.js +2 -4
  62. package/PrivoClient.js.map +1 -1
  63. package/PrivoConfiguration.d.ts +6 -72
  64. package/PrivoConfiguration.js +30 -31
  65. package/PrivoConfiguration.js.map +1 -1
  66. package/README.md +276 -2
  67. package/RateLimitController.js +2 -2
  68. package/RateLimitController.js.map +1 -1
  69. package/RecordsClient.d.ts +3 -1
  70. package/RecordsClient.js +6 -6
  71. package/RecordsClient.js.map +1 -1
  72. package/RecordsController.d.ts +145 -3
  73. package/RecordsController.js +399 -61
  74. package/RecordsController.js.map +1 -1
  75. package/RecordsServer.d.ts +1932 -1109
  76. package/RecordsStore.d.ts +99 -14
  77. package/RecordsStore.js +8 -10
  78. package/RecordsStore.js.map +1 -1
  79. package/ServerConfig.d.ts +698 -9867
  80. package/ServerConfig.js +457 -377
  81. package/ServerConfig.js.map +1 -1
  82. package/SloydInterface.js +1 -1
  83. package/SloydInterface.js.map +1 -1
  84. package/StabilityAIImageInterface.js +6 -9
  85. package/StabilityAIImageInterface.js.map +1 -1
  86. package/StripeInterface.d.ts +431 -287
  87. package/StripeInterface.js +21 -1
  88. package/StripeInterface.js.map +1 -1
  89. package/SubscriptionConfigBuilder.d.ts +10 -1
  90. package/SubscriptionConfigBuilder.js +72 -41
  91. package/SubscriptionConfigBuilder.js.map +1 -1
  92. package/SubscriptionConfiguration.d.ts +606 -6334
  93. package/SubscriptionConfiguration.js +460 -277
  94. package/SubscriptionConfiguration.js.map +1 -1
  95. package/SubscriptionController.d.ts +677 -4
  96. package/SubscriptionController.js +2994 -187
  97. package/SubscriptionController.js.map +1 -1
  98. package/SystemNotificationMessenger.d.ts +14 -70
  99. package/SystemNotificationMessenger.js +17 -20
  100. package/SystemNotificationMessenger.js.map +1 -1
  101. package/TestUtils.d.ts +18 -3
  102. package/TestUtils.js +84 -8
  103. package/TestUtils.js.map +1 -1
  104. package/TypeUtils.d.ts +991 -0
  105. package/TypeUtils.js +2 -0
  106. package/TypeUtils.js.map +1 -0
  107. package/Utils.d.ts +59 -0
  108. package/Utils.js +507 -3
  109. package/Utils.js.map +1 -1
  110. package/Validations.d.ts +24 -108
  111. package/Validations.js +62 -45
  112. package/Validations.js.map +1 -1
  113. package/ViewTemplateRenderer.d.ts +39 -0
  114. package/ViewTemplateRenderer.js +19 -0
  115. package/ViewTemplateRenderer.js.map +1 -0
  116. package/contracts/ContractRecordsController.d.ts +58 -0
  117. package/contracts/ContractRecordsController.js +144 -0
  118. package/contracts/ContractRecordsController.js.map +1 -0
  119. package/contracts/ContractRecordsStore.d.ts +285 -0
  120. package/contracts/ContractRecordsStore.js +19 -0
  121. package/contracts/ContractRecordsStore.js.map +1 -0
  122. package/contracts/MemoryContractRecordsStore.d.ts +27 -0
  123. package/contracts/MemoryContractRecordsStore.js +124 -0
  124. package/contracts/MemoryContractRecordsStore.js.map +1 -0
  125. package/contracts/index.d.ts +4 -0
  126. package/contracts/index.js +21 -0
  127. package/contracts/index.js.map +1 -0
  128. package/crud/CrudHelpers.d.ts +25 -26
  129. package/crud/CrudHelpers.js +1 -1
  130. package/crud/CrudHelpers.js.map +1 -1
  131. package/crud/CrudRecordsController.js +13 -16
  132. package/crud/CrudRecordsController.js.map +1 -1
  133. package/crud/CrudRecordsControllerTests.d.ts +2 -2
  134. package/crud/CrudRecordsControllerTests.js +605 -580
  135. package/crud/CrudRecordsControllerTests.js.map +1 -1
  136. package/crud/MemoryCrudRecordsStore.js +1 -2
  137. package/crud/MemoryCrudRecordsStore.js.map +1 -1
  138. package/crud/sub/MemorySubCrudRecordsStore.js +4 -6
  139. package/crud/sub/MemorySubCrudRecordsStore.js.map +1 -1
  140. package/crud/sub/SubCrudRecordsController.js +8 -8
  141. package/crud/sub/SubCrudRecordsController.js.map +1 -1
  142. package/database/DatabaseRecordsController.js +1 -2
  143. package/database/DatabaseRecordsController.js.map +1 -1
  144. package/database/SqliteDatabaseInterface.js +1 -2
  145. package/database/SqliteDatabaseInterface.js.map +1 -1
  146. package/dns/DNSDomainNameValidator.d.ts +11 -0
  147. package/dns/DNSDomainNameValidator.js +59 -0
  148. package/dns/DNSDomainNameValidator.js.map +1 -0
  149. package/dns/DomainNameValidator.d.ts +36 -0
  150. package/dns/DomainNameValidator.js +19 -0
  151. package/dns/DomainNameValidator.js.map +1 -0
  152. package/dns/index.d.ts +3 -0
  153. package/dns/index.js +20 -0
  154. package/dns/index.js.map +1 -0
  155. package/financial/FinancialController.d.ts +272 -0
  156. package/financial/FinancialController.js +762 -0
  157. package/financial/FinancialController.js.map +1 -0
  158. package/financial/FinancialInterface.d.ts +352 -0
  159. package/financial/FinancialInterface.js +642 -0
  160. package/financial/FinancialInterface.js.map +1 -0
  161. package/financial/FinancialStore.d.ts +186 -0
  162. package/financial/FinancialStore.js +19 -0
  163. package/financial/FinancialStore.js.map +1 -0
  164. package/financial/MemoryFinancialInterface.d.ts +23 -0
  165. package/financial/MemoryFinancialInterface.js +592 -0
  166. package/financial/MemoryFinancialInterface.js.map +1 -0
  167. package/financial/TigerBeetleFinancialInterface.d.ts +46 -0
  168. package/financial/TigerBeetleFinancialInterface.js +109 -0
  169. package/financial/TigerBeetleFinancialInterface.js.map +1 -0
  170. package/financial/TigerBeetleTestUtils.d.ts +40 -0
  171. package/financial/TigerBeetleTestUtils.js +185 -0
  172. package/financial/TigerBeetleTestUtils.js.map +1 -0
  173. package/financial/Types.d.ts +1 -0
  174. package/financial/Types.js +801 -0
  175. package/financial/Types.js.map +1 -0
  176. package/financial/index.d.ts +6 -0
  177. package/financial/index.js +24 -0
  178. package/financial/index.js.map +1 -0
  179. package/index.d.ts +4 -0
  180. package/index.js +3 -0
  181. package/index.js.map +1 -1
  182. package/notifications/NotificationRecordsController.js +14 -14
  183. package/notifications/NotificationRecordsController.js.map +1 -1
  184. package/notifications/WebPushInterface.d.ts +24 -155
  185. package/notifications/WebPushInterface.js +2 -2
  186. package/notifications/WebPushInterface.js.map +1 -1
  187. package/package.json +72 -70
  188. package/packages/version/MemoryPackageVersionRecordsStore.js +12 -15
  189. package/packages/version/MemoryPackageVersionRecordsStore.js.map +1 -1
  190. package/packages/version/PackageVersionRecordsController.d.ts +19 -0
  191. package/packages/version/PackageVersionRecordsController.js +102 -22
  192. package/packages/version/PackageVersionRecordsController.js.map +1 -1
  193. package/packages/version/PackageVersionRecordsStore.js +6 -8
  194. package/packages/version/PackageVersionRecordsStore.js.map +1 -1
  195. package/purchasable-items/MemoryPurchasableItemRecordsStore.d.ts +7 -0
  196. package/purchasable-items/MemoryPurchasableItemRecordsStore.js +33 -0
  197. package/purchasable-items/MemoryPurchasableItemRecordsStore.js.map +1 -0
  198. package/purchasable-items/PurchasableItemRecordsController.d.ts +16 -0
  199. package/purchasable-items/PurchasableItemRecordsController.js +72 -0
  200. package/purchasable-items/PurchasableItemRecordsController.js.map +1 -0
  201. package/purchasable-items/PurchasableItemRecordsStore.d.ts +66 -0
  202. package/purchasable-items/PurchasableItemRecordsStore.js +2 -0
  203. package/purchasable-items/PurchasableItemRecordsStore.js.map +1 -0
  204. package/purchasable-items/index.d.ts +4 -0
  205. package/purchasable-items/index.js +21 -0
  206. package/purchasable-items/index.js.map +1 -0
  207. package/queue/MemoryQueue.js +1 -1
  208. package/queue/MemoryQueue.js.map +1 -1
  209. package/search/MemorySearchInterface.js +1 -2
  210. package/search/MemorySearchInterface.js.map +1 -1
  211. package/search/SearchRecordsController.d.ts +66 -244
  212. package/search/SearchRecordsController.js +35 -36
  213. package/search/SearchRecordsController.js.map +1 -1
  214. package/search/SearchSyncProcessor.d.ts +7 -83
  215. package/search/TypesenseSearchInterface.js +8 -11
  216. package/search/TypesenseSearchInterface.js.map +1 -1
  217. package/tracing/TracingDecorators.js +5 -8
  218. package/tracing/TracingDecorators.js.map +1 -1
  219. package/webhooks/WebhookEnvironment.d.ts +36 -560
  220. package/webhooks/WebhookEnvironment.js +1 -1
  221. package/webhooks/WebhookEnvironment.js.map +1 -1
  222. package/webhooks/WebhookRecordsController.js +14 -16
  223. package/webhooks/WebhookRecordsController.js.map +1 -1
  224. package/websockets/MemoryLockStore.d.ts +18 -0
  225. package/websockets/MemoryLockStore.js +2 -0
  226. package/websockets/MemoryLockStore.js.map +1 -0
  227. package/websockets/MemoryTempInstRecordsStore.js +15 -27
  228. package/websockets/MemoryTempInstRecordsStore.js.map +1 -1
  229. package/websockets/MemoryWebsocketConnectionStore.js +6 -8
  230. package/websockets/MemoryWebsocketConnectionStore.js.map +1 -1
  231. package/websockets/MemoryWebsocketMessenger.js +1 -2
  232. package/websockets/MemoryWebsocketMessenger.js.map +1 -1
  233. package/websockets/SplitInstRecordsStore.js +2 -2
  234. package/websockets/SplitInstRecordsStore.js.map +1 -1
  235. package/websockets/WebsocketController.d.ts +5 -0
  236. package/websockets/WebsocketController.js +122 -83
  237. package/websockets/WebsocketController.js.map +1 -1
  238. package/RecordsServer.js +0 -6435
  239. package/RecordsServer.js.map +0 -1
@@ -5,22 +5,48 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { INVALID_KEY_ERROR_MESSAGE } from './AuthController';
8
- import { STRIPE_EVENT_INVOICE_PAID_SCHEMA } from './StripeInterface';
8
+ import { STRIPE_EVENT_ACCOUNT_UPDATED_SCHEMA, STRIPE_EVENT_CHECKOUT_SESSION_SCHEMA, STRIPE_EVENT_INVOICE_PAID_SCHEMA, } from './StripeInterface';
9
9
  import { isActiveSubscription } from './Utils';
10
+ import { getContractFeatures, getPurchasableItemsFeatures, } from './SubscriptionConfiguration';
10
11
  import { traced } from './tracing/TracingDecorators';
11
12
  import { SpanStatusCode, trace } from '@opentelemetry/api';
12
- import { isSuperUserRole } from '@casual-simulation/aux-common';
13
+ import { failure, genericResult, isFailure, isSuccess, isSuperUserRole, logError, success, wrap, } from '@casual-simulation/aux-common';
13
14
  const TRACE_NAME = 'SubscriptionController';
15
+ import { ADMIN_ROLE_NAME, fromBase64String, toBase64String, } from '@casual-simulation/aux-common';
16
+ import { v4 as uuid } from 'uuid';
17
+ import { hashHighEntropyPasswordWithSalt } from '@casual-simulation/crypto';
18
+ import { randomBytes } from 'tweetnacl';
19
+ import { fromByteArray } from 'base64-js';
20
+ import { ACCOUNT_IDS, ACCOUNT_NAMES, AMOUNT_MAX, convertBetweenLedgers, CURRENCIES, CurrencyCodes, getAccountBalance, getLiquidityAccountByLedger, LEDGERS, TransferCodes, } from './financial';
21
+ import { TransferFlags } from 'tigerbeetle-node';
22
+ /**
23
+ * The number of bytes that the access key secret should be.
24
+ */
25
+ export const ACCESS_KEY_SECRET_BYTE_LENGTH = 16; // 128-bit
26
+ /**
27
+ * The number of bytes that the access key ID should be.
28
+ */
29
+ export const ACCESS_KEY_ID_BYTE_LENGTH = 16; // 128-bit
30
+ /**
31
+ * The number of seconds to wait before a Stripe payout transfer times out.
32
+ */
33
+ export const STRIPE_PAYOUT_TIMEOUT_SECONDS = 60 * 5; // 5 minutes
14
34
  /**
15
35
  * Defines a class that is able to handle subscriptions.
16
36
  */
17
37
  export class SubscriptionController {
18
- constructor(stripe, auth, authStore, recordsStore, config) {
38
+ constructor(stripe, auth, authStore, recordsStore, config, policies, policyStore, purchasableItems, financialController, financialStore, contractRecords) {
19
39
  this._stripe = stripe;
20
40
  this._auth = auth;
21
41
  this._authStore = authStore;
22
42
  this._recordsStore = recordsStore;
23
43
  this._config = config;
44
+ this._policies = policies;
45
+ this._policyStore = policyStore;
46
+ this._purchasableItems = purchasableItems;
47
+ this._financialController = financialController;
48
+ this._financialStore = financialStore;
49
+ this._contractRecords = contractRecords;
24
50
  }
25
51
  async _getConfig() {
26
52
  return await this._config.getSubscriptionConfiguration();
@@ -30,7 +56,6 @@ export class SubscriptionController {
30
56
  * @param request
31
57
  */
32
58
  async getSubscriptionStatus(request) {
33
- var _a, _b;
34
59
  if (!this._stripe) {
35
60
  return {
36
61
  success: false,
@@ -74,6 +99,7 @@ export class SubscriptionController {
74
99
  };
75
100
  }
76
101
  const keyResult = await this._auth.validateSessionKey(request.sessionKey);
102
+ let accountBalances = success(undefined);
77
103
  let customerId;
78
104
  let role;
79
105
  if (keyResult.success === false) {
@@ -93,6 +119,12 @@ export class SubscriptionController {
93
119
  const user = await this._authStore.findUser(request.userId);
94
120
  customerId = user.stripeCustomerId;
95
121
  role = 'user';
122
+ if (this._financialController) {
123
+ accountBalances =
124
+ await this._financialController.getAccountBalances({
125
+ userId: request.userId,
126
+ });
127
+ }
96
128
  }
97
129
  else if (request.studioId) {
98
130
  const assignments = await this._recordsStore.listStudioAssignments(request.studioId, {
@@ -111,19 +143,30 @@ export class SubscriptionController {
111
143
  const studio = await this._recordsStore.getStudioById(request.studioId);
112
144
  customerId = studio.stripeCustomerId;
113
145
  role = 'studio';
146
+ if (this._financialController) {
147
+ accountBalances =
148
+ await this._financialController.getAccountBalances({
149
+ studioId: request.studioId,
150
+ });
151
+ }
114
152
  }
115
153
  }
154
+ if (isFailure(accountBalances)) {
155
+ logError(accountBalances.error, '[SubscriptionController] [getSubscriptionStatus] Failed to get account balances:');
156
+ return genericResult(accountBalances);
157
+ }
116
158
  // const user = await this._authStore.findUser(keyResult.userId);
117
159
  // let customerId = user.stripeCustomerId;
118
160
  if (!customerId) {
119
161
  const config = await this._getConfig();
120
162
  return {
121
163
  success: true,
122
- userId: (_a = request.userId) !== null && _a !== void 0 ? _a : keyResult.userId,
164
+ userId: request.userId ?? keyResult.userId,
123
165
  studioId: request.studioId,
124
166
  publishableKey: this._stripe.publishableKey,
125
167
  subscriptions: [],
126
168
  purchasableSubscriptions: await this._getPurchasableSubscriptions(role, config),
169
+ accountBalances: accountBalances.value,
127
170
  };
128
171
  }
129
172
  const listResult = await this._stripe.listActiveSubscriptionsForCustomer(customerId);
@@ -134,7 +177,7 @@ export class SubscriptionController {
134
177
  return (sub.eligibleProducts &&
135
178
  sub.eligibleProducts.some((p) => p === item.price.product.id));
136
179
  });
137
- const featureList = subscriptionInfo === null || subscriptionInfo === void 0 ? void 0 : subscriptionInfo.featureList;
180
+ const featureList = subscriptionInfo?.featureList;
138
181
  return {
139
182
  active: s.status === 'active',
140
183
  statusCode: s.status,
@@ -157,17 +200,18 @@ export class SubscriptionController {
157
200
  : await this._getPurchasableSubscriptions(role, config);
158
201
  return {
159
202
  success: true,
160
- userId: (_b = request.userId) !== null && _b !== void 0 ? _b : keyResult.userId,
203
+ userId: request.userId ?? keyResult.userId,
161
204
  studioId: request.studioId,
162
205
  publishableKey: this._stripe.publishableKey,
163
206
  subscriptions,
164
207
  purchasableSubscriptions,
208
+ accountBalances: accountBalances.value,
165
209
  };
166
210
  }
167
211
  catch (err) {
168
212
  const span = trace.getActiveSpan();
169
- span === null || span === void 0 ? void 0 : span.recordException(err);
170
- span === null || span === void 0 ? void 0 : span.setStatus({ code: SpanStatusCode.ERROR });
213
+ span?.recordException(err);
214
+ span?.setStatus({ code: SpanStatusCode.ERROR });
171
215
  console.error('[SubscriptionController] An error occurred while getting subscription status:', err);
172
216
  return {
173
217
  success: false,
@@ -176,6 +220,136 @@ export class SubscriptionController {
176
220
  };
177
221
  }
178
222
  }
223
+ /**
224
+ * Gets the account balances for the user/studio/contract.
225
+ * @param request
226
+ */
227
+ async getBalances(request) {
228
+ if (!this._financialController) {
229
+ return failure({
230
+ errorCode: 'not_supported',
231
+ errorMessage: 'This feature is not supported.',
232
+ });
233
+ }
234
+ const authorizationResult = await this._checkAuthorizationForFilter(request.filter, request.userId, request.userRole);
235
+ if (isFailure(authorizationResult)) {
236
+ return authorizationResult;
237
+ }
238
+ return await this._financialController.getAccountBalances(request.filter);
239
+ }
240
+ async _checkAuthorizationForFilter(filter, userId, userRole) {
241
+ if (!this._financialController) {
242
+ return failure({
243
+ errorCode: 'not_supported',
244
+ errorMessage: 'This feature is not supported.',
245
+ });
246
+ }
247
+ // Check if the user has permission to access this account
248
+ if (!isSuperUserRole(userRole)) {
249
+ // Users can only access their own accounts
250
+ if ('userId' in filter && filter.userId) {
251
+ if (filter.userId !== userId) {
252
+ return failure({
253
+ errorCode: 'not_authorized',
254
+ errorMessage: 'You are not authorized to perform this action.',
255
+ });
256
+ }
257
+ }
258
+ else if ('studioId' in filter && filter.studioId) {
259
+ const assignments = await this._recordsStore.listStudioAssignments(filter.studioId, {
260
+ role: 'admin',
261
+ });
262
+ const userAssignment = assignments.find((a) => a.userId === userId);
263
+ if (!userAssignment || userAssignment.role !== 'admin') {
264
+ return failure({
265
+ errorCode: 'not_authorized',
266
+ errorMessage: 'You are not authorized to perform this action.',
267
+ });
268
+ }
269
+ }
270
+ else if ('contractId' in filter && filter.contractId) {
271
+ const contract = await this._contractRecords.getItemById(filter.contractId);
272
+ if (!contract) {
273
+ return failure({
274
+ errorCode: 'not_found',
275
+ errorMessage: 'The contract was not found.',
276
+ });
277
+ }
278
+ // Holding and issuing users can read contract accounts by default.
279
+ // Other users need an explicit check
280
+ if (contract.contract.holdingUserId !== userId &&
281
+ contract.contract.issuingUserId !== userId) {
282
+ const context = await this._policies.constructAuthorizationContext({
283
+ recordKeyOrRecordName: contract.recordName,
284
+ userId: userId,
285
+ userRole: userRole,
286
+ });
287
+ if (context.success === false) {
288
+ return failure(context);
289
+ }
290
+ const authorization = await this._policies.authorizeSubject(context, {
291
+ action: 'read',
292
+ resourceKind: 'contract',
293
+ resourceId: contract.contract.address,
294
+ subjectType: 'user',
295
+ subjectId: userId,
296
+ markers: contract.contract.markers,
297
+ });
298
+ if (authorization.success === false) {
299
+ return failure(authorization);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ return success();
305
+ }
306
+ /**
307
+ * Lists the transfers for the given account.
308
+ * @param request The request.
309
+ */
310
+ async listAccountTransfers(request) {
311
+ if (!this._financialController) {
312
+ return failure({
313
+ errorCode: 'not_supported',
314
+ errorMessage: 'This feature is not supported.',
315
+ });
316
+ }
317
+ // Get the account details to verify it exists and get permissions info
318
+ const accountDetailsResult = await this._financialController.getAccountDetails(request.accountId);
319
+ if (isFailure(accountDetailsResult)) {
320
+ return accountDetailsResult;
321
+ }
322
+ const { account, financialAccount } = accountDetailsResult.value;
323
+ const authorizationResult = await this._checkAuthorizationForFilter(financialAccount, request.userId, request.userRole);
324
+ if (isFailure(authorizationResult)) {
325
+ return authorizationResult;
326
+ }
327
+ // Get the transfers for this account
328
+ const transfersResult = await this._financialController.listTransfers(request.accountId);
329
+ if (isFailure(transfersResult)) {
330
+ return transfersResult;
331
+ }
332
+ const transfers = transfersResult.value;
333
+ // Map Transfer objects to AccountTransfer objects
334
+ const accountTransfers = transfers.map((transfer) => ({
335
+ id: transfer.id.toString(),
336
+ amount: transfer.amount,
337
+ debitAccountId: transfer.debit_account_id.toString(),
338
+ creditAccountId: transfer.credit_account_id.toString(),
339
+ pending: (transfer.flags & TransferFlags.pending) !== 0,
340
+ code: transfer.code,
341
+ timeMs: Number(transfer.timestamp / 1000000n), // Convert nanoseconds to milliseconds
342
+ transactionId: transfer.user_data_128 !== 0n
343
+ ? transfer.user_data_128.toString()
344
+ : undefined,
345
+ note: charactarizeTransfer(transfer),
346
+ }));
347
+ return success({
348
+ accountDetails: financialAccount,
349
+ account: this._financialController.convertToAccountBalance(account),
350
+ transfers: accountTransfers,
351
+ });
352
+ }
179
353
  /**
180
354
  * Attempts to update the subscription for the given user.
181
355
  * @param request The request to update the subscription.
@@ -280,8 +454,8 @@ export class SubscriptionController {
280
454
  }
281
455
  catch (err) {
282
456
  const span = trace.getActiveSpan();
283
- span === null || span === void 0 ? void 0 : span.recordException(err);
284
- span === null || span === void 0 ? void 0 : span.setStatus({ code: SpanStatusCode.ERROR });
457
+ span?.recordException(err);
458
+ span?.setStatus({ code: SpanStatusCode.ERROR });
285
459
  console.error('[SubscriptionController] An error occurred while updating a subscription:', err);
286
460
  return {
287
461
  success: false,
@@ -292,10 +466,9 @@ export class SubscriptionController {
292
466
  }
293
467
  _getPurchasableSubscriptionsForRole(role, config) {
294
468
  return config.subscriptions.filter((s) => {
295
- var _a, _b, _c;
296
- const isPurchasable = (_a = s.purchasable) !== null && _a !== void 0 ? _a : true;
297
- const isUserOnly = (_b = s.userOnly) !== null && _b !== void 0 ? _b : false;
298
- const isStudioOnly = (_c = s.studioOnly) !== null && _c !== void 0 ? _c : false;
469
+ const isPurchasable = s.purchasable ?? true;
470
+ const isUserOnly = s.userOnly ?? false;
471
+ const isStudioOnly = s.studioOnly ?? false;
299
472
  const matchesRole = (isUserOnly && role === 'user') ||
300
473
  (isStudioOnly && role === 'studio') ||
301
474
  (!isUserOnly && !isStudioOnly);
@@ -354,7 +527,6 @@ export class SubscriptionController {
354
527
  * Returns a link that the user can be redirected to to initiate a purchase of the subscription.
355
528
  */
356
529
  async createManageSubscriptionLink(request) {
357
- var _a;
358
530
  if (!this._stripe) {
359
531
  return {
360
532
  success: false,
@@ -537,7 +709,7 @@ export class SubscriptionController {
537
709
  if (hasSubscription) {
538
710
  console.log(`[SubscriptionController] [createManageSubscriptionLink] Customer has a managable subscription. Creating a portal session.`);
539
711
  const session = await this._stripe.createPortalSession({
540
- ...((_a = config.portalConfig) !== null && _a !== void 0 ? _a : {}),
712
+ ...(config.portalConfig ?? {}),
541
713
  customer: customerId,
542
714
  return_url: returnRoute(config.returnUrl, user, studio),
543
715
  });
@@ -552,8 +724,8 @@ export class SubscriptionController {
552
724
  }
553
725
  catch (err) {
554
726
  const span = trace.getActiveSpan();
555
- span === null || span === void 0 ? void 0 : span.recordException(err);
556
- span === null || span === void 0 ? void 0 : span.setStatus({ code: SpanStatusCode.ERROR });
727
+ span?.recordException(err);
728
+ span?.setStatus({ code: SpanStatusCode.ERROR });
557
729
  console.error('[SubscriptionController] An error occurred while creating a manage subscription link:', err);
558
730
  return {
559
731
  success: false,
@@ -562,8 +734,296 @@ export class SubscriptionController {
562
734
  };
563
735
  }
564
736
  }
737
+ /**
738
+ * Creates a link that the user can be redirected to in order to manage their store account.
739
+ * @param request The request to create the manage store account link.
740
+ * @returns
741
+ */
742
+ async createManageStoreAccountLink(request) {
743
+ if (!this._stripe) {
744
+ return failure({
745
+ errorCode: 'not_supported',
746
+ errorMessage: 'This method is not supported.',
747
+ });
748
+ }
749
+ let studio = await this._recordsStore.getStudioById(request.studioId);
750
+ if (!studio) {
751
+ return failure({
752
+ errorCode: 'studio_not_found',
753
+ errorMessage: 'The given studio was not found.',
754
+ });
755
+ }
756
+ const assignments = await this._recordsStore.listStudioAssignments(studio.id, {
757
+ userId: request.userId,
758
+ role: ADMIN_ROLE_NAME,
759
+ });
760
+ if (assignments.length <= 0) {
761
+ return failure({
762
+ errorCode: 'not_authorized',
763
+ errorMessage: 'You are not authorized to perform this action.',
764
+ });
765
+ }
766
+ const config = await this._config.getSubscriptionConfiguration();
767
+ const features = getPurchasableItemsFeatures(config, studio.subscriptionStatus, studio.subscriptionId, 'studio', studio.subscriptionPeriodStartMs, studio.subscriptionPeriodEndMs);
768
+ if (!features.allowed) {
769
+ return failure({
770
+ errorCode: 'not_authorized',
771
+ errorMessage: 'You are not authorized to perform this action.',
772
+ });
773
+ }
774
+ let type = 'account_update';
775
+ if (!studio.stripeAccountId) {
776
+ console.log('[SubscriptionController] [createManageStoreAccountLink] Studio does not have a stripe account. Creating one.');
777
+ type = 'account_onboarding';
778
+ const account = await this._stripe.createAccount({
779
+ controller: {
780
+ fees: {
781
+ payer: 'account',
782
+ },
783
+ losses: {
784
+ payments: 'stripe',
785
+ },
786
+ requirement_collection: 'stripe',
787
+ stripe_dashboard: {
788
+ type: 'full',
789
+ },
790
+ },
791
+ metadata: {
792
+ studioId: studio.id,
793
+ },
794
+ });
795
+ console.log('[SubscriptionController] [createManageStoreAccountLink] Created account:', account.id);
796
+ studio = {
797
+ ...studio,
798
+ stripeAccountId: account.id,
799
+ stripeAccountStatus: getAccountStatus(account),
800
+ stripeAccountRequirementsStatus: getAccountRequirementsStatus(account),
801
+ };
802
+ await this._recordsStore.updateStudio(studio);
803
+ }
804
+ if (studio.stripeAccountRequirementsStatus === 'incomplete') {
805
+ type = 'account_onboarding';
806
+ }
807
+ const session = await this._stripe.createAccountLink({
808
+ account: studio.stripeAccountId,
809
+ refresh_url: config.returnUrl,
810
+ return_url: config.returnUrl,
811
+ type,
812
+ });
813
+ return success({
814
+ url: session.url,
815
+ });
816
+ }
817
+ /**
818
+ * Creates a link that the user can be redirected to in order to manage their stripe XP account.
819
+ * @param request The request to create the manage xp account link.
820
+ */
821
+ async createManageXpAccountLink(request) {
822
+ if (!this._stripe || !this._financialController) {
823
+ return failure({
824
+ errorCode: 'not_supported',
825
+ errorMessage: 'This method is not supported.',
826
+ });
827
+ }
828
+ let user = await this._authStore.findUser(request.userId);
829
+ if (!user) {
830
+ console.log('[SubscriptionController] [createManageXpAccountLink] User not found.');
831
+ return failure({
832
+ errorCode: 'user_not_found',
833
+ errorMessage: 'The user was not found.',
834
+ });
835
+ }
836
+ let updatedUser = false;
837
+ const config = await this._config.getSubscriptionConfiguration();
838
+ const account = await this._financialController.getOrCreateFinancialAccount({
839
+ userId: user.id,
840
+ ledger: LEDGERS.usd,
841
+ });
842
+ if (isFailure(account)) {
843
+ logError(account.error, `[SubscriptionController] [createManageXpAccountLink] Failed to get USD financial account for user: ${user.id}`);
844
+ return failure({
845
+ errorCode: 'server_error',
846
+ errorMessage: 'Failed to get financial account.',
847
+ });
848
+ }
849
+ if (!user.stripeAccountId) {
850
+ console.log('[SubscriptionController] [createManageXpAccountLink] User does not have a stripe account. Creating one.');
851
+ const account = await this._stripe.createAccount({
852
+ controller: {
853
+ fees: {
854
+ payer: 'application',
855
+ },
856
+ losses: {
857
+ payments: 'application',
858
+ },
859
+ requirement_collection: 'stripe',
860
+ stripe_dashboard: {
861
+ type: 'express',
862
+ },
863
+ },
864
+ metadata: {
865
+ userId: user.id,
866
+ },
867
+ });
868
+ console.log('[SubscriptionController] [createManageXpAccountLink] Created account:', account.id);
869
+ user = {
870
+ ...user,
871
+ stripeAccountId: account.id,
872
+ stripeAccountStatus: getAccountStatus(account),
873
+ stripeAccountRequirementsStatus: getAccountRequirementsStatus(account),
874
+ };
875
+ updatedUser = true;
876
+ }
877
+ if (updatedUser) {
878
+ await this._authStore.saveUser(user);
879
+ }
880
+ const session = await this._stripe.createAccountLink({
881
+ account: user.stripeAccountId,
882
+ refresh_url: config.returnUrl,
883
+ return_url: config.returnUrl,
884
+ // We have to always use onboarding because Stripe is responsible for collecting requirements
885
+ type: 'account_onboarding',
886
+ });
887
+ return success({
888
+ url: session.url,
889
+ });
890
+ }
891
+ /**
892
+ * Creates a link that the user can be redirected to in order to login to their stripe account.
893
+ * @param request The request to create the manage xp account link.
894
+ */
895
+ async createStripeLoginLink(request) {
896
+ if (!this._stripe || !this._financialController) {
897
+ return failure({
898
+ errorCode: 'not_supported',
899
+ errorMessage: 'This method is not supported.',
900
+ });
901
+ }
902
+ let user = await this._authStore.findUser(request.userId);
903
+ if (!user) {
904
+ console.log('[SubscriptionController] [createStripeLoginLink] User not found.');
905
+ return failure({
906
+ errorCode: 'user_not_found',
907
+ errorMessage: 'The user was not found.',
908
+ });
909
+ }
910
+ let accountId;
911
+ if (request.studioId) {
912
+ let studio = await this._recordsStore.getStudioById(request.studioId);
913
+ if (!studio) {
914
+ return failure({
915
+ errorCode: 'studio_not_found',
916
+ errorMessage: 'The given studio was not found.',
917
+ });
918
+ }
919
+ const assignments = await this._recordsStore.listStudioAssignments(studio.id, {
920
+ userId: request.userId,
921
+ role: ADMIN_ROLE_NAME,
922
+ });
923
+ if (assignments.length <= 0) {
924
+ return failure({
925
+ errorCode: 'not_authorized',
926
+ errorMessage: 'You are not authorized to perform this action.',
927
+ });
928
+ }
929
+ const config = await this._config.getSubscriptionConfiguration();
930
+ const features = getPurchasableItemsFeatures(config, studio.subscriptionStatus, studio.subscriptionId, 'studio', studio.subscriptionPeriodStartMs, studio.subscriptionPeriodEndMs);
931
+ if (!features.allowed) {
932
+ return failure({
933
+ errorCode: 'not_authorized',
934
+ errorMessage: 'You are not authorized to perform this action.',
935
+ });
936
+ }
937
+ accountId = studio.stripeAccountId;
938
+ }
939
+ else {
940
+ accountId = user.stripeAccountId;
941
+ }
942
+ if (!accountId) {
943
+ return failure({
944
+ errorCode: 'not_found',
945
+ errorMessage: 'No Stripe account found.',
946
+ });
947
+ }
948
+ const session = await this._stripe.createLoginLink({
949
+ account: accountId,
950
+ });
951
+ return success({
952
+ url: session.url,
953
+ });
954
+ }
955
+ /**
956
+ * Creates a session that can be used to display stripe embedded components for the user.
957
+ * @param request The request to create the account session.
958
+ */
959
+ async createStripeAccountSession(request) {
960
+ if (!this._stripe || !this._financialController) {
961
+ return failure({
962
+ errorCode: 'not_supported',
963
+ errorMessage: 'This method is not supported.',
964
+ });
965
+ }
966
+ let user = await this._authStore.findUser(request.userId);
967
+ if (!user) {
968
+ console.log('[SubscriptionController] [createStripeAccountSession] User not found.');
969
+ return failure({
970
+ errorCode: 'user_not_found',
971
+ errorMessage: 'The user was not found.',
972
+ });
973
+ }
974
+ let accountId;
975
+ if (request.studioId) {
976
+ let studio = await this._recordsStore.getStudioById(request.studioId);
977
+ if (!studio) {
978
+ return failure({
979
+ errorCode: 'studio_not_found',
980
+ errorMessage: 'The given studio was not found.',
981
+ });
982
+ }
983
+ const assignments = await this._recordsStore.listStudioAssignments(studio.id, {
984
+ userId: request.userId,
985
+ role: ADMIN_ROLE_NAME,
986
+ });
987
+ if (assignments.length <= 0) {
988
+ return failure({
989
+ errorCode: 'not_authorized',
990
+ errorMessage: 'You are not authorized to perform this action.',
991
+ });
992
+ }
993
+ const config = await this._config.getSubscriptionConfiguration();
994
+ const features = getPurchasableItemsFeatures(config, studio.subscriptionStatus, studio.subscriptionId, 'studio', studio.subscriptionPeriodStartMs, studio.subscriptionPeriodEndMs);
995
+ if (!features.allowed) {
996
+ return failure({
997
+ errorCode: 'not_authorized',
998
+ errorMessage: 'You are not authorized to perform this action.',
999
+ });
1000
+ }
1001
+ accountId = studio.stripeAccountId;
1002
+ }
1003
+ else {
1004
+ accountId = user.stripeAccountId;
1005
+ }
1006
+ if (!accountId) {
1007
+ return failure({
1008
+ errorCode: 'not_found',
1009
+ errorMessage: 'No Stripe account found for the user.',
1010
+ });
1011
+ }
1012
+ const session = await this._stripe.createAccountSession({
1013
+ account: user.stripeAccountId,
1014
+ components: {
1015
+ payouts: {
1016
+ enabled: true,
1017
+ features: {},
1018
+ },
1019
+ },
1020
+ });
1021
+ return success({
1022
+ clientSecret: session.client_secret,
1023
+ expiresAt: session.expires_at,
1024
+ });
1025
+ }
565
1026
  async _createCheckoutSession(request, customerId, metadata, role, user, studio) {
566
- var _a;
567
1027
  const config = await this._getConfig();
568
1028
  const purchasableSubscriptions = this._getPurchasableSubscriptionsForRole(role, config);
569
1029
  let sub;
@@ -600,7 +1060,7 @@ export class SubscriptionController {
600
1060
  }
601
1061
  console.log(`[SubscriptionController] [createManageSubscriptionLink] Creating Checkout Session.`);
602
1062
  const session = await this._stripe.createCheckoutSession({
603
- ...((_a = config.checkoutConfig) !== null && _a !== void 0 ? _a : {}),
1063
+ ...(config.checkoutConfig ?? {}),
604
1064
  customer: customerId,
605
1065
  success_url: returnRoute(config.successUrl, user, studio),
606
1066
  cancel_url: returnRoute(config.cancelUrl, user, studio),
@@ -620,228 +1080,2312 @@ export class SubscriptionController {
620
1080
  };
621
1081
  }
622
1082
  /**
623
- * Handles the webhook from Stripe for updating the internal database.
1083
+ * Creates a link that the user can be redirected to in order to purchase a purchasable item.
1084
+ * @param request The request to create the purchase item link.
624
1085
  */
625
- async handleStripeWebhook(request) {
626
- var _a;
627
- if (!this._stripe) {
628
- return {
629
- success: false,
630
- errorCode: 'not_supported',
631
- errorMessage: 'This method is not supported.',
632
- };
633
- }
1086
+ async createPurchaseItemLink(request) {
634
1087
  try {
635
- if (typeof request.requestBody !== 'string' ||
636
- request.requestBody === '') {
1088
+ const context = await this._policies.constructAuthorizationContext({
1089
+ recordKeyOrRecordName: request.item.recordName,
1090
+ userId: request.userId,
1091
+ });
1092
+ if (context.success === false) {
1093
+ return context;
1094
+ }
1095
+ const item = await this._purchasableItems.getItemByAddress(request.item.recordName, request.item.address);
1096
+ if (!item) {
637
1097
  return {
638
1098
  success: false,
639
- errorCode: 'invalid_request',
640
- errorMessage: 'The request was not valid.',
1099
+ errorCode: 'item_not_found',
1100
+ errorMessage: 'The item could not be found.',
641
1101
  };
642
1102
  }
643
- if (typeof request.signature !== 'string' ||
644
- request.signature === '') {
1103
+ if (item.currency !== request.item.currency ||
1104
+ item.cost !== request.item.expectedCost) {
645
1105
  return {
646
1106
  success: false,
647
- errorCode: 'invalid_request',
648
- errorMessage: 'The request was not valid.',
1107
+ errorCode: 'price_does_not_match',
1108
+ errorMessage: 'The expected price does not match the actual price of the item.',
649
1109
  };
650
1110
  }
1111
+ const recordName = context.context.recordName;
1112
+ const authorization = await this._policies.authorizeUserAndInstances(context.context, {
1113
+ userId: request.userId,
1114
+ resourceKind: 'purchasableItem',
1115
+ resourceId: item.address,
1116
+ markers: item.markers,
1117
+ action: 'purchase',
1118
+ instances: request.instances,
1119
+ });
1120
+ if (authorization.success === false) {
1121
+ return authorization;
1122
+ }
1123
+ const metrics = await this._purchasableItems.getSubscriptionMetrics({
1124
+ ownerId: context.context.recordOwnerId,
1125
+ studioId: context.context.recordStudioId,
1126
+ });
651
1127
  const config = await this._getConfig();
652
- const body = request.requestBody;
653
- const signature = request.signature;
654
- let event;
655
- try {
656
- event = this._stripe.constructWebhookEvent(body, signature, config.webhookSecret);
1128
+ const features = getPurchasableItemsFeatures(config, metrics.subscriptionStatus, metrics.subscriptionId, 'studio', metrics.currentPeriodStartMs, metrics.currentPeriodEndMs);
1129
+ if (!features.allowed) {
1130
+ console.log(`[SubscriptionController] [createPurchaseItemLink studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} subscriptionStatus: ${metrics.subscriptionStatus}] Store features not allowed.`);
1131
+ return {
1132
+ success: false,
1133
+ errorCode: 'store_disabled',
1134
+ errorMessage: 'The store you are trying to purchase from is disabled.',
1135
+ };
657
1136
  }
658
- catch (err) {
659
- console.log(`[SubscriptionController] [handleStripeWebhook] Unable to construct webhook event:`, err);
1137
+ if (!metrics.stripeAccountId || !metrics.stripeAccountStatus) {
1138
+ console.log(`[SubscriptionController] [createPurchaseItemLink studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} subscriptionStatus: ${metrics.subscriptionStatus} stripeAccountId: ${metrics.stripeAccountId} stripeAccountStatus: ${metrics.stripeAccountStatus}] Store has no stripe account.`);
660
1139
  return {
661
1140
  success: false,
662
- errorCode: 'invalid_request',
663
- errorMessage: 'The request was not valid.',
1141
+ errorCode: 'store_disabled',
1142
+ errorMessage: 'The store you are trying to purchase from is disabled.',
664
1143
  };
665
1144
  }
666
- console.log(`[SubscriptionController] [handleStripeWebhook] Got event: ${event.type}`);
667
- if (event.type === 'customer.subscription.created' ||
668
- event.type === 'customer.subscription.deleted' ||
669
- event.type === 'customer.subscription.updated') {
670
- const subscription = event.data.object;
671
- const items = subscription.items.data;
672
- let item;
673
- let sub;
674
- items_loop: for (let i of items) {
675
- for (let s of config.subscriptions) {
676
- if (s.eligibleProducts &&
677
- s.eligibleProducts.some((p) => p === i.price.product)) {
678
- sub = s;
679
- item = i;
680
- break items_loop;
681
- }
682
- }
683
- }
684
- if (!item || !sub) {
685
- console.log(`[SubscriptionController] [handleStripeWebhook] No item in the subscription matches an eligible product in the config.`);
686
- return {
687
- success: true,
688
- };
689
- }
690
- console.log(`[SubscriptionController] [handleStripeWebhook] Subscription (${sub.id}) found!`);
691
- const status = subscription.status;
692
- const active = isActiveSubscription(status);
693
- const tier = (_a = sub.tier) !== null && _a !== void 0 ? _a : 'beta';
694
- const customerId = subscription.customer;
695
- const stripeSubscriptionId = subscription.id;
696
- const periodStartMs = subscription.current_period_start * 1000;
697
- const periodEndMs = subscription.current_period_end * 1000;
698
- console.log(`[SubscriptionController] [handleStripeWebhook] Customer ID: ${customerId}. Subscription status: ${status}. Tier: ${tier}. Is Active: ${active}.`);
699
- let user = await this._authStore.findUserByStripeCustomerId(customerId);
700
- let studio;
701
- if (user) {
702
- await this._authStore.updateSubscriptionInfo({
703
- userId: user.id,
704
- subscriptionStatus: status,
705
- subscriptionId: sub.id,
706
- stripeSubscriptionId,
707
- stripeCustomerId: customerId,
708
- currentPeriodEndMs: periodEndMs,
709
- currentPeriodStartMs: periodStartMs,
710
- });
1145
+ if (metrics.stripeAccountStatus !== 'active') {
1146
+ console.log(`[SubscriptionController] [createPurchaseItemLink studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} subscriptionStatus: ${metrics.subscriptionStatus} stripeAccountId: ${metrics.stripeAccountId} stripeAccountStatus: ${metrics.stripeAccountStatus}] Store stripe account is not active.`);
1147
+ return {
1148
+ success: false,
1149
+ errorCode: 'store_disabled',
1150
+ errorMessage: 'The store you are trying to purchase from is disabled.',
1151
+ };
1152
+ }
1153
+ const limits = features.currencyLimits[item.currency];
1154
+ if (!limits) {
1155
+ console.log(`[SubscriptionController] [createPurchaseItemLink studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} currency: ${request.item.currency}] Currency not supported.`);
1156
+ return {
1157
+ success: false,
1158
+ errorCode: 'currency_not_supported',
1159
+ errorMessage: 'The currency is not supported.',
1160
+ };
1161
+ }
1162
+ if (item.cost !== 0 &&
1163
+ (item.cost < limits.minCost || item.cost > limits.maxCost)) {
1164
+ console.log(`[SubscriptionController] [createPurchaseItemLink studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} currency: ${request.item.currency} minCost: ${limits.minCost} maxCost: ${limits.maxCost} cost: ${item.cost}] Cost not valid.`);
1165
+ return {
1166
+ success: false,
1167
+ errorCode: 'subscription_limit_reached',
1168
+ errorMessage: 'The item you are trying to purchase has a price that is not allowed.',
1169
+ };
1170
+ }
1171
+ let applicationFee = 0;
1172
+ if (item.cost !== 0 && limits.fee) {
1173
+ if (limits.fee.type === 'percent') {
1174
+ // calculate percent when fee is between 1 - 100
1175
+ applicationFee = Math.ceil(item.cost * (limits.fee.percent / 100));
711
1176
  }
712
1177
  else {
713
- console.log(`[SubscriptionController] [handleStripeWebhook] No user found for Customer ID (${customerId})`);
714
- studio =
715
- await this._recordsStore.getStudioByStripeCustomerId(customerId);
716
- if (studio) {
717
- await this._authStore.updateSubscriptionInfo({
718
- studioId: studio.id,
719
- subscriptionStatus: status,
720
- subscriptionId: sub.id,
721
- stripeSubscriptionId,
722
- stripeCustomerId: customerId,
723
- currentPeriodEndMs: periodEndMs,
724
- currentPeriodStartMs: periodStartMs,
725
- });
726
- }
727
- else {
728
- console.log(`[SubscriptionController] [handleStripeWebhook] No studio found for Customer ID (${customerId})`);
1178
+ if (limits.fee.amount > item.cost) {
1179
+ console.warn(`[SubscriptionController] [createPurchaseItemLink studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} currency: ${request.item.currency} fee: ${limits.fee.amount} cost: ${item.cost}] Fee greater than cost.`);
1180
+ return {
1181
+ success: false,
1182
+ errorCode: 'server_error',
1183
+ errorMessage: 'The application fee is greater than the cost of the item.',
1184
+ };
729
1185
  }
1186
+ applicationFee = limits.fee.amount;
730
1187
  }
731
- return {
732
- success: true,
733
- };
734
1188
  }
735
- else if (event.type === 'invoice.paid') {
736
- const parseResult = STRIPE_EVENT_INVOICE_PAID_SCHEMA.safeParse(event);
737
- if (parseResult.success === false) {
738
- console.error(`[SubscriptionController] [handleStripeWebhook] Unable to parse stripe event!`, parseResult.error);
1189
+ let customerEmail = null;
1190
+ if (request.userId) {
1191
+ const user = await this._authStore.findUser(request.userId);
1192
+ if (!user) {
739
1193
  return {
740
- success: true,
1194
+ success: false,
1195
+ errorCode: 'invalid_request',
1196
+ errorMessage: 'The user could not be found.',
741
1197
  };
742
1198
  }
743
- const invoice = parseResult.data.data.object;
744
- const stripeSubscriptionId = invoice.subscription;
745
- const subscription = await this._stripe.getSubscriptionById(stripeSubscriptionId);
746
- const status = subscription.status;
747
- const customerId = invoice.customer;
748
- const lineItems = invoice.lines.data;
749
- const periodStartMs = subscription.current_period_start * 1000;
750
- const periodEndMs = subscription.current_period_end * 1000;
751
- const { sub, item } = findMatchingSubscription(lineItems);
752
- const authInvoice = {
753
- currency: invoice.currency,
754
- description: invoice.description,
755
- paid: invoice.paid,
756
- status: invoice.status,
757
- tax: invoice.tax,
758
- total: invoice.total,
759
- subtotal: invoice.subtotal,
760
- stripeInvoiceId: invoice.id,
761
- stripeHostedInvoiceUrl: invoice.hosted_invoice_url,
762
- stripeInvoicePdfUrl: invoice.invoice_pdf,
763
- };
764
- console.log(`[SubscriptionController] [handleStripeWebhook] New invoice paid for customer ID (${customerId}). Subscription ID: ${subscription.id}. Period start: ${periodStartMs}. Period end: ${periodEndMs}.`);
765
- const user = await this._authStore.findUserByStripeCustomerId(customerId);
766
- if (user) {
767
- console.log(`[SubscriptionController] [handleStripeWebhook] Found user (${user.id}) with customer ID (${customerId}).`);
768
- await this._authStore.updateSubscriptionPeriod({
769
- userId: user.id,
770
- subscriptionStatus: status,
771
- subscriptionId: sub.id,
772
- stripeSubscriptionId,
773
- stripeCustomerId: customerId,
774
- currentPeriodEndMs: periodEndMs,
1199
+ const roles = await this._policyStore.listRolesForUser(recordName, user.id);
1200
+ const hasRole = roles.some((r) => r.role === item.roleName &&
1201
+ (!r.expireTimeMs || r.expireTimeMs > Date.now()));
1202
+ if (hasRole) {
1203
+ return {
1204
+ success: false,
1205
+ errorCode: 'item_already_purchased',
1206
+ errorMessage: 'You already have the role that the item would grant.',
1207
+ };
1208
+ }
1209
+ customerEmail = user.email ?? null;
1210
+ }
1211
+ const sessionId = uuid();
1212
+ console.log(`[SubscriptionController] [createPurchaseItemLink studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} sessionId: ${sessionId} currency: ${request.item.currency} cost: ${item.cost} applicationFee: ${applicationFee}] Creating checkout session.`);
1213
+ const expirationSeconds = 60 * 60; // 1 hour
1214
+ const session = await this._stripe.createCheckoutSession({
1215
+ mode: 'payment',
1216
+ line_items: [
1217
+ {
1218
+ price_data: {
1219
+ currency: item.currency,
1220
+ unit_amount: item.cost,
1221
+ product_data: {
1222
+ name: item.name,
1223
+ description: item.description,
1224
+ images: item.imageUrls,
1225
+ metadata: {
1226
+ recordName: recordName,
1227
+ address: item.address,
1228
+ },
1229
+ tax_code: item.taxCode ?? undefined,
1230
+ },
1231
+ },
1232
+ quantity: 1,
1233
+ },
1234
+ ],
1235
+ expires_at: Math.floor(Date.now() / 1000) + expirationSeconds,
1236
+ success_url: fulfillmentRoute(config.returnUrl, sessionId),
1237
+ cancel_url: request.returnUrl,
1238
+ client_reference_id: sessionId,
1239
+ customer_email: customerEmail,
1240
+ metadata: {
1241
+ userId: request.userId,
1242
+ checkoutSessionId: sessionId,
1243
+ },
1244
+ payment_intent_data: {
1245
+ application_fee_amount: applicationFee,
1246
+ },
1247
+ connect: {
1248
+ stripeAccount: metrics.stripeAccountId,
1249
+ },
1250
+ });
1251
+ await this._authStore.updateCheckoutSessionInfo({
1252
+ id: sessionId,
1253
+ stripeCheckoutSessionId: session.id,
1254
+ invoice: session.invoice
1255
+ ? {
1256
+ currency: session.invoice.currency,
1257
+ paid: session.invoice.paid,
1258
+ description: session.invoice.description,
1259
+ status: session.invoice.status,
1260
+ stripeInvoiceId: session.invoice.id,
1261
+ stripeHostedInvoiceUrl: session.invoice.hosted_invoice_url,
1262
+ stripeInvoicePdfUrl: session.invoice.invoice_pdf,
1263
+ tax: session.invoice.tax,
1264
+ total: session.invoice.total,
1265
+ subtotal: session.invoice.subtotal,
1266
+ }
1267
+ : null,
1268
+ userId: request.userId,
1269
+ status: session.status,
1270
+ paymentStatus: session.payment_status,
1271
+ paid: session.payment_status === 'paid' ||
1272
+ session.payment_status === 'no_payment_required',
1273
+ fulfilledAtMs: null,
1274
+ items: [
1275
+ {
1276
+ type: 'role',
1277
+ recordName: recordName,
1278
+ purchasableItemAddress: item.address,
1279
+ role: item.roleName,
1280
+ roleGrantTimeMs: item.roleGrantTimeMs,
1281
+ },
1282
+ ],
1283
+ });
1284
+ return {
1285
+ success: true,
1286
+ url: session.url,
1287
+ sessionId: sessionId,
1288
+ };
1289
+ }
1290
+ catch (err) {
1291
+ console.error('[SubscriptionController] An error occurred while creating a purchase item link:', err);
1292
+ return {
1293
+ success: false,
1294
+ errorCode: 'server_error',
1295
+ errorMessage: 'A server error occurred.',
1296
+ };
1297
+ }
1298
+ }
1299
+ /**
1300
+ * Gets the details required for purchasing a contract.
1301
+ * @param request The request.
1302
+ */
1303
+ async _getContractPurchaseDetails(request) {
1304
+ const context = await this._policies.constructAuthorizationContext({
1305
+ recordKeyOrRecordName: request.contract.recordName,
1306
+ userId: request.userId,
1307
+ });
1308
+ if (context.success === false) {
1309
+ return failure(context);
1310
+ }
1311
+ const item = await this._contractRecords.getItemByAddress(request.contract.recordName, request.contract.address);
1312
+ if (!item) {
1313
+ return failure({
1314
+ errorCode: 'item_not_found',
1315
+ errorMessage: 'The item could not be found.',
1316
+ });
1317
+ }
1318
+ if (item.status !== 'pending') {
1319
+ return failure({
1320
+ errorCode: 'item_already_purchased',
1321
+ errorMessage: 'The contract has already been purchased.',
1322
+ });
1323
+ }
1324
+ // TODO: Pull this from the contract.
1325
+ const currency = 'usd';
1326
+ const authorization = await this._policies.authorizeUserAndInstances(context.context, {
1327
+ userId: request.userId,
1328
+ resourceKind: 'contract',
1329
+ resourceId: item.address,
1330
+ markers: item.markers,
1331
+ action: 'purchase',
1332
+ instances: request.instances,
1333
+ });
1334
+ if (authorization.success === false) {
1335
+ return failure(authorization);
1336
+ }
1337
+ const metrics = await this._contractRecords.getSubscriptionMetrics({
1338
+ ownerId: context.context.recordOwnerId,
1339
+ studioId: context.context.recordStudioId,
1340
+ });
1341
+ const config = await this._getConfig();
1342
+ const features = getContractFeatures(config, metrics.subscriptionStatus, metrics.subscriptionId, metrics.subscriptionType, metrics.currentPeriodStartMs, metrics.currentPeriodEndMs);
1343
+ if (!features.allowed) {
1344
+ console.log(`[SubscriptionController] [_getContractPurchaseDetails studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} subscriptionStatus: ${metrics.subscriptionStatus}] Store features not allowed.`);
1345
+ return failure({
1346
+ errorCode: 'store_disabled',
1347
+ errorMessage: "The account you are trying to purchase the contract for doesn't have access to contracting features.",
1348
+ });
1349
+ }
1350
+ const limits = features.currencyLimits[currency];
1351
+ if (!limits) {
1352
+ console.log(`[SubscriptionController] [_getContractPurchaseDetails studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} currency: ${currency}] Currency not supported.`);
1353
+ return failure({
1354
+ errorCode: 'currency_not_supported',
1355
+ errorMessage: 'The currency is not supported.',
1356
+ });
1357
+ }
1358
+ if (item.initialValue !== 0 &&
1359
+ (item.initialValue < limits.minCost ||
1360
+ item.initialValue > limits.maxCost)) {
1361
+ console.log(`[SubscriptionController] [_getContractPurchaseDetails studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} currency: ${currency} minCost: ${limits.minCost} maxCost: ${limits.maxCost} initialValue: ${item.initialValue}] Cost not valid.`);
1362
+ return failure({
1363
+ errorCode: 'subscription_limit_reached',
1364
+ errorMessage: 'The contract you are trying to purchase has a price that is not allowed.',
1365
+ });
1366
+ }
1367
+ let applicationFee = 0;
1368
+ if (item.initialValue !== 0 && limits.fee) {
1369
+ if (limits.fee.type === 'percent') {
1370
+ // calculate percent when fee is between 1 - 100
1371
+ applicationFee = Math.ceil(item.initialValue * (limits.fee.percent / 100));
1372
+ }
1373
+ else {
1374
+ // if (limits.fee.amount > item.initialValue) {
1375
+ // console.warn(
1376
+ // `[SubscriptionController] [purchaseContract studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} currency: ${currency} fee: ${limits.fee.amount} initialValue: ${item.initialValue}] Fee greater than cost.`
1377
+ // );
1378
+ // return {
1379
+ // success: false,
1380
+ // errorCode: 'server_error',
1381
+ // errorMessage:
1382
+ // 'The application fee is greater than the cost of the item.',
1383
+ // };
1384
+ // }
1385
+ applicationFee = limits.fee.amount;
1386
+ }
1387
+ }
1388
+ const totalCost = item.initialValue + applicationFee;
1389
+ return success({
1390
+ totalCost,
1391
+ applicationFee,
1392
+ item,
1393
+ features,
1394
+ metrics,
1395
+ limits,
1396
+ currency,
1397
+ context: context.context,
1398
+ authorization,
1399
+ });
1400
+ }
1401
+ /**
1402
+ * Gets the pricing information for a contract.
1403
+ * @param request The request.
1404
+ */
1405
+ async getContractPricing(request) {
1406
+ const details = await this._getContractPurchaseDetails(request);
1407
+ if (isFailure(details)) {
1408
+ return details;
1409
+ }
1410
+ const lineItems = [];
1411
+ lineItems.push({
1412
+ name: 'Contract',
1413
+ amount: details.value.item.initialValue,
1414
+ });
1415
+ if (details.value.applicationFee > 0) {
1416
+ lineItems.push({
1417
+ name: 'Application Fee',
1418
+ amount: details.value.applicationFee,
1419
+ });
1420
+ }
1421
+ return success({
1422
+ total: details.value.totalCost,
1423
+ currency: details.value.currency,
1424
+ lineItems,
1425
+ contract: details.value.item,
1426
+ });
1427
+ }
1428
+ /**
1429
+ * Creates a link that the user can be redirected to in order to purchase a contract.
1430
+ * @param request The request to purchase the contract.
1431
+ * @returns A promise that resolves to the result of the purchase contract operation.
1432
+ */
1433
+ async purchaseContract(request) {
1434
+ try {
1435
+ const details = await this._getContractPurchaseDetails(request);
1436
+ if (isFailure(details)) {
1437
+ return details;
1438
+ }
1439
+ const item = details.value.item;
1440
+ const currency = details.value.currency;
1441
+ if (currency !== request.contract.currency) {
1442
+ return failure({
1443
+ errorCode: 'price_does_not_match',
1444
+ errorMessage: 'The expected price does not match the actual price of the contract.',
1445
+ });
1446
+ }
1447
+ const recordName = details.value.context.recordName;
1448
+ const metrics = details.value.metrics;
1449
+ const config = await this._getConfig();
1450
+ const features = details.value.features;
1451
+ const limits = details.value.limits;
1452
+ const totalCost = details.value.totalCost;
1453
+ const applicationFee = details.value.applicationFee;
1454
+ if (totalCost !== request.contract.expectedCost) {
1455
+ return failure({
1456
+ errorCode: 'price_does_not_match',
1457
+ errorMessage: 'The expected price does not match the actual price of the contract.',
1458
+ });
1459
+ }
1460
+ let customerEmail = null;
1461
+ if (request.userId) {
1462
+ const user = await this._authStore.findUser(request.userId);
1463
+ if (!user) {
1464
+ return failure({
1465
+ success: false,
1466
+ errorCode: 'invalid_request',
1467
+ errorMessage: 'The user could not be found.',
1468
+ });
1469
+ }
1470
+ // const roles = await this._policyStore.listRolesForUser(
1471
+ // recordName,
1472
+ // user.id
1473
+ // );
1474
+ // const hasRole = roles.some(
1475
+ // (r) =>
1476
+ // r.role === item.roleName &&
1477
+ // (!r.expireTimeMs || r.expireTimeMs > Date.now())
1478
+ // );
1479
+ // if (hasRole) {
1480
+ // return {
1481
+ // success: false,
1482
+ // errorCode: 'item_already_purchased',
1483
+ // errorMessage:
1484
+ // 'You already have the role that the item would grant.',
1485
+ // };
1486
+ // }
1487
+ customerEmail = user.email ?? null;
1488
+ }
1489
+ const contractAccount = await this._financialController.getOrCreateFinancialAccount({
1490
+ contractId: item.id,
1491
+ ledger: LEDGERS.usd,
1492
+ });
1493
+ if (isFailure(contractAccount)) {
1494
+ logError(contractAccount.error, `[SubscriptionController] [purchaseContract] Failed to get USD financial account for contract: ${item.id}`);
1495
+ return failure({
1496
+ success: false,
1497
+ errorCode: 'server_error',
1498
+ errorMessage: 'Failed to get a financial account for the contract.',
1499
+ });
1500
+ }
1501
+ const sessionId = uuid();
1502
+ console.log(`[SubscriptionController] [purchaseContract studioId: ${metrics.studioId} subscriptionId: ${metrics.subscriptionId} sessionId: ${sessionId} currency: ${request.contract.currency} initialValue: ${item.initialValue} applicationFee: ${applicationFee}] Creating checkout session.`);
1503
+ const userUsdAccount = await this._financialController.getFinancialAccount({
1504
+ userId: request.userId,
1505
+ ledger: LEDGERS.usd,
1506
+ });
1507
+ let immediateFulfillment = false;
1508
+ const checkoutSession = {
1509
+ id: sessionId,
1510
+ stripeCheckoutSessionId: null,
1511
+ invoice: null,
1512
+ userId: request.userId,
1513
+ status: 'complete',
1514
+ paymentStatus: 'no_payment_required',
1515
+ paid: false,
1516
+ fulfilledAtMs: null,
1517
+ items: [
1518
+ {
1519
+ type: 'contract',
1520
+ recordName: recordName,
1521
+ contractAddress: item.address,
1522
+ contractId: item.id,
1523
+ value: item.initialValue,
1524
+ },
1525
+ ],
1526
+ transactionId: null,
1527
+ transferIds: null,
1528
+ shouldBeAutomaticallyFulfilled: true,
1529
+ };
1530
+ if (isFailure(userUsdAccount)) {
1531
+ logError(userUsdAccount.error, `[SubscriptionController] [purchaseContract] Failed to get USD financial account for user: ${request.userId}`, console.warn);
1532
+ }
1533
+ else {
1534
+ const balance = getAccountBalance(userUsdAccount.value.account);
1535
+ if (balance >= totalCost) {
1536
+ // try to create a transfer from the user account
1537
+ console.log(`[SubscriptionController] [purchaseContract accountId: ${userUsdAccount.value.account.id}] Attempting to pay out of user USD account.`);
1538
+ const builder = new TransactionBuilder();
1539
+ builder.disablePendingTransfers();
1540
+ builder.addContract({
1541
+ recordName,
1542
+ item,
1543
+ contractAccountId: contractAccount.value.account.id,
1544
+ debitAccountId: userUsdAccount.value.account.id,
1545
+ });
1546
+ if (applicationFee > 0) {
1547
+ builder.addContractApplicationFee({
1548
+ recordName,
1549
+ item,
1550
+ fee: applicationFee,
1551
+ debitAccountId: userUsdAccount.value.account.id,
1552
+ });
1553
+ }
1554
+ const transferResult = await this._financialController.internalTransaction({
1555
+ transfers: builder.transfers,
1556
+ });
1557
+ if (isFailure(transferResult)) {
1558
+ logError(transferResult.error, `[SubscriptionController] [purchaseContract] Failed to pay for contract from user's USD account:`, console.warn);
1559
+ }
1560
+ else {
1561
+ console.log(`[SubscriptionController] [purchaseContract] Successfully paid for contract from user's USD account.`);
1562
+ checkoutSession.paid = true;
1563
+ checkoutSession.transferIds =
1564
+ transferResult.value.transferIds;
1565
+ checkoutSession.transactionId =
1566
+ transferResult.value.transactionId;
1567
+ immediateFulfillment = true;
1568
+ }
1569
+ }
1570
+ }
1571
+ if (!checkoutSession.paid) {
1572
+ const userCreditAccount = await this._financialController.getFinancialAccount({
1573
+ userId: request.userId,
1574
+ ledger: LEDGERS.credits,
1575
+ });
1576
+ if (isFailure(userCreditAccount)) {
1577
+ logError(userCreditAccount.error, `[SubscriptionController] [purchaseContract] Failed to get Credit financial account for user: ${request.userId}`, console.warn);
1578
+ }
1579
+ else {
1580
+ const balance = getAccountBalance(userCreditAccount.value.account);
1581
+ if (balance >= totalCost) {
1582
+ // try to create a transfer from the user account
1583
+ console.log(`[SubscriptionController] [purchaseContract accountId: ${userCreditAccount.value.account.id}] Attempting to pay out of user credits account.`);
1584
+ const totalCreditCost = convertBetweenLedgers(LEDGERS.usd, LEDGERS.credits, BigInt(totalCost));
1585
+ if (totalCreditCost === null) {
1586
+ console.error(`[SubscriptionController] [purchaseContract] Failed to convert cost from USD to Credits.`);
1587
+ return failure({
1588
+ errorCode: 'server_error',
1589
+ errorMessage: 'Failed to convert cost from USD to Credits.',
1590
+ });
1591
+ }
1592
+ const builder = new TransactionBuilder();
1593
+ builder.disablePendingTransfers();
1594
+ builder.addTransfer({
1595
+ debitAccountId: userCreditAccount.value.account.id,
1596
+ creditAccountId: getLiquidityAccountByLedger(userCreditAccount.value.account.ledger),
1597
+ currency: CURRENCIES.get(userCreditAccount.value.account.ledger),
1598
+ amount: totalCreditCost.value,
1599
+ code: TransferCodes.exchange,
1600
+ });
1601
+ builder.addContract({
1602
+ recordName,
1603
+ item,
1604
+ contractAccountId: contractAccount.value.account.id,
1605
+ debitAccountId: getLiquidityAccountByLedger(contractAccount.value.account.ledger),
1606
+ });
1607
+ if (applicationFee > 0) {
1608
+ builder.addContractApplicationFee({
1609
+ recordName,
1610
+ item,
1611
+ fee: applicationFee,
1612
+ debitAccountId: getLiquidityAccountByLedger(contractAccount.value.account.ledger),
1613
+ });
1614
+ }
1615
+ const transferResult = await this._financialController.internalTransaction({
1616
+ transfers: builder.transfers,
1617
+ });
1618
+ if (isFailure(transferResult)) {
1619
+ logError(transferResult.error, `[SubscriptionController] [purchaseContract] Failed to pay for contract from user's credit account:`, console.warn);
1620
+ }
1621
+ else {
1622
+ console.log(`[SubscriptionController] [purchaseContract] Successfully paid for contract from user's credit account.`);
1623
+ checkoutSession.paid = true;
1624
+ checkoutSession.transferIds =
1625
+ transferResult.value.transferIds;
1626
+ checkoutSession.transactionId =
1627
+ transferResult.value.transactionId;
1628
+ immediateFulfillment = true;
1629
+ }
1630
+ }
1631
+ }
1632
+ }
1633
+ let url = undefined;
1634
+ let voidTransfers = async () => { };
1635
+ if (!checkoutSession.paid) {
1636
+ console.log(`[SubscriptionController] [purchaseContract] Attempting to pay out of Stripe.`);
1637
+ const expirationSeconds = 60 * 60; // 1 hour
1638
+ const builder = new TransactionBuilder();
1639
+ builder.usePendingTransfers(expirationSeconds);
1640
+ builder.addContract({
1641
+ recordName,
1642
+ item,
1643
+ contractAccountId: contractAccount.value.account.id,
1644
+ debitAccountId: ACCOUNT_IDS.assets_stripe,
1645
+ });
1646
+ if (applicationFee > 0) {
1647
+ builder.addContractApplicationFee({
1648
+ recordName,
1649
+ item,
1650
+ fee: applicationFee,
1651
+ debitAccountId: ACCOUNT_IDS.assets_stripe,
1652
+ });
1653
+ }
1654
+ const transferResult = await this._financialController.internalTransaction({
1655
+ transfers: builder.transfers,
1656
+ });
1657
+ if (isFailure(transferResult)) {
1658
+ logError(transferResult.error, `[SubscriptionController] [purchaseContract] Failed to create internal transfer for contract: ${item.id}`);
1659
+ // TODO: Map out better error codes
1660
+ return failure({
1661
+ errorCode: 'server_error',
1662
+ errorMessage: 'Failed to create internal transfer for contract.',
1663
+ });
1664
+ }
1665
+ voidTransfers = async () => {
1666
+ console.log(`[SubscriptionController] [purchaseContract] Voiding pending transfers for failed purchase.`);
1667
+ const voidResult = await this._financialController.completePendingTransfers({
1668
+ transfers: transferResult.value.transferIds,
1669
+ transactionId: transferResult.value.transactionId,
1670
+ flags: TransferFlags.void_pending_transfer,
1671
+ });
1672
+ if (isFailure(voidResult)) {
1673
+ logError(voidResult.error, `[SubscriptionController] [purchaseContract] Failed to void pending transfers for purchase:`);
1674
+ }
1675
+ };
1676
+ const sessionResult = await wrap(() => this._stripe.createCheckoutSession({
1677
+ mode: 'payment',
1678
+ line_items: builder.lineItems,
1679
+ success_url: checkoutSession.shouldBeAutomaticallyFulfilled
1680
+ ? request.successUrl
1681
+ : fulfillmentRoute(config.returnUrl, sessionId),
1682
+ cancel_url: request.returnUrl,
1683
+ client_reference_id: sessionId,
1684
+ customer_email: customerEmail,
1685
+ expires_at: Math.floor(Date.now() / 1000) + expirationSeconds,
1686
+ metadata: {
1687
+ userId: request.userId,
1688
+ checkoutSessionId: sessionId,
1689
+ transactionId: transferResult.value.transactionId,
1690
+ },
1691
+ payment_intent_data: {
1692
+ // application_fee_amount: applicationFee,
1693
+ transfer_group: item.id,
1694
+ },
1695
+ // connect: {
1696
+ // stripeAccount: metrics.,
1697
+ // },
1698
+ }));
1699
+ if (isFailure(sessionResult)) {
1700
+ logError(sessionResult.error, `[SubscriptionController] [purchaseContract] Failed to create checkout session for contract: ${item.id}`);
1701
+ await voidTransfers();
1702
+ return failure({
1703
+ errorCode: 'server_error',
1704
+ errorMessage: 'Failed to create checkout session for contract.',
1705
+ });
1706
+ }
1707
+ const session = sessionResult.value;
1708
+ checkoutSession.stripeCheckoutSessionId = session.id;
1709
+ if (session.invoice) {
1710
+ checkoutSession.invoice = {
1711
+ currency: session.invoice.currency,
1712
+ paid: session.invoice.paid,
1713
+ description: session.invoice.description,
1714
+ status: session.invoice.status,
1715
+ stripeInvoiceId: session.invoice.id,
1716
+ stripeHostedInvoiceUrl: session.invoice.hosted_invoice_url,
1717
+ stripeInvoicePdfUrl: session.invoice.invoice_pdf,
1718
+ tax: session.invoice.tax,
1719
+ total: session.invoice.total,
1720
+ subtotal: session.invoice.subtotal,
1721
+ };
1722
+ }
1723
+ checkoutSession.status = session.status;
1724
+ checkoutSession.paymentStatus = session.payment_status;
1725
+ checkoutSession.paid =
1726
+ session.payment_status === 'paid' ||
1727
+ session.payment_status === 'no_payment_required';
1728
+ checkoutSession.transactionId =
1729
+ transferResult.value.transactionId;
1730
+ checkoutSession.transferIds = transferResult.value.transferIds;
1731
+ checkoutSession.transfersPending = true;
1732
+ url = session.url;
1733
+ // Do not immediately fulfill since we need to wait for Stripe webhook
1734
+ immediateFulfillment = false;
1735
+ }
1736
+ await this._authStore.updateCheckoutSessionInfo(checkoutSession);
1737
+ if (immediateFulfillment) {
1738
+ const fulfillmentResult = await this.fulfillCheckoutSession({
1739
+ sessionId: checkoutSession.id,
1740
+ activation: 'now',
1741
+ userId: request.userId,
1742
+ });
1743
+ if (fulfillmentResult.success === false) {
1744
+ console.error(`[SubscriptionController] [purchaseContract] Failed to immediately fulfill checkout session for contract: ${item.id}:`, fulfillmentResult.errorMessage);
1745
+ }
1746
+ }
1747
+ return success({
1748
+ url,
1749
+ sessionId: sessionId,
1750
+ });
1751
+ }
1752
+ catch (err) {
1753
+ console.error('[SubscriptionController] An error occurred while purchasing a contract:', err);
1754
+ return failure({
1755
+ errorCode: 'server_error',
1756
+ errorMessage: 'A server error occurred.',
1757
+ });
1758
+ }
1759
+ }
1760
+ /**
1761
+ * Cancels a contract and issues a refund if applicable.
1762
+ * @param request The request to cancel the contract.
1763
+ * @returns A promise that resolves to the result of the cancel contract operation.
1764
+ */
1765
+ async cancelContract(request) {
1766
+ const context = await this._policies.constructAuthorizationContext({
1767
+ recordKeyOrRecordName: request.recordName,
1768
+ userId: request.userId,
1769
+ });
1770
+ if (context.success === false) {
1771
+ return failure(context);
1772
+ }
1773
+ const recordName = context.context.recordName;
1774
+ const item = await this._contractRecords.getItemByAddress(recordName, request.address);
1775
+ if (!item) {
1776
+ return failure({
1777
+ errorCode: 'not_found',
1778
+ errorMessage: 'The contract could not be found.',
1779
+ });
1780
+ }
1781
+ const authorization = await this._policies.authorizeUserAndInstances(context.context, {
1782
+ userId: request.userId,
1783
+ instances: request.instances,
1784
+ resourceKind: 'contract',
1785
+ resourceId: item.address,
1786
+ markers: item.markers,
1787
+ action: 'cancel',
1788
+ });
1789
+ if (authorization.success === false) {
1790
+ return failure(authorization);
1791
+ }
1792
+ if (item.status === 'closed') {
1793
+ return success({
1794
+ refundedAmount: 0,
1795
+ refundCurrency: CurrencyCodes.usd,
1796
+ });
1797
+ }
1798
+ const refundResult = await this._refundContract(request, item, context.context);
1799
+ if (isSuccess(refundResult)) {
1800
+ await this._contractRecords.markContractAsClosed(recordName, item.address);
1801
+ }
1802
+ return refundResult;
1803
+ }
1804
+ /**
1805
+ * Issues a new invoice for a contract.
1806
+ * @param request The request to invoice the contract.
1807
+ */
1808
+ async invoiceContract(request) {
1809
+ if (!this._financialController) {
1810
+ return failure({
1811
+ errorCode: 'not_supported',
1812
+ errorMessage: 'This operation is not supported.',
1813
+ });
1814
+ }
1815
+ if (request.amount <= 0) {
1816
+ return failure({
1817
+ errorCode: 'invalid_request',
1818
+ errorMessage: 'The invoice amount must be greater than zero.',
1819
+ });
1820
+ }
1821
+ const contract = await this._contractRecords.getItemById(request.contractId);
1822
+ if (!contract?.contract) {
1823
+ return failure({
1824
+ errorCode: 'not_found',
1825
+ errorMessage: 'The contract could not be found.',
1826
+ });
1827
+ }
1828
+ if (!isSuperUserRole(request.userRole)) {
1829
+ if (contract.contract.holdingUserId !== request.userId) {
1830
+ return failure({
1831
+ errorCode: 'not_authorized',
1832
+ errorMessage: 'You are not authorized to invoice for the contract.',
1833
+ });
1834
+ }
1835
+ }
1836
+ if (contract.contract.status !== 'open') {
1837
+ return failure({
1838
+ errorCode: 'invalid_request',
1839
+ errorMessage: 'The contract is not open for invoicing.',
1840
+ });
1841
+ }
1842
+ const balance = await this._financialController.getAccountBalance({
1843
+ contractId: contract.contract.id,
1844
+ ledger: LEDGERS.usd,
1845
+ });
1846
+ if (isFailure(balance)) {
1847
+ logError(balance.error, `[SubscriptionController] [invoiceContract] Failed to get account balance for contract: ${contract.contract.id}`);
1848
+ return balance;
1849
+ }
1850
+ if (balance.value.freeCreditBalance() < request.amount) {
1851
+ return failure({
1852
+ errorCode: 'insufficient_funds',
1853
+ errorMessage: 'The contract does not have sufficient funds to cover the invoice amount.',
1854
+ });
1855
+ }
1856
+ const now = Date.now();
1857
+ const invoiceId = uuid();
1858
+ await this._contractRecords.createInvoice({
1859
+ id: invoiceId,
1860
+ contractId: contract.contract.id,
1861
+ amount: request.amount,
1862
+ status: 'open',
1863
+ payoutDestination: request.payoutDestination,
1864
+ note: request.note,
1865
+ openedAtMs: now,
1866
+ createdAtMs: now,
1867
+ updatedAtMs: now,
1868
+ });
1869
+ return success({
1870
+ invoiceId,
1871
+ });
1872
+ }
1873
+ /**
1874
+ * Cancels an invoice for a contract.
1875
+ * @param request The request to cancel the invoice.
1876
+ */
1877
+ async cancelInvoice(request) {
1878
+ const invoice = await this._contractRecords.getInvoiceById(request.invoiceId);
1879
+ if (!invoice?.invoice) {
1880
+ return failure({
1881
+ errorCode: 'not_found',
1882
+ errorMessage: 'The invoice could not be found.',
1883
+ });
1884
+ }
1885
+ // TODO: Use the permissions system for viewing invoices
1886
+ if (!isSuperUserRole(request.userRole)) {
1887
+ if (invoice.contract.issuingUserId !== request.userId &&
1888
+ invoice.contract.holdingUserId !== request.userId) {
1889
+ return failure({
1890
+ errorCode: 'not_authorized',
1891
+ errorMessage: 'You are not authorized to perform this operation.',
1892
+ });
1893
+ }
1894
+ }
1895
+ if (invoice.invoice.status !== 'open') {
1896
+ return failure({
1897
+ errorCode: 'invalid_request',
1898
+ errorMessage: 'Only open invoices can be cancelled.',
1899
+ });
1900
+ }
1901
+ await this._contractRecords.markOpenInvoiceAs(request.invoiceId, 'void');
1902
+ return success();
1903
+ }
1904
+ /**
1905
+ * Lists all invoices for a contract.
1906
+ * @param request The request to list the invoices.
1907
+ */
1908
+ async listContractInvoices(request) {
1909
+ const contract = await this._contractRecords.getItemById(request.contractId);
1910
+ if (!contract?.contract) {
1911
+ return failure({
1912
+ errorCode: 'not_found',
1913
+ errorMessage: 'The contract could not be found.',
1914
+ });
1915
+ }
1916
+ // TODO: Use the permissions system for viewing invoices
1917
+ if (!isSuperUserRole(request.userRole)) {
1918
+ if (contract.contract.issuingUserId !== request.userId &&
1919
+ contract.contract.holdingUserId !== request.userId) {
1920
+ return failure({
1921
+ errorCode: 'not_authorized',
1922
+ errorMessage: 'You are not authorized to view invoices for the contract.',
1923
+ });
1924
+ }
1925
+ }
1926
+ const invoices = await this._contractRecords.listInvoicesForContract(request.contractId);
1927
+ return success(invoices);
1928
+ }
1929
+ /**
1930
+ * Pays an invoice for a contract.
1931
+ * @param request The request to pay the invoice.
1932
+ */
1933
+ async payContractInvoice(request) {
1934
+ if (!this._financialController) {
1935
+ return failure({
1936
+ errorCode: 'not_supported',
1937
+ errorMessage: 'This operation is not supported.',
1938
+ });
1939
+ }
1940
+ const invoice = await this._contractRecords.getInvoiceById(request.invoiceId);
1941
+ if (!invoice) {
1942
+ return failure({
1943
+ errorCode: 'not_found',
1944
+ errorMessage: 'The invoice could not be found.',
1945
+ });
1946
+ }
1947
+ if (!isSuperUserRole(request.userRole)) {
1948
+ if (invoice.contract.issuingUserId !== request.userId) {
1949
+ return failure({
1950
+ errorCode: 'not_authorized',
1951
+ errorMessage: 'You are not authorized to pay invoices for the contract.',
1952
+ });
1953
+ }
1954
+ }
1955
+ // transfer from contract account to payout destination
1956
+ if (invoice.invoice.status !== 'open') {
1957
+ return failure({
1958
+ errorCode: 'invalid_request',
1959
+ errorMessage: 'The invoice is not open and cannot be paid.',
1960
+ });
1961
+ }
1962
+ const holdingUser = await this._authStore.findUser(invoice.contract.holdingUserId);
1963
+ if (!holdingUser) {
1964
+ console.error(`[SubscriptionController] [payInvoice] Failed to find holding user for contract: ${invoice.contract.id}`);
1965
+ return failure({
1966
+ errorCode: 'server_error',
1967
+ errorMessage: 'The server encountered an error.',
1968
+ });
1969
+ }
1970
+ const [userAccount, contractAccount] = await Promise.all([
1971
+ this._financialController.getOrCreateFinancialAccount({
1972
+ userId: holdingUser.id,
1973
+ ledger: LEDGERS.usd,
1974
+ }),
1975
+ this._financialController.getFinancialAccount({
1976
+ contractId: invoice.contract.id,
1977
+ ledger: LEDGERS.usd,
1978
+ }),
1979
+ ]);
1980
+ if (isFailure(userAccount)) {
1981
+ logError(userAccount.error, `[SubscriptionController] [payInvoice] Failed to get or create financial account for user: ${holdingUser.id}`);
1982
+ return userAccount;
1983
+ }
1984
+ else if (isFailure(contractAccount)) {
1985
+ logError(contractAccount.error, `[SubscriptionController] [payInvoice] Failed to get financial account for contract: ${invoice.contract.id}`);
1986
+ return contractAccount;
1987
+ }
1988
+ if (invoice.invoice.payoutDestination === 'stripe') {
1989
+ if (!holdingUser.stripeAccountId) {
1990
+ return failure({
1991
+ errorCode: 'invalid_request',
1992
+ errorMessage: 'The user to be paid does not have a Stripe account connected.',
1993
+ });
1994
+ }
1995
+ if (holdingUser.stripeAccountStatus !== 'active') {
1996
+ return failure({
1997
+ errorCode: 'invalid_request',
1998
+ errorMessage: 'The user to be paid does not have an active Stripe account.',
1999
+ });
2000
+ }
2001
+ }
2002
+ const transactionResult = await this._financialController.internalTransaction({
2003
+ transfers: [
2004
+ {
2005
+ amount: invoice.invoice.amount,
2006
+ creditAccountId: userAccount.value.account.id,
2007
+ debitAccountId: contractAccount.value.account.id,
2008
+ code: TransferCodes.contract_payment,
2009
+ currency: CurrencyCodes.usd,
2010
+ },
2011
+ ],
2012
+ });
2013
+ if (isFailure(transactionResult)) {
2014
+ if (transactionResult.error.errorCode === 'debits_exceed_credits') {
2015
+ return failure({
2016
+ errorCode: 'insufficient_funds',
2017
+ errorMessage: 'The contract does not have sufficient funds to pay the invoice.',
2018
+ });
2019
+ }
2020
+ logError(transactionResult.error, `[SubscriptionController] [payInvoice] Failed to pay invoice: ${invoice.invoice.id}`);
2021
+ return failure({
2022
+ errorCode: 'server_error',
2023
+ errorMessage: 'The server encountered an error.',
2024
+ });
2025
+ }
2026
+ await this._contractRecords.markOpenInvoiceAs(request.invoiceId, 'paid');
2027
+ if (invoice.invoice.payoutDestination === 'stripe') {
2028
+ const result = await this.payoutAccount({
2029
+ userId: null,
2030
+ userRole: 'system',
2031
+ payoutUserId: holdingUser.id,
2032
+ payoutDestination: 'stripe',
2033
+ payoutAmount: invoice.invoice.amount,
2034
+ contractId: invoice.contract.id,
2035
+ invoiceId: invoice.invoice.id,
2036
+ });
2037
+ if (isFailure(result)) {
2038
+ return result;
2039
+ }
2040
+ }
2041
+ else if (invoice.invoice.payoutDestination === 'account') {
2042
+ // Nothing to do since we already transferred to the user's account
2043
+ }
2044
+ else {
2045
+ return failure({
2046
+ errorCode: 'invalid_request',
2047
+ errorMessage: 'The invoice has an invalid payout destination.',
2048
+ });
2049
+ }
2050
+ return success();
2051
+ }
2052
+ /**
2053
+ * Attempts to payout an account.
2054
+ * @param request The request.
2055
+ */
2056
+ async payoutAccount(request) {
2057
+ if (!this._financialController) {
2058
+ return failure({
2059
+ errorCode: 'not_supported',
2060
+ errorMessage: 'This operation is not supported.',
2061
+ });
2062
+ }
2063
+ if (typeof request.payoutAmount === 'number' &&
2064
+ request.payoutAmount <= 0) {
2065
+ return failure({
2066
+ errorCode: 'invalid_request',
2067
+ errorMessage: 'The payout amount must be greater than zero.',
2068
+ });
2069
+ }
2070
+ if (!isSuperUserRole(request.userRole)) {
2071
+ if (request.payoutUserId &&
2072
+ request.userId !== request.payoutUserId) {
2073
+ return failure({
2074
+ errorCode: 'not_authorized',
2075
+ errorMessage: 'You are not authorized to payout this account.',
2076
+ });
2077
+ }
2078
+ else if (request.payoutStudioId) {
2079
+ // TODO: Check studio permissions
2080
+ return failure({
2081
+ errorCode: 'not_supported',
2082
+ errorMessage: 'Studio payouts are not supported yet.',
2083
+ });
2084
+ }
2085
+ }
2086
+ if (request.payoutDestination === 'stripe') {
2087
+ return this._payoutToStripe(request);
2088
+ }
2089
+ else if (request.payoutDestination === 'cash') {
2090
+ if (!isSuperUserRole(request.userRole)) {
2091
+ return failure({
2092
+ errorCode: 'not_authorized',
2093
+ errorMessage: 'You are not authorized to payout to cash.',
2094
+ });
2095
+ }
2096
+ return this._payoutToCash(request);
2097
+ }
2098
+ }
2099
+ async _payoutToStripe(request) {
2100
+ if (request.payoutStudioId && request.payoutUserId) {
2101
+ return failure({
2102
+ errorCode: 'invalid_request',
2103
+ errorMessage: 'Cannot payout to both a user and a studio at the same time.',
2104
+ });
2105
+ }
2106
+ let destinationStripeAccount;
2107
+ if (request.payoutUserId) {
2108
+ const user = await this._authStore.findUser(request.payoutUserId);
2109
+ if (!user) {
2110
+ return failure({
2111
+ errorCode: 'not_found',
2112
+ errorMessage: 'The user could not be found.',
2113
+ });
2114
+ }
2115
+ if (!user.stripeAccountId) {
2116
+ return failure({
2117
+ errorCode: 'invalid_request',
2118
+ errorMessage: 'The user does not have a Stripe account connected.',
2119
+ });
2120
+ }
2121
+ if (user.stripeAccountStatus !== 'active') {
2122
+ return failure({
2123
+ errorCode: 'invalid_request',
2124
+ errorMessage: 'The user does not have an active Stripe account.',
2125
+ });
2126
+ }
2127
+ destinationStripeAccount = user.stripeAccountId;
2128
+ }
2129
+ else if (request.payoutStudioId) {
2130
+ const studio = await this._recordsStore.getStudioById(request.payoutStudioId);
2131
+ if (!studio) {
2132
+ return failure({
2133
+ errorCode: 'not_found',
2134
+ errorMessage: 'The studio could not be found.',
2135
+ });
2136
+ }
2137
+ if (!studio.stripeAccountId) {
2138
+ return failure({
2139
+ errorCode: 'invalid_request',
2140
+ errorMessage: 'The studio does not have a Stripe account connected.',
2141
+ });
2142
+ }
2143
+ if (studio.stripeAccountStatus !== 'active') {
2144
+ return failure({
2145
+ errorCode: 'invalid_request',
2146
+ errorMessage: 'The studio does not have an active Stripe account.',
2147
+ });
2148
+ }
2149
+ destinationStripeAccount = studio.stripeAccountId;
2150
+ }
2151
+ const [debitAccount] = await Promise.all([
2152
+ this._financialController.getOrCreateFinancialAccount({
2153
+ userId: request.payoutUserId,
2154
+ studioId: request.payoutStudioId,
2155
+ ledger: LEDGERS.usd,
2156
+ }),
2157
+ ]);
2158
+ if (isFailure(debitAccount)) {
2159
+ logError(debitAccount.error, `[SubscriptionController] [payoutAccount] Failed to get or create financial account:`);
2160
+ return debitAccount;
2161
+ }
2162
+ let amount;
2163
+ let balancingDebit = false;
2164
+ if (request.payoutAmount) {
2165
+ amount = request.payoutAmount;
2166
+ }
2167
+ else {
2168
+ amount = AMOUNT_MAX;
2169
+ balancingDebit = true;
2170
+ }
2171
+ const transactionResult = await this._financialController.internalTransaction({
2172
+ transfers: [
2173
+ {
2174
+ amount: amount,
2175
+ creditAccountId: ACCOUNT_IDS.assets_stripe,
2176
+ debitAccountId: debitAccount.value.account.id,
2177
+ code: TransferCodes.user_payout,
2178
+ currency: CurrencyCodes.usd,
2179
+ pending: true,
2180
+ timeoutSeconds: STRIPE_PAYOUT_TIMEOUT_SECONDS,
2181
+ balancingDebit: balancingDebit,
2182
+ },
2183
+ ],
2184
+ });
2185
+ if (isFailure(transactionResult)) {
2186
+ if (transactionResult.error.errorCode === 'debits_exceed_credits') {
2187
+ return failure({
2188
+ errorCode: 'insufficient_funds',
2189
+ errorMessage: 'The account does not have sufficient funds to complete the payout.',
2190
+ });
2191
+ }
2192
+ else if (transactionResult.error.errorCode === 'credits_exceed_debits') {
2193
+ logError(transactionResult.error, `[SubscriptionController] [payoutAccount] Unable to payout to Stripe due to insufficient funds:`);
2194
+ return failure({
2195
+ errorCode: 'server_error',
2196
+ errorMessage: 'The server encountered an error.',
2197
+ });
2198
+ }
2199
+ logError(transactionResult.error, `[SubscriptionController] [payoutAccount] Failed to create payout transaction:`);
2200
+ return failure({
2201
+ errorCode: 'server_error',
2202
+ errorMessage: 'The server encountered an error.',
2203
+ });
2204
+ }
2205
+ const voidTransfers = async (payoutId) => {
2206
+ console.log(`[SubscriptionController] [payoutAccount] Voiding pending transfers for failed payout.`);
2207
+ const voidResult = await this._financialController.completePendingTransfers({
2208
+ transfers: transactionResult.value.transferIds,
2209
+ transactionId: transactionResult.value.transactionId,
2210
+ flags: TransferFlags.void_pending_transfer,
2211
+ });
2212
+ if (isFailure(voidResult)) {
2213
+ logError(voidResult.error, `[SubscriptionController] [payoutAccount] Failed to void pending transfers for payout:`);
2214
+ }
2215
+ else if (payoutId) {
2216
+ await this._financialStore.markPayoutAsVoided(payoutId, voidResult.value.transferIds[0], Date.now());
2217
+ }
2218
+ };
2219
+ const transfer = await this._financialController.getTransfer(transactionResult.value.transferIds[0]);
2220
+ if (isFailure(transfer)) {
2221
+ logError(transfer.error, `[SubscriptionController] [payoutAccount] Failed to get transfer for payout:`);
2222
+ await voidTransfers();
2223
+ return failure({
2224
+ errorCode: 'server_error',
2225
+ errorMessage: 'The server encountered an error.',
2226
+ });
2227
+ }
2228
+ else if (transfer.value.amount <= 0) {
2229
+ return failure({
2230
+ errorCode: 'invalid_request',
2231
+ errorMessage: 'There are no funds to transfer.',
2232
+ });
2233
+ }
2234
+ const payoutId = uuid();
2235
+ try {
2236
+ await this._financialStore.createExternalPayout({
2237
+ id: payoutId,
2238
+ amount: Number(transfer.value.amount),
2239
+ externalDestination: 'stripe',
2240
+ destinationStripeAccountId: destinationStripeAccount,
2241
+ initatedAtMs: Date.now(),
2242
+ transferId: transfer.value.id.toString(),
2243
+ transactionId: transactionResult.value.transactionId,
2244
+ invoiceId: request.invoiceId,
2245
+ userId: request.payoutUserId,
2246
+ studioId: request.payoutStudioId,
2247
+ });
2248
+ let sourceTransaction = undefined;
2249
+ if (request.contractId) {
2250
+ const contract = await this._contractRecords.getItemById(request.contractId);
2251
+ if (contract?.contract.stripePaymentIntentId) {
2252
+ const paymentIntent = await this._stripe.getPaymentIntentById(contract.contract.stripePaymentIntentId);
2253
+ sourceTransaction = paymentIntent.latest_charge;
2254
+ }
2255
+ }
2256
+ const result = await this._stripe.createTransfer({
2257
+ currency: 'usd',
2258
+ destination: destinationStripeAccount,
2259
+ amount: Number(transfer.value.amount),
2260
+ description: 'Account Payout',
2261
+ transferGroup: request.contractId,
2262
+ sourceTransaction,
2263
+ metadata: {
2264
+ transactionId: transactionResult.value.transactionId,
2265
+ payoutId: payoutId,
2266
+ payoutUserId: request.payoutUserId,
2267
+ payoutStudioId: request.payoutStudioId,
2268
+ },
2269
+ });
2270
+ // TODO: update external payout with stripe transfer id
2271
+ await this._financialStore.updateExternalPayout({
2272
+ id: payoutId,
2273
+ stripeTransferId: result.id,
2274
+ });
2275
+ const postResult = await this._financialController.completePendingTransfers({
2276
+ transfers: transactionResult.value.transferIds,
2277
+ transactionId: transactionResult.value.transactionId,
2278
+ });
2279
+ if (isFailure(postResult)) {
2280
+ logError(postResult.error, `[SubscriptionController] [payoutAccount] Failed to complete pending transfers for payout:`);
2281
+ await voidTransfers(payoutId);
2282
+ return failure({
2283
+ errorCode: 'server_error',
2284
+ errorMessage: 'The server encountered an error.',
2285
+ });
2286
+ }
2287
+ await this._financialStore.markPayoutAsPosted(payoutId, postResult.value.transferIds[0], Date.now());
2288
+ return success({
2289
+ payoutId,
2290
+ });
2291
+ }
2292
+ catch (err) {
2293
+ console.error(`[SubscriptionController] [payoutAccount] Failed to create Stripe transfer for payout:`, err);
2294
+ await voidTransfers(payoutId);
2295
+ return failure({
2296
+ errorCode: 'server_error',
2297
+ errorMessage: 'The server encountered an error.',
2298
+ });
2299
+ }
2300
+ }
2301
+ async _payoutToCash(request) {
2302
+ if (request.payoutStudioId && request.payoutUserId) {
2303
+ return failure({
2304
+ errorCode: 'invalid_request',
2305
+ errorMessage: 'Cannot payout to both a user and a studio at the same time.',
2306
+ });
2307
+ }
2308
+ const [debitAccount] = await Promise.all([
2309
+ this._financialController.getOrCreateFinancialAccount({
2310
+ userId: request.payoutUserId,
2311
+ studioId: request.payoutStudioId,
2312
+ ledger: LEDGERS.usd,
2313
+ }),
2314
+ ]);
2315
+ if (isFailure(debitAccount)) {
2316
+ logError(debitAccount.error, `[SubscriptionController] [payoutAccount] Failed to get or create financial account:`);
2317
+ return debitAccount;
2318
+ }
2319
+ let amount;
2320
+ let balancingDebit = false;
2321
+ if (request.payoutAmount) {
2322
+ amount = request.payoutAmount;
2323
+ }
2324
+ else {
2325
+ amount = AMOUNT_MAX;
2326
+ balancingDebit = true;
2327
+ }
2328
+ const transactionResult = await this._financialController.internalTransaction({
2329
+ transfers: [
2330
+ {
2331
+ amount: amount,
2332
+ creditAccountId: ACCOUNT_IDS.assets_cash,
2333
+ debitAccountId: debitAccount.value.account.id,
2334
+ code: TransferCodes.user_payout,
2335
+ currency: CurrencyCodes.usd,
2336
+ balancingDebit: balancingDebit,
2337
+ },
2338
+ ],
2339
+ });
2340
+ if (isFailure(transactionResult)) {
2341
+ if (transactionResult.error.errorCode === 'debits_exceed_credits') {
2342
+ return failure({
2343
+ errorCode: 'insufficient_funds',
2344
+ errorMessage: 'The account does not have sufficient funds to complete the payout.',
2345
+ });
2346
+ }
2347
+ else if (transactionResult.error.errorCode === 'credits_exceed_debits') {
2348
+ logError(transactionResult.error, `[SubscriptionController] [payoutAccount] Unable to payout to Stripe due to insufficient funds:`);
2349
+ return failure({
2350
+ errorCode: 'server_error',
2351
+ errorMessage: 'The server encountered an error.',
2352
+ });
2353
+ }
2354
+ logError(transactionResult.error, `[SubscriptionController] [payoutAccount] Failed to create payout transaction:`);
2355
+ return failure({
2356
+ errorCode: 'server_error',
2357
+ errorMessage: 'The server encountered an error.',
2358
+ });
2359
+ }
2360
+ const transfer = await this._financialController.getTransfer(transactionResult.value.transferIds[0]);
2361
+ if (isFailure(transfer)) {
2362
+ logError(transfer.error, `[SubscriptionController] [payoutAccount] Failed to get transfer for payout:`);
2363
+ return failure({
2364
+ errorCode: 'server_error',
2365
+ errorMessage: 'The server encountered an error.',
2366
+ });
2367
+ }
2368
+ const payoutId = uuid();
2369
+ await this._financialStore.createExternalPayout({
2370
+ id: payoutId,
2371
+ amount: Number(transfer.value.amount),
2372
+ externalDestination: 'cash',
2373
+ initatedAtMs: Date.now(),
2374
+ transferId: transfer.value.id.toString(),
2375
+ transactionId: transactionResult.value.transactionId,
2376
+ invoiceId: request.invoiceId,
2377
+ userId: request.payoutUserId,
2378
+ studioId: request.payoutStudioId,
2379
+ postedAtMs: Date.now(),
2380
+ postedTransferId: transfer.value.id.toString(),
2381
+ });
2382
+ return success({
2383
+ payoutId,
2384
+ });
2385
+ }
2386
+ /**
2387
+ * Initiates a refund for a canceled contract.
2388
+ * @param request The request to cancel the contract.
2389
+ * @param item The contract record to refund.
2390
+ * @param context The authorization context for the operation.
2391
+ * @returns A promise that resolves to the result of the refund operation.
2392
+ */
2393
+ async _refundContract(request, item, context) {
2394
+ const contractAccount = await this._financialController.getFinancialAccount({
2395
+ contractId: item.id,
2396
+ ledger: LEDGERS.usd,
2397
+ });
2398
+ if (isFailure(contractAccount)) {
2399
+ if (contractAccount.error.errorCode === 'not_found') {
2400
+ return success({
2401
+ refundedAmount: 0,
2402
+ refundCurrency: CurrencyCodes.usd,
2403
+ });
2404
+ }
2405
+ logError(contractAccount.error, `[SubscriptionController] [cancelContract] Failed to get USD financial account for contract:`);
2406
+ return contractAccount;
2407
+ }
2408
+ let refundAccount;
2409
+ if (request.refundAccountId) {
2410
+ const account = await this._financialController.getAccount(request.refundAccountId);
2411
+ if (isFailure(account)) {
2412
+ logError(account.error, `[SubscriptionController] [cancelContract] Failed to get refund account:`);
2413
+ return account;
2414
+ }
2415
+ refundAccount = account.value;
2416
+ }
2417
+ if (!refundAccount) {
2418
+ const account = await this._financialController.getOrCreateFinancialAccount({
2419
+ userId: context.recordOwnerId,
2420
+ studioId: context.recordStudioId,
2421
+ ledger: contractAccount.value.account.ledger,
2422
+ });
2423
+ if (isFailure(account)) {
2424
+ logError(account.error, `[SubscriptionController] [cancelContract] Failed to get or create refund account:`);
2425
+ return account;
2426
+ }
2427
+ refundAccount = account.value.account;
2428
+ }
2429
+ console.log(`[SubscriptionController] [cancelContract contractId: ${item.id} contractAccountId: ${contractAccount.value.account.id} refundAccountId: ${refundAccount.id}] Attempting to cancel contract.`);
2430
+ const refundId = this._financialController.generateId();
2431
+ const cancelId = this._financialController.generateId();
2432
+ const transferResult = await this._financialController.internalTransaction({
2433
+ transfers: [
2434
+ {
2435
+ transferId: refundId,
2436
+ amount: AMOUNT_MAX,
2437
+ debitAccountId: contractAccount.value.account.id,
2438
+ creditAccountId: refundAccount.id,
2439
+ code: TransferCodes.contract_refund,
2440
+ currency: CurrencyCodes.usd,
2441
+ balancingDebit: true,
2442
+ },
2443
+ {
2444
+ transferId: cancelId,
2445
+ amount: 0,
2446
+ debitAccountId: contractAccount.value.account.id,
2447
+ creditAccountId: refundAccount.id,
2448
+ code: TransferCodes.account_closing,
2449
+ currency: CurrencyCodes.usd,
2450
+ closingDebit: true,
2451
+ },
2452
+ ],
2453
+ });
2454
+ if (isFailure(transferResult)) {
2455
+ logError(transferResult.error, `[SubscriptionController] [cancelContract] Failed to refund contract:`);
2456
+ return failure({
2457
+ errorCode: 'server_error',
2458
+ errorMessage: 'Failed to refund the contract.',
2459
+ });
2460
+ }
2461
+ const transfer = await this._financialController.getTransfer(refundId);
2462
+ if (isFailure(transfer)) {
2463
+ logError(transfer.error, `[SubscriptionController] [cancelContract] Failed to get transfer for contract refund:`);
2464
+ return failure({
2465
+ errorCode: 'server_error',
2466
+ errorMessage: 'The server encountered an error.',
2467
+ });
2468
+ }
2469
+ return success({
2470
+ refundedAmount: Number(transfer.value.amount),
2471
+ refundCurrency: CURRENCIES.get(transfer.value.ledger),
2472
+ });
2473
+ }
2474
+ /**
2475
+ * Completes a checkout session. Grants the user access to the purchased items or completes a contract purchase.
2476
+ * @param request The request for the checkout session fulfillment.
2477
+ * @returns A promise that resolves to the result of the checkout session fulfillment.
2478
+ */
2479
+ async fulfillCheckoutSession(request) {
2480
+ try {
2481
+ const session = await this._authStore.getCheckoutSessionById(request.sessionId);
2482
+ if (!session) {
2483
+ return {
2484
+ success: false,
2485
+ errorCode: 'not_found',
2486
+ errorMessage: 'The checkout session does not exist.',
2487
+ };
2488
+ }
2489
+ if (!!session.userId && session.userId !== request.userId) {
2490
+ return {
2491
+ success: false,
2492
+ errorCode: 'not_authorized',
2493
+ errorMessage: 'You are not authorized to accept fulfillment of this checkout session.',
2494
+ };
2495
+ }
2496
+ if (session.stripeStatus === 'expired') {
2497
+ return {
2498
+ success: false,
2499
+ errorCode: 'invalid_request',
2500
+ errorMessage: 'The checkout session has expired.',
2501
+ };
2502
+ }
2503
+ else if (session.stripeStatus === 'open') {
2504
+ return {
2505
+ success: false,
2506
+ errorCode: 'invalid_request',
2507
+ errorMessage: 'The checkout session has not been completed.',
2508
+ };
2509
+ }
2510
+ else if (session.stripePaymentStatus === 'unpaid') {
2511
+ return {
2512
+ success: false,
2513
+ errorCode: 'invalid_request',
2514
+ errorMessage: 'The checkout session has not been paid for.',
2515
+ };
2516
+ }
2517
+ else if (!session.paid) {
2518
+ return {
2519
+ success: false,
2520
+ errorCode: 'invalid_request',
2521
+ errorMessage: 'The checkout session has not been paid for.',
2522
+ };
2523
+ }
2524
+ else if (session.fulfilledAtMs > 0) {
2525
+ return {
2526
+ success: true,
2527
+ };
2528
+ }
2529
+ console.log(`[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId}] Fulfilling checkout session.`);
2530
+ if (session.transferIds && session.transfersPending) {
2531
+ console.log(`[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId} transactionId: ${session.transactionId}] Posting pending transfers.`);
2532
+ const transferResult = await this._financialController.completePendingTransfers({
2533
+ transfers: session.transferIds,
2534
+ transactionId: session.transactionId,
2535
+ });
2536
+ if (isFailure(transferResult)) {
2537
+ if (transferResult.error.errorCode !==
2538
+ 'transfer_already_completed') {
2539
+ logError(transferResult.error, `[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId}] Failed to complete pending transfers for checkout session:`);
2540
+ return {
2541
+ success: false,
2542
+ errorCode: 'server_error',
2543
+ errorMessage: 'A server error occurred.',
2544
+ };
2545
+ }
2546
+ }
2547
+ }
2548
+ if (request.activation === 'now') {
2549
+ if (!request.userId) {
2550
+ return {
2551
+ success: false,
2552
+ errorCode: 'invalid_request',
2553
+ errorMessage: 'Guests cannot accept immediate fulfillment of a checkout session.',
2554
+ };
2555
+ }
2556
+ console.log(`[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId}] Activating checkout session.`);
2557
+ // grant user access to the items
2558
+ for (let item of session.items) {
2559
+ if (item.type === 'role') {
2560
+ const result = await this._policyStore.assignSubjectRole(item.recordName, session.userId, 'user', {
2561
+ role: item.role,
2562
+ expireTimeMs: item.roleGrantTimeMs
2563
+ ? Date.now() + item.roleGrantTimeMs
2564
+ : null,
2565
+ });
2566
+ if (result.success === false) {
2567
+ console.error(`[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId}] Unable to grant role to user:`, result);
2568
+ return {
2569
+ success: false,
2570
+ errorCode: 'server_error',
2571
+ errorMessage: 'A server error occurred.',
2572
+ };
2573
+ }
2574
+ await this._authStore.savePurchasedItem({
2575
+ id: uuid(),
2576
+ activatedTimeMs: Date.now(),
2577
+ recordName: item.recordName,
2578
+ purchasableItemAddress: item.purchasableItemAddress,
2579
+ roleName: item.role,
2580
+ roleGrantTimeMs: item.roleGrantTimeMs,
2581
+ userId: session.userId,
2582
+ activationKeyId: null,
2583
+ checkoutSessionId: session.id,
2584
+ });
2585
+ }
2586
+ else if (item.type === 'contract') {
2587
+ // open contract
2588
+ await this._contractRecords.markPendingContractAsOpen(item.recordName, item.contractAddress);
2589
+ }
2590
+ else {
2591
+ // console.warn(
2592
+ // `[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId}] Unknown item type: ${item.type}`
2593
+ // );
2594
+ }
2595
+ }
2596
+ await this._authStore.markCheckoutSessionFulfilled(session.id, Date.now());
2597
+ return {
2598
+ success: true,
2599
+ };
2600
+ }
2601
+ else {
2602
+ if (session.items.some((i) => i.type === 'contract')) {
2603
+ console.log(`[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId}] Cannot defer fulfillment of checkout session with contracts.`);
2604
+ return {
2605
+ success: false,
2606
+ errorCode: 'invalid_request',
2607
+ errorMessage: 'You cannot defer fulfillment of a checkout session with contracts.',
2608
+ };
2609
+ }
2610
+ console.log(`[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId}] Deferring activation for checkout session.`);
2611
+ const secret = fromByteArray(randomBytes(ACCESS_KEY_SECRET_BYTE_LENGTH));
2612
+ const keyId = fromByteArray(randomBytes(ACCESS_KEY_SECRET_BYTE_LENGTH));
2613
+ const hash = hashHighEntropyPasswordWithSalt(secret, keyId);
2614
+ await this._authStore.createActivationKey({
2615
+ id: keyId,
2616
+ secretHash: hash,
2617
+ });
2618
+ const key = formatV1ActivationKey(keyId, secret);
2619
+ // grant user access to the items
2620
+ for (let item of session.items) {
2621
+ if (item.type === 'role') {
2622
+ await this._authStore.savePurchasedItem({
2623
+ id: uuid(),
2624
+ activatedTimeMs: null,
2625
+ recordName: item.recordName,
2626
+ purchasableItemAddress: item.purchasableItemAddress,
2627
+ roleName: item.role,
2628
+ roleGrantTimeMs: item.roleGrantTimeMs,
2629
+ userId: null,
2630
+ activationKeyId: keyId,
2631
+ checkoutSessionId: session.id,
2632
+ });
2633
+ }
2634
+ else {
2635
+ console.warn(`[SubscriptionController] [fulfillCheckoutSession sessionId: ${session.id} userId: ${session.userId}] Unknown item type: ${item.type}`);
2636
+ }
2637
+ }
2638
+ await this._authStore.markCheckoutSessionFulfilled(session.id, Date.now());
2639
+ const config = await this._getConfig();
2640
+ return {
2641
+ success: true,
2642
+ activationKey: key,
2643
+ activationUrl: activationRoute(config.returnUrl, key),
2644
+ };
2645
+ }
2646
+ }
2647
+ catch (err) {
2648
+ console.error('[SubscriptionController] An error occurred while fulfilling a checkout session:', err);
2649
+ return {
2650
+ success: false,
2651
+ errorCode: 'server_error',
2652
+ errorMessage: 'A server error occurred.',
2653
+ };
2654
+ }
2655
+ }
2656
+ async claimActivationKey(request) {
2657
+ try {
2658
+ if (!request.userId && request.target === 'self') {
2659
+ return {
2660
+ success: false,
2661
+ errorCode: 'not_logged_in',
2662
+ errorMessage: 'You need to be logged in to use target = self.',
2663
+ };
2664
+ }
2665
+ const key = parseActivationKey(request.activationKey);
2666
+ if (!key) {
2667
+ return {
2668
+ success: false,
2669
+ errorCode: 'invalid_request',
2670
+ errorMessage: 'The activation key is invalid.',
2671
+ };
2672
+ }
2673
+ const [keyId, secret] = key;
2674
+ const activationKey = await this._authStore.getActivationKeyById(keyId);
2675
+ if (!activationKey) {
2676
+ return {
2677
+ success: false,
2678
+ errorCode: 'invalid_request',
2679
+ errorMessage: 'The activation key is invalid.',
2680
+ };
2681
+ }
2682
+ const hash = hashHighEntropyPasswordWithSalt(secret, keyId);
2683
+ if (activationKey.secretHash !== hash) {
2684
+ return {
2685
+ success: false,
2686
+ errorCode: 'invalid_request',
2687
+ errorMessage: 'The activation key is invalid.',
2688
+ };
2689
+ }
2690
+ let userId;
2691
+ let sessionKey;
2692
+ let connectionKey;
2693
+ let expireTimeMs;
2694
+ if (request.target === 'self') {
2695
+ userId = request.userId;
2696
+ }
2697
+ else if (request.target === 'guest') {
2698
+ console.log('[SubscriptionController] [claimActivationKey] Creating user for guest activation key.');
2699
+ const accountResult = await this._auth.createAccount({
2700
+ userRole: 'superUser',
2701
+ ipAddress: request.ipAddress,
2702
+ });
2703
+ if (accountResult.success === false) {
2704
+ console.error(`[SubscriptionController] [claimActivationKey keyId: ${keyId}] Unable to create user for guest activation key:`, accountResult);
2705
+ return {
2706
+ success: false,
2707
+ errorCode: 'server_error',
2708
+ errorMessage: 'A server error occurred.',
2709
+ };
2710
+ }
2711
+ userId = accountResult.userId;
2712
+ sessionKey = accountResult.sessionKey;
2713
+ connectionKey = accountResult.connectionKey;
2714
+ expireTimeMs = accountResult.expireTimeMs;
2715
+ }
2716
+ console.log(`[SubscriptionController] [claimActivationKey keyId: ${keyId} userId: ${request.userId}] Claiming activation key.`);
2717
+ if (!userId) {
2718
+ return {
2719
+ success: false,
2720
+ errorCode: 'invalid_request',
2721
+ errorMessage: 'The activation key is invalid.',
2722
+ };
2723
+ }
2724
+ const items = await this._authStore.listPurchasedItemsByActivationKeyId(keyId);
2725
+ for (let item of items) {
2726
+ if (item.activatedTimeMs || item.userId) {
2727
+ continue;
2728
+ }
2729
+ const result = await this._policyStore.assignSubjectRole(item.recordName, userId, 'user', {
2730
+ role: item.roleName,
2731
+ expireTimeMs: item.roleGrantTimeMs
2732
+ ? Date.now() + item.roleGrantTimeMs
2733
+ : null,
2734
+ });
2735
+ if (result.success === false) {
2736
+ console.error(`[SubscriptionController] [claimActivationKey keyId: ${keyId} userId: ${userId}] Unable to grant role to user:`, result);
2737
+ return {
2738
+ success: false,
2739
+ errorCode: 'server_error',
2740
+ errorMessage: 'A server error occurred.',
2741
+ };
2742
+ }
2743
+ await this._authStore.savePurchasedItem({
2744
+ ...item,
2745
+ activatedTimeMs: Date.now(),
2746
+ userId,
2747
+ });
2748
+ }
2749
+ return {
2750
+ success: true,
2751
+ userId,
2752
+ sessionKey,
2753
+ connectionKey,
2754
+ expireTimeMs,
2755
+ };
2756
+ }
2757
+ catch (err) {
2758
+ console.error('[SubscriptionController] An error occurred while claiming an activation key:', err);
2759
+ return {
2760
+ success: false,
2761
+ errorCode: 'server_error',
2762
+ errorMessage: 'A server error occurred.',
2763
+ };
2764
+ }
2765
+ }
2766
+ /**
2767
+ * Handles the webhook from Stripe for updating the internal database.
2768
+ */
2769
+ async handleStripeWebhook(request) {
2770
+ if (!this._stripe) {
2771
+ return {
2772
+ success: false,
2773
+ errorCode: 'not_supported',
2774
+ errorMessage: 'This method is not supported.',
2775
+ };
2776
+ }
2777
+ try {
2778
+ if (typeof request.requestBody !== 'string' ||
2779
+ request.requestBody === '') {
2780
+ return {
2781
+ success: false,
2782
+ errorCode: 'invalid_request',
2783
+ errorMessage: 'The request was not valid.',
2784
+ };
2785
+ }
2786
+ if (typeof request.signature !== 'string' ||
2787
+ request.signature === '') {
2788
+ return {
2789
+ success: false,
2790
+ errorCode: 'invalid_request',
2791
+ errorMessage: 'The request was not valid.',
2792
+ };
2793
+ }
2794
+ const config = await this._getConfig();
2795
+ const body = request.requestBody;
2796
+ const signature = request.signature;
2797
+ let event;
2798
+ try {
2799
+ event = this._stripe.constructWebhookEvent(body, signature, config.webhookSecret);
2800
+ }
2801
+ catch (err) {
2802
+ console.log(`[SubscriptionController] [handleStripeWebhook] Unable to construct webhook event:`, err);
2803
+ return {
2804
+ success: false,
2805
+ errorCode: 'invalid_request',
2806
+ errorMessage: 'The request was not valid.',
2807
+ };
2808
+ }
2809
+ console.log(`[SubscriptionController] [handleStripeWebhook] Got event: ${event.type}`);
2810
+ if (event.type === 'customer.subscription.created' ||
2811
+ event.type === 'customer.subscription.deleted' ||
2812
+ event.type === 'customer.subscription.updated') {
2813
+ const subscription = event.data.object;
2814
+ return await this._handleStripeSubscriptionEvent(config, event, subscription);
2815
+ }
2816
+ else if (event.type === 'invoice.paid') {
2817
+ const parseResult = STRIPE_EVENT_INVOICE_PAID_SCHEMA.safeParse(event);
2818
+ if (parseResult.success === false) {
2819
+ console.error(`[SubscriptionController] [handleStripeWebhook] Unable to parse stripe event!`, parseResult.error);
2820
+ return {
2821
+ success: true,
2822
+ };
2823
+ }
2824
+ return await this._handleStripeInvoicePaidEvent(config, event, parseResult.data.data.object);
2825
+ }
2826
+ else if (event.type === 'account.updated') {
2827
+ const parseResult = STRIPE_EVENT_ACCOUNT_UPDATED_SCHEMA.safeParse(event);
2828
+ if (parseResult.success === false) {
2829
+ console.error(`[SubscriptionController] [handleStripeWebhook] Unable to parse stripe event!`, parseResult.error);
2830
+ return {
2831
+ success: false,
2832
+ errorCode: 'invalid_request',
2833
+ errorMessage: 'The request was not able to be parsed.',
2834
+ };
2835
+ }
2836
+ return await this._handleStripeAccountUpdatedEvent(config, parseResult.data);
2837
+ }
2838
+ else if (event.type === 'checkout.session.completed' ||
2839
+ event.type === 'checkout.session.expired' ||
2840
+ event.type === 'checkout.session.async_payment_failed' ||
2841
+ event.type === 'checkout.session.async_payment_succeeded') {
2842
+ const parseResult = STRIPE_EVENT_CHECKOUT_SESSION_SCHEMA.safeParse(event);
2843
+ if (parseResult.success === false) {
2844
+ console.error(`[SubscriptionController] [handleStripeWebhook] Unable to parse stripe event!`, parseResult.error);
2845
+ return {
2846
+ success: false,
2847
+ errorCode: 'invalid_request',
2848
+ errorMessage: 'The request was not able to be parsed.',
2849
+ };
2850
+ }
2851
+ return await this._handleStripeCheckoutSessionEvent(config, event, parseResult.data);
2852
+ }
2853
+ return {
2854
+ success: true,
2855
+ };
2856
+ }
2857
+ catch (err) {
2858
+ const span = trace.getActiveSpan();
2859
+ span?.recordException(err);
2860
+ console.error('[SubscriptionController] An error occurred while handling a stripe webhook:', err);
2861
+ return {
2862
+ success: false,
2863
+ errorCode: 'server_error',
2864
+ errorMessage: 'A server error occurred.',
2865
+ };
2866
+ }
2867
+ }
2868
+ async _handleStripeCheckoutSessionEvent(config, event, sessionEvent) {
2869
+ const stripeSession = sessionEvent.data.object;
2870
+ const sessionId = stripeSession.client_reference_id;
2871
+ if (!sessionId) {
2872
+ console.log(`[SubscriptionController] [handleStripeWebhook] No client_reference_id found in the event.`);
2873
+ return {
2874
+ success: true,
2875
+ };
2876
+ }
2877
+ const session = await this._authStore.getCheckoutSessionById(sessionId);
2878
+ if (!session) {
2879
+ console.log(`[SubscriptionController] [handleStripeWebhook] Could not find session with ID (${sessionId}).`);
2880
+ return {
2881
+ success: false,
2882
+ errorCode: 'invalid_request',
2883
+ errorMessage: 'The session could not be found.',
2884
+ };
2885
+ }
2886
+ if (session.stripeCheckoutSessionId !== stripeSession.id) {
2887
+ console.log(`[SubscriptionController] [handleStripeWebhook] Stripe checkout session ID (${stripeSession.id}) does not match stored ID (${session.stripeCheckoutSessionId}).`);
2888
+ return {
2889
+ success: false,
2890
+ errorCode: 'invalid_request',
2891
+ errorMessage: 'The session ID does not match the expected session ID.',
2892
+ };
2893
+ }
2894
+ console.log(`[SubscriptionController] [handleStripeWebhook] [sessionId: ${sessionId} stripeCheckoutSessionId: ${stripeSession.id} status: ${stripeSession.status} paymentStatus: ${stripeSession.payment_status}] Checkout session updated for session ID.`);
2895
+ const paid = stripeSession.payment_status === 'no_payment_required' ||
2896
+ stripeSession.payment_status === 'paid';
2897
+ await this._authStore.updateCheckoutSessionInfo({
2898
+ ...session,
2899
+ paid,
2900
+ paymentStatus: stripeSession.payment_status,
2901
+ status: stripeSession.status,
2902
+ invoice: null,
2903
+ });
2904
+ if (event.type === 'checkout.session.expired') {
2905
+ if (session.transferIds && session.transfersPending) {
2906
+ console.log(`[SubscriptionController] [handleStripeWebhook] [sessionId: ${sessionId} stripeCheckoutSessionId: ${stripeSession.id} transferIds: [${session.transferIds.join(',')}]] Voiding pending transfers.`);
2907
+ const result = await this._financialController.completePendingTransfers({
2908
+ transfers: session.transferIds,
2909
+ transactionId: session.transactionId,
2910
+ flags: TransferFlags.void_pending_transfer,
2911
+ });
2912
+ if (isFailure(result)) {
2913
+ logError(result.error, `[SubscriptionController] [handleStripeWebhook] Failed to void pending transfers for session ID: ${sessionId}`);
2914
+ return {
2915
+ success: false,
2916
+ errorCode: 'server_error',
2917
+ errorMessage: 'Failed to void pending transfers for session ID.',
2918
+ };
2919
+ }
2920
+ else {
2921
+ await this._authStore.updateCheckoutSessionInfo({
2922
+ ...session,
2923
+ paid,
2924
+ paymentStatus: stripeSession.payment_status,
2925
+ status: stripeSession.status,
2926
+ invoice: null,
2927
+ transfersPending: false,
2928
+ });
2929
+ }
2930
+ }
2931
+ }
2932
+ if (paid &&
2933
+ !session.fulfilledAtMs &&
2934
+ session.shouldBeAutomaticallyFulfilled) {
2935
+ console.log(`[SubscriptionController] [handleStripeWebhook] [sessionId: ${sessionId} stripeCheckoutSessionId: ${stripeSession.id}] Automatically fulfilling checkout session.`);
2936
+ const result = await this.fulfillCheckoutSession({
2937
+ userId: session.userId,
2938
+ activation: 'now',
2939
+ sessionId,
2940
+ });
2941
+ if (result.success === false) {
2942
+ console.error(`[SubscriptionController] [handleStripeWebhook] Failed to fulfill checkout session:`, result);
2943
+ return {
2944
+ success: false,
2945
+ errorCode: 'server_error',
2946
+ errorMessage: 'Failed to fulfill checkout session for session ID.',
2947
+ };
2948
+ }
2949
+ }
2950
+ return {
2951
+ success: true,
2952
+ };
2953
+ }
2954
+ async _handleStripeAccountUpdatedEvent(config, event) {
2955
+ const accountId = event.data.object.id;
2956
+ const account = await this._stripe.getAccountById(accountId);
2957
+ let studio = await this._recordsStore.getStudioByStripeAccountId(accountId);
2958
+ if (studio) {
2959
+ return await this._handleStudioStripeAccountUpdatedEvent(account, studio);
2960
+ }
2961
+ const user = await this._authStore.findUserByStripeAccountId(accountId);
2962
+ if (user) {
2963
+ return await this._handleUserStripeAccountUpdatedEvent(account, user);
2964
+ }
2965
+ console.warn(`[SubscriptionController] [handleStripeWebhook] No user or studio found for account ID (${accountId}).`);
2966
+ return {
2967
+ success: true,
2968
+ };
2969
+ }
2970
+ async _handleStudioStripeAccountUpdatedEvent(account, studio) {
2971
+ if (!studio) {
2972
+ console.log(`[SubscriptionController] [handleStripeWebhook] No studio found for account ID (${account.id}).`);
2973
+ return {
2974
+ success: true,
2975
+ };
2976
+ }
2977
+ const newStatus = getAccountStatus(account);
2978
+ const newRequirementsStatus = getAccountRequirementsStatus(account);
2979
+ if (studio.stripeAccountStatus !== newStatus ||
2980
+ studio.stripeAccountRequirementsStatus !== newRequirementsStatus) {
2981
+ console.log(`[SubscriptionController] [handleStripeWebhook] Updating studio (${studio.id}) account status to ${newStatus} and requirements status to ${newRequirementsStatus}.`);
2982
+ studio = {
2983
+ ...studio,
2984
+ stripeAccountStatus: newStatus,
2985
+ stripeAccountRequirementsStatus: newRequirementsStatus,
2986
+ };
2987
+ await this._recordsStore.updateStudio(studio);
2988
+ }
2989
+ return {
2990
+ success: true,
2991
+ };
2992
+ }
2993
+ async _handleUserStripeAccountUpdatedEvent(account, user) {
2994
+ if (!user) {
2995
+ console.log(`[SubscriptionController] [handleStripeWebhook] No user found for account ID (${account.id}).`);
2996
+ return {
2997
+ success: true,
2998
+ };
2999
+ }
3000
+ const newStatus = getAccountStatus(account);
3001
+ const newRequirementsStatus = getAccountRequirementsStatus(account);
3002
+ if (user.stripeAccountStatus !== newStatus ||
3003
+ user.stripeAccountRequirementsStatus !== newRequirementsStatus) {
3004
+ console.log(`[SubscriptionController] [handleStripeWebhook] Updating user (${user.id}) account status to ${newStatus} and requirements status to ${newRequirementsStatus}.`);
3005
+ user = {
3006
+ ...user,
3007
+ stripeAccountStatus: newStatus,
3008
+ stripeAccountRequirementsStatus: newRequirementsStatus,
3009
+ };
3010
+ await this._authStore.saveUser(user);
3011
+ }
3012
+ return {
3013
+ success: true,
3014
+ };
3015
+ }
3016
+ async _handleStripeInvoicePaidEvent(config, event, invoice) {
3017
+ const stripeSubscriptionId = invoice.subscription;
3018
+ if (stripeSubscriptionId) {
3019
+ const subscription = await this._stripe.getSubscriptionById(stripeSubscriptionId);
3020
+ const status = subscription.status;
3021
+ const customerId = invoice.customer;
3022
+ const lineItems = invoice.lines.data;
3023
+ const periodStartMs = subscription.current_period_start * 1000;
3024
+ const periodEndMs = subscription.current_period_end * 1000;
3025
+ const { sub, item } = findMatchingSubscription(lineItems);
3026
+ const authInvoice = {
3027
+ currency: invoice.currency,
3028
+ description: invoice.description,
3029
+ paid: invoice.paid,
3030
+ status: invoice.status,
3031
+ tax: invoice.tax,
3032
+ total: invoice.total,
3033
+ subtotal: invoice.subtotal,
3034
+ stripeInvoiceId: invoice.id,
3035
+ stripeHostedInvoiceUrl: invoice.hosted_invoice_url,
3036
+ stripeInvoicePdfUrl: invoice.invoice_pdf,
3037
+ };
3038
+ console.log(`[SubscriptionController] [handleStripeWebhook] New invoice paid for customer ID (${customerId}). Subscription ID: ${subscription.id}. Period start: ${periodStartMs}. Period end: ${periodEndMs}.`);
3039
+ if (!sub) {
3040
+ console.error(`[SubscriptionController] [handleStripeWebhook] No matching subscription found for invoice (${invoice.id}).`);
3041
+ return {
3042
+ success: false,
3043
+ errorCode: 'server_error',
3044
+ errorMessage: 'No matching subscription found.',
3045
+ };
3046
+ }
3047
+ const user = await this._authStore.findUserByStripeCustomerId(customerId);
3048
+ if (user) {
3049
+ console.log(`[SubscriptionController] [handleStripeWebhook] Found user (${user.id}) with customer ID (${customerId}).`);
3050
+ await this._authStore.updateSubscriptionPeriod({
3051
+ userId: user.id,
3052
+ subscriptionStatus: status,
3053
+ subscriptionId: sub.id,
3054
+ stripeSubscriptionId,
3055
+ stripeCustomerId: customerId,
3056
+ currentPeriodEndMs: periodEndMs,
3057
+ currentPeriodStartMs: periodStartMs,
3058
+ invoice: authInvoice,
3059
+ });
3060
+ const creditResult = await this._internalTransactionPurchaseCreditsStripe(sub, invoice, {
3061
+ userId: user.id,
3062
+ });
3063
+ if (isFailure(creditResult)) {
3064
+ return genericResult(creditResult);
3065
+ }
3066
+ }
3067
+ else {
3068
+ console.log(`[SubscriptionController] [handleStripeWebhook] No user found for customer ID (${customerId}).`);
3069
+ const studio = await this._recordsStore.getStudioByStripeCustomerId(customerId);
3070
+ if (studio) {
3071
+ await this._authStore.updateSubscriptionPeriod({
3072
+ studioId: studio.id,
3073
+ subscriptionStatus: status,
3074
+ subscriptionId: sub.id,
3075
+ stripeSubscriptionId,
3076
+ stripeCustomerId: customerId,
3077
+ currentPeriodEndMs: periodEndMs,
775
3078
  currentPeriodStartMs: periodStartMs,
776
3079
  invoice: authInvoice,
777
3080
  });
3081
+ const creditResult = await this._internalTransactionPurchaseCreditsStripe(sub, invoice, {
3082
+ studioId: studio.id,
3083
+ });
3084
+ if (isFailure(creditResult)) {
3085
+ return genericResult(creditResult);
3086
+ }
778
3087
  }
779
3088
  else {
780
- console.log(`[SubscriptionController] [handleStripeWebhook] No user found for customer ID (${customerId}).`);
781
- const studio = await this._recordsStore.getStudioByStripeCustomerId(customerId);
782
- if (studio) {
783
- await this._authStore.updateSubscriptionPeriod({
784
- studioId: studio.id,
785
- subscriptionStatus: status,
786
- subscriptionId: sub.id,
787
- stripeSubscriptionId,
788
- stripeCustomerId: customerId,
789
- currentPeriodEndMs: periodEndMs,
790
- currentPeriodStartMs: periodStartMs,
791
- invoice: authInvoice,
792
- });
793
- }
794
- else {
795
- console.log(`[SubscriptionController] [handleStripeWebhook] No studio found for customer ID (${customerId}).`);
796
- }
3089
+ console.log(`[SubscriptionController] [handleStripeWebhook] No studio found for customer ID (${customerId}).`);
797
3090
  }
798
- function findMatchingSubscription(lineItems) {
799
- let item;
800
- let sub;
801
- items_loop: for (let i of lineItems) {
802
- for (let s of config.subscriptions) {
803
- if (s.eligibleProducts &&
804
- s.eligibleProducts.some((p) => p === i.price.product)) {
805
- sub = s;
806
- item = i;
807
- break items_loop;
808
- }
3091
+ }
3092
+ function findMatchingSubscription(lineItems) {
3093
+ let item;
3094
+ let sub;
3095
+ items_loop: for (let i of lineItems) {
3096
+ for (let s of config.subscriptions) {
3097
+ if ((s.eligibleProducts &&
3098
+ s.eligibleProducts.some((p) => p === i.price.product)) ||
3099
+ s.product === i.price.product) {
3100
+ sub = s;
3101
+ item = i;
3102
+ break items_loop;
809
3103
  }
810
3104
  }
811
- return { item, sub };
812
3105
  }
3106
+ return { item, sub };
813
3107
  }
814
- return {
815
- success: true,
816
- };
817
3108
  }
818
- catch (err) {
819
- const span = trace.getActiveSpan();
820
- span.recordException(err);
821
- console.error('[SubscriptionController] An error occurred while handling a stripe webhook:', err);
822
- return {
823
- success: false,
3109
+ else {
3110
+ console.log(`[SubscriptionController] [handleStripeWebhook] No subscription ID found in invoice.`);
3111
+ const authInvoice = await this._authStore.getInvoiceByStripeId(invoice.id);
3112
+ if (!authInvoice) {
3113
+ console.log(`[SubscriptionController] [handleStripeWebhook] No invoice found for stripe ID (${invoice.id}).`);
3114
+ return {
3115
+ success: true,
3116
+ };
3117
+ }
3118
+ await this._authStore.saveInvoice({
3119
+ ...authInvoice,
3120
+ currency: invoice.currency,
3121
+ description: invoice.description,
3122
+ paid: invoice.paid,
3123
+ status: invoice.status,
3124
+ tax: invoice.tax,
3125
+ total: invoice.total,
3126
+ subtotal: invoice.subtotal,
3127
+ });
3128
+ }
3129
+ return {
3130
+ success: true,
3131
+ };
3132
+ }
3133
+ async _internalTransactionPurchaseCreditsStripe(sub, invoice, accountFilter) {
3134
+ const creditGrant = sub.creditGrant ?? 0;
3135
+ if (creditGrant === 0 || !this._financialController) {
3136
+ return success();
3137
+ }
3138
+ const account = await this._financialController.getOrCreateFinancialAccount({
3139
+ ...accountFilter,
3140
+ ledger: LEDGERS.credits,
3141
+ });
3142
+ if (isFailure(account)) {
3143
+ logError(account.error, `[SubscriptionController] [_internalTransactionPurchaseCreditsStripe invoice: ${invoice.id}] Unable to get or create credit account!`);
3144
+ return failure({
824
3145
  errorCode: 'server_error',
825
3146
  errorMessage: 'A server error occurred.',
3147
+ });
3148
+ }
3149
+ const accountId = account.value.account.id;
3150
+ let creditAmount;
3151
+ if (creditGrant === 'match-invoice') {
3152
+ const converted = convertBetweenLedgers(LEDGERS.usd, LEDGERS.credits, BigInt(invoice.total));
3153
+ if (converted.remainder > 0n) {
3154
+ console.warn(`[SubscriptionController] [_internalTransactionPurchaseCreditsStripe invoice: ${invoice.id} account: ${accountId}] Rounding down remainder when converting invoice amount to credits.`);
3155
+ }
3156
+ creditAmount = converted.value;
3157
+ }
3158
+ else {
3159
+ creditAmount = BigInt(creditGrant);
3160
+ }
3161
+ if (creditAmount > 0) {
3162
+ const transactionResult = await this._financialController.internalTransaction({
3163
+ transfers: [
3164
+ {
3165
+ amount: invoice.total,
3166
+ code: TransferCodes.purchase_credits,
3167
+ debitAccountId: ACCOUNT_IDS.assets_stripe,
3168
+ creditAccountId: ACCOUNT_IDS.liquidity_usd,
3169
+ currency: 'usd',
3170
+ },
3171
+ {
3172
+ amount: creditAmount,
3173
+ code: TransferCodes.purchase_credits,
3174
+ debitAccountId: ACCOUNT_IDS.liquidity_credits,
3175
+ creditAccountId: accountId,
3176
+ currency: 'credits',
3177
+ },
3178
+ ],
3179
+ });
3180
+ if (isFailure(transactionResult)) {
3181
+ logError(transactionResult.error, `[SubscriptionController] [_internalTransactionPurchaseCreditsStripe invoice: ${invoice.id} account: ${accountId}] Unable to record credit grant for invoice!`);
3182
+ return failure({
3183
+ errorCode: 'server_error',
3184
+ errorMessage: 'Unable to record credit grant for invoice.',
3185
+ });
3186
+ }
3187
+ console.log(`[SubscriptionController] [_internalTransactionPurchaseCreditsStripe invoice: ${invoice.id} account: ${accountId}] Granted ${creditAmount} credits for invoice (${invoice.id}).`);
3188
+ }
3189
+ else {
3190
+ console.warn(`[SubscriptionController] [_internalTransactionPurchaseCreditsStripe invoice: ${invoice.id} account: ${accountId}] No credits granted for invoice (${invoice.id}).`);
3191
+ }
3192
+ return success();
3193
+ }
3194
+ async _handleStripeSubscriptionEvent(config, event, subscription) {
3195
+ const items = subscription.items.data;
3196
+ let item;
3197
+ let sub;
3198
+ items_loop: for (let i of items) {
3199
+ for (let s of config.subscriptions) {
3200
+ if (s.eligibleProducts &&
3201
+ s.eligibleProducts.some((p) => p === i.price.product)) {
3202
+ sub = s;
3203
+ item = i;
3204
+ break items_loop;
3205
+ }
3206
+ }
3207
+ }
3208
+ if (!item || !sub) {
3209
+ console.log(`[SubscriptionController] [handleStripeWebhook] No item in the subscription matches an eligible product in the config.`);
3210
+ return {
3211
+ success: true,
826
3212
  };
827
3213
  }
3214
+ console.log(`[SubscriptionController] [handleStripeWebhook] Subscription (${sub.id}) found!`);
3215
+ const status = subscription.status;
3216
+ const active = isActiveSubscription(status);
3217
+ const tier = sub.tier ?? 'beta';
3218
+ const customerId = subscription.customer;
3219
+ const stripeSubscriptionId = subscription.id;
3220
+ const periodStartMs = subscription.current_period_start * 1000;
3221
+ const periodEndMs = subscription.current_period_end * 1000;
3222
+ console.log(`[SubscriptionController] [handleStripeWebhook] Customer ID: ${customerId}. Subscription status: ${status}. Tier: ${tier}. Is Active: ${active}.`);
3223
+ let user = await this._authStore.findUserByStripeCustomerId(customerId);
3224
+ let studio;
3225
+ if (user) {
3226
+ await this._authStore.updateSubscriptionInfo({
3227
+ userId: user.id,
3228
+ subscriptionStatus: status,
3229
+ subscriptionId: sub.id,
3230
+ stripeSubscriptionId,
3231
+ stripeCustomerId: customerId,
3232
+ currentPeriodEndMs: periodEndMs,
3233
+ currentPeriodStartMs: periodStartMs,
3234
+ });
3235
+ }
3236
+ else {
3237
+ console.log(`[SubscriptionController] [handleStripeWebhook] No user found for Customer ID (${customerId})`);
3238
+ studio = await this._recordsStore.getStudioByStripeCustomerId(customerId);
3239
+ if (studio) {
3240
+ await this._authStore.updateSubscriptionInfo({
3241
+ studioId: studio.id,
3242
+ subscriptionStatus: status,
3243
+ subscriptionId: sub.id,
3244
+ stripeSubscriptionId,
3245
+ stripeCustomerId: customerId,
3246
+ currentPeriodEndMs: periodEndMs,
3247
+ currentPeriodStartMs: periodStartMs,
3248
+ });
3249
+ }
3250
+ else {
3251
+ console.log(`[SubscriptionController] [handleStripeWebhook] No studio found for Customer ID (${customerId})`);
3252
+ }
3253
+ }
3254
+ return {
3255
+ success: true,
3256
+ };
828
3257
  }
829
3258
  }
830
3259
  __decorate([
831
3260
  traced(TRACE_NAME)
832
3261
  ], SubscriptionController.prototype, "getSubscriptionStatus", null);
3262
+ __decorate([
3263
+ traced(TRACE_NAME)
3264
+ ], SubscriptionController.prototype, "getBalances", null);
3265
+ __decorate([
3266
+ traced(TRACE_NAME)
3267
+ ], SubscriptionController.prototype, "listAccountTransfers", null);
833
3268
  __decorate([
834
3269
  traced(TRACE_NAME)
835
3270
  ], SubscriptionController.prototype, "updateSubscription", null);
836
3271
  __decorate([
837
3272
  traced(TRACE_NAME)
838
3273
  ], SubscriptionController.prototype, "createManageSubscriptionLink", null);
3274
+ __decorate([
3275
+ traced(TRACE_NAME)
3276
+ ], SubscriptionController.prototype, "createManageStoreAccountLink", null);
3277
+ __decorate([
3278
+ traced(TRACE_NAME)
3279
+ ], SubscriptionController.prototype, "createManageXpAccountLink", null);
3280
+ __decorate([
3281
+ traced(TRACE_NAME)
3282
+ ], SubscriptionController.prototype, "createStripeLoginLink", null);
3283
+ __decorate([
3284
+ traced(TRACE_NAME)
3285
+ ], SubscriptionController.prototype, "createStripeAccountSession", null);
839
3286
  __decorate([
840
3287
  traced(TRACE_NAME)
841
3288
  ], SubscriptionController.prototype, "_createCheckoutSession", null);
3289
+ __decorate([
3290
+ traced(TRACE_NAME)
3291
+ ], SubscriptionController.prototype, "createPurchaseItemLink", null);
3292
+ __decorate([
3293
+ traced(TRACE_NAME)
3294
+ ], SubscriptionController.prototype, "_getContractPurchaseDetails", null);
3295
+ __decorate([
3296
+ traced(TRACE_NAME)
3297
+ ], SubscriptionController.prototype, "purchaseContract", null);
3298
+ __decorate([
3299
+ traced(TRACE_NAME)
3300
+ ], SubscriptionController.prototype, "cancelContract", null);
3301
+ __decorate([
3302
+ traced(TRACE_NAME)
3303
+ ], SubscriptionController.prototype, "invoiceContract", null);
3304
+ __decorate([
3305
+ traced(TRACE_NAME)
3306
+ ], SubscriptionController.prototype, "cancelInvoice", null);
3307
+ __decorate([
3308
+ traced(TRACE_NAME)
3309
+ ], SubscriptionController.prototype, "listContractInvoices", null);
3310
+ __decorate([
3311
+ traced(TRACE_NAME)
3312
+ ], SubscriptionController.prototype, "payContractInvoice", null);
3313
+ __decorate([
3314
+ traced(TRACE_NAME)
3315
+ ], SubscriptionController.prototype, "payoutAccount", null);
3316
+ __decorate([
3317
+ traced(TRACE_NAME)
3318
+ ], SubscriptionController.prototype, "_payoutToStripe", null);
3319
+ __decorate([
3320
+ traced(TRACE_NAME)
3321
+ ], SubscriptionController.prototype, "_payoutToCash", null);
3322
+ __decorate([
3323
+ traced(TRACE_NAME)
3324
+ ], SubscriptionController.prototype, "fulfillCheckoutSession", null);
3325
+ __decorate([
3326
+ traced(TRACE_NAME)
3327
+ ], SubscriptionController.prototype, "claimActivationKey", null);
842
3328
  __decorate([
843
3329
  traced(TRACE_NAME)
844
3330
  ], SubscriptionController.prototype, "handleStripeWebhook", null);
3331
+ __decorate([
3332
+ traced(TRACE_NAME)
3333
+ ], SubscriptionController.prototype, "_handleStripeCheckoutSessionEvent", null);
3334
+ __decorate([
3335
+ traced(TRACE_NAME)
3336
+ ], SubscriptionController.prototype, "_handleStripeAccountUpdatedEvent", null);
3337
+ __decorate([
3338
+ traced(TRACE_NAME)
3339
+ ], SubscriptionController.prototype, "_handleStudioStripeAccountUpdatedEvent", null);
3340
+ __decorate([
3341
+ traced(TRACE_NAME)
3342
+ ], SubscriptionController.prototype, "_handleUserStripeAccountUpdatedEvent", null);
3343
+ __decorate([
3344
+ traced(TRACE_NAME)
3345
+ ], SubscriptionController.prototype, "_handleStripeInvoicePaidEvent", null);
3346
+ __decorate([
3347
+ traced(TRACE_NAME)
3348
+ ], SubscriptionController.prototype, "_handleStripeSubscriptionEvent", null);
3349
+ /**
3350
+ * Gets the account status for the given stripe account.
3351
+ * @param account The account that the status should be retrieved for.
3352
+ */
3353
+ export function getAccountStatus(account) {
3354
+ const disabledReason = account?.requirements?.disabled_reason;
3355
+ if (disabledReason === 'under_review' ||
3356
+ disabledReason === 'requirements.pending_verification') {
3357
+ return 'pending';
3358
+ }
3359
+ else if (disabledReason === 'rejected.fraud' ||
3360
+ disabledReason === 'rejected.incomplete_verification' ||
3361
+ disabledReason === 'rejected.listed' ||
3362
+ disabledReason === 'rejected.other' ||
3363
+ disabledReason === 'rejected.terms_of_service') {
3364
+ return 'rejected';
3365
+ }
3366
+ else if (disabledReason) {
3367
+ return 'disabled';
3368
+ }
3369
+ else if (account.charges_enabled) {
3370
+ return 'active';
3371
+ }
3372
+ return 'pending';
3373
+ }
3374
+ /**
3375
+ * Gets the requirements status for the given stripe account.
3376
+ * @param account The account.
3377
+ */
3378
+ export function getAccountRequirementsStatus(account) {
3379
+ const requirements = account?.requirements;
3380
+ if (!requirements) {
3381
+ return 'incomplete';
3382
+ }
3383
+ if (requirements.currently_due?.length > 0 ||
3384
+ requirements.past_due?.length > 0) {
3385
+ return 'incomplete';
3386
+ }
3387
+ return 'complete';
3388
+ }
845
3389
  function returnRoute(basePath, user, studio) {
846
3390
  if (user) {
847
3391
  return basePath;
@@ -853,4 +3397,267 @@ function returnRoute(basePath, user, studio) {
853
3397
  function studiosRoute(basePath, studioId, studioName) {
854
3398
  return new URL(`/studios/${encodeURIComponent(studioId)}/${encodeURIComponent(studioName)}`, basePath).href;
855
3399
  }
3400
+ function fulfillmentRoute(basePath, sessionId) {
3401
+ return new URL(`/store/fulfillment/${sessionId}`, basePath).href;
3402
+ }
3403
+ function activationRoute(basePath, key) {
3404
+ const url = new URL(`/store/activate`, basePath);
3405
+ url.searchParams.set('key', key);
3406
+ return url.href;
3407
+ }
3408
+ /**
3409
+ * Formats a V1 access key.
3410
+ * @param itemId The ID of the purchased item.
3411
+ * @param secret The secret that should be used to access the purchased item.
3412
+ */
3413
+ export function formatV1ActivationKey(itemId, secret) {
3414
+ return `vAK1.${toBase64String(itemId)}.${toBase64String(secret)}`;
3415
+ }
3416
+ /**
3417
+ * Parses the given access key.
3418
+ * Returns null if the access key is invalid.
3419
+ * @param key The key to parse.
3420
+ */
3421
+ export function parseActivationKey(key) {
3422
+ if (!key) {
3423
+ return null;
3424
+ }
3425
+ if (!key.startsWith('vAK1.')) {
3426
+ return null;
3427
+ }
3428
+ const withoutVersion = key.slice('vAK1.'.length);
3429
+ let periodAfterId = withoutVersion.indexOf('.');
3430
+ if (periodAfterId < 0) {
3431
+ return null;
3432
+ }
3433
+ const idBase64 = withoutVersion.slice(0, periodAfterId);
3434
+ const secretBase64 = withoutVersion.slice(periodAfterId + 1);
3435
+ if (idBase64.length <= 0 || secretBase64.length <= 0) {
3436
+ return null;
3437
+ }
3438
+ try {
3439
+ const name = fromBase64String(idBase64);
3440
+ const secret = fromBase64String(secretBase64);
3441
+ return [name, secret];
3442
+ }
3443
+ catch (err) {
3444
+ return null;
3445
+ }
3446
+ }
3447
+ const TRANSFER_CODE_ACTIONS = new Map([
3448
+ [TransferCodes.admin_credit, 'Admin credit'],
3449
+ [TransferCodes.admin_debit, 'Admin debit'],
3450
+ [TransferCodes.account_closing, 'Account closing'],
3451
+ [TransferCodes.purchase_credits, 'Purchase credits'],
3452
+ [TransferCodes.contract_payment, 'Contract payment'],
3453
+ [TransferCodes.contract_refund, 'Contract refund'],
3454
+ [TransferCodes.exchange, 'Exchange'],
3455
+ [TransferCodes.invoice_payment, 'Invoice payment'],
3456
+ [TransferCodes.item_payment, 'Item payment'],
3457
+ [TransferCodes.purchase_credits, 'Credit purchase'],
3458
+ [TransferCodes.reverse_transfer, 'Transfer reversal'],
3459
+ [TransferCodes.store_platform_fee, 'Platform fee'],
3460
+ [TransferCodes.xp_platform_fee, 'Platform fee'],
3461
+ [TransferCodes.user_payout, 'Payout'],
3462
+ [TransferCodes.control, 'Controlling transfer'],
3463
+ [TransferCodes.exchange, 'Exchange'],
3464
+ ]);
3465
+ /**
3466
+ * Generates a human readable note for the given transfer.
3467
+ * @param transfer The transfer that should be characterized.
3468
+ */
3469
+ export function charactarizeTransfer(transfer) {
3470
+ let action;
3471
+ let source;
3472
+ let destination;
3473
+ action = TRANSFER_CODE_ACTIONS.get(transfer.code);
3474
+ if (!action) {
3475
+ return null;
3476
+ }
3477
+ if (transfer.code === TransferCodes.admin_credit) {
3478
+ source = ACCOUNT_NAMES.get(transfer.debit_account_id);
3479
+ destination = ACCOUNT_NAMES.get(transfer.credit_account_id);
3480
+ }
3481
+ else if (transfer.code === TransferCodes.admin_debit) {
3482
+ destination = ACCOUNT_NAMES.get(transfer.credit_account_id);
3483
+ source = ACCOUNT_NAMES.get(transfer.debit_account_id);
3484
+ }
3485
+ else if (transfer.code === TransferCodes.user_payout) {
3486
+ destination = ACCOUNT_NAMES.get(transfer.credit_account_id);
3487
+ }
3488
+ else if (transfer.code === TransferCodes.exchange) {
3489
+ if (transfer.debit_account_id === ACCOUNT_IDS.liquidity_usd ||
3490
+ transfer.credit_account_id === ACCOUNT_IDS.liquidity_credits) {
3491
+ // Exchanging from Credits to USD
3492
+ source = 'Credits';
3493
+ destination = 'USD';
3494
+ }
3495
+ else if (transfer.debit_account_id === ACCOUNT_IDS.liquidity_credits ||
3496
+ transfer.credit_account_id === ACCOUNT_IDS.liquidity_usd) {
3497
+ // Exchanging from USD to Credits
3498
+ source = 'USD';
3499
+ destination = 'Credits';
3500
+ }
3501
+ }
3502
+ else if (transfer.code === TransferCodes.purchase_credits) {
3503
+ source = ACCOUNT_NAMES.get(transfer.debit_account_id);
3504
+ }
3505
+ else if (transfer.code === TransferCodes.account_closing) {
3506
+ source = ACCOUNT_NAMES.get(transfer.debit_account_id);
3507
+ destination = ACCOUNT_NAMES.get(transfer.credit_account_id);
3508
+ }
3509
+ else if (transfer.code === TransferCodes.contract_payment ||
3510
+ transfer.code === TransferCodes.invoice_payment ||
3511
+ transfer.code === TransferCodes.item_payment) {
3512
+ source = ACCOUNT_NAMES.get(transfer.debit_account_id);
3513
+ destination = ACCOUNT_NAMES.get(transfer.credit_account_id);
3514
+ }
3515
+ else if (transfer.code === TransferCodes.contract_refund) {
3516
+ destination = ACCOUNT_NAMES.get(transfer.credit_account_id);
3517
+ }
3518
+ if (source && destination) {
3519
+ return `${action} from ${source} to ${destination}`;
3520
+ }
3521
+ else if (source) {
3522
+ return `${action} from ${source}`;
3523
+ }
3524
+ else if (destination) {
3525
+ return `${action} to ${destination}`;
3526
+ }
3527
+ else {
3528
+ return action;
3529
+ }
3530
+ }
3531
+ class TransactionBuilder {
3532
+ constructor() {
3533
+ this.transfers = [];
3534
+ this.lineItems = [];
3535
+ this._pending = false;
3536
+ this._timeoutSeconds = 60 * 60; // 1 hour
3537
+ }
3538
+ usePendingTransfers(timeoutSeconds) {
3539
+ this._pending = true;
3540
+ this._timeoutSeconds = timeoutSeconds;
3541
+ return this;
3542
+ }
3543
+ disablePendingTransfers() {
3544
+ this._pending = false;
3545
+ return this;
3546
+ }
3547
+ addTransfer(tansfer) {
3548
+ this.transfers.push(tansfer);
3549
+ return this;
3550
+ }
3551
+ addItem(item) {
3552
+ if (item.pending ?? this._pending) {
3553
+ this.transfers.push({
3554
+ transferId: item.transferId,
3555
+ amount: item.amount,
3556
+ currency: item.currency,
3557
+ code: item.code,
3558
+ creditAccountId: item.creditAccountId,
3559
+ debitAccountId: item.debitAccountId,
3560
+ pending: true,
3561
+ timeoutSeconds: item.timeoutSeconds ?? this._timeoutSeconds,
3562
+ });
3563
+ }
3564
+ else {
3565
+ this.transfers.push({
3566
+ transferId: item.transferId,
3567
+ amount: item.amount,
3568
+ currency: item.currency,
3569
+ code: item.code,
3570
+ creditAccountId: item.creditAccountId,
3571
+ debitAccountId: item.debitAccountId,
3572
+ });
3573
+ }
3574
+ this.lineItems.push({
3575
+ price_data: {
3576
+ currency: item.currency,
3577
+ unit_amount: item.amount,
3578
+ product_data: {
3579
+ name: item.name,
3580
+ description: item.description,
3581
+ images: [],
3582
+ metadata: item.metadata,
3583
+ },
3584
+ },
3585
+ quantity: 1,
3586
+ });
3587
+ return this;
3588
+ }
3589
+ // addTransfer(transfer: InternalTransfer): this {
3590
+ // this.transfers.push(transfer);
3591
+ // return this;
3592
+ // }
3593
+ // addExchange(info: {
3594
+ // debitAccountId: bigint,
3595
+ // debitCurrency: string,
3596
+ // credits: {
3597
+ // accountId: bigint,
3598
+ // amount: number,
3599
+ // }[],
3600
+ // creditCurrency: string,
3601
+ // pending?: boolean,
3602
+ // }): this {
3603
+ // const total = info.credits.reduce((acc, credit) => acc + credit.amount, 0);
3604
+ // this.transfers.push({
3605
+ // amount: total,
3606
+ // code: TransferCodes.exchange,
3607
+ // debitAccountId: info.debitAccountId,
3608
+ // currency: info.debitCurrency,
3609
+ // creditAccountId: getLiquidityAccount(info.debitCurrency),
3610
+ // pending: info.pending ?? this._pending,
3611
+ // });
3612
+ // for (let credit of info.credits) {
3613
+ // this.transfers.push({
3614
+ // amount: credit.amount,
3615
+ // code: TransferCodes.exchange,
3616
+ // debitAccountId: getLiquidityAccount(info.creditCurrency),
3617
+ // creditAccountId: credit.accountId,
3618
+ // currency: info.creditCurrency,
3619
+ // pending: info.pending ?? this._pending,
3620
+ // });
3621
+ // }
3622
+ // return this;
3623
+ // }
3624
+ addContract(info) {
3625
+ return this.addItem({
3626
+ amount: info.item.initialValue,
3627
+ code: TransferCodes.contract_payment,
3628
+ creditAccountId: info.contractAccountId,
3629
+ debitAccountId: info.debitAccountId,
3630
+ currency: CurrencyCodes.usd,
3631
+ pending: this._pending,
3632
+ name: 'Contract',
3633
+ description: info.item.description,
3634
+ metadata: {
3635
+ resourceKind: 'contract',
3636
+ recordName: info.recordName,
3637
+ address: info.item.address,
3638
+ },
3639
+ });
3640
+ }
3641
+ addContractApplicationFee(info) {
3642
+ if (info.fee > 0) {
3643
+ this.addItem({
3644
+ amount: info.fee,
3645
+ code: TransferCodes.xp_platform_fee,
3646
+ debitAccountId: info.debitAccountId,
3647
+ creditAccountId: ACCOUNT_IDS.revenue_xp_platform_fees,
3648
+ currency: CurrencyCodes.usd,
3649
+ pending: this._pending,
3650
+ name: 'Application Fee',
3651
+ // description: 'XP Platform Fee',
3652
+ metadata: {
3653
+ fee: true,
3654
+ resourceKind: 'contract',
3655
+ recordName: info.recordName,
3656
+ address: info.item.address,
3657
+ },
3658
+ });
3659
+ }
3660
+ return this;
3661
+ }
3662
+ }
856
3663
  //# sourceMappingURL=SubscriptionController.js.map