@casual-simulation/aux-records 3.8.1 → 3.10.2

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