feat: add redeem billing loop

This commit is contained in:
lyon
2026-06-14 21:21:01 +08:00
parent acd15ed5b0
commit ea96ebf75d
13 changed files with 1325 additions and 2 deletions
+148
View File
@@ -320,6 +320,21 @@ async function handleRestAdapter(request, response, url, options) {
return;
}
if (url.pathname === "/v1/redeem" && request.method === "POST") {
await handleRedeemHttp(request, response, options);
return;
}
if (url.pathname === "/v1/subscription/summary" && request.method === "GET") {
await handleSubscriptionSummaryHttp(request, response, options);
return;
}
if (url.pathname === "/v1/payments/summary" && request.method === "GET") {
await handlePaymentSummaryHttp(request, response, options);
return;
}
if (url.pathname === "/v1/admin/billing/summary" && request.method === "GET") {
await handleAdminBillingSummaryHttp(request, response, url, options);
return;
@@ -350,6 +365,22 @@ async function handleRestAdapter(request, response, url, options) {
return;
}
if (url.pathname === "/v1/admin/billing/redeem-codes" && (request.method === "GET" || request.method === "POST")) {
await handleAdminBillingRedeemCodesHttp(request, response, url, options);
return;
}
const adminBillingRedeemCodeMatch = url.pathname.match(/^\/v1\/admin\/billing\/redeem-codes\/([^/]+)$/u);
if (adminBillingRedeemCodeMatch && request.method === "PATCH") {
await handleAdminBillingRedeemCodeHttp(request, response, decodeURIComponent(adminBillingRedeemCodeMatch[1]), options);
return;
}
if (url.pathname === "/v1/admin/billing/redeem-redemptions" && request.method === "GET") {
await handleAdminBillingRedeemRedemptionsHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/admin/billing/users" && (request.method === "GET" || request.method === "POST")) {
await handleAdminBillingUsersHttp(request, response, url, options);
return;
@@ -1119,6 +1150,64 @@ async function requireAdminBillingClient(request, response, options, operation)
return { auth, client };
}
async function requireUserBillingClient(request, response, options, operation) {
const nextOptions = await codeAgentOptions(request, response, options, { required: true });
if (!nextOptions) return null;
const client = nextOptions.userBillingClient;
const token = userBillingTokenFromRequest(request);
if (nextOptions.userBillingAuth?.active !== true || !token) {
sendRestError(request, response, 403, "user_billing_auth_required", "user-billing session or API key is required", {
operation,
target: { type: "route", id: operation },
result: "rejected"
});
return null;
}
if (!client?.configured) {
sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing API is not configured", {
operation,
target: { type: "service", id: "hwlab-user-billing" }
});
return null;
}
return { auth: nextOptions, client, token };
}
async function handleRedeemHttp(request, response, options) {
const context = await requireUserBillingClient(request, response, options, "POST /v1/redeem");
if (!context) return;
const { auth, client, token } = context;
if (typeof client.redeem !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing redeem API is not configured", { operation: "POST /v1/redeem", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.redeem(token, parsed.value);
sendUserBillingProxyResult({ request, response, result, operation: "POST /v1/redeem", route: "/v1/redeem", actor: adminActorSummary(auth.actor) });
}
async function handleSubscriptionSummaryHttp(request, response, options) {
const context = await requireUserBillingClient(request, response, options, "GET /v1/subscription/summary");
if (!context) return;
const { auth, client, token } = context;
if (typeof client.subscriptionSummary !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing subscription API is not configured", { operation: "GET /v1/subscription/summary", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.subscriptionSummary(token);
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/subscription/summary", route: "/v1/subscription/summary", actor: adminActorSummary(auth.actor) });
}
async function handlePaymentSummaryHttp(request, response, options) {
const context = await requireUserBillingClient(request, response, options, "GET /v1/payments/summary");
if (!context) return;
const { auth, client, token } = context;
if (typeof client.paymentSummary !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing payment API is not configured", { operation: "GET /v1/payments/summary", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.paymentSummary(token);
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/payments/summary", route: "/v1/payments/summary", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingUsersHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, `${request.method} /v1/admin/billing/users`);
if (!context) return;
@@ -1201,6 +1290,56 @@ async function handleAdminBillingLedgerExportHttp(request, response, url, option
sendUserBillingCsvProxyResult({ request, response, result, operation: "GET /v1/admin/billing/ledger/export", filename: "hwlab-admin-ledger.csv" });
}
async function handleAdminBillingRedeemCodesHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, `${request.method} /v1/admin/billing/redeem-codes`);
if (!context) return;
const { auth, client } = context;
let result;
if (request.method === "GET") {
if (typeof client.adminRedeemCodes !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin redeem code API is not configured", { operation: "GET /v1/admin/billing/redeem-codes", target: { type: "service", id: "hwlab-user-billing" } });
}
result = await client.adminRedeemCodes(adminRedeemParams(url));
} else {
if (typeof client.createAdminRedeemCode !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin redeem code create API is not configured", { operation: "POST /v1/admin/billing/redeem-codes", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
result = await client.createAdminRedeemCode({
...parsed.value,
createdByUserId: String(auth.actor?.id ?? ""),
createdByUsername: String(auth.actor?.username ?? ""),
metadata: { ...(parsed.value?.metadata && typeof parsed.value.metadata === "object" && !Array.isArray(parsed.value.metadata) ? parsed.value.metadata : {}), operator: adminActorSummary(auth.actor) }
});
}
sendUserBillingProxyResult({ request, response, result, operation: `${request.method} /v1/admin/billing/redeem-codes`, route: "/v1/admin/billing/redeem-codes", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingRedeemCodeHttp(request, response, codeId, options) {
const context = await requireAdminBillingClient(request, response, options, "PATCH /v1/admin/billing/redeem-codes/{codeId}");
if (!context) return;
const { auth, client } = context;
if (typeof client.updateAdminRedeemCode !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin redeem code update API is not configured", { operation: "PATCH /v1/admin/billing/redeem-codes/{codeId}", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.updateAdminRedeemCode(codeId, parsed.value);
sendUserBillingProxyResult({ request, response, result, operation: "PATCH /v1/admin/billing/redeem-codes/{codeId}", route: "/v1/admin/billing/redeem-codes/{codeId}", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingRedeemRedemptionsHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, "GET /v1/admin/billing/redeem-redemptions");
if (!context) return;
const { auth, client } = context;
if (typeof client.adminRedeemRedemptions !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin redeem redemptions API is not configured", { operation: "GET /v1/admin/billing/redeem-redemptions", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.adminRedeemRedemptions(adminRedeemParams(url));
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/admin/billing/redeem-redemptions", route: "/v1/admin/billing/redeem-redemptions", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingUserHttp(request, response, url, userId, options) {
const context = await requireAdminBillingClient(request, response, options, `${request.method} /v1/admin/billing/users/{userId}`);
if (!context) return;
@@ -1425,6 +1564,15 @@ function adminAuditParams(url) {
return params;
}
function adminRedeemParams(url) {
const params = {};
for (const key of ["limit", "status", "userId", "codeId"]) {
const value = String(url.searchParams.get(key) ?? "").trim();
if (value) params[key] = value;
}
return params;
}
function adminActorSummary(actor) {
return actor ? {
id: String(actor.id ?? ""),