feat(sase): trial extend + manual activate — Phase C
Billing tab on user detail page now has writable actions wired to spoke
endpoints landed in sase.tr#28.
Admin SDK
- extendTrial({ subscriptionId, days, reason, founderId }) + result type
- activateSubscription({ subscriptionId, reason, founderId }) + result type
- SASE_ADMIN_ENDPOINTS list updated
Repo
- getUser() now returns subscriptionId so the billing UI can act on it.
Route
- POST /api/sase/subscriptions/[subId] — single multiplexed endpoint:
body { action: 'trial-extend' | 'activate', reason, days? }.
Auth + spoke-wired + reason ≥ 5 chars + days 1..90 (trial-extend).
Audit on success and failure paths.
UI
- BillingActions client component on the Subscription & Billing tab.
- Trial state: [+7g][+14g][+30g] quick buttons + custom days input +
reason modal.
- Trial or pending state: [Subscription'ı aktive et] button.
- Other states show "no billing action available" hint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
80
apps/web/src/app/api/sase/subscriptions/[subId]/route.ts
Normal file
80
apps/web/src/app/api/sase/subscriptions/[subId]/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { createSaseAdmin, saseAdminWired } from "@/lib/admin-sdk/sase";
|
||||
import { writeAudit } from "@/lib/audit";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const ALLOWED = new Set(["trial-extend", "activate"]);
|
||||
|
||||
export async function POST(
|
||||
req: Request,
|
||||
ctx: { params: Promise<{ subId: string }> },
|
||||
) {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, error: "unauthenticated" }, { status: 401 });
|
||||
}
|
||||
if (!saseAdminWired()) {
|
||||
return NextResponse.json({ ok: false, error: "spoke_not_wired" }, { status: 503 });
|
||||
}
|
||||
|
||||
const { subId } = await ctx.params;
|
||||
const body = (await req.json().catch(() => ({}))) as {
|
||||
action?: string;
|
||||
days?: number;
|
||||
reason?: string;
|
||||
};
|
||||
if (!body.action || !ALLOWED.has(body.action)) {
|
||||
return NextResponse.json({ ok: false, error: "invalid_action" }, { status: 400 });
|
||||
}
|
||||
const reason = (body.reason ?? "").trim();
|
||||
if (reason.length < 5) {
|
||||
return NextResponse.json({ ok: false, error: "reason_required" }, { status: 400 });
|
||||
}
|
||||
if (body.action === "trial-extend") {
|
||||
if (typeof body.days !== "number" || body.days <= 0 || body.days > 90) {
|
||||
return NextResponse.json({ ok: false, error: "invalid_days" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `/api/sase/subscriptions/${subId}/${body.action}`;
|
||||
try {
|
||||
const sdk = createSaseAdmin();
|
||||
const result =
|
||||
body.action === "trial-extend"
|
||||
? await sdk.extendTrial({
|
||||
subscriptionId: subId,
|
||||
days: body.days!,
|
||||
reason,
|
||||
founderId: session.user.id,
|
||||
})
|
||||
: await sdk.activateSubscription({
|
||||
subscriptionId: subId,
|
||||
reason,
|
||||
founderId: session.user.id,
|
||||
});
|
||||
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint,
|
||||
method: "POST",
|
||||
requestPayload: { action: body.action, days: body.days, reasonLen: reason.length },
|
||||
responseStatus: 200,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, result });
|
||||
} catch (err) {
|
||||
const status = (err as { status?: number } | undefined)?.status ?? 500;
|
||||
const message = err instanceof Error ? err.message : "unknown";
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint,
|
||||
method: "POST",
|
||||
requestPayload: { action: body.action, days: body.days, reasonLen: reason.length },
|
||||
responseStatus: status,
|
||||
});
|
||||
return NextResponse.json({ ok: false, error: message }, { status });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user