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
|
@@ -0,0 +1,1369 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { renderAdminScreen } from "../admin-actions.js";
|
|
3
|
+
import { isAdmin } from "../auth.js";
|
|
4
|
+
import { buildCustomerActionKeyboard } from "../customer-actions.js";
|
|
5
|
+
import { formatDateTime, formatField, formatMessage, formatRawField, formatSection } from "../message-format.js";
|
|
6
|
+
import { URL } from "node:url";
|
|
7
|
+
import { readCustomerBillingOverview } from "../customer-billing.js";
|
|
8
|
+
import { answerCallbackQuerySafely, replyOrEditMessage } from "../callbacks.js";
|
|
9
|
+
import { renewCustomerAccess } from "../grants.js";
|
|
10
|
+
import { getProxyErrorMessage } from "../actions.js";
|
|
11
|
+
import { sendCustomerCodexSetup } from "../codex-config-delivery.js";
|
|
12
|
+
import { buildTelegramSessionScope, } from "../sessions.js";
|
|
13
|
+
const DEFAULT_PURCHASE_AMOUNT_VND = 5_000;
|
|
14
|
+
const DEFAULT_PURCHASE_TOKEN_LIMIT = 10_000_000;
|
|
15
|
+
const DEFAULT_PURCHASE_PLAN_ID = "basic";
|
|
16
|
+
const DEFAULT_PURCHASE_DAYS = 1;
|
|
17
|
+
const DEFAULT_TOKEN_TOPUP_LOT_DAYS = 1;
|
|
18
|
+
const RENEWAL_CALLBACK_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
19
|
+
function resolveRequestPriceVnd(request, billing) {
|
|
20
|
+
if (typeof request.priceVnd === "number" && request.priceVnd > 0) {
|
|
21
|
+
return request.priceVnd;
|
|
22
|
+
}
|
|
23
|
+
if (request.kind === "renewal") {
|
|
24
|
+
const planId = request.requestedPlanId ?? DEFAULT_PURCHASE_PLAN_ID;
|
|
25
|
+
const plan = billing.getPlan(planId);
|
|
26
|
+
if (plan && plan.currency === "VND" && plan.priceCents > 0) {
|
|
27
|
+
return plan.priceCents;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return DEFAULT_PURCHASE_AMOUNT_VND;
|
|
31
|
+
}
|
|
32
|
+
export function registerRenewCommand(bot, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog) {
|
|
33
|
+
bot.command("renew", async (ctx) => {
|
|
34
|
+
const rawArgs = ctx.match?.toString() || "";
|
|
35
|
+
if (isAdmin(ctx, deps.config)) {
|
|
36
|
+
await handleAdminRenewCommand(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog, rawArgs);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await handleCustomerRenewCommand(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog, rawArgs);
|
|
40
|
+
});
|
|
41
|
+
bot.callbackQuery("v1:renew:open", async (ctx) => {
|
|
42
|
+
if (ctx.chat?.type !== "private") {
|
|
43
|
+
await answerCallbackQuerySafely(ctx, { text: "Open a private chat with the bot.", show_alert: true });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
await answerCallbackQuerySafely(ctx, { text: "24h renewal request" });
|
|
47
|
+
await handleCustomerRenewCommand(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog, "");
|
|
48
|
+
});
|
|
49
|
+
bot.callbackQuery("v1:topup:open", async (ctx) => {
|
|
50
|
+
if (ctx.chat?.type !== "private") {
|
|
51
|
+
await answerCallbackQuerySafely(ctx, { text: "Open a private chat with the bot.", show_alert: true });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await answerCallbackQuerySafely(ctx, { text: "Token top-up request" });
|
|
55
|
+
await handleCustomerRenewRequest(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog, {
|
|
56
|
+
kind: "token_topup",
|
|
57
|
+
tokenDelta: DEFAULT_PURCHASE_TOKEN_LIMIT,
|
|
58
|
+
lotDays: DEFAULT_TOKEN_TOPUP_LOT_DAYS,
|
|
59
|
+
priceVnd: DEFAULT_PURCHASE_AMOUNT_VND,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
bot.callbackQuery(/^v1:renew:(confirm-payment|approve|approve-rotate|approve-30|approve-90|approve-custom|close|view-customer|reject-reasons|reject|reject-custom|back):([A-Za-z0-9_-]+)$/, async (ctx) => {
|
|
63
|
+
if (!isAdmin(ctx, deps.config)) {
|
|
64
|
+
await answerCallbackQuerySafely(ctx, { text: "Admin only.", show_alert: true });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const action = normalizeRenewalAction(ctx.match[1]);
|
|
68
|
+
const token = ctx.match[2];
|
|
69
|
+
const callbackState = stateStore.readCallbackToken(token);
|
|
70
|
+
if (callbackState?.kind !== "renewal_request_action" || callbackState.action !== action) {
|
|
71
|
+
await answerCallbackQuerySafely(ctx, { text: "Action expired. Refresh /renew list.", show_alert: true });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (action === "confirm_payment") {
|
|
75
|
+
const confirmed = await confirmRenewalPayment(ctx, billing, auditLog, callbackState.requestId);
|
|
76
|
+
if (confirmed) {
|
|
77
|
+
const updatedRequest = billing.getRenewalRequest(callbackState.requestId);
|
|
78
|
+
if (updatedRequest) {
|
|
79
|
+
await renderRenewalPromptCard(ctx, stateStore, updatedRequest, confirmed.message, "main");
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
await updateRenewalReviewMessage(ctx, confirmed.message);
|
|
83
|
+
}
|
|
84
|
+
await answerCallbackQuerySafely(ctx, { text: "Payment marked as confirmed" });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await answerCallbackQuerySafely(ctx, { text: "Could not confirm payment.", show_alert: true });
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (action === "approve") {
|
|
92
|
+
const request = billing.getRenewalRequest(callbackState.requestId);
|
|
93
|
+
if (request?.status !== "payment_confirmed") {
|
|
94
|
+
await answerCallbackQuerySafely(ctx, { text: "Confirm payment first.", show_alert: true });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const approved = await approveRenewalRequest(ctx, deps, identities, workspaces, customerKeys, billing, auditLog, callbackState.requestId, undefined, undefined, false, true);
|
|
98
|
+
if (approved) {
|
|
99
|
+
await updateRenewalReviewMessage(ctx, approved.message);
|
|
100
|
+
await answerCallbackQuerySafely(ctx, { text: "Renewal approved" });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await answerCallbackQuerySafely(ctx, { text: "Renewal approval failed. Check the bot message.", show_alert: true });
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (action === "approve_rotate") {
|
|
108
|
+
const request = billing.getRenewalRequest(callbackState.requestId);
|
|
109
|
+
if (request?.status !== "payment_confirmed") {
|
|
110
|
+
await answerCallbackQuerySafely(ctx, { text: "Confirm payment first.", show_alert: true });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const approved = await approveRenewalRequest(ctx, deps, identities, workspaces, customerKeys, billing, auditLog, callbackState.requestId, undefined, undefined, true, true);
|
|
114
|
+
if (approved) {
|
|
115
|
+
await updateRenewalReviewMessage(ctx, approved.message);
|
|
116
|
+
await answerCallbackQuerySafely(ctx, { text: "Renewal approved and key rotated" });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
await answerCallbackQuerySafely(ctx, { text: "Renewal approval failed. Check the bot message.", show_alert: true });
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (action === "approve_override") {
|
|
124
|
+
const request = billing.getRenewalRequest(callbackState.requestId);
|
|
125
|
+
if (request?.status !== "payment_confirmed") {
|
|
126
|
+
await answerCallbackQuerySafely(ctx, { text: "Confirm payment first.", show_alert: true });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const approved = await approveRenewalRequest(ctx, deps, identities, workspaces, customerKeys, billing, auditLog, callbackState.requestId, undefined, callbackState.overrideDays, false, true);
|
|
130
|
+
if (approved) {
|
|
131
|
+
await updateRenewalReviewMessage(ctx, approved.message);
|
|
132
|
+
await answerCallbackQuerySafely(ctx, { text: `Renewal approved for ${callbackState.overrideDays} days` });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
await answerCallbackQuerySafely(ctx, { text: "Renewal approval failed. Check the bot message.", show_alert: true });
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (action === "prompt_custom_days") {
|
|
140
|
+
const request = billing.getRenewalRequest(callbackState.requestId);
|
|
141
|
+
if (request?.status !== "payment_confirmed") {
|
|
142
|
+
await answerCallbackQuerySafely(ctx, { text: "Confirm payment first.", show_alert: true });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const prepared = prepareRenewalAdminInput(ctx);
|
|
146
|
+
if (!prepared) {
|
|
147
|
+
await answerCallbackQuerySafely(ctx, { text: "This action only works from a message button.", show_alert: true });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
stateStore.set(buildTelegramSessionScope(prepared.chatId, prepared.userId), {
|
|
151
|
+
kind: "awaiting_renewal_custom_days",
|
|
152
|
+
requestId: callbackState.requestId,
|
|
153
|
+
sourceChatId: prepared.chatId,
|
|
154
|
+
sourceMessageId: prepared.messageId,
|
|
155
|
+
});
|
|
156
|
+
if (request) {
|
|
157
|
+
await renderRenewalPromptCard(ctx, stateStore, request, [
|
|
158
|
+
"Renewal review",
|
|
159
|
+
`request_id: ${callbackState.requestId}`,
|
|
160
|
+
"Awaiting custom approval days.",
|
|
161
|
+
"Send a positive integer in this chat. Example: 45",
|
|
162
|
+
].join("\n"), "prompt");
|
|
163
|
+
}
|
|
164
|
+
await answerCallbackQuerySafely(ctx, { text: "Send the number of days in this chat" });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (action === "view_customer") {
|
|
168
|
+
const shown = await showCustomerRenewalContext(ctx, stateStore, identities, workspaces, customerKeys, billing, auditLog, callbackState.requestId);
|
|
169
|
+
if (shown) {
|
|
170
|
+
await answerCallbackQuerySafely(ctx, { text: "Customer details loaded" });
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (action === "show_reject_reasons") {
|
|
175
|
+
const request = billing.getRenewalRequest(callbackState.requestId);
|
|
176
|
+
if (!request) {
|
|
177
|
+
await answerCallbackQuerySafely(ctx, { text: "Renewal request was not found.", show_alert: true });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (request.status === "closed") {
|
|
181
|
+
await answerCallbackQuerySafely(ctx, { text: "Request is closed.", show_alert: true });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
await showRenewalRejectKeyboard(ctx, stateStore, request);
|
|
185
|
+
await answerCallbackQuerySafely(ctx, { text: "Choose a rejection reason" });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (action === "prompt_custom_reason") {
|
|
189
|
+
const prepared = prepareRenewalAdminInput(ctx);
|
|
190
|
+
if (!prepared) {
|
|
191
|
+
await answerCallbackQuerySafely(ctx, { text: "This action only works from a message button.", show_alert: true });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
stateStore.set(buildTelegramSessionScope(prepared.chatId, prepared.userId), {
|
|
195
|
+
kind: "awaiting_renewal_reject_reason",
|
|
196
|
+
requestId: callbackState.requestId,
|
|
197
|
+
sourceChatId: prepared.chatId,
|
|
198
|
+
sourceMessageId: prepared.messageId,
|
|
199
|
+
});
|
|
200
|
+
const request = billing.getRenewalRequest(callbackState.requestId);
|
|
201
|
+
if (request) {
|
|
202
|
+
await renderRenewalPromptCard(ctx, stateStore, request, [
|
|
203
|
+
"Renewal review",
|
|
204
|
+
`request_id: ${callbackState.requestId}`,
|
|
205
|
+
"Awaiting custom rejection reason.",
|
|
206
|
+
"Send the rejection reason in this chat.",
|
|
207
|
+
].join("\n"), "prompt");
|
|
208
|
+
}
|
|
209
|
+
await answerCallbackQuerySafely(ctx, { text: "Send the rejection reason in this chat" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (action === "show_main_actions") {
|
|
213
|
+
const request = billing.getRenewalRequest(callbackState.requestId);
|
|
214
|
+
if (!request) {
|
|
215
|
+
await answerCallbackQuerySafely(ctx, { text: "Renewal request was not found.", show_alert: true });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
await showRenewalMainKeyboard(ctx, stateStore, request, formatAdminRenewalRequestText({
|
|
219
|
+
request,
|
|
220
|
+
identities,
|
|
221
|
+
workspaces,
|
|
222
|
+
customerKeys,
|
|
223
|
+
billing,
|
|
224
|
+
}));
|
|
225
|
+
await answerCallbackQuerySafely(ctx, { text: "Back to actions" });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const closed = await closeRenewalRequest(ctx, billing, auditLog, callbackState.requestId, callbackState.resolution ?? "closed_by_admin", true, true);
|
|
229
|
+
if (closed) {
|
|
230
|
+
await updateRenewalReviewMessage(ctx, closed.message);
|
|
231
|
+
await answerCallbackQuerySafely(ctx, { text: action === "reject_reason" ? "Renewal rejected" : "Renewal closed" });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
bot.on("message:text", async (ctx, next) => {
|
|
235
|
+
const chatId = ctx.chat?.id?.toString();
|
|
236
|
+
const userId = ctx.from?.id?.toString();
|
|
237
|
+
if (!chatId || !userId || !isAdmin(ctx, deps.config)) {
|
|
238
|
+
await next();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const scope = buildTelegramSessionScope(chatId, userId);
|
|
242
|
+
const session = stateStore.get(scope);
|
|
243
|
+
if (!session) {
|
|
244
|
+
await next();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (ctx.message.text.startsWith("/")) {
|
|
248
|
+
await next();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (session.kind === "awaiting_renewal_custom_days") {
|
|
252
|
+
const days = Number(ctx.message.text.trim());
|
|
253
|
+
if (!Number.isInteger(days) || days <= 0) {
|
|
254
|
+
await renderRenewalPromptCardByRef(ctx, stateStore, billing.getRenewalRequest(session.requestId), session.sourceChatId, session.sourceMessageId, [
|
|
255
|
+
"Renewal review",
|
|
256
|
+
`request_id: ${session.requestId}`,
|
|
257
|
+
"Awaiting custom approval days.",
|
|
258
|
+
"Please send a positive integer number of days.",
|
|
259
|
+
].join("\n"), "prompt");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const result = await approveRenewalRequest(ctx, deps, identities, workspaces, customerKeys, billing, auditLog, session.requestId, undefined, days, false, true);
|
|
263
|
+
stateStore.clear(scope);
|
|
264
|
+
if (result) {
|
|
265
|
+
await updateRenewalReviewMessageByRef(ctx, session.sourceChatId, session.sourceMessageId, result.message);
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (session.kind === "awaiting_renewal_reject_reason") {
|
|
270
|
+
const resolution = ctx.message.text.trim();
|
|
271
|
+
if (!resolution) {
|
|
272
|
+
await renderRenewalPromptCardByRef(ctx, stateStore, billing.getRenewalRequest(session.requestId), session.sourceChatId, session.sourceMessageId, [
|
|
273
|
+
"Renewal review",
|
|
274
|
+
`request_id: ${session.requestId}`,
|
|
275
|
+
"Awaiting custom rejection reason.",
|
|
276
|
+
"Please send a non-empty rejection reason.",
|
|
277
|
+
].join("\n"), "prompt");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const result = await closeRenewalRequest(ctx, billing, auditLog, session.requestId, resolution, true, true);
|
|
281
|
+
stateStore.clear(scope);
|
|
282
|
+
if (result) {
|
|
283
|
+
await updateRenewalReviewMessageByRef(ctx, session.sourceChatId, session.sourceMessageId, result.message);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
await next();
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async function handleCustomerRenewCommand(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog, rawArgs) {
|
|
291
|
+
if (ctx.chat?.type !== "private") {
|
|
292
|
+
await replyOrEditMessage(ctx, "For safety, open a private chat with this bot and run /renew there.");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const telegramUserId = ctx.from?.id?.toString();
|
|
296
|
+
if (!telegramUserId) {
|
|
297
|
+
await replyOrEditMessage(ctx, "Could not determine your Telegram user.");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const workspace = workspaces.getDefaultWorkspace(telegramUserId);
|
|
301
|
+
if (!workspace) {
|
|
302
|
+
await replyOrEditMessage(ctx, "No customer workspace has been assigned to your Telegram user yet.");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!rawArgs.trim()) {
|
|
306
|
+
await handleCustomerRenewRequest(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog, {
|
|
307
|
+
planId: DEFAULT_PURCHASE_PLAN_ID,
|
|
308
|
+
days: DEFAULT_PURCHASE_DAYS,
|
|
309
|
+
});
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
await replyOrEditMessage(ctx, "Usage: /renew - request a 24h renewal");
|
|
313
|
+
}
|
|
314
|
+
async function handleCustomerRenewRequest(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog, parsed) {
|
|
315
|
+
const telegramUserId = ctx.from?.id?.toString();
|
|
316
|
+
if (!telegramUserId) {
|
|
317
|
+
await replyOrEditMessage(ctx, "Could not determine your Telegram user.");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const workspace = workspaces.getDefaultWorkspace(telegramUserId);
|
|
321
|
+
if (!workspace) {
|
|
322
|
+
await replyOrEditMessage(ctx, "No customer workspace has been assigned to your Telegram user yet.");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const activeKey = customerKeys.getActiveKeyForUser(telegramUserId);
|
|
326
|
+
const kind = parsed?.kind ?? "renewal";
|
|
327
|
+
if (kind === "token_topup") {
|
|
328
|
+
const activeLots = billing.getActiveEntitlementLotsForWorkspace(workspace.id);
|
|
329
|
+
if (!activeKey || activeLots.length === 0) {
|
|
330
|
+
await replyOrEditMessage(ctx, "Buy API key or renew access before buying more tokens.");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const renewalPriceVnd = kind === "renewal"
|
|
335
|
+
? (() => {
|
|
336
|
+
const planId = parsed?.planId ?? DEFAULT_PURCHASE_PLAN_ID;
|
|
337
|
+
const plan = billing.getPlan(planId);
|
|
338
|
+
return plan && plan.currency === "VND" && plan.priceCents > 0
|
|
339
|
+
? plan.priceCents
|
|
340
|
+
: DEFAULT_PURCHASE_AMOUNT_VND;
|
|
341
|
+
})()
|
|
342
|
+
: undefined;
|
|
343
|
+
const created = billing.createRenewalRequest({
|
|
344
|
+
workspaceId: workspace.id,
|
|
345
|
+
telegramUserId,
|
|
346
|
+
kind,
|
|
347
|
+
requestedPlanId: kind === "renewal" ? parsed?.planId : undefined,
|
|
348
|
+
requestedDays: kind === "renewal" ? parsed?.days : undefined,
|
|
349
|
+
requestedTokenDelta: kind === "token_topup" ? parsed?.tokenDelta : undefined,
|
|
350
|
+
requestedTokenLotDays: kind === "token_topup" ? parsed?.lotDays : undefined,
|
|
351
|
+
priceVnd: kind === "token_topup" ? parsed?.priceVnd : renewalPriceVnd,
|
|
352
|
+
});
|
|
353
|
+
if (created.created) {
|
|
354
|
+
auditLog.record({
|
|
355
|
+
event: "renewal.requested",
|
|
356
|
+
actor: { type: "customer", id: telegramUserId },
|
|
357
|
+
subjectType: "renewal_request",
|
|
358
|
+
subjectId: created.request.id,
|
|
359
|
+
metadata: {
|
|
360
|
+
workspaceId: workspace.id,
|
|
361
|
+
telegramUserId,
|
|
362
|
+
kind: created.request.kind,
|
|
363
|
+
requestedPlanId: created.request.requestedPlanId,
|
|
364
|
+
requestedDays: created.request.requestedDays,
|
|
365
|
+
requestedTokenDelta: created.request.requestedTokenDelta,
|
|
366
|
+
requestedTokenLotDays: created.request.requestedTokenLotDays,
|
|
367
|
+
priceVnd: created.request.priceVnd,
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const paymentAmount = resolveRequestPriceVnd(created.request, billing);
|
|
372
|
+
const renewalPlan = created.request.kind === "renewal"
|
|
373
|
+
? billing.getPlan(created.request.requestedPlanId ?? DEFAULT_PURCHASE_PLAN_ID)
|
|
374
|
+
: undefined;
|
|
375
|
+
const renewalTokenLimit = renewalPlan?.monthlyTokenLimit ?? DEFAULT_PURCHASE_TOKEN_LIMIT;
|
|
376
|
+
const paymentUrl = created.created && deps.config.sepayAccountNumber
|
|
377
|
+
? buildSePayPaymentUrl({
|
|
378
|
+
accountNumber: deps.config.sepayAccountNumber,
|
|
379
|
+
bankCode: deps.config.sepayBankCode ?? "MBBank",
|
|
380
|
+
amount: paymentAmount,
|
|
381
|
+
description: `Chuc ngon mieng ma ${created.request.id}`,
|
|
382
|
+
template: deps.config.sepayTemplate ?? "compact",
|
|
383
|
+
download: deps.config.sepayDownload ?? false,
|
|
384
|
+
})
|
|
385
|
+
: undefined;
|
|
386
|
+
const message = created.created
|
|
387
|
+
? formatMessage(created.request.kind === "token_topup"
|
|
388
|
+
? "Token top-up request submitted."
|
|
389
|
+
: activeKey
|
|
390
|
+
? "Renewal request submitted."
|
|
391
|
+
: "API key purchase request submitted.", [
|
|
392
|
+
formatField("Request id", created.request.id),
|
|
393
|
+
formatField("Workspace", workspace.id),
|
|
394
|
+
created.request.kind === "token_topup"
|
|
395
|
+
? `pack: ${paymentAmount} VND -> ${created.request.requestedTokenDelta ?? DEFAULT_PURCHASE_TOKEN_LIMIT} tokens`
|
|
396
|
+
: `plan: ${paymentAmount} VND -> ${renewalTokenLimit} tokens`,
|
|
397
|
+
created.request.requestedPlanId ? `requested_plan_id: ${created.request.requestedPlanId}` : undefined,
|
|
398
|
+
created.request.requestedDays ? `requested_days: ${created.request.requestedDays}` : undefined,
|
|
399
|
+
created.request.requestedTokenDelta ? `requested_token_delta: ${created.request.requestedTokenDelta}` : undefined,
|
|
400
|
+
created.request.requestedTokenLotDays ? `token_lot_days: ${created.request.requestedTokenLotDays}` : undefined,
|
|
401
|
+
paymentUrl ? formatPaymentSection(paymentAmount, created.request.id, paymentUrl) : undefined,
|
|
402
|
+
"Admin will verify the transfer manually before approval.",
|
|
403
|
+
])
|
|
404
|
+
: formatMessage("You already have an open renewal request.", [
|
|
405
|
+
formatField("Request id", created.request.id),
|
|
406
|
+
formatRawField("kind", created.request.kind),
|
|
407
|
+
created.request.requestedPlanId ? `requested_plan_id: ${created.request.requestedPlanId}` : undefined,
|
|
408
|
+
created.request.requestedDays ? `requested_days: ${created.request.requestedDays}` : undefined,
|
|
409
|
+
created.request.requestedTokenDelta ? `requested_token_delta: ${created.request.requestedTokenDelta}` : undefined,
|
|
410
|
+
created.request.requestedTokenLotDays ? `token_lot_days: ${created.request.requestedTokenLotDays}` : undefined,
|
|
411
|
+
"Please wait for admin review.",
|
|
412
|
+
]);
|
|
413
|
+
const resultLines = [message];
|
|
414
|
+
const notification = await notifyAdminsAboutRenewalRequest(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, created.request);
|
|
415
|
+
if (notification.sent === 0) {
|
|
416
|
+
resultLines.push("admin_notification: pending_manual_follow_up");
|
|
417
|
+
}
|
|
418
|
+
else if (!created.created) {
|
|
419
|
+
resultLines.push("admin_notification: reminder_sent");
|
|
420
|
+
}
|
|
421
|
+
await replyOrEditMessage(ctx, resultLines.filter(Boolean).join("\n"), {
|
|
422
|
+
reply_markup: buildCustomerActionKeyboard(activeKey?.status === "active"),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function formatPaymentSection(paymentAmount, requestId, paymentUrl) {
|
|
426
|
+
return formatSection("Payment", [
|
|
427
|
+
formatField("Amount", `${formatNumber(paymentAmount)} VND`),
|
|
428
|
+
formatField("Transfer note", `Chuc ngon mieng ma ${requestId}`),
|
|
429
|
+
formatField("Scan QR", paymentUrl),
|
|
430
|
+
]);
|
|
431
|
+
}
|
|
432
|
+
function formatPaymentConfirmationSection(requestId, status, amountVnd) {
|
|
433
|
+
return formatSection("Payment", [
|
|
434
|
+
formatField("Request ID", requestId),
|
|
435
|
+
formatField("Status", status),
|
|
436
|
+
formatField("Amount", `${formatNumber(amountVnd)} VND`),
|
|
437
|
+
]);
|
|
438
|
+
}
|
|
439
|
+
async function handleAdminRenewCommand(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, auditLog, rawArgs) {
|
|
440
|
+
const args = rawArgs.trim().split(/\s+/g).filter(Boolean);
|
|
441
|
+
const subcommand = args[0]?.toLowerCase();
|
|
442
|
+
if (!subcommand) {
|
|
443
|
+
await renderAdminRenewalInfo(ctx, "🧾 Renewal commands\nUsage: /renew list | /renew paid <requestId> | /renew approve <requestId> <planId> <days> [replace-key] | /renew approve <requestId> topup [tokenDelta] [days] | /renew close <requestId> [reason]");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (subcommand === "list") {
|
|
447
|
+
const openRequests = [
|
|
448
|
+
...billing.listRenewalRequests("payment_confirmed"),
|
|
449
|
+
...billing.listRenewalRequests("open"),
|
|
450
|
+
];
|
|
451
|
+
await renderAdminRenewalList(ctx, stateStore, openRequests);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (subcommand === "paid" || subcommand === "confirm-payment") {
|
|
455
|
+
const requestId = args[1];
|
|
456
|
+
if (!requestId) {
|
|
457
|
+
await renderAdminRenewalInfo(ctx, "🧾 Mark renewal payment\nUsage: /renew paid <requestId>");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const confirmed = await confirmRenewalPayment(ctx, billing, auditLog, requestId);
|
|
461
|
+
if (confirmed) {
|
|
462
|
+
const updatedRequest = billing.getRenewalRequest(requestId);
|
|
463
|
+
if (updatedRequest) {
|
|
464
|
+
await ctx.reply(confirmed.message, {
|
|
465
|
+
reply_markup: buildAdminRenewalKeyboard(stateStore, updatedRequest),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
await updateRenewalReviewMessage(ctx, confirmed.message);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (subcommand === "close") {
|
|
475
|
+
const requestId = args[1];
|
|
476
|
+
const reason = args.slice(2).join(" ").trim();
|
|
477
|
+
if (!requestId) {
|
|
478
|
+
await renderAdminRenewalInfo(ctx, "🧾 Close renewal request\nUsage: /renew close <requestId> [reason]");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
await closeRenewalRequest(ctx, billing, auditLog, requestId, reason || "closed_by_admin", false, true);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (subcommand === "approve") {
|
|
485
|
+
const rest = args.slice(1);
|
|
486
|
+
const [requestId, second, third, fourth] = rest;
|
|
487
|
+
if (!requestId) {
|
|
488
|
+
await renderAdminRenewalInfo(ctx, "Usage: /renew approve <requestId> <planId> <days> [replace-key] | /renew approve <requestId> topup [tokenDelta] [days]");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const existingRequest = billing.getRenewalRequest(requestId);
|
|
492
|
+
const topupMode = second?.toLowerCase() === "topup" || existingRequest?.kind === "token_topup";
|
|
493
|
+
if (topupMode) {
|
|
494
|
+
const tokenDeltaRaw = second?.toLowerCase() === "topup" ? third : second;
|
|
495
|
+
const daysRaw = second?.toLowerCase() === "topup" ? fourth : third;
|
|
496
|
+
const tokenDelta = tokenDeltaRaw ? Number(tokenDeltaRaw) : undefined;
|
|
497
|
+
const days = daysRaw ? Number(daysRaw) : undefined;
|
|
498
|
+
if ((tokenDelta !== undefined && (!Number.isInteger(tokenDelta) || tokenDelta <= 0))
|
|
499
|
+
|| (days !== undefined && (!Number.isInteger(days) || days <= 0))) {
|
|
500
|
+
await renderAdminRenewalInfo(ctx, "Usage: /renew approve <requestId> topup [tokenDelta] [days]");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
await approveRenewalRequest(ctx, deps, identities, workspaces, customerKeys, billing, auditLog, requestId, undefined, days, false, false, tokenDelta);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const planId = second;
|
|
507
|
+
const daysRaw = third;
|
|
508
|
+
const replaceKeyRaw = fourth;
|
|
509
|
+
const days = Number(daysRaw);
|
|
510
|
+
if (!planId || !Number.isInteger(days) || days <= 0 || (replaceKeyRaw && replaceKeyRaw !== "replace-key")) {
|
|
511
|
+
await renderAdminRenewalInfo(ctx, "Usage: /renew approve <requestId> <planId> <days> [replace-key]");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
await approveRenewalRequest(ctx, deps, identities, workspaces, customerKeys, billing, auditLog, requestId, planId, days, replaceKeyRaw === "replace-key");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
await renderAdminRenewalInfo(ctx, "Usage: /renew list | /renew paid <requestId> | /renew approve <requestId> <planId> <days> [replace-key] | /renew close <requestId> [reason]");
|
|
518
|
+
}
|
|
519
|
+
async function notifyAdminsAboutRenewalRequest(ctx, deps, stateStore, identities, workspaces, customerKeys, billing, request) {
|
|
520
|
+
const recipients = new Set([
|
|
521
|
+
...deps.config.ownerUserIds,
|
|
522
|
+
...deps.config.adminUserIds,
|
|
523
|
+
]);
|
|
524
|
+
const requesterId = ctx.from?.id?.toString();
|
|
525
|
+
if (requesterId) {
|
|
526
|
+
recipients.delete(requesterId);
|
|
527
|
+
}
|
|
528
|
+
const text = formatAdminRenewalRequestText({
|
|
529
|
+
request,
|
|
530
|
+
identities,
|
|
531
|
+
workspaces,
|
|
532
|
+
customerKeys,
|
|
533
|
+
billing,
|
|
534
|
+
});
|
|
535
|
+
let sent = 0;
|
|
536
|
+
let failed = 0;
|
|
537
|
+
for (const recipient of recipients) {
|
|
538
|
+
try {
|
|
539
|
+
await ctx.api.sendMessage(Number(recipient), text, {
|
|
540
|
+
reply_markup: buildAdminRenewalKeyboard(stateStore, request),
|
|
541
|
+
});
|
|
542
|
+
sent += 1;
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
failed += 1;
|
|
546
|
+
// best effort admin notification
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return { sent, failed };
|
|
550
|
+
}
|
|
551
|
+
function buildAdminRenewalListKeyboard(stateStore, requests) {
|
|
552
|
+
const keyboard = new InlineKeyboard();
|
|
553
|
+
requests.slice(0, 10).forEach((request) => {
|
|
554
|
+
const reviewToken = issueRenewalCallbackToken(stateStore, {
|
|
555
|
+
kind: "renewal_request_action",
|
|
556
|
+
action: "show_main_actions",
|
|
557
|
+
requestId: request.id,
|
|
558
|
+
});
|
|
559
|
+
keyboard.text(`${request.telegramUserId} · ${request.id.slice(0, 8)}`, `v1:renew:back:${reviewToken}`).row();
|
|
560
|
+
});
|
|
561
|
+
return requests.length > 0 ? keyboard : undefined;
|
|
562
|
+
}
|
|
563
|
+
function buildSePayPaymentUrl(args) {
|
|
564
|
+
const url = new URL("https://qr.sepay.vn/img");
|
|
565
|
+
url.searchParams.set("acc", args.accountNumber);
|
|
566
|
+
url.searchParams.set("bank", args.bankCode);
|
|
567
|
+
url.searchParams.set("amount", String(args.amount));
|
|
568
|
+
url.searchParams.set("des", args.description);
|
|
569
|
+
url.searchParams.set("template", args.template);
|
|
570
|
+
url.searchParams.set("download", String(args.download));
|
|
571
|
+
return url.toString();
|
|
572
|
+
}
|
|
573
|
+
function buildAdminRenewalKeyboard(stateStore, request) {
|
|
574
|
+
const approveToken = issueRenewalCallbackToken(stateStore, {
|
|
575
|
+
kind: "renewal_request_action",
|
|
576
|
+
action: "approve",
|
|
577
|
+
requestId: request.id,
|
|
578
|
+
});
|
|
579
|
+
const approveRotateToken = issueRenewalCallbackToken(stateStore, {
|
|
580
|
+
kind: "renewal_request_action",
|
|
581
|
+
action: "approve_rotate",
|
|
582
|
+
requestId: request.id,
|
|
583
|
+
});
|
|
584
|
+
const approve30Token = issueRenewalCallbackToken(stateStore, {
|
|
585
|
+
kind: "renewal_request_action",
|
|
586
|
+
action: "approve_override",
|
|
587
|
+
requestId: request.id,
|
|
588
|
+
overrideDays: 30,
|
|
589
|
+
});
|
|
590
|
+
const approve90Token = issueRenewalCallbackToken(stateStore, {
|
|
591
|
+
kind: "renewal_request_action",
|
|
592
|
+
action: "approve_override",
|
|
593
|
+
requestId: request.id,
|
|
594
|
+
overrideDays: 90,
|
|
595
|
+
});
|
|
596
|
+
const approveCustomToken = issueRenewalCallbackToken(stateStore, {
|
|
597
|
+
kind: "renewal_request_action",
|
|
598
|
+
action: "prompt_custom_days",
|
|
599
|
+
requestId: request.id,
|
|
600
|
+
});
|
|
601
|
+
const viewCustomerToken = issueRenewalCallbackToken(stateStore, {
|
|
602
|
+
kind: "renewal_request_action",
|
|
603
|
+
action: "view_customer",
|
|
604
|
+
requestId: request.id,
|
|
605
|
+
});
|
|
606
|
+
const rejectToken = issueRenewalCallbackToken(stateStore, {
|
|
607
|
+
kind: "renewal_request_action",
|
|
608
|
+
action: "show_reject_reasons",
|
|
609
|
+
requestId: request.id,
|
|
610
|
+
});
|
|
611
|
+
const closeToken = issueRenewalCallbackToken(stateStore, {
|
|
612
|
+
kind: "renewal_request_action",
|
|
613
|
+
action: "close",
|
|
614
|
+
requestId: request.id,
|
|
615
|
+
});
|
|
616
|
+
const confirmPaymentToken = issueRenewalCallbackToken(stateStore, {
|
|
617
|
+
kind: "renewal_request_action",
|
|
618
|
+
action: "confirm_payment",
|
|
619
|
+
requestId: request.id,
|
|
620
|
+
});
|
|
621
|
+
const keyboard = new InlineKeyboard();
|
|
622
|
+
if (request.status !== "payment_confirmed") {
|
|
623
|
+
keyboard.text("✅ Confirm transfer", `v1:renew:confirm-payment:${confirmPaymentToken}`).row();
|
|
624
|
+
}
|
|
625
|
+
if (request.kind === "token_topup") {
|
|
626
|
+
if (request.status === "payment_confirmed") {
|
|
627
|
+
keyboard.text("🟢 Approve top-up", `v1:renew:approve:${approveToken}`).row();
|
|
628
|
+
}
|
|
629
|
+
keyboard
|
|
630
|
+
.text("⚪ View customer", `v1:renew:view-customer:${viewCustomerToken}`)
|
|
631
|
+
.url("⚪ Open customer chat", `tg://user?id=${request.telegramUserId}`)
|
|
632
|
+
.row()
|
|
633
|
+
.text("🔴 Reject with reason", `v1:renew:reject-reasons:${rejectToken}`)
|
|
634
|
+
.row()
|
|
635
|
+
.text("⚫ Close", `v1:renew:close:${closeToken}`);
|
|
636
|
+
return keyboard;
|
|
637
|
+
}
|
|
638
|
+
if (request.status === "payment_confirmed") {
|
|
639
|
+
keyboard
|
|
640
|
+
.text("🟢 Approve", `v1:renew:approve:${approveToken}`)
|
|
641
|
+
.text("🔵 Approve + rotate key", `v1:renew:approve-rotate:${approveRotateToken}`)
|
|
642
|
+
.row()
|
|
643
|
+
.text("🟢 Approve 30d", `v1:renew:approve-30:${approve30Token}`)
|
|
644
|
+
.text("🟢 Approve 90d", `v1:renew:approve-90:${approve90Token}`)
|
|
645
|
+
.row()
|
|
646
|
+
.text("🟡 Approve custom days", `v1:renew:approve-custom:${approveCustomToken}`)
|
|
647
|
+
.row();
|
|
648
|
+
}
|
|
649
|
+
keyboard
|
|
650
|
+
.text("⚪ View customer", `v1:renew:view-customer:${viewCustomerToken}`)
|
|
651
|
+
.url("⚪ Open customer chat", `tg://user?id=${request.telegramUserId}`)
|
|
652
|
+
.row()
|
|
653
|
+
.text("🔴 Reject with reason", `v1:renew:reject-reasons:${rejectToken}`)
|
|
654
|
+
.row()
|
|
655
|
+
.text("⚫ Close", `v1:renew:close:${closeToken}`);
|
|
656
|
+
return keyboard;
|
|
657
|
+
}
|
|
658
|
+
function buildRejectReasonKeyboard(stateStore, request) {
|
|
659
|
+
const unpaidToken = issueRenewalCallbackToken(stateStore, {
|
|
660
|
+
kind: "renewal_request_action",
|
|
661
|
+
action: "reject_reason",
|
|
662
|
+
requestId: request.id,
|
|
663
|
+
resolution: "rejected_unpaid",
|
|
664
|
+
});
|
|
665
|
+
const duplicateToken = issueRenewalCallbackToken(stateStore, {
|
|
666
|
+
kind: "renewal_request_action",
|
|
667
|
+
action: "reject_reason",
|
|
668
|
+
requestId: request.id,
|
|
669
|
+
resolution: "rejected_duplicate_request",
|
|
670
|
+
});
|
|
671
|
+
const invalidPlanToken = issueRenewalCallbackToken(stateStore, {
|
|
672
|
+
kind: "renewal_request_action",
|
|
673
|
+
action: "reject_reason",
|
|
674
|
+
requestId: request.id,
|
|
675
|
+
resolution: "rejected_invalid_plan",
|
|
676
|
+
});
|
|
677
|
+
const backToken = issueRenewalCallbackToken(stateStore, {
|
|
678
|
+
kind: "renewal_request_action",
|
|
679
|
+
action: "show_main_actions",
|
|
680
|
+
requestId: request.id,
|
|
681
|
+
});
|
|
682
|
+
const customReasonToken = issueRenewalCallbackToken(stateStore, {
|
|
683
|
+
kind: "renewal_request_action",
|
|
684
|
+
action: "prompt_custom_reason",
|
|
685
|
+
requestId: request.id,
|
|
686
|
+
});
|
|
687
|
+
return new InlineKeyboard()
|
|
688
|
+
.text("🔴 Unpaid", `v1:renew:reject:${unpaidToken}`)
|
|
689
|
+
.text("🟠 Duplicate", `v1:renew:reject:${duplicateToken}`)
|
|
690
|
+
.row()
|
|
691
|
+
.text("🟠 Invalid plan", `v1:renew:reject:${invalidPlanToken}`)
|
|
692
|
+
.text("🟡 Custom reason", `v1:renew:reject-custom:${customReasonToken}`)
|
|
693
|
+
.row()
|
|
694
|
+
.text("⚪ Back", `v1:renew:back:${backToken}`);
|
|
695
|
+
}
|
|
696
|
+
function buildRenewalPromptKeyboard(stateStore, request) {
|
|
697
|
+
const backToken = issueRenewalCallbackToken(stateStore, {
|
|
698
|
+
kind: "renewal_request_action",
|
|
699
|
+
action: "show_main_actions",
|
|
700
|
+
requestId: request.id,
|
|
701
|
+
});
|
|
702
|
+
return new InlineKeyboard().text("⚪ Back", `v1:renew:back:${backToken}`);
|
|
703
|
+
}
|
|
704
|
+
function buildRenewalCustomerKeyboard(stateStore, request) {
|
|
705
|
+
const backToken = issueRenewalCallbackToken(stateStore, {
|
|
706
|
+
kind: "renewal_request_action",
|
|
707
|
+
action: "show_main_actions",
|
|
708
|
+
requestId: request.id,
|
|
709
|
+
});
|
|
710
|
+
return new InlineKeyboard()
|
|
711
|
+
.text("⚪ Back", `v1:renew:back:${backToken}`)
|
|
712
|
+
.url("⚪ Open customer chat", `tg://user?id=${request.telegramUserId}`);
|
|
713
|
+
}
|
|
714
|
+
function normalizeRenewalAction(value) {
|
|
715
|
+
if (value === "approve-rotate") {
|
|
716
|
+
return "approve_rotate";
|
|
717
|
+
}
|
|
718
|
+
if (value === "approve-30" || value === "approve-90") {
|
|
719
|
+
return "approve_override";
|
|
720
|
+
}
|
|
721
|
+
if (value === "approve-custom") {
|
|
722
|
+
return "prompt_custom_days";
|
|
723
|
+
}
|
|
724
|
+
if (value === "confirm-payment") {
|
|
725
|
+
return "confirm_payment";
|
|
726
|
+
}
|
|
727
|
+
if (value === "view-customer") {
|
|
728
|
+
return "view_customer";
|
|
729
|
+
}
|
|
730
|
+
if (value === "reject-reasons") {
|
|
731
|
+
return "show_reject_reasons";
|
|
732
|
+
}
|
|
733
|
+
if (value === "reject") {
|
|
734
|
+
return "reject_reason";
|
|
735
|
+
}
|
|
736
|
+
if (value === "reject-custom") {
|
|
737
|
+
return "prompt_custom_reason";
|
|
738
|
+
}
|
|
739
|
+
if (value === "back") {
|
|
740
|
+
return "show_main_actions";
|
|
741
|
+
}
|
|
742
|
+
return value;
|
|
743
|
+
}
|
|
744
|
+
function formatAdminRenewalRequestText(args) {
|
|
745
|
+
const user = args.identities.getUser(args.request.telegramUserId);
|
|
746
|
+
const overview = readCustomerBillingOverview({
|
|
747
|
+
telegramUserId: args.request.telegramUserId,
|
|
748
|
+
workspaces: args.workspaces,
|
|
749
|
+
customerKeys: args.customerKeys,
|
|
750
|
+
billing: args.billing,
|
|
751
|
+
});
|
|
752
|
+
const plan = args.request.requestedPlanId ? args.billing.getPlan(args.request.requestedPlanId) : undefined;
|
|
753
|
+
const fallbackPlan = !args.request.requestedPlanId ? args.billing.getPlan(DEFAULT_PURCHASE_PLAN_ID) : undefined;
|
|
754
|
+
const userStatus = user?.status ?? "unknown";
|
|
755
|
+
const workspaceStatus = overview.workspace?.status ?? "none";
|
|
756
|
+
const accountPending = userStatus === "pending_approval" || workspaceStatus === "pending_approval";
|
|
757
|
+
return [
|
|
758
|
+
accountPending
|
|
759
|
+
? "⚠ Approving this request will also activate a pending approval account/workspace."
|
|
760
|
+
: undefined,
|
|
761
|
+
args.request.kind === "token_topup"
|
|
762
|
+
? "Token top-up request"
|
|
763
|
+
: overview.apiKey ? "Renewal request" : "New access request",
|
|
764
|
+
formatSection("Customer", [
|
|
765
|
+
formatField("Telegram user", formatTelegramUserLabel(user, args.request.telegramUserId)),
|
|
766
|
+
formatField("Account status", formatStatus(userStatus)),
|
|
767
|
+
formatField("Workspace status", formatStatus(workspaceStatus)),
|
|
768
|
+
]),
|
|
769
|
+
formatSection("Request", [
|
|
770
|
+
formatField("Request ID", args.request.id),
|
|
771
|
+
formatField("Workspace ID", args.request.workspaceId),
|
|
772
|
+
formatField("Kind", formatStatus(args.request.kind)),
|
|
773
|
+
formatField("Status", formatStatus(args.request.status)),
|
|
774
|
+
plan
|
|
775
|
+
? formatField("Requested plan", `${plan.id} (${plan.name})`)
|
|
776
|
+
: fallbackPlan
|
|
777
|
+
? formatField("Requested plan", `${fallbackPlan.id} (${fallbackPlan.name})`)
|
|
778
|
+
: args.request.requestedPlanId
|
|
779
|
+
? formatField("Requested plan", args.request.requestedPlanId)
|
|
780
|
+
: formatField("Requested plan", "manual review needed"),
|
|
781
|
+
args.request.requestedDays ? formatField("Requested days", args.request.requestedDays) : undefined,
|
|
782
|
+
args.request.requestedTokenDelta ? formatField("Token top-up", formatNumber(args.request.requestedTokenDelta)) : undefined,
|
|
783
|
+
args.request.requestedTokenLotDays ? formatField("Token lot days", args.request.requestedTokenLotDays) : undefined,
|
|
784
|
+
args.request.priceVnd ? formatField("Price", `${formatNumber(args.request.priceVnd)} VND`) : undefined,
|
|
785
|
+
formatField("Requested at", formatDateTime(args.request.requestedAt)),
|
|
786
|
+
]),
|
|
787
|
+
formatSection("Current access", [
|
|
788
|
+
overview.apiKey ? formatField("Key preview", overview.apiKey.apiKeyPreview) : undefined,
|
|
789
|
+
overview.entitlement ? formatField("Current expiry", formatDateTime(overview.entitlement.validUntil)) : formatField("Current expiry", "none"),
|
|
790
|
+
formatField("Entitlement", formatStatus(overview.entitlementStatus)),
|
|
791
|
+
overview.remainingTokens !== null ? formatField("Remaining tokens", formatNumber(overview.remainingTokens)) : undefined,
|
|
792
|
+
]),
|
|
793
|
+
]
|
|
794
|
+
.filter(Boolean)
|
|
795
|
+
.join("\n\n");
|
|
796
|
+
}
|
|
797
|
+
function formatTelegramUserLabel(user, telegramUserId) {
|
|
798
|
+
const name = [user?.firstName, user?.lastName].filter(Boolean).join(" ").trim();
|
|
799
|
+
const username = user?.username ? `@${user.username}` : undefined;
|
|
800
|
+
return [name || undefined, username, `id=${telegramUserId}`].filter(Boolean).join(" | ");
|
|
801
|
+
}
|
|
802
|
+
function formatStatus(value) {
|
|
803
|
+
return value.replace(/_/g, " ");
|
|
804
|
+
}
|
|
805
|
+
function formatNumber(value) {
|
|
806
|
+
return new Intl.NumberFormat("en-US").format(value);
|
|
807
|
+
}
|
|
808
|
+
async function renderAdminRenewalList(ctx, stateStore, openRequests) {
|
|
809
|
+
await renderAdminScreen(ctx, {
|
|
810
|
+
text: openRequests.length === 0
|
|
811
|
+
? "No open renewal requests."
|
|
812
|
+
: [
|
|
813
|
+
"Open renewal requests:",
|
|
814
|
+
...openRequests.slice(0, 10).map(formatRenewalRequestLine),
|
|
815
|
+
].join("\n"),
|
|
816
|
+
loop: "billing",
|
|
817
|
+
primaryKeyboard: buildAdminRenewalListKeyboard(stateStore, openRequests),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
async function renderAdminRenewalInfo(ctx, text) {
|
|
821
|
+
await renderAdminScreen(ctx, {
|
|
822
|
+
text,
|
|
823
|
+
loop: "billing",
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
async function showCustomerRenewalContext(ctx, stateStore, identities, workspaces, customerKeys, billing, auditLog, requestId) {
|
|
827
|
+
const request = billing.getRenewalRequest(requestId);
|
|
828
|
+
if (!request) {
|
|
829
|
+
await renderAdminRenewalInfo(ctx, "Renewal request was not found.");
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
const user = identities.getUser(request.telegramUserId);
|
|
833
|
+
const overview = readCustomerBillingOverview({
|
|
834
|
+
telegramUserId: request.telegramUserId,
|
|
835
|
+
workspaces,
|
|
836
|
+
customerKeys,
|
|
837
|
+
billing,
|
|
838
|
+
});
|
|
839
|
+
const currentApiKey = overview.apiKey;
|
|
840
|
+
const canShowApiKeyToAdmin = ctx.chat?.type === "private" && !!currentApiKey;
|
|
841
|
+
const apiKey = currentApiKey && canShowApiKeyToAdmin ? customerKeys.getApiKeySecret(currentApiKey.id) : undefined;
|
|
842
|
+
if (apiKey && currentApiKey) {
|
|
843
|
+
auditLog.record({
|
|
844
|
+
event: "api_key.revealed",
|
|
845
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
846
|
+
subjectType: "customer_api_key",
|
|
847
|
+
subjectId: currentApiKey.id,
|
|
848
|
+
metadata: {
|
|
849
|
+
telegramUserId: request.telegramUserId,
|
|
850
|
+
workspaceId: request.workspaceId,
|
|
851
|
+
keyPreview: currentApiKey.apiKeyPreview,
|
|
852
|
+
audience: "admin_customer_review",
|
|
853
|
+
apiKey,
|
|
854
|
+
},
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
await renderRenewalCustomerContext(ctx, stateStore, request, [
|
|
858
|
+
"Customer renewal review",
|
|
859
|
+
`customer: ${formatTelegramUserLabel(user, request.telegramUserId)}`,
|
|
860
|
+
`request_id: ${request.id}`,
|
|
861
|
+
`workspace_id: ${request.workspaceId}`,
|
|
862
|
+
overview.workspace ? `workspace_status: ${overview.workspace.status}` : "workspace_status: none",
|
|
863
|
+
overview.workspace ? `client_route: ${overview.workspace.defaultClientRoute}` : undefined,
|
|
864
|
+
overview.apiKey ? `key_status: ${overview.apiKey.status}` : undefined,
|
|
865
|
+
overview.apiKey ? `key_preview: ${overview.apiKey.apiKeyPreview}` : undefined,
|
|
866
|
+
apiKey ? `api_key: ${apiKey}` : undefined,
|
|
867
|
+
overview.apiKey && canShowApiKeyToAdmin && !apiKey ? "full_key: unavailable_for_legacy_key" : undefined,
|
|
868
|
+
overview.entitlement ? `current_expiry: ${formatDateTime(overview.entitlement.validUntil)}` : "current_expiry: none",
|
|
869
|
+
`entitlement_status: ${overview.entitlementStatus}`,
|
|
870
|
+
overview.entitlement ? `token_limit: ${overview.entitlement.monthlyTokenLimit}` : undefined,
|
|
871
|
+
overview.remainingTokens !== null ? `remaining_tokens: ${overview.remainingTokens}` : undefined,
|
|
872
|
+
`request_status: ${request.status}`,
|
|
873
|
+
]
|
|
874
|
+
.filter(Boolean)
|
|
875
|
+
.join("\n"));
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
async function confirmRenewalPayment(ctx, billing, auditLog, requestId) {
|
|
879
|
+
const request = billing.getRenewalRequest(requestId);
|
|
880
|
+
if (!request) {
|
|
881
|
+
await updateRenewalReviewMessage(ctx, "Renewal request was not found.");
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
if (request.status === "approved" || request.status === "closed") {
|
|
885
|
+
await updateRenewalReviewMessage(ctx, "Renewal request is not open anymore.");
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
const confirmed = billing.confirmRenewalPayment({
|
|
889
|
+
id: requestId,
|
|
890
|
+
resolution: "payment_confirmed_manual",
|
|
891
|
+
expectedStatus: "open",
|
|
892
|
+
});
|
|
893
|
+
if (!confirmed) {
|
|
894
|
+
await updateRenewalReviewMessage(ctx, "Payment was already confirmed by another admin.");
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
const amountVnd = resolveRequestPriceVnd(confirmed, billing);
|
|
898
|
+
auditLog.record({
|
|
899
|
+
event: "payment.confirmed_manual",
|
|
900
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
901
|
+
subjectType: "renewal_request",
|
|
902
|
+
subjectId: requestId,
|
|
903
|
+
metadata: {
|
|
904
|
+
telegramUserId: request.telegramUserId,
|
|
905
|
+
workspaceId: request.workspaceId,
|
|
906
|
+
amountVnd,
|
|
907
|
+
transferDescription: `Chuc ngon mieng ma ${requestId}`,
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
const message = [
|
|
911
|
+
"Payment confirmed manually.",
|
|
912
|
+
formatPaymentConfirmationSection(confirmed.id, confirmed.status, amountVnd),
|
|
913
|
+
"Admin can approve this request now.",
|
|
914
|
+
].join("\n");
|
|
915
|
+
return { message };
|
|
916
|
+
}
|
|
917
|
+
async function approveRenewalRequest(ctx, deps, identities, workspaces, customerKeys, billing, auditLog, requestId, overridePlanId, overrideDays, replaceKey = false, silentReply = false, overrideTokenDelta) {
|
|
918
|
+
const request = billing.getRenewalRequest(requestId);
|
|
919
|
+
if (!request) {
|
|
920
|
+
await updateRenewalReviewMessage(ctx, "Renewal request was not found.");
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
if (request.status === "approved" || request.status === "closed") {
|
|
924
|
+
await updateRenewalReviewMessage(ctx, "Renewal request is not open anymore.");
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
if (request.status !== "payment_confirmed") {
|
|
928
|
+
await updateRenewalReviewMessage(ctx, "Please confirm payment first.");
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
const planId = overridePlanId ?? request.requestedPlanId ?? DEFAULT_PURCHASE_PLAN_ID;
|
|
932
|
+
const days = overrideDays ?? request.requestedDays;
|
|
933
|
+
if (request.kind === "token_topup") {
|
|
934
|
+
const tokenDelta = overrideTokenDelta ?? request.requestedTokenDelta ?? DEFAULT_PURCHASE_TOKEN_LIMIT;
|
|
935
|
+
const lotDays = overrideDays ?? request.requestedTokenLotDays ?? DEFAULT_TOKEN_TOPUP_LOT_DAYS;
|
|
936
|
+
try {
|
|
937
|
+
const activeKey = customerKeys.getActiveKeyForUser(request.telegramUserId);
|
|
938
|
+
const activeLots = billing.getActiveEntitlementLotsForWorkspace(request.workspaceId);
|
|
939
|
+
if (!activeKey || activeLots.length === 0) {
|
|
940
|
+
throw new Error("Token top-up requires an active customer key and active token lot.");
|
|
941
|
+
}
|
|
942
|
+
const approved = billing.approveTokenTopUpAtomically({
|
|
943
|
+
requestId,
|
|
944
|
+
workspaceId: request.workspaceId,
|
|
945
|
+
tokenDelta,
|
|
946
|
+
lotDays,
|
|
947
|
+
resolution: "approved_by_admin",
|
|
948
|
+
});
|
|
949
|
+
if (!approved) {
|
|
950
|
+
await updateRenewalReviewMessage(ctx, "Request is no longer pending approval.");
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
const lot = approved.lot;
|
|
954
|
+
auditLog.record({
|
|
955
|
+
event: "renewal.approved",
|
|
956
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
957
|
+
subjectType: "renewal_request",
|
|
958
|
+
subjectId: requestId,
|
|
959
|
+
metadata: {
|
|
960
|
+
telegramUserId: request.telegramUserId,
|
|
961
|
+
workspaceId: request.workspaceId,
|
|
962
|
+
kind: request.kind,
|
|
963
|
+
tokenDelta,
|
|
964
|
+
lotDays,
|
|
965
|
+
lotId: lot.id,
|
|
966
|
+
lotExpiresAt: lot.validUntil,
|
|
967
|
+
},
|
|
968
|
+
});
|
|
969
|
+
const customerNotified = await notifyCustomerAboutApprovedTokenTopUp(ctx, {
|
|
970
|
+
telegramUserId: request.telegramUserId,
|
|
971
|
+
tokenDelta,
|
|
972
|
+
expiresAt: lot.validUntil,
|
|
973
|
+
});
|
|
974
|
+
const message = [
|
|
975
|
+
"Token top-up request approved",
|
|
976
|
+
formatSection("Request", [
|
|
977
|
+
formatField("Request ID", requestId),
|
|
978
|
+
formatField("Telegram user ID", request.telegramUserId),
|
|
979
|
+
formatField("Token delta", formatNumber(tokenDelta)),
|
|
980
|
+
formatField("Lot expires at", formatDateTime(lot.validUntil)),
|
|
981
|
+
]),
|
|
982
|
+
formatSection("Delivery", [
|
|
983
|
+
customerNotified ? "Customer notified: yes" : "Customer notified: pending manual follow-up",
|
|
984
|
+
]),
|
|
985
|
+
formatSection("Status", [formatField("Value", "approved")]),
|
|
986
|
+
]
|
|
987
|
+
.filter(Boolean)
|
|
988
|
+
.join("\n\n");
|
|
989
|
+
if (!silentReply) {
|
|
990
|
+
await updateRenewalReviewMessage(ctx, message);
|
|
991
|
+
}
|
|
992
|
+
return { message };
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
return closeFailedApprovalRequest(ctx, billing, auditLog, request, requestId, error);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (!planId || !days) {
|
|
999
|
+
await updateRenewalReviewMessage(ctx, "This renewal request needs manual review. Use /renew approve <requestId> <planId> <days>.");
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
const transitioned = billing.approveRenewalRequest({
|
|
1004
|
+
id: requestId,
|
|
1005
|
+
approvedPlanId: planId,
|
|
1006
|
+
approvedDays: days,
|
|
1007
|
+
resolution: "approved_by_admin",
|
|
1008
|
+
expectedStatus: "payment_confirmed",
|
|
1009
|
+
});
|
|
1010
|
+
if (!transitioned) {
|
|
1011
|
+
await updateRenewalReviewMessage(ctx, "Request is no longer pending approval.");
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
const result = await renewCustomerAccess({
|
|
1015
|
+
telegramUserId: request.telegramUserId,
|
|
1016
|
+
planId,
|
|
1017
|
+
days,
|
|
1018
|
+
replaceKey,
|
|
1019
|
+
defaultClientRoute: deps.config.defaultCustomerRoute,
|
|
1020
|
+
identities,
|
|
1021
|
+
workspaces,
|
|
1022
|
+
customerKeys,
|
|
1023
|
+
billing,
|
|
1024
|
+
proxyClient: deps.proxyClient,
|
|
1025
|
+
auditLog,
|
|
1026
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
1027
|
+
});
|
|
1028
|
+
auditLog.record({
|
|
1029
|
+
event: "renewal.approved",
|
|
1030
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
1031
|
+
subjectType: "renewal_request",
|
|
1032
|
+
subjectId: requestId,
|
|
1033
|
+
metadata: {
|
|
1034
|
+
telegramUserId: request.telegramUserId,
|
|
1035
|
+
workspaceId: request.workspaceId,
|
|
1036
|
+
approvedPlanId: planId,
|
|
1037
|
+
approvedDays: days,
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1040
|
+
const canShowApiKeyToAdmin = !!result.apiKey && ctx.chat?.type === "private";
|
|
1041
|
+
if (canShowApiKeyToAdmin) {
|
|
1042
|
+
auditLog.record({
|
|
1043
|
+
event: "api_key.revealed",
|
|
1044
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
1045
|
+
subjectType: "customer_api_key",
|
|
1046
|
+
subjectId: result.keyId,
|
|
1047
|
+
metadata: {
|
|
1048
|
+
telegramUserId: request.telegramUserId,
|
|
1049
|
+
workspaceId: result.workspaceId,
|
|
1050
|
+
keyPreview: result.keyPreview,
|
|
1051
|
+
audience: "admin_private_chat",
|
|
1052
|
+
apiKey: result.apiKey,
|
|
1053
|
+
},
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
const customerNotified = await notifyCustomerAboutApprovedRenewal(ctx, {
|
|
1057
|
+
telegramUserId: request.telegramUserId,
|
|
1058
|
+
baseUrl: deps.config.publicResponsesBaseUrl,
|
|
1059
|
+
model: deps.config.defaultModel,
|
|
1060
|
+
planId,
|
|
1061
|
+
clientRoute: result.clientRoute,
|
|
1062
|
+
subscriptionEndsAt: result.subscriptionEndsAt,
|
|
1063
|
+
apiKey: result.apiKey,
|
|
1064
|
+
});
|
|
1065
|
+
if (result.apiKey && customerNotified) {
|
|
1066
|
+
auditLog.record({
|
|
1067
|
+
event: "api_key.revealed",
|
|
1068
|
+
actor: { type: "bot", id: "renewal-approval" },
|
|
1069
|
+
subjectType: "customer_api_key",
|
|
1070
|
+
subjectId: result.keyId,
|
|
1071
|
+
metadata: {
|
|
1072
|
+
telegramUserId: request.telegramUserId,
|
|
1073
|
+
workspaceId: result.workspaceId,
|
|
1074
|
+
keyPreview: result.keyPreview,
|
|
1075
|
+
audience: "customer_private_chat",
|
|
1076
|
+
apiKey: result.apiKey,
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
const message = [
|
|
1081
|
+
"Renewal request approved",
|
|
1082
|
+
formatSection("Request", [
|
|
1083
|
+
formatField("Request ID", requestId),
|
|
1084
|
+
formatField("Telegram user ID", request.telegramUserId),
|
|
1085
|
+
formatField("Plan ID", planId),
|
|
1086
|
+
formatField("Days", days),
|
|
1087
|
+
formatField("Mode", result.mode),
|
|
1088
|
+
formatField("Subscription ends at", formatDateTime(result.subscriptionEndsAt)),
|
|
1089
|
+
]),
|
|
1090
|
+
formatSection("Delivery", [
|
|
1091
|
+
canShowApiKeyToAdmin ? `api_key: ${result.apiKey}` : undefined,
|
|
1092
|
+
customerNotified ? "Customer notified: yes" : "Customer notified: pending manual follow-up",
|
|
1093
|
+
]),
|
|
1094
|
+
formatSection("Status", [formatField("Value", "approved")]),
|
|
1095
|
+
]
|
|
1096
|
+
.filter(Boolean)
|
|
1097
|
+
.join("\n\n");
|
|
1098
|
+
if (!silentReply) {
|
|
1099
|
+
await updateRenewalReviewMessage(ctx, message);
|
|
1100
|
+
}
|
|
1101
|
+
return { message };
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
return closeFailedApprovalRequest(ctx, billing, auditLog, request, requestId, error);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function notifyCustomerAboutApprovedRenewal(ctx, input) {
|
|
1108
|
+
if (input.apiKey) {
|
|
1109
|
+
return sendCustomerCodexSetup(ctx, {
|
|
1110
|
+
telegramUserId: input.telegramUserId,
|
|
1111
|
+
baseUrl: input.baseUrl,
|
|
1112
|
+
apiKey: input.apiKey,
|
|
1113
|
+
model: input.model,
|
|
1114
|
+
title: "Your access has been approved",
|
|
1115
|
+
details: [
|
|
1116
|
+
formatField("Plan ID", input.planId),
|
|
1117
|
+
formatField("Client route", input.clientRoute),
|
|
1118
|
+
formatField("Subscription ends at", formatDateTime(input.subscriptionEndsAt)),
|
|
1119
|
+
],
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
try {
|
|
1123
|
+
await ctx.api.sendMessage(Number(input.telegramUserId), [
|
|
1124
|
+
"Your access has been approved",
|
|
1125
|
+
"Run /apikey in this private chat to receive your Codex config files.",
|
|
1126
|
+
].join("\n\n"));
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
async function notifyCustomerAboutApprovedTokenTopUp(ctx, input) {
|
|
1134
|
+
try {
|
|
1135
|
+
await ctx.api.sendMessage(Number(input.telegramUserId), [
|
|
1136
|
+
"Your token top-up has been approved",
|
|
1137
|
+
formatSection("Tokens", [
|
|
1138
|
+
formatField("Token delta", formatNumber(input.tokenDelta)),
|
|
1139
|
+
formatField("Expires at", formatDateTime(input.expiresAt)),
|
|
1140
|
+
]),
|
|
1141
|
+
"Your existing API key can use this token lot.",
|
|
1142
|
+
].join("\n\n"));
|
|
1143
|
+
return true;
|
|
1144
|
+
}
|
|
1145
|
+
catch {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async function closeFailedApprovalRequest(ctx, billing, auditLog, request, requestId, error) {
|
|
1150
|
+
const currentRequest = billing.getRenewalRequest(requestId);
|
|
1151
|
+
if (currentRequest && currentRequest.status !== "closed") {
|
|
1152
|
+
billing.closeRenewalRequest({
|
|
1153
|
+
id: requestId,
|
|
1154
|
+
resolution: "approval_failed",
|
|
1155
|
+
});
|
|
1156
|
+
auditLog.record({
|
|
1157
|
+
event: "renewal.closed",
|
|
1158
|
+
actor: { type: "system", id: "renewal-approval-failed" },
|
|
1159
|
+
subjectType: "renewal_request",
|
|
1160
|
+
subjectId: requestId,
|
|
1161
|
+
metadata: {
|
|
1162
|
+
telegramUserId: currentRequest.telegramUserId,
|
|
1163
|
+
workspaceId: currentRequest.workspaceId,
|
|
1164
|
+
kind: currentRequest.kind,
|
|
1165
|
+
resolution: "approval_failed",
|
|
1166
|
+
priorStatus: currentRequest.status,
|
|
1167
|
+
},
|
|
1168
|
+
});
|
|
1169
|
+
try {
|
|
1170
|
+
await ctx.api.sendMessage(Number(currentRequest.telegramUserId), [
|
|
1171
|
+
currentRequest.kind === "token_topup"
|
|
1172
|
+
? "Your token top-up approval failed before tokens could be added."
|
|
1173
|
+
: "Your API key purchase approval failed before access could be provisioned.",
|
|
1174
|
+
`request_id: ${requestId}`,
|
|
1175
|
+
currentRequest.kind === "token_topup"
|
|
1176
|
+
? "The failed request was closed automatically. You can tap Buy tokens again."
|
|
1177
|
+
: "The failed request was closed automatically. You can tap Buy API key again.",
|
|
1178
|
+
].join("\n"));
|
|
1179
|
+
}
|
|
1180
|
+
catch {
|
|
1181
|
+
// best effort notification
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
await updateRenewalReviewMessage(ctx, [
|
|
1185
|
+
request.kind === "token_topup" ? "Token top-up approval failed." : "Renewal approval failed.",
|
|
1186
|
+
`request_id: ${requestId}`,
|
|
1187
|
+
"status: closed",
|
|
1188
|
+
"resolution: approval_failed",
|
|
1189
|
+
`error: ${getProxyErrorMessage(error)}`,
|
|
1190
|
+
request.kind === "token_topup"
|
|
1191
|
+
? "Customer can submit a new Buy tokens request."
|
|
1192
|
+
: "Customer can submit a new Buy API key request.",
|
|
1193
|
+
].join("\n"));
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
async function closeRenewalRequest(ctx, billing, auditLog, requestId, resolution = "closed_by_admin", silentReply = false, notifyCustomer = false) {
|
|
1197
|
+
const request = billing.getRenewalRequest(requestId);
|
|
1198
|
+
if (!request) {
|
|
1199
|
+
await updateRenewalReviewMessage(ctx, "Renewal request was not found.");
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
if (request.status === "approved" || request.status === "closed") {
|
|
1203
|
+
await updateRenewalReviewMessage(ctx, "Renewal request is not open anymore.");
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
const closed = billing.closeRenewalRequest({
|
|
1207
|
+
id: requestId,
|
|
1208
|
+
resolution,
|
|
1209
|
+
});
|
|
1210
|
+
auditLog.record({
|
|
1211
|
+
event: "renewal.closed",
|
|
1212
|
+
actor: { type: "admin", id: ctx.from?.id?.toString() },
|
|
1213
|
+
subjectType: "renewal_request",
|
|
1214
|
+
subjectId: requestId,
|
|
1215
|
+
metadata: {
|
|
1216
|
+
telegramUserId: request.telegramUserId,
|
|
1217
|
+
workspaceId: request.workspaceId,
|
|
1218
|
+
resolution: closed?.resolution ?? resolution,
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
const message = [
|
|
1222
|
+
"Renewal request closed.",
|
|
1223
|
+
`request_id: ${closed?.id ?? requestId}`,
|
|
1224
|
+
`status: ${closed?.status ?? "closed"}`,
|
|
1225
|
+
`resolution: ${closed?.resolution ?? resolution}`,
|
|
1226
|
+
];
|
|
1227
|
+
if (notifyCustomer) {
|
|
1228
|
+
try {
|
|
1229
|
+
await ctx.api.sendMessage(Number(request.telegramUserId), [
|
|
1230
|
+
"Your renewal request was not approved.",
|
|
1231
|
+
`request_id: ${closed?.id ?? requestId}`,
|
|
1232
|
+
`reason: ${closed?.resolution ?? resolution}`,
|
|
1233
|
+
"Contact support if you believe this needs another review.",
|
|
1234
|
+
].join("\n"));
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
message.push("customer_notification: pending_manual_follow_up");
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (!silentReply) {
|
|
1241
|
+
await updateRenewalReviewMessage(ctx, message.join("\n"));
|
|
1242
|
+
}
|
|
1243
|
+
return { message: message.join("\n") };
|
|
1244
|
+
}
|
|
1245
|
+
async function updateRenewalReviewMessage(ctx, text) {
|
|
1246
|
+
const callbackMessage = ctx.callbackQuery && "message" in ctx.callbackQuery ? ctx.callbackQuery.message : undefined;
|
|
1247
|
+
try {
|
|
1248
|
+
if (callbackMessage && "editMessageText" in ctx && typeof ctx.editMessageText === "function") {
|
|
1249
|
+
await ctx.editMessageText(text, {
|
|
1250
|
+
reply_markup: undefined,
|
|
1251
|
+
});
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (callbackMessage && "editMessageReplyMarkup" in ctx && typeof ctx.editMessageReplyMarkup === "function") {
|
|
1255
|
+
await ctx.editMessageReplyMarkup({ reply_markup: undefined });
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
catch {
|
|
1259
|
+
// best effort cleanup
|
|
1260
|
+
}
|
|
1261
|
+
await ctx.reply(text);
|
|
1262
|
+
}
|
|
1263
|
+
async function renderRenewalPromptCard(ctx, stateStore, request, text, mode) {
|
|
1264
|
+
try {
|
|
1265
|
+
if ("editMessageText" in ctx && typeof ctx.editMessageText === "function") {
|
|
1266
|
+
await ctx.editMessageText(text, {
|
|
1267
|
+
reply_markup: mode === "reject"
|
|
1268
|
+
? buildRejectReasonKeyboard(stateStore, request)
|
|
1269
|
+
: mode === "prompt"
|
|
1270
|
+
? buildRenewalPromptKeyboard(stateStore, request)
|
|
1271
|
+
: buildAdminRenewalKeyboard(stateStore, request),
|
|
1272
|
+
});
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if ("editMessageReplyMarkup" in ctx && typeof ctx.editMessageReplyMarkup === "function") {
|
|
1276
|
+
await ctx.editMessageReplyMarkup({
|
|
1277
|
+
reply_markup: mode === "reject"
|
|
1278
|
+
? buildRejectReasonKeyboard(stateStore, request)
|
|
1279
|
+
: mode === "prompt"
|
|
1280
|
+
? buildRenewalPromptKeyboard(stateStore, request)
|
|
1281
|
+
: buildAdminRenewalKeyboard(stateStore, request),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
catch {
|
|
1286
|
+
// best effort cleanup
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
async function renderRenewalCustomerContext(ctx, stateStore, request, text) {
|
|
1290
|
+
try {
|
|
1291
|
+
if ("editMessageText" in ctx && typeof ctx.editMessageText === "function") {
|
|
1292
|
+
await ctx.editMessageText(text, {
|
|
1293
|
+
reply_markup: buildRenewalCustomerKeyboard(stateStore, request),
|
|
1294
|
+
});
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
catch {
|
|
1299
|
+
// best effort cleanup
|
|
1300
|
+
}
|
|
1301
|
+
await ctx.reply(text, {
|
|
1302
|
+
reply_markup: buildRenewalCustomerKeyboard(stateStore, request),
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
async function updateRenewalReviewMessageByRef(ctx, chatId, messageId, text) {
|
|
1306
|
+
try {
|
|
1307
|
+
await ctx.api.editMessageText(Number(chatId), messageId, text, {
|
|
1308
|
+
reply_markup: undefined,
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
catch {
|
|
1312
|
+
// best effort cleanup
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
async function renderRenewalPromptCardByRef(ctx, stateStore, request, chatId, messageId, text, mode) {
|
|
1316
|
+
if (!request) {
|
|
1317
|
+
await updateRenewalReviewMessageByRef(ctx, chatId, messageId, text);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
await ctx.api.editMessageText(Number(chatId), messageId, text, {
|
|
1322
|
+
reply_markup: mode === "reject"
|
|
1323
|
+
? buildRejectReasonKeyboard(stateStore, request)
|
|
1324
|
+
: mode === "prompt"
|
|
1325
|
+
? buildRenewalPromptKeyboard(stateStore, request)
|
|
1326
|
+
: buildAdminRenewalKeyboard(stateStore, request),
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
catch {
|
|
1330
|
+
// best effort cleanup
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async function showRenewalRejectKeyboard(ctx, stateStore, request) {
|
|
1334
|
+
await renderRenewalPromptCard(ctx, stateStore, request, [
|
|
1335
|
+
"Renewal review",
|
|
1336
|
+
`request_id: ${request.id}`,
|
|
1337
|
+
"Choose a rejection reason.",
|
|
1338
|
+
].join("\n"), "reject");
|
|
1339
|
+
}
|
|
1340
|
+
async function showRenewalMainKeyboard(ctx, stateStore, request, text) {
|
|
1341
|
+
await renderRenewalPromptCard(ctx, stateStore, request, text, "main");
|
|
1342
|
+
}
|
|
1343
|
+
function prepareRenewalAdminInput(ctx) {
|
|
1344
|
+
const chatId = ctx.chat?.id?.toString();
|
|
1345
|
+
const userId = ctx.from?.id?.toString();
|
|
1346
|
+
const message = ctx.callbackQuery?.message;
|
|
1347
|
+
const messageId = typeof message?.message_id === "number" ? message.message_id : undefined;
|
|
1348
|
+
if (!chatId || !userId || messageId === undefined) {
|
|
1349
|
+
return undefined;
|
|
1350
|
+
}
|
|
1351
|
+
return { chatId, userId, messageId };
|
|
1352
|
+
}
|
|
1353
|
+
function formatRenewalRequestLine(request) {
|
|
1354
|
+
return [
|
|
1355
|
+
`- ${request.id}`,
|
|
1356
|
+
`kind=${request.kind}`,
|
|
1357
|
+
`telegram_user_id=${request.telegramUserId}`,
|
|
1358
|
+
request.requestedPlanId ? `plan_id=${request.requestedPlanId}` : undefined,
|
|
1359
|
+
request.requestedDays ? `days=${request.requestedDays}` : undefined,
|
|
1360
|
+
request.requestedTokenDelta ? `token_delta=${request.requestedTokenDelta}` : undefined,
|
|
1361
|
+
request.requestedTokenLotDays ? `token_lot_days=${request.requestedTokenLotDays}` : undefined,
|
|
1362
|
+
`requested_at=${formatDateTime(request.requestedAt)}`,
|
|
1363
|
+
]
|
|
1364
|
+
.filter(Boolean)
|
|
1365
|
+
.join(" ");
|
|
1366
|
+
}
|
|
1367
|
+
function issueRenewalCallbackToken(stateStore, payload) {
|
|
1368
|
+
return stateStore.issueCallbackToken(payload, Date.now(), RENEWAL_CALLBACK_TOKEN_TTL_MS);
|
|
1369
|
+
}
|