perf: memoize parseImageIds, fix checkLimit OR query, loading skeletons, dashboard cache, compound indexes, sidebar active state, matches notified fix, padding fixes, match criteria in property detail

This commit is contained in:
egecankomur
2026-05-13 13:08:05 +03:00
parent 933cb17107
commit 7c677dfa4b
34 changed files with 1257 additions and 308 deletions
+66 -7
View File
@@ -1,17 +1,17 @@
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { Query } from "node-appwrite";
import { DATABASE_ID, TABLES, type TenantPlan } from "./schema";
import { DATABASE_ID, TABLES, type TenantPlan, type PlanPeriod } from "./schema";
import { createAdminClient } from "./server";
import { requireRole, requireTenant } from "./tenant-guard";
import { getEffectivePlan } from "./plan-limits";
import { PLAN_CATALOG } from "./subscription-types";
import { PLAN_CATALOG, planPrice } from "./subscription-types";
import { getShopierPlanUrl, isShopierEnabled } from "../payments/shopier";
import { createPolarCheckout, isPolarEnabled } from "../payments/polar";
const PRO_VALIDITY_DAYS = 30;
import { getPayTRToken } from "../payments/paytr";
function generateOrderId(): string {
const t = Date.now().toString(36);
@@ -19,15 +19,15 @@ function generateOrderId(): string {
return `ord_${t}_${r}`;
}
// Webhook handler'larından da çağrılabilir — provider "polar" | "shopier" | "mock"
// Webhook handler'larından da çağrılabilir — provider "polar" | "shopier" | "paytr" | "mock"
export async function activatePlanInDb(
tenantId: string,
plan: TenantPlan,
provider: string,
period: PlanPeriod = "monthly",
): Promise<void> {
const { tablesDB } = createAdminClient();
// tenant_settings satırını bul
const result = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.tenantSettings,
@@ -36,11 +36,13 @@ export async function activatePlanInDb(
const row = result.rows[0];
if (!row) throw new Error(`tenant_settings bulunamadı: ${tenantId}`);
const validityDays = period === "yearly" ? 365 : 30;
const now = new Date();
const expires = new Date(now.getTime() + PRO_VALIDITY_DAYS * 24 * 60 * 60 * 1000);
const expires = new Date(now.getTime() + validityDays * 24 * 60 * 60 * 1000);
await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, row.$id, {
plan,
planPeriod: period,
planExpiresAt: expires.toISOString(),
planProvider: provider,
});
@@ -118,6 +120,63 @@ export async function startPolarCheckoutAction(formData: FormData): Promise<void
redirect(checkout.url);
}
// ── PayTR checkout ─────────────────────────────────────────────────────────────
export async function getPayTRTokenAction(formData: FormData): Promise<string> {
const planId = String(formData.get("plan") ?? "") as Exclude<TenantPlan, "free">;
const period = String(formData.get("period") ?? "monthly") as PlanPeriod;
if (!["starter", "pro", "enterprise"].includes(planId)) throw new Error("Geçersiz plan.");
if (!["monthly", "yearly"].includes(period)) throw new Error("Geçersiz dönem.");
const ctx = await requireTenant();
requireRole(ctx, ["owner"]);
const catalog = PLAN_CATALOG[planId];
const price = planPrice(catalog, period);
// APP_URL: server-to-server callback (HTTPS zorunlu, prod'da veya ngrok)
// APP_BROWSER_URL: browser redirect (dev'de localhost, prod'da aynı domain)
const appUrl = process.env.APP_URL?.replace(/\/$/, "") ?? "http://localhost:3000";
const browserUrl = (process.env.APP_BROWSER_URL ?? appUrl).replace(/\/$/, "");
const requestHeaders = await headers();
const userIp =
requestHeaders.get("x-forwarded-for")?.split(",")[0]?.trim() ??
requestHeaders.get("x-real-ip") ??
"1.2.3.4";
const timestamp = Date.now().toString();
const random = Math.random().toString(36).slice(2, 8);
// Uppercase harfler separator — tenantId (lowercase a-z0-9) hiçbir zaman içermez
// Format: {tenantId}T{timestamp}{random}P{plan}X{period}
const merchantOid = `${ctx.tenantId}T${timestamp}${random}P${planId}X${period}`;
const userBasket: Array<[string, string, number]> = [
[
`KovakEmlak ${catalog.name} (${period === "yearly" ? "Yıllık" : "Aylık"})`,
price.toFixed(2),
1,
],
];
const officeName = ctx.settings?.officeName ?? ctx.user.name ?? "Müşteri";
return getPayTRToken({
merchantOid,
email: ctx.user.email,
userName: officeName,
userAddress: ctx.settings?.address ?? "Türkiye",
userPhone: ctx.settings?.phone ?? "05000000000",
paymentAmountKurus: price * 100,
userBasket,
userIp,
notifyUrl: `${appUrl}/api/payments/paytr/callback`,
okUrl: `${browserUrl}/settings/billing?upgraded=1`,
failUrl: `${browserUrl}/settings/billing?failed=1`,
});
}
// ── Unified entry point ────────────────────────────────────────────────────────
export async function startCheckoutAction(formData: FormData): Promise<void> {