feat: add expiring reset card wallet
This commit is contained in:
@@ -81,6 +81,13 @@ const TOKEN_POINT_METADATA_FIELDS = [
|
||||
const TOKEN_POINT_ENTITLEMENT_SOURCES = new Set(['self', 'family_owner', 'shared_group']);
|
||||
const TOKEN_POINT_UPGRADE_ACTIONS = new Set(['self_service', 'contact_family_owner']);
|
||||
const TOKEN_POINT_VALUE_PATTERN = /^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/u;
|
||||
const RESET_CARD_STATUSES = new Set(['available', 'expired', 'redeemed']);
|
||||
const RESET_CARD_ERROR_MESSAGES: Record<string, string> = {
|
||||
reset_card_expired: '这张重置卡已过期。',
|
||||
token_point_wallet_owner_required: '当前额度由家庭管理员管理,无法使用这张重置卡。',
|
||||
billing_temporarily_paused: '计费服务正在切换,请稍后再试。',
|
||||
token_point_transaction_conflict: '重置卡状态刚刚发生变化,请刷新后重试。',
|
||||
};
|
||||
|
||||
function readRequiredString(value: unknown, field: string): string {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
@@ -168,7 +175,11 @@ function getErrorMessage(payload: unknown, fallback: string): string {
|
||||
function getPayloadErrorCode(payload: unknown): string | undefined {
|
||||
if (!isRecord(payload)) return undefined;
|
||||
const detail = isRecord(payload.detail) ? payload.detail : undefined;
|
||||
return readOptionalString(payload.code) ?? (detail ? readOptionalString(detail.code) : undefined);
|
||||
return readOptionalString(payload.code)
|
||||
?? readOptionalString(payload.error_code)
|
||||
?? (detail
|
||||
? readOptionalString(detail.code) ?? readOptionalString(detail.error_code)
|
||||
: undefined);
|
||||
}
|
||||
|
||||
function stripReadOnlyAgentProfileFields(body: AgentProfileUpdateInput): AgentProfileUpdateInput {
|
||||
@@ -190,6 +201,31 @@ async function sendUpstreamError(
|
||||
});
|
||||
}
|
||||
|
||||
async function sendResetCardUpstreamError(
|
||||
res: ServerResponse,
|
||||
response: Response,
|
||||
): Promise<void> {
|
||||
const payload = await readResponsePayload(response);
|
||||
const upstreamCode = getPayloadErrorCode(payload);
|
||||
const code = upstreamCode && RESET_CARD_ERROR_MESSAGES[upstreamCode]
|
||||
? upstreamCode
|
||||
: undefined;
|
||||
const status = response.status >= 400 && response.status < 500 ? response.status : 502;
|
||||
const error = code
|
||||
? RESET_CARD_ERROR_MESSAGES[code]
|
||||
: response.status === 404
|
||||
? '重置卡不存在或不可用。'
|
||||
: response.status === 409
|
||||
? '当前有待处理的计费操作,暂时不能使用重置卡。'
|
||||
: '重置卡服务暂时不可用,请稍后重试。';
|
||||
sendJson(res, status, {
|
||||
success: false,
|
||||
status: response.status,
|
||||
...(code ? { code } : {}),
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
function sendPublishSourceFailure(
|
||||
res: ServerResponse,
|
||||
status: number,
|
||||
@@ -344,6 +380,56 @@ function projectSafeTokenPointBalance(value: unknown): Record<string, unknown> |
|
||||
};
|
||||
}
|
||||
|
||||
function readResetCardTimestamp(value: unknown): string | null {
|
||||
if (typeof value !== 'string' || !value.trim()) return null;
|
||||
const normalized = value.trim();
|
||||
return Number.isNaN(Date.parse(normalized)) ? null : normalized;
|
||||
}
|
||||
|
||||
function projectSafeResetCard(value: unknown): Record<string, unknown> | null {
|
||||
if (!isRecord(value)) return null;
|
||||
const id = readOptionalString(value.id);
|
||||
const status = readOptionalString(value.status);
|
||||
const grantedAt = readResetCardTimestamp(value.granted_at);
|
||||
const expiresAt = readResetCardTimestamp(value.expires_at);
|
||||
const redeemedAt = value.redeemed_at === null
|
||||
? null
|
||||
: readResetCardTimestamp(value.redeemed_at);
|
||||
const redeemedCycleId = readNullableStringField(value, 'redeemed_cycle_id');
|
||||
if (
|
||||
!id
|
||||
|| !status
|
||||
|| !RESET_CARD_STATUSES.has(status)
|
||||
|| !grantedAt
|
||||
|| !expiresAt
|
||||
|| redeemedAt === null && value.redeemed_at !== null
|
||||
|| redeemedCycleId === undefined
|
||||
) return null;
|
||||
|
||||
if (status === 'redeemed') {
|
||||
if (!redeemedAt || !redeemedCycleId) return null;
|
||||
} else if (redeemedAt !== null || redeemedCycleId !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
status: status === 'available' && Date.parse(expiresAt) <= Date.now() ? 'expired' : status,
|
||||
granted_at: grantedAt,
|
||||
expires_at: expiresAt,
|
||||
redeemed_at: redeemedAt,
|
||||
redeemed_cycle_id: redeemedCycleId,
|
||||
};
|
||||
}
|
||||
|
||||
function projectSafeResetCardPage(value: unknown): Record<string, unknown>[] | null {
|
||||
if (!isRecord(value) || !Array.isArray(value.items)) return null;
|
||||
const cards = value.items.map(projectSafeResetCard);
|
||||
return cards.some((card) => card === null)
|
||||
? null
|
||||
: cards as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
function projectSafeProject(
|
||||
value: unknown,
|
||||
options: { requireStatus?: boolean } = {},
|
||||
@@ -716,6 +802,60 @@ async function handleGetBillingTokenPoints(
|
||||
sendJson(res, response.status, { success: true, points });
|
||||
}
|
||||
|
||||
async function handleListBillingResetCards(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
): Promise<void> {
|
||||
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
|
||||
const response = await proxyAwareFetch(createWorksUrl('/api/billing/reset-cards').toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
await sendResetCardUpstreamError(res, response);
|
||||
return;
|
||||
}
|
||||
|
||||
const cards = projectSafeResetCardPage(await readResponsePayload(response));
|
||||
if (!cards) {
|
||||
sendJson(res, 502, { success: false, error: 'Works Square returned an invalid reset-card list' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, response.status, { success: true, cards });
|
||||
}
|
||||
|
||||
async function handleRedeemBillingResetCard(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
cardId: string,
|
||||
): Promise<void> {
|
||||
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
|
||||
const response = await proxyAwareFetch(
|
||||
createWorksUrl(`/api/billing/reset-cards/${encodeURIComponent(readRequiredString(cardId, 'card_id'))}/redeem`).toString(),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
await sendResetCardUpstreamError(res, response);
|
||||
return;
|
||||
}
|
||||
|
||||
const card = projectSafeResetCard(await readResponsePayload(response));
|
||||
if (!card) {
|
||||
sendJson(res, 502, { success: false, error: 'Works Square returned an invalid reset-card result' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, response.status, { success: true, card });
|
||||
}
|
||||
|
||||
async function handleAgentProfile(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
@@ -1500,6 +1640,17 @@ export async function handleWorksRoutes(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/works/billing/reset-cards' && req.method === 'GET') {
|
||||
await handleListBillingResetCards(req, res);
|
||||
return true;
|
||||
}
|
||||
|
||||
const resetCardRedeemMatch = url.pathname.match(/^\/api\/works\/billing\/reset-cards\/([^/]+)\/redeem$/);
|
||||
if (resetCardRedeemMatch && req.method === 'POST') {
|
||||
await handleRedeemBillingResetCard(req, res, decodeURIComponent(resetCardRedeemMatch[1]));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/works/ai-gateway/images/generations' && req.method === 'POST') {
|
||||
await handleSubmitImageGeneration(req, res);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user