feat(sms): bring-your-own-provider — cafés use their own SMS account
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 5m16s
CI/CD / CI · API (dotnet build + test) (push) Successful in 40s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 30s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 45s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 5m16s
The platform no longer sells SMS. Each café saves its OWN Kavenegar API key + sender line (new Cafes columns + migration) and campaigns are sent and billed through that account. Backend: - GET/PUT /sms/settings (Manager/Owner; key echoed masked, verified against the provider before saving) - campaign + balance use the café's credentials; SMS_NOT_CONFIGURED error when missing; plan-tier SMS gating removed everywhere (PlanLimitChecker, SmsMarketingService, billing status) - platform Kavenegar config stays ONLY for login OTPs (env/DB) - design-time DbContext factory so `dotnet ef migrations add` works without booting the host Dashboard: - SMS screen: provider-settings card, not-configured callout, campaign form disabled until configured; quota bar removed (usage stays as info) - subscription screen + plan comparison no longer show SMS limits Admin panel: - Kavenegar/SMS section removed from integrations (request field now optional; stored OTP config untouched) - SMS limit field removed from the plan editor - nav label "درگاه و پیامک" → "درگاه پرداخت و AI" fa/en/ar translations. 86 tests pass; all tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1020,7 +1020,7 @@
|
||||
"title": "مدیریت سامانه",
|
||||
"dashboard": "داشبورد",
|
||||
"plans": "اشتراک و قیمت",
|
||||
"integrations": "درگاه و پیامک",
|
||||
"integrations": "درگاه پرداخت و AI",
|
||||
"notifications": "اعلانها",
|
||||
"settings": "تنظیمات اپ",
|
||||
"features": "قابلیتها",
|
||||
|
||||
@@ -143,7 +143,7 @@ const LIMIT_FIELDS: { key: keyof PlanLimitsData; label: string }[] = [
|
||||
{ key: "maxMenuItems", label: "maxItems" },
|
||||
{ key: "maxCustomers", label: "maxCustomers" },
|
||||
{ key: "maxReportHistoryDays", label: "maxReportDays" },
|
||||
{ key: "maxSmsPerMonth", label: "maxSms" },
|
||||
// No SMS limit — marketing SMS is bring-your-own-provider per café.
|
||||
{ key: "maxMenuAi3dPerMonth", label: "maxAi3d" },
|
||||
];
|
||||
|
||||
@@ -701,11 +701,6 @@ export function AdminIntegrationsScreen() {
|
||||
hasStoredClientSecret: prev?.hasStoredClientSecret ?? false,
|
||||
...patch,
|
||||
});
|
||||
const [kavenegar, setKavenegar] = useState({
|
||||
isEnabled: true,
|
||||
apiKey: "",
|
||||
otpTemplate: "verify",
|
||||
});
|
||||
const [openAi, setOpenAi] = useState({
|
||||
isEnabled: false,
|
||||
apiKey: "",
|
||||
@@ -722,11 +717,6 @@ export function AdminIntegrationsScreen() {
|
||||
if (!data) return;
|
||||
setActiveGateway(data.activePaymentGateway);
|
||||
setGateways(data.paymentGateways.map((g) => ({ ...g })));
|
||||
setKavenegar({
|
||||
isEnabled: data.kavenegar.isEnabled,
|
||||
apiKey: data.kavenegar.apiKey ?? "",
|
||||
otpTemplate: data.kavenegar.otpTemplate,
|
||||
});
|
||||
setOpenAi({
|
||||
isEnabled: data.ai.openAi.isEnabled,
|
||||
apiKey: data.ai.openAi.apiKey ?? "",
|
||||
@@ -770,7 +760,7 @@ export function AdminIntegrationsScreen() {
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
kavenegar,
|
||||
// SMS is bring-your-own-provider per café — no platform SMS config here.
|
||||
ai: { openAi, meshy },
|
||||
}),
|
||||
onSuccess: () => {
|
||||
@@ -998,39 +988,6 @@ export function AdminIntegrationsScreen() {
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.06em] text-muted-foreground">
|
||||
{t("kavenegarTitle")}
|
||||
</p>
|
||||
<Card className="rounded-xl border border-border/80 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Toggle
|
||||
checked={kavenegar.isEnabled}
|
||||
onChange={(v) => setKavenegar((k) => ({ ...k, isEnabled: v }))}
|
||||
/>
|
||||
<span>{t("enabled")}</span>
|
||||
</div>
|
||||
<label className="block text-sm">
|
||||
{t("apiKey")}
|
||||
<Input
|
||||
className="mt-1"
|
||||
type="password"
|
||||
placeholder={data?.kavenegar.hasStoredApiKey ? "••••••••" : ""}
|
||||
value={kavenegar.apiKey}
|
||||
onChange={(e) => setKavenegar((k) => ({ ...k, apiKey: e.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
<label className="block text-sm">
|
||||
{t("otpTemplate")}
|
||||
<Input
|
||||
className="mt-1"
|
||||
value={kavenegar.otpTemplate}
|
||||
onChange={(e) => setKavenegar((k) => ({ ...k, otpTemplate: e.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.06em] text-muted-foreground">
|
||||
{t("aiTitle")}
|
||||
|
||||
Reference in New Issue
Block a user