feat(billing): add products subscriptions orders and usage

This commit is contained in:
Jesse_Chen
2026-08-06 20:15:08 +08:00
parent 6457c836fa
commit 7040fd998e
22 changed files with 2439 additions and 277 deletions
+100 -30
View File
@@ -1,8 +1,7 @@
import "server-only";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import type { IdentityUser } from "@/modules/identity/contracts";
import type { AdminRole } from "./auth";
import { queryAdminRows } from "./database";
import type { AdminSession } from "./auth";
export type RedemptionCodeRecord = {
id: string;
@@ -23,57 +22,128 @@ type RpcCodeRow = {
id: string;
code_mask: string;
credits: number;
expires_at: string | null;
expires_at: Date | string | null;
note: string | null;
created_at: string;
created_at: Date | string;
redeemed_by: string | null;
redeemed_email: string | null;
redeemed_at: string | null;
redeemed_at: Date | string | null;
revoked_by: string | null;
revoked_at: string | null;
revoked_at: Date | string | null;
};
type CodeRpcArgs = {
admin_create_redemption_codes: {
p_codes: unknown;
p_reason: string;
};
admin_update_redemption_code: {
p_code_id: string;
p_set_note: boolean;
p_note: string | null;
p_set_expires_at: boolean;
p_expires_at: string | null;
p_reason: string;
};
admin_revoke_redemption_code: {
p_code_id: string;
p_reason: string;
};
};
function isoTimestamp(value: Date | string | null): string | null {
return value instanceof Date ? value.toISOString() : value;
}
export function codeStatus(row: RpcCodeRow): RedemptionCodeRecord["status"] {
const expiresAt = isoTimestamp(row.expires_at);
if (row.redeemed_at) return "redeemed";
if (row.revoked_at) return "revoked";
if (row.expires_at && Date.parse(row.expires_at) <= Date.now()) return "expired";
if (expiresAt && Date.parse(expiresAt) <= Date.now()) return "expired";
return "available";
}
export function mapCode(row: RpcCodeRow): RedemptionCodeRecord {
const expiresAt = isoTimestamp(row.expires_at);
const redeemedAt = isoTimestamp(row.redeemed_at);
const revokedAt = isoTimestamp(row.revoked_at);
return {
id: row.id,
mask: row.code_mask,
credits: row.credits,
expiresAt: row.expires_at,
expiresAt,
note: row.note,
createdAt: row.created_at,
createdAt: isoTimestamp(row.created_at)!,
redeemedBy: row.redeemed_by,
redeemedEmail: row.redeemed_email,
redeemedAt: row.redeemed_at,
redeemedAt,
revokedBy: row.revoked_by,
revokedAt: row.revoked_at,
status: codeStatus(row),
revokedAt,
status: codeStatus({
...row,
expires_at: expiresAt,
created_at: isoTimestamp(row.created_at)!,
redeemed_at: redeemedAt,
revoked_at: revokedAt,
}),
};
}
export async function runCodeRpc(
functionName:
| "admin_create_redemption_codes"
| "admin_update_redemption_code"
| "admin_revoke_redemption_code",
session: { user: IdentityUser; role: AdminRole },
export async function runCodeRpc<T extends keyof CodeRpcArgs>(
functionName: T,
session: Pick<AdminSession, "user" | "roles">,
id: string,
args: Record<string, unknown>,
args: CodeRpcArgs[T],
): Promise<RedemptionCodeRecord[]> {
const admin = createAdminSupabaseClient();
const { data, error } = await admin.rpc(functionName, {
p_actor_user_id: session.user.id,
p_actor_email: session.user.email,
p_actor_role: session.role,
p_request_id: id,
...args,
});
if (error) throw new Error(error.message);
return ((data ?? []) as RpcCodeRow[]).map(mapCode);
const common = [
session.user.id,
session.user.email,
session.roles[0] ?? "admin",
id,
];
let rows: RpcCodeRow[] = [];
switch (functionName) {
case "admin_create_redemption_codes": {
const input = args as CodeRpcArgs["admin_create_redemption_codes"];
rows = await queryAdminRows<RpcCodeRow>(
`select * from public.admin_create_redemption_codes(
$1::uuid,$2::text,$3::text,$4::text,$5::jsonb,$6::text
)`,
[...common, input.p_codes, input.p_reason],
);
break;
}
case "admin_update_redemption_code": {
const input = args as CodeRpcArgs["admin_update_redemption_code"];
rows = await queryAdminRows<RpcCodeRow>(
`select * from public.admin_update_redemption_code(
$1::uuid,$2::text,$3::text,$4::text,$5::uuid,
$6::boolean,$7::text,$8::boolean,$9::timestamptz,$10::text
)`,
[
...common,
input.p_code_id,
input.p_set_note,
input.p_note,
input.p_set_expires_at,
input.p_expires_at,
input.p_reason,
],
);
break;
}
case "admin_revoke_redemption_code": {
const input = args as CodeRpcArgs["admin_revoke_redemption_code"];
rows = await queryAdminRows<RpcCodeRow>(
`select * from public.admin_revoke_redemption_code(
$1::uuid,$2::text,$3::text,$4::text,$5::uuid,$6::text
)`,
[...common, input.p_code_id, input.p_reason],
);
break;
}
}
return rows.map(mapCode);
}