81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { requirePlatformRole } from "@/lib/session";
|
|
import { listSkillPricing, setSkillPricing } from "@/lib/db";
|
|
import { getPackageDef } from "@/lib/packages";
|
|
import { safeError } from "@/lib/errors";
|
|
|
|
/**
|
|
* GET /api/admin/billing/skill-pricing
|
|
* List all configured skill prices.
|
|
*
|
|
* PUT /api/admin/billing/skill-pricing
|
|
* Upsert a daily price for a single skill. Body:
|
|
* { skillId: string, dailyPriceChf: number }
|
|
*
|
|
* Both endpoints are platform-only.
|
|
*
|
|
* Note on skillId validation: we accept any package id that exists
|
|
* in PACKAGE_CATALOG. The PIN to "skills only" is enforced at the
|
|
* UI layer, not here, so admins can price a non-skill package in
|
|
* an emergency without code changes.
|
|
*/
|
|
|
|
const upsertSchema = z.object({
|
|
skillId: z.string().min(1).max(100),
|
|
dailyPriceChf: z.number().min(0).max(1_000_000),
|
|
// Optional with default 0 so existing API callers keep working.
|
|
// Setup fee fires once per (tenant, skill); see billing.ts.
|
|
setupFeeChf: z.number().min(0).max(1_000_000).optional().default(0),
|
|
});
|
|
|
|
export async function GET() {
|
|
try {
|
|
await requirePlatformRole();
|
|
} catch {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
const rows = await listSkillPricing();
|
|
return NextResponse.json(rows);
|
|
}
|
|
|
|
export async function PUT(request: Request) {
|
|
try {
|
|
await requirePlatformRole();
|
|
} catch {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
const body = await request.json().catch(() => ({}));
|
|
const parsed = upsertSchema.safeParse(body);
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid payload", details: parsed.error.flatten() },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
// Validate the skill id exists in PACKAGE_CATALOG. Returns null
|
|
// for unknown ids; we reject those rather than persist a row that
|
|
// would never match a real toggle event.
|
|
const pkg = getPackageDef(parsed.data.skillId);
|
|
if (!pkg) {
|
|
return NextResponse.json(
|
|
{ error: `Unknown package id: ${parsed.data.skillId}` },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
try {
|
|
const row = await setSkillPricing(
|
|
parsed.data.skillId,
|
|
parsed.data.dailyPriceChf,
|
|
parsed.data.setupFeeChf
|
|
);
|
|
return NextResponse.json(row);
|
|
} catch (e) {
|
|
console.error("Failed to upsert skill pricing:", e);
|
|
return NextResponse.json(
|
|
{ error: safeError(e, "Upsert failed") },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|