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:
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user