"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { ID, Permission, Query, Role } from "node-appwrite"; import { logAudit } from "./audit"; import { persistCardFromMockCheckout } from "./saved-card-actions"; import { getDefaultCard } from "./saved-card-queries"; import { DATABASE_ID, TABLES, type SubscriptionPayment, type TenantPlan } from "./schema"; import { createAdminClient } from "./server"; import { requireRole, requireTenant } from "./tenant-guard"; import { PLAN_CATALOG } from "./subscription-types"; import { isShopierEnabled } from "../payments/shopier"; const PRO_VALIDITY_DAYS = 30; function teamRowPermissions(tenantId: string) { return [ Permission.read(Role.team(tenantId, "owner")), Permission.read(Role.team(tenantId, "admin")), Permission.update(Role.team(tenantId, "owner")), Permission.delete(Role.team(tenantId, "owner")), ]; } function generateOrderId(): string { const t = Date.now().toString(36); const r = Math.random().toString(36).slice(2, 10); return `ord_${t}_${r}`; } export async function startMockCheckoutAction(formData: FormData): Promise { const plan = String(formData.get("plan") ?? "") as TenantPlan; if (plan !== "pro") throw new Error("Geçersiz plan."); const ctx = await requireTenant(); requireRole(ctx, ["owner"]); const catalog = PLAN_CATALOG[plan]; const orderId = generateOrderId(); const { tablesDB } = createAdminClient(); await tablesDB.createRow( DATABASE_ID, TABLES.subscriptionPayments, ID.unique(), { tenantId: ctx.tenantId, createdBy: ctx.user.id, orderId, plan, amount: catalog.price, currency: catalog.currency, status: "pending", provider: "mock", }, teamRowPermissions(ctx.tenantId), ); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "create", entityType: "subscription_payment", entityId: orderId, changes: { plan, amount: catalog.price, provider: "mock" }, }); redirect(`/settings/billing/checkout/${orderId}`); } async function findPendingPaymentByOrderId( tenantId: string, orderId: string, ): Promise { const { tablesDB } = createAdminClient(); const result = await tablesDB.listRows({ databaseId: DATABASE_ID, tableId: TABLES.subscriptionPayments, queries: [Query.equal("orderId", orderId), Query.equal("tenantId", tenantId), Query.limit(1)], }); return (result.rows[0] as unknown as SubscriptionPayment) ?? null; } export async function confirmMockPaymentAction(formData: FormData): Promise { const orderId = String(formData.get("orderId") ?? ""); if (!orderId) throw new Error("orderId eksik."); const ctx = await requireTenant(); requireRole(ctx, ["owner"]); const payment = await findPendingPaymentByOrderId(ctx.tenantId, orderId); if (!payment) throw new Error("Ödeme bulunamadı."); if (payment.status === "success") { redirect(`/settings/billing?upgraded=1`); } if (payment.provider !== "mock") { throw new Error("Bu ödeme mock olarak onaylanamaz."); } const saveCard = String(formData.get("saveCard") ?? "") === "true"; const useSavedCardId = String(formData.get("savedCardId") ?? ""); const { tablesDB } = createAdminClient(); 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({ mock: true, confirmedBy: ctx.user.id, usedSavedCardId: useSavedCardId || undefined, }), }); if (ctx.settings) { await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, ctx.settings.$id, { plan: payment.plan, planStartedAt: now.toISOString(), planExpiresAt: expires.toISOString(), lastPaymentId: payment.$id, }); } if (saveCard && !useSavedCardId) { const last4 = String(formData.get("cardLast4") ?? "").replace(/\D/g, "").slice(-4); const month = parseInt(String(formData.get("cardExpiryMonth") ?? "0"), 10); const year = parseInt(String(formData.get("cardExpiryYear") ?? "0"), 10); const brand = String(formData.get("cardBrand") ?? "unknown"); const holder = String(formData.get("cardHolder") ?? "").trim(); if (last4.length === 4 && month > 0 && year >= 2026) { const existingDefault = await getDefaultCard(ctx.tenantId); try { await persistCardFromMockCheckout({ brand, last4, expiryMonth: month, expiryYear: year, holderName: holder, makeDefault: !existingDefault, }); } catch { // best-effort — payment already succeeded, don't fail upgrade if card save errors } } } await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "update", entityType: "subscription_payment", entityId: payment.$id, changes: { status: "success", plan: payment.plan, expires: expires.toISOString() }, }); revalidatePath("/settings/billing"); redirect(`/settings/billing?upgraded=1`); } export async function cancelMockPaymentAction(formData: FormData): Promise { const orderId = String(formData.get("orderId") ?? ""); if (!orderId) throw new Error("orderId eksik."); const ctx = await requireTenant(); requireRole(ctx, ["owner"]); const payment = await findPendingPaymentByOrderId(ctx.tenantId, orderId); if (!payment) { redirect(`/settings/billing`); } if (payment && payment.status === "pending") { const { tablesDB } = createAdminClient(); await tablesDB.updateRow(DATABASE_ID, TABLES.subscriptionPayments, payment.$id, { status: "failed", processedAt: new Date().toISOString(), providerPayload: JSON.stringify({ mock: true, cancelledBy: ctx.user.id }), }); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "update", entityType: "subscription_payment", entityId: payment.$id, changes: { status: "failed" }, }); } revalidatePath("/settings/billing"); redirect(`/settings/billing?cancelled=1`); } export async function downgradeToFreeAction(): Promise { const ctx = await requireTenant(); requireRole(ctx, ["owner"]); if (!ctx.settings) throw new Error("Ayar yok."); const { tablesDB } = createAdminClient(); await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, ctx.settings.$id, { plan: "free", planExpiresAt: null, }); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "update", entityType: "tenant_settings", entityId: ctx.settings.$id, changes: { plan: "free" }, }); revalidatePath("/settings/billing"); redirect(`/settings/billing?downgraded=1`); } export async function startShopierCheckoutAction(formData: FormData): Promise { const plan = String(formData.get("plan") ?? "") as TenantPlan; if (plan !== "pro") throw new Error("Geçersiz plan."); const ctx = await requireTenant(); requireRole(ctx, ["owner"]); const catalog = PLAN_CATALOG[plan]; const orderId = generateOrderId(); const { tablesDB } = createAdminClient(); await tablesDB.createRow( DATABASE_ID, TABLES.subscriptionPayments, ID.unique(), { tenantId: ctx.tenantId, createdBy: ctx.user.id, orderId, plan, amount: catalog.price, currency: catalog.currency, status: "pending", provider: "shopier", }, teamRowPermissions(ctx.tenantId), ); await logAudit({ tenantId: ctx.tenantId, userId: ctx.user.id, action: "create", entityType: "subscription_payment", entityId: orderId, changes: { plan, amount: catalog.price, provider: "shopier" }, }); redirect(`/settings/billing/checkout/${orderId}/shopier`); } // Unified entry point — branches on PAYMENT_PROVIDER env variable. // Set PAYMENT_PROVIDER=shopier in production; leave unset (or "mock") for testing. export async function startCheckoutAction(formData: FormData): Promise { if (isShopierEnabled()) { return startShopierCheckoutAction(formData); } return startMockCheckoutAction(formData); }