init: kovakemlak-crm project scaffold

- Next.js 16 + Appwrite multi-tenant emlak CRM
- Database: kovakemlak-db (properties, customers, customer_searches, property_matches, presentations, investors, activities, tenant_settings)
- Same stack as isletmem-kovakcrm (shadcn/ui template base)
- Modules: portföy, müşteri takibi, arama kriterleri, otomatik eşleştirme, sunum linki, yatırımcı portalı
This commit is contained in:
egecankomur
2026-05-05 04:37:04 +03:00
commit 37679e83e6
383 changed files with 53525 additions and 0 deletions
@@ -0,0 +1,137 @@
import { NextRequest, NextResponse } from "next/server";
import { Query } from "node-appwrite";
import { logAudit } from "@/lib/appwrite/audit";
import { DATABASE_ID, TABLES, type SubscriptionPayment } from "@/lib/appwrite/schema";
import { createAdminClient } from "@/lib/appwrite/server";
import {
verifyShopierWebhookSignature,
type ShopierWebhookOrder,
} from "@/lib/payments/shopier";
const PRO_VALIDITY_DAYS = 30;
export async function POST(req: NextRequest): Promise<NextResponse> {
const signature = req.headers.get("Shopier-Signature") ?? "";
const event = req.headers.get("Shopier-Event") ?? "";
// Raw body'yi hem imza doğrulama hem de parse için kullan
let rawBody: string;
try {
rawBody = await req.text();
} catch {
return NextResponse.json({ error: "invalid body" }, { status: 400 });
}
if (!verifyShopierWebhookSignature(signature, rawBody)) {
console.error("[shopier/callback] signature mismatch", { event });
return NextResponse.json({ error: "signature mismatch" }, { status: 403 });
}
// Yalnızca sipariş oluşturma eventini işle
if (event !== "order.created") {
return new NextResponse("OK", { status: 200 });
}
let order: ShopierWebhookOrder;
try {
order = JSON.parse(rawBody) as ShopierWebhookOrder;
} catch {
return NextResponse.json({ error: "invalid json" }, { status: 400 });
}
// Yalnızca ödenmiş siparişleri işle
if (order.paymentStatus !== "paid") {
return new NextResponse("OK", { status: 200 });
}
const buyerEmail = order.shippingInfo?.email ?? "";
if (!buyerEmail) {
console.error("[shopier/callback] no buyer email in order", order.id);
return new NextResponse("OK", { status: 200 });
}
const { tablesDB } = createAdminClient();
// Bekleyen Shopier ödemelerini al, email ile eşleştir
const pendingResult = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.subscriptionPayments,
queries: [
Query.equal("provider", "shopier"),
Query.equal("status", "pending"),
Query.orderDesc("$createdAt"),
Query.limit(20),
],
});
const pendingPayments = pendingResult.rows as unknown as SubscriptionPayment[];
const payment = pendingPayments.find((p) => {
try {
const payload = JSON.parse(p.providerPayload ?? "{}") as { userEmail?: string };
return payload.userEmail?.toLowerCase() === buyerEmail.toLowerCase();
} catch {
return false;
}
});
if (!payment) {
console.error("[shopier/callback] no matching pending payment for email", buyerEmail);
// 200 döndür — Shopier'in retry yapmasını engelle, durumu logla
return new NextResponse("OK", { status: 200 });
}
// Idempotency — zaten işlendiyse atla
if (payment.status === "success" || payment.status === "failed") {
return new NextResponse("OK", { status: 200 });
}
const now = new Date();
const expires = new Date(now.getTime() + PRO_VALIDITY_DAYS * 24 * 60 * 60 * 1000);
await tablesDB.updateRow(DATABASE_ID, TABLES.subscriptionPayments, payment.$id, {
status: "success",
processedAt: now.toISOString(),
providerPayload: JSON.stringify({
shopierOrderId: order.id,
buyerEmail,
total: order.totals?.total,
currency: order.currency,
dateCreated: order.dateCreated,
}),
});
// Tenant planını aktive et
const settingsResult = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.tenantSettings,
queries: [Query.equal("tenantId", payment.tenantId), Query.limit(1)],
});
const settings = settingsResult.rows[0] as unknown as { $id: string } | undefined;
if (settings) {
await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, settings.$id, {
plan: payment.plan,
planStartedAt: now.toISOString(),
planExpiresAt: expires.toISOString(),
lastPaymentId: payment.$id,
});
}
await logAudit({
tenantId: payment.tenantId,
userId: payment.createdBy,
action: "update",
entityType: "subscription_payment",
entityId: payment.$id,
changes: {
provider: "shopier",
status: "success",
plan: payment.plan,
shopierOrderId: order.id,
},
});
return new NextResponse("OK", { status: 200 });
}