Files
pieced-portal/src/app/api/admin/billing/skill-pricing/route.ts
admin cd15b391ac
All checks were successful
Build and Push / build (push) Successful in 1m34s
Phase2: Invoicecomputation/AdminpricingUI/Ainvoicemgnt
2026-05-24 16:38:41 +02:00

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 }
);
}
}