responses-proxy 0.1.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 +56 -0
- package/cli.js +118 -0
- package/dist/anthropic-messages.js +383 -0
- package/dist/anthropic-messages.test.js +209 -0
- package/dist/audit-log.js +138 -0
- package/dist/audit-log.test.js +480 -0
- package/dist/billing-expiration.js +70 -0
- package/dist/billing-expiration.test.js +114 -0
- package/dist/billing.js +716 -0
- package/dist/billing.test.js +228 -0
- package/dist/chatgpt-oauth-store.js +240 -0
- package/dist/chatgpt-oauth-store.test.js +88 -0
- package/dist/chatgpt-oauth.js +118 -0
- package/dist/chatgpt-oauth.test.js +63 -0
- package/dist/chatgpt-provider-auth.js +60 -0
- package/dist/chatgpt-provider-auth.test.js +101 -0
- package/dist/client/app-icon.svg +17 -0
- package/dist/client/assets/index-C7Vvhst8.js +14 -0
- package/dist/client/assets/index-DpqgYK3L.css +1 -0
- package/dist/client/favicon.svg +17 -0
- package/dist/client/index.html +31 -0
- package/dist/client-config-apply.js +345 -0
- package/dist/client-config-apply.test.js +185 -0
- package/dist/client-token-limits.js +111 -0
- package/dist/client-token-limits.test.js +129 -0
- package/dist/codex-config.js +47 -0
- package/dist/codex-setup.js +87 -0
- package/dist/codex-setup.test.js +30 -0
- package/dist/config.js +314 -0
- package/dist/cost-analytics.js +31 -0
- package/dist/cost-analytics.test.js +38 -0
- package/dist/customer-key-access.js +126 -0
- package/dist/customer-key-access.test.js +178 -0
- package/dist/customer-keys.js +209 -0
- package/dist/customer-keys.test.js +68 -0
- package/dist/customer-usage.js +18 -0
- package/dist/customer-usage.test.js +55 -0
- package/dist/dashboard-auth.js +318 -0
- package/dist/dashboard-auth.test.js +133 -0
- package/dist/dashboard-serving.test.js +235 -0
- package/dist/error-response.js +174 -0
- package/dist/error-response.test.js +88 -0
- package/dist/forward.js +357 -0
- package/dist/health-websocket-manager.js +174 -0
- package/dist/http-rate-limit.js +36 -0
- package/dist/http-rate-limit.test.js +62 -0
- package/dist/kiro-auth.js +136 -0
- package/dist/kiro-auth.test.js +234 -0
- package/dist/kiro-codewhisperer.js +646 -0
- package/dist/kiro-codewhisperer.test.js +219 -0
- package/dist/kiro-device-login.js +338 -0
- package/dist/kiro-eventstream.js +219 -0
- package/dist/kiro-eventstream.test.js +79 -0
- package/dist/kiro-forward.js +401 -0
- package/dist/kiro-import-cli.js +69 -0
- package/dist/kiro-import.js +94 -0
- package/dist/kiro-import.test.js +125 -0
- package/dist/kiro-token-store.js +196 -0
- package/dist/kiro-token-store.test.js +207 -0
- package/dist/krouter-usage.js +243 -0
- package/dist/model-combo-repository.js +147 -0
- package/dist/model-routing.js +69 -0
- package/dist/model-routing.test.js +41 -0
- package/dist/normalize-request.js +531 -0
- package/dist/normalize-request.test.js +277 -0
- package/dist/omv-public-firewall.test.js +11 -0
- package/dist/package.json +17 -0
- package/dist/prompt-cache-state.js +146 -0
- package/dist/prompt-cache-state.test.js +71 -0
- package/dist/prompt-cache.js +229 -0
- package/dist/provider-health-service.js +404 -0
- package/dist/provider-request-parameters.js +107 -0
- package/dist/provider-request-parameters.test.js +26 -0
- package/dist/provider-routing.js +114 -0
- package/dist/provider-routing.test.js +64 -0
- package/dist/provider-usage.js +314 -0
- package/dist/request-timeout-policy.js +61 -0
- package/dist/request-timeout-policy.test.js +40 -0
- package/dist/response-cache.js +69 -0
- package/dist/response-cache.test.js +28 -0
- package/dist/routing-combo-repository.js +300 -0
- package/dist/routing-engine.js +377 -0
- package/dist/routing-integration.js +155 -0
- package/dist/routing-simulation-engine.js +326 -0
- package/dist/rtk-layer.js +483 -0
- package/dist/rtk-layer.test.js +198 -0
- package/dist/runtime-provider-repository.js +1742 -0
- package/dist/runtime-provider-repository.test.js +1177 -0
- package/dist/schema.js +118 -0
- package/dist/schema.test.js +16 -0
- package/dist/sepay-webhook.js +87 -0
- package/dist/sepay-webhook.test.js +142 -0
- package/dist/server-body-limit.test.js +35 -0
- package/dist/server-client-token-limits.test.js +161 -0
- package/dist/server-codex-config-setup.test.js +76 -0
- package/dist/server-http-rate-limit.test.js +80 -0
- package/dist/server-response-cache.test.js +105 -0
- package/dist/server-routes-alias.test.js +39 -0
- package/dist/server-sepay-webhook-security.test.js +59 -0
- package/dist/server.js +5906 -0
- package/dist/session-log.js +178 -0
- package/dist/tailnet-funnel-script.test.js +33 -0
- package/dist/telegram-bot/actions.js +118 -0
- package/dist/telegram-bot/admin-actions.js +103 -0
- package/dist/telegram-bot/auth.js +46 -0
- package/dist/telegram-bot/auth.test.js +1 -0
- package/dist/telegram-bot/bot-identity-repository.js +189 -0
- package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
- package/dist/telegram-bot/callbacks.js +30 -0
- package/dist/telegram-bot/codex-config-delivery.js +38 -0
- package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
- package/dist/telegram-bot/commands/accounts.js +140 -0
- package/dist/telegram-bot/commands/apikey.js +737 -0
- package/dist/telegram-bot/commands/apply.js +265 -0
- package/dist/telegram-bot/commands/clients.js +13 -0
- package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
- package/dist/telegram-bot/commands/grant.js +138 -0
- package/dist/telegram-bot/commands/grant.test.js +217 -0
- package/dist/telegram-bot/commands/help.js +52 -0
- package/dist/telegram-bot/commands/me.js +53 -0
- package/dist/telegram-bot/commands/models.js +6 -0
- package/dist/telegram-bot/commands/oauth.js +64 -0
- package/dist/telegram-bot/commands/plans.js +96 -0
- package/dist/telegram-bot/commands/providers.js +27 -0
- package/dist/telegram-bot/commands/quota.js +10 -0
- package/dist/telegram-bot/commands/renew-user.js +139 -0
- package/dist/telegram-bot/commands/renew-user.test.js +184 -0
- package/dist/telegram-bot/commands/renew.js +1369 -0
- package/dist/telegram-bot/commands/renew.test.js +1633 -0
- package/dist/telegram-bot/commands/start.js +212 -0
- package/dist/telegram-bot/commands/start.test.js +280 -0
- package/dist/telegram-bot/commands/status.js +6 -0
- package/dist/telegram-bot/commands/tailscale.js +15 -0
- package/dist/telegram-bot/commands/tailscale.test.js +76 -0
- package/dist/telegram-bot/commands/test.js +51 -0
- package/dist/telegram-bot/commands/test.test.js +14 -0
- package/dist/telegram-bot/commands/usage.js +10 -0
- package/dist/telegram-bot/config.js +98 -0
- package/dist/telegram-bot/config.test.js +42 -0
- package/dist/telegram-bot/customer-actions.js +160 -0
- package/dist/telegram-bot/customer-api-keys.js +68 -0
- package/dist/telegram-bot/customer-billing.js +72 -0
- package/dist/telegram-bot/customer-workspace-repository.js +134 -0
- package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
- package/dist/telegram-bot/dashboard-login.js +39 -0
- package/dist/telegram-bot/format.js +140 -0
- package/dist/telegram-bot/grants.js +370 -0
- package/dist/telegram-bot/grants.test.js +290 -0
- package/dist/telegram-bot/index.js +85 -0
- package/dist/telegram-bot/message-cleanup.js +55 -0
- package/dist/telegram-bot/message-cleanup.test.js +77 -0
- package/dist/telegram-bot/message-format.js +45 -0
- package/dist/telegram-bot/message-format.test.js +10 -0
- package/dist/telegram-bot/proxy-client.js +174 -0
- package/dist/telegram-bot/rate-limit.js +95 -0
- package/dist/telegram-bot/rate-limit.test.js +58 -0
- package/dist/telegram-bot/sessions.js +171 -0
- package/dist/telegram-bot/sessions.test.js +107 -0
- package/dist/telegram-bot/telegram-adapter.js +126 -0
- package/dist/telegram-bot/worker.js +63 -0
- package/package.json +39 -0
package/dist/billing.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
5
|
+
export class BillingRepository {
|
|
6
|
+
db;
|
|
7
|
+
constructor(db) {
|
|
8
|
+
this.db = db;
|
|
9
|
+
}
|
|
10
|
+
static create(dbFile) {
|
|
11
|
+
mkdirSync(path.dirname(dbFile), { recursive: true });
|
|
12
|
+
const db = new BetterSqlite3(dbFile);
|
|
13
|
+
ensureBillingSchema(db);
|
|
14
|
+
const repository = new BillingRepository(db);
|
|
15
|
+
repository.seedDefaultPlans();
|
|
16
|
+
return repository;
|
|
17
|
+
}
|
|
18
|
+
seedDefaultPlans(now = new Date()) {
|
|
19
|
+
const timestamp = now.toISOString();
|
|
20
|
+
const insertPlan = this.db.prepare(`INSERT INTO plans (
|
|
21
|
+
id,
|
|
22
|
+
name,
|
|
23
|
+
status,
|
|
24
|
+
price_cents,
|
|
25
|
+
currency,
|
|
26
|
+
billing_interval,
|
|
27
|
+
monthly_token_limit,
|
|
28
|
+
max_api_keys,
|
|
29
|
+
allowed_models_json,
|
|
30
|
+
metadata_json,
|
|
31
|
+
created_at,
|
|
32
|
+
updated_at
|
|
33
|
+
)
|
|
34
|
+
VALUES (?, ?, 'active', ?, 'VND', 'month', ?, ?, '[]', '{}', ?, ?)
|
|
35
|
+
ON CONFLICT(id) DO NOTHING`);
|
|
36
|
+
insertPlan.run("basic", "Basic", 5_000, 10_000_000, 1, timestamp, timestamp);
|
|
37
|
+
}
|
|
38
|
+
listPlans() {
|
|
39
|
+
const rows = this.db.prepare("SELECT * FROM plans ORDER BY id").all();
|
|
40
|
+
return rows.map(mapPlanRow);
|
|
41
|
+
}
|
|
42
|
+
getPlan(planId) {
|
|
43
|
+
const row = this.db.prepare("SELECT * FROM plans WHERE id = ?").get(planId);
|
|
44
|
+
return row ? mapPlanRow(row) : undefined;
|
|
45
|
+
}
|
|
46
|
+
createPlan(input) {
|
|
47
|
+
const now = input.now ?? new Date();
|
|
48
|
+
const timestamp = now.toISOString();
|
|
49
|
+
const status = input.status ?? "active";
|
|
50
|
+
const currency = (input.currency ?? "USD").toUpperCase();
|
|
51
|
+
const billingInterval = input.billingInterval ?? "month";
|
|
52
|
+
this.db
|
|
53
|
+
.prepare(`INSERT INTO plans (
|
|
54
|
+
id,
|
|
55
|
+
name,
|
|
56
|
+
status,
|
|
57
|
+
price_cents,
|
|
58
|
+
currency,
|
|
59
|
+
billing_interval,
|
|
60
|
+
monthly_token_limit,
|
|
61
|
+
max_api_keys,
|
|
62
|
+
allowed_models_json,
|
|
63
|
+
metadata_json,
|
|
64
|
+
created_at,
|
|
65
|
+
updated_at
|
|
66
|
+
)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
68
|
+
.run(input.id, input.name, status, input.priceCents ?? 0, currency, billingInterval, input.monthlyTokenLimit, input.maxApiKeys, JSON.stringify(input.allowedModels ?? []), input.metadataJson ?? "{}", timestamp, timestamp);
|
|
69
|
+
const plan = this.getPlan(input.id);
|
|
70
|
+
if (!plan) {
|
|
71
|
+
throw new Error(`Plan not found after create: ${input.id}`);
|
|
72
|
+
}
|
|
73
|
+
return plan;
|
|
74
|
+
}
|
|
75
|
+
grantSubscription(input) {
|
|
76
|
+
const plan = this.getPlan(input.planId);
|
|
77
|
+
if (!plan) {
|
|
78
|
+
throw new Error(`Plan not found: ${input.planId}`);
|
|
79
|
+
}
|
|
80
|
+
const now = input.now ?? new Date();
|
|
81
|
+
const nowIso = now.toISOString();
|
|
82
|
+
const currentSubscription = this.getLatestSubscriptionForWorkspace(input.workspaceId);
|
|
83
|
+
const periodStart = currentSubscription &&
|
|
84
|
+
(currentSubscription.status === "active" || currentSubscription.status === "trialing") &&
|
|
85
|
+
new Date(currentSubscription.currentPeriodEnd).getTime() > now.getTime()
|
|
86
|
+
? new Date(currentSubscription.currentPeriodEnd)
|
|
87
|
+
: now;
|
|
88
|
+
const periodStartIso = periodStart.toISOString();
|
|
89
|
+
const periodEnd = new Date(periodStart.getTime() + input.days * 24 * 60 * 60 * 1000);
|
|
90
|
+
const subscriptionId = currentSubscription?.id ?? randomUUID();
|
|
91
|
+
this.db
|
|
92
|
+
.prepare(`INSERT INTO subscriptions (
|
|
93
|
+
id,
|
|
94
|
+
workspace_id,
|
|
95
|
+
plan_id,
|
|
96
|
+
status,
|
|
97
|
+
current_period_start,
|
|
98
|
+
current_period_end,
|
|
99
|
+
cancel_at_period_end,
|
|
100
|
+
created_at,
|
|
101
|
+
updated_at
|
|
102
|
+
)
|
|
103
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
104
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
105
|
+
plan_id = excluded.plan_id,
|
|
106
|
+
status = excluded.status,
|
|
107
|
+
current_period_start = excluded.current_period_start,
|
|
108
|
+
current_period_end = excluded.current_period_end,
|
|
109
|
+
updated_at = excluded.updated_at`)
|
|
110
|
+
.run(subscriptionId, input.workspaceId, plan.id, plan.id === "trial" ? "trialing" : "active", periodStartIso, periodEnd.toISOString(), currentSubscription?.createdAt ?? nowIso, nowIso);
|
|
111
|
+
const entitlementId = randomUUID();
|
|
112
|
+
this.db
|
|
113
|
+
.prepare(`INSERT INTO entitlements (
|
|
114
|
+
id,
|
|
115
|
+
workspace_id,
|
|
116
|
+
subscription_id,
|
|
117
|
+
monthly_token_limit,
|
|
118
|
+
remaining_tokens,
|
|
119
|
+
allowed_models_json,
|
|
120
|
+
max_api_keys,
|
|
121
|
+
valid_from,
|
|
122
|
+
valid_until,
|
|
123
|
+
status,
|
|
124
|
+
created_at,
|
|
125
|
+
updated_at
|
|
126
|
+
)
|
|
127
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)`)
|
|
128
|
+
.run(entitlementId, input.workspaceId, subscriptionId, plan.monthlyTokenLimit, plan.monthlyTokenLimit, JSON.stringify(plan.allowedModels), plan.maxApiKeys, periodStartIso, periodEnd.toISOString(), nowIso, nowIso);
|
|
129
|
+
return {
|
|
130
|
+
subscription: this.getLatestSubscriptionForWorkspace(input.workspaceId),
|
|
131
|
+
entitlement: this.getLatestEntitlementForWorkspace(input.workspaceId),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
getLatestSubscriptionForWorkspace(workspaceId) {
|
|
135
|
+
const row = this.db
|
|
136
|
+
.prepare(`SELECT * FROM subscriptions
|
|
137
|
+
WHERE workspace_id = ?
|
|
138
|
+
ORDER BY current_period_end DESC, updated_at DESC
|
|
139
|
+
LIMIT 1`)
|
|
140
|
+
.get(workspaceId);
|
|
141
|
+
return row ? mapSubscriptionRow(row) : undefined;
|
|
142
|
+
}
|
|
143
|
+
getActiveEntitlementForWorkspace(workspaceId, now = new Date()) {
|
|
144
|
+
const nowIso = now.toISOString();
|
|
145
|
+
const row = this.db
|
|
146
|
+
.prepare(`SELECT * FROM entitlements
|
|
147
|
+
WHERE workspace_id = ?
|
|
148
|
+
AND status = 'active'
|
|
149
|
+
AND valid_from <= ?
|
|
150
|
+
AND valid_until >= ?
|
|
151
|
+
ORDER BY valid_until DESC, updated_at DESC
|
|
152
|
+
LIMIT 1`)
|
|
153
|
+
.get(workspaceId, nowIso, nowIso);
|
|
154
|
+
return row ? mapEntitlementRow(row) : undefined;
|
|
155
|
+
}
|
|
156
|
+
getActiveEntitlementLotsForWorkspace(workspaceId, now = new Date()) {
|
|
157
|
+
const nowIso = now.toISOString();
|
|
158
|
+
const rows = this.db
|
|
159
|
+
.prepare(`SELECT * FROM entitlements
|
|
160
|
+
WHERE workspace_id = ?
|
|
161
|
+
AND status = 'active'
|
|
162
|
+
AND valid_from <= ?
|
|
163
|
+
AND valid_until >= ?
|
|
164
|
+
ORDER BY valid_until ASC, created_at ASC`)
|
|
165
|
+
.all(workspaceId, nowIso, nowIso);
|
|
166
|
+
return rows.map((row) => {
|
|
167
|
+
const entitlement = mapEntitlementRow(row);
|
|
168
|
+
const usage = this.getEntitlementUsage(entitlement.id) ?? buildEmptyUsage(entitlement.id, entitlement.workspaceId);
|
|
169
|
+
return {
|
|
170
|
+
entitlement,
|
|
171
|
+
usage,
|
|
172
|
+
remainingTokens: Math.max(0, entitlement.monthlyTokenLimit - usage.totalTokens),
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
getUsableActiveEntitlementForWorkspace(workspaceId, now = new Date()) {
|
|
177
|
+
return this.getActiveEntitlementLotsForWorkspace(workspaceId, now)
|
|
178
|
+
.find((lot) => lot.remainingTokens > 0)
|
|
179
|
+
?.entitlement;
|
|
180
|
+
}
|
|
181
|
+
consumeWorkspaceUsage(input) {
|
|
182
|
+
const totalTokens = Math.max(0, Math.trunc(input.totalTokens));
|
|
183
|
+
if (totalTokens <= 0) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const now = input.now ?? new Date();
|
|
187
|
+
let remaining = totalTokens;
|
|
188
|
+
let remainingInput = Math.max(0, Math.trunc(input.inputTokens));
|
|
189
|
+
let remainingOutput = Math.max(0, Math.trunc(input.outputTokens));
|
|
190
|
+
const updates = [];
|
|
191
|
+
const lots = this.getActiveEntitlementLotsForWorkspace(input.workspaceId, now)
|
|
192
|
+
.filter((lot) => lot.remainingTokens > 0)
|
|
193
|
+
.sort((left, right) => new Date(left.entitlement.validUntil).getTime() - new Date(right.entitlement.validUntil).getTime());
|
|
194
|
+
const available = lots.reduce((sum, lot) => sum + lot.remainingTokens, 0);
|
|
195
|
+
if (available < totalTokens) {
|
|
196
|
+
throw new Error(`Token lot quota exceeded for workspace ${input.workspaceId}.`);
|
|
197
|
+
}
|
|
198
|
+
for (let index = 0; index < lots.length; index += 1) {
|
|
199
|
+
const lot = lots[index];
|
|
200
|
+
if (remaining <= 0) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
const consumed = Math.min(remaining, lot.remainingTokens);
|
|
204
|
+
const isLast = index === lots.length - 1 || remaining === consumed;
|
|
205
|
+
const inputTokens = isLast ? remainingInput : Math.min(remainingInput, Math.round((remainingInput * consumed) / remaining));
|
|
206
|
+
const outputTokens = isLast ? remainingOutput : Math.min(remainingOutput, Math.round((remainingOutput * consumed) / remaining));
|
|
207
|
+
remainingInput -= inputTokens;
|
|
208
|
+
remainingOutput -= outputTokens;
|
|
209
|
+
updates.push(this.incrementEntitlementUsage({
|
|
210
|
+
entitlementId: lot.entitlement.id,
|
|
211
|
+
workspaceId: input.workspaceId,
|
|
212
|
+
customerApiKeyId: input.customerApiKeyId,
|
|
213
|
+
inputTokens,
|
|
214
|
+
outputTokens,
|
|
215
|
+
totalTokens: consumed,
|
|
216
|
+
now,
|
|
217
|
+
}));
|
|
218
|
+
remaining -= consumed;
|
|
219
|
+
}
|
|
220
|
+
return updates;
|
|
221
|
+
}
|
|
222
|
+
getLatestEntitlementForWorkspace(workspaceId) {
|
|
223
|
+
const row = this.db
|
|
224
|
+
.prepare(`SELECT * FROM entitlements
|
|
225
|
+
WHERE workspace_id = ?
|
|
226
|
+
ORDER BY valid_until DESC, updated_at DESC
|
|
227
|
+
LIMIT 1`)
|
|
228
|
+
.get(workspaceId);
|
|
229
|
+
return row ? mapEntitlementRow(row) : undefined;
|
|
230
|
+
}
|
|
231
|
+
createTokenTopUpLot(input) {
|
|
232
|
+
const tokenDelta = Math.max(0, Math.trunc(input.tokenDelta));
|
|
233
|
+
const days = Math.max(1, Math.trunc(input.days));
|
|
234
|
+
if (tokenDelta <= 0) {
|
|
235
|
+
throw new Error("Token top-up amount must be positive.");
|
|
236
|
+
}
|
|
237
|
+
const now = input.now ?? new Date();
|
|
238
|
+
const validFrom = now.toISOString();
|
|
239
|
+
const validUntil = new Date(now.getTime() + days * 24 * 60 * 60 * 1000).toISOString();
|
|
240
|
+
const id = randomUUID();
|
|
241
|
+
const latest = this.getLatestEntitlementForWorkspace(input.workspaceId);
|
|
242
|
+
this.db
|
|
243
|
+
.prepare(`INSERT INTO entitlements (
|
|
244
|
+
id,
|
|
245
|
+
workspace_id,
|
|
246
|
+
subscription_id,
|
|
247
|
+
monthly_token_limit,
|
|
248
|
+
remaining_tokens,
|
|
249
|
+
allowed_models_json,
|
|
250
|
+
max_api_keys,
|
|
251
|
+
valid_from,
|
|
252
|
+
valid_until,
|
|
253
|
+
status,
|
|
254
|
+
created_at,
|
|
255
|
+
updated_at
|
|
256
|
+
)
|
|
257
|
+
VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, 'active', ?, ?)`)
|
|
258
|
+
.run(id, input.workspaceId, tokenDelta, tokenDelta, JSON.stringify(input.allowedModels ?? latest?.allowedModels ?? []), input.maxApiKeys ?? latest?.maxApiKeys ?? 1, validFrom, validUntil, validFrom, validFrom);
|
|
259
|
+
return this.getEntitlementById(id);
|
|
260
|
+
}
|
|
261
|
+
getEntitlementById(id) {
|
|
262
|
+
const row = this.db
|
|
263
|
+
.prepare(`SELECT * FROM entitlements
|
|
264
|
+
WHERE id = ?`)
|
|
265
|
+
.get(id);
|
|
266
|
+
return row ? mapEntitlementRow(row) : undefined;
|
|
267
|
+
}
|
|
268
|
+
getEntitlementUsage(entitlementId) {
|
|
269
|
+
const row = this.db
|
|
270
|
+
.prepare(`SELECT * FROM entitlement_usage
|
|
271
|
+
WHERE entitlement_id = ?`)
|
|
272
|
+
.get(entitlementId);
|
|
273
|
+
return row ? mapEntitlementUsageRow(row) : undefined;
|
|
274
|
+
}
|
|
275
|
+
incrementEntitlementUsage(input) {
|
|
276
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
277
|
+
this.db
|
|
278
|
+
.prepare(`INSERT INTO entitlement_usage (
|
|
279
|
+
entitlement_id,
|
|
280
|
+
workspace_id,
|
|
281
|
+
customer_api_key_id,
|
|
282
|
+
input_tokens,
|
|
283
|
+
output_tokens,
|
|
284
|
+
total_tokens,
|
|
285
|
+
created_at,
|
|
286
|
+
updated_at
|
|
287
|
+
)
|
|
288
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
289
|
+
ON CONFLICT(entitlement_id) DO UPDATE SET
|
|
290
|
+
workspace_id = excluded.workspace_id,
|
|
291
|
+
customer_api_key_id = COALESCE(excluded.customer_api_key_id, entitlement_usage.customer_api_key_id),
|
|
292
|
+
input_tokens = entitlement_usage.input_tokens + excluded.input_tokens,
|
|
293
|
+
output_tokens = entitlement_usage.output_tokens + excluded.output_tokens,
|
|
294
|
+
total_tokens = entitlement_usage.total_tokens + excluded.total_tokens,
|
|
295
|
+
updated_at = excluded.updated_at`)
|
|
296
|
+
.run(input.entitlementId, input.workspaceId, input.customerApiKeyId ?? null, Math.max(0, Math.trunc(input.inputTokens)), Math.max(0, Math.trunc(input.outputTokens)), Math.max(0, Math.trunc(input.totalTokens)), now, now);
|
|
297
|
+
return this.getEntitlementUsage(input.entitlementId);
|
|
298
|
+
}
|
|
299
|
+
createRenewalRequest(input) {
|
|
300
|
+
const kind = input.kind ?? "renewal";
|
|
301
|
+
const existing = this.getOpenRenewalRequestForWorkspace(input.workspaceId, kind);
|
|
302
|
+
if (existing) {
|
|
303
|
+
return { request: existing, created: false };
|
|
304
|
+
}
|
|
305
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
306
|
+
const id = randomUUID();
|
|
307
|
+
this.db
|
|
308
|
+
.prepare(`INSERT INTO renewal_requests (
|
|
309
|
+
id,
|
|
310
|
+
workspace_id,
|
|
311
|
+
telegram_user_id,
|
|
312
|
+
request_kind,
|
|
313
|
+
requested_plan_id,
|
|
314
|
+
requested_days,
|
|
315
|
+
requested_token_delta,
|
|
316
|
+
requested_token_lot_days,
|
|
317
|
+
price_vnd,
|
|
318
|
+
status,
|
|
319
|
+
requested_at,
|
|
320
|
+
created_at,
|
|
321
|
+
updated_at
|
|
322
|
+
)
|
|
323
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, ?)`)
|
|
324
|
+
.run(id, input.workspaceId, input.telegramUserId, kind, input.requestedPlanId ?? null, input.requestedDays ?? null, input.requestedTokenDelta ?? null, input.requestedTokenLotDays ?? null, input.priceVnd ?? null, now, now, now);
|
|
325
|
+
return {
|
|
326
|
+
request: this.getRenewalRequest(id),
|
|
327
|
+
created: true,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
getRenewalRequest(id) {
|
|
331
|
+
const row = this.db
|
|
332
|
+
.prepare(`SELECT * FROM renewal_requests
|
|
333
|
+
WHERE id = ?`)
|
|
334
|
+
.get(id);
|
|
335
|
+
return row ? mapRenewalRequestRow(row) : undefined;
|
|
336
|
+
}
|
|
337
|
+
getOpenRenewalRequestForWorkspace(workspaceId, kind = "renewal") {
|
|
338
|
+
const row = this.db
|
|
339
|
+
.prepare(`SELECT * FROM renewal_requests
|
|
340
|
+
WHERE workspace_id = ?
|
|
341
|
+
AND request_kind = ?
|
|
342
|
+
AND status IN ('open', 'payment_confirmed')
|
|
343
|
+
ORDER BY created_at DESC
|
|
344
|
+
LIMIT 1`)
|
|
345
|
+
.get(workspaceId, kind);
|
|
346
|
+
return row ? mapRenewalRequestRow(row) : undefined;
|
|
347
|
+
}
|
|
348
|
+
listRenewalRequests(status) {
|
|
349
|
+
const rows = status
|
|
350
|
+
? this.db
|
|
351
|
+
.prepare(`SELECT * FROM renewal_requests
|
|
352
|
+
WHERE status = ?
|
|
353
|
+
ORDER BY created_at DESC`)
|
|
354
|
+
.all(status)
|
|
355
|
+
: this.db
|
|
356
|
+
.prepare(`SELECT * FROM renewal_requests
|
|
357
|
+
ORDER BY created_at DESC`)
|
|
358
|
+
.all();
|
|
359
|
+
return rows.map(mapRenewalRequestRow);
|
|
360
|
+
}
|
|
361
|
+
confirmRenewalPayment(input) {
|
|
362
|
+
return this.setRenewalRequestStatus({
|
|
363
|
+
id: input.id,
|
|
364
|
+
status: "payment_confirmed",
|
|
365
|
+
resolution: input.resolution ?? "payment_confirmed_manual",
|
|
366
|
+
expectedStatus: input.expectedStatus ?? "open",
|
|
367
|
+
now: input.now,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
closeRenewalRequest(input) {
|
|
371
|
+
return this.setRenewalRequestStatus({
|
|
372
|
+
id: input.id,
|
|
373
|
+
status: "closed",
|
|
374
|
+
resolution: input.resolution,
|
|
375
|
+
expectedStatus: input.expectedStatus,
|
|
376
|
+
now: input.now,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
approveRenewalRequest(input) {
|
|
380
|
+
return this.setRenewalRequestStatus({
|
|
381
|
+
id: input.id,
|
|
382
|
+
status: "approved",
|
|
383
|
+
resolution: input.resolution,
|
|
384
|
+
approvedPlanId: input.approvedPlanId,
|
|
385
|
+
approvedDays: input.approvedDays,
|
|
386
|
+
expectedStatus: input.expectedStatus ?? "payment_confirmed",
|
|
387
|
+
now: input.now,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
approveTokenTopUpRequest(input) {
|
|
391
|
+
return this.setRenewalRequestStatus({
|
|
392
|
+
id: input.id,
|
|
393
|
+
status: "approved",
|
|
394
|
+
resolution: input.resolution,
|
|
395
|
+
approvedTokenDelta: input.approvedTokenDelta,
|
|
396
|
+
approvedTokenLotDays: input.approvedTokenLotDays,
|
|
397
|
+
expectedStatus: input.expectedStatus ?? "payment_confirmed",
|
|
398
|
+
now: input.now,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
approveTokenTopUpAtomically(input) {
|
|
402
|
+
return this.db.transaction(() => {
|
|
403
|
+
const transitioned = this.setRenewalRequestStatus({
|
|
404
|
+
id: input.requestId,
|
|
405
|
+
status: "approved",
|
|
406
|
+
resolution: input.resolution,
|
|
407
|
+
approvedTokenDelta: input.tokenDelta,
|
|
408
|
+
approvedTokenLotDays: input.lotDays,
|
|
409
|
+
expectedStatus: "payment_confirmed",
|
|
410
|
+
now: input.now,
|
|
411
|
+
});
|
|
412
|
+
if (!transitioned) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
const lot = this.createTokenTopUpLot({
|
|
416
|
+
workspaceId: input.workspaceId,
|
|
417
|
+
tokenDelta: input.tokenDelta,
|
|
418
|
+
days: input.lotDays,
|
|
419
|
+
allowedModels: input.allowedModels,
|
|
420
|
+
maxApiKeys: input.maxApiKeys,
|
|
421
|
+
sourceRequestId: input.requestId,
|
|
422
|
+
now: input.now,
|
|
423
|
+
});
|
|
424
|
+
return { request: transitioned, lot };
|
|
425
|
+
})();
|
|
426
|
+
}
|
|
427
|
+
setRenewalRequestStatus(input) {
|
|
428
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
429
|
+
const query = input.expectedStatus
|
|
430
|
+
? `UPDATE renewal_requests
|
|
431
|
+
SET status = ?,
|
|
432
|
+
resolution = COALESCE(?, resolution),
|
|
433
|
+
approved_plan_id = COALESCE(?, approved_plan_id),
|
|
434
|
+
approved_days = COALESCE(?, approved_days),
|
|
435
|
+
approved_token_delta = COALESCE(?, approved_token_delta),
|
|
436
|
+
approved_token_lot_days = COALESCE(?, approved_token_lot_days),
|
|
437
|
+
closed_at = CASE WHEN ? IN ('approved', 'closed') THEN ? ELSE closed_at END,
|
|
438
|
+
updated_at = ?
|
|
439
|
+
WHERE id = ?
|
|
440
|
+
AND status = ?`
|
|
441
|
+
: `UPDATE renewal_requests
|
|
442
|
+
SET status = ?,
|
|
443
|
+
resolution = COALESCE(?, resolution),
|
|
444
|
+
approved_plan_id = COALESCE(?, approved_plan_id),
|
|
445
|
+
approved_days = COALESCE(?, approved_days),
|
|
446
|
+
approved_token_delta = COALESCE(?, approved_token_delta),
|
|
447
|
+
approved_token_lot_days = COALESCE(?, approved_token_lot_days),
|
|
448
|
+
closed_at = CASE WHEN ? IN ('approved', 'closed') THEN ? ELSE closed_at END,
|
|
449
|
+
updated_at = ?
|
|
450
|
+
WHERE id = ?`;
|
|
451
|
+
const params = [
|
|
452
|
+
input.status,
|
|
453
|
+
input.resolution ?? null,
|
|
454
|
+
input.approvedPlanId ?? null,
|
|
455
|
+
input.approvedDays ?? null,
|
|
456
|
+
input.approvedTokenDelta ?? null,
|
|
457
|
+
input.approvedTokenLotDays ?? null,
|
|
458
|
+
input.status,
|
|
459
|
+
now,
|
|
460
|
+
now,
|
|
461
|
+
input.id,
|
|
462
|
+
];
|
|
463
|
+
if (input.expectedStatus) {
|
|
464
|
+
params.push(input.expectedStatus);
|
|
465
|
+
}
|
|
466
|
+
const result = this.db.prepare(query).run(...params);
|
|
467
|
+
if (result.changes === 0) {
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
return this.getRenewalRequest(input.id);
|
|
471
|
+
}
|
|
472
|
+
expireEntitlements(now = new Date()) {
|
|
473
|
+
const nowIso = now.toISOString();
|
|
474
|
+
const result = this.db
|
|
475
|
+
.prepare(`UPDATE entitlements
|
|
476
|
+
SET status = 'expired',
|
|
477
|
+
updated_at = ?
|
|
478
|
+
WHERE status = 'active'
|
|
479
|
+
AND valid_until < ?`)
|
|
480
|
+
.run(nowIso, nowIso);
|
|
481
|
+
this.db
|
|
482
|
+
.prepare(`UPDATE subscriptions
|
|
483
|
+
SET status = 'expired',
|
|
484
|
+
updated_at = ?
|
|
485
|
+
WHERE status IN ('active', 'trialing')
|
|
486
|
+
AND current_period_end < ?`)
|
|
487
|
+
.run(nowIso, nowIso);
|
|
488
|
+
return result.changes;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function ensureBillingSchema(db) {
|
|
492
|
+
db.exec(`
|
|
493
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
494
|
+
id TEXT PRIMARY KEY,
|
|
495
|
+
name TEXT NOT NULL,
|
|
496
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
497
|
+
price_cents INTEGER NOT NULL,
|
|
498
|
+
currency TEXT NOT NULL DEFAULT 'USD',
|
|
499
|
+
billing_interval TEXT NOT NULL,
|
|
500
|
+
monthly_token_limit INTEGER NOT NULL,
|
|
501
|
+
max_api_keys INTEGER NOT NULL DEFAULT 1,
|
|
502
|
+
allowed_models_json TEXT NOT NULL DEFAULT '[]',
|
|
503
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
504
|
+
created_at TEXT NOT NULL,
|
|
505
|
+
updated_at TEXT NOT NULL
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
CREATE TABLE IF NOT EXISTS subscriptions (
|
|
509
|
+
id TEXT PRIMARY KEY,
|
|
510
|
+
workspace_id TEXT NOT NULL,
|
|
511
|
+
plan_id TEXT NOT NULL,
|
|
512
|
+
status TEXT NOT NULL,
|
|
513
|
+
current_period_start TEXT NOT NULL,
|
|
514
|
+
current_period_end TEXT NOT NULL,
|
|
515
|
+
cancel_at_period_end INTEGER NOT NULL DEFAULT 0,
|
|
516
|
+
provider TEXT,
|
|
517
|
+
provider_customer_id TEXT,
|
|
518
|
+
provider_subscription_id TEXT,
|
|
519
|
+
created_at TEXT NOT NULL,
|
|
520
|
+
updated_at TEXT NOT NULL
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
CREATE TABLE IF NOT EXISTS entitlements (
|
|
524
|
+
id TEXT PRIMARY KEY,
|
|
525
|
+
workspace_id TEXT NOT NULL,
|
|
526
|
+
subscription_id TEXT,
|
|
527
|
+
monthly_token_limit INTEGER NOT NULL,
|
|
528
|
+
remaining_tokens INTEGER,
|
|
529
|
+
allowed_models_json TEXT NOT NULL DEFAULT '[]',
|
|
530
|
+
max_api_keys INTEGER NOT NULL DEFAULT 1,
|
|
531
|
+
valid_from TEXT NOT NULL,
|
|
532
|
+
valid_until TEXT NOT NULL,
|
|
533
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
534
|
+
created_at TEXT NOT NULL,
|
|
535
|
+
updated_at TEXT NOT NULL
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
CREATE INDEX IF NOT EXISTS idx_entitlements_workspace_valid_until
|
|
539
|
+
ON entitlements(workspace_id, status, valid_until, created_at);
|
|
540
|
+
|
|
541
|
+
CREATE TABLE IF NOT EXISTS entitlement_usage (
|
|
542
|
+
entitlement_id TEXT PRIMARY KEY,
|
|
543
|
+
workspace_id TEXT NOT NULL,
|
|
544
|
+
customer_api_key_id TEXT,
|
|
545
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
546
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
547
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
548
|
+
created_at TEXT NOT NULL,
|
|
549
|
+
updated_at TEXT NOT NULL,
|
|
550
|
+
FOREIGN KEY (entitlement_id) REFERENCES entitlements(id) ON DELETE CASCADE
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
CREATE TABLE IF NOT EXISTS renewal_requests (
|
|
554
|
+
id TEXT PRIMARY KEY,
|
|
555
|
+
workspace_id TEXT NOT NULL,
|
|
556
|
+
telegram_user_id TEXT NOT NULL,
|
|
557
|
+
request_kind TEXT NOT NULL DEFAULT 'renewal',
|
|
558
|
+
requested_plan_id TEXT,
|
|
559
|
+
requested_days INTEGER,
|
|
560
|
+
requested_token_delta INTEGER,
|
|
561
|
+
requested_token_lot_days INTEGER,
|
|
562
|
+
price_vnd INTEGER,
|
|
563
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
564
|
+
resolution TEXT,
|
|
565
|
+
approved_plan_id TEXT,
|
|
566
|
+
approved_days INTEGER,
|
|
567
|
+
approved_token_delta INTEGER,
|
|
568
|
+
approved_token_lot_days INTEGER,
|
|
569
|
+
requested_at TEXT NOT NULL,
|
|
570
|
+
closed_at TEXT,
|
|
571
|
+
created_at TEXT NOT NULL,
|
|
572
|
+
updated_at TEXT NOT NULL
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
CREATE INDEX IF NOT EXISTS idx_subscriptions_workspace_status
|
|
576
|
+
ON subscriptions(workspace_id, status, current_period_end);
|
|
577
|
+
|
|
578
|
+
CREATE INDEX IF NOT EXISTS idx_entitlements_workspace_status
|
|
579
|
+
ON entitlements(workspace_id, status, valid_until);
|
|
580
|
+
|
|
581
|
+
CREATE INDEX IF NOT EXISTS idx_entitlement_usage_workspace
|
|
582
|
+
ON entitlement_usage(workspace_id, updated_at);
|
|
583
|
+
|
|
584
|
+
CREATE INDEX IF NOT EXISTS idx_renewal_requests_workspace_status
|
|
585
|
+
ON renewal_requests(workspace_id, status, created_at);
|
|
586
|
+
`);
|
|
587
|
+
ensureBillingColumn(db, "renewal_requests", "request_kind", "TEXT NOT NULL DEFAULT 'renewal'");
|
|
588
|
+
ensureBillingColumn(db, "renewal_requests", "requested_token_delta", "INTEGER");
|
|
589
|
+
ensureBillingColumn(db, "renewal_requests", "requested_token_lot_days", "INTEGER");
|
|
590
|
+
ensureBillingColumn(db, "renewal_requests", "price_vnd", "INTEGER");
|
|
591
|
+
ensureBillingColumn(db, "renewal_requests", "approved_token_delta", "INTEGER");
|
|
592
|
+
ensureBillingColumn(db, "renewal_requests", "approved_token_lot_days", "INTEGER");
|
|
593
|
+
}
|
|
594
|
+
function mapPlanRow(row) {
|
|
595
|
+
return {
|
|
596
|
+
id: row.id,
|
|
597
|
+
name: row.name,
|
|
598
|
+
status: row.status === "archived" ? "archived" : "active",
|
|
599
|
+
priceCents: row.price_cents,
|
|
600
|
+
currency: row.currency,
|
|
601
|
+
billingInterval: row.billing_interval === "year" || row.billing_interval === "one_time"
|
|
602
|
+
? row.billing_interval
|
|
603
|
+
: "month",
|
|
604
|
+
monthlyTokenLimit: row.monthly_token_limit,
|
|
605
|
+
maxApiKeys: row.max_api_keys,
|
|
606
|
+
allowedModels: parseStringArray(row.allowed_models_json),
|
|
607
|
+
metadataJson: row.metadata_json,
|
|
608
|
+
createdAt: row.created_at,
|
|
609
|
+
updatedAt: row.updated_at,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function mapSubscriptionRow(row) {
|
|
613
|
+
return {
|
|
614
|
+
id: row.id,
|
|
615
|
+
workspaceId: row.workspace_id,
|
|
616
|
+
planId: row.plan_id,
|
|
617
|
+
status: row.status === "trialing" ||
|
|
618
|
+
row.status === "active" ||
|
|
619
|
+
row.status === "past_due" ||
|
|
620
|
+
row.status === "canceled" ||
|
|
621
|
+
row.status === "expired"
|
|
622
|
+
? row.status
|
|
623
|
+
: "expired",
|
|
624
|
+
currentPeriodStart: row.current_period_start,
|
|
625
|
+
currentPeriodEnd: row.current_period_end,
|
|
626
|
+
cancelAtPeriodEnd: row.cancel_at_period_end !== 0,
|
|
627
|
+
provider: row.provider ?? undefined,
|
|
628
|
+
providerCustomerId: row.provider_customer_id ?? undefined,
|
|
629
|
+
providerSubscriptionId: row.provider_subscription_id ?? undefined,
|
|
630
|
+
createdAt: row.created_at,
|
|
631
|
+
updatedAt: row.updated_at,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function mapEntitlementRow(row) {
|
|
635
|
+
return {
|
|
636
|
+
id: row.id,
|
|
637
|
+
workspaceId: row.workspace_id,
|
|
638
|
+
subscriptionId: row.subscription_id ?? undefined,
|
|
639
|
+
monthlyTokenLimit: row.monthly_token_limit,
|
|
640
|
+
remainingTokens: row.remaining_tokens ?? undefined,
|
|
641
|
+
allowedModels: parseStringArray(row.allowed_models_json),
|
|
642
|
+
maxApiKeys: row.max_api_keys,
|
|
643
|
+
validFrom: row.valid_from,
|
|
644
|
+
validUntil: row.valid_until,
|
|
645
|
+
status: row.status === "expired" || row.status === "suspended" ? row.status : "active",
|
|
646
|
+
createdAt: row.created_at,
|
|
647
|
+
updatedAt: row.updated_at,
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function mapEntitlementUsageRow(row) {
|
|
651
|
+
return {
|
|
652
|
+
entitlementId: row.entitlement_id,
|
|
653
|
+
workspaceId: row.workspace_id,
|
|
654
|
+
customerApiKeyId: row.customer_api_key_id ?? undefined,
|
|
655
|
+
inputTokens: Math.max(0, Number(row.input_tokens ?? 0)),
|
|
656
|
+
outputTokens: Math.max(0, Number(row.output_tokens ?? 0)),
|
|
657
|
+
totalTokens: Math.max(0, Number(row.total_tokens ?? 0)),
|
|
658
|
+
createdAt: row.created_at,
|
|
659
|
+
updatedAt: row.updated_at,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function mapRenewalRequestRow(row) {
|
|
663
|
+
return {
|
|
664
|
+
id: row.id,
|
|
665
|
+
workspaceId: row.workspace_id,
|
|
666
|
+
telegramUserId: row.telegram_user_id,
|
|
667
|
+
kind: row.request_kind === "token_topup" ? "token_topup" : "renewal",
|
|
668
|
+
requestedPlanId: row.requested_plan_id ?? undefined,
|
|
669
|
+
requestedDays: row.requested_days ?? undefined,
|
|
670
|
+
requestedTokenDelta: row.requested_token_delta ?? undefined,
|
|
671
|
+
requestedTokenLotDays: row.requested_token_lot_days ?? undefined,
|
|
672
|
+
priceVnd: row.price_vnd ?? undefined,
|
|
673
|
+
status: row.status === "payment_confirmed" || row.status === "approved" || row.status === "closed"
|
|
674
|
+
? row.status
|
|
675
|
+
: "open",
|
|
676
|
+
resolution: row.resolution ?? undefined,
|
|
677
|
+
approvedPlanId: row.approved_plan_id ?? undefined,
|
|
678
|
+
approvedDays: row.approved_days ?? undefined,
|
|
679
|
+
approvedTokenDelta: row.approved_token_delta ?? undefined,
|
|
680
|
+
approvedTokenLotDays: row.approved_token_lot_days ?? undefined,
|
|
681
|
+
requestedAt: row.requested_at,
|
|
682
|
+
closedAt: row.closed_at ?? undefined,
|
|
683
|
+
createdAt: row.created_at,
|
|
684
|
+
updatedAt: row.updated_at,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function buildEmptyUsage(entitlementId, workspaceId) {
|
|
688
|
+
const timestamp = new Date(0).toISOString();
|
|
689
|
+
return {
|
|
690
|
+
entitlementId: entitlementId ?? "none",
|
|
691
|
+
workspaceId: workspaceId ?? "none",
|
|
692
|
+
inputTokens: 0,
|
|
693
|
+
outputTokens: 0,
|
|
694
|
+
totalTokens: 0,
|
|
695
|
+
createdAt: timestamp,
|
|
696
|
+
updatedAt: timestamp,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function ensureBillingColumn(db, tableName, columnName, columnDefinition) {
|
|
700
|
+
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
701
|
+
if (columns.some((column) => column.name === columnName)) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnDefinition}`);
|
|
705
|
+
}
|
|
706
|
+
function parseStringArray(raw) {
|
|
707
|
+
try {
|
|
708
|
+
const parsed = JSON.parse(raw);
|
|
709
|
+
return Array.isArray(parsed)
|
|
710
|
+
? parsed.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
711
|
+
: [];
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
return [];
|
|
715
|
+
}
|
|
716
|
+
}
|