@wipperoz/wipperoz-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +35 -0
  2. package/bin/run-clean.js +53 -0
  3. package/dist/account/index.d.ts +2 -0
  4. package/dist/account/index.d.ts.map +1 -0
  5. package/dist/account/index.js +5 -0
  6. package/dist/account/index.js.map +1 -0
  7. package/dist/account/interfaces/accountRecord.interface.d.ts +9 -0
  8. package/dist/account/interfaces/accountRecord.interface.d.ts.map +1 -0
  9. package/dist/account/interfaces/accountRecord.interface.js +3 -0
  10. package/dist/account/interfaces/accountRecord.interface.js.map +1 -0
  11. package/dist/account/services/account.service.d.ts +9 -0
  12. package/dist/account/services/account.service.d.ts.map +1 -0
  13. package/dist/account/services/account.service.js +30 -0
  14. package/dist/account/services/account.service.js.map +1 -0
  15. package/dist/billing/constants.d.ts +39 -0
  16. package/dist/billing/constants.d.ts.map +1 -0
  17. package/dist/billing/constants.js +57 -0
  18. package/dist/billing/constants.js.map +1 -0
  19. package/dist/billing/enums/accountType.enum.d.ts +2 -0
  20. package/dist/billing/enums/accountType.enum.d.ts.map +1 -0
  21. package/dist/billing/enums/accountType.enum.js +6 -0
  22. package/dist/billing/enums/accountType.enum.js.map +1 -0
  23. package/dist/billing/enums/creditReason.enum.d.ts +7 -0
  24. package/dist/billing/enums/creditReason.enum.d.ts.map +1 -0
  25. package/dist/billing/enums/creditReason.enum.js +11 -0
  26. package/dist/billing/enums/creditReason.enum.js.map +1 -0
  27. package/dist/billing/enums/displayCurrency.enum.d.ts +2 -0
  28. package/dist/billing/enums/displayCurrency.enum.d.ts.map +1 -0
  29. package/dist/billing/enums/displayCurrency.enum.js +6 -0
  30. package/dist/billing/enums/displayCurrency.enum.js.map +1 -0
  31. package/dist/billing/enums/index.d.ts +8 -0
  32. package/dist/billing/enums/index.d.ts.map +1 -0
  33. package/dist/billing/enums/index.js +11 -0
  34. package/dist/billing/enums/index.js.map +1 -0
  35. package/dist/billing/enums/reasoningEffort.enum.d.ts +8 -0
  36. package/dist/billing/enums/reasoningEffort.enum.d.ts.map +1 -0
  37. package/dist/billing/enums/reasoningEffort.enum.js +12 -0
  38. package/dist/billing/enums/reasoningEffort.enum.js.map +1 -0
  39. package/dist/billing/enums/stripePaymentStatus.enum.d.ts +7 -0
  40. package/dist/billing/enums/stripePaymentStatus.enum.d.ts.map +1 -0
  41. package/dist/billing/enums/stripePaymentStatus.enum.js +11 -0
  42. package/dist/billing/enums/stripePaymentStatus.enum.js.map +1 -0
  43. package/dist/billing/enums/stripeRegion.enum.d.ts +2 -0
  44. package/dist/billing/enums/stripeRegion.enum.d.ts.map +1 -0
  45. package/dist/billing/enums/stripeRegion.enum.js +6 -0
  46. package/dist/billing/enums/stripeRegion.enum.js.map +1 -0
  47. package/dist/billing/enums/transactionType.enum.d.ts +8 -0
  48. package/dist/billing/enums/transactionType.enum.d.ts.map +1 -0
  49. package/dist/billing/enums/transactionType.enum.js +12 -0
  50. package/dist/billing/enums/transactionType.enum.js.map +1 -0
  51. package/dist/billing/index.d.ts +3 -0
  52. package/dist/billing/index.d.ts.map +1 -0
  53. package/dist/billing/index.js +6 -0
  54. package/dist/billing/index.js.map +1 -0
  55. package/dist/billing/interfaces/addCreditsResult.interface.d.ts +7 -0
  56. package/dist/billing/interfaces/addCreditsResult.interface.d.ts.map +1 -0
  57. package/dist/billing/interfaces/addCreditsResult.interface.js +3 -0
  58. package/dist/billing/interfaces/addCreditsResult.interface.js.map +1 -0
  59. package/dist/billing/interfaces/aiRequestBillingResponse.interface.d.ts +30 -0
  60. package/dist/billing/interfaces/aiRequestBillingResponse.interface.d.ts.map +1 -0
  61. package/dist/billing/interfaces/aiRequestBillingResponse.interface.js +3 -0
  62. package/dist/billing/interfaces/aiRequestBillingResponse.interface.js.map +1 -0
  63. package/dist/billing/interfaces/autoTopUpCheckResult.interface.d.ts +6 -0
  64. package/dist/billing/interfaces/autoTopUpCheckResult.interface.d.ts.map +1 -0
  65. package/dist/billing/interfaces/autoTopUpCheckResult.interface.js +3 -0
  66. package/dist/billing/interfaces/autoTopUpCheckResult.interface.js.map +1 -0
  67. package/dist/billing/interfaces/autoTopUpMessage.interface.d.ts +15 -0
  68. package/dist/billing/interfaces/autoTopUpMessage.interface.d.ts.map +1 -0
  69. package/dist/billing/interfaces/autoTopUpMessage.interface.js +3 -0
  70. package/dist/billing/interfaces/autoTopUpMessage.interface.js.map +1 -0
  71. package/dist/billing/interfaces/balanceResponse.interface.d.ts +9 -0
  72. package/dist/billing/interfaces/balanceResponse.interface.d.ts.map +1 -0
  73. package/dist/billing/interfaces/balanceResponse.interface.js +3 -0
  74. package/dist/billing/interfaces/balanceResponse.interface.js.map +1 -0
  75. package/dist/billing/interfaces/billing.interface.d.ts +2 -0
  76. package/dist/billing/interfaces/billing.interface.d.ts.map +1 -0
  77. package/dist/billing/interfaces/billing.interface.js +3 -0
  78. package/dist/billing/interfaces/billing.interface.js.map +1 -0
  79. package/dist/billing/interfaces/canAffordResult.interface.d.ts +6 -0
  80. package/dist/billing/interfaces/canAffordResult.interface.d.ts.map +1 -0
  81. package/dist/billing/interfaces/canAffordResult.interface.js +3 -0
  82. package/dist/billing/interfaces/canAffordResult.interface.js.map +1 -0
  83. package/dist/billing/interfaces/chargeResult.interface.d.ts +7 -0
  84. package/dist/billing/interfaces/chargeResult.interface.d.ts.map +1 -0
  85. package/dist/billing/interfaces/chargeResult.interface.js +3 -0
  86. package/dist/billing/interfaces/chargeResult.interface.js.map +1 -0
  87. package/dist/billing/interfaces/costBreakdown.interface.d.ts +9 -0
  88. package/dist/billing/interfaces/costBreakdown.interface.d.ts.map +1 -0
  89. package/dist/billing/interfaces/costBreakdown.interface.js +3 -0
  90. package/dist/billing/interfaces/costBreakdown.interface.js.map +1 -0
  91. package/dist/billing/interfaces/creditTransaction.interface.d.ts +28 -0
  92. package/dist/billing/interfaces/creditTransaction.interface.d.ts.map +1 -0
  93. package/dist/billing/interfaces/creditTransaction.interface.js +3 -0
  94. package/dist/billing/interfaces/creditTransaction.interface.js.map +1 -0
  95. package/dist/billing/interfaces/depositMetadata.interface.d.ts +10 -0
  96. package/dist/billing/interfaces/depositMetadata.interface.d.ts.map +1 -0
  97. package/dist/billing/interfaces/depositMetadata.interface.js +3 -0
  98. package/dist/billing/interfaces/depositMetadata.interface.js.map +1 -0
  99. package/dist/billing/interfaces/iAccountCreditManager.interface.d.ts +43 -0
  100. package/dist/billing/interfaces/iAccountCreditManager.interface.d.ts.map +1 -0
  101. package/dist/billing/interfaces/iAccountCreditManager.interface.js +3 -0
  102. package/dist/billing/interfaces/iAccountCreditManager.interface.js.map +1 -0
  103. package/dist/billing/interfaces/iCostCalculator.interface.d.ts +7 -0
  104. package/dist/billing/interfaces/iCostCalculator.interface.d.ts.map +1 -0
  105. package/dist/billing/interfaces/iCostCalculator.interface.js +3 -0
  106. package/dist/billing/interfaces/iCostCalculator.interface.js.map +1 -0
  107. package/dist/billing/interfaces/index.d.ts +16 -0
  108. package/dist/billing/interfaces/index.d.ts.map +1 -0
  109. package/dist/billing/interfaces/index.js +19 -0
  110. package/dist/billing/interfaces/index.js.map +1 -0
  111. package/dist/billing/interfaces/initialCreditResult.interface.d.ts +7 -0
  112. package/dist/billing/interfaces/initialCreditResult.interface.d.ts.map +1 -0
  113. package/dist/billing/interfaces/initialCreditResult.interface.js +3 -0
  114. package/dist/billing/interfaces/initialCreditResult.interface.js.map +1 -0
  115. package/dist/billing/interfaces/requestCostParams.interface.d.ts +7 -0
  116. package/dist/billing/interfaces/requestCostParams.interface.d.ts.map +1 -0
  117. package/dist/billing/interfaces/requestCostParams.interface.js +3 -0
  118. package/dist/billing/interfaces/requestCostParams.interface.js.map +1 -0
  119. package/dist/billing/interfaces/stripePayment.interface.d.ts +29 -0
  120. package/dist/billing/interfaces/stripePayment.interface.d.ts.map +1 -0
  121. package/dist/billing/interfaces/stripePayment.interface.js +3 -0
  122. package/dist/billing/interfaces/stripePayment.interface.js.map +1 -0
  123. package/dist/billing/interfaces/transactionHistory.interface.d.ts +16 -0
  124. package/dist/billing/interfaces/transactionHistory.interface.d.ts.map +1 -0
  125. package/dist/billing/interfaces/transactionHistory.interface.js +3 -0
  126. package/dist/billing/interfaces/transactionHistory.interface.js.map +1 -0
  127. package/dist/billing/interfaces/triggeredBy.interface.d.ts +1 -0
  128. package/dist/billing/interfaces/triggeredBy.interface.d.ts.map +1 -0
  129. package/dist/billing/interfaces/triggeredBy.interface.js +2 -0
  130. package/dist/billing/interfaces/triggeredBy.interface.js.map +1 -0
  131. package/dist/billing/interfaces/usageMetadata.interface.d.ts +12 -0
  132. package/dist/billing/interfaces/usageMetadata.interface.d.ts.map +1 -0
  133. package/dist/billing/interfaces/usageMetadata.interface.js +3 -0
  134. package/dist/billing/interfaces/usageMetadata.interface.js.map +1 -0
  135. package/dist/billing/services/accountCreditManager.service.d.ts +81 -0
  136. package/dist/billing/services/accountCreditManager.service.d.ts.map +1 -0
  137. package/dist/billing/services/accountCreditManager.service.js +658 -0
  138. package/dist/billing/services/accountCreditManager.service.js.map +1 -0
  139. package/dist/billing/services/costCalculator.service.d.ts +36 -0
  140. package/dist/billing/services/costCalculator.service.d.ts.map +1 -0
  141. package/dist/billing/services/costCalculator.service.js +100 -0
  142. package/dist/billing/services/costCalculator.service.js.map +1 -0
  143. package/dist/billing/services/tokenFormatter.d.ts +10 -0
  144. package/dist/billing/services/tokenFormatter.d.ts.map +1 -0
  145. package/dist/billing/services/tokenFormatter.js +27 -0
  146. package/dist/billing/services/tokenFormatter.js.map +1 -0
  147. package/dist/billing/utils.d.ts +8 -0
  148. package/dist/billing/utils.d.ts.map +1 -0
  149. package/dist/billing/utils.js +33 -0
  150. package/dist/billing/utils.js.map +1 -0
  151. package/dist/index.d.ts +3 -0
  152. package/dist/index.d.ts.map +1 -0
  153. package/dist/index.js +6 -0
  154. package/dist/index.js.map +1 -0
  155. package/dist/tsconfig.tsbuildinfo +1 -0
  156. package/package.json +196 -0
@@ -0,0 +1,658 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccountCreditManagerService = void 0;
4
+ const costCalculator_service_1 = require("./costCalculator.service");
5
+ const client_sqs_1 = require("@aws-sdk/client-sqs");
6
+ const constants_1 = require("../constants");
7
+ const utils_1 = require("../utils");
8
+ const enums_1 = require("../enums");
9
+ const account_service_1 = require("../../account/services/account.service");
10
+ class AccountCreditManagerService {
11
+ db;
12
+ tableName;
13
+ costCalculator;
14
+ sqsClient;
15
+ autoTopUpQueueUrl;
16
+ accountService;
17
+ constructor(db, tableName, sqsClient, autoTopUpQueueUrl, costCalculator, accountService) {
18
+ this.db = db;
19
+ this.tableName = tableName;
20
+ this.sqsClient = sqsClient;
21
+ this.autoTopUpQueueUrl = autoTopUpQueueUrl;
22
+ this.costCalculator = costCalculator ?? new costCalculator_service_1.CostCalculatorService();
23
+ this.accountService = accountService ?? new account_service_1.AccountService(db, tableName);
24
+ }
25
+ /**
26
+ * Update billing alert settings
27
+ */
28
+ async updateAlertSettings(accountId, settings) {
29
+ const account = await this.accountService.getAccountById(accountId);
30
+ if (!account?.Details.accountSettings?.billing) {
31
+ return { success: false, error: 'Account Settings not found' };
32
+ }
33
+ const updateExpressions = [];
34
+ const expressionValues = {};
35
+ if (settings.lowBalanceThresholdTokens !== undefined) {
36
+ updateExpressions.push('Details.accountSettings.billing.alerts.lowBalanceThresholdTokens = :lowThreshold');
37
+ expressionValues[':lowThreshold'] = settings.lowBalanceThresholdTokens;
38
+ }
39
+ if (settings.lowBalanceAlertSent !== undefined) {
40
+ updateExpressions.push('Details.accountSettings.billing.alerts.lowBalanceAlertSent = :lowSent');
41
+ expressionValues[':lowSent'] = settings.lowBalanceAlertSent;
42
+ }
43
+ if (settings.monthlyReportEnabled !== undefined) {
44
+ updateExpressions.push('Details.accountSettings.billing.alerts.monthlyReportEnabled = :monthlyReport');
45
+ expressionValues[':monthlyReport'] = settings.monthlyReportEnabled;
46
+ }
47
+ if (settings.alertEmails !== undefined) {
48
+ updateExpressions.push('Details.accountSettings.billing.alerts.alertEmails = :alertEmails');
49
+ expressionValues[':alertEmails'] = settings.alertEmails;
50
+ }
51
+ if (!updateExpressions.length) {
52
+ return { success: true };
53
+ }
54
+ try {
55
+ await this.db.update({
56
+ TableName: this.tableName,
57
+ Key: {
58
+ Pkey: account.Pkey,
59
+ Skey: account.Skey,
60
+ },
61
+ UpdateExpression: `SET ${updateExpressions.join(', ')}`,
62
+ ExpressionAttributeValues: expressionValues,
63
+ });
64
+ return { success: true };
65
+ }
66
+ catch (error) {
67
+ const errorMessage = error instanceof Error ? error.message : 'Update failed';
68
+ return { success: false, error: errorMessage };
69
+ }
70
+ }
71
+ /**
72
+ * Update spending controls
73
+ */
74
+ async updateSpendingControls(accountId, controls) {
75
+ const account = await this.accountService.getAccountById(accountId);
76
+ if (!account?.Details.accountSettings?.billing) {
77
+ return { success: false, error: 'Account Settings not found' };
78
+ }
79
+ const updateExpressions = [];
80
+ const expressionValues = {};
81
+ if (controls.monthlyBudgetTokens !== undefined) {
82
+ updateExpressions.push('Details.accountSettings.billing.controls.monthlyBudgetTokens = :monthlyBudget');
83
+ expressionValues[':monthlyBudget'] = controls.monthlyBudgetTokens;
84
+ }
85
+ if (controls.budgetAlertThresholdPercent !== undefined) {
86
+ updateExpressions.push('Details.accountSettings.billing.controls.budgetAlertThresholdPercent = :budgetThreshold');
87
+ expressionValues[':budgetThreshold'] = controls.budgetAlertThresholdPercent;
88
+ }
89
+ if (controls.currentMonthSpentTokens !== undefined) {
90
+ updateExpressions.push('Details.accountSettings.billing.controls.currentMonthSpentTokens = :currentMonthSpent');
91
+ expressionValues[':currentMonthSpent'] = controls.currentMonthSpentTokens;
92
+ }
93
+ if (controls.budgetResetDay !== undefined) {
94
+ updateExpressions.push('Details.accountSettings.billing.controls.budgetResetDay = :budgetResetDay');
95
+ expressionValues[':budgetResetDay'] = controls.budgetResetDay;
96
+ }
97
+ if (!updateExpressions.length) {
98
+ return { success: true };
99
+ }
100
+ try {
101
+ await this.db.update({
102
+ TableName: this.tableName,
103
+ Key: {
104
+ Pkey: account.Pkey,
105
+ Skey: account.Skey,
106
+ },
107
+ UpdateExpression: `SET ${updateExpressions.join(', ')}`,
108
+ ExpressionAttributeValues: expressionValues,
109
+ });
110
+ return { success: true };
111
+ }
112
+ catch (error) {
113
+ const errorMessage = error instanceof Error ? error.message : 'Update failed';
114
+ return { success: false, error: errorMessage };
115
+ }
116
+ }
117
+ async updateStripeCustomerId(accountId, customerId) {
118
+ const account = await this.accountService.getAccountById(accountId);
119
+ if (!account?.Details.accountSettings?.billing) {
120
+ return { success: false, error: 'Account Settings not found' };
121
+ }
122
+ try {
123
+ await this.db.update({
124
+ TableName: this.tableName,
125
+ Key: {
126
+ Pkey: account.Pkey,
127
+ Skey: account.Skey,
128
+ },
129
+ UpdateExpression: 'SET Details.accountSettings.billing.stripe.customerId = :customerId',
130
+ ExpressionAttributeValues: {
131
+ ':customerId': customerId,
132
+ },
133
+ });
134
+ return { success: true };
135
+ }
136
+ catch (error) {
137
+ const errorMessage = error instanceof Error ? error.message : 'Update failed';
138
+ return { success: false, error: errorMessage };
139
+ }
140
+ }
141
+ async updateDefaultPaymentMethod(accountId, paymentMethodId) {
142
+ const account = await this.accountService.getAccountById(accountId);
143
+ if (!account?.Details.accountSettings?.billing) {
144
+ return { success: false, error: 'Account Settings not found' };
145
+ }
146
+ try {
147
+ await this.db.update({
148
+ TableName: this.tableName,
149
+ Key: {
150
+ Pkey: account.Pkey,
151
+ Skey: account.Skey,
152
+ },
153
+ UpdateExpression: 'SET Details.accountSettings.billing.stripe.defaultPaymentMethodId = :paymentMethodId',
154
+ ExpressionAttributeValues: {
155
+ ':paymentMethodId': paymentMethodId,
156
+ },
157
+ });
158
+ return { success: true };
159
+ }
160
+ catch (error) {
161
+ const errorMessage = error instanceof Error ? error.message : 'Update failed';
162
+ return { success: false, error: errorMessage };
163
+ }
164
+ }
165
+ async resetMonthlyCounters(accountId) {
166
+ const account = await this.accountService.getAccountById(accountId);
167
+ if (!account?.Details.accountSettings?.billing) {
168
+ return { success: false, error: 'Account Settings not found' };
169
+ }
170
+ try {
171
+ await this.db.update({
172
+ TableName: this.tableName,
173
+ Key: {
174
+ Pkey: account.Pkey,
175
+ Skey: account.Skey,
176
+ },
177
+ UpdateExpression: 'SET Details.accountSettings.billing.controls.currentMonthSpentTokens = :zero, Details.accountSettings.billing.autoTopUp.currentMonthTopUpsTokens = :zero',
178
+ ExpressionAttributeValues: {
179
+ ':zero': 0,
180
+ },
181
+ });
182
+ return { success: true };
183
+ }
184
+ catch (error) {
185
+ const errorMessage = error instanceof Error ? error.message : 'Update failed';
186
+ return { success: false, error: errorMessage };
187
+ }
188
+ }
189
+ /**
190
+ * Get account balance
191
+ */
192
+ async getBalance(accountId) {
193
+ const account = await this.accountService.getAccountById(accountId);
194
+ if (!account) {
195
+ throw new Error(`Account not found: ${accountId}`);
196
+ }
197
+ const { billing } = account.Details.accountSettings;
198
+ return {
199
+ balanceTokens: billing?.balanceTokens ?? 0,
200
+ balanceFormatted: (0, utils_1.formatTokens)(billing?.balanceTokens ?? 0),
201
+ totalSpentTokens: billing?.totalSpentTokens ?? 0,
202
+ totalDepositedTokens: billing?.totalDepositedTokens ?? 0,
203
+ displayCurrency: billing?.displayCurrency ?? enums_1.DisplayCurrencyEnum.USD,
204
+ };
205
+ }
206
+ /**
207
+ * Check if account can afford a request
208
+ */
209
+ async canAffordRequest(accountId, estimatedCostTokens) {
210
+ const balance = await this.getBalance(accountId);
211
+ const canAfford = balance.balanceTokens >= estimatedCostTokens;
212
+ const shortfallTokens = canAfford ? undefined : estimatedCostTokens - balance.balanceTokens;
213
+ return {
214
+ canAfford,
215
+ currentBalanceTokens: balance.balanceTokens,
216
+ shortfallTokens,
217
+ };
218
+ }
219
+ /**
220
+ * Atomically charge account for an AI request
221
+ */
222
+ async chargeAccount(accountId, cost, triggeredBy, metadata) {
223
+ const transactionId = constants_1.BillingKeys.generateTransactionId();
224
+ const timestamp = new Date().toISOString();
225
+ const ttl = constants_1.BillingKeys.calculateTtl(90);
226
+ const account = await this.accountService.getAccountById(accountId);
227
+ if (!account) {
228
+ return {
229
+ success: false,
230
+ newBalanceTokens: 0,
231
+ transactionId,
232
+ error: 'Account not found',
233
+ };
234
+ }
235
+ const { billing } = account.Details.accountSettings;
236
+ const chargeAmount = cost.userPriceTokens;
237
+ if ((billing?.balanceTokens ?? 0) < chargeAmount) {
238
+ return {
239
+ success: false,
240
+ newBalanceTokens: billing?.balanceTokens ?? 0,
241
+ transactionId,
242
+ error: 'Insufficient balance',
243
+ };
244
+ }
245
+ // Enforce monthly budget limits when spending controls are configured
246
+ const controls = billing?.controls;
247
+ if (controls?.monthlyBudgetTokens && controls.monthlyBudgetTokens > 0) {
248
+ const currentMonthSpent = controls.currentMonthSpentTokens ?? 0;
249
+ const projectedSpent = currentMonthSpent + chargeAmount;
250
+ if (projectedSpent > controls.monthlyBudgetTokens) {
251
+ return {
252
+ success: false,
253
+ newBalanceTokens: billing?.balanceTokens ?? 0,
254
+ transactionId,
255
+ error: 'Monthly budget exceeded',
256
+ };
257
+ }
258
+ }
259
+ const newBalance = (billing?.balanceTokens ?? 0) - chargeAmount;
260
+ const transactionDetails = {
261
+ transactionId,
262
+ type: enums_1.TransactionTypeEnum.USAGE,
263
+ amountTokens: chargeAmount,
264
+ timestamp,
265
+ triggeredBy,
266
+ usageMetadata: {
267
+ ...metadata,
268
+ costBreakdown: cost,
269
+ },
270
+ };
271
+ try {
272
+ await this.db.transactWrite({
273
+ TransactItems: [
274
+ {
275
+ Update: {
276
+ TableName: this.tableName,
277
+ Key: {
278
+ Pkey: account.Pkey,
279
+ Skey: account.Skey,
280
+ },
281
+ UpdateExpression: `
282
+ SET Details.accountSettings.billing.balanceTokens = :newBalance,
283
+ Details.accountSettings.billing.totalSpentTokens = Details.accountSettings.billing.totalSpentTokens + :charge,
284
+ Details.accountSettings.billing.lastAiUsage = :timestamp,
285
+ Details.accountSettings.billing.controls.currentMonthSpentTokens = if_not_exists(Details.accountSettings.billing.controls.currentMonthSpentTokens, :zero) + :charge
286
+ `,
287
+ ConditionExpression: 'Details.accountSettings.billing.balanceTokens >= :charge',
288
+ ExpressionAttributeValues: {
289
+ ':newBalance': newBalance,
290
+ ':charge': chargeAmount,
291
+ ':timestamp': timestamp,
292
+ ':zero': 0,
293
+ },
294
+ },
295
+ },
296
+ {
297
+ Put: {
298
+ TableName: this.tableName,
299
+ Item: {
300
+ Pkey: constants_1.BillingKeys.accountPkey(accountId),
301
+ Skey: constants_1.BillingKeys.creditTransactionSkey(transactionId),
302
+ Details: transactionDetails,
303
+ Ttl: ttl,
304
+ },
305
+ },
306
+ },
307
+ ],
308
+ });
309
+ return {
310
+ success: true,
311
+ newBalanceTokens: newBalance,
312
+ transactionId,
313
+ };
314
+ }
315
+ catch (error) {
316
+ const errorMessage = error instanceof Error ? error.message : 'Transaction failed';
317
+ return {
318
+ success: false,
319
+ newBalanceTokens: billing?.balanceTokens ?? 0,
320
+ transactionId,
321
+ error: errorMessage,
322
+ };
323
+ }
324
+ }
325
+ /**
326
+ * Add credits to account (from Stripe payment)
327
+ */
328
+ async addCredits(accountId, tokens, depositMetadata) {
329
+ const transactionId = constants_1.BillingKeys.generateTransactionId();
330
+ const timestamp = new Date().toISOString();
331
+ const account = await this.accountService.getAccountById(accountId);
332
+ if (!account) {
333
+ return {
334
+ success: false,
335
+ newBalanceTokens: 0,
336
+ transactionId,
337
+ error: 'Account not found',
338
+ };
339
+ }
340
+ const { billing } = account.Details.accountSettings;
341
+ const newBalance = (billing?.balanceTokens ?? 0) + tokens;
342
+ const transactionType = depositMetadata.isAutoTopUp ? enums_1.TransactionTypeEnum.AUTO_TOPUP : enums_1.TransactionTypeEnum.DEPOSIT;
343
+ const transactionDetails = {
344
+ transactionId,
345
+ type: transactionType,
346
+ amountTokens: tokens,
347
+ timestamp,
348
+ triggeredBy: transactionType === enums_1.TransactionTypeEnum.AUTO_TOPUP ? 'system' : 'user',
349
+ depositMetadata,
350
+ };
351
+ try {
352
+ await this.db.transactWrite({
353
+ TransactItems: [
354
+ {
355
+ Update: {
356
+ TableName: this.tableName,
357
+ Key: {
358
+ Pkey: account.Pkey,
359
+ Skey: account.Skey,
360
+ },
361
+ UpdateExpression: `
362
+ SET Details.accountSettings.billing.balanceTokens = :newBalance,
363
+ Details.accountSettings.billing.totalDepositedTokens = Details.accountSettings.billing.totalDepositedTokens + :tokens,
364
+ Details.accountSettings.billing.lastDeposit = :timestamp,
365
+ Details.accountSettings.billing.alerts.lowBalanceAlertSent = :resetAlert
366
+ `,
367
+ ExpressionAttributeValues: {
368
+ ':newBalance': newBalance,
369
+ ':tokens': tokens,
370
+ ':timestamp': timestamp,
371
+ ':resetAlert': false,
372
+ },
373
+ },
374
+ },
375
+ {
376
+ Put: {
377
+ TableName: this.tableName,
378
+ Item: {
379
+ Pkey: constants_1.BillingKeys.accountPkey(accountId),
380
+ Skey: constants_1.BillingKeys.creditTransactionSkey(transactionId),
381
+ Details: transactionDetails,
382
+ // No TTL for deposits
383
+ },
384
+ },
385
+ },
386
+ ],
387
+ });
388
+ return {
389
+ success: true,
390
+ newBalanceTokens: newBalance,
391
+ transactionId,
392
+ };
393
+ }
394
+ catch (error) {
395
+ const errorMessage = error instanceof Error ? error.message : 'Transaction failed';
396
+ return {
397
+ success: false,
398
+ newBalanceTokens: billing?.balanceTokens ?? 0,
399
+ transactionId,
400
+ error: errorMessage,
401
+ };
402
+ }
403
+ }
404
+ /**
405
+ * Apply initial credit for new accounts
406
+ */
407
+ async applyInitialCredit(accountId) {
408
+ const transactionId = constants_1.BillingKeys.generateTransactionId();
409
+ const timestamp = new Date().toISOString();
410
+ const initialTokens = constants_1.COST_CONFIG.initialCreditTokens;
411
+ const account = await this.accountService.getAccountById(accountId);
412
+ if (!account) {
413
+ return {
414
+ success: false,
415
+ applied: false,
416
+ error: 'Account not found',
417
+ };
418
+ }
419
+ if (account.Details.accountSettings.billing?.initialCreditApplied) {
420
+ return {
421
+ success: true,
422
+ applied: false, // Already applied
423
+ };
424
+ }
425
+ const newBalance = (account.Details.accountSettings.billing?.balanceTokens ?? 0) + initialTokens;
426
+ const transactionDetails = {
427
+ transactionId,
428
+ type: enums_1.TransactionTypeEnum.INITIAL_CREDIT,
429
+ amountTokens: initialTokens,
430
+ timestamp,
431
+ triggeredBy: 'system',
432
+ reason: enums_1.CreditReasonEnum.INITIAL_CREDIT,
433
+ };
434
+ try {
435
+ await this.db.transactWrite({
436
+ TransactItems: [
437
+ {
438
+ Update: {
439
+ TableName: this.tableName,
440
+ Key: {
441
+ Pkey: account.Pkey,
442
+ Skey: account.Skey,
443
+ },
444
+ UpdateExpression: `
445
+ SET Details.accountSettings.billing.balanceTokens = :newBalance,
446
+ Details.accountSettings.billing.initialCreditApplied = :applied
447
+ `,
448
+ ConditionExpression: 'attribute_not_exists(Details.accountSettings.billing.initialCreditApplied) OR Details.accountSettings.billing.initialCreditApplied = :false',
449
+ ExpressionAttributeValues: {
450
+ ':newBalance': newBalance,
451
+ ':applied': true,
452
+ ':false': false,
453
+ },
454
+ },
455
+ },
456
+ {
457
+ Put: {
458
+ TableName: this.tableName,
459
+ Item: {
460
+ Pkey: constants_1.BillingKeys.accountPkey(accountId),
461
+ Skey: constants_1.BillingKeys.creditTransactionSkey(transactionId),
462
+ Details: transactionDetails,
463
+ },
464
+ },
465
+ },
466
+ ],
467
+ });
468
+ return {
469
+ success: true,
470
+ applied: true,
471
+ transactionId,
472
+ };
473
+ }
474
+ catch (error) {
475
+ const errorMessage = error instanceof Error ? error.message : 'Transaction failed';
476
+ return {
477
+ success: false,
478
+ applied: false,
479
+ error: errorMessage,
480
+ };
481
+ }
482
+ }
483
+ /**
484
+ * Check and trigger auto top-up if needed (async via SQS)
485
+ */
486
+ async checkAndTriggerAutoTopUp(accountId) {
487
+ const account = await this.accountService.getAccountById(accountId);
488
+ if (!account) {
489
+ return { triggered: false, reason: 'Account not found' };
490
+ }
491
+ const billing = account.Details.accountSettings.billing;
492
+ if (!billing) {
493
+ return { triggered: false, reason: 'Billing settings not found' };
494
+ }
495
+ const { autoTopUp, stripe } = billing;
496
+ // Check conditions
497
+ if (!autoTopUp.enabled) {
498
+ return { triggered: false, reason: 'Auto top-up disabled' };
499
+ }
500
+ if (billing.balanceTokens >= autoTopUp.thresholdTokens) {
501
+ return { triggered: false, reason: 'Balance above threshold' };
502
+ }
503
+ if (autoTopUp?.failedAttempts ?? 0 >= 3) {
504
+ return { triggered: false, reason: 'Too many failed attempts' };
505
+ }
506
+ if (autoTopUp.pausedUntil && new Date(autoTopUp.pausedUntil) > new Date()) {
507
+ return { triggered: false, reason: 'Auto top-up paused' };
508
+ }
509
+ if (!stripe?.customerId || !stripe.defaultPaymentMethodId) {
510
+ return { triggered: false, reason: 'No payment method configured' };
511
+ }
512
+ // Check monthly cap
513
+ if (autoTopUp.maxMonthlyTopUpsTokens) {
514
+ const wouldExceed = (autoTopUp.currentMonthTopUpsTokens ?? 0) + autoTopUp.amountTokens > autoTopUp.maxMonthlyTopUpsTokens;
515
+ if (wouldExceed) {
516
+ return { triggered: false, reason: 'Monthly cap reached' };
517
+ }
518
+ }
519
+ // Queue auto top-up message
520
+ const message = {
521
+ accountId,
522
+ amountTokens: autoTopUp.amountTokens,
523
+ displayCurrency: billing.displayCurrency,
524
+ stripeRegion: stripe.stripeRegion,
525
+ stripeCustomerId: stripe.customerId,
526
+ stripePaymentMethodId: stripe.defaultPaymentMethodId,
527
+ triggeredAt: new Date().toISOString(),
528
+ currentBalanceTokens: billing.balanceTokens,
529
+ thresholdTokens: autoTopUp.thresholdTokens,
530
+ attemptNumber: autoTopUp.failedAttempts ? autoTopUp.failedAttempts + 1 : 1,
531
+ };
532
+ await this.sqsClient.send(new client_sqs_1.SendMessageCommand({
533
+ QueueUrl: this.autoTopUpQueueUrl,
534
+ MessageBody: JSON.stringify(message),
535
+ }));
536
+ return {
537
+ triggered: true,
538
+ amountTokens: autoTopUp.amountTokens,
539
+ };
540
+ }
541
+ /**
542
+ * Get transaction history for account
543
+ */
544
+ async getTransactionHistory(accountId, limit = 50, cursor) {
545
+ const exclusiveStartKey = cursor
546
+ ? JSON.parse(Buffer.from(cursor, 'base64').toString())
547
+ : undefined;
548
+ const result = await this.db.query({
549
+ TableName: this.tableName,
550
+ KeyConditionExpression: 'Pkey = :pk AND begins_with(Skey, :sk)',
551
+ ExpressionAttributeValues: {
552
+ ':pk': constants_1.BillingKeys.accountPkey(accountId),
553
+ ':sk': 'credit-txn:',
554
+ },
555
+ ScanIndexForward: false,
556
+ Limit: limit + 1,
557
+ ...(exclusiveStartKey && { ExclusiveStartKey: exclusiveStartKey }),
558
+ });
559
+ const items = result?.Items ?? [];
560
+ const hasMore = items.length > limit;
561
+ const transactions = items.slice(0, limit).map(item => {
562
+ const details = item.Details;
563
+ return {
564
+ transactionId: details.transactionId,
565
+ type: details.type,
566
+ amountTokens: details.amountTokens,
567
+ amountFormatted: this.formatTransactionAmount(details),
568
+ timestamp: details.timestamp,
569
+ triggeredBy: details.triggeredBy,
570
+ description: this.getTransactionDescription(details),
571
+ };
572
+ });
573
+ const nextCursor = hasMore && result?.LastEvaluatedKey
574
+ ? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64')
575
+ : undefined;
576
+ return {
577
+ transactions,
578
+ hasMore,
579
+ nextCursor,
580
+ };
581
+ }
582
+ /**
583
+ * Update auto top-up settings
584
+ */
585
+ async updateAutoTopUpSettings(accountId, settings) {
586
+ const accountSettings = await this.accountService.getAccountById(accountId);
587
+ if (!accountSettings) {
588
+ return { success: false, error: 'Account Settings not found' };
589
+ }
590
+ const updateExpressions = [];
591
+ const expressionValues = {};
592
+ if (settings.enabled !== undefined) {
593
+ updateExpressions.push('Details.accountSettings.billing.autoTopUp.enabled = :enabled');
594
+ expressionValues[':enabled'] = settings.enabled;
595
+ // Update Data2 for GSI
596
+ updateExpressions.push('Data2 = :data2');
597
+ expressionValues[':data2'] = constants_1.BillingKeys.autoTopUpData2(settings.enabled);
598
+ }
599
+ if (settings.thresholdTokens !== undefined) {
600
+ updateExpressions.push('Details.accountSettings.billing.autoTopUp.thresholdTokens = :threshold');
601
+ expressionValues[':threshold'] = settings.thresholdTokens;
602
+ }
603
+ if (settings.amountTokens !== undefined) {
604
+ updateExpressions.push('Details.accountSettings.billing.autoTopUp.amountTokens = :amount');
605
+ expressionValues[':amount'] = settings.amountTokens;
606
+ }
607
+ if (settings.maxMonthlyTopUpsTokens !== undefined) {
608
+ updateExpressions.push('Details.accountSettings.billing.autoTopUp.maxMonthlyTopUpsTokens = :maxMonthly');
609
+ expressionValues[':maxMonthly'] = settings.maxMonthlyTopUpsTokens;
610
+ }
611
+ try {
612
+ await this.db.update({
613
+ TableName: this.tableName,
614
+ Key: {
615
+ Pkey: accountSettings.Pkey, // we are not returning the full data
616
+ Skey: accountSettings.Skey,
617
+ },
618
+ UpdateExpression: `SET ${updateExpressions.join(', ')}`,
619
+ ExpressionAttributeValues: expressionValues,
620
+ });
621
+ return { success: true };
622
+ }
623
+ catch (error) {
624
+ const errorMessage = error instanceof Error ? error.message : 'Update failed';
625
+ return { success: false, error: errorMessage };
626
+ }
627
+ }
628
+ // ===========================================================================
629
+ // PRIVATE HELPERS
630
+ // ===========================================================================
631
+ formatTransactionAmount(details) {
632
+ const isCredit = [
633
+ enums_1.TransactionTypeEnum.DEPOSIT,
634
+ enums_1.TransactionTypeEnum.INITIAL_CREDIT,
635
+ enums_1.TransactionTypeEnum.AUTO_TOPUP,
636
+ ].includes(details.type);
637
+ const sign = isCredit ? '+' : '-';
638
+ return `${sign}${details.amountTokens} tokens`;
639
+ }
640
+ getTransactionDescription(details) {
641
+ switch (details.type) {
642
+ case enums_1.TransactionTypeEnum.USAGE: {
643
+ const feature = details.usageMetadata?.feature ?? 'AI Request';
644
+ return `AI Usage - ${feature}`;
645
+ }
646
+ case enums_1.TransactionTypeEnum.DEPOSIT:
647
+ return 'Manual Top-up via Stripe';
648
+ case enums_1.TransactionTypeEnum.AUTO_TOPUP:
649
+ return 'Auto Top-up via Stripe';
650
+ case enums_1.TransactionTypeEnum.INITIAL_CREDIT:
651
+ return 'Welcome Credit';
652
+ default:
653
+ return 'Transaction';
654
+ }
655
+ }
656
+ }
657
+ exports.AccountCreditManagerService = AccountCreditManagerService;
658
+ //# sourceMappingURL=accountCreditManager.service.js.map