feat(admin): grant a free subscription to any café from the admin panel
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m43s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 33s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m14s
CI/CD / CI · Admin Web (tsc) (push) Successful in 40s
CI/CD / CI · Website (tsc) (push) Successful in 47s
CI/CD / CI · Koja (tsc) (push) Successful in 54s
CI/CD / Deploy · all services (push) Successful in 5m13s
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m43s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 33s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m14s
CI/CD / CI · Admin Web (tsc) (push) Successful in 40s
CI/CD / CI · Website (tsc) (push) Successful in 47s
CI/CD / CI · Koja (tsc) (push) Successful in 54s
CI/CD / Deploy · all services (push) Successful in 5m13s
Adds POST /api/admin/cafes/{cafeId}/grant-subscription (admin-auth): sets the
café's plan and adds N months of coverage, appended to any time it already has so
a grant never shortens existing paid time. Records the gift as a SubscriptionPayment
(provider Manual, amount 0, Completed) for billing history/audit. New
PaymentProvider.Manual = 4 (int append, no migration). Admin-web café cards get a
"grant free subscription" panel (plan select + months + apply), showing the current
expiry; fa/en/ar strings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1159,6 +1159,15 @@
|
||||
"save": "حفظ",
|
||||
"saved": "تم الحفظ",
|
||||
"loading": "جاري التحميل..."
|
||||
},
|
||||
"grant": {
|
||||
"title": "منح اشتراك مجاني",
|
||||
"plan": "الباقة",
|
||||
"months": "عدد الأشهر",
|
||||
"submit": "منح",
|
||||
"granted": "تم منح الاشتراك",
|
||||
"failed": "تعذّر منح الاشتراك",
|
||||
"currentExpiry": "انتهاء الصلاحية الحالي"
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
|
||||
@@ -1152,6 +1152,15 @@
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"grant": {
|
||||
"title": "Grant free subscription",
|
||||
"plan": "Plan",
|
||||
"months": "Months",
|
||||
"submit": "Grant",
|
||||
"granted": "Subscription granted",
|
||||
"failed": "Could not grant subscription",
|
||||
"currentExpiry": "Current expiry"
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
|
||||
@@ -1152,6 +1152,15 @@
|
||||
"save": "ذخیره",
|
||||
"saved": "ذخیره شد",
|
||||
"loading": "در حال بارگذاری..."
|
||||
},
|
||||
"grant": {
|
||||
"title": "افزودن اشتراک رایگان",
|
||||
"plan": "پلن",
|
||||
"months": "تعداد ماه",
|
||||
"submit": "اعطا",
|
||||
"granted": "اشتراک اعطا شد",
|
||||
"failed": "اعطای اشتراک ناموفق بود",
|
||||
"currentExpiry": "انقضای فعلی"
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
|
||||
@@ -493,6 +493,7 @@ export function AdminCafesScreen() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<GrantSubscriptionPanel cafe={c} />
|
||||
<RecoveryKeyPanel cafe={c} />
|
||||
{profileCafeId === c.id ? (
|
||||
<CafeDiscoverProfilePanel cafeId={c.id} mode="admin" compact />
|
||||
@@ -504,6 +505,70 @@ export function AdminCafesScreen() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Gift a café a free subscription: pick a plan + number of months and apply.
|
||||
* Months are appended to any coverage the café already has. */
|
||||
function GrantSubscriptionPanel({ cafe }: { cafe: AdminCafe }) {
|
||||
const t = useTranslations("admin.cafes.grant");
|
||||
const qc = useQueryClient();
|
||||
const [tier, setTier] = useState("Pro");
|
||||
const [months, setMonths] = useState(1);
|
||||
|
||||
const TIERS = ["Starter", "Pro", "Business", "Enterprise"];
|
||||
|
||||
const grant = useMutation({
|
||||
mutationFn: () =>
|
||||
adminPost(`/api/admin/cafes/${cafe.id}/grant-subscription`, { planTier: tier, months }),
|
||||
onSuccess: () => {
|
||||
notify.success(t("granted"));
|
||||
void qc.invalidateQueries({ queryKey: ["admin", "cafes"] });
|
||||
},
|
||||
onError: () => notify.error(t("failed")),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-lg border border-border/70 bg-muted/20 p-3 text-sm">
|
||||
<p className="font-medium">{t("title")}</p>
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">{t("plan")}</span>
|
||||
<select
|
||||
value={tier}
|
||||
onChange={(e) => setTier(e.target.value)}
|
||||
className="h-9 rounded-md border border-input bg-background px-2 text-sm"
|
||||
>
|
||||
{TIERS.map((x) => (
|
||||
<option key={x} value={x}>
|
||||
{x}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-muted-foreground">{t("months")}</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={120}
|
||||
value={months}
|
||||
onChange={(e) =>
|
||||
setMonths(Math.max(1, Math.min(120, Number(e.target.value) || 1)))
|
||||
}
|
||||
className="h-9 w-24"
|
||||
/>
|
||||
</label>
|
||||
<Button size="sm" disabled={grant.isPending} onClick={() => grant.mutate()}>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</div>
|
||||
{cafe.planExpiresAt ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("currentExpiry")}: {new Date(cafe.planExpiresAt).toLocaleDateString("fa-IR")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate / revoke a café's permanent recovery key. The raw key is returned
|
||||
* once on generate — shown here for copy, never retrievable again.
|
||||
|
||||
Reference in New Issue
Block a user