feat(billing-actions): expired/cancelled kullanıcıya "Yeni abonelik başlat" butonu
Expired abonelikte panel "bu durum için aksiyon yok" diyordu; manuel paket tanımları DB'den yapılıyordu. Artık expired/cancelled/aboneliksiz kullanıcıda plan + dönem (aylık/yıllık) + süre (dönem default'u / 30g / 1 yıl / 3 yıl / özel gün) + marka-limitli planda marka seçimi ile yeni active abonelik açılıyor. - admin-sdk: hiç implement edilmemiş setSubscription (PATCH) yerine startSubscription → POST /internal/admin/users/:id/subscriptions (Sase PR #266 ile gelir; merge edilene kadar spoke 404 döner). - POST /api/sase/users/[id]/subscriptions route + audit kaydı. - BillingActions artık subscription'sız kullanıcıda da render ediliyor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ww3tfpuxBcettz81dDvyYt
This commit is contained in:
88
apps/web/src/app/api/sase/users/[id]/subscriptions/route.ts
Normal file
88
apps/web/src/app/api/sase/users/[id]/subscriptions/route.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yeni abonelik başlat (expired / cancelled / aboneliği olmayan kullanıcı).
|
||||||
|
* Spoke: POST /internal/admin/users/:id/subscriptions — canlı (active/trial)
|
||||||
|
* abonelik varsa spoke 409 döner; onlar için /api/sase/subscriptions/[subId].
|
||||||
|
*/
|
||||||
|
export async function POST(req: Request, ctx: { params: Promise<{ id: 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 { id } = await ctx.params;
|
||||||
|
const body = (await req.json().catch(() => ({}))) as {
|
||||||
|
planId?: string;
|
||||||
|
billingPeriod?: string;
|
||||||
|
days?: number | null;
|
||||||
|
brandIds?: string[];
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
const reason = (body.reason ?? "").trim();
|
||||||
|
if (reason.length < 5) {
|
||||||
|
return NextResponse.json({ ok: false, error: "reason_required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (!body.planId) {
|
||||||
|
return NextResponse.json({ ok: false, error: "planId_required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (body.billingPeriod !== "monthly" && body.billingPeriod !== "yearly") {
|
||||||
|
return NextResponse.json({ ok: false, error: "invalid_billing_period" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const days = body.days == null ? undefined : body.days;
|
||||||
|
if (days !== undefined && (typeof days !== "number" || days <= 0 || days > 3650)) {
|
||||||
|
return NextResponse.json({ ok: false, error: "invalid_days" }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (body.brandIds !== undefined && !Array.isArray(body.brandIds)) {
|
||||||
|
return NextResponse.json({ ok: false, error: "invalid_brand_ids" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = `/api/sase/users/${id}/subscriptions`;
|
||||||
|
const requestPayload = {
|
||||||
|
action: "start-subscription",
|
||||||
|
planId: body.planId,
|
||||||
|
billingPeriod: body.billingPeriod,
|
||||||
|
days,
|
||||||
|
brandCount: body.brandIds?.length ?? 0,
|
||||||
|
reasonLen: reason.length,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const result = await createSaseAdmin().startSubscription({
|
||||||
|
userId: id,
|
||||||
|
planId: body.planId,
|
||||||
|
billingPeriod: body.billingPeriod,
|
||||||
|
days,
|
||||||
|
brandIds: body.brandIds,
|
||||||
|
reason,
|
||||||
|
founderId: session.user.id,
|
||||||
|
});
|
||||||
|
await writeAudit({
|
||||||
|
projectKey: "sase",
|
||||||
|
endpoint,
|
||||||
|
method: "POST",
|
||||||
|
requestPayload,
|
||||||
|
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,
|
||||||
|
responseStatus: status,
|
||||||
|
});
|
||||||
|
return NextResponse.json({ ok: false, error: message }, { status });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,28 +21,46 @@ type Action =
|
|||||||
| "change-plan"
|
| "change-plan"
|
||||||
| "cancel"
|
| "cancel"
|
||||||
| "resume"
|
| "resume"
|
||||||
|
| "start"
|
||||||
| null;
|
| null;
|
||||||
|
|
||||||
const QUICK_DAYS = [7, 14, 30];
|
const QUICK_DAYS = [7, 14, 30];
|
||||||
|
/** "Yeni abonelik" için hızlı süre seçenekleri (gün). 0 = plan dönemi default'u. */
|
||||||
|
const START_QUICK_DAYS: Array<{ label: string; days: number }> = [
|
||||||
|
{ label: "Dönem default'u", days: 0 },
|
||||||
|
{ label: "30g", days: 30 },
|
||||||
|
{ label: "1 yıl", days: 365 },
|
||||||
|
{ label: "3 yıl", days: 1095 },
|
||||||
|
];
|
||||||
|
|
||||||
export function BillingActions({
|
export function BillingActions({
|
||||||
|
userId,
|
||||||
subscriptionId,
|
subscriptionId,
|
||||||
subscriptionStatus,
|
subscriptionStatus,
|
||||||
subscriptionEndsAt,
|
subscriptionEndsAt,
|
||||||
currentPlanId,
|
currentPlanId,
|
||||||
plans,
|
plans,
|
||||||
|
brands,
|
||||||
}: {
|
}: {
|
||||||
subscriptionId: string;
|
userId: string;
|
||||||
subscriptionStatus: string;
|
/** null = kullanıcının hiç subscription'ı yok (sadece "Yeni abonelik" gösterilir). */
|
||||||
|
subscriptionId: string | null;
|
||||||
|
subscriptionStatus: string | null;
|
||||||
subscriptionEndsAt: Date | null;
|
subscriptionEndsAt: Date | null;
|
||||||
currentPlanId: string | null;
|
currentPlanId: string | null;
|
||||||
plans: Array<{ id: string; name: string; brandCount: number }>;
|
plans: Array<{ id: string; name: string; brandCount: number }>;
|
||||||
|
brands: Array<{ id: string; name: string }>;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = useState<Action>(null);
|
const [open, setOpen] = useState<Action>(null);
|
||||||
const [days, setDays] = useState<number>(7);
|
const [days, setDays] = useState<number>(7);
|
||||||
const [reason, setReason] = useState("");
|
const [reason, setReason] = useState("");
|
||||||
const [newPlanId, setNewPlanId] = useState<string>("");
|
const [newPlanId, setNewPlanId] = useState<string>("");
|
||||||
|
// "Yeni abonelik" formu
|
||||||
|
const [startPlanId, setStartPlanId] = useState<string>("");
|
||||||
|
const [startPeriod, setStartPeriod] = useState<"monthly" | "yearly">("yearly");
|
||||||
|
const [startDays, setStartDays] = useState<number>(0);
|
||||||
|
const [startBrandIds, setStartBrandIds] = useState<string[]>([]);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
|
|
||||||
@@ -51,11 +69,80 @@ export function BillingActions({
|
|||||||
setReason("");
|
setReason("");
|
||||||
setDays(7);
|
setDays(7);
|
||||||
setNewPlanId("");
|
setNewPlanId("");
|
||||||
|
setStartPlanId("");
|
||||||
|
setStartPeriod("yearly");
|
||||||
|
setStartDays(0);
|
||||||
|
setStartBrandIds([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openStart() {
|
||||||
|
// Default: kullanıcının son planı (varsa), yoksa ilk plan.
|
||||||
|
setStartPlanId(currentPlanId && plans.some((p) => p.id === currentPlanId) ? currentPlanId : (plans[0]?.id ?? ""));
|
||||||
|
setStartPeriod("yearly");
|
||||||
|
setStartDays(0);
|
||||||
|
setStartBrandIds([]);
|
||||||
|
setOpen("start");
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPlan = plans.find((p) => p.id === startPlanId) ?? null;
|
||||||
|
|
||||||
|
function toggleStartBrand(id: string) {
|
||||||
|
setStartBrandIds((prev) => {
|
||||||
|
if (prev.includes(id)) return prev.filter((b) => b !== id);
|
||||||
|
const cap = startPlan?.brandCount ?? 0;
|
||||||
|
if (cap > 0 && prev.length >= cap) return prev;
|
||||||
|
return [...prev, id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitStart() {
|
||||||
|
setError(null);
|
||||||
|
if (reason.trim().length < 5) {
|
||||||
|
setError("Sebep en az 5 karakter olmalı.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!startPlan) {
|
||||||
|
setError("Plan seç.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (startDays < 0 || startDays > 3650) {
|
||||||
|
setError("Gün sayısı 0-3650 arası olmalı (0 = dönem default'u).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (startPlan.brandCount > 0 && startBrandIds.length !== startPlan.brandCount) {
|
||||||
|
setError(`Bu plan tam olarak ${startPlan.brandCount} marka gerektirir (${startBrandIds.length} seçili).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startTransition(async () => {
|
||||||
|
const res = await fetch(`/api/sase/users/${userId}/subscriptions`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
planId: startPlan.id,
|
||||||
|
billingPeriod: startPeriod,
|
||||||
|
days: startDays > 0 ? startDays : null,
|
||||||
|
brandIds: startPlan.brandCount > 0 ? startBrandIds : undefined,
|
||||||
|
reason: reason.trim(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
||||||
|
setError(data.error ?? `HTTP ${res.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
router.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
if (open === "start") {
|
||||||
|
submitStart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!subscriptionId) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
if (reason.trim().length < 5) {
|
if (reason.trim().length < 5) {
|
||||||
setError("Sebep en az 5 karakter olmalı.");
|
setError("Sebep en az 5 karakter olmalı.");
|
||||||
@@ -100,7 +187,20 @@ export function BillingActions({
|
|||||||
const canChangePlan = subscriptionStatus === "active" || subscriptionStatus === "trial";
|
const canChangePlan = subscriptionStatus === "active" || subscriptionStatus === "trial";
|
||||||
const canCancel = subscriptionStatus === "active" || subscriptionStatus === "trial";
|
const canCancel = subscriptionStatus === "active" || subscriptionStatus === "trial";
|
||||||
const canResume = subscriptionStatus === "cancelled";
|
const canResume = subscriptionStatus === "cancelled";
|
||||||
|
// Canlı (active/trial/pending) abonelik yokken yeni bir tane başlatılabilir;
|
||||||
|
// spoke active/trial varsa zaten 409 döner.
|
||||||
|
const canStart =
|
||||||
|
subscriptionStatus === null ||
|
||||||
|
subscriptionStatus === "expired" ||
|
||||||
|
subscriptionStatus === "cancelled";
|
||||||
const otherPlans = plans.filter((p) => p.id !== currentPlanId);
|
const otherPlans = plans.filter((p) => p.id !== currentPlanId);
|
||||||
|
const startEndsAt = (() => {
|
||||||
|
const d = new Date();
|
||||||
|
if (startDays > 0) d.setDate(d.getDate() + startDays);
|
||||||
|
else if (startPeriod === "yearly") d.setFullYear(d.getFullYear() + 1);
|
||||||
|
else d.setMonth(d.getMonth() + 1);
|
||||||
|
return d.toISOString().slice(0, 10);
|
||||||
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -179,11 +279,22 @@ export function BillingActions({
|
|||||||
Subscription'ı devam ettir
|
Subscription'ı devam ettir
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!canExtendTrial && !canBonusExtend && !canActivate && !canChangePlan && !canCancel && !canResume && (
|
{canStart && plans.length > 0 && (
|
||||||
<span className="text-xs text-muted-foreground">
|
<Button variant="default" size="sm" onClick={openStart}>
|
||||||
Bu durum için billing aksiyonu yok ({subscriptionStatus}).
|
Yeni abonelik başlat
|
||||||
</span>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{!canExtendTrial &&
|
||||||
|
!canBonusExtend &&
|
||||||
|
!canActivate &&
|
||||||
|
!canChangePlan &&
|
||||||
|
!canCancel &&
|
||||||
|
!canResume &&
|
||||||
|
!canStart && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Bu durum için billing aksiyonu yok ({subscriptionStatus}).
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={open !== null} onOpenChange={(v) => !v && close()}>
|
<Dialog open={open !== null} onOpenChange={(v) => !v && close()}>
|
||||||
@@ -286,6 +397,114 @@ export function BillingActions({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
)}
|
)}
|
||||||
|
{open === "start" && (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Yeni abonelik başlat</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{subscriptionStatus
|
||||||
|
? `Mevcut ${subscriptionStatus} kayıt olduğu gibi kalır; `
|
||||||
|
: ""}
|
||||||
|
yeni bir active subscription açılır (başlangıç = şimdi). Full
|
||||||
|
Paket'te tüm markalar otomatik atanır. Ödeme / gelir eventi
|
||||||
|
oluşmaz (manuel founder tanımı).
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm">Plan</label>
|
||||||
|
<select
|
||||||
|
value={startPlanId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setStartPlanId(e.target.value);
|
||||||
|
setStartBrandIds([]);
|
||||||
|
}}
|
||||||
|
className="w-full rounded-md border bg-background p-2 text-sm"
|
||||||
|
>
|
||||||
|
{plans.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.name} ({p.brandCount === 0 ? "all brands" : `${p.brandCount} brand`})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm">Dönem</label>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{(["monthly", "yearly"] as const).map((p) => (
|
||||||
|
<Button
|
||||||
|
key={p}
|
||||||
|
type="button"
|
||||||
|
variant={startPeriod === p ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setStartPeriod(p)}
|
||||||
|
>
|
||||||
|
{p === "monthly" ? "Aylık" : "Yıllık"}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm">Süre</label>
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{START_QUICK_DAYS.map((q) => (
|
||||||
|
<Button
|
||||||
|
key={q.days}
|
||||||
|
type="button"
|
||||||
|
variant={startDays === q.days ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setStartDays(q.days)}
|
||||||
|
>
|
||||||
|
{q.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<label className="flex items-center gap-1 text-sm">
|
||||||
|
Özel gün:
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={3650}
|
||||||
|
value={startDays || ""}
|
||||||
|
onChange={(e) => setStartDays(Number(e.target.value) || 0)}
|
||||||
|
className="w-24"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Bitiş: {startEndsAt}
|
||||||
|
{startDays > 0 ? ` (+${startDays} gün)` : ` (${startPeriod === "yearly" ? "+1 yıl" : "+1 ay"})`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{startPlan && startPlan.brandCount > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm">
|
||||||
|
Markalar ({startBrandIds.length}/{startPlan.brandCount})
|
||||||
|
</label>
|
||||||
|
<div className="grid max-h-48 grid-cols-2 gap-1 overflow-y-auto rounded-md border p-2 text-sm md:grid-cols-3">
|
||||||
|
{brands.map((b) => {
|
||||||
|
const checked = startBrandIds.includes(b.id);
|
||||||
|
const full = !checked && startBrandIds.length >= startPlan.brandCount;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={b.id}
|
||||||
|
className={`flex items-center gap-2 ${full ? "opacity-50" : ""}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
disabled={full}
|
||||||
|
onChange={() => toggleStartBrand(b.id)}
|
||||||
|
/>
|
||||||
|
{b.name}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -319,7 +538,9 @@ export function BillingActions({
|
|||||||
? "Plan değiştir"
|
? "Plan değiştir"
|
||||||
: open === "cancel"
|
: open === "cancel"
|
||||||
? "İptal et"
|
? "İptal et"
|
||||||
: "Devam ettir"}
|
: open === "start"
|
||||||
|
? "Aboneliği başlat"
|
||||||
|
: "Devam ettir"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -178,13 +178,15 @@ export default async function SaseUserDetailPage({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{saseAdminWired() && user.subscriptionId && user.subscriptionStatus && (
|
{saseAdminWired() && (
|
||||||
<BillingActions
|
<BillingActions
|
||||||
|
userId={user.id}
|
||||||
subscriptionId={user.subscriptionId}
|
subscriptionId={user.subscriptionId}
|
||||||
subscriptionStatus={user.subscriptionStatus}
|
subscriptionStatus={user.subscriptionStatus}
|
||||||
subscriptionEndsAt={user.subscriptionEndsAt}
|
subscriptionEndsAt={user.subscriptionEndsAt}
|
||||||
currentPlanId={user.currentPlanId}
|
currentPlanId={user.currentPlanId}
|
||||||
plans={plans}
|
plans={plans}
|
||||||
|
brands={brands}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -5,14 +5,21 @@ export type SaseSubscriptionStatus = "pending" | "active" | "cancelled" | "expir
|
|||||||
|
|
||||||
export type SaseAdmin = {
|
export type SaseAdmin = {
|
||||||
/**
|
/**
|
||||||
* Update a user's subscription. Spoke endpoint: PATCH /internal/admin/users/:id/subscription
|
* Start a brand-new ACTIVE subscription for a user whose previous one is
|
||||||
* Not yet implemented spoke-side.
|
* expired/cancelled (or who has none). Full plan → all brands; brand-limited
|
||||||
|
* plan needs exactly `brandCount` brandIds. `days` overrides the period
|
||||||
|
* default (monthly +1 ay / yearly +1 yıl). No revenue events fire spoke-side.
|
||||||
|
* Spoke endpoint: POST /internal/admin/users/:id/subscriptions
|
||||||
*/
|
*/
|
||||||
setSubscription(input: {
|
startSubscription(input: {
|
||||||
userId: string;
|
userId: string;
|
||||||
planId: string;
|
planId: string;
|
||||||
billingPeriod: "monthly" | "yearly";
|
billingPeriod: "monthly" | "yearly";
|
||||||
}): Promise<{ id: string; status: SaseSubscriptionStatus }>;
|
days?: number;
|
||||||
|
brandIds?: string[];
|
||||||
|
reason: string;
|
||||||
|
founderId: string;
|
||||||
|
}): Promise<SubscriptionStartResult>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force-cancel a subscription. Spoke endpoint: DELETE /internal/admin/subscriptions/:id
|
* Force-cancel a subscription. Spoke endpoint: DELETE /internal/admin/subscriptions/:id
|
||||||
@@ -259,6 +266,19 @@ export type TrialExtendResult = {
|
|||||||
daysAdded: number;
|
daysAdded: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SubscriptionStartResult = {
|
||||||
|
success: boolean;
|
||||||
|
subscriptionId: string;
|
||||||
|
userId: string;
|
||||||
|
planId: string;
|
||||||
|
planName: string;
|
||||||
|
billingPeriod: "monthly" | "yearly";
|
||||||
|
status: string;
|
||||||
|
startDate: string | null;
|
||||||
|
endDate: string | null;
|
||||||
|
brandCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type SubscriptionActivateResult = {
|
export type SubscriptionActivateResult = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
subscriptionId: string;
|
subscriptionId: string;
|
||||||
@@ -286,8 +306,8 @@ export function createSaseAdmin(): SaseAdmin {
|
|||||||
}
|
}
|
||||||
const client = new AdminClient({ projectKey: "sase", baseUrl: base, token });
|
const client = new AdminClient({ projectKey: "sase", baseUrl: base, token });
|
||||||
return {
|
return {
|
||||||
setSubscription: (input) =>
|
startSubscription: ({ userId, ...rest }) =>
|
||||||
client.call("PATCH", `/internal/admin/users/${input.userId}/subscription`, input),
|
client.call("POST", `/internal/admin/users/${userId}/subscriptions`, rest),
|
||||||
cancelSubscription: (id) =>
|
cancelSubscription: (id) =>
|
||||||
client.call("DELETE", `/internal/admin/subscriptions/${id}`),
|
client.call("DELETE", `/internal/admin/subscriptions/${id}`),
|
||||||
resendVerification: (id) =>
|
resendVerification: (id) =>
|
||||||
@@ -412,7 +432,7 @@ function notWiredSdk(projectKey: string): SaseAdmin {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
setSubscription: () => Promise.reject(reject()),
|
startSubscription: () => Promise.reject(reject()),
|
||||||
cancelSubscription: () => Promise.reject(reject()),
|
cancelSubscription: () => Promise.reject(reject()),
|
||||||
resendVerification: () => Promise.reject(reject()),
|
resendVerification: () => Promise.reject(reject()),
|
||||||
impersonateReadonly: () => Promise.reject(reject()),
|
impersonateReadonly: () => Promise.reject(reject()),
|
||||||
@@ -440,7 +460,7 @@ export function saseAdminWired(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SASE_ADMIN_ENDPOINTS = [
|
export const SASE_ADMIN_ENDPOINTS = [
|
||||||
"PATCH /internal/admin/users/:id/subscription",
|
"POST /internal/admin/users/:id/subscriptions",
|
||||||
"DELETE /internal/admin/subscriptions/:id",
|
"DELETE /internal/admin/subscriptions/:id",
|
||||||
"POST /internal/admin/users/:id/resend-verification",
|
"POST /internal/admin/users/:id/resend-verification",
|
||||||
"POST /internal/admin/users/:id/impersonate-readonly",
|
"POST /internal/admin/users/:id/impersonate-readonly",
|
||||||
|
|||||||
Reference in New Issue
Block a user