feat(sase): plan change + subscription cancel/resume — Phase D

Completes the billing surface; pairs with sase.tr#29.

Admin SDK
- changePlan(input) + cancelSubscriptionById(input) + resumeSubscription(input).
- PlanChangeResult + SubscriptionStateChange types exported.
- SASE_ADMIN_ENDPOINTS lists the three new spoke routes.

Route
- /api/sase/subscriptions/[subId] now accepts change-plan/cancel/resume
  in addition to trial-extend/activate. newPlanId required when
  action=change-plan. Reason ≥ 5 chars enforced. Audit captures the
  action + days + newPlanId + reasonLen on both paths.

Repo
- getUser() also returns currentPlanId for the plan picker.

UI
- BillingActions extended:
    active/trial → [Plan değiştir][İptal et]
    cancelled    → [Devam ettir]
- Change-plan modal lists active plans (current excluded) with brand
  count hint. The spoke flags brand-reassignment via response field;
  v1 surfaces only the confirmation.
- Cancel uses destructive variant + clear messaging.

Phase D ships the user-management mutation surface — A/B/C/D all live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Semih
2026-05-18 09:47:05 +03:00
parent 01bb820c57
commit 3621c0801f
5 changed files with 203 additions and 14 deletions

View File

@@ -6,7 +6,7 @@ import { writeAudit } from "@/lib/audit";
export const dynamic = "force-dynamic";
const ALLOWED = new Set(["trial-extend", "activate"]);
const ALLOWED = new Set(["trial-extend", "activate", "change-plan", "cancel", "resume"]);
export async function POST(
req: Request,
@@ -25,6 +25,7 @@ export async function POST(
action?: string;
days?: number;
reason?: string;
newPlanId?: string;
};
if (!body.action || !ALLOWED.has(body.action)) {
return NextResponse.json({ ok: false, error: "invalid_action" }, { status: 400 });
@@ -38,6 +39,9 @@ export async function POST(
return NextResponse.json({ ok: false, error: "invalid_days" }, { status: 400 });
}
}
if (body.action === "change-plan" && !body.newPlanId) {
return NextResponse.json({ ok: false, error: "newPlanId_required" }, { status: 400 });
}
const endpoint = `/api/sase/subscriptions/${subId}/${body.action}`;
try {
@@ -50,17 +54,41 @@ export async function POST(
reason,
founderId: session.user.id,
})
: await sdk.activateSubscription({
subscriptionId: subId,
reason,
founderId: session.user.id,
});
: body.action === "activate"
? await sdk.activateSubscription({
subscriptionId: subId,
reason,
founderId: session.user.id,
})
: body.action === "change-plan"
? await sdk.changePlan({
subscriptionId: subId,
newPlanId: body.newPlanId!,
reason,
founderId: session.user.id,
})
: body.action === "cancel"
? await sdk.cancelSubscriptionById({
subscriptionId: subId,
reason,
founderId: session.user.id,
})
: await sdk.resumeSubscription({
subscriptionId: subId,
reason,
founderId: session.user.id,
});
await writeAudit({
projectKey: "sase",
endpoint,
method: "POST",
requestPayload: { action: body.action, days: body.days, reasonLen: reason.length },
requestPayload: {
action: body.action,
days: body.days,
newPlanId: body.newPlanId,
reasonLen: reason.length,
},
responseStatus: 200,
});
@@ -72,7 +100,12 @@ export async function POST(
projectKey: "sase",
endpoint,
method: "POST",
requestPayload: { action: body.action, days: body.days, reasonLen: reason.length },
requestPayload: {
action: body.action,
days: body.days,
newPlanId: body.newPlanId,
reasonLen: reason.length,
},
responseStatus: status,
});
return NextResponse.json({ ok: false, error: message }, { status });