feat(sase): brand picker — Phase F

Pairs with sase.tr#31 (POST /internal/admin/subscriptions/:id/brands).

Admin SDK
- setSubscriptionBrands({ subscriptionId, brandIds, reason, founderId }).
- BrandSetResult type; SASE_ADMIN_ENDPOINTS list updated.

Repo
- listBrands() helper (RO).
- getUser() now returns currentPlanBrandCount + brandIds for the picker.

Route
- POST /api/sase/subscriptions/[subId]/brands — auth + spoke-wired,
  brandIds array required, reason ≥ 5 chars. Audit on both paths.

UI (BrandPicker on the Billing tab, below subscription actions)
- Full plan (brandCount=0): shows "all brands automatic" badge list,
  no picker.
- Active/trial with brandCount > 0: checkbox grid of all active brands.
  Clicking past the plan limit shows an inline error. Visual deltas:
  outline "kaldırılacak" badge on rows being removed, default "yeni"
  badge on rows being added.
- Cancelled/expired: read-only message.
- Save button enabled only when selection is exactly plan.brandCount,
  there's a dirty diff, and a reason ≥ 5 chars is entered. Saves +
  router.refresh().

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Semih
2026-05-18 10:38:34 +03:00
parent 0eacc32286
commit 096ca9b4a3
5 changed files with 342 additions and 2 deletions

View File

@@ -0,0 +1,69 @@
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";
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 {
brandIds?: string[];
reason?: string;
};
if (!Array.isArray(body.brandIds)) {
return NextResponse.json({ ok: false, error: "brandIds_required" }, { status: 400 });
}
const reason = (body.reason ?? "").trim();
if (reason.length < 5) {
return NextResponse.json({ ok: false, error: "reason_required" }, { status: 400 });
}
const endpoint = `/api/sase/subscriptions/${subId}/brands`;
try {
const sdk = createSaseAdmin();
const result = await sdk.setSubscriptionBrands({
subscriptionId: subId,
brandIds: body.brandIds,
reason,
founderId: session.user.id,
});
await writeAudit({
projectKey: "sase",
endpoint,
method: "POST",
requestPayload: {
subId,
brandCount: body.brandIds.length,
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: { subId, brandCount: body.brandIds.length, reasonLen: reason.length },
responseStatus: status,
});
return NextResponse.json({ ok: false, error: message }, { status });
}
}