feat: add redeem billing loop
This commit is contained in:
@@ -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 ?? ""),
|
||||
|
||||
Reference in New Issue
Block a user