feat: emlak CRM iskelet kurulumu
- schema.ts tamamen yeniden yazıldı (properties, customers, customer_searches, property_matches, presentations, investors, activities, tenant_settings) - Sidebar emlak modüllerine güncellendi (İlanlar, Müşteriler, Yatırımcılar, Sunumlar, Aktiviteler) - Eski CRM lib dosyaları temizlendi (finance, invoice, lead, task, software, vs.) - Yeni modül dizinleri oluşturuldu (stub pages) - command-search emlak navigasyonuna güncellendi - site-header temizlendi - Typecheck: 0 hata (chart.tsx template hariç)
This commit is contained in:
@@ -1,137 +1,5 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function POST() {
|
||||
return new NextResponse("OK", { status: 200 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user