feat(billing): add products subscriptions orders and usage
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { requirePermission } from "@/lib/admin/auth";
|
||||
import { pageOffset, queryAdminRows } from "@/lib/admin/database";
|
||||
import {
|
||||
adminErrorResponse,
|
||||
invalidQueryResponse,
|
||||
parseListQuery,
|
||||
requestId,
|
||||
requireHighRiskAdminMutation,
|
||||
} from "@/lib/admin/http";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const adjustmentSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
action: z.enum(["retry_grant", "compensate", "record_refund"]),
|
||||
expectedVersion: z.number().int().min(0),
|
||||
reason: z.string().trim().min(1).max(500),
|
||||
});
|
||||
|
||||
type Row = {
|
||||
id: string;
|
||||
order_no: string;
|
||||
user_id: string;
|
||||
email: string | null;
|
||||
product_code: string | null;
|
||||
product_version: number | null;
|
||||
money_cents: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
grant_type: string | null;
|
||||
grant_status: string;
|
||||
grant_error: string | null;
|
||||
adjustment_version: number;
|
||||
refund_status: string;
|
||||
refund_amount_cents: number | null;
|
||||
refunded_at: Date | null;
|
||||
paid_at: Date | null;
|
||||
created_at: Date;
|
||||
total_count: string;
|
||||
};
|
||||
|
||||
type AdjustmentRow = {
|
||||
id: string;
|
||||
order_no: string;
|
||||
status: string;
|
||||
grant_status: string;
|
||||
grant_error: string | null;
|
||||
adjustment_version: number;
|
||||
refund_status: string;
|
||||
refund_amount_cents: number | null;
|
||||
refunded_at: Date | null;
|
||||
action_success: boolean;
|
||||
};
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
await requirePermission("billing.orders.read");
|
||||
const parsed = parseListQuery(request);
|
||||
if (!parsed.success) return invalidQueryResponse(parsed.error.flatten());
|
||||
const query = parsed.data.q ? `%${parsed.data.q}%` : null;
|
||||
const rows = await queryAdminRows<Row>(
|
||||
`select o.id,o.order_no,o.user_id,u.email,o.product_code,o.product_version,
|
||||
o.money_cents,o.currency,o.status,o.grant_type,o.grant_status,o.grant_error,
|
||||
o.adjustment_version,o.refund_status,o.refund_amount_cents,o.refunded_at,
|
||||
o.paid_at,o.created_at,count(*) over()::text total_count
|
||||
from public.payment_orders o
|
||||
left join identity.users u on u.id=o.user_id
|
||||
where ($1::text is null or o.order_no ilike $1 or u.email ilike $1 or o.user_id::text ilike $1)
|
||||
and ($2::text is null or o.status=$2 or o.grant_status=$2 or o.refund_status=$2)
|
||||
order by o.created_at desc limit $3 offset $4`,
|
||||
[
|
||||
query,
|
||||
parsed.data.status ?? null,
|
||||
parsed.data.pageSize,
|
||||
pageOffset(parsed.data.page, parsed.data.pageSize),
|
||||
],
|
||||
);
|
||||
return NextResponse.json({
|
||||
data: rows.map((row) => ({
|
||||
id: row.id,
|
||||
orderNo: row.order_no,
|
||||
userId: row.user_id,
|
||||
email: row.email,
|
||||
productCode: row.product_code,
|
||||
productVersion: row.product_version,
|
||||
moneyCents: row.money_cents,
|
||||
currency: row.currency,
|
||||
status: row.status,
|
||||
grantType: row.grant_type,
|
||||
grantStatus: row.grant_status,
|
||||
grantError: row.grant_error,
|
||||
adjustmentVersion: row.adjustment_version,
|
||||
refundStatus: row.refund_status,
|
||||
refundAmountCents: row.refund_amount_cents,
|
||||
refundedAt: row.refunded_at?.toISOString() ?? null,
|
||||
paidAt: row.paid_at?.toISOString() ?? null,
|
||||
createdAt: row.created_at.toISOString(),
|
||||
})),
|
||||
total: Number(rows[0]?.total_count ?? 0),
|
||||
});
|
||||
} catch (error) {
|
||||
return adminErrorResponse(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await requireHighRiskAdminMutation(
|
||||
request,
|
||||
"billing.adjustments.write",
|
||||
);
|
||||
const parsed = adjustmentSchema.safeParse(
|
||||
await request.json().catch(() => null),
|
||||
);
|
||||
if (!parsed.success) return invalidQueryResponse(parsed.error.flatten());
|
||||
const rows = await queryAdminRows<AdjustmentRow>(
|
||||
`select * from public.admin_adjust_order(
|
||||
$1::uuid,$2::uuid,$3::text,$4::integer,$5::text,$6::text
|
||||
)`,
|
||||
[
|
||||
session.user.id,
|
||||
parsed.data.id,
|
||||
parsed.data.action,
|
||||
parsed.data.expectedVersion,
|
||||
parsed.data.reason,
|
||||
requestId(request),
|
||||
],
|
||||
);
|
||||
const row = rows[0];
|
||||
return NextResponse.json({
|
||||
data: row
|
||||
? {
|
||||
id: row.id,
|
||||
orderNo: row.order_no,
|
||||
status: row.status,
|
||||
grantStatus: row.grant_status,
|
||||
grantError: row.grant_error,
|
||||
adjustmentVersion: row.adjustment_version,
|
||||
refundStatus: row.refund_status,
|
||||
refundAmountCents: row.refund_amount_cents,
|
||||
refundedAt: row.refunded_at?.toISOString() ?? null,
|
||||
actionSuccess: row.action_success,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return adminErrorResponse(error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user