106 lines
4.6 KiB
TypeScript
106 lines
4.6 KiB
TypeScript
import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
|
|
import { requireAdmin, requireManager } from '../_shared/admin-auth.ts'
|
|
import { createServiceRoleClient } from '../_shared/quota.ts'
|
|
import { verifyCurrentAdminActor } from '../_shared/admin-current-role.ts'
|
|
import {
|
|
AdminPublicError,
|
|
type SubscriptionAction,
|
|
actorEmail,
|
|
adminErrorResponse,
|
|
adminJsonResponse,
|
|
adminRpcError,
|
|
parsePagination,
|
|
parseSubscriptionMutationRequest,
|
|
readAdminJson,
|
|
requireIdempotencyKey,
|
|
requireUuid,
|
|
subscriptionStatusFilter,
|
|
subscriptionTierFilter,
|
|
validateQueryKeys,
|
|
validateSubscriptionRpcResult,
|
|
} from '../_shared/admin-contract.ts'
|
|
|
|
const SUBSCRIPTION_COLUMNS = 'id, user_id, tier, status, provider, payment_provider, current_period_start, current_period_end, overage_credits, admin_note, cancel_at, created_at, updated_at'
|
|
|
|
async function readSubscriptions(req: Request): Promise<Response> {
|
|
const admin = await requireManager(req)
|
|
const url = new URL(req.url)
|
|
validateQueryKeys(url, ['userId', 'page', 'limit', 'status', 'tier'])
|
|
const serviceClient = createServiceRoleClient()
|
|
await verifyCurrentAdminActor(serviceClient, admin)
|
|
const userIdValue = url.searchParams.get('userId')
|
|
|
|
if (userIdValue) {
|
|
const userId = requireUuid(userIdValue, 'invalid_userId')
|
|
const [{ data: subscription, error: subscriptionError }, { data: profile, error: profileError }] =
|
|
await Promise.all([
|
|
serviceClient.from('subscriptions').select(SUBSCRIPTION_COLUMNS).eq('user_id', userId).maybeSingle(),
|
|
serviceClient.from('profiles').select('id, name, tier, role').eq('id', userId).maybeSingle(),
|
|
])
|
|
if (subscriptionError || profileError) throw new AdminPublicError(500, 'admin_subscription_read_failed')
|
|
if (!subscription) throw new AdminPublicError(404, 'subscription_not_found')
|
|
return adminJsonResponse({ subscription, profile: profile ?? null }, corsHeaders)
|
|
}
|
|
|
|
const { page, limit, from, to } = parsePagination(url)
|
|
const status = subscriptionStatusFilter(url.searchParams.get('status'))
|
|
const tier = subscriptionTierFilter(url.searchParams.get('tier'))
|
|
let query = serviceClient
|
|
.from('subscriptions')
|
|
.select(`${SUBSCRIPTION_COLUMNS}, profiles!subscriptions_user_id_fkey(name, avatar_url)`, { count: 'exact' })
|
|
if (status) query = query.eq('status', status)
|
|
if (tier) query = query.eq('tier', tier)
|
|
|
|
const { data, count, error } = await query
|
|
.order('updated_at', { ascending: false })
|
|
.range(from, to)
|
|
if (error) throw new AdminPublicError(500, 'admin_subscription_read_failed')
|
|
return adminJsonResponse({ subscriptions: data ?? [], total: count ?? 0, page, limit }, corsHeaders)
|
|
}
|
|
|
|
async function mutateSubscription(req: Request, action: SubscriptionAction): Promise<Response> {
|
|
const admin = action === 'update' ? await requireManager(req) : await requireAdmin(req)
|
|
const url = new URL(req.url)
|
|
validateQueryKeys(url, action === 'create' ? [] : ['userId'])
|
|
const body = await readAdminJson(req)
|
|
const mutation = parseSubscriptionMutationRequest(
|
|
action,
|
|
body,
|
|
requireIdempotencyKey(req),
|
|
url.searchParams.get('userId'),
|
|
)
|
|
const serviceClient = createServiceRoleClient()
|
|
const { data, error } = await serviceClient.rpc('admin_mutate_subscription_v1', {
|
|
p_actor_email: actorEmail(admin),
|
|
p_idempotency_key: mutation.idempotencyKey,
|
|
p_action: mutation.action,
|
|
p_user_id: mutation.userId,
|
|
p_tier: mutation.tier ?? null,
|
|
p_status: mutation.status ?? null,
|
|
p_current_period_end: mutation.currentPeriodEnd ?? null,
|
|
p_overage_credits: mutation.overageCredits ?? null,
|
|
p_admin_note: mutation.adminNote ?? null,
|
|
p_memo: mutation.memo,
|
|
})
|
|
if (error) throw adminRpcError(error)
|
|
const response = validateSubscriptionRpcResult(data, mutation)
|
|
return adminJsonResponse(response, corsHeaders, action === 'create' ? 201 : 200)
|
|
}
|
|
|
|
export async function handleAdminSubscriptions(req: Request): Promise<Response> {
|
|
const preflight = handleCorsPreflightRequest(req)
|
|
if (preflight) return preflight
|
|
|
|
try {
|
|
if (req.method === 'GET') return await readSubscriptions(req)
|
|
if (req.method === 'POST') return await mutateSubscription(req, 'create')
|
|
if (req.method === 'PATCH') return await mutateSubscription(req, 'update')
|
|
if (req.method === 'DELETE') return await mutateSubscription(req, 'delete')
|
|
return adminJsonResponse({ error: 'method_not_allowed' }, corsHeaders, 405)
|
|
} catch (error) {
|
|
console.error('admin-subscriptions request failed', error)
|
|
return adminErrorResponse(error, corsHeaders)
|
|
}
|
|
}
|
|
|
|
if (import.meta.main) Deno.serve(handleAdminSubscriptions)
|