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