feat: integrate token point usage v2
This commit is contained in:
@@ -60,6 +60,27 @@ const AGENT_AVATAR_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp'
|
||||
const MAX_PROJECT_COVER_BYTES = 10 * 1024 * 1024;
|
||||
const PROJECT_COVER_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp']);
|
||||
const SAFE_PROJECT_STATUSES = new Set(['draft', 'published']);
|
||||
const TOKEN_POINT_FIELDS = [
|
||||
'weekly_allowance',
|
||||
'weekly_used',
|
||||
'weekly_reserved',
|
||||
'weekly_remaining',
|
||||
'permanent_total',
|
||||
'permanent_used',
|
||||
'permanent_reserved',
|
||||
'permanent_remaining',
|
||||
'total_remaining',
|
||||
] as const;
|
||||
const TOKEN_POINT_METADATA_FIELDS = [
|
||||
'plan_code',
|
||||
'plan_name',
|
||||
'cycle_start',
|
||||
'cycle_end',
|
||||
'next_refresh_at',
|
||||
] as const;
|
||||
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;
|
||||
|
||||
function readRequiredString(value: unknown, field: string): string {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
@@ -267,6 +288,62 @@ function readNullableStringField(
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
|
||||
function readNullablePointField(
|
||||
source: Record<string, unknown>,
|
||||
field: string,
|
||||
): string | null | undefined {
|
||||
const value = source[field];
|
||||
if (value === undefined || value === null) return null;
|
||||
if (typeof value !== 'string') return undefined;
|
||||
const normalized = value.trim();
|
||||
return TOKEN_POINT_VALUE_PATTERN.test(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function projectSafeTokenPointBalance(value: unknown): Record<string, unknown> | null {
|
||||
if (!isRecord(value)) return null;
|
||||
|
||||
const entitlementSource = readOptionalString(value.entitlement_source);
|
||||
const upgradeAction = readOptionalString(value.upgrade_action);
|
||||
if (
|
||||
!entitlementSource
|
||||
|| !TOKEN_POINT_ENTITLEMENT_SOURCES.has(entitlementSource)
|
||||
|| typeof value.family_shared !== 'boolean'
|
||||
|| typeof value.can_manage_membership !== 'boolean'
|
||||
|| !upgradeAction
|
||||
|| !TOKEN_POINT_UPGRADE_ACTIONS.has(upgradeAction)
|
||||
) return null;
|
||||
|
||||
if (
|
||||
value.shared_available !== undefined
|
||||
&& value.shared_available !== null
|
||||
&& typeof value.shared_available !== 'boolean'
|
||||
) return null;
|
||||
|
||||
const canManageMembership = value.can_manage_membership;
|
||||
if (!canManageMembership && typeof value.shared_available !== 'boolean') return null;
|
||||
|
||||
const projected: Record<string, unknown> = {};
|
||||
for (const field of TOKEN_POINT_METADATA_FIELDS) {
|
||||
const fieldValue = canManageMembership ? readNullableStringField(value, field) : null;
|
||||
if (fieldValue === undefined) return null;
|
||||
projected[field] = fieldValue;
|
||||
}
|
||||
for (const field of TOKEN_POINT_FIELDS) {
|
||||
const fieldValue = canManageMembership ? readNullablePointField(value, field) : null;
|
||||
if (fieldValue === undefined) return null;
|
||||
projected[field] = fieldValue;
|
||||
}
|
||||
|
||||
return {
|
||||
...projected,
|
||||
entitlement_source: entitlementSource,
|
||||
family_shared: value.family_shared,
|
||||
can_manage_membership: canManageMembership,
|
||||
upgrade_action: upgradeAction,
|
||||
shared_available: canManageMembership ? null : value.shared_available,
|
||||
};
|
||||
}
|
||||
|
||||
function projectSafeProject(
|
||||
value: unknown,
|
||||
options: { requireStatus?: boolean } = {},
|
||||
@@ -614,12 +691,12 @@ async function handleListMyProjects(
|
||||
sendJson(res, response.status, { success: true, page });
|
||||
}
|
||||
|
||||
async function handleGetBillingTokenUsage(
|
||||
async function handleGetBillingTokenPoints(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
): Promise<void> {
|
||||
const accessToken = readRequiredHeader(req, 'x-niancode-access-token');
|
||||
const response = await proxyAwareFetch(createWorksUrl('/api/billing/token-usage').toString(), {
|
||||
const response = await proxyAwareFetch(createWorksUrl('/api/billing/points').toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
@@ -627,11 +704,16 @@ async function handleGetBillingTokenUsage(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
await sendUpstreamError(res, response, `Works Square token usage failed (${response.status})`);
|
||||
await sendUpstreamError(res, response, `Works Square token points failed (${response.status})`);
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(res, response.status, { success: true, usage: await readResponsePayload(response) });
|
||||
const points = projectSafeTokenPointBalance(await readResponsePayload(response));
|
||||
if (!points) {
|
||||
sendJson(res, 502, { success: false, error: 'Works Square returned an invalid token point balance' });
|
||||
return;
|
||||
}
|
||||
sendJson(res, response.status, { success: true, points });
|
||||
}
|
||||
|
||||
async function handleAgentProfile(
|
||||
@@ -1413,8 +1495,8 @@ export async function handleWorksRoutes(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/works/billing/token-usage' && req.method === 'GET') {
|
||||
await handleGetBillingTokenUsage(req, res);
|
||||
if (url.pathname === '/api/works/billing/points' && req.method === 'GET') {
|
||||
await handleGetBillingTokenPoints(req, res);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user