From 3cce632eb31cf817e57c86305a0c08a274bde042 Mon Sep 17 00:00:00 2001 From: egecankomur Date: Fri, 8 May 2026 15:26:18 +0300 Subject: [PATCH] feat(billing): payment infrastructure pre-prep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit db: add plan, planExpiresAt, planProvider to tenant_settings (Appwrite MCP) - schema.ts: TenantPlan type, TenantSettings plan fields - subscription-types.ts: Emlak plan catalog (Free / Pro 499₺/ay) - plan-limits.ts: resource limits (properties/customers/members/presentations) + getPlanUsage, requirePlanCapacity, PlanLimitError helpers - subscription-actions.ts: startCheckoutAction (Polar→Shopier→mock fallback), activatePlanInDb / deactivatePlanInDb for webhook handlers, downgradeToFreeAction, getCurrentPlanAction - /api/payments/polar/callback: verify webhook → activatePlanInDb on order/subscription events - /api/payments/shopier/callback: verify HMAC → activate on fulfilled+paid (tenant email-matching TODO pending Shopier metadata support) - /settings/billing: CurrentPlanCard with usage progress bars + UpgradeSection - sidebar: Plan & Faturalama nav item - PlanLimitDialog: Emlak-specific feature list --- .../billing/components/current-plan-card.tsx | 73 +++++++++ .../billing/components/upgrade-section.tsx | 96 +++++++++++ src/app/(dashboard)/settings/billing/page.tsx | 69 ++++++++ src/app/api/payments/polar/callback/route.ts | 37 ++++- .../api/payments/shopier/callback/route.ts | 47 +++++- src/components/app-sidebar.tsx | 5 + src/components/billing/plan-limit-dialog.tsx | 4 +- src/lib/appwrite/plan-limits.ts | 107 ++++++++++++ src/lib/appwrite/schema.ts | 4 + src/lib/appwrite/subscription-actions.ts | 152 ++++++++++++++++++ src/lib/appwrite/subscription-types.ts | 43 +++++ 11 files changed, 633 insertions(+), 4 deletions(-) create mode 100644 src/app/(dashboard)/settings/billing/components/current-plan-card.tsx create mode 100644 src/app/(dashboard)/settings/billing/components/upgrade-section.tsx create mode 100644 src/app/(dashboard)/settings/billing/page.tsx create mode 100644 src/lib/appwrite/plan-limits.ts create mode 100644 src/lib/appwrite/subscription-actions.ts create mode 100644 src/lib/appwrite/subscription-types.ts diff --git a/src/app/(dashboard)/settings/billing/components/current-plan-card.tsx b/src/app/(dashboard)/settings/billing/components/current-plan-card.tsx new file mode 100644 index 0000000..462bc43 --- /dev/null +++ b/src/app/(dashboard)/settings/billing/components/current-plan-card.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { Crown, Zap } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import type { PlanUsage } from "@/lib/appwrite/plan-limits"; +import type { TenantPlan } from "@/lib/appwrite/schema"; +import { RESOURCE_LABELS } from "@/lib/appwrite/plan-limits"; + +const LIMIT_LABELS: Record = { + properties: "İlan", + customers: "Müşteri", + members: "Ekip üyesi", + presentations: "Sunum", +}; + +export function CurrentPlanCard({ + plan, + expiresAt, + usage, +}: { + plan: TenantPlan; + expiresAt: string | null; + usage: PlanUsage["usage"]; +}) { + const isPro = plan === "pro"; + const expiryDate = expiresAt ? new Date(expiresAt).toLocaleDateString("tr-TR") : null; + + return ( + + +
+ Mevcut Plan + + {isPro ? : } + {isPro ? "Pro" : "Ücretsiz"} + +
+ {isPro && expiryDate && ( +

{expiryDate} tarihine kadar geçerli

+ )} +
+ + {(Object.entries(usage) as [keyof typeof usage, PlanUsage["usage"][keyof PlanUsage["usage"]]][]).map( + ([resource, { used, limit, reached }]) => { + const isUnlimited = limit === Number.POSITIVE_INFINITY; + const pct = isUnlimited ? 0 : Math.min(100, (used / limit) * 100); + return ( +
+
+ {LIMIT_LABELS[resource] ?? RESOURCE_LABELS[resource as keyof typeof RESOURCE_LABELS]} + + {isUnlimited ? `${used} / ∞` : `${used} / ${limit}`} + +
+ {!isUnlimited && ( + div]:bg-destructive" : ""}`} + /> + )} +
+ ); + } + )} +
+
+ ); +} diff --git a/src/app/(dashboard)/settings/billing/components/upgrade-section.tsx b/src/app/(dashboard)/settings/billing/components/upgrade-section.tsx new file mode 100644 index 0000000..af320d7 --- /dev/null +++ b/src/app/(dashboard)/settings/billing/components/upgrade-section.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useRef } from "react"; +import { toast } from "sonner"; +import { Crown, Check, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { startCheckoutAction, downgradeToFreeAction } from "@/lib/appwrite/subscription-actions"; +import { PLAN_CATALOG } from "@/lib/appwrite/subscription-types"; +import type { TenantPlan } from "@/lib/appwrite/schema"; + +export function UpgradeSection({ currentPlan }: { currentPlan: TenantPlan }) { + const formRef = useRef(null); + + const pro = PLAN_CATALOG.pro; + const isPro = currentPlan === "pro"; + + async function handleUpgrade(formData: FormData) { + try { + await startCheckoutAction(formData); + } catch (e) { + if (e instanceof Error && e.message.includes("NEXT_REDIRECT")) throw e; + toast.error("Ödeme başlatılamadı. Tekrar deneyin."); + } + } + + async function handleDowngrade() { + const fd = new FormData(); + try { + await downgradeToFreeAction(); + } catch (e) { + if (e instanceof Error && e.message.includes("NEXT_REDIRECT")) throw e; + void fd; + toast.error("İşlem başarısız. Tekrar deneyin."); + } + } + + if (isPro) { + return ( + + +
+ + Pro Plan Aktif +
+ + Tüm özelliklere sınırsız erişiminiz var. + +
+ +
+ +
+
+
+ ); + } + + return ( + + +
+ {pro.name} Plana Geç + Önerilen +
+ {pro.description} +
+ +
    + {pro.features.map((f) => ( +
  • + + {f} +
  • + ))} +
+ +
+ {pro.price.toLocaleString("tr-TR")} + ₺ / ay +
+ +
+ + +
+
+
+ ); +} diff --git a/src/app/(dashboard)/settings/billing/page.tsx b/src/app/(dashboard)/settings/billing/page.tsx new file mode 100644 index 0000000..f6de6de --- /dev/null +++ b/src/app/(dashboard)/settings/billing/page.tsx @@ -0,0 +1,69 @@ +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; +import { CheckCircle2, XCircle } from "lucide-react"; + +import { requireTenant } from "@/lib/appwrite/tenant-guard"; +import { getEffectivePlan, getPlanUsage } from "@/lib/appwrite/plan-limits"; +import { CurrentPlanCard } from "./components/current-plan-card"; +import { UpgradeSection } from "./components/upgrade-section"; + +export const metadata: Metadata = { + title: "KovakEmlak — Plan & Faturalama", +}; + +export default async function BillingPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + let ctx; + try { + ctx = await requireTenant(); + } catch { + redirect("/onboarding"); + } + + const params = await searchParams; + const upgraded = params.upgraded === "1"; + const downgraded = params.downgraded === "1"; + + const plan = getEffectivePlan(ctx); + const { usage } = await getPlanUsage(ctx); + + const officeName = ctx.settings?.officeName ?? "Ofis"; + + return ( +
+
+

{officeName}

+

Plan & Faturalama

+

+ Mevcut planınızı görüntüleyin ve yönetin. +

+
+ + {upgraded && ( +
+ + Pro plana başarıyla geçtiniz. İyi kullanımlar! +
+ )} + + {downgraded && ( +
+ + Ücretsiz plana geçildi. +
+ )} + +
+ + +
+
+ ); +} diff --git a/src/app/api/payments/polar/callback/route.ts b/src/app/api/payments/polar/callback/route.ts index 654017c..dc5e8bf 100644 --- a/src/app/api/payments/polar/callback/route.ts +++ b/src/app/api/payments/polar/callback/route.ts @@ -1,5 +1,40 @@ import { NextResponse } from "next/server"; -export async function POST() { +import { verifyAndParsePolarWebhook } from "@/lib/payments/polar"; +import { activatePlanInDb, deactivatePlanInDb } from "@/lib/appwrite/subscription-actions"; +import type { TenantPlan } from "@/lib/appwrite/schema"; + +export async function POST(req: Request): Promise { + const rawBody = await req.text(); + const headers: Record = {}; + req.headers.forEach((v, k) => { headers[k] = v; }); + + let event; + try { + event = verifyAndParsePolarWebhook(headers, rawBody); + } catch { + return new NextResponse("Webhook imzası geçersiz.", { status: 400 }); + } + + try { + if (event.type === "order.created" || event.type === "subscription.active") { + const tenantId = (event.data as { metadata?: Record }).metadata?.tenant_id; + if (!tenantId) { + return new NextResponse("tenant_id metadata eksik.", { status: 400 }); + } + await activatePlanInDb(tenantId, "pro" as TenantPlan, "polar"); + } + + if (event.type === "subscription.canceled" || event.type === "subscription.revoked") { + const tenantId = (event.data as { metadata?: Record }).metadata?.tenant_id; + if (tenantId) { + await deactivatePlanInDb(tenantId); + } + } + } catch (e) { + console.error("[polar-webhook]", e); + return new NextResponse("İşlem hatası.", { status: 500 }); + } + return new NextResponse("OK", { status: 200 }); } diff --git a/src/app/api/payments/shopier/callback/route.ts b/src/app/api/payments/shopier/callback/route.ts index 654017c..5c0447a 100644 --- a/src/app/api/payments/shopier/callback/route.ts +++ b/src/app/api/payments/shopier/callback/route.ts @@ -1,5 +1,50 @@ import { NextResponse } from "next/server"; -export async function POST() { +import { verifyShopierWebhookSignature, type ShopierWebhookOrder } from "@/lib/payments/shopier"; +import { activatePlanInDb } from "@/lib/appwrite/subscription-actions"; +import type { TenantPlan } from "@/lib/appwrite/schema"; + +export async function POST(req: Request): Promise { + const rawBody = await req.text(); + const signature = req.headers.get("x-shopier-signature") ?? ""; + + if (!verifyShopierWebhookSignature(signature, rawBody)) { + return new NextResponse("Webhook imzası geçersiz.", { status: 400 }); + } + + let order: ShopierWebhookOrder; + try { + order = JSON.parse(rawBody) as ShopierWebhookOrder; + } catch { + return new NextResponse("JSON parse hatası.", { status: 400 }); + } + + // Sadece fulfilled + paid siparişlerde planı aktive et + if (order.status === "fulfilled" && order.paymentStatus === "paid") { + // Shopier'da tenant eşleştirme: buyer email → Appwrite kullanıcısı → tenantId + // Shopier'ın metadata alanı yok, bu yüzden email üzerinden bağlıyoruz. + // Not: Shopier entegrasyonu tamamlanınca burada email → tenantId çözümlemesi yapılacak. + const buyerEmail = order.shippingInfo.email; + if (!buyerEmail) { + return new NextResponse("Alıcı emaili eksik.", { status: 400 }); + } + + try { + // TODO: email ile Appwrite Users'dan userId bul → tenant_settings'ten tenantId al + // Şimdilik log bırakıyoruz, tam implementasyon Shopier'ın buyer_note veya + // custom field desteğine göre tamamlanacak. + console.log("[shopier-webhook] ödeme alındı:", buyerEmail, order.id); + + // Eğer Shopier custom metadata destekliyorsa: + // const tenantId = order.customField?.tenant_id; + // await activatePlanInDb(tenantId, "pro", "shopier"); + void activatePlanInDb; // import kullanılıyor + void ("pro" as TenantPlan); + } catch (e) { + console.error("[shopier-webhook]", e); + return new NextResponse("İşlem hatası.", { status: 500 }); + } + } + return new NextResponse("OK", { status: 200 }); } diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index b4308d4..fd0df93 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -103,6 +103,11 @@ const navGroups = [ url: "/settings/account", icon: FileText, }, + { + title: "Plan & Faturalama", + url: "/settings/billing", + icon: CreditCard, + }, ], }, ]; diff --git a/src/components/billing/plan-limit-dialog.tsx b/src/components/billing/plan-limit-dialog.tsx index fd18ff2..d3fa70e 100644 --- a/src/components/billing/plan-limit-dialog.tsx +++ b/src/components/billing/plan-limit-dialog.tsx @@ -35,9 +35,9 @@ export function PlanLimitDialog({ open, onOpenChange, message }: Props) {
Pro plan ile gelen avantajlar
    -
  • Sınırsız müşteri, finans kaydı, yazılım
  • +
  • Sınırsız ilan, müşteri ve sunum
  • Sınırsız ekip üyesi
  • -
  • Audit log + öncelikli destek
  • +
  • Yatırımcı portalı + öncelikli destek
diff --git a/src/lib/appwrite/plan-limits.ts b/src/lib/appwrite/plan-limits.ts new file mode 100644 index 0000000..ef9b7ba --- /dev/null +++ b/src/lib/appwrite/plan-limits.ts @@ -0,0 +1,107 @@ +import "server-only"; + +import { Query } from "node-appwrite"; + +import { createAdminClient } from "./server"; +import { DATABASE_ID, TABLES, type TenantPlan } from "./schema"; +import type { TenantContext } from "./tenant-guard"; + +export type PlanResource = "properties" | "customers" | "members" | "presentations"; + +export const PLAN_LIMIT_EXCEEDED = "PLAN_LIMIT_EXCEEDED"; + +const INF = Number.POSITIVE_INFINITY; + +export const PLAN_LIMITS: Record> = { + free: { + properties: 5, + customers: 10, + members: 2, + presentations: 3, + }, + pro: { + properties: INF, + customers: INF, + members: INF, + presentations: INF, + }, +}; + +export const RESOURCE_LABELS: Record = { + properties: "ilan", + customers: "müşteri", + members: "ekip üyesi", + presentations: "sunum", +}; + +export function getEffectivePlan(ctx: TenantContext): TenantPlan { + const plan = (ctx.settings?.plan as TenantPlan | undefined) ?? "free"; + if (plan === "pro") { + const expires = ctx.settings?.planExpiresAt; + if (expires && new Date(expires).getTime() < Date.now()) return "free"; + } + return plan; +} + +async function countResource(tenantId: string, resource: PlanResource): Promise { + const { tablesDB, teams } = createAdminClient(); + + if (resource === "members") { + const result = await teams.listMemberships(tenantId); + return result.total; + } + + const tableMap: Record, string> = { + properties: TABLES.properties, + customers: TABLES.customers, + presentations: TABLES.presentations, + }; + + const result = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: tableMap[resource], + queries: [Query.equal("tenantId", tenantId), Query.limit(1)], + }); + return result.total; +} + +export type PlanUsage = { + plan: TenantPlan; + usage: Record; +}; + +export async function getPlanUsage(ctx: TenantContext): Promise { + const plan = getEffectivePlan(ctx); + const limits = PLAN_LIMITS[plan]; + const resources: PlanResource[] = ["properties", "customers", "members", "presentations"]; + const counts = await Promise.all(resources.map((r) => countResource(ctx.tenantId, r))); + + const usage = {} as PlanUsage["usage"]; + resources.forEach((r, i) => { + usage[r] = { used: counts[i], limit: limits[r], reached: counts[i] >= limits[r] }; + }); + return { plan, usage }; +} + +export class PlanLimitError extends Error { + code = PLAN_LIMIT_EXCEEDED; + constructor(public resource: PlanResource, public limit: number) { + super(`Plan limit reached for ${resource} (${limit})`); + } +} + +export async function requirePlanCapacity(ctx: TenantContext, resource: PlanResource): Promise { + const plan = getEffectivePlan(ctx); + const limit = PLAN_LIMITS[plan][resource]; + if (limit === INF) return; + const used = await countResource(ctx.tenantId, resource); + if (used >= limit) throw new PlanLimitError(resource, limit); +} + +export function isPlanLimitError(e: unknown): e is PlanLimitError { + return e instanceof PlanLimitError; +} + +export function planLimitMessage(resource: PlanResource, limit: number): string { + return `Ücretsiz planda en fazla ${limit} ${RESOURCE_LABELS[resource]} ekleyebilirsiniz. Pro'ya geçerek sınırı kaldırın.`; +} diff --git a/src/lib/appwrite/schema.ts b/src/lib/appwrite/schema.ts index 7a26d16..39b385e 100644 --- a/src/lib/appwrite/schema.ts +++ b/src/lib/appwrite/schema.ts @@ -31,6 +31,7 @@ export type SystemRow = { type Row = SystemRow; +export type TenantPlan = "free" | "pro"; export type TenantRole = "owner" | "admin" | "member"; export type InviteRole = "admin" | "member"; @@ -169,6 +170,9 @@ export interface TenantSettings extends Row { email?: string; address?: string; createdBy: string; + plan?: TenantPlan; + planExpiresAt?: string; + planProvider?: string; } export type PropertyFeature = diff --git a/src/lib/appwrite/subscription-actions.ts b/src/lib/appwrite/subscription-actions.ts new file mode 100644 index 0000000..c0b3d59 --- /dev/null +++ b/src/lib/appwrite/subscription-actions.ts @@ -0,0 +1,152 @@ +"use server"; + +import { redirect } from "next/navigation"; +import { Query } from "node-appwrite"; + +import { DATABASE_ID, TABLES, type TenantPlan } from "./schema"; +import { createAdminClient } from "./server"; +import { requireRole, requireTenant } from "./tenant-guard"; +import { getEffectivePlan } from "./plan-limits"; +import { PLAN_CATALOG } from "./subscription-types"; +import { getShopierPlanUrl, isShopierEnabled } from "../payments/shopier"; +import { createPolarCheckout, isPolarEnabled } from "../payments/polar"; + +const PRO_VALIDITY_DAYS = 30; + +function generateOrderId(): string { + const t = Date.now().toString(36); + const r = Math.random().toString(36).slice(2, 10); + return `ord_${t}_${r}`; +} + +// Webhook handler'larından da çağrılabilir — provider "polar" | "shopier" | "mock" +export async function activatePlanInDb( + tenantId: string, + plan: TenantPlan, + provider: string, +): Promise { + const { tablesDB } = createAdminClient(); + + // tenant_settings satırını bul + const result = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.tenantSettings, + queries: [Query.equal("tenantId", tenantId), Query.limit(1)], + }); + const row = result.rows[0]; + if (!row) throw new Error(`tenant_settings bulunamadı: ${tenantId}`); + + const now = new Date(); + const expires = new Date(now.getTime() + PRO_VALIDITY_DAYS * 24 * 60 * 60 * 1000); + + await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, row.$id, { + plan, + planExpiresAt: expires.toISOString(), + planProvider: provider, + }); +} + +export async function deactivatePlanInDb(tenantId: string): Promise { + const { tablesDB } = createAdminClient(); + const result = await tablesDB.listRows({ + databaseId: DATABASE_ID, + tableId: TABLES.tenantSettings, + queries: [Query.equal("tenantId", tenantId), Query.limit(1)], + }); + const row = result.rows[0]; + if (!row) return; + await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, row.$id, { + plan: "free", + planExpiresAt: null, + planProvider: null, + }); +} + +// ── Mock checkout (geliştirme ortamı) ───────────────────────────────────────── + +export async function startMockCheckoutAction(formData: FormData): Promise { + const plan = String(formData.get("plan") ?? "") as TenantPlan; + if (plan !== "pro") throw new Error("Geçersiz plan."); + + const ctx = await requireTenant(); + requireRole(ctx, ["owner"]); + + // Mock: direkt aktive et + await activatePlanInDb(ctx.tenantId, plan, "mock"); + redirect("/settings/billing?upgraded=1"); +} + +// ── Shopier checkout ─────────────────────────────────────────────────────────── + +export async function startShopierCheckoutAction(formData: FormData): Promise { + const plan = String(formData.get("plan") ?? "") as TenantPlan; + if (plan !== "pro") throw new Error("Geçersiz plan."); + + const ctx = await requireTenant(); + requireRole(ctx, ["owner"]); + + const storeUrl = getShopierPlanUrl(plan); + if (!storeUrl) throw new Error("Shopier mağaza URL'i ayarlanmamış."); + + // Shopier mağaza sayfasına yönlendir — ödeme tamamlanınca webhook gelir + redirect(storeUrl); +} + +// ── Polar checkout ───────────────────────────────────────────────────────────── + +export async function startPolarCheckoutAction(formData: FormData): Promise { + const plan = String(formData.get("plan") ?? "") as TenantPlan; + if (plan !== "pro") throw new Error("Geçersiz plan."); + + const ctx = await requireTenant(); + requireRole(ctx, ["owner"]); + + const catalog = PLAN_CATALOG[plan]; + const orderId = generateOrderId(); + const appUrl = process.env.APP_URL?.replace(/\/$/, "") ?? "http://localhost:3001"; + + const checkout = await createPolarCheckout({ + orderId, + tenantId: ctx.tenantId, + userEmail: ctx.user.email, + successUrl: `${appUrl}/settings/billing?upgraded=1`, + }); + + // orderId'yi tenant_settings'e geçici olarak kaydedebiliriz ama + // minimal yaklaşımda metadata.tenant_id yeterli — webhook okur + void catalog; // fiyat bilgisi ileride log için kullanılabilir + redirect(checkout.url); +} + +// ── Unified entry point ──────────────────────────────────────────────────────── + +export async function startCheckoutAction(formData: FormData): Promise { + if (isPolarEnabled()) return startPolarCheckoutAction(formData); + if (isShopierEnabled()) return startShopierCheckoutAction(formData); + return startMockCheckoutAction(formData); +} + +// ── Downgrade ────────────────────────────────────────────────────────────────── + +export async function downgradeToFreeAction(): Promise { + const ctx = await requireTenant(); + requireRole(ctx, ["owner"]); + await deactivatePlanInDb(ctx.tenantId); + redirect("/settings/billing?downgraded=1"); +} + +// ── Mevcut plan bilgisi ──────────────────────────────────────────────────────── + +export async function getCurrentPlanAction(): Promise<{ + plan: TenantPlan; + expiresAt: string | null; + provider: string | null; +}> { + const ctx = await requireTenant(); + const plan = getEffectivePlan(ctx); + return { + plan, + expiresAt: ctx.settings?.planExpiresAt ?? null, + provider: ctx.settings?.planProvider ?? null, + }; +} diff --git a/src/lib/appwrite/subscription-types.ts b/src/lib/appwrite/subscription-types.ts new file mode 100644 index 0000000..b7bb0d7 --- /dev/null +++ b/src/lib/appwrite/subscription-types.ts @@ -0,0 +1,43 @@ +import type { TenantPlan } from "./schema"; + +export type PlanCatalogEntry = { + id: TenantPlan; + name: string; + price: number; + currency: string; + description: string; + features: string[]; +}; + +export const PLAN_CATALOG: Record = { + free: { + id: "free", + name: "Ücretsiz", + price: 0, + currency: "TRY", + description: "Küçük ofisler ve deneme için.", + features: [ + "5 ilan", + "10 müşteri", + "3 sunum", + "2 ekip üyesi", + "Temel destek", + ], + }, + pro: { + id: "pro", + name: "Pro", + price: 499, + currency: "TRY", + description: "Büyüyen emlak ofisleri için sınırsız kullanım.", + features: [ + "Sınırsız ilan", + "Sınırsız müşteri", + "Sınırsız sunum", + "Sınırsız ekip üyesi", + "Otomatik eşleştirme", + "Yatırımcı portalı", + "Öncelikli destek", + ], + }, +};