feat(billing): payment infrastructure pre-prep

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
This commit is contained in:
egecankomur
2026-05-08 15:26:18 +03:00
parent 95a7cbaf0d
commit 3cce632eb3
11 changed files with 633 additions and 4 deletions
+46 -1
View File
@@ -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<NextResponse> {
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 });
}