@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.
- package/README.md +35 -0
- package/bin/run-clean.js +53 -0
- package/dist/account/index.d.ts +2 -0
- package/dist/account/index.d.ts.map +1 -0
- package/dist/account/index.js +5 -0
- package/dist/account/index.js.map +1 -0
- package/dist/account/interfaces/accountRecord.interface.d.ts +9 -0
- package/dist/account/interfaces/accountRecord.interface.d.ts.map +1 -0
- package/dist/account/interfaces/accountRecord.interface.js +3 -0
- package/dist/account/interfaces/accountRecord.interface.js.map +1 -0
- package/dist/account/services/account.service.d.ts +9 -0
- package/dist/account/services/account.service.d.ts.map +1 -0
- package/dist/account/services/account.service.js +30 -0
- package/dist/account/services/account.service.js.map +1 -0
- package/dist/billing/constants.d.ts +39 -0
- package/dist/billing/constants.d.ts.map +1 -0
- package/dist/billing/constants.js +57 -0
- package/dist/billing/constants.js.map +1 -0
- package/dist/billing/enums/accountType.enum.d.ts +2 -0
- package/dist/billing/enums/accountType.enum.d.ts.map +1 -0
- package/dist/billing/enums/accountType.enum.js +6 -0
- package/dist/billing/enums/accountType.enum.js.map +1 -0
- package/dist/billing/enums/creditReason.enum.d.ts +7 -0
- package/dist/billing/enums/creditReason.enum.d.ts.map +1 -0
- package/dist/billing/enums/creditReason.enum.js +11 -0
- package/dist/billing/enums/creditReason.enum.js.map +1 -0
- package/dist/billing/enums/displayCurrency.enum.d.ts +2 -0
- package/dist/billing/enums/displayCurrency.enum.d.ts.map +1 -0
- package/dist/billing/enums/displayCurrency.enum.js +6 -0
- package/dist/billing/enums/displayCurrency.enum.js.map +1 -0
- package/dist/billing/enums/index.d.ts +8 -0
- package/dist/billing/enums/index.d.ts.map +1 -0
- package/dist/billing/enums/index.js +11 -0
- package/dist/billing/enums/index.js.map +1 -0
- package/dist/billing/enums/reasoningEffort.enum.d.ts +8 -0
- package/dist/billing/enums/reasoningEffort.enum.d.ts.map +1 -0
- package/dist/billing/enums/reasoningEffort.enum.js +12 -0
- package/dist/billing/enums/reasoningEffort.enum.js.map +1 -0
- package/dist/billing/enums/stripePaymentStatus.enum.d.ts +7 -0
- package/dist/billing/enums/stripePaymentStatus.enum.d.ts.map +1 -0
- package/dist/billing/enums/stripePaymentStatus.enum.js +11 -0
- package/dist/billing/enums/stripePaymentStatus.enum.js.map +1 -0
- package/dist/billing/enums/stripeRegion.enum.d.ts +2 -0
- package/dist/billing/enums/stripeRegion.enum.d.ts.map +1 -0
- package/dist/billing/enums/stripeRegion.enum.js +6 -0
- package/dist/billing/enums/stripeRegion.enum.js.map +1 -0
- package/dist/billing/enums/transactionType.enum.d.ts +8 -0
- package/dist/billing/enums/transactionType.enum.d.ts.map +1 -0
- package/dist/billing/enums/transactionType.enum.js +12 -0
- package/dist/billing/enums/transactionType.enum.js.map +1 -0
- package/dist/billing/index.d.ts +3 -0
- package/dist/billing/index.d.ts.map +1 -0
- package/dist/billing/index.js +6 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing/interfaces/addCreditsResult.interface.d.ts +7 -0
- package/dist/billing/interfaces/addCreditsResult.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/addCreditsResult.interface.js +3 -0
- package/dist/billing/interfaces/addCreditsResult.interface.js.map +1 -0
- package/dist/billing/interfaces/aiRequestBillingResponse.interface.d.ts +30 -0
- package/dist/billing/interfaces/aiRequestBillingResponse.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/aiRequestBillingResponse.interface.js +3 -0
- package/dist/billing/interfaces/aiRequestBillingResponse.interface.js.map +1 -0
- package/dist/billing/interfaces/autoTopUpCheckResult.interface.d.ts +6 -0
- package/dist/billing/interfaces/autoTopUpCheckResult.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/autoTopUpCheckResult.interface.js +3 -0
- package/dist/billing/interfaces/autoTopUpCheckResult.interface.js.map +1 -0
- package/dist/billing/interfaces/autoTopUpMessage.interface.d.ts +15 -0
- package/dist/billing/interfaces/autoTopUpMessage.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/autoTopUpMessage.interface.js +3 -0
- package/dist/billing/interfaces/autoTopUpMessage.interface.js.map +1 -0
- package/dist/billing/interfaces/balanceResponse.interface.d.ts +9 -0
- package/dist/billing/interfaces/balanceResponse.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/balanceResponse.interface.js +3 -0
- package/dist/billing/interfaces/balanceResponse.interface.js.map +1 -0
- package/dist/billing/interfaces/billing.interface.d.ts +2 -0
- package/dist/billing/interfaces/billing.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/billing.interface.js +3 -0
- package/dist/billing/interfaces/billing.interface.js.map +1 -0
- package/dist/billing/interfaces/canAffordResult.interface.d.ts +6 -0
- package/dist/billing/interfaces/canAffordResult.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/canAffordResult.interface.js +3 -0
- package/dist/billing/interfaces/canAffordResult.interface.js.map +1 -0
- package/dist/billing/interfaces/chargeResult.interface.d.ts +7 -0
- package/dist/billing/interfaces/chargeResult.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/chargeResult.interface.js +3 -0
- package/dist/billing/interfaces/chargeResult.interface.js.map +1 -0
- package/dist/billing/interfaces/costBreakdown.interface.d.ts +9 -0
- package/dist/billing/interfaces/costBreakdown.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/costBreakdown.interface.js +3 -0
- package/dist/billing/interfaces/costBreakdown.interface.js.map +1 -0
- package/dist/billing/interfaces/creditTransaction.interface.d.ts +28 -0
- package/dist/billing/interfaces/creditTransaction.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/creditTransaction.interface.js +3 -0
- package/dist/billing/interfaces/creditTransaction.interface.js.map +1 -0
- package/dist/billing/interfaces/depositMetadata.interface.d.ts +10 -0
- package/dist/billing/interfaces/depositMetadata.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/depositMetadata.interface.js +3 -0
- package/dist/billing/interfaces/depositMetadata.interface.js.map +1 -0
- package/dist/billing/interfaces/iAccountCreditManager.interface.d.ts +43 -0
- package/dist/billing/interfaces/iAccountCreditManager.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/iAccountCreditManager.interface.js +3 -0
- package/dist/billing/interfaces/iAccountCreditManager.interface.js.map +1 -0
- package/dist/billing/interfaces/iCostCalculator.interface.d.ts +7 -0
- package/dist/billing/interfaces/iCostCalculator.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/iCostCalculator.interface.js +3 -0
- package/dist/billing/interfaces/iCostCalculator.interface.js.map +1 -0
- package/dist/billing/interfaces/index.d.ts +16 -0
- package/dist/billing/interfaces/index.d.ts.map +1 -0
- package/dist/billing/interfaces/index.js +19 -0
- package/dist/billing/interfaces/index.js.map +1 -0
- package/dist/billing/interfaces/initialCreditResult.interface.d.ts +7 -0
- package/dist/billing/interfaces/initialCreditResult.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/initialCreditResult.interface.js +3 -0
- package/dist/billing/interfaces/initialCreditResult.interface.js.map +1 -0
- package/dist/billing/interfaces/requestCostParams.interface.d.ts +7 -0
- package/dist/billing/interfaces/requestCostParams.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/requestCostParams.interface.js +3 -0
- package/dist/billing/interfaces/requestCostParams.interface.js.map +1 -0
- package/dist/billing/interfaces/stripePayment.interface.d.ts +29 -0
- package/dist/billing/interfaces/stripePayment.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/stripePayment.interface.js +3 -0
- package/dist/billing/interfaces/stripePayment.interface.js.map +1 -0
- package/dist/billing/interfaces/transactionHistory.interface.d.ts +16 -0
- package/dist/billing/interfaces/transactionHistory.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/transactionHistory.interface.js +3 -0
- package/dist/billing/interfaces/transactionHistory.interface.js.map +1 -0
- package/dist/billing/interfaces/triggeredBy.interface.d.ts +1 -0
- package/dist/billing/interfaces/triggeredBy.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/triggeredBy.interface.js +2 -0
- package/dist/billing/interfaces/triggeredBy.interface.js.map +1 -0
- package/dist/billing/interfaces/usageMetadata.interface.d.ts +12 -0
- package/dist/billing/interfaces/usageMetadata.interface.d.ts.map +1 -0
- package/dist/billing/interfaces/usageMetadata.interface.js +3 -0
- package/dist/billing/interfaces/usageMetadata.interface.js.map +1 -0
- package/dist/billing/services/accountCreditManager.service.d.ts +81 -0
- package/dist/billing/services/accountCreditManager.service.d.ts.map +1 -0
- package/dist/billing/services/accountCreditManager.service.js +658 -0
- package/dist/billing/services/accountCreditManager.service.js.map +1 -0
- package/dist/billing/services/costCalculator.service.d.ts +36 -0
- package/dist/billing/services/costCalculator.service.d.ts.map +1 -0
- package/dist/billing/services/costCalculator.service.js +100 -0
- package/dist/billing/services/costCalculator.service.js.map +1 -0
- package/dist/billing/services/tokenFormatter.d.ts +10 -0
- package/dist/billing/services/tokenFormatter.d.ts.map +1 -0
- package/dist/billing/services/tokenFormatter.js +27 -0
- package/dist/billing/services/tokenFormatter.js.map +1 -0
- package/dist/billing/utils.d.ts +8 -0
- package/dist/billing/utils.d.ts.map +1 -0
- package/dist/billing/utils.js +33 -0
- package/dist/billing/utils.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- 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
|