{"repo": "ahenkbt/haberler", "files": {"goalgo/artifacts/api-server/src/routes/providers.ts": {"5a4028290a3c4e95d8eec61872daf094760fc582": {"t": 1786067417.6586764, "n": 188876, "h": "19253fbca83f8841", "c": "import { Router, type NextFunction, type Request, type Response } from \"express\";\nimport { db } from \"@workspace/db\";\nimport { sql, eq, and, inArray } from \"drizzle-orm\";\nimport { vendorMenuItemsTable } from \"@workspace/db\";\nimport {\n orderMessagesTable,\n deliveryOrdersTable,\n deliveryOrderStatusEventsTable,\n vendorsTable,\n siteSettingsTable,\n} from \"@workspace/db\";\nimport {\n notifyVendorWhatsApp,\n notifyAdminWhatsApp,\n notifyCustomerOrderWhatsApp,\n trySendSiteOutboundWhatsApp,\n phoneDigitsVariants,\n digitsForWhatsAppLink,\n} from \"../lib/whatsapp\";\nimport { deliveryStatusTransitionAllowed } from \"../lib/delivery-status-transitions\";\nimport bcrypt from \"bcryptjs\";\nimport jwt from \"jsonwebtoken\";\nimport crypto from \"crypto\";\nimport { verifyLoginMathCaptcha } from \"../lib/loginMathCaptcha.js\";\nimport { sendEmail, buildActivationEmail, buildPasswordResetEmail } from \"../lib/email\";\nimport {\n importVendorFromExternalUrl,\n buildExternalMenuPreview,\n externalMenuPreviewPayload,\n buildMergedAddress,\n filterMenuSelection,\n type ImportedMenuItem,\n type ImportedVendorData,\n type ExternalPlatform,\n} from \"../lib/external-delivery-import\";\nimport { normalizeRevenueModel, sanitizeTrIban } from \"../lib/vendor-revenue.js\";\nimport { generateTurkishVendorAboutDetailed } from \"../lib/vendor-about-ai.js\";\nimport { sitePublicOrigin } from \"../lib/site-public-origin.js\";\nimport { getSessionSecret, getGeliverWebhookSecret } from \"../lib/secrets.js\";\nimport { vendorApprovalTempPassword } from \"../lib/demo-credentials.js\";\nimport {\n createGeliverDraftShipmentForOrder,\n createGeliverShipmentForOrder,\n createGeliverManualShipment,\n ensureGeliverIntegrationColumns,\n geliverJsonSafe,\n geliverApiListCitiesForVendor,\n geliverApiListDistrictsForVendor,\n geliverApiListShipmentsForVendor,\n geliverApiGetShipmentForVendor,\n geliverApiUpdateShipmentPackageForVendor,\n geliverApiAcceptOfferForVendor,\n geliverApiListAddressesForVendor,\n geliverApiGetAddressForVendor,\n geliverApiCreateSenderAddressForVendor,\n geliverApiDeleteAddressForVendor,\n geliverApiGetBalanceForVendor,\n geliverApiListWebhooksForVendor,\n geliverApiCreateWebhookForVendor,\n geliverApiDeleteWebhookForVendor,\n geliverApiTestWebhookForVendor,\n geliverApiListParcelTemplatesForVendor,\n geliverApiCreateParcelTemplateForVendor,\n geliverApiDeleteParcelTemplateForVendor,\n geliverApiListPricesForVendor,\n geliverApiListProviderAccountsForVendor,\n geliverApiCreateProviderAccountForVendor,\n geliverApiDeleteProviderAccountForVendor,\n} from \"../lib/geliver-shipment.js\";\nimport {\n getEcommerceCategoryTree,\n resolveProductCategoryForVendor,\n} from \"../lib/ecommerce-product-categories.js\";\nimport { logger } from \"../lib/logger.js\";\nimport { denyUnlessAdminMaintenance, denyUnlessAdminMaintenanceAny } from \"../lib/admin-guard.js\";\nimport { ensureVendorBlogTables } from \"../lib/vendor-blog-db.js\";\nimport {\n VENDOR_THEME_CATALOG,\n ensureVendorCustomDomainsTable,\n ensureVendorThemeColumns,\n listVendorDomains,\n mergeThemeConfig,\n cleanVendorNavMenuItems,\n readVendorNavMenuFromThemeConfig,\n normalizeVendorDomain,\n resolveDefaultThemeKey,\n resolveVendorCustomDomainShortPath,\n resolveVendorStorefrontPath,\n themeDefByKey,\n themesForVendor,\n} from \"../lib/vendor-storefront.js\";\n\nconst router = Router();\n\nfunction escapeHtmlLite(s: string): string {\n return String(s)\n .replace(/&/g, \"&\")\n .replace(/ {\n try {\n await db.execute(sql`ALTER TABLE vendor_couriers ADD COLUMN IF NOT EXISTS password_hash text`);\n } catch { /* ignore */ }\n try {\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS password_hash text`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS is_open BOOLEAN NOT NULL DEFAULT true`);\n } catch { /* ignore */ }\n try {\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS revenue_model TEXT NOT NULL DEFAULT 'subscription'`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS commission_rate_pct NUMERIC(8,4)`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS payout_bank_holder TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS payout_bank_iban TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS payout_bank_branch TEXT`);\n } catch { /* ignore */ }\n try {\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_api_token TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_sender_address_id TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_sender_zip TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_sender_mahalle TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_auto_ship_on_order BOOLEAN NOT NULL DEFAULT false`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_organization_id TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS customer_postal_code TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_shipment_id TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_tracking_number TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_label_url TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_transaction_id TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_status TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_last_error TEXT`);\n } catch { /* ignore */ }\n try {\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS vendor_subscription_requests (\n id SERIAL PRIMARY KEY,\n vendor_id INTEGER NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,\n start_date DATE NOT NULL,\n end_date DATE NOT NULL,\n payment_method TEXT NOT NULL DEFAULT 'bank_transfer',\n receipt_url TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n admin_note TEXT,\n processed_at TIMESTAMP,\n created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n )\n `);\n } catch { /* ignore */ }\n})();\n\n/* Servis tipi → Türkçe etiket */\nexport const PROVIDER_TYPE_LABELS: Record = {\n siparis: \"Sipariş İşletmesi\",\n alisveris:\"Alışveriş Mağazası\",\n hizmet: \"Hizmet Sağlayıcı\",\n ilan: \"İlan Odaklı İşletme\",\n turizm: \"Turizm & Seyahat\",\n ulasim: \"Ulaşım & Bildirim\",\n \"ulaşım\": \"Ulaşım & Bildirim\",\n transport:\"Ulaşım & Bildirim\",\n delivery: \"Sipariş İşletmesi\",\n ecommerce:\"Alışveriş Mağazası\",\n};\n\nexport const PROVIDER_SUBTYPE_LABELS: Record = {\n otel:\"Otel\", arac:\"Rent a Car\", villa:\"Villa & Ev Kiralama\", tur:\"Tur\", yat:\"Yat & Tekne\",\n restoran:\"Restoran / Kafe\", market:\"Market / Bakkal\", eczane:\"Eczane\",\n cicekci:\"Çiçekçi\", giyim:\"Giyim / Tekstil\", elektronik:\"Elektronik\",\n kitap:\"Kitap / Kırtasiye\", ev:\"Ev & Yaşam\", spor:\"Spor / Outdoor\",\n kozmetik:\"Kozmetik / Güzellik\", cekici:\"Çekici / Yol Yardım\",\n nakliyeci:\"Nakliyeci\", oto_galeri:\"Oto Galeri\",\n kurs:\"Kurs / Eğitim\", tadilat:\"Tadilat / Tamir\", temizlik:\"Temizlik\",\n boya:\"Boya / Badana\", guzellik:\"Güzellik Salonu\", saglik:\"Sağlık / Klinik\",\n ikinci_el:\"2. El Eşya\", is_makinesi:\"İş Makineleri\", telefon:\"Telefon / GSM\",\n beyaz_esya:\"Beyaz Eşya\", hayvan:\"Hayvanlar / Pet\", diger:\"Diğer\",\n taksi:\"Şehir İçi Yolcu Taşıma\", taxi:\"Şehir İçi Yolcu Taşıma\",\n kurye:\"Kurye\", courier:\"Kurye\", rideshare:\"Ortak Yolculuk\",\n};\n\ntype Row = Record;\n\nfunction normalizeProviderPanelText(v: unknown): string {\n return String(v ?? \"\").trim().toLocaleLowerCase(\"tr-TR\");\n}\n\nfunction providerPanelKindForRow(row: Row): \"delivery\" | \"tourism\" | \"transport\" {\n const type = normalizeProviderPanelText(row.provider_type ?? row.vendor_type);\n const subtype = normalizeProviderPanelText(row.provider_subtype);\n if ([\"turizm\", \"tourism\"].includes(type)) return \"tourism\";\n if ([\"ulasim\", \"ulaşım\", \"transport\"].includes(type)) return \"transport\";\n if ([\"otel\", \"hotel\", \"arac\", \"car\", \"rentacar\", \"villa\", \"tur\", \"tour\", \"yat\", \"boat\", \"tekne\"].includes(subtype)) {\n return \"tourism\";\n }\n if ([\"taksi\", \"taxi\", \"kurye\", \"courier\", \"cekici\", \"tow\", \"nakliyeci\", \"moving\", \"kargo\", \"cargo\", \"rideshare\"].includes(subtype)) {\n return \"transport\";\n }\n return \"delivery\";\n}\n\nfunction attachProviderPanelRoute(row: Row): Row {\n const kind = providerPanelKindForRow(row);\n row.panel_kind = kind;\n row.panel_route = kind === \"tourism\" ? \"/turizm-paneli\" : \"/servis-saglayici-paneli\";\n return row;\n}\n\nconst TABLE_SERVICE_PROVIDER_SUBTYPES = new Set([\"\", \"restoran\", \"restaurant\", \"cafe\", \"kafe\", \"restoran-kafe\", \"restoran_cafe\"]);\n\nfunction canVendorUseTableService(row: { provider_type?: unknown; vendor_type?: unknown; provider_subtype?: unknown }): boolean {\n const type = String(row.provider_type || row.vendor_type || \"\").toLowerCase();\n const subtype = String(row.provider_subtype || \"\").toLocaleLowerCase(\"tr-TR\").replace(/\\s+/g, \"_\");\n return [\"siparis\", \"delivery\"].includes(type) && TABLE_SERVICE_PROVIDER_SUBTYPES.has(subtype);\n}\n\nfunction membershipPricingFromSiteRow(site: typeof siteSettingsTable.$inferSelect | undefined) {\n const rateRaw = site?.usdTryRate != null ? parseFloat(String(site.usdTryRate)) : NaN;\n const rate = Number.isFinite(rateRaw) && rateRaw > 0 ? rateRaw : null;\n const num = (d: unknown, fallback: number) => {\n if (d == null || d === \"\") return fallback;\n const n = parseFloat(String(d).replace(\",\", \".\"));\n return Number.isFinite(n) && n >= 0 ? n : fallback;\n };\n const std = num(site?.providerMembershipStandardUsd, 10);\n const gold = num(site?.providerMembershipGoldUsd, 10);\n const prem = num(site?.providerMembershipPremiumPerBusinessUsd, 10);\n const tryMul = (usd: number) => (rate != null ? Math.round(usd * rate * 100) / 100 : null);\n return {\n usdTryRate: rate,\n usdTryUpdatedAt: site?.usdTryUpdatedAt ? site.usdTryUpdatedAt.toISOString() : null,\n standardUsdMonthly: std,\n goldUsdMonthly: gold,\n premiumPerBusinessUsdMonthly: prem,\n standardTryMonthly: tryMul(std),\n goldTryMonthly: tryMul(gold),\n premiumPerBusinessTryMonthly: tryMul(prem),\n businessMembershipTryDaily: 10,\n businessMembershipTryAnnual: 3650,\n /** Tek işletme için tahmini Premium aylık (Gold + 1× işletme premium USD), TRY */\n premiumBundleTryMonthlyOneBusiness: rate != null ? Math.round((gold + prem) * rate * 100) / 100 : null,\n };\n}\n\n/** Panel oturumu: istemci `x-vendor-email` ile hem yetkili hem işletme e-postası doğrulanır */\nfunction sqlVendorSessionEmailMatches(headerEmail: string) {\n const e = String(headerEmail).trim();\n return sql`(\n LOWER(TRIM(COALESCE(owner_email, ''))) = LOWER(TRIM(${e}))\n OR LOWER(TRIM(COALESCE(email, ''))) = LOWER(TRIM(${e}))\n )`;\n}\n\n/** `notes` içinde satır bazlı `anahtar: değer` güncelle (ör. qr_menu_public:1) */\nfunction mergeVendorNoteLine(notes: string | null | undefined, key: string, value: string): string {\n const lines = String(notes ?? \"\")\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter(Boolean);\n const prefix = `${key.toLowerCase()}:`;\n const kept = lines.filter((l) => !l.toLowerCase().startsWith(prefix));\n kept.push(`${key}: ${value}`);\n return kept.join(\"\\n\");\n}\n\nfunction vendorNoteFlagOn(notes: string | null | undefined, key: string): boolean {\n const raw = String(notes ?? \"\");\n const esc = key.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const re = new RegExp(`(?:^|\\n)${esc}\\\\s*:\\\\s*(\\\\S+)`, \"i\");\n const m = raw.match(re);\n const v = (m?.[1] ?? \"\").toLowerCase();\n return v === \"1\" || v === \"true\" || v === \"on\" || v === \"yes\";\n}\n\nconst r = (res: unknown): Row[] => ((res as { rows?: Row[] }).rows ?? res) as Row[];\n\ntype VendorSession = {\n id: number;\n email: string;\n};\n\ntype VendorJwtPayload = jwt.JwtPayload & {\n typ?: string;\n vendorId?: number;\n email?: string;\n};\n\ntype CourierSession = {\n id: number;\n phone: string;\n};\n\ntype CourierJwtPayload = jwt.JwtPayload & {\n typ?: string;\n courierId?: number;\n phone?: string;\n};\n\nfunction signVendorSessionToken(vendorId: number, email: string): string {\n return jwt.sign(\n { typ: \"vendor\", vendorId, email },\n getSessionSecret(),\n { expiresIn: \"30d\", audience: \"yekpare:vendor\", issuer: \"yekpare-api\", subject: String(vendorId) },\n );\n}\n\nfunction verifyVendorSessionToken(req: Request): VendorSession | null {\n const auth = String(req.headers.authorization ?? \"\").trim();\n const token = auth.toLowerCase().startsWith(\"bearer \") ? auth.slice(7).trim() : \"\";\n if (!token) return null;\n try {\n const decoded = jwt.verify(token, getSessionSecret(), {\n audience: \"yekpare:vendor\",\n issuer: \"yekpare-api\",\n }) as VendorJwtPayload;\n const id = Number(decoded.vendorId ?? decoded.sub);\n const email = String(decoded.email ?? \"\").trim();\n if (decoded.typ !== \"vendor\" || !Number.isFinite(id) || id <= 0 || !email) return null;\n return { id, email };\n } catch {\n return null;\n }\n}\n\nfunction requireVendorSession(req: Request, res: Response, next: NextFunction): void {\n const path = (req.path || \"\").replace(/^\\/providers\\/?/, \"/\");\n if (path === \"/geliver/webhook\") {\n next();\n return;\n }\n const session = verifyVendorSessionToken(req);\n if (!session) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n req.headers[\"x-vendor-id\"] = String(session.id);\n req.headers[\"x-vendor-email\"] = session.email;\n next();\n}\n\nfunction signCourierSessionToken(courierId: number, phone: string): string {\n return jwt.sign(\n { typ: \"courier\", courierId, phone },\n getSessionSecret(),\n { expiresIn: \"30d\", audience: \"yekpare:courier\", issuer: \"yekpare-api\", subject: String(courierId) },\n );\n}\n\nfunction verifyCourierSessionToken(req: Request): CourierSession | null {\n const auth = String(req.headers.authorization ?? \"\").trim();\n const token = auth.toLowerCase().startsWith(\"bearer \") ? auth.slice(7).trim() : \"\";\n if (!token) return null;\n try {\n const decoded = jwt.verify(token, getSessionSecret(), {\n audience: \"yekpare:courier\",\n issuer: \"yekpare-api\",\n }) as CourierJwtPayload;\n const id = Number(decoded.courierId ?? decoded.sub);\n const phone = String(decoded.phone ?? \"\").trim();\n if (decoded.typ !== \"courier\" || !Number.isFinite(id) || id <= 0 || !phone) return null;\n return { id, phone };\n } catch {\n return null;\n }\n}\n\nfunction requireCourierSession(req: Request, res: Response, next: NextFunction): void {\n const session = verifyCourierSessionToken(req);\n if (!session) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n req.headers[\"x-courier-id\"] = String(session.id);\n req.headers[\"x-courier-phone\"] = session.phone;\n next();\n}\n\nfunction courierPhoneFromSession(req: Request): string {\n return String(req.headers[\"x-courier-phone\"] ?? \"\").trim();\n}\n\nasync function vendorSessionOwnsRow(vendorId: unknown, vendorEmail: unknown): Promise {\n if (!vendorId || !vendorEmail) return false;\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n return Boolean(own);\n}\n\n/* — Yardımcı: slug üret — */\nfunction slugify(str: string): string {\n return str.toLowerCase()\n .replace(/ğ/g, \"g\").replace(/ü/g, \"u\").replace(/ş/g, \"s\")\n .replace(/ı/g, \"i\").replace(/ö/g, \"o\").replace(/ç/g, \"c\")\n .replace(/[^a-z0-9]+/g, \"-\").replace(/^-|-$/g, \"\");\n}\n\nfunction noteField(notes: string | null | undefined, key: string): string {\n const raw = String(notes || \"\");\n const m = raw.match(new RegExp(`${key}:([^|\\\\n]+)`));\n return m?.[1]?.trim() || \"\";\n}\n\nfunction upsertNoteField(notes: string | null | undefined, key: string, value: string | null | undefined): string {\n const raw = String(notes || \"\");\n const parts = raw.split(\"|\").map((x) => x.trim()).filter(Boolean);\n const filtered = parts.filter((p) => !p.toLowerCase().startsWith(`${key.toLowerCase()}:`));\n const v = String(value || \"\").trim();\n if (v) filtered.push(`${key}:${v}`);\n return filtered.join(\" | \");\n}\n\nfunction maskSecret(v: string): string {\n if (!v) return \"\";\n if (v.length <= 8) return \"****\";\n return `${v.slice(0, 4)}...${v.slice(-4)}`;\n}\n\nexport async function importMenuIntoVendor(vendorId: number, menu: Array<{ name: string; category?: string | null; description?: string | null; price?: number | null; imageUrl?: string | null; options?: Array<{ name: string; required?: boolean; multiple?: boolean; choices: Array<{ name: string; price?: number }> }> }>) {\n const catMap = new Map();\n let categories = 0;\n let items = 0;\n let options = 0;\n for (const m of menu) {\n const catName = String(m.category || \"Diğer\").trim() || \"Diğer\";\n const catKey = catName.toLowerCase();\n let catId = catMap.get(catKey);\n if (!catId) {\n const q = r(await db.execute(sql`\n INSERT INTO vendor_menu_categories (vendor_id, name, position, active)\n VALUES (${vendorId}, ${catName}, 999, true)\n ON CONFLICT DO NOTHING\n RETURNING id\n `));\n if (q[0]?.id) catId = Number(q[0].id);\n if (!catId) {\n const ex = r(await db.execute(sql`\n SELECT id FROM vendor_menu_categories\n WHERE vendor_id = ${vendorId} AND LOWER(name) = LOWER(${catName})\n LIMIT 1\n `));\n if (ex[0]?.id) catId = Number(ex[0].id);\n }\n if (catId) {\n catMap.set(catKey, catId);\n categories++;\n }\n }\n if (!catId || !m.name) continue;\n const ins = r(await db.execute(sql`\n INSERT INTO vendor_menu_items (vendor_id, menu_category_id, name, description, price, image_url, active)\n VALUES (${vendorId}, ${catId}, ${m.name}, ${m.description || null}, ${m.price != null ? String(m.price) : \"0\"}, ${m.imageUrl || null}, true)\n ON CONFLICT DO NOTHING\n RETURNING id\n `));\n let itemId: number | null = ins[0]?.id ? Number(ins[0].id) : null;\n if (!itemId) {\n const ex = r(await db.execute(sql`\n SELECT id, menu_category_id, image_url FROM vendor_menu_items\n WHERE vendor_id = ${vendorId} AND LOWER(name) = LOWER(${m.name})\n LIMIT 1\n `));\n if (ex[0]?.id) {\n itemId = Number(ex[0].id);\n const img = String(m.imageUrl ?? \"\").trim() || null;\n await db.execute(sql`\n UPDATE vendor_menu_items SET\n menu_category_id = ${catId},\n description = COALESCE(${m.description || null}, description),\n price = COALESCE(${m.price != null ? String(m.price) : null}, price),\n image_url = CASE WHEN ${img} IS NOT NULL AND ${img} <> '' THEN ${img} ELSE image_url END,\n updated_at = now()\n WHERE id = ${itemId} AND vendor_id = ${vendorId}\n `);\n }\n }\n if (itemId) items++;\n if (itemId && m.options?.length) {\n for (const og of m.options) {\n if (!og.name || !og.choices?.length) continue;\n await db.execute(sql`\n INSERT INTO vendor_item_options (menu_item_id, name, required, multiple, choices)\n VALUES (\n ${itemId},\n ${og.name},\n ${Boolean(og.required)},\n ${Boolean(og.multiple)},\n ${JSON.stringify(og.choices)}::jsonb\n )\n `);\n options++;\n }\n }\n }\n return { categories, items, options };\n}\n\ntype ExternalMenuImportInput = {\n sourceUrl?: string;\n menu?: ImportedMenuItem[];\n selectedCategories?: string[];\n city?: string;\n district?: string;\n neighborhood?: string;\n address?: string;\n phone?: string;\n lat?: number | null;\n lng?: number | null;\n rating?: number | null;\n reviewCount?: number | null;\n platform?: ExternalPlatform | string;\n name?: string;\n prefetchedHtml?: string;\n prefetchedMenuJson?: unknown;\n};\n\nfunction pickPrefetchedHtml(body: Record | undefined): string | undefined {\n const html = String(body?.prefetchedHtml ?? \"\").trim();\n if (html.length > 400) return html;\n if (html.startsWith(\"{\") && /menu_categories|productCategories|menuCategories/i.test(html)) return html;\n return undefined;\n}\n\nfunction pickPrefetchedMenuJson(body: Record | undefined): unknown {\n const raw = body?.prefetchedMenuJson;\n if (raw && typeof raw === \"object\") return raw;\n if (typeof raw === \"string\" && raw.trim().startsWith(\"{\")) {\n try {\n return JSON.parse(raw);\n } catch {\n return undefined;\n }\n }\n return undefined;\n}\n\nexport async function runExternalMenuImportForVendor(vendorId: number, input: ExternalMenuImportInput) {\n let imported: ImportedVendorData;\n const prefetchedHtml = String(input.prefetchedHtml ?? \"\").trim();\n const hasPrefetchedHtml =\n prefetchedHtml.length > 400 ||\n (prefetchedHtml.startsWith(\"{\") && /menu_categories|productCategories|menuCategories/i.test(prefetchedHtml));\n\n if (hasPrefetchedHtml) {\n const sourceUrl = String(input.sourceUrl || \"\").trim();\n if (!sourceUrl) throw new Error(\"sourceUrl zorunlu\");\n imported = await importVendorFromExternalUrl(sourceUrl, {\n prefetchedHtml,\n prefetchedMenuJson: input.prefetchedMenuJson,\n });\n } else if (Array.isArray(input.menu) && input.menu.length > 0) {\n imported = {\n platform: (input.platform as ExternalPlatform) || \"unknown\",\n sourceUrl: String(input.sourceUrl || \"\").trim(),\n name: String(input.name || \"\").trim(),\n description: null,\n phone: input.phone ?? null,\n address: input.address ?? null,\n city: input.city ?? null,\n district: input.district ?? null,\n neighborhood: input.neighborhood ?? null,\n lat: input.lat ?? null,\n lng: input.lng ?? null,\n rating: input.rating ?? null,\n reviewCount: input.reviewCount ?? null,\n isOpen: true,\n menu: input.menu,\n };\n } else {\n const sourceUrl = String(input.sourceUrl || \"\").trim();\n if (!sourceUrl) throw new Error(\"sourceUrl zorunlu\");\n imported = await importVendorFromExternalUrl(sourceUrl, {\n prefetchedHtml: input.prefetchedHtml,\n prefetchedMenuJson: input.prefetchedMenuJson,\n });\n }\n const menuToImport = filterMenuSelection(imported.menu, input.selectedCategories);\n const [vendor] = r(await db.execute(sql`SELECT id, notes FROM vendors WHERE id = ${vendorId} LIMIT 1`));\n if (!vendor) throw new Error(\"İşletme bulunamadı\");\n const neighborhood = input.neighborhood || imported.neighborhood || null;\n const notes = [\n String(vendor.notes ?? \"\"),\n `source_platform:${imported.platform}`,\n imported.sourceUrl ? `source_url:${imported.sourceUrl}` : \"\",\n imported.sourceId ? `source_id:${imported.sourceId}` : \"\",\n neighborhood ? `neighborhood:${neighborhood}` : \"\",\n ]\n .filter(Boolean)\n .join(\" | \");\n const ratingVal =\n imported.rating != null && imported.rating > 0 && imported.rating <= 5 ? imported.rating : null;\n const reviewCountVal =\n imported.reviewCount != null && imported.reviewCount >= 0 ? Math.round(imported.reviewCount) : null;\n const mergedAddress = buildMergedAddress({\n address: input.address || imported.address,\n neighborhood,\n });\n await db.execute(sql`\n UPDATE vendors SET\n notes = ${notes},\n working_hours = COALESCE(${imported.workingHours || null}, working_hours),\n image_url = COALESCE(${imported.imageUrl || null}, image_url),\n cover_url = COALESCE(${imported.coverUrl || null}, cover_url),\n phone = COALESCE(${input.phone || imported.phone || null}, phone),\n address = COALESCE(${mergedAddress || null}, address),\n city = COALESCE(${input.city || imported.city || null}, city),\n district = COALESCE(${input.district || imported.district || null}, district),\n lat = COALESCE(${Number.isFinite(Number(input.lat)) ? Number(input.lat) : (imported.lat ?? null)}, lat),\n lng = COALESCE(${Number.isFinite(Number(input.lng)) ? Number(input.lng) : (imported.lng ?? null)}, lng),\n rating = COALESCE(${ratingVal}, rating),\n review_count = COALESCE(${reviewCountVal}, review_count),\n updated_at = now()\n WHERE id = ${vendorId}\n `);\n const menuStats = await importMenuIntoVendor(vendorId, menuToImport);\n return { imported, menuStats, menuToImport, preview: buildExternalMenuPreview({ ...imported, menu: menuToImport }) };\n}\n\nfunction sanitizeRegistrationHost(raw: unknown): string | null {\n const s = String(raw ?? \"\")\n .trim()\n .toLowerCase();\n if (!s || s.length > 200) return null;\n if (!/^[a-z0-9.-]+$/.test(s)) return null;\n return s;\n}\n\n/* ────────────────────────────────────────────────────────\n PUBLIC — Başvuru gönder\n POST /api/providers/apply\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/apply\", async (req, res): Promise => {\n const {\n businessName, providerType, providerSubtype,\n ownerName, ownerEmail, phone, address, city, district,\n lat, lng, description, docKimlik, docVergi, docImza,\n revenueModel: revenueModelRaw, commissionRatePct: commissionRateRaw,\n registrationHost: registrationHostBody,\n } = req.body as Record;\n\n const registrationHost =\n sanitizeRegistrationHost(registrationHostBody) ??\n sanitizeRegistrationHost(\n typeof req.headers[\"x-forwarded-host\"] === \"string\"\n ? req.headers[\"x-forwarded-host\"].split(\",\")[0]?.trim()\n : undefined,\n ) ??\n sanitizeRegistrationHost(typeof req.headers.host === \"string\" ? req.headers.host.split(\":\")[0] : undefined);\n\n const registrationNote = registrationHost\n ? `Kayıt kaynağı: ${registrationHost} alan adından işletme başvurusu (Yekpare).`\n : null;\n\n if (!businessName || !providerType || !ownerName || !ownerEmail || !phone) {\n res.status(400).json({ error: \"Zorunlu alanlar eksik\" });\n return;\n }\n\n const businessNameStr = String(businessName).trim();\n const providerTypeStr = String(providerType).trim();\n\n const revenueModel = normalizeRevenueModel(revenueModelRaw ?? \"subscription\");\n let commissionRatePct: number | null = null;\n if (revenueModel === \"commission\") {\n const r = parseFloat(String(commissionRateRaw ?? \"\").replace(\",\", \".\"));\n if (!Number.isFinite(r) || r <= 0 || r > 100) {\n res.status(400).json({\n error: \"Komisyon modelinde geçerli bir komisyon oranı zorunludur (%1 ile %100 arası)\",\n });\n return;\n }\n commissionRatePct = r;\n }\n\n /* Benzersiz slug oluştur */\n let baseSlug = slugify(businessNameStr);\n if (!baseSlug) baseSlug = \"isletme\";\n const existing = r(await db.execute(sql`SELECT slug FROM vendors WHERE slug LIKE ${baseSlug + \"%\"} ORDER BY slug`));\n const slug = existing.length === 0 ? baseSlug : `${baseSlug}-${existing.length + 1}`;\n\n /* Vendor tipini belirle */\n const vendorType = providerTypeStr === \"siparis\" ? \"delivery\"\n : providerTypeStr === \"alisveris\" ? \"ecommerce\"\n : providerTypeStr;\n\n try {\n const rows = r(await db.execute(sql`\n INSERT INTO vendors (\n name, slug, description, vendor_type, phone, email, address, city, district,\n lat, lng, owner_name, owner_email, whatsapp,\n provider_type, provider_subtype, application_status,\n doc_kimlik, doc_vergi, doc_imza,\n revenue_model, commission_rate_pct,\n notes,\n active, is_open, featured, rating, review_count\n ) VALUES (\n ${businessNameStr}, ${slug}, ${description || null}, ${vendorType},\n ${phone}, ${ownerEmail}, ${address || null}, ${city || null}, ${district || null},\n ${lat ? Number(lat) : null}, ${lng ? Number(lng) : null},\n ${ownerName}, ${ownerEmail}, ${phone},\n ${providerTypeStr}, ${providerSubtype || null}, 'pending',\n ${docKimlik || null}, ${docVergi || null}, ${docImza || null},\n ${revenueModel}, ${commissionRatePct},\n ${registrationNote},\n false, false, false, 0, 0\n ) RETURNING id, slug, name\n `));\n\n const newVendor = rows[0];\n\n /* Admin'e WhatsApp bildirimi — fire and forget */\n notifyAdminWhatsApp({\n eventType: \"Yeni Servis Sağlayıcı Başvurusu\",\n details: [\n `🏢 ${businessNameStr}`,\n `👤 ${ownerName} (${ownerEmail})`,\n `📞 ${phone}`,\n `📍 ${city || \"\"} ${district || \"\"}`.trim(),\n `🏷️ ${PROVIDER_TYPE_LABELS[providerTypeStr] ?? providerTypeStr}`,\n revenueModel === \"commission\"\n ? `💳 Gelir: Komisyon (%${commissionRatePct})`\n : `💳 Gelir: Abonelik`,\n registrationNote ? `🌐 ${registrationNote}` : \"\",\n ].filter(Boolean).join(\"\\n\"),\n panelUrl: `https://turk.eco/admin/servis-saglayicilar`,\n }).catch(() => {});\n\n const welcomeWa = [\n `Yekpare — Merhaba ${ownerName},`,\n ``,\n `${businessName} başvurunuz alındı. Kaydınız başarıyla oluşturulmuştur.`,\n ``,\n `Hesabınızın aktifleşmesi için kimlik, vergi levhası ve imza sirküsü gibi işletme belgelerinizi eksikse tamamlayın; başvuru formunda yüklediyseniz yönetici onayını bekleyin.`,\n ``,\n `Teşekkürler — Yekpare`,\n ].join(\"\\n\");\n void trySendSiteOutboundWhatsApp(String(phone), welcomeWa).catch(() => {});\n\n res.json({ success: true, vendor: newVendor });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n res.status(500).json({ error: \"Kayıt oluşturulamadı\", detail: msg });\n }\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — Tüm başvuruları listele (durum filtresi ile)\n GET /api/admin/providers?status=pending|approved|rejected&type=...\n──────────────────────────────────────────────────────── */\nrouter.get(\"/admin/providers\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { status, type, q } = req.query as Record;\n const statusFilter = typeof status === \"string\" && status.trim() ? status.trim() : null;\n const typeFilter = typeof type === \"string\" && type.trim() ? type.trim() : null;\n\n const rows = r(await db.execute(\n sql`\n SELECT\n v.id, v.name, v.slug, v.vendor_type, v.provider_type, v.provider_subtype,\n v.application_status, v.rejection_reason,\n v.owner_name, v.owner_email, v.phone, v.email,\n v.address, v.city, v.district, v.lat, v.lng,\n v.description, v.image_url, v.cover_url,\n v.active, v.is_open, v.featured, v.rating, v.review_count,\n v.doc_kimlik, v.doc_vergi, v.doc_imza, v.verified_at,\n v.notes, v.created_at, v.updated_at,\n v.revenue_model, v.commission_rate_pct,\n v.payout_bank_holder, v.payout_bank_iban, v.payout_bank_branch,\n v.geliver_api_token, v.geliver_sender_zip, v.geliver_sender_mahalle, v.geliver_auto_ship_on_order,\n (SELECT COUNT(*)::int FROM delivery_orders WHERE vendor_id = v.id) as order_count,\n (SELECT COUNT(*)::int FROM vendor_menu_items WHERE vendor_id = v.id) as product_count\n FROM vendors v\n WHERE (${statusFilter} IS NULL OR v.application_status = ${statusFilter})\n AND (${typeFilter} IS NULL OR v.provider_type = ${typeFilter} OR v.vendor_type = ${typeFilter})\n ORDER BY v.created_at DESC\n `\n ));\n\n const withGeliver = rows.map((raw) => {\n const v = raw as Row;\n const tok = String(v.geliver_api_token ?? \"\").trim();\n const { geliver_api_token: _t, ...rest } = v;\n return {\n ...rest,\n geliver_api_token_masked: tok ? maskSecret(tok) : \"\",\n geliver_configured: Boolean(tok),\n } as Row;\n });\n\n let result = withGeliver;\n if (q) {\n const lq = q.toLowerCase();\n result = withGeliver.filter((v) => {\n const id = Number((v as Row).id);\n return (\n String(v.name || \"\").toLowerCase().includes(lq) ||\n String(v.slug || \"\").toLowerCase().includes(lq) ||\n String(v.owner_name || \"\").toLowerCase().includes(lq) ||\n String(v.owner_email || \"\").toLowerCase().includes(lq) ||\n String(v.email || \"\").toLowerCase().includes(lq) ||\n String(v.city || \"\").toLowerCase().includes(lq) ||\n String(id).includes(lq)\n );\n });\n }\n\n res.json(result);\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — Onayla\n PUT /api/admin/providers/:id/approve\n──────────────────────────────────────────────────────── */\nrouter.put(\"/admin/providers/:id/approve\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n\n const tempPassword = vendorApprovalTempPassword();\n const passwordHash = await bcrypt.hash(tempPassword, 10);\n\n const rows = r(await db.execute(sql`\n UPDATE vendors\n SET application_status = 'approved', active = true, is_open = true,\n verified_at = NOW(), updated_at = NOW(),\n password_hash = ${passwordHash}\n WHERE id = ${id}\n RETURNING id, name, slug, lat, lng, image_url, city, district, address, phone,\n vendor_type, provider_type, provider_subtype, owner_name, owner_email\n `));\n\n const v = rows[0];\n if (!v) { res.status(404).json({ error: \"Bulunamadı\" }); return; }\n\n /* Haritada premium pin olarak ekle / güncelle */\n if (v.lat && v.lng) {\n await db.execute(sql`\n INSERT INTO map_businesses (\n name, address, city, district, phone, lat, lng,\n is_premium, is_active, vendor_id,\n photo_url, category_id, created_at\n ) VALUES (\n ${v.name}, ${v.address || \"\"}, ${v.city || \"Kırşehir\"},\n ${v.district || null}, ${v.phone || null},\n ${v.lat}, ${v.lng},\n true, true, ${id},\n ${v.image_url || null}, 1, NOW()\n )\n ON CONFLICT (vendor_id) DO UPDATE SET\n name = EXCLUDED.name, is_premium = true, is_active = true,\n lat = EXCLUDED.lat, lng = EXCLUDED.lng, updated_at = NOW()\n `).catch(() => {});\n }\n\n const baseUrl = sitePublicOrigin();\n\n /* Aktivasyon e-postası gönder */\n let emailSent = false;\n if (v.owner_email) {\n const mail = buildActivationEmail({\n vendorName: v.name as string,\n ownerName: (v.owner_name as string) || \"İşletme Sahibi\",\n email: v.owner_email as string,\n password: tempPassword,\n loginUrl: `${baseUrl}/servis-saglayici-giris`,\n });\n const result = await sendEmail({ to: v.owner_email as string, subject: mail.subject, html: mail.html, text: mail.text });\n emailSent = result.sent;\n }\n\n /* Vendor'a WhatsApp onay bildirimi — fire and forget */\n const waResult = await notifyVendorWhatsApp({\n vendorId: id,\n eventType: \"new_request\",\n details: [\n `🎉 *${v.name}* işletmenizin başvurusu onaylandı!`,\n `📧 Giriş e-postanız: ${v.owner_email || \"kayıtlı e-posta\"}`,\n `🔑 Varsayılan şifreniz: ${tempPassword}`,\n `⚠️ Güvenliğiniz için şifrenizi değiştirin.`,\n ].join(\"\\n\"),\n panelUrl: `${baseUrl}/servis-saglayici-giris`,\n });\n\n res.json({ success: true, vendor: v, emailSent, whatsapp: waResult });\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — İşletme şifresini sıfırla\n POST /api/admin/providers/:id/set-password\n──────────────────────────────────────────────────────── */\nrouter.post(\"/admin/providers/:id/set-password\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenanceAny(req, res, [\"servis_saglayicilar\", \"turizm\"])) return;\n const id = Number(req.params.id);\n const { password } = req.body as { password?: string };\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n if (!password || password.length < 4) { res.status(400).json({ error: \"Şifre en az 4 karakter olmalı\" }); return; }\n\n try {\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS password_hash text`);\n const hash = await bcrypt.hash(password, 10);\n await db.execute(sql`UPDATE vendors SET password_hash = ${hash}, updated_at = NOW() WHERE id = ${id}`);\n res.json({ success: true });\n } catch (e) {\n console.error(\"[set-password]\", e);\n res.status(500).json({ success: false, error: \"Şifre kaydedilemedi\", detail: String(e) });\n }\n});\n\nrouter.post(\"/admin/providers/:id/panel-session\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenanceAny(req, res, [\"servis_saglayicilar\", \"turizm\"])) return;\n const id = Number(req.params.id);\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, name, slug, vendor_type, provider_type, provider_subtype, owner_email, email, active, is_open\n FROM vendors\n WHERE id = ${id}\n LIMIT 1\n `));\n const v = rows[0];\n if (!v) { res.status(404).json({ error: \"İşletme bulunamadı\" }); return; }\n const loginEmail = String(v.owner_email ?? \"\").trim() || String(v.email ?? \"\").trim();\n if (!loginEmail) { res.status(400).json({ error: \"Bu işletmede giriş e-postası yok\" }); return; }\n attachProviderPanelRoute(v);\n const token = signVendorSessionToken(Number(v.id), loginEmail);\n res.json({\n success: true,\n token,\n vendor: {\n ...v,\n login_email: loginEmail,\n },\n session: {\n id: Number(v.id),\n email: loginEmail,\n name: String(v.name ?? \"\"),\n token,\n panelKind: v.panel_kind,\n panelPath: v.panel_route,\n },\n });\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — Reddet\n PUT /api/admin/providers/:id/reject\n──────────────────────────────────────────────────────── */\nrouter.put(\"/admin/providers/:id/reject\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const { reason } = req.body;\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n\n const rows = r(await db.execute(sql`\n UPDATE vendors\n SET application_status = 'rejected', active = false,\n rejection_reason = ${reason || null}, updated_at = NOW()\n WHERE id = ${id}\n RETURNING id, name\n `));\n\n const v = rows[0];\n\n /* Vendor'a WhatsApp red bildirimi — fire and forget */\n const waResult = await notifyVendorWhatsApp({\n vendorId: id,\n eventType: \"new_request\",\n details: [\n `❌ *${v?.name ?? \"İşletme\"}* başvurunuz incelendi.`,\n reason ? `📋 Sebep: ${reason}` : \"\",\n `Eksiklerinizi tamamlayıp yeniden başvurabilirsiniz.`,\n ].filter(Boolean).join(\"\\n\"),\n panelUrl: `https://turk.eco/isletme-basvuru`,\n });\n\n res.json({ success: true, vendor: v, whatsapp: waResult });\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — Notlar güncelle\n PUT /api/admin/providers/:id/notes\n──────────────────────────────────────────────────────── */\nrouter.put(\"/admin/providers/:id/notes\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const { notes } = req.body;\n await db.execute(sql`UPDATE vendors SET notes = ${notes || null}, updated_at = NOW() WHERE id = ${id}`);\n res.json({ success: true });\n});\n\nrouter.patch(\"/admin/providers/:id/geliver-settings\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n await ensureGeliverIntegrationColumns();\n\n const body = req.body as Record;\n const geliverApiToken = body[\"geliverApiToken\"];\n const geliverSenderZip = body[\"geliverSenderZip\"];\n const geliverSenderMahalle = body[\"geliverSenderMahalle\"];\n const geliverAutoShipOnOrder = body[\"geliverAutoShipOnOrder\"];\n\n const curRows = r(await db.execute(sql`\n SELECT geliver_api_token, geliver_sender_zip, geliver_sender_address_id, geliver_auto_ship_on_order,\n geliver_sender_mahalle\n FROM vendors WHERE id = ${id} LIMIT 1\n `));\n const cur = curRows[0];\n if (!cur) { res.status(404).json({ error: \"Bulunamadı\" }); return; }\n\n let nextToken = cur.geliver_api_token != null ? String(cur.geliver_api_token) : null;\n if (geliverApiToken !== undefined) {\n const t = String(geliverApiToken ?? \"\").trim();\n nextToken = t.length ? t : null;\n }\n\n const prevZ = cur.geliver_sender_zip != null ? String(cur.geliver_sender_zip) : null;\n let nextZip = prevZ;\n if (geliverSenderZip !== undefined) {\n const z = String(geliverSenderZip ?? \"\").trim().replace(/\\D/g, \"\").slice(0, 5);\n nextZip = z.length ? z : null;\n }\n\n const prevM = normGeliverMahalle(cur.geliver_sender_mahalle);\n let nextM = prevM;\n if (geliverSenderMahalle !== undefined) {\n nextM = normGeliverMahalle(geliverSenderMahalle);\n }\n\n let nextSender = cur.geliver_sender_address_id != null ? String(cur.geliver_sender_address_id) : null;\n if (geliverApiToken !== undefined && !String(geliverApiToken ?? \"\").trim()) {\n nextSender = null;\n }\n\n const zipChanged = geliverSenderZip !== undefined && String(nextZip ?? \"\") !== String(prevZ ?? \"\");\n const mahalleChanged = geliverSenderMahalle !== undefined && String(nextM ?? \"\") !== String(prevM ?? \"\");\n if (zipChanged || mahalleChanged) nextSender = null;\n\n let nextAuto = Boolean(cur.geliver_auto_ship_on_order);\n if (geliverAutoShipOnOrder !== undefined) nextAuto = Boolean(geliverAutoShipOnOrder);\n\n await db.execute(sql`\n UPDATE vendors SET\n geliver_api_token = ${nextToken},\n geliver_sender_zip = ${nextZip},\n geliver_sender_address_id = ${nextSender},\n geliver_sender_mahalle = ${nextM},\n geliver_auto_ship_on_order = ${nextAuto},\n updated_at = NOW()\n WHERE id = ${id}\n `);\n\n res.json({\n success: true,\n geliver_api_token_masked: nextToken ? maskSecret(nextToken) : \"\",\n geliver_configured: Boolean(nextToken),\n geliver_sender_zip: nextZip,\n geliver_sender_mahalle: nextM,\n geliver_auto_ship_on_order: nextAuto,\n });\n});\n\nrouter.patch(\"/admin/providers/:id/revenue-model\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n const { revenueModel: rmRaw, commissionRatePct: crRaw } = req.body as Record;\n const model = normalizeRevenueModel(rmRaw ?? \"subscription\");\n let rate: string | null = null;\n if (model === \"commission\") {\n const r = parseFloat(String(crRaw ?? \"\").replace(\",\", \".\"));\n if (!Number.isFinite(r) || r <= 0 || r > 100) {\n res.status(400).json({ error: \"Komisyon oranı 1 ile 100 arasında olmalıdır\" });\n return;\n }\n rate = String(r);\n }\n await db.execute(sql`\n UPDATE vendors SET\n revenue_model = ${model},\n commission_rate_pct = ${rate},\n updated_at = NOW()\n WHERE id = ${id}\n `);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER AUTH — E-posta ile giriş\n POST /api/providers/forgot-password\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/forgot-password\", async (req, res): Promise => {\n const { email, whatsapp } = req.body as { email?: string; whatsapp?: string };\n const emailTrim = String(email ?? \"\").trim();\n const waTrim = String(whatsapp ?? \"\").trim();\n if (!emailTrim && !waTrim) {\n res.status(400).json({ error: \"E-posta veya WhatsApp numarası girin.\" });\n return;\n }\n\n let v: Row | undefined;\n if (emailTrim) {\n const rows = r(await db.execute(sql`\n SELECT id, name, owner_name, owner_email, email, whatsapp, phone, callmebot_key FROM vendors\n WHERE ${sqlVendorSessionEmailMatches(emailTrim)} AND application_status = 'approved'\n LIMIT 1\n `));\n v = rows[0];\n } else {\n const variants = phoneDigitsVariants(waTrim);\n if (variants.length === 0) {\n res.status(400).json({ error: \"Geçerli bir telefon numarası girin.\" });\n return;\n }\n const orParts = variants.flatMap((variant) => [\n sql`regexp_replace(COALESCE(v.whatsapp,''), '[^0-9]', '', 'g') = ${variant}`,\n sql`regexp_replace(COALESCE(v.phone,''), '[^0-9]', '', 'g') = ${variant}`,\n ]);\n const orClause = sql.join(orParts, sql` OR `);\n const rows = r(await db.execute(sql`\n SELECT id, name, owner_name, owner_email, email, whatsapp, phone, callmebot_key FROM vendors v\n WHERE v.application_status = 'approved' AND (${orClause})\n ORDER BY v.id DESC\n LIMIT 1\n `));\n v = rows[0];\n }\n\n /* Güvenlik: hesap yoksa da success döndür */\n if (!v) {\n res.json({ success: true });\n return;\n }\n\n const token = crypto.randomBytes(32).toString(\"hex\");\n const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString();\n await db.execute(sql`\n UPDATE vendors SET pw_reset_token = ${token}, pw_reset_expires_at = ${expiresAt}\n WHERE id = ${Number(v.id)}\n `);\n\n const baseUrl = sitePublicOrigin();\n const resetUrl = `${baseUrl}/sifre-yenile?token=${token}`;\n\n let emailSent = false;\n const mail = buildPasswordResetEmail({\n ownerName: (v.owner_name as string) || \"İşletme Sahibi\",\n vendorName: v.name as string,\n resetUrl,\n });\n const toAddr = String(v.owner_email || v.email || \"\").trim();\n if (toAddr) {\n const result = await sendEmail({ to: toAddr, subject: mail.subject, html: mail.html, text: mail.text });\n emailSent = result.sent;\n }\n\n const waMsg = `Yekpare — Şifre sıfırlama\\n\\nBağlantıyı açın (yaklaşık 30 dk):\\n${resetUrl}\\n\\nBu isteği siz yapmadıysanız bu mesajı yok sayın.`;\n const waKey = String(v.callmebot_key ?? \"\").trim();\n const destRaw = String(v.whatsapp ?? v.phone ?? \"\").trim();\n let whatsappSent = false;\n if (destRaw) {\n if (waKey) {\n const waPhone = digitsForWhatsAppLink(destRaw);\n if (waPhone) {\n const u = `https://api.callmebot.com/whatsapp.php?phone=${encodeURIComponent(waPhone)}&text=${encodeURIComponent(waMsg)}&apikey=${encodeURIComponent(waKey)}`;\n try {\n const wr = await fetch(u, { signal: AbortSignal.timeout(8000) });\n whatsappSent = wr.ok;\n } catch {\n whatsappSent = false;\n }\n }\n }\n if (!whatsappSent) {\n const r2 = await trySendSiteOutboundWhatsApp(destRaw, waMsg);\n whatsappSent = r2.sent;\n }\n }\n\n res.json({\n success: true,\n emailSent,\n whatsappSent,\n ...(process.env[\"NODE_ENV\"] !== \"production\" ? { _devLink: resetUrl } : {}),\n });\n});\n\n/* ────────────────────────────────────────────────────────\n POST /api/providers/reset-password\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/reset-password\", async (req, res): Promise => {\n const { token, password } = req.body as { token?: string; password?: string };\n if (!token || !password) { res.status(400).json({ error: \"Token ve şifre gerekli\" }); return; }\n if (password.length < 6) { res.status(400).json({ error: \"Şifre en az 6 karakter olmalı\" }); return; }\n\n const rows = r(await db.execute(sql`\n SELECT id, pw_reset_expires_at FROM vendors WHERE pw_reset_token = ${token} LIMIT 1\n `));\n const v = rows[0];\n if (!v) { res.status(400).json({ error: \"Geçersiz sıfırlama linki.\" }); return; }\n if (!v.pw_reset_expires_at || new Date(v.pw_reset_expires_at as string) < new Date()) {\n res.status(400).json({ error: \"Sıfırlama linkinin süresi dolmuş. Lütfen tekrar talep edin.\" }); return;\n }\n\n const hash = await bcrypt.hash(password, 10);\n await db.execute(sql`\n UPDATE vendors SET password_hash = ${hash}, pw_reset_token = NULL, pw_reset_expires_at = NULL\n WHERE id = ${Number(v.id)}\n `);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n POST /api/providers/login\n Body: { email, password }\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/login\", async (req, res): Promise => {\n const { email, password, captchaToken, captchaAnswer } = req.body as {\n email?: string;\n password?: string;\n captchaToken?: string;\n captchaAnswer?: string;\n };\n if (!verifyLoginMathCaptcha(String(captchaToken ?? \"\"), String(captchaAnswer ?? \"\"))) {\n res.status(400).json({ error: \"Güvenlik doğrulaması hatalı veya süresi doldu.\" });\n return;\n }\n const emailTrim = String(email ?? \"\").trim();\n if (!emailTrim) { res.status(400).json({ error: \"E-posta gerekli\" }); return; }\n if (!password) { res.status(400).json({ error: \"Şifre gerekli\" }); return; }\n\n /** Aynı e-postayla birden fazla `vendors` satırı varsa `created_at` yerine `updated_at` ile seç; admin şifre sıfırlaması güncellenen satırda kalır. */\n const rows = r(await db.execute(sql`\n SELECT id, name, slug, vendor_type, provider_type, provider_subtype,\n application_status, rejection_reason, owner_name, owner_email,\n email,\n phone, city, active, image_url, cover_url, verified_at, notes, password_hash,\n linked_map_business_id, membership_tier\n FROM vendors\n WHERE ${sqlVendorSessionEmailMatches(emailTrim)}\n ORDER BY updated_at DESC NULLS LAST, id DESC\n LIMIT 1\n `));\n\n const v = rows[0];\n if (!v) {\n res.status(404).json({ error: \"Bu e-posta ile kayıtlı işletme bulunamadı.\" });\n return;\n }\n\n const storedHash = v.password_hash as string | null;\n if (!storedHash) {\n res.status(403).json({ error: \"Bu hesap için şifre tanımlanmamış. Lütfen yönetici ile iletişime geçin.\" });\n return;\n }\n const passwordOk = await bcrypt.compare(String(password), storedHash);\n if (!passwordOk) {\n res.status(401).json({ error: \"Şifre hatalı.\" });\n return;\n }\n\n const loginEmail =\n String(v.owner_email ?? \"\").trim() ||\n String(v.email ?? \"\").trim() ||\n String(email).trim();\n delete v.password_hash;\n (v as Record).login_email = loginEmail;\n attachProviderPanelRoute(v);\n\n const token = signVendorSessionToken(Number(v.id), loginEmail);\n res.json({ success: true, token, vendor: v });\n});\n\nrouter.use(\"/providers\", requireVendorSession);\n\n/* ────────────────────────────────────────────────────────\n PROVIDER AUTH — Mevcut oturumu doğrula\n GET /api/providers/me\n Header: Authorization: Bearer \n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/me\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n\n const rows = r(await db.execute(sql`\n SELECT id, name, slug, vendor_type, provider_type, provider_subtype,\n application_status, rejection_reason, owner_name, owner_email,\n phone, email, address, city, district, lat, lng, description, about_html,\n image_url, cover_url, whatsapp, working_hours,\n active, is_open, featured, rating, review_count,\n doc_kimlik, doc_vergi, doc_imza, verified_at, notes,\n revenue_model, commission_rate_pct,\n payout_bank_holder, payout_bank_iban, payout_bank_branch,\n geliver_api_token, geliver_sender_address_id, geliver_sender_zip, geliver_sender_mahalle, geliver_auto_ship_on_order,\n geliver_organization_id,\n paytr_merchant_id, paytr_merchant_key, paytr_merchant_salt, paytr_test_mode,\n iyzico_api_key, iyzico_secret_key, iyzico_sandbox, preferred_tr_gateway,\n linked_map_business_id, membership_tier, about_html,\n theme_key, theme_config,\n created_at, updated_at\n FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `));\n\n const v = rows[0];\n if (!v) { res.status(401).json({ error: \"Geçersiz oturum\" }); return; }\n\n const iyzicoConfigured = Boolean(String(v.iyzico_api_key ?? \"\").trim());\n const paytrMerchantIdRaw = String(v.paytr_merchant_id ?? \"\").trim();\n const paytrConfigured = Boolean(\n paytrMerchantIdRaw && String(v.paytr_merchant_key ?? \"\").trim() && String(v.paytr_merchant_salt ?? \"\").trim(),\n );\n delete v.paytr_merchant_id;\n delete v.paytr_merchant_key;\n delete v.paytr_merchant_salt;\n delete v.iyzico_api_key;\n delete v.iyzico_secret_key;\n (v as Record).paytr_merchant_id_masked = paytrMerchantIdRaw ? `****${paytrMerchantIdRaw.slice(-4)}` : \"\";\n (v as Record).paytr_configured = paytrConfigured;\n (v as Record).iyzico_configured = iyzicoConfigured;\n\n const geliverTok = String(v.geliver_api_token ?? \"\").trim();\n delete v.geliver_api_token;\n (v as Record).geliver_api_token_masked = geliverTok ? maskSecret(geliverTok) : \"\";\n\n await ensureVendorThemeColumns();\n /* Sipariş sayısı ve ürün sayısı */\n const stats = r(await db.execute(sql`\n SELECT\n (SELECT COUNT(*)::int FROM delivery_orders WHERE vendor_id = ${Number(vendorId)}) as order_count,\n (SELECT COUNT(*)::int FROM vendor_menu_items WHERE vendor_id = ${Number(vendorId)}) as product_count,\n (SELECT COUNT(*)::int FROM delivery_orders WHERE vendor_id = ${Number(vendorId)} AND status = 'pending') as pending_orders\n `));\n\n const [siteRow] = await db.select().from(siteSettingsTable).limit(1);\n (v as Record).membership_pricing = membershipPricingFromSiteRow(siteRow);\n\n await ensureVendorCustomDomainsTable();\n (v as Record).custom_domains = await listVendorDomains(Number(vendorId));\n attachProviderPanelRoute(v);\n\n res.json({ success: true, vendor: { ...v, ...(stats[0] || {}) } });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Profil güncelle\n PUT /api/providers/profile\n──────────────────────────────────────────────────────── */\nrouter.put(\"/providers/profile\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n\n const {\n name,\n phone,\n address,\n city,\n district,\n description,\n aboutHtml,\n whatsapp,\n workingHours,\n callmebotKey,\n imageUrl,\n coverUrl,\n contactEmail,\n ownerEmail,\n } = req.body as Record;\n\n const hasImage = Object.prototype.hasOwnProperty.call(req.body, \"imageUrl\");\n const hasCover = Object.prototype.hasOwnProperty.call(req.body, \"coverUrl\");\n const hasContact = Object.prototype.hasOwnProperty.call(req.body, \"contactEmail\");\n const hasOwnerEm = Object.prototype.hasOwnProperty.call(req.body, \"ownerEmail\");\n const hasAboutHtml = Object.prototype.hasOwnProperty.call(req.body, \"aboutHtml\");\n const imageVal = hasImage\n ? (imageUrl == null ? null : String(imageUrl).trim() || null)\n : undefined;\n const coverVal = hasCover ? (coverUrl == null ? null : String(coverUrl).trim() || null) : undefined;\n const emailVal = hasContact ? (contactEmail == null ? null : String(contactEmail).trim() || null) : undefined;\n const ownerEmVal = hasOwnerEm ? (ownerEmail == null ? null : String(ownerEmail).trim() || null) : undefined;\n const aboutHtmlVal = hasAboutHtml\n ? aboutHtml == null\n ? null\n : String(aboutHtml).trim().slice(0, 60_000) || null\n : undefined;\n\n const rows = r(await db.execute(sql`\n UPDATE vendors\n SET\n name = COALESCE(${name || null}, name),\n phone = COALESCE(${phone || null}, phone),\n address = COALESCE(${address || null}, address),\n city = COALESCE(${city || null}, city),\n district = COALESCE(${district || null}, district),\n description = COALESCE(${description || null}, description),\n about_html = CASE WHEN ${hasAboutHtml}::boolean THEN ${hasAboutHtml ? aboutHtmlVal : null} ELSE about_html END,\n whatsapp = COALESCE(${whatsapp || null}, whatsapp),\n callmebot_key = COALESCE(${callmebotKey !== undefined ? (callmebotKey || null) : null}, callmebot_key),\n working_hours = COALESCE(${workingHours ? JSON.stringify(workingHours) : null}, working_hours),\n image_url = CASE WHEN ${hasImage}::boolean THEN ${hasImage ? imageVal : null} ELSE image_url END,\n cover_url = CASE WHEN ${hasCover}::boolean THEN ${hasCover ? coverVal : null} ELSE cover_url END,\n email = CASE WHEN ${hasContact}::boolean THEN ${hasContact ? emailVal : null} ELSE email END,\n owner_email = CASE WHEN ${hasOwnerEm}::boolean THEN ${hasOwnerEm ? ownerEmVal : null} ELSE owner_email END,\n updated_at = NOW()\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n RETURNING id, name, phone, address, city, district, description, about_html, whatsapp, callmebot_key, working_hours,\n image_url, cover_url, email, owner_email\n `));\n\n if (!rows[0]) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n res.json({ success: true, vendor: rows[0] });\n});\n\nrouter.put(\"/providers/payout-bank\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n\n const { payoutBankHolder, payoutBankIban, payoutBankBranch } = req.body as Record;\n const holder = String(payoutBankHolder ?? \"\").trim();\n const ibanRaw = sanitizeTrIban(String(payoutBankIban ?? \"\"));\n const branch = String(payoutBankBranch ?? \"\").trim() || null;\n\n if (!holder || !ibanRaw) {\n res.status(400).json({ error: \"Hesap sahibi adı ve IBAN zorunludur\" });\n return;\n }\n\n const check = r(await db.execute(sql`\n SELECT id, revenue_model FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `))[0];\n if (!check) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n if (String(check.revenue_model ?? \"subscription\") !== \"commission\") {\n res.status(400).json({ error: \"Banka bilgisi yalnızca komisyon modelindeki işletmeler için kullanılır\" });\n return;\n }\n\n await db.execute(sql`\n UPDATE vendors SET\n payout_bank_holder = ${holder},\n payout_bank_iban = ${ibanRaw},\n payout_bank_branch = ${branch},\n updated_at = NOW()\n WHERE id = ${Number(vendorId)}\n `);\n\n res.json({ success: true });\n});\n\nrouter.put(\"/providers/geliver-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n await ensureGeliverIntegrationColumns();\n\n const body = req.body as Record;\n const geliverApiToken = body[\"geliverApiToken\"];\n const geliverSenderZip = body[\"geliverSenderZip\"];\n const geliverSenderMahalle = body[\"geliverSenderMahalle\"];\n const geliverSenderAddressId = body[\"geliverSenderAddressId\"];\n const geliverOrganizationId = body[\"geliverOrganizationId\"];\n const geliverAutoShipOnOrder = body[\"geliverAutoShipOnOrder\"];\n\n const rows = r(await db.execute(sql`\n SELECT id, geliver_api_token, geliver_sender_zip, geliver_sender_address_id, geliver_auto_ship_on_order,\n geliver_sender_mahalle, geliver_organization_id\n FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `));\n const cur = rows[0];\n if (!cur) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n let nextToken = cur.geliver_api_token != null ? String(cur.geliver_api_token) : null;\n if (geliverApiToken !== undefined) {\n const t = String(geliverApiToken ?? \"\").trim();\n nextToken = t.length ? t : null;\n }\n\n const prevZ = cur.geliver_sender_zip != null ? String(cur.geliver_sender_zip) : null;\n let nextZip = prevZ;\n if (geliverSenderZip !== undefined) {\n const z = String(geliverSenderZip ?? \"\").trim().replace(/\\D/g, \"\").slice(0, 5);\n nextZip = z.length ? z : null;\n }\n\n const prevM = normGeliverMahalle(cur.geliver_sender_mahalle);\n let nextM = prevM;\n if (geliverSenderMahalle !== undefined) {\n nextM = normGeliverMahalle(geliverSenderMahalle);\n }\n\n let nextSender = cur.geliver_sender_address_id != null ? String(cur.geliver_sender_address_id) : null;\n if (geliverSenderAddressId !== undefined) {\n const s = String(geliverSenderAddressId ?? \"\").trim();\n nextSender = s.length ? s : null;\n }\n\n const zipChanged = geliverSenderZip !== undefined && String(nextZip ?? \"\") !== String(prevZ ?? \"\");\n const mahalleChanged = geliverSenderMahalle !== undefined && String(nextM ?? \"\") !== String(prevM ?? \"\");\n if (zipChanged || mahalleChanged) nextSender = null;\n\n const prevOrg = cur.geliver_organization_id != null ? String(cur.geliver_organization_id) : null;\n let nextOrg = prevOrg;\n if (geliverOrganizationId !== undefined) {\n const o = String(geliverOrganizationId ?? \"\").trim();\n nextOrg = o.length ? o : null;\n }\n\n if (geliverApiToken !== undefined && !String(geliverApiToken ?? \"\").trim()) {\n nextSender = null;\n nextOrg = null;\n }\n\n let nextAuto = Boolean(cur.geliver_auto_ship_on_order);\n if (geliverAutoShipOnOrder !== undefined) nextAuto = Boolean(geliverAutoShipOnOrder);\n\n await db.execute(sql`\n UPDATE vendors SET\n geliver_api_token = ${nextToken},\n geliver_sender_zip = ${nextZip},\n geliver_sender_address_id = ${nextSender},\n geliver_sender_mahalle = ${nextM},\n geliver_organization_id = ${nextOrg},\n geliver_auto_ship_on_order = ${nextAuto},\n updated_at = NOW()\n WHERE id = ${Number(vendorId)}\n `);\n\n res.json({\n success: true,\n geliverApiTokenMasked: nextToken ? maskSecret(nextToken) : \"\",\n geliverSenderZip: nextZip,\n geliverSenderMahalle: nextM,\n geliverSenderAddressId: nextSender,\n geliverOrganizationId: nextOrg,\n geliverAutoShipOnOrder: nextAuto,\n });\n});\n\n/**\n * POST /api/providers/geliver/manual-shipment\n * Siparişe bağlı olmayan manuel gönderi: yalnızca teklifler (etiket Geliver panelinde).\n */\nrouter.post(\"/providers/geliver/manual-shipment\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n await ensureGeliverIntegrationColumns();\n\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const body = req.body as Record;\n const rec = body.recipient as Record | undefined;\n if (!rec || typeof rec !== \"object\") {\n res.status(400).json({ error: \"Alıcı bilgisi (recipient) gerekli.\" });\n return;\n }\n const name = String(rec.name ?? \"\").trim();\n const phone = String(rec.phone ?? \"\").trim();\n const address1 = String(rec.address1 ?? \"\").trim();\n const city = String(rec.city ?? \"\").trim();\n const district = String(rec.district ?? \"\").trim();\n const neighborhood = String(rec.neighborhood ?? rec.mahalle ?? \"\").trim();\n if (!name || !phone || !address1 || !city) {\n res.status(400).json({ error: \"Alıcı: ad, telefon, adres (sokak/satır), şehir zorunlu.\" });\n return;\n }\n if (!district) {\n res.status(400).json({ error: \"İlçe seçilmeli.\" });\n return;\n }\n if (!neighborhood) {\n res.status(400).json({ error: \"Mahalle seçilmeli (il–ilçe–mahalle listesinden).\" });\n return;\n }\n\n const lengthCm = String(body.lengthCm ?? \"\").trim();\n const widthCm = String(body.widthCm ?? \"\").trim();\n const heightCm = String(body.heightCm ?? \"\").trim();\n const weightKg = String(body.weightKg ?? \"\").trim();\n if (!lengthCm || !widthCm || !heightCm || !weightKg) {\n res.status(400).json({ error: \"Paket: boy, en, yükseklik (cm) ve ağırlık (kg) zorunlu.\" });\n return;\n }\n\n const zipRaw = rec.zip != null ? String(rec.zip).trim().replace(/\\D/g, \"\").slice(0, 5) : \"\";\n const emailRaw = rec.email != null ? String(rec.email).trim() : \"\";\n const reference = body.reference != null ? String(body.reference).trim() : \"\";\n const senderAddressIdRaw = body.senderAddressId != null ? String(body.senderAddressId).trim() : \"\";\n\n const result = await createGeliverManualShipment({\n vendorId: Number(vendorId),\n senderAddressId: senderAddressIdRaw || undefined,\n recipient: {\n name,\n phone,\n address1,\n city,\n district: district || \"Merkez\",\n neighborhood,\n zip: zipRaw || undefined,\n email: emailRaw || undefined,\n },\n lengthCm,\n widthCm,\n heightCm,\n weightKg,\n reference: reference || undefined,\n });\n\n if (!result.ok) {\n res.status(400).json({ error: result.error });\n return;\n }\n res.json({\n success: true,\n purchaseMode: \"manual\",\n shipmentId: result.shipmentId,\n shipment: result.shipment ?? null,\n });\n});\n\n/** Geliver — şehir listesi (API token ile, bkz. https://docs.geliver.io/docs/home ) */\nrouter.get(\"/providers/geliver/cities\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const r0 = await geliverApiListCitiesForVendor(Number(vendorId));\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data });\n});\n\n/** Geliver — ilçe listesi */\nrouter.get(\"/providers/geliver/districts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const cityCode = typeof req.query.cityCode === \"string\" ? req.query.cityCode.trim() : \"\";\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!cityCode) {\n res.status(400).json({ error: \"cityCode zorunlu\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const r0 = await geliverApiListDistrictsForVendor(Number(vendorId), cityCode);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data });\n});\n\nrouter.get(\"/providers/geliver/shipments\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const page = Math.max(1, parseInt(String(req.query.page ?? \"1\"), 10) || 1);\n const limit = Math.min(50, Math.max(1, parseInt(String(req.query.limit ?? \"20\"), 10) || 20));\n const r0 = await geliverApiListShipmentsForVendor(Number(vendorId), { page, limit });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, ...((r0.envelope as Record) ?? {}) });\n});\n\nrouter.get(\"/providers/geliver/shipment/:shipmentId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const shipmentId = String(req.params.shipmentId ?? \"\").trim();\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!shipmentId) {\n res.status(400).json({ error: \"shipmentId gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const r0 = await geliverApiGetShipmentForVendor(Number(vendorId), shipmentId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, shipment: r0.shipment });\n});\n\nrouter.patch(\"/providers/geliver/shipment/:shipmentId/package\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const shipmentId = String(req.params.shipmentId ?? \"\").trim();\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!shipmentId) {\n res.status(400).json({ error: \"shipmentId gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const b = req.body as Record;\n const lengthCm = String(b.lengthCm ?? \"\").trim();\n const widthCm = String(b.widthCm ?? \"\").trim();\n const heightCm = String(b.heightCm ?? \"\").trim();\n const weightKg = String(b.weightKg ?? \"\").trim();\n if (!lengthCm || !widthCm || !heightCm || !weightKg) {\n res.status(400).json({ error: \"Boy, en, yükseklik (cm) ve ağırlık (kg) zorunlu.\" });\n return;\n }\n const r0 = await geliverApiUpdateShipmentPackageForVendor(Number(vendorId), shipmentId, {\n lengthCm,\n widthCm,\n heightCm,\n weightKg,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, shipment: r0.shipment });\n});\n\nrouter.post(\"/providers/geliver/shipment/:shipmentId/accept-offer\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const shipmentId = String(req.params.shipmentId ?? \"\").trim();\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!shipmentId) {\n res.status(400).json({ error: \"shipmentId gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const body = (req.body ?? {}) as Record;\n const offerId = String(body.offerId ?? body.offerID ?? body.offer_id ?? \"\").trim();\n if (!offerId) {\n res.status(400).json({ error: \"offerId (teklif kimliği) zorunlu\" });\n return;\n }\n try {\n logger.info({ vendorId, shipmentId, offerId }, \"[geliver] accept-offer route start\");\n // Hosting / proxy 30 sn’de kestiğinde kullanıcı “Application failed to respond” görüyordu;\n // 28 sn’lik kendi bütçemizle açık bir mesaj dönelim.\n const r0 = await Promise.race([\n geliverApiAcceptOfferForVendor(Number(vendorId), offerId, shipmentId),\n new Promise<{ ok: false; error: string }>((resolve) =>\n setTimeout(\n () =>\n resolve({\n ok: false,\n error:\n \"Geliver yanıtı zaman aşımına uğradı. Birkaç saniye sonra «API'den yenile» deyip tekrar deneyin; satın alma muhtemelen Geliver tarafında işlendi.\",\n }),\n 28_000,\n ),\n ),\n ]);\n if (!r0.ok) {\n logger.warn(\n { vendorId, shipmentId, offerId, error: r0.error, debug: \"debug\" in r0 ? r0.debug : undefined },\n \"[geliver] accept-offer failed\",\n );\n const manualUrl = \"manualPurchaseUrl\" in r0 ? r0.manualPurchaseUrl : undefined;\n const manualHint = \"manualPurchaseHint\" in r0 ? r0.manualPurchaseHint : undefined;\n res.status(400).json({\n error: r0.error,\n debug: \"debug\" in r0 ? r0.debug : undefined,\n ...(typeof manualUrl === \"string\" && manualUrl ? { manualPurchaseUrl: manualUrl } : {}),\n ...(typeof manualHint === \"string\" && manualHint ? { manualPurchaseHint: manualHint } : {}),\n });\n return;\n }\n const isPending = \"pending\" in r0 && r0.pending === true;\n logger.info(\n {\n vendorId,\n shipmentId,\n offerId,\n transactionId: r0.transactionId,\n hasTracking: Boolean(r0.trackingNumber),\n hasLabel: Boolean(r0.labelUrl),\n pending: isPending,\n statusCode: \"statusCode\" in r0 ? r0.statusCode ?? null : null,\n },\n \"[geliver] accept-offer success\",\n );\n // Sipariş satırı bu shipmentId ile bağlıysa Geliver alanlarını anında senkronla.\n // Pending’de label/tracking henüz boş olabilir; status 'offer_accepted' kalır, ileride yenileme/webhook doldurur.\n const tracking = r0.trackingNumber != null ? String(r0.trackingNumber).trim() : \"\";\n const label = r0.labelUrl != null ? String(r0.labelUrl).trim() : \"\";\n const txId = r0.transactionId != null ? String(r0.transactionId).trim() : \"\";\n const nextStatus = tracking || label ? \"label_created\" : \"offer_accepted\";\n await db\n .execute(sql`\n UPDATE delivery_orders\n SET geliver_tracking_number = ${tracking || null},\n geliver_label_url = ${label || null},\n geliver_transaction_id = ${txId || null},\n geliver_status = ${nextStatus},\n geliver_last_error = NULL,\n updated_at = NOW()\n WHERE vendor_id = ${Number(vendorId)}\n AND geliver_shipment_id = ${shipmentId}\n `)\n .catch((err) => logger.warn({ err, vendorId, shipmentId }, \"[geliver] accept-offer sync delivery_orders failed\"));\n res.json(\n geliverJsonSafe({\n success: true,\n pending: isPending,\n message: \"message\" in r0 ? r0.message ?? null : null,\n statusCode: \"statusCode\" in r0 ? r0.statusCode ?? null : null,\n transactionId: r0.transactionId,\n trackingNumber: r0.trackingNumber,\n labelUrl: r0.labelUrl,\n debug: \"debug\" in r0 ? r0.debug ?? null : null,\n }) as Record,\n );\n } catch (e: unknown) {\n logger.error({ err: e, vendorId, shipmentId }, \"[geliver] accept-offer route\");\n res.status(500).json({ error: e instanceof Error ? e.message : \"Etiket yanıtı işlenemedi\" });\n }\n});\n\nrouter.put(\"/providers/open-status\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n const isOpen = Boolean((req.body as Record)?.isOpen);\n try {\n await db.execute(sql`\n UPDATE vendors\n SET is_open = ${isOpen}, updated_at = NOW()\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n `);\n res.json({ success: true, isOpen });\n } catch (e) {\n res.status(500).json({ error: String(e) });\n }\n});\n\n/** Bakiye — [Geliver organizations balance](https://docs.geliver.io/docs/home); organizationId sorguda veya işletme kaydında */\nrouter.get(\"/providers/geliver/balance\", async (req, res): Promise => {\n try {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const vid = Number(vendorId);\n if (!Number.isFinite(vid) || vid <= 0) {\n res.status(400).json({ error: \"Geçersiz işletme oturumu.\" });\n return;\n }\n const qOrg = typeof req.query.organizationId === \"string\" ? req.query.organizationId.trim() : \"\";\n const qShipmentHint =\n typeof req.query.shipmentId === \"string\" ? req.query.shipmentId.trim() : \"\";\n const rows = r(\n await db.execute(sql`\n SELECT geliver_organization_id FROM vendors WHERE id = ${vid} LIMIT 1\n `),\n );\n const orgId = qOrg || String(rows[0]?.geliver_organization_id ?? \"\").trim();\n // Upstream proxy timeout’una takılmamak için kendi 14 sn’lik bütçemiz: süre dolarsa kullanıcıya açık mesaj.\n const r0 = await Promise.race([\n geliverApiGetBalanceForVendor(vid, orgId, qShipmentHint ? { hintShipmentId: qShipmentHint } : undefined),\n new Promise<{ ok: false; error: string }>((resolve) =>\n setTimeout(\n () =>\n resolve({\n ok: false,\n error:\n \"Geliver bakiye sorgusu zaman aşımına uğradı. Geliver tarafı yavaş yanıt verdi; birkaç saniye sonra tekrar deneyin.\",\n }),\n 14_000,\n ),\n ),\n ]);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n const balPayload: Record = { success: true, balance: r0.balance };\n if (\"resolvedOrganizationId\" in r0 && r0.resolvedOrganizationId) {\n balPayload.resolvedOrganizationId = r0.resolvedOrganizationId;\n }\n res.json(geliverJsonSafe(balPayload) as Record);\n } catch (e: unknown) {\n logger.error({ err: e }, \"[geliver] balance route\");\n res.status(500).json({ error: e instanceof Error ? e.message : \"Bakiye yanıtı işlenemedi\" });\n }\n});\n\nrouter.get(\"/providers/geliver/addresses\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const role = String(req.query.role ?? \"sender\").toLowerCase();\n const isRecipient = role === \"recipient\" || role === \"customer\" || role === \"alıcı\";\n const page = Math.max(1, parseInt(String(req.query.page ?? \"1\"), 10) || 1);\n const limit = Math.min(50, Math.max(1, parseInt(String(req.query.limit ?? \"30\"), 10) || 30));\n const r0 = await geliverApiListAddressesForVendor(Number(vendorId), {\n isRecipientAddress: isRecipient,\n page,\n limit,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({\n success: true,\n data: r0.data,\n totalRows: r0.totalRows,\n page: r0.page,\n limit: r0.limit,\n totalPages: r0.totalPages,\n });\n});\n\nrouter.get(\"/providers/geliver/address/:addressId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const addressId = String(req.params.addressId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!addressId) {\n res.status(400).json({ error: \"addressId gerekli\" });\n return;\n }\n const r0 = await geliverApiGetAddressForVendor(Number(vendorId), addressId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, address: r0.address });\n});\n\nrouter.post(\"/providers/geliver/sender-address\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const name = String(b.name ?? \"\").trim();\n const email = String(b.email ?? \"\").trim();\n const phone = String(b.phone ?? \"\").trim();\n const address1 = String(b.address1 ?? \"\").trim();\n const cityName = String(b.cityName ?? b.city ?? \"\").trim();\n const cityCode = String(b.cityCode ?? \"\").trim();\n const districtName = String(b.districtName ?? b.district ?? \"\").trim();\n const districtID = Number(b.districtID ?? b.districtId ?? NaN);\n const zip = String(b.zip ?? \"\").trim().replace(/\\D/g, \"\").slice(0, 5);\n const shortName = b.shortName != null ? String(b.shortName).trim() : \"\";\n const address2 = b.address2 != null ? String(b.address2).trim() : undefined;\n if (!name || !email || !phone || !address1 || !cityName || !cityCode || !districtName || !Number.isFinite(districtID)) {\n res.status(400).json({ error: \"Gönderici: ad, e-posta, telefon, address1, şehir, cityCode, ilçe adı, districtID (Geliver), posta kodu zorunlu.\" });\n return;\n }\n if (!zip) {\n res.status(400).json({ error: \"Posta kodu (zip) zorunludur.\" });\n return;\n }\n const r0 = await geliverApiCreateSenderAddressForVendor(Number(vendorId), {\n name,\n email,\n phone,\n address1,\n address2,\n countryCode: \"TR\",\n cityName,\n cityCode,\n districtName,\n districtID,\n zip,\n shortName: shortName || undefined,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, address: r0.address });\n});\n\nrouter.delete(\"/providers/geliver/address/:addressId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const addressId = String(req.params.addressId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!addressId) {\n res.status(400).json({ error: \"addressId gerekli\" });\n return;\n }\n const r0 = await geliverApiDeleteAddressForVendor(Number(vendorId), addressId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n await db.execute(sql`\n UPDATE vendors SET\n geliver_sender_address_id = CASE WHEN geliver_sender_address_id = ${addressId} THEN NULL ELSE geliver_sender_address_id END,\n updated_at = NOW()\n WHERE id = ${Number(vendorId)}\n `);\n res.json({ success: true });\n});\n\nrouter.get(\"/providers/geliver/webhooks\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const r0 = await geliverApiListWebhooksForVendor(Number(vendorId));\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data });\n});\n\nrouter.post(\"/providers/geliver/webhooks\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const url = String(b.url ?? \"\").trim();\n const type = b.type != null ? String(b.type).trim() : undefined;\n if (!url) {\n res.status(400).json({ error: \"url zorunlu\" });\n return;\n }\n const r0 = await geliverApiCreateWebhookForVendor(Number(vendorId), { url, type });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, raw: r0.raw });\n});\n\nrouter.delete(\"/providers/geliver/webhooks/:webhookId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const webhookId = String(req.params.webhookId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!webhookId) {\n res.status(400).json({ error: \"webhookId gerekli\" });\n return;\n }\n const r0 = await geliverApiDeleteWebhookForVendor(Number(vendorId), webhookId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true });\n});\n\nrouter.post(\"/providers/geliver/webhooks/test\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const url = String(b.url ?? \"\").trim();\n const type = String(b.type ?? \"\").trim();\n if (!url || !type) {\n res.status(400).json({ error: \"url ve type zorunlu\" });\n return;\n }\n const r0 = await geliverApiTestWebhookForVendor(Number(vendorId), { url, type });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, raw: r0.raw });\n});\n\n/**\n * Geliver webhook alıcı endpoint'i.\n * Geliver burayı `shipment_status_changed`, `TRACK_UPDATED` vb. olaylar için POST'lar.\n * Hangi shipment veya tracking değişti onu bulup `delivery_orders`'i güncelliyoruz.\n *\n * Ham gövde + imza doğrulama; `app.ts` içinde `express.json` öncesinde kayıtlıdır.\n */\nfunction geliverWebhookHeader(headers: Record, name: string): string {\n const direct = headers[name];\n if (typeof direct === \"string\" && direct.trim()) return direct.trim();\n const lower = headers[name.toLowerCase()];\n if (typeof lower === \"string\" && lower.trim()) return lower.trim();\n return \"\";\n}\n\nfunction geliverWebhookSecretFromAuth(authHeader: string): string {\n const auth = authHeader.trim();\n if (auth.toLowerCase().startsWith(\"bearer \")) return auth.slice(7).trim();\n return auth;\n}\n\nfunction geliverWebhookNormalizeSignature(sig: string): string {\n const s = sig.trim();\n const m = /^sha256=(.+)$/i.exec(s);\n return (m?.[1] ?? s).trim();\n}\n\nfunction geliverWebhookSafeEqual(a: string, b: string): boolean {\n const ba = Buffer.from(a, \"utf8\");\n const bb = Buffer.from(b, \"utf8\");\n if (ba.length !== bb.length) return false;\n return crypto.timingSafeEqual(ba, bb);\n}\n\n/** Geliver SDK: HMAC-SHA256(secret, `${timestamp}.${rawBody}`) ↔ X-Signature; yedek: paylaşımlı secret başlığı. */\nfunction verifyGeliverWebhook(req: Request, rawBody: Buffer): boolean {\n const secret = getGeliverWebhookSecret();\n if (!secret) {\n logger.warn(\"[geliver:webhook] GELIVER_WEBHOOK_SECRET yapılandırılmamış — istek reddedildi\");\n return false;\n }\n\n const headers = req.headers as Record;\n const signature = geliverWebhookHeader(headers, \"x-signature\");\n const timestamp = geliverWebhookHeader(headers, \"x-timestamp\");\n\n if (signature && timestamp) {\n const tsNum = Number(timestamp);\n if (!Number.isFinite(tsNum)) return false;\n const tsMs = tsNum > 1e12 ? tsNum : tsNum * 1000;\n if (Math.abs(Date.now() - tsMs) > 5 * 60 * 1000) return false;\n\n const payload = `${timestamp}.${rawBody.toString(\"utf8\")}`;\n const expectedHex = crypto.createHmac(\"sha256\", secret).update(payload).digest(\"hex\");\n const expectedB64 = crypto.createHmac(\"sha256\", secret).update(payload).digest(\"base64\");\n const normalized = geliverWebhookNormalizeSignature(signature);\n if (geliverWebhookSafeEqual(normalized, expectedHex)) return true;\n if (geliverWebhookSafeEqual(normalized, expectedB64)) return true;\n return false;\n }\n\n const providedSecret =\n geliverWebhookHeader(headers, \"x-geliver-webhook-secret\")\n || geliverWebhookHeader(headers, \"x-webhook-secret\")\n || geliverWebhookSecretFromAuth(geliverWebhookHeader(headers, \"authorization\"));\n if (providedSecret && geliverWebhookSafeEqual(providedSecret, secret)) return true;\n\n return false;\n}\n\nasync function ingestGeliverWebhookEvent(\n body: Record,\n headers: Record,\n): Promise {\n // Olay tipini ve shipmentId'yi gevşek biçimde topla.\n const flat = (() => {\n try { return JSON.stringify(body).slice(0, 4000); } catch { return \"\"; }\n })();\n const pickStr = (...vals: unknown[]): string => {\n for (const v of vals) {\n if (typeof v === \"string\" && v.trim()) return v.trim();\n }\n return \"\";\n };\n const data = (body.data && typeof body.data === \"object\" ? (body.data as Record) : null);\n const shipmentId = pickStr(\n body.shipmentId, body.shipmentID, body.shipment_id,\n data?.shipmentId, data?.shipmentID, data?.shipment_id,\n (body.shipment as { id?: string } | undefined)?.id,\n );\n const trackingNumber = pickStr(\n body.trackingNumber, body.tracking_number,\n data?.trackingNumber, data?.tracking_number,\n );\n const labelUrl = pickStr(\n body.labelUrl, body.label_url,\n data?.labelUrl, data?.label_url,\n );\n const statusCode = pickStr(\n body.statusCode, body.status,\n data?.statusCode, data?.status,\n );\n const eventType = pickStr(body.type, body.event, body.eventType);\n logger.info(\n {\n eventType,\n shipmentId,\n trackingNumber: trackingNumber ? \"[set]\" : \"\",\n labelUrl: labelUrl ? \"[set]\" : \"\",\n statusCode,\n contentType: headers[\"content-type\"],\n bodyPreview: flat.slice(0, 500),\n },\n \"[geliver:webhook] inbound\",\n );\n if (!shipmentId) return;\n\n try {\n if (trackingNumber || labelUrl) {\n const patch: Partial = {\n geliverStatus: \"purchased\",\n updatedAt: new Date(),\n };\n if (trackingNumber) patch.geliverTrackingNumber = trackingNumber;\n if (labelUrl) patch.geliverLabelUrl = labelUrl;\n await db\n .update(deliveryOrdersTable)\n .set(patch)\n .where(eq(deliveryOrdersTable.geliverShipmentId, shipmentId));\n return;\n }\n\n if (/shipped|TRACK_UPDATED|in[_ ]?transit|delivered/i.test(`${eventType} ${statusCode}`)) {\n await db\n .update(deliveryOrdersTable)\n .set({ geliverStatus: \"shipped\", updatedAt: new Date() })\n .where(eq(deliveryOrdersTable.geliverShipmentId, shipmentId));\n }\n } catch (err) {\n logger.warn({ err, shipmentId }, \"[geliver:webhook] db update failed\");\n }\n}\n\nexport async function handleGeliverWebhook(req: Request, res: Response): Promise {\n const buf = req.body;\n if (!Buffer.isBuffer(buf)) {\n res.status(400).json({ error: \"Raw body gerekli\" });\n return;\n }\n\n if (!verifyGeliverWebhook(req, buf)) {\n logger.warn(\"[geliver:webhook] imza/secret doğrulanamadı — istek reddedildi\");\n res.status(401).json({ error: \"Webhook doğrulanamadı\" });\n return;\n }\n\n let body: Record;\n try {\n body = JSON.parse(buf.toString(\"utf8\")) as Record;\n } catch {\n res.status(400).json({ error: \"Geçersiz JSON\" });\n return;\n }\n\n res.status(200).json({ ok: true });\n try {\n await ingestGeliverWebhookEvent(body, req.headers as Record);\n } catch (err) {\n logger.warn({ err }, \"[geliver:webhook] handler failed\");\n }\n}\n\n// Geliver bazen test panelinde `GET` ile sağlık kontrolü yapabilir - 200 OK dönelim.\nrouter.get(\"/providers/geliver/webhook\", (_req, res) => {\n res.status(200).json({ ok: true, service: \"geliver-webhook\", ts: new Date().toISOString() });\n});\n\nrouter.get(\"/providers/geliver/parcel-templates\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const r0 = await geliverApiListParcelTemplatesForVendor(Number(vendorId));\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data });\n});\n\nrouter.post(\"/providers/geliver/parcel-templates\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const r0 = await geliverApiCreateParcelTemplateForVendor(Number(vendorId), b);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, template: r0.template });\n});\n\nrouter.delete(\"/providers/geliver/parcel-templates/:templateId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const templateId = String(req.params.templateId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!templateId) {\n res.status(400).json({ error: \"templateId gerekli\" });\n return;\n }\n const r0 = await geliverApiDeleteParcelTemplateForVendor(Number(vendorId), templateId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true });\n});\n\nrouter.get(\"/providers/geliver/prices\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const length = String(req.query.length ?? \"\").trim();\n const width = String(req.query.width ?? \"\").trim();\n const height = String(req.query.height ?? \"\").trim();\n const weight = String(req.query.weight ?? \"\").trim();\n if (!length || !width || !height || !weight) {\n res.status(400).json({ error: \"length, width, height, weight sorgu parametreleri zorunlu.\" });\n return;\n }\n const r0 = await geliverApiListPricesForVendor(Number(vendorId), {\n length,\n width,\n height,\n weight,\n distanceUnit: typeof req.query.distanceUnit === \"string\" ? req.query.distanceUnit : undefined,\n massUnit: typeof req.query.massUnit === \"string\" ? req.query.massUnit : undefined,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, raw: r0.raw });\n});\n\nrouter.get(\"/providers/geliver/provider-accounts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const r0 = await geliverApiListProviderAccountsForVendor(Number(vendorId));\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data, envelope: r0.envelope ?? null });\n});\n\nrouter.post(\"/providers/geliver/provider-accounts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const username = String(b.username ?? \"\").trim();\n const name = String(b.name ?? \"\").trim();\n const providerCode = String(b.providerCode ?? \"\").trim();\n const version = parseInt(String(b.version ?? \"1\"), 10) || 1;\n if (!username || !name || !providerCode) {\n res.status(400).json({ error: \"username, name, providerCode zorunlu\" });\n return;\n }\n const password = b.password != null ? String(b.password) : undefined;\n const r0 = await geliverApiCreateProviderAccountForVendor(Number(vendorId), {\n username,\n password,\n name,\n providerCode,\n version,\n isActive: b.isActive !== false,\n isPublic: Boolean(b.isPublic),\n sharable: Boolean(b.sharable),\n isDynamicPrice: Boolean(b.isDynamicPrice),\n parameters:\n b.parameters && typeof b.parameters === \"object\"\n ? (b.parameters as Record)\n : undefined,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, account: r0.account });\n});\n\nrouter.delete(\"/providers/geliver/provider-accounts/:accountId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const accountId = String(req.params.accountId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!accountId) {\n res.status(400).json({ error: \"accountId gerekli\" });\n return;\n }\n const isDel =\n req.query.isDeleteAccountConnection === \"1\" || String(req.query.isDeleteAccountConnection ?? \"\") === \"true\";\n const r0 = await geliverApiDeleteProviderAccountForVendor(Number(vendorId), accountId, {\n isDeleteAccountConnection: isDel,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true });\n});\n\nrouter.put(\"/providers/tr-gateway-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS paytr_merchant_id TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS paytr_merchant_key TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS paytr_merchant_salt TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS paytr_test_mode BOOLEAN NOT NULL DEFAULT true`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS iyzico_api_key TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS iyzico_secret_key TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS iyzico_sandbox BOOLEAN NOT NULL DEFAULT true`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS preferred_tr_gateway TEXT`);\n\n const check = r(\n await db.execute(sql`\n SELECT id, revenue_model FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!check) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n if (normalizeRevenueModel(check.revenue_model) !== \"subscription\") {\n res.status(400).json({\n error: \"Bu bağlantı yalnızca abonelik gelir modelindeki işletmeler içindir.\",\n });\n return;\n }\n\n const body = req.body as Record;\n\n const [cur] = r(\n await db.execute(sql`\n SELECT paytr_merchant_id, paytr_merchant_key, paytr_merchant_salt, paytr_test_mode,\n iyzico_api_key, iyzico_secret_key, iyzico_sandbox, preferred_tr_gateway\n FROM vendors WHERE id = ${Number(vendorId)} LIMIT 1\n `),\n );\n if (!cur) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const pick = (k: string, prev: unknown): unknown =>\n k in body ? (body[k] === null || body[k] === \"\" ? null : body[k]) : prev;\n\n const paytrMerchantId = pick(\"paytrMerchantId\", cur.paytr_merchant_id);\n let paytrMerchantKey = pick(\"paytrMerchantKey\", cur.paytr_merchant_key);\n let paytrMerchantSalt = pick(\"paytrMerchantSalt\", cur.paytr_merchant_salt);\n let iyzicoApiKey = pick(\"iyzicoApiKey\", cur.iyzico_api_key);\n let iyzicoSecretKey = pick(\"iyzicoSecretKey\", cur.iyzico_secret_key);\n if (body[\"paytrMerchantKey\"] === \"***\") paytrMerchantKey = cur.paytr_merchant_key;\n if (body[\"paytrMerchantSalt\"] === \"***\") paytrMerchantSalt = cur.paytr_merchant_salt;\n if (body[\"iyzicoApiKey\"] === \"***\") iyzicoApiKey = cur.iyzico_api_key;\n if (body[\"iyzicoSecretKey\"] === \"***\") iyzicoSecretKey = cur.iyzico_secret_key;\n\n const paytrTestMode =\n body[\"paytrTestMode\"] !== undefined ? Boolean(body[\"paytrTestMode\"]) : Boolean(cur.paytr_test_mode !== false);\n const iyzicoSandbox =\n body[\"iyzicoSandbox\"] !== undefined ? Boolean(body[\"iyzicoSandbox\"]) : Boolean(cur.iyzico_sandbox !== false);\n\n let preferred: string | null =\n body[\"preferredTrGateway\"] !== undefined\n ? String(body[\"preferredTrGateway\"] ?? \"\").trim().toLowerCase() || null\n : (cur.preferred_tr_gateway as string | null);\n if (preferred && preferred !== \"paytr\" && preferred !== \"iyzico\") preferred = null;\n\n await db.execute(sql`\n UPDATE vendors SET\n paytr_merchant_id = ${paytrMerchantId != null ? String(paytrMerchantId).trim() || null : null},\n paytr_merchant_key = ${paytrMerchantKey != null ? String(paytrMerchantKey) : null},\n paytr_merchant_salt = ${paytrMerchantSalt != null ? String(paytrMerchantSalt) : null},\n paytr_test_mode = ${paytrTestMode},\n iyzico_api_key = ${iyzicoApiKey != null ? String(iyzicoApiKey).trim() || null : null},\n iyzico_secret_key = ${iyzicoSecretKey != null ? String(iyzicoSecretKey) : null},\n iyzico_sandbox = ${iyzicoSandbox},\n preferred_tr_gateway = ${preferred},\n updated_at = NOW()\n WHERE id = ${Number(vendorId)}\n `);\n\n const pid = String(paytrMerchantId ?? \"\").trim();\n res.json({\n success: true,\n paytrMerchantIdMasked: pid ? `****${pid.slice(-4)}` : \"\",\n paytrTestMode,\n iyzicoSandbox,\n preferredTrGateway: preferred,\n });\n});\n\nrouter.post(\"/providers/subscription-renewal\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n const { startDate, endDate, paymentMethod, receiptUrl } = req.body as {\n startDate?: string; endDate?: string; paymentMethod?: \"bank_transfer\" | \"stripe\"; receiptUrl?: string;\n };\n if (!startDate || !endDate) { res.status(400).json({ error: \"Başlangıç ve bitiş tarihi zorunlu\" }); return; }\n if ((paymentMethod || \"bank_transfer\") === \"bank_transfer\" && !String(receiptUrl || \"\").trim()) {\n res.status(400).json({ error: \"Havale için dekont zorunlu\" }); return;\n }\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `));\n if (!own[0]) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_subscription_requests (\n vendor_id, start_date, end_date, payment_method, receipt_url, status\n ) VALUES (\n ${Number(vendorId)}, ${startDate}, ${endDate}, ${(paymentMethod || \"bank_transfer\")}, ${String(receiptUrl || \"\").trim() || null}, 'pending'\n )\n RETURNING id\n `));\n if (!rows[0]) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n res.json({ success: true, requestId: rows[0].id });\n});\n\nrouter.get(\"/providers/integrations\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, notes FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `));\n const row = rows[0];\n if (!row) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const notes = String(row.notes || \"\");\n const openai = noteField(notes, \"openai_api_key\");\n const gemini = noteField(notes, \"gemini_api_key\");\n const googleAi = noteField(notes, \"google_ai_api_key\");\n const deepseek = noteField(notes, \"deepseek_api_key\");\n const model = noteField(notes, \"openai_model\") || \"gpt-4o-mini\";\n const waEnabled = noteField(notes, \"whatsapp_enabled\") !== \"0\";\n const waNewOrder = noteField(notes, \"whatsapp_feature_new_order\") !== \"0\";\n const waCustomer = noteField(notes, \"whatsapp_feature_customer_contact\") !== \"0\";\n const waStatus = noteField(notes, \"whatsapp_feature_order_status\") === \"1\";\n const waBulk = noteField(notes, \"whatsapp_feature_bulk_marketing\") === \"1\";\n res.json({\n success: true,\n integrations: {\n openaiApiKeyMasked: openai ? maskSecret(openai) : \"\",\n geminiApiKeyMasked: gemini ? maskSecret(gemini) : \"\",\n googleAiApiKeyMasked: googleAi ? maskSecret(googleAi) : \"\",\n deepseekApiKeyMasked: deepseek ? maskSecret(deepseek) : \"\",\n openaiModel: model,\n whatsappEnabled: waEnabled,\n whatsappFeatureNewOrder: waNewOrder,\n whatsappFeatureCustomerContact: waCustomer,\n whatsappFeatureOrderStatus: waStatus,\n whatsappFeatureBulkMarketing: waBulk,\n },\n });\n});\n\nrouter.patch(\"/providers/integrations\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const b = req.body as {\n openaiApiKey?: string;\n openaiModel?: string;\n geminiApiKey?: string;\n googleAiApiKey?: string;\n deepseekApiKey?: string;\n whatsappEnabled?: boolean;\n whatsappFeatureNewOrder?: boolean;\n whatsappFeatureCustomerContact?: boolean;\n whatsappFeatureOrderStatus?: boolean;\n whatsappFeatureBulkMarketing?: boolean;\n };\n const rows = r(await db.execute(sql`\n SELECT id, notes FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `));\n const row = rows[0];\n if (!row) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n let notes = String(row.notes || \"\");\n if (b.openaiApiKey !== undefined) notes = upsertNoteField(notes, \"openai_api_key\", b.openaiApiKey);\n if (b.geminiApiKey !== undefined) notes = upsertNoteField(notes, \"gemini_api_key\", b.geminiApiKey);\n if (b.googleAiApiKey !== undefined) notes = upsertNoteField(notes, \"google_ai_api_key\", b.googleAiApiKey);\n if (b.deepseekApiKey !== undefined) notes = upsertNoteField(notes, \"deepseek_api_key\", b.deepseekApiKey);\n if (b.openaiModel !== undefined) notes = upsertNoteField(notes, \"openai_model\", b.openaiModel);\n if (b.whatsappEnabled !== undefined) notes = upsertNoteField(notes, \"whatsapp_enabled\", b.whatsappEnabled ? \"1\" : \"0\");\n if (b.whatsappFeatureNewOrder !== undefined) notes = upsertNoteField(notes, \"whatsapp_feature_new_order\", b.whatsappFeatureNewOrder ? \"1\" : \"0\");\n if (b.whatsappFeatureCustomerContact !== undefined) notes = upsertNoteField(notes, \"whatsapp_feature_customer_contact\", b.whatsappFeatureCustomerContact ? \"1\" : \"0\");\n if (b.whatsappFeatureOrderStatus !== undefined) notes = upsertNoteField(notes, \"whatsapp_feature_order_status\", b.whatsappFeatureOrderStatus ? \"1\" : \"0\");\n if (b.whatsappFeatureBulkMarketing !== undefined) notes = upsertNoteField(notes, \"whatsapp_feature_bulk_marketing\", b.whatsappFeatureBulkMarketing ? \"1\" : \"0\");\n await db.execute(sql`UPDATE vendors SET notes = ${notes}, updated_at = NOW() WHERE id = ${Number(vendorId)}`);\n res.json({ success: true });\n});\n\n/** Panel: sipariş WhatsApp bildirimini dene (CallMeBot + numara kontrolü). */\nrouter.post(\"/providers/me/whatsapp-order-test\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const waResult = await notifyVendorWhatsApp({\n vendorId: Number(vendorId),\n eventType: \"new_order\",\n details: [\n \"Bu bir *test bildirimidir*.\",\n \"Gerçek siparişlerde de aynı kanal kullanılır.\",\n `Zaman: ${new Date().toLocaleString(\"tr-TR\", { timeZone: \"Europe/Istanbul\" })}`,\n ].join(\"\\n\"),\n });\n\n if (waResult.sent) {\n res.json({ success: true, sent: true, message: \"Test mesajı WhatsApp numaranıza gönderildi.\" });\n return;\n }\n\n res.status(400).json({\n success: false,\n sent: false,\n link: waResult.link ?? null,\n error:\n \"Otomatik mesaj gönderilemedi. İşletme WhatsApp numarasını kaydedin, «Siparişi WhatsApp'tan al» seçeneğini açın ve CallMeBot'u bu numarayla etkinleştirin (veya platform CallMeBot anahtarı tanımlı olsun).\",\n });\n});\n\n/* — Mağaza blogu (panel) — */\n\nrouter.get(\"/providers/me/blog-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id, slug, name FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT enabled FROM vendor_blog_settings WHERE vendor_id = ${Number(vendorId)} LIMIT 1\n `));\n res.json({\n success: true,\n enabled: Boolean(rows[0]?.enabled),\n vendorSlug: String(own.slug || \"\"),\n vendorName: String(own.name || \"\"),\n });\n});\n\nrouter.put(\"/providers/me/blog-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const enabled = Boolean((req.body as { enabled?: boolean })?.enabled);\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await db.execute(sql`\n INSERT INTO vendor_blog_settings (vendor_id, enabled, updated_at)\n VALUES (${Number(vendorId)}, ${enabled}, NOW())\n ON CONFLICT (vendor_id) DO UPDATE SET enabled = ${enabled}, updated_at = NOW()\n `);\n res.json({ success: true });\n});\n\nrouter.get(\"/providers/me/blog-posts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, slug, title, excerpt, cover_image_url, published, published_at, created_at, updated_at\n FROM vendor_blog_posts WHERE vendor_id = ${Number(vendorId)}\n ORDER BY updated_at DESC NULLS LAST, id DESC\n LIMIT 200\n `));\n res.json({ success: true, posts: rows });\n});\n\nrouter.post(\"/providers/me/blog-posts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const b = req.body as {\n title?: string;\n slug?: string;\n excerpt?: string;\n coverImageUrl?: string;\n contentJson?: unknown;\n published?: boolean;\n };\n const title = String(b.title ?? \"\").trim().slice(0, 300);\n let slug = String(b.slug ?? \"\").trim().toLowerCase().slice(0, 120);\n if (!title) { res.status(400).json({ error: \"Başlık zorunlu\" }); return; }\n if (!slug) slug = slugify(title);\n slug = slug.replace(/[^a-z0-9-]/g, \"-\").replace(/-+/g, \"-\").replace(/^-|-$/g, \"\") || `yazi-${Date.now()}`;\n const excerpt = String(b.excerpt ?? \"\").trim().slice(0, 2000) || null;\n const cover = String(b.coverImageUrl ?? \"\").trim().slice(0, 2000) || null;\n const published = b.published === true;\n const contentArr = Array.isArray(b.contentJson) ? b.contentJson : [];\n const contentJson = JSON.stringify(contentArr).slice(0, 500_000);\n const pubAt = published ? new Date().toISOString() : null;\n try {\n const ins = r(await db.execute(sql`\n INSERT INTO vendor_blog_posts (vendor_id, slug, title, excerpt, cover_image_url, content_json, published, published_at, updated_at)\n VALUES (\n ${Number(vendorId)}, ${slug}, ${title}, ${excerpt}, ${cover},\n ${contentJson}::jsonb, ${published}, ${pubAt}, NOW()\n )\n RETURNING id, slug\n `));\n res.status(201).json({ success: true, id: ins[0]?.id, slug: ins[0]?.slug });\n } catch {\n res.status(400).json({ error: \"Slug kullanımda veya kayıt başarısız.\" });\n }\n});\n\nrouter.patch(\"/providers/me/blog-posts/:id\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const id = parseInt(req.params.id, 10);\n if (!Number.isFinite(id)) { res.status(400).json({ error: \"Geçersiz id\" }); return; }\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const cur = r(await db.execute(sql`\n SELECT id, slug, published FROM vendor_blog_posts WHERE id = ${id} AND vendor_id = ${Number(vendorId)} LIMIT 1\n `))[0];\n if (!cur) { res.status(404).json({ error: \"Yazı yok\" }); return; }\n const b = req.body as {\n title?: string;\n slug?: string;\n excerpt?: string;\n coverImageUrl?: string;\n contentJson?: unknown;\n published?: boolean;\n };\n let slug = cur.slug ? String(cur.slug) : \"\";\n if (b.slug !== undefined) {\n slug = String(b.slug).trim().toLowerCase().slice(0, 120);\n slug = slug.replace(/[^a-z0-9-]/g, \"-\").replace(/-+/g, \"-\").replace(/^-|-$/g, \"\");\n }\n const title = b.title !== undefined ? String(b.title).trim().slice(0, 300) : undefined;\n const excerpt = b.excerpt !== undefined ? (String(b.excerpt).trim().slice(0, 2000) || null) : undefined;\n const cover = b.coverImageUrl !== undefined ? (String(b.coverImageUrl).trim().slice(0, 2000) || null) : undefined;\n const published = typeof b.published === \"boolean\" ? b.published : undefined;\n let contentSql = sql``;\n if (b.contentJson !== undefined) {\n const contentArr = Array.isArray(b.contentJson) ? b.contentJson : [];\n const contentJson = JSON.stringify(contentArr).slice(0, 500_000);\n contentSql = sql`, content_json = ${contentJson}::jsonb`;\n }\n await db.execute(sql`\n UPDATE vendor_blog_posts SET\n title = COALESCE(${title ?? null}, title),\n slug = COALESCE(${b.slug !== undefined ? slug : null}, slug),\n excerpt = COALESCE(${excerpt !== undefined ? excerpt : null}, excerpt),\n cover_image_url = COALESCE(${cover !== undefined ? cover : null}, cover_image_url),\n updated_at = NOW()\n ${contentSql}\n WHERE id = ${id} AND vendor_id = ${Number(vendorId)}\n `);\n if (published === true) {\n await db.execute(sql`\n UPDATE vendor_blog_posts SET published = true, published_at = COALESCE(published_at, NOW()), updated_at = NOW()\n WHERE id = ${id} AND vendor_id = ${Number(vendorId)}\n `);\n } else if (published === false) {\n await db.execute(sql`\n UPDATE vendor_blog_posts SET published = false, published_at = NULL, updated_at = NOW()\n WHERE id = ${id} AND vendor_id = ${Number(vendorId)}\n `);\n }\n res.json({ success: true });\n});\n\nrouter.delete(\"/providers/me/blog-posts/:id\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const id = parseInt(req.params.id, 10);\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await db.execute(sql`DELETE FROM vendor_blog_posts WHERE id = ${id} AND vendor_id = ${Number(vendorId)}`);\n res.json({ success: true });\n});\n\nrouter.post(\"/providers/ai/generate\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { purpose = \"about\", title = \"\", keywords = \"\" } = req.body as { purpose?: string; title?: string; keywords?: string };\n const rows = r(await db.execute(sql`\n SELECT id, name, city, district, address, vendor_type, notes\n FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `));\n const row = rows[0];\n if (!row) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const notes = String(row.notes || \"\");\n let apiKey = noteField(notes, \"openai_api_key\");\n let geminiKey = noteField(notes, \"gemini_api_key\");\n let deepseekKey = noteField(notes, \"deepseek_api_key\");\n const [siteAi] = await db\n .select({\n openaiApiKey: siteSettingsTable.openaiApiKey,\n openaiModel: siteSettingsTable.openaiModel,\n geminiApiKey: siteSettingsTable.geminiApiKey,\n deepseekApiKey: siteSettingsTable.deepseekApiKey,\n })\n .from(siteSettingsTable)\n .limit(1);\n if (!String(apiKey ?? \"\").trim()) apiKey = String(siteAi?.openaiApiKey ?? \"\").trim();\n const model =\n (noteField(notes, \"openai_model\") || String(siteAi?.openaiModel ?? \"\").trim() || \"gpt-4o-mini\").trim() || \"gpt-4o-mini\";\n if (!String(geminiKey ?? \"\").trim()) geminiKey = String(siteAi?.geminiApiKey ?? \"\").trim();\n if (!String(geminiKey ?? \"\").trim()) geminiKey = noteField(notes, \"google_ai_api_key\");\n if (!String(deepseekKey ?? \"\").trim()) deepseekKey = String(siteAi?.deepseekApiKey ?? \"\").trim();\n\n if (purpose === \"about\") {\n const gen = await generateTurkishVendorAboutDetailed(\n {\n openaiApiKey: apiKey || null,\n openaiModel: model,\n geminiApiKey: geminiKey || null,\n deepseekApiKey: deepseekKey || null,\n },\n {\n name: String(row.name || title || \"İşletme\"),\n city: row.city != null ? String(row.city) : null,\n district: row.district != null ? String(row.district) : null,\n address: row.address != null ? String(row.address) : null,\n vendorType: row.vendor_type != null ? String(row.vendor_type) : null,\n },\n );\n if (!gen.text) {\n res.status(400).json({\n error: gen.hasAnyKey\n ? `AI metni üretilemedi: ${gen.errors.join(\" | \").slice(0, 450)}`\n : \"AI metni üretilemedi: OpenAI, Gemini veya DeepSeek anahtarı tanımlı değil (işletme Entegrasyonlar veya site Genel Ayarlar).\",\n });\n return;\n }\n res.json({ success: true, text: gen.text, provider: gen.usedProvider });\n return;\n }\n\n const prompt = `Türkçe, kısa ve satış odaklı ürün açıklaması yaz. Ürün: ${title}. Anahtar kelimeler: ${keywords || \"-\"}.`;\n\n if (String(apiKey ?? \"\").trim()) {\n try {\n const rr = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: \"system\", content: \"Sen Türkçe içerik üreten yardımcı bir asistansın.\" },\n { role: \"user\", content: prompt },\n ],\n temperature: 0.6,\n }),\n });\n const d = (await rr.json().catch(() => ({}))) as Record;\n const text = String((d as { choices?: Array<{ message?: { content?: string } }> }).choices?.[0]?.message?.content || \"\").trim();\n if (rr.ok && text) {\n res.json({ success: true, text });\n return;\n }\n } catch {\n /* Gemini’ye düş */\n }\n }\n\n if (String(geminiKey ?? \"\").trim()) {\n try {\n const { callGeminiChat } = await import(\"../lib/aiChatProviders.js\");\n const g = await callGeminiChat(\n String(geminiKey),\n \"Sen Türkçe içerik üreten yardımcı bir asistansın.\",\n prompt,\n 0.55,\n );\n if (g.text) {\n res.json({ success: true, text: g.text });\n return;\n }\n } catch {\n /* DeepSeek’e düş */\n }\n }\n\n if (String(deepseekKey ?? \"\").trim()) {\n try {\n const rr = await fetch(\"https://api.deepseek.com/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${deepseekKey}` },\n body: JSON.stringify({\n model: \"deepseek-chat\",\n messages: [\n { role: \"system\", content: \"Sen Türkçe içerik üreten yardımcı bir asistansın.\" },\n { role: \"user\", content: prompt },\n ],\n temperature: 0.55,\n }),\n });\n const d = (await rr.json().catch(() => ({}))) as { choices?: Array<{ message?: { content?: string } }> };\n const text = String(d?.choices?.[0]?.message?.content || \"\").trim();\n if (rr.ok && text) {\n res.json({ success: true, text });\n return;\n }\n } catch {\n /* noop */\n }\n }\n\n res.status(400).json({\n error:\n \"OpenAI, Gemini veya DeepSeek API anahtarı yok veya üretim başarısız. Genel Ayarlar → Entegrasyonlar veya işletme paneli API anahtarlarını kontrol edin.\",\n });\n});\n\nrouter.get(\"/admin/providers/subscription-requests\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const status = String(req.query.status || \"pending\");\n const rows = r(await db.execute(sql`\n SELECT\n r.id, r.vendor_id, r.start_date, r.end_date, r.payment_method, r.receipt_url,\n r.status, r.admin_note, r.processed_at, r.created_at,\n v.name as vendor_name, v.slug as vendor_slug, v.owner_name, v.owner_email, v.city, v.district\n FROM vendor_subscription_requests r\n JOIN vendors v ON v.id = r.vendor_id\n WHERE ${status === \"all\" ? sql`1=1` : sql`r.status = ${status}`}\n ORDER BY r.created_at DESC\n LIMIT 300\n `));\n res.json(rows);\n});\n\nrouter.get(\"/admin/providers/subscription-expiring\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const days = Math.max(1, Math.min(90, Number(req.query.days || 30)));\n const rows = r(await db.execute(sql`\n SELECT\n v.id as vendor_id,\n v.name as vendor_name,\n v.slug as vendor_slug,\n v.owner_name,\n v.owner_email,\n v.city,\n v.district,\n MAX(r.end_date) as end_date,\n MAX(r.start_date) as start_date\n FROM vendors v\n JOIN vendor_subscription_requests r ON r.vendor_id = v.id AND r.status = 'approved'\n GROUP BY v.id, v.name, v.slug, v.owner_name, v.owner_email, v.city, v.district\n HAVING MAX(r.end_date) >= CURRENT_DATE\n AND MAX(r.end_date) <= (CURRENT_DATE + (${days} * INTERVAL '1 day'))\n ORDER BY MAX(r.end_date) ASC\n LIMIT 300\n `));\n res.json(rows);\n});\n\nrouter.patch(\"/admin/providers/subscription-requests/:id/approve\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const adminNote = String((req.body as any)?.adminNote || \"\").trim() || null;\n const reqRows = r(await db.execute(sql`\n SELECT id, vendor_id, start_date, end_date, payment_method, receipt_url\n FROM vendor_subscription_requests\n WHERE id = ${id} AND status = 'pending'\n LIMIT 1\n `));\n const row = reqRows[0];\n if (!row) { res.status(404).json({ error: \"Talep bulunamadı veya zaten işlenmiş\" }); return; }\n await db.execute(sql`\n UPDATE vendor_subscription_requests\n SET status = 'approved', admin_note = ${adminNote}, processed_at = NOW(), updated_at = NOW()\n WHERE id = ${id}\n `);\n await db.execute(sql`\n UPDATE vendors\n SET status = 'active',\n notes = CONCAT(\n COALESCE(notes, ''),\n CASE WHEN COALESCE(notes, '') = '' THEN '' ELSE ' | ' END,\n 'sub_start:', ${String(row.start_date)},\n ' | sub_end:', ${String(row.end_date)},\n ' | sub_payment:', ${String(row.payment_method)},\n ' | sub_receipt:', ${String(row.receipt_url || \"-\")},\n ' | sub_approved_at:', TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI')\n ),\n updated_at = NOW()\n WHERE id = ${Number(row.vendor_id)}\n `);\n res.json({ success: true });\n});\n\nrouter.patch(\"/admin/providers/subscription-requests/:id/reject\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const adminNote = String((req.body as any)?.adminNote || \"\").trim() || null;\n const row = r(await db.execute(sql`SELECT id FROM vendor_subscription_requests WHERE id = ${id} AND status = 'pending' LIMIT 1`))[0];\n if (!row) { res.status(404).json({ error: \"Talep bulunamadı veya zaten işlenmiş\" }); return; }\n await db.execute(sql`\n UPDATE vendor_subscription_requests\n SET status = 'rejected', admin_note = ${adminNote}, processed_at = NOW(), updated_at = NOW()\n WHERE id = ${id}\n `);\n res.json({ success: true });\n});\n\nrouter.patch(\"/admin/providers/subscription-requests/:id/note\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const adminNote = String((req.body as any)?.adminNote || \"\").trim() || null;\n const row = r(await db.execute(sql`SELECT id FROM vendor_subscription_requests WHERE id = ${id} LIMIT 1`))[0];\n if (!row) { res.status(404).json({ error: \"Talep bulunamadı\" }); return; }\n await db.execute(sql`\n UPDATE vendor_subscription_requests\n SET admin_note = ${adminNote}, updated_at = NOW()\n WHERE id = ${id}\n `);\n res.json({ success: true });\n});\n\n/** Admin: onaylı abonelik kaydı oluşturur (yıl doldu / manuel uzatma). Bitiş, başlangıç + ay sayısı ile hesaplanır. */\nrouter.post(\"/admin/providers/:id/subscription-manual-extend\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const vendorId = Number(req.params.id);\n if (!Number.isFinite(vendorId) || vendorId <= 0) {\n res.status(400).json({ error: \"Geçersiz işletme id\" });\n return;\n }\n const months = Math.max(1, Math.min(60, Math.floor(Number((req.body as { months?: unknown })?.months ?? 12))));\n const adminNote = String((req.body as { adminNote?: unknown })?.adminNote ?? \"\").trim() || null;\n\n const exists = r(await db.execute(sql`SELECT id FROM vendors WHERE id = ${vendorId} LIMIT 1`))[0];\n if (!exists) {\n res.status(404).json({ error: \"İşletme bulunamadı\" });\n return;\n }\n\n const maxRow = r(\n await db.execute(sql`\n SELECT MAX(end_date)::text as max_end\n FROM vendor_subscription_requests\n WHERE vendor_id = ${vendorId} AND status = 'approved'\n `),\n )[0];\n const maxEndStr = maxRow?.max_end != null ? String(maxRow.max_end) : \"\";\n\n const today = new Date();\n today.setUTCHours(0, 0, 0, 0);\n let start = new Date(today);\n if (maxEndStr) {\n const parts = maxEndStr.split(\"-\").map((x) => parseInt(x, 10));\n if (parts.length === 3 && parts.every((n) => Number.isFinite(n))) {\n const maxEnd = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));\n if (!Number.isNaN(maxEnd.getTime())) {\n const dayAfter = new Date(maxEnd);\n dayAfter.setUTCDate(dayAfter.getUTCDate() + 1);\n if (dayAfter.getTime() > start.getTime()) start = dayAfter;\n }\n }\n }\n\n const end = new Date(start);\n end.setUTCMonth(end.getUTCMonth() + months);\n end.setUTCDate(end.getUTCDate() - 1);\n\n const startStr = start.toISOString().slice(0, 10);\n const endStr = end.toISOString().slice(0, 10);\n const append = ` | admin_extend:${startStr}→${endStr} | at:${new Date().toISOString().slice(0, 16)}`;\n\n await db.execute(sql`\n INSERT INTO vendor_subscription_requests (\n vendor_id, start_date, end_date, payment_method, receipt_url, status, admin_note, processed_at, updated_at\n ) VALUES (\n ${vendorId},\n ${startStr}::date,\n ${endStr}::date,\n 'admin_manual',\n null,\n 'approved',\n ${adminNote},\n NOW(),\n NOW()\n )\n `);\n\n await db.execute(sql`\n UPDATE vendors\n SET notes = CONCAT(COALESCE(notes, ''), ${append}),\n updated_at = NOW()\n WHERE id = ${vendorId}\n `);\n\n res.json({ success: true, start_date: startStr, end_date: endStr, vendor_id: vendorId, months });\n});\n\n/* ────────────────────────────────────────────────────────\n PATCH /api/providers/service-settings (Masa & Rezervasyon)\n──────────────────────────────────────────────────────── */\nrouter.patch(\"/providers/service-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const body = req.body as Record;\n const { tableServiceEnabled, reservationEnabled, reservationAutoConfirm, tableSections, qrMenuPublic } = body;\n const vid = Number(vendorId);\n const vendorRows = r(\n await db.execute(sql`\n SELECT vendor_type, provider_type, provider_subtype, notes\n FROM vendors\n WHERE id = ${vid} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n );\n const vendorRow = vendorRows[0];\n if (!vendorRow) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const tableAllowed = canVendorUseTableService(vendorRow);\n const effectiveTableServiceEnabled = tableAllowed && Boolean(tableServiceEnabled);\n const effectiveQrMenuPublic = tableAllowed && Boolean(qrMenuPublic);\n let mergedNotes: string | undefined;\n if (typeof qrMenuPublic === \"boolean\") {\n const prevNotes = vendorRow.notes != null ? String(vendorRow.notes) : \"\";\n mergedNotes = mergeVendorNoteLine(prevNotes, \"qr_menu_public\", effectiveQrMenuPublic ? \"1\" : \"0\");\n }\n await db.execute(sql`\n UPDATE vendors SET\n table_service_enabled = ${effectiveTableServiceEnabled},\n reservation_enabled = ${Boolean(reservationEnabled)},\n reservation_auto_confirm = ${Boolean(reservationAutoConfirm)},\n table_sections = ${effectiveTableServiceEnabled && tableSections !== undefined ? (tableSections === null ? null : JSON.stringify(tableSections)) : null},\n updated_at = NOW()\n WHERE id = ${vid} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n `);\n if (mergedNotes !== undefined) {\n await db.execute(sql`\n UPDATE vendors SET notes = ${mergedNotes}, updated_at = NOW()\n WHERE id = ${vid} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n `);\n }\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n GET /api/providers/service-settings\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/service-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n if (!vendorId) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT vendor_type, provider_type, provider_subtype, table_service_enabled, reservation_enabled, reservation_auto_confirm, table_sections, notes\n FROM vendors WHERE id = ${Number(vendorId)} LIMIT 1\n `));\n const row: Row = rows[0] ?? {};\n const tableAllowed = canVendorUseTableService(row);\n res.json({\n tableServiceEnabled: tableAllowed && Boolean(row.table_service_enabled),\n reservationEnabled: Boolean(row.reservation_enabled),\n reservationAutoConfirm: Boolean(row.reservation_auto_confirm),\n tableSections: tableAllowed && row.table_sections ? JSON.parse(String(row.table_sections)) : [],\n qrMenuPublic: tableAllowed && vendorNoteFlagOn(row.notes != null ? String(row.notes) : \"\", \"qr_menu_public\"),\n });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürün & Kategori yardımcı: vendor sahipliği doğrula\n──────────────────────────────────────────────────────── */\nasync function verifyVendor(vendorId: string | string[] | undefined, vendorEmail: string | string[] | undefined): Promise {\n if (!vendorId || !vendorEmail) return null;\n const rows = r(await db.execute(sql`SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1`));\n return rows[0] ? Number(vendorId) : null;\n}\n\nasync function getVendorTypeForId(vid: number): Promise {\n const rows = r(\n await db.execute(sql`SELECT provider_type, vendor_type FROM vendors WHERE id = ${vid} LIMIT 1`),\n ) as Array<{ provider_type: string | null; vendor_type: string | null }>;\n const row = rows[0];\n return String(row?.provider_type || row?.vendor_type || \"\").toLowerCase();\n}\n\nasync function verifyVendorOwnsMenuItem(vid: number, itemId: number): Promise {\n const rows = r(await db.execute(sql`SELECT id FROM vendor_menu_items WHERE id = ${itemId} AND vendor_id = ${vid} LIMIT 1`));\n return Boolean(rows[0]);\n}\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürünleri + kategorileri listele\n GET /api/providers/products\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/products\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const [categories, items] = await Promise.all([\n db.execute(sql`\n SELECT id, name, position FROM vendor_menu_categories\n WHERE vendor_id = ${vid} AND active = true\n ORDER BY position, name\n `),\n db.execute(sql`\n SELECT mi.id, mi.name, mi.description, mi.price, mi.sale_price,\n mi.image_url, mi.active, mi.is_popular, mi.is_vegan, mi.is_spicy,\n mi.stock, mi.menu_category_id, mi.ecommerce_category_id,\n mc.name as category_name\n FROM vendor_menu_items mi\n LEFT JOIN vendor_menu_categories mc ON mc.id = mi.menu_category_id\n WHERE mi.vendor_id = ${vid} AND mi.active = true\n ORDER BY mc.position NULLS LAST, mi.name\n `),\n ]);\n\n res.json({ success: true, categories: r(categories), items: r(items) });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Yeni ürün ekle\n POST /api/providers/products\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/products\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const {\n name, price, salePrice, description, imageUrl, menuCategoryId, isPopular, isVegan, isSpicy, stock,\n ecommerceCategoryId, customCategoryName,\n } = req.body;\n if (!name || price === undefined) { res.status(400).json({ error: \"name ve price zorunlu\" }); return; }\n\n const vendorType = await getVendorTypeForId(vid);\n const resolved = await resolveProductCategoryForVendor({\n vendorId: vid,\n vendorType,\n ecommerceCategoryId: ecommerceCategoryId ? Number(ecommerceCategoryId) : null,\n customCategoryName: customCategoryName ? String(customCategoryName) : null,\n menuCategoryId: menuCategoryId ? Number(menuCategoryId) : null,\n });\n\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_menu_items (\n vendor_id, name, price, sale_price, description, image_url,\n menu_category_id, ecommerce_category_id, is_popular, is_vegan, is_spicy, stock, active\n )\n VALUES (\n ${vid}, ${name}, ${Number(price)}, ${salePrice ? Number(salePrice) : null},\n ${description || null}, ${imageUrl || null},\n ${resolved.menuCategoryId},\n ${resolved.ecommerceCategoryId},\n ${!!isPopular}, ${!!isVegan}, ${!!isSpicy},\n ${stock !== undefined ? Number(stock) : null}, true\n )\n RETURNING id, name, price, sale_price, description, image_url, menu_category_id, ecommerce_category_id, is_popular, active\n `));\n res.status(201).json({ success: true, item: rows[0] });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürün güncelle\n PUT /api/providers/products/:id\n──────────────────────────────────────────────────────── */\nrouter.put(\"/providers/products/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const itemId = Number(req.params.id);\n /* Ownership check */\n const own = r(await db.execute(sql`SELECT id FROM vendor_menu_items WHERE id = ${itemId} AND vendor_id = ${vid} LIMIT 1`));\n if (!own[0]) { res.status(403).json({ error: \"Bu ürün size ait değil\" }); return; }\n\n const { name, price, salePrice, description, imageUrl, menuCategoryId, isPopular, isVegan, isSpicy, stock, ecommerceCategoryId, customCategoryName } = req.body;\n\n const vendorType = await getVendorTypeForId(vid);\n let resolvedMenuCatId = menuCategoryId !== undefined ? (menuCategoryId ? Number(menuCategoryId) : null) : undefined;\n let resolvedEcomCatId: number | null | undefined = undefined;\n if (ecommerceCategoryId !== undefined || customCategoryName !== undefined) {\n const resolved = await resolveProductCategoryForVendor({\n vendorId: vid,\n vendorType,\n ecommerceCategoryId: ecommerceCategoryId ? Number(ecommerceCategoryId) : null,\n customCategoryName: customCategoryName ? String(customCategoryName) : null,\n menuCategoryId: menuCategoryId ? Number(menuCategoryId) : null,\n });\n resolvedMenuCatId = resolved.menuCategoryId;\n resolvedEcomCatId = resolved.ecommerceCategoryId;\n }\n\n const newPrice = price !== undefined ? Number(price) : null;\n const newSalePrice = salePrice !== undefined ? (salePrice ? Number(salePrice) : null) : undefined;\n const newDescription = description !== undefined ? (description || null) : null;\n const newImageUrl = imageUrl !== undefined ? (imageUrl || null) : null;\n const newCatId = resolvedMenuCatId !== undefined ? resolvedMenuCatId : (menuCategoryId !== undefined ? (menuCategoryId ? Number(menuCategoryId) : null) : null);\n const newEcomCatId = resolvedEcomCatId !== undefined ? resolvedEcomCatId : null;\n const newIsPopular = isPopular !== undefined ? !!isPopular : null;\n const newIsVegan = isVegan !== undefined ? !!isVegan : null;\n const newIsSpicy = isSpicy !== undefined ? !!isSpicy : null;\n const newStock = stock !== undefined ? Number(stock) : null;\n\n const rows = r(await db.execute(sql`\n UPDATE vendor_menu_items\n SET name = COALESCE(${name || null}, name),\n price = COALESCE(${newPrice}, price),\n sale_price = CASE WHEN ${newSalePrice !== undefined} THEN ${newSalePrice ?? null} ELSE sale_price END,\n description = COALESCE(${newDescription}, description),\n image_url = COALESCE(${newImageUrl}, image_url),\n menu_category_id = COALESCE(${newCatId}, menu_category_id),\n ecommerce_category_id = CASE WHEN ${resolvedEcomCatId !== undefined} THEN ${newEcomCatId ?? null} ELSE ecommerce_category_id END,\n is_popular = COALESCE(${newIsPopular}, is_popular),\n is_vegan = COALESCE(${newIsVegan}, is_vegan),\n is_spicy = COALESCE(${newIsSpicy}, is_spicy),\n stock = COALESCE(${newStock}, stock),\n updated_at = NOW()\n WHERE id = ${itemId} AND vendor_id = ${vid}\n RETURNING id, name, price, sale_price, description, image_url, menu_category_id, is_popular, active\n `));\n res.json({ success: true, item: rows[0] });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürün sil (soft)\n DELETE /api/providers/products/:id\n──────────────────────────────────────────────────────── */\nrouter.delete(\"/providers/products/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const itemId = Number(req.params.id);\n await db.execute(sql`UPDATE vendor_menu_items SET active = false WHERE id = ${itemId} AND vendor_id = ${vid}`);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Toplu ürün sil (soft)\n POST /api/providers/products/bulk-delete { ids: number[] }\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/products/bulk-delete\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const idsRaw = (req.body as { ids?: unknown }).ids;\n if (!Array.isArray(idsRaw) || idsRaw.length === 0) {\n res.status(400).json({ success: false, error: \"ids dizisi gerekli\" });\n return;\n }\n const ids = [...new Set(idsRaw.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0))];\n if (ids.length === 0) {\n res.status(400).json({ success: false, error: \"Geçerli ürün seçilmedi\" });\n return;\n }\n if (ids.length > 500) {\n res.status(400).json({ success: false, error: \"Tek seferde en fazla 500 ürün silinebilir\" });\n return;\n }\n\n const updated = await db\n .update(vendorMenuItemsTable)\n .set({ active: false, updatedAt: new Date() })\n .where(and(eq(vendorMenuItemsTable.vendorId, vid), inArray(vendorMenuItemsTable.id, ids)))\n .returning({ id: vendorMenuItemsTable.id });\n\n res.json({ success: true, deleted: updated.length });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Kategori listele\n GET /api/providers/categories\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/categories\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const rows = r(await db.execute(sql`SELECT id, name, position, is_custom, ecommerce_category_id FROM vendor_menu_categories WHERE vendor_id = ${vid} AND active = true ORDER BY position, name`));\n res.json({ success: true, categories: rows });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Global alışveriş kategori ağacı\n GET /api/providers/ecommerce-categories\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/ecommerce-categories\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const tree = await getEcommerceCategoryTree();\n res.json({ success: true, tree });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürün varyasyonları\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/item-options/:itemId\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const itemId = Number(req.params.itemId);\n if (!await verifyVendorOwnsMenuItem(vid, itemId)) { res.status(403).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`SELECT * FROM vendor_item_options WHERE menu_item_id = ${itemId} ORDER BY id`));\n res.json({ success: true, options: rows });\n});\n\nrouter.post(\"/providers/item-options\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { menuItemId, name, required, multiple, choices } = req.body ?? {};\n const itemId = Number(menuItemId);\n if (!itemId || !name || !Array.isArray(choices) || !choices.length) {\n res.status(400).json({ error: \"menuItemId, name ve choices zorunlu\" });\n return;\n }\n if (!await verifyVendorOwnsMenuItem(vid, itemId)) { res.status(403).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_item_options (menu_item_id, name, required, multiple, choices)\n VALUES (${itemId}, ${String(name)}, ${Boolean(required)}, ${Boolean(multiple)}, ${JSON.stringify(choices)}::jsonb)\n RETURNING *\n `));\n res.status(201).json({ success: true, option: rows[0] });\n});\n\nrouter.put(\"/providers/item-options/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const optId = Number(req.params.id);\n const { name, required, multiple, choices } = req.body ?? {};\n const own = r(await db.execute(sql`\n SELECT o.id FROM vendor_item_options o\n JOIN vendor_menu_items mi ON mi.id = o.menu_item_id\n WHERE o.id = ${optId} AND mi.vendor_id = ${vid}\n LIMIT 1\n `));\n if (!own[0]) { res.status(403).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n UPDATE vendor_item_options SET\n name = COALESCE(${name ?? null}, name),\n required = COALESCE(${required !== undefined ? Boolean(required) : null}, required),\n multiple = COALESCE(${multiple !== undefined ? Boolean(multiple) : null}, multiple),\n choices = COALESCE(${choices ? JSON.stringify(choices) : null}::jsonb, choices)\n WHERE id = ${optId}\n RETURNING *\n `));\n res.json({ success: true, option: rows[0] });\n});\n\nrouter.delete(\"/providers/item-options/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const optId = Number(req.params.id);\n const own = r(await db.execute(sql`\n SELECT o.id FROM vendor_item_options o\n JOIN vendor_menu_items mi ON mi.id = o.menu_item_id\n WHERE o.id = ${optId} AND mi.vendor_id = ${vid}\n LIMIT 1\n `));\n if (!own[0]) { res.status(403).json({ error: \"Yetkisiz\" }); return; }\n await db.execute(sql`DELETE FROM vendor_item_options WHERE id = ${optId}`);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Yeni kategori ekle\n POST /api/providers/categories\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/categories\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const { name } = req.body;\n if (!name) { res.status(400).json({ error: \"Kategori adı zorunlu\" }); return; }\n\n const posRes = r(await db.execute(sql`SELECT COALESCE(MAX(position),0)+1 as pos FROM vendor_menu_categories WHERE vendor_id = ${vid}`));\n const pos = Number((posRes[0] as Row)?.pos ?? 1);\n const vendorType = await getVendorTypeForId(vid);\n const isShop = [\"alisveris\", \"ecommerce\"].includes(vendorType);\n\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_menu_categories (vendor_id, name, position, is_custom)\n VALUES (${vid}, ${name}, ${pos}, ${isShop})\n RETURNING id, name, position, is_custom, ecommerce_category_id\n `));\n res.status(201).json({ success: true, category: rows[0] });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Kategori sil (soft)\n DELETE /api/providers/categories/:id\n──────────────────────────────────────────────────────── */\nrouter.delete(\"/providers/categories/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const catId = Number(req.params.id);\n await db.execute(sql`UPDATE vendor_menu_categories SET active = false WHERE id = ${catId} AND vendor_id = ${vid}`);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Siparişleri listele\n GET /api/providers/orders\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/orders\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n\n const check = r(await db.execute(sql`SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1`));\n if (!check[0]) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const orders = r(await db.execute(sql`\n SELECT o.id, o.order_number, o.customer_name, o.customer_phone,\n o.customer_address, o.customer_city, o.customer_district,\n o.customer_postal_code, o.customer_email, o.vendor_note,\n o.subtotal, o.delivery_fee, o.total, o.status,\n o.payment_method, o.payment_status, o.notes, o.items,\n o.driver_name, o.driver_phone, o.estimated_time,\n o.assigned_usta_id, o.usta_name,\n o.assigned_servis_id, o.servis_name,\n o.order_source, o.created_by_staff,\n o.platform_commission_amount, o.commission_base_amount,\n o.commission_rate_pct_snapshot, o.revenue_model_snapshot,\n o.geliver_shipment_id, o.geliver_tracking_number, o.geliver_label_url,\n o.geliver_transaction_id, o.geliver_status, o.geliver_last_error,\n o.created_at, o.updated_at\n FROM delivery_orders o\n WHERE o.vendor_id = ${Number(vendorId)}\n ORDER BY o.created_at DESC\n LIMIT 100\n `));\n\n res.json({ success: true, orders });\n});\n\nrouter.post(\"/providers/orders/:id/geliver-shipment\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await ensureGeliverIntegrationColumns();\n\n const orderId = Number(req.params.id);\n if (!Number.isFinite(orderId)) {\n res.status(400).json({ error: \"Geçersiz sipariş\" });\n return;\n }\n\n const force = String(req.query.force ?? \"\") === \"1\";\n const mode = String(req.query.mode ?? \"draft\").toLowerCase();\n\n if (mode === \"label\") {\n const result = await createGeliverShipmentForOrder({ orderId, vendorId: vid, force });\n if (!result.ok) {\n res.status(400).json({ error: result.error ?? \"geliver_failed\" });\n return;\n }\n res.json({ success: true });\n return;\n }\n\n const draft = await createGeliverDraftShipmentForOrder({ orderId, vendorId: vid, force });\n if (!draft.ok) {\n res.status(400).json({ error: draft.error ?? \"geliver_failed\" });\n return;\n }\n res.json({ success: true, shipmentId: draft.shipmentId, shipment: draft.shipment });\n});\n\n/* ────────────────────────────────────────────────────────\n PATCH /api/providers/orders/:id/status\n──────────────────────────────────────────────────────── */\nrouter.patch(\"/providers/orders/:id/status\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const orderId = Number(req.params.id);\n const { status, estimatedTime: estimatedTimeRaw, vendorNote } = req.body as {\n status: string;\n estimatedTime?: number | string;\n vendorNote?: string;\n };\n\n const curr = r(await db.execute(sql`\n SELECT status FROM delivery_orders WHERE id = ${orderId} AND vendor_id = ${vid} LIMIT 1\n `));\n if (!curr[0]) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n\n const gate = deliveryStatusTransitionAllowed(\"vendor\", curr[0].status as string, status);\n if (!gate.ok) {\n res.status(400).json({ error: gate.error });\n return;\n }\n\n const tsField: Record = {\n confirmed: \"confirmed_at\", preparing: \"prepared_at\",\n picked_up: \"picked_up_at\", delivered: \"delivered_at\", cancelled: \"cancelled_at\",\n };\n const col = tsField[status];\n\n /* estimatedTime ve vendorNote sadece confirmed adımında yazılır */\n const etSql =\n status === \"confirmed\" && estimatedTimeRaw != null && estimatedTimeRaw !== \"\" && Number.isFinite(Number(estimatedTimeRaw))\n ? sql`, estimated_time = ${Number(estimatedTimeRaw)}`\n : sql.raw(\"\");\n const vnSql = (status === \"confirmed\" && vendorNote !== undefined)\n ? sql`, vendor_note = ${String(vendorNote)}`\n : sql.raw(\"\");\n\n const updated = r(await db.execute(sql`\n UPDATE delivery_orders\n SET status = ${status}, updated_at = now()\n ${col ? sql.raw(`, ${col} = now()`) : sql.raw(\"\")}\n ${etSql}\n ${vnSql}\n WHERE id = ${orderId} AND vendor_id = ${vid}\n RETURNING id, status, estimated_time, vendor_note, updated_at\n `));\n\n const fromStatus = curr[0].status as string;\n const etNum = estimatedTimeRaw != null && estimatedTimeRaw !== \"\" ? Number(estimatedTimeRaw) : NaN;\n const evNote =\n status === \"confirmed\" && Number.isFinite(etNum) && etNum > 0\n ? etNum >= 1440\n ? `Kargoya verilme: yaklaşık ${Math.round(etNum / 1440)} gün`\n : `Tahmini teslimat: ${Math.round(etNum)} dk`\n : null;\n await db.insert(deliveryOrderStatusEventsTable).values({\n orderId,\n fromStatus,\n toStatus: status,\n source: \"vendor_panel\",\n note: evNote,\n }).catch(() => {});\n\n if (status === \"confirmed\") {\n const [fullOrder] = await db\n .select()\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.vendorId, vid)))\n .limit(1);\n const [venRow] = await db\n .select({ name: vendorsTable.name, callmebotKey: vendorsTable.callmebotKey })\n .from(vendorsTable)\n .where(eq(vendorsTable.id, vid))\n .limit(1);\n if (fullOrder && venRow) {\n const track = fullOrder.orderNumber;\n const gTrack = fullOrder.geliverTrackingNumber?.trim();\n void notifyCustomerOrderWhatsApp({\n vendorId: vid,\n customerName: fullOrder.customerName,\n customerPhone: fullOrder.customerPhone,\n orderNumber: track,\n event: \"confirmed\",\n vendorNote: vendorNote != null ? String(vendorNote) : null,\n geliverTracking: gTrack || null,\n }).catch(() => {});\n const em = String((fullOrder as { customerEmail?: string | null }).customerEmail ?? \"\").trim();\n if (em) {\n const html = `

Merhaba ${escapeHtmlLite(fullOrder.customerName)},

\n

Siparişiniz işleme alındı.

\n

Takip numaranız: ${escapeHtmlLite(track)}

\n${gTrack ? `

Kargo takip: ${escapeHtmlLite(gTrack)}

` : \"\"}\n${vendorNote ? `

İşletme notu: ${escapeHtmlLite(String(vendorNote))}

` : \"\"}\n

${escapeHtmlLite(venRow.name)}

`;\n const textLines = [\n `Merhaba ${fullOrder.customerName},`,\n `Siparişiniz işleme alındı.`,\n `Takip: ${track}`,\n gTrack ? `Kargo: ${gTrack}` : \"\",\n vendorNote ? `Not: ${String(vendorNote).trim()}` : \"\",\n venRow.name,\n ].filter(Boolean).join(\"\\n\");\n void sendEmail({\n to: em,\n subject: `Siparişiniz alındı — ${venRow.name}`,\n html,\n text: textLines,\n }).catch(() => {});\n }\n }\n }\n\n res.json({ success: true, order: updated[0] });\n});\n\n/* — Kurye Yönetimi (Vendor) ──────────────────────────────────── */\n\nrouter.get(\"/providers/couriers\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, name, phone, active, created_at FROM vendor_couriers\n WHERE vendor_id = ${vid} ORDER BY id ASC\n `));\n res.json(rows);\n});\n\nrouter.post(\"/providers/couriers\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { name, phone, password } = req.body as { name: string; phone: string; password?: string };\n if (!name || !phone) { res.status(400).json({ error: \"Ad ve telefon zorunlu\" }); return; }\n const rawPassword = password?.trim() || phone.trim();\n const hash = await bcrypt.hash(rawPassword, 10);\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_couriers (vendor_id, name, phone, password_hash)\n VALUES (${vid}, ${name}, ${phone}, ${hash})\n RETURNING id, name, phone, active, created_at\n `));\n res.status(201).json({ ...rows[0], _defaultPassword: rawPassword });\n});\n\nrouter.delete(\"/providers/couriers/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await db.execute(sql`\n DELETE FROM vendor_couriers WHERE id = ${Number(req.params.id)} AND vendor_id = ${vid}\n `);\n res.json({ success: true });\n});\n\n/* Vendor assigns a courier to an order */\nrouter.patch(\"/providers/orders/:id/assign-courier\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { courierId } = req.body as { courierId: number };\n const [courier] = r(await db.execute(sql`\n SELECT name, phone FROM vendor_couriers WHERE id = ${Number(courierId)} AND vendor_id = ${vid} LIMIT 1\n `));\n if (!courier) { res.status(404).json({ error: \"Kurye bulunamadı\" }); return; }\n await db.execute(sql`\n UPDATE delivery_orders SET driver_name = ${courier.name as string}, driver_phone = ${courier.phone as string},\n updated_at = now() WHERE id = ${Number(req.params.id)} AND vendor_id = ${vid}\n `);\n res.json({ success: true, driverName: courier.name, driverPhone: courier.phone });\n});\n\n/* — Kurye Paneli (Public) ──────────────────────────────────── */\n\n/* Kurye login — phone + password */\nrouter.post(\"/courier/login\", async (req, res): Promise => {\n const { phone, password } = req.body as { phone?: string; password?: string };\n if (!phone || !password) { res.status(400).json({ error: \"Telefon ve şifre zorunlu\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, name, phone, active, password_hash\n FROM vendor_couriers WHERE phone = ${phone.trim()} LIMIT 1\n `));\n if (!rows[0]) { res.status(401).json({ error: \"Telefon numarası kayıtlı değil\" }); return; }\n const courier = rows[0];\n if (!courier.active) { res.status(401).json({ error: \"Hesabınız aktif değil. İşletme sahibiyle iletişime geçin.\" }); return; }\n const storedHash = courier.password_hash as string | null;\n if (!storedHash) {\n res.status(403).json({ error: \"Bu kurye hesabı için şifre tanımlanmamış. İşletme panelinden şifre atayın.\" });\n return;\n }\n const ok = await bcrypt.compare(password, storedHash);\n if (!ok) { res.status(401).json({ error: \"Şifre yanlış\" }); return; }\n const token = signCourierSessionToken(Number(courier.id), String(courier.phone));\n res.json({ id: courier.id, name: courier.name, phone: courier.phone, token });\n});\n\nrouter.use(\"/courier\", requireCourierSession);\n\n/* Kurye şifre değiştirme */\nrouter.patch(\"/courier/change-password\", async (req, res): Promise => {\n const { currentPassword, newPassword } = req.body as { currentPassword?: string; newPassword?: string };\n const phone = courierPhoneFromSession(req);\n if (!phone || !currentPassword || !newPassword) { res.status(400).json({ error: \"Eksik bilgi\" }); return; }\n if (newPassword.length < 4) { res.status(400).json({ error: \"Yeni şifre en az 4 karakter olmalı\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, active, password_hash FROM vendor_couriers WHERE phone = ${phone.trim()} LIMIT 1\n `));\n if (!rows[0]) { res.status(404).json({ error: \"Kurye bulunamadı\" }); return; }\n const courier = rows[0];\n if (!courier.active) { res.status(401).json({ error: \"Hesabınız aktif değil\" }); return; }\n const storedHash = courier.password_hash as string | null;\n const ok = storedHash ? await bcrypt.compare(currentPassword, storedHash) : false;\n if (!ok) { res.status(401).json({ error: \"Mevcut şifre yanlış\" }); return; }\n const newHash = await bcrypt.hash(newPassword, 10);\n await db.execute(sql`UPDATE vendor_couriers SET password_hash = ${newHash} WHERE id = ${courier.id as number}`);\n res.json({ success: true });\n});\n\n/* Kurye phone ile login → kendine atanmış siparişleri görür */\nrouter.get(\"/courier/orders\", async (req, res): Promise => {\n const phone = courierPhoneFromSession(req);\n if (!phone) { res.status(400).json({ error: \"Telefon zorunlu\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT o.*, v.name as vendor_name, v.address as vendor_address, v.phone as vendor_phone\n FROM delivery_orders o\n JOIN vendors v ON v.id = o.vendor_id\n WHERE o.driver_phone = ${phone}\n AND o.status NOT IN ('cancelled')\n ORDER BY o.created_at DESC LIMIT 50\n `));\n res.json(rows);\n});\n\n/* Kurye sipariş durumunu günceller: picked_up → delivered + proof photo */\nrouter.patch(\"/courier/orders/:id/status\", async (req, res): Promise => {\n const { status, proofUrl } = req.body as { status: string; proofUrl?: string };\n const phone = courierPhoneFromSession(req);\n if (!phone || !status) { res.status(400).json({ error: \"Eksik alan\" }); return; }\n\n const curr = r(await db.execute(sql`\n SELECT id, status FROM delivery_orders WHERE id = ${Number(req.params.id)} AND driver_phone = ${phone} LIMIT 1\n `));\n if (!curr[0]) { res.status(404).json({ error: \"Sipariş bulunamadı veya bu siparişe atanmış değilsiniz\" }); return; }\n\n const fromCourier = curr[0].status as string;\n const gate = deliveryStatusTransitionAllowed(\"courier\", fromCourier, status);\n if (!gate.ok) {\n res.status(400).json({ error: gate.error });\n return;\n }\n\n const tsField: Record = { picked_up: \"picked_up_at\", on_the_way: \"picked_up_at\", delivered: \"delivered_at\" };\n const col = tsField[status];\n const orderId = Number(req.params.id);\n\n await db.execute(sql`\n UPDATE delivery_orders\n SET status = ${status}, updated_at = now()\n ${col ? sql.raw(`, ${col} = now()`) : sql.raw(\"\")}\n ${proofUrl && status === \"delivered\" ? sql`, delivery_proof_url = ${proofUrl}` : sql.raw(\"\")}\n WHERE id = ${orderId} AND driver_phone = ${phone}\n `);\n\n await db.insert(deliveryOrderStatusEventsTable).values({\n orderId,\n fromStatus: fromCourier,\n toStatus: status,\n source: \"courier_panel\",\n note: null,\n }).catch(() => {});\n\n res.json({ success: true, status });\n});\n\n/* Kurye canlı konum ping */\nrouter.post(\"/courier/location\", async (req, res): Promise => {\n const { orderId, lat, lng, accuracy, heading, speed } = req.body as {\n phone?: string; orderId?: number; lat?: number; lng?: number; accuracy?: number; heading?: number; speed?: number;\n };\n const courierPhone = courierPhoneFromSession(req);\n const latNum = Number(lat);\n const lngNum = Number(lng);\n if (!courierPhone || !Number.isFinite(latNum) || !Number.isFinite(lngNum)) {\n res.status(400).json({ error: \"phone, lat, lng zorunlu\" }); return;\n }\n\n const rows = r(await db.execute(sql`\n SELECT id, active FROM vendor_couriers WHERE phone = ${courierPhone} LIMIT 1\n `));\n if (!rows[0]) { res.status(404).json({ error: \"Kurye bulunamadı\" }); return; }\n if (!rows[0].active) { res.status(401).json({ error: \"Kurye pasif\" }); return; }\n const courierId = Number(rows[0].id);\n\n let orderIdNum: number | null = null;\n if (orderId !== undefined && orderId !== null && Number.isFinite(Number(orderId))) {\n const check = r(await db.execute(sql`\n SELECT id FROM delivery_orders\n WHERE id = ${Number(orderId)} AND driver_phone = ${courierPhone}\n LIMIT 1\n `));\n if (check[0]) orderIdNum = Number(check[0].id);\n }\n if (!orderIdNum) {\n const activeOrder = r(await db.execute(sql`\n SELECT id FROM delivery_orders\n WHERE driver_phone = ${courierPhone}\n AND status IN ('picked_up', 'on_the_way')\n ORDER BY updated_at DESC\n LIMIT 1\n `));\n if (activeOrder[0]) orderIdNum = Number(activeOrder[0].id);\n }\n\n const rowsIns = r(await db.execute(sql`\n INSERT INTO driver_locations (order_id, courier_id, courier_phone, lat, lng, accuracy, heading, speed, source)\n VALUES (\n ${orderIdNum},\n ${courierId},\n ${courierPhone},\n ${latNum},\n ${lngNum},\n ${Number.isFinite(Number(accuracy)) ? Number(accuracy) : null},\n ${Number.isFinite(Number(heading)) ? Number(heading) : null},\n ${Number.isFinite(Number(speed)) ? Number(speed) : null},\n 'courier_panel'\n )\n RETURNING id, order_id, lat, lng, created_at\n `));\n\n res.json({ success: true, location: rowsIns[0] ?? null });\n});\n\n/* — Mesajlaşma (Vendor Panel & Kurye Panel) ──────────────────── */\n\n/* Vendor: sipariş mesajlarını listele */\nrouter.get(\"/providers/orders/:id/messages\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const orderId = Number(req.params.id);\n /* Siparişin bu vendor'a ait olduğunu doğrula */\n const [order] = await db.select({ id: deliveryOrdersTable.id })\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.vendorId, vid)))\n .limit(1);\n if (!order) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n const msgs = await db.select().from(orderMessagesTable)\n .where(eq(orderMessagesTable.orderId, orderId))\n .orderBy(orderMessagesTable.createdAt);\n res.json(msgs);\n});\n\n/* Vendor: mesaj gönder */\nrouter.post(\"/providers/orders/:id/messages\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { senderName, message } = req.body as { senderName: string; message: string };\n if (!message?.trim()) { res.status(400).json({ error: \"Mesaj boş olamaz\" }); return; }\n const orderId = Number(req.params.id);\n const [order] = await db.select({ id: deliveryOrdersTable.id })\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.vendorId, vid)))\n .limit(1);\n if (!order) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n const [msg] = await db.insert(orderMessagesTable).values({\n orderId, senderType: \"vendor\",\n senderName: senderName || \"İşletme\", message: message.trim(),\n }).returning();\n res.status(201).json(msg);\n});\n\n/* Kurye: mesajları listele */\nrouter.get(\"/courier/orders/:id/messages\", async (req, res): Promise => {\n const orderId = Number(req.params.id);\n const phone = courierPhoneFromSession(req);\n const [order] = await db.select({ id: deliveryOrdersTable.id })\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.driverPhone, phone)))\n .limit(1);\n if (!order) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n const msgs = await db.select().from(orderMessagesTable)\n .where(eq(orderMessagesTable.orderId, orderId))\n .orderBy(orderMessagesTable.createdAt);\n res.json(msgs);\n});\n\n/* Kurye: mesaj gönder */\nrouter.post(\"/courier/orders/:id/messages\", async (req, res): Promise => {\n const { senderName, message } = req.body as { senderName: string; message: string };\n if (!message?.trim()) { res.status(400).json({ error: \"Mesaj boş olamaz\" }); return; }\n const orderId = Number(req.params.id);\n const phone = courierPhoneFromSession(req);\n const [order] = await db.select({ id: deliveryOrdersTable.id })\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.driverPhone, phone)))\n .limit(1);\n if (!order) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n const [msg] = await db.insert(orderMessagesTable).values({\n orderId, senderType: \"courier\",\n senderName: senderName || \"Kurye\", message: message.trim(),\n }).returning();\n res.status(201).json(msg);\n});\n\n/* Vendor: dış platform URL'sinden menü önizleme (kaydetmez) */\nrouter.post(\"/providers/import/external-menu/preview\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const body = req.body as Record;\n const sourceUrl = String(body.sourceUrl ?? \"\").trim();\n if (!sourceUrl) { res.status(400).json({ error: \"sourceUrl zorunlu\" }); return; }\n try {\n const imported = await importVendorFromExternalUrl(sourceUrl, {\n prefetchedHtml: pickPrefetchedHtml(body),\n prefetchedMenuJson: pickPrefetchedMenuJson(body),\n });\n res.json({ success: true, ...externalMenuPreviewPayload(imported) });\n } catch (e: any) {\n res.status(500).json({ error: e?.message || \"Menü önizleme başarısız\" });\n }\n});\n\n/* Vendor: dış platform URL'sinden kendi menüsünü içe aktar */\nrouter.post(\"/providers/import/external-menu\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const body = req.body as Record;\n const sourceUrl = String(body.sourceUrl ?? \"\").trim();\n const selectedCategories = Array.isArray(body.selectedCategories)\n ? (body.selectedCategories as unknown[]).map((c) => String(c)).filter(Boolean)\n : undefined;\n const menu = Array.isArray(body.menu) ? (body.menu as ImportedMenuItem[]) : undefined;\n if (!sourceUrl && !(menu && menu.length > 0)) {\n res.status(400).json({ error: \"sourceUrl veya menu zorunlu\" });\n return;\n }\n try {\n const { imported, menuStats } = await runExternalMenuImportForVendor(vid, {\n sourceUrl: sourceUrl || undefined,\n prefetchedHtml: pickPrefetchedHtml(body),\n prefetchedMenuJson: pickPrefetchedMenuJson(body),\n menu,\n selectedCategories,\n city: body.city != null ? String(body.city) : undefined,\n district: body.district != null ? String(body.district) : undefined,\n neighborhood: body.neighborhood != null ? String(body.neighborhood) : undefined,\n address: body.address != null ? String(body.address) : undefined,\n phone: body.phone != null ? String(body.phone) : undefined,\n lat: body.lat != null ? Number(body.lat) : undefined,\n lng: body.lng != null ? Number(body.lng) : undefined,\n rating: body.rating != null ? Number(body.rating) : undefined,\n reviewCount: body.reviewCount != null ? Number(body.reviewCount) : undefined,\n platform: body.platform != null ? String(body.platform) : undefined,\n name: body.name != null ? String(body.name) : undefined,\n });\n res.json({\n success: true,\n vendorId: vid,\n importedMeta: {\n platform: imported.platform,\n itemCount: menuStats.items,\n rating: imported.rating ?? null,\n reviewCount: imported.reviewCount ?? null,\n },\n menuStats,\n });\n } catch (e: any) {\n res.status(500).json({ error: e?.message || \"Menü içe aktarma başarısız\" });\n }\n});\n\n/* — Admin: Vendor Kategori Yönetimi ──────────────────────────── */\n\nrouter.get(\"/admin/vendor-categories\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const rows = r(await db.execute(sql`\n SELECT id, name, slug, icon, image_url, position, active, super_category, created_at\n FROM vendor_categories ORDER BY position ASC, id ASC\n `));\n res.json(rows);\n});\n\nrouter.post(\"/admin/vendor-categories\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { name, slug, icon, imageUrl, position, superCategory } = req.body as Record;\n if (!name || !slug) { res.status(400).json({ error: \"Ad ve slug zorunlu\" }); return; }\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_categories (name, slug, icon, image_url, position, super_category)\n VALUES (${name as string}, ${slug as string}, ${(icon as string) || null}, ${(imageUrl as string) || null},\n ${Number(position) || 0}, ${(superCategory as string) || \"siparis\"})\n RETURNING id, name, slug, icon, image_url, position, active, super_category, created_at\n `));\n res.status(201).json(rows[0]);\n});\n\nrouter.put(\"/admin/vendor-categories/:id\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { name, slug, icon, imageUrl, position, superCategory, active } = req.body as Record;\n const parts: string[] = [];\n const vals: (string | number | boolean | null)[] = [];\n if (name !== undefined) { parts.push(`name = $${parts.length+1}`); vals.push(name as string); }\n if (slug !== undefined) { parts.push(`slug = $${parts.length+1}`); vals.push(slug as string); }\n if (icon !== undefined) { parts.push(`icon = $${parts.length+1}`); vals.push((icon as string) || null); }\n if (imageUrl !== undefined) { parts.push(`image_url = $${parts.length+1}`); vals.push((imageUrl as string) || null); }\n if (position !== undefined) { parts.push(`position = $${parts.length+1}`); vals.push(Number(position) || 0); }\n if (superCategory !== undefined) { parts.push(`super_category = $${parts.length+1}`);vals.push(superCategory as string); }\n if (active !== undefined) { parts.push(`active = $${parts.length+1}`); vals.push(Boolean(active)); }\n if (parts.length === 0) { res.status(400).json({ error: \"Güncellenecek alan yok\" }); return; }\n vals.push(Number(req.params.id));\n const { pool } = await import(\"@workspace/db\");\n const r2 = await pool.query(\n `UPDATE vendor_categories SET ${parts.join(\", \")} WHERE id = $${parts.length+1} RETURNING id, name, slug, icon, image_url, position, active, super_category, created_at`,\n vals\n );\n res.json(r2.rows[0] ?? {});\n});\n\nrouter.delete(\"/admin/vendor-categories/:id\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n await db.execute(sql`DELETE FROM vendor_categories WHERE id = ${Number(req.params.id)}`);\n res.json({ success: true });\n});\n\n/* — Mağaza vitrin teması ve özel alan — */\n\nrouter.get(\"/providers/storefront/theme\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n await ensureVendorThemeColumns();\n\n const rows = r(await db.execute(sql`\n SELECT id, slug, vendor_type, provider_type, provider_subtype, theme_key, theme_config\n FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `));\n const v = rows[0];\n if (!v) {\n res.status(401).json({ error: \"Geçersiz oturum\" });\n return;\n }\n\n const vendorCtx = {\n vendorType: String(v.vendor_type ?? \"\"),\n providerType: String(v.provider_type ?? \"\"),\n providerSubtype: String(v.provider_subtype ?? \"\"),\n };\n const themeKey = String(v.theme_key ?? \"\").trim() || resolveDefaultThemeKey(vendorCtx);\n const savedRaw = v.theme_config;\n const saved =\n savedRaw && typeof savedRaw === \"object\" && !Array.isArray(savedRaw)\n ? (savedRaw as Record)\n : typeof savedRaw === \"string\"\n ? (() => {\n try {\n return JSON.parse(savedRaw) as Record;\n } catch {\n return {};\n }\n })()\n : {};\n\n const domains = await listVendorDomains(Number(vendorId));\n const available = themesForVendor(vendorCtx);\n const internalStorefrontPath = resolveVendorStorefrontPath({ slug: String(v.slug ?? \"\"), ...vendorCtx });\n const approvedDomain = domains.find((d) => d.status === \"approved\" || d.verified_at);\n const shortPath = resolveVendorCustomDomainShortPath({\n id: Number(vendorId),\n name: undefined,\n slug: String(v.slug ?? \"\"),\n });\n const storefrontPath = approvedDomain?.domain\n ? `https://${approvedDomain.domain}${shortPath}`\n : internalStorefrontPath;\n\n res.json({\n success: true,\n themeKey,\n themeConfig: mergeThemeConfig(themeKey, saved),\n defaultThemeKey: resolveDefaultThemeKey(vendorCtx),\n storefrontPath,\n internalStorefrontPath,\n shortPath,\n availableThemes: available,\n allThemes: VENDOR_THEME_CATALOG,\n domains,\n ...readVendorNavMenuFromThemeConfig(saved),\n });\n});\n\nrouter.put(\"/providers/storefront/theme\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n await ensureVendorThemeColumns();\n\n const body = req.body as Record;\n const hasThemeKey = Object.prototype.hasOwnProperty.call(body, \"themeKey\");\n const hasThemeConfig = Object.prototype.hasOwnProperty.call(body, \"themeConfig\");\n const hasNavMenuEnabled = Object.prototype.hasOwnProperty.call(body, \"navMenuEnabled\");\n const hasNavMenuItems = Object.prototype.hasOwnProperty.call(body, \"navMenuItems\");\n const hasStripMenuEnabled = Object.prototype.hasOwnProperty.call(body, \"stripMenuEnabled\");\n const hasStripMenuItems = Object.prototype.hasOwnProperty.call(body, \"stripMenuItems\");\n\n const current = r(await db.execute(sql`\n SELECT id, vendor_type, provider_type, provider_subtype, theme_key, theme_config\n FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `))[0];\n if (!current) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const vendorCtx = {\n vendorType: String(current.vendor_type ?? \"\"),\n providerType: String(current.provider_type ?? \"\"),\n providerSubtype: String(current.provider_subtype ?? \"\"),\n };\n\n let nextKey = String(current.theme_key ?? \"\").trim() || resolveDefaultThemeKey(vendorCtx);\n if (hasThemeKey) {\n const raw = String(body.themeKey ?? \"\").trim().toLowerCase();\n const allowed = themesForVendor(vendorCtx);\n if (!allowed.some((t) => t.key === raw)) {\n res.status(400).json({ error: \"Bu mağaza türü için tema seçilemez\" });\n return;\n }\n nextKey = raw;\n }\n\n let nextConfig: Record = {};\n const prevRaw = current.theme_config;\n if (prevRaw && typeof prevRaw === \"object\" && !Array.isArray(prevRaw)) {\n nextConfig = { ...(prevRaw as Record) };\n } else if (typeof prevRaw === \"string\") {\n try {\n nextConfig = JSON.parse(prevRaw) as Record;\n } catch {\n nextConfig = {};\n }\n }\n\n if (hasThemeConfig && body.themeConfig && typeof body.themeConfig === \"object\") {\n const def = themeDefByKey(nextKey);\n const incoming = body.themeConfig as Record;\n for (const slot of def?.slots ?? []) {\n if (Object.prototype.hasOwnProperty.call(incoming, slot.key)) {\n const val = incoming[slot.key];\n nextConfig[slot.key] = val == null ? \"\" : String(val).trim().slice(0, 4000);\n }\n }\n }\n\n if (hasNavMenuEnabled) {\n nextConfig.navMenuEnabled = body.navMenuEnabled === true;\n }\n\n if (hasNavMenuItems) {\n const rawItems = Array.isArray(body.navMenuItems) ? body.navMenuItems : [];\n const cleaned = cleanVendorNavMenuItems(\n rawItems.map((item, index) => {\n const o = item && typeof item === \"object\" && !Array.isArray(item) ? (item as Record) : {};\n return {\n id: String(o.id ?? `vnav-${index + 1}`),\n label: String(o.label ?? \"\"),\n href: String(o.href ?? \"#\"),\n enabled: o.enabled === false ? false : true,\n };\n }),\n );\n if (cleaned?.length) nextConfig.navMenuItems = cleaned;\n else delete nextConfig.navMenuItems;\n }\n\n if (hasStripMenuEnabled) {\n nextConfig.stripMenuEnabled = body.stripMenuEnabled === true;\n }\n\n if (hasStripMenuItems) {\n const rawItems = Array.isArray(body.stripMenuItems) ? body.stripMenuItems : [];\n const cleaned = cleanVendorNavMenuItems(\n rawItems.map((item, index) => {\n const o = item && typeof item === \"object\" && !Array.isArray(item) ? (item as Record) : {};\n return {\n id: String(o.id ?? `vstrip-${index + 1}`),\n label: String(o.label ?? \"\"),\n href: String(o.href ?? \"#\"),\n enabled: o.enabled === false ? false : true,\n };\n }),\n );\n if (cleaned?.length) nextConfig.stripMenuItems = cleaned;\n else delete nextConfig.stripMenuItems;\n }\n\n const rows = r(await db.execute(sql`\n UPDATE vendors\n SET theme_key = ${nextKey},\n theme_config = ${JSON.stringify(nextConfig)}::jsonb,\n updated_at = NOW()\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n RETURNING theme_key, theme_config\n `));\n\n res.json({\n success: true,\n themeKey: String(rows[0]?.theme_key ?? nextKey),\n themeConfig: mergeThemeConfig(nextKey, nextConfig),\n ...readVendorNavMenuFromThemeConfig(nextConfig),\n });\n});\n\nrouter.put(\"/providers/storefront/domains\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n await ensureVendorCustomDomainsTable();\n\n const check = r(await db.execute(sql`\n SELECT id, name, slug FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `))[0];\n if (!check) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const rawList = (req.body as { domains?: unknown }).domains;\n if (!Array.isArray(rawList)) {\n res.status(400).json({ error: \"domains dizisi gerekli\" });\n return;\n }\n\n const normalized: string[] = [];\n const seen = new Set();\n for (const item of rawList.slice(0, 3)) {\n const d = normalizeVendorDomain(String(item ?? \"\"));\n if (!d || seen.has(d)) continue;\n seen.add(d);\n normalized.push(d);\n }\n\n for (const d of normalized) {\n const conflict = r(await db.execute(sql`\n SELECT vendor_id FROM vendor_custom_domains\n WHERE lower(trim(domain)) = ${d} AND vendor_id <> ${Number(vendorId)}\n LIMIT 1\n `))[0];\n if (conflict) {\n res.status(409).json({ error: `${d} başka bir mağazaya bağlı` });\n return;\n }\n }\n\n await db.execute(sql`DELETE FROM vendor_custom_domains WHERE vendor_id = ${Number(vendorId)}`);\n\n let pos = 1;\n for (const d of normalized) {\n await db.execute(sql`\n INSERT INTO vendor_custom_domains (vendor_id, domain, position, status, verified_at, requested_at, updated_at)\n VALUES (${Number(vendorId)}, ${d}, ${pos}, 'pending', NULL, NOW(), NOW())\n `);\n pos += 1;\n }\n\n if (normalized.length > 0) {\n notifyAdminWhatsApp({\n eventType: \"Servis Sağlayıcı Domain Talebi\",\n details: [\n `🏢 ${String(check.name ?? \"Servis sağlayıcı\")}`,\n `🆔 Vendor ID: ${Number(vendorId)}`,\n `🔗 ${normalized.join(\", \")}`,\n `🌐 DNS: ns1.vercel-dns.com / ns2.vercel-dns.com`,\n `📌 Admin panelden onay bekliyor`,\n ].join(\"\\n\"),\n }).catch(() => {});\n }\n\n const domains = await listVendorDomains(Number(vendorId));\n res.json({ success: true, domains, message: \"Domain talebiniz admin onayına gönderildi\" });\n});\n\nrouter.get(\"/admin/vendor-domain-requests\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n await ensureVendorCustomDomainsTable();\n const rows = r(await db.execute(sql`\n SELECT d.id, d.vendor_id, d.domain, d.position, d.status, d.admin_note, d.verified_at,\n d.requested_at, d.approved_at, d.rejected_at,\n v.name AS vendor_name, v.slug AS vendor_slug, v.vendor_type, v.provider_type, v.provider_subtype\n FROM vendor_custom_domains d\n JOIN vendors v ON v.id = d.vendor_id\n ORDER BY\n CASE WHEN d.status = 'pending' THEN 0 WHEN d.status = 'approved' THEN 1 ELSE 2 END,\n d.requested_at DESC,\n d.id DESC\n LIMIT 200\n `));\n res.json({ success: true, requests: rows });\n});\n\nrouter.put(\"/admin/vendor-domain-requests/:id\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n await ensureVendorCustomDomainsTable();\n const id = Number(req.params.id);\n const body = req.body as { action?: unknown; adminNote?: unknown };\n const action = String(body.action ?? \"\").trim().toLowerCase();\n const adminNote = String(body.adminNote ?? \"\").trim().slice(0, 1000) || null;\n if (!Number.isFinite(id) || id <= 0) {\n res.status(400).json({ error: \"Geçersiz kayıt\" });\n return;\n }\n if (action !== \"approve\" && action !== \"reject\") {\n res.status(400).json({ error: \"action approve veya reject olmalı\" });\n return;\n }\n\n const rows = r(await db.execute(sql`\n UPDATE vendor_custom_domains\n SET status = ${action === \"approve\" ? \"approved\" : \"rejected\"},\n admin_note = ${adminNote},\n verified_at = ${action === \"approve\" ? sql`NOW()` : sql`NULL`},\n approved_at = ${action === \"approve\" ? sql`NOW()` : sql`NULL`},\n rejected_at = ${action === \"reject\" ? sql`NOW()` : sql`NULL`},\n updated_at = NOW()\n WHERE id = ${id}\n RETURNING id, vendor_id, domain, status, verified_at, admin_note, approved_at, rejected_at\n `));\n const row = rows[0];\n if (!row) {\n res.status(404).json({ error: \"Talep bulunamadı\" });\n return;\n }\n res.json({ success: true, request: row });\n});\n\nexport default router;\n"}, "0264ad76295c63315d43d053a550cfd01d723f3e": {"t": 1786068278.550142, "n": 188876, "h": "19253fbca83f8841", "c": "import { Router, type NextFunction, type Request, type Response } from \"express\";\nimport { db } from \"@workspace/db\";\nimport { sql, eq, and, inArray } from \"drizzle-orm\";\nimport { vendorMenuItemsTable } from \"@workspace/db\";\nimport {\n orderMessagesTable,\n deliveryOrdersTable,\n deliveryOrderStatusEventsTable,\n vendorsTable,\n siteSettingsTable,\n} from \"@workspace/db\";\nimport {\n notifyVendorWhatsApp,\n notifyAdminWhatsApp,\n notifyCustomerOrderWhatsApp,\n trySendSiteOutboundWhatsApp,\n phoneDigitsVariants,\n digitsForWhatsAppLink,\n} from \"../lib/whatsapp\";\nimport { deliveryStatusTransitionAllowed } from \"../lib/delivery-status-transitions\";\nimport bcrypt from \"bcryptjs\";\nimport jwt from \"jsonwebtoken\";\nimport crypto from \"crypto\";\nimport { verifyLoginMathCaptcha } from \"../lib/loginMathCaptcha.js\";\nimport { sendEmail, buildActivationEmail, buildPasswordResetEmail } from \"../lib/email\";\nimport {\n importVendorFromExternalUrl,\n buildExternalMenuPreview,\n externalMenuPreviewPayload,\n buildMergedAddress,\n filterMenuSelection,\n type ImportedMenuItem,\n type ImportedVendorData,\n type ExternalPlatform,\n} from \"../lib/external-delivery-import\";\nimport { normalizeRevenueModel, sanitizeTrIban } from \"../lib/vendor-revenue.js\";\nimport { generateTurkishVendorAboutDetailed } from \"../lib/vendor-about-ai.js\";\nimport { sitePublicOrigin } from \"../lib/site-public-origin.js\";\nimport { getSessionSecret, getGeliverWebhookSecret } from \"../lib/secrets.js\";\nimport { vendorApprovalTempPassword } from \"../lib/demo-credentials.js\";\nimport {\n createGeliverDraftShipmentForOrder,\n createGeliverShipmentForOrder,\n createGeliverManualShipment,\n ensureGeliverIntegrationColumns,\n geliverJsonSafe,\n geliverApiListCitiesForVendor,\n geliverApiListDistrictsForVendor,\n geliverApiListShipmentsForVendor,\n geliverApiGetShipmentForVendor,\n geliverApiUpdateShipmentPackageForVendor,\n geliverApiAcceptOfferForVendor,\n geliverApiListAddressesForVendor,\n geliverApiGetAddressForVendor,\n geliverApiCreateSenderAddressForVendor,\n geliverApiDeleteAddressForVendor,\n geliverApiGetBalanceForVendor,\n geliverApiListWebhooksForVendor,\n geliverApiCreateWebhookForVendor,\n geliverApiDeleteWebhookForVendor,\n geliverApiTestWebhookForVendor,\n geliverApiListParcelTemplatesForVendor,\n geliverApiCreateParcelTemplateForVendor,\n geliverApiDeleteParcelTemplateForVendor,\n geliverApiListPricesForVendor,\n geliverApiListProviderAccountsForVendor,\n geliverApiCreateProviderAccountForVendor,\n geliverApiDeleteProviderAccountForVendor,\n} from \"../lib/geliver-shipment.js\";\nimport {\n getEcommerceCategoryTree,\n resolveProductCategoryForVendor,\n} from \"../lib/ecommerce-product-categories.js\";\nimport { logger } from \"../lib/logger.js\";\nimport { denyUnlessAdminMaintenance, denyUnlessAdminMaintenanceAny } from \"../lib/admin-guard.js\";\nimport { ensureVendorBlogTables } from \"../lib/vendor-blog-db.js\";\nimport {\n VENDOR_THEME_CATALOG,\n ensureVendorCustomDomainsTable,\n ensureVendorThemeColumns,\n listVendorDomains,\n mergeThemeConfig,\n cleanVendorNavMenuItems,\n readVendorNavMenuFromThemeConfig,\n normalizeVendorDomain,\n resolveDefaultThemeKey,\n resolveVendorCustomDomainShortPath,\n resolveVendorStorefrontPath,\n themeDefByKey,\n themesForVendor,\n} from \"../lib/vendor-storefront.js\";\n\nconst router = Router();\n\nfunction escapeHtmlLite(s: string): string {\n return String(s)\n .replace(/&/g, \"&\")\n .replace(/ {\n try {\n await db.execute(sql`ALTER TABLE vendor_couriers ADD COLUMN IF NOT EXISTS password_hash text`);\n } catch { /* ignore */ }\n try {\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS password_hash text`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS is_open BOOLEAN NOT NULL DEFAULT true`);\n } catch { /* ignore */ }\n try {\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS revenue_model TEXT NOT NULL DEFAULT 'subscription'`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS commission_rate_pct NUMERIC(8,4)`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS payout_bank_holder TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS payout_bank_iban TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS payout_bank_branch TEXT`);\n } catch { /* ignore */ }\n try {\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_api_token TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_sender_address_id TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_sender_zip TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_sender_mahalle TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_auto_ship_on_order BOOLEAN NOT NULL DEFAULT false`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS geliver_organization_id TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS customer_postal_code TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_shipment_id TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_tracking_number TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_label_url TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_transaction_id TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_status TEXT`);\n await db.execute(sql`ALTER TABLE delivery_orders ADD COLUMN IF NOT EXISTS geliver_last_error TEXT`);\n } catch { /* ignore */ }\n try {\n await db.execute(sql`\n CREATE TABLE IF NOT EXISTS vendor_subscription_requests (\n id SERIAL PRIMARY KEY,\n vendor_id INTEGER NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,\n start_date DATE NOT NULL,\n end_date DATE NOT NULL,\n payment_method TEXT NOT NULL DEFAULT 'bank_transfer',\n receipt_url TEXT,\n status TEXT NOT NULL DEFAULT 'pending',\n admin_note TEXT,\n processed_at TIMESTAMP,\n created_at TIMESTAMP NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMP NOT NULL DEFAULT NOW()\n )\n `);\n } catch { /* ignore */ }\n})();\n\n/* Servis tipi → Türkçe etiket */\nexport const PROVIDER_TYPE_LABELS: Record = {\n siparis: \"Sipariş İşletmesi\",\n alisveris:\"Alışveriş Mağazası\",\n hizmet: \"Hizmet Sağlayıcı\",\n ilan: \"İlan Odaklı İşletme\",\n turizm: \"Turizm & Seyahat\",\n ulasim: \"Ulaşım & Bildirim\",\n \"ulaşım\": \"Ulaşım & Bildirim\",\n transport:\"Ulaşım & Bildirim\",\n delivery: \"Sipariş İşletmesi\",\n ecommerce:\"Alışveriş Mağazası\",\n};\n\nexport const PROVIDER_SUBTYPE_LABELS: Record = {\n otel:\"Otel\", arac:\"Rent a Car\", villa:\"Villa & Ev Kiralama\", tur:\"Tur\", yat:\"Yat & Tekne\",\n restoran:\"Restoran / Kafe\", market:\"Market / Bakkal\", eczane:\"Eczane\",\n cicekci:\"Çiçekçi\", giyim:\"Giyim / Tekstil\", elektronik:\"Elektronik\",\n kitap:\"Kitap / Kırtasiye\", ev:\"Ev & Yaşam\", spor:\"Spor / Outdoor\",\n kozmetik:\"Kozmetik / Güzellik\", cekici:\"Çekici / Yol Yardım\",\n nakliyeci:\"Nakliyeci\", oto_galeri:\"Oto Galeri\",\n kurs:\"Kurs / Eğitim\", tadilat:\"Tadilat / Tamir\", temizlik:\"Temizlik\",\n boya:\"Boya / Badana\", guzellik:\"Güzellik Salonu\", saglik:\"Sağlık / Klinik\",\n ikinci_el:\"2. El Eşya\", is_makinesi:\"İş Makineleri\", telefon:\"Telefon / GSM\",\n beyaz_esya:\"Beyaz Eşya\", hayvan:\"Hayvanlar / Pet\", diger:\"Diğer\",\n taksi:\"Şehir İçi Yolcu Taşıma\", taxi:\"Şehir İçi Yolcu Taşıma\",\n kurye:\"Kurye\", courier:\"Kurye\", rideshare:\"Ortak Yolculuk\",\n};\n\ntype Row = Record;\n\nfunction normalizeProviderPanelText(v: unknown): string {\n return String(v ?? \"\").trim().toLocaleLowerCase(\"tr-TR\");\n}\n\nfunction providerPanelKindForRow(row: Row): \"delivery\" | \"tourism\" | \"transport\" {\n const type = normalizeProviderPanelText(row.provider_type ?? row.vendor_type);\n const subtype = normalizeProviderPanelText(row.provider_subtype);\n if ([\"turizm\", \"tourism\"].includes(type)) return \"tourism\";\n if ([\"ulasim\", \"ulaşım\", \"transport\"].includes(type)) return \"transport\";\n if ([\"otel\", \"hotel\", \"arac\", \"car\", \"rentacar\", \"villa\", \"tur\", \"tour\", \"yat\", \"boat\", \"tekne\"].includes(subtype)) {\n return \"tourism\";\n }\n if ([\"taksi\", \"taxi\", \"kurye\", \"courier\", \"cekici\", \"tow\", \"nakliyeci\", \"moving\", \"kargo\", \"cargo\", \"rideshare\"].includes(subtype)) {\n return \"transport\";\n }\n return \"delivery\";\n}\n\nfunction attachProviderPanelRoute(row: Row): Row {\n const kind = providerPanelKindForRow(row);\n row.panel_kind = kind;\n row.panel_route = kind === \"tourism\" ? \"/turizm-paneli\" : \"/servis-saglayici-paneli\";\n return row;\n}\n\nconst TABLE_SERVICE_PROVIDER_SUBTYPES = new Set([\"\", \"restoran\", \"restaurant\", \"cafe\", \"kafe\", \"restoran-kafe\", \"restoran_cafe\"]);\n\nfunction canVendorUseTableService(row: { provider_type?: unknown; vendor_type?: unknown; provider_subtype?: unknown }): boolean {\n const type = String(row.provider_type || row.vendor_type || \"\").toLowerCase();\n const subtype = String(row.provider_subtype || \"\").toLocaleLowerCase(\"tr-TR\").replace(/\\s+/g, \"_\");\n return [\"siparis\", \"delivery\"].includes(type) && TABLE_SERVICE_PROVIDER_SUBTYPES.has(subtype);\n}\n\nfunction membershipPricingFromSiteRow(site: typeof siteSettingsTable.$inferSelect | undefined) {\n const rateRaw = site?.usdTryRate != null ? parseFloat(String(site.usdTryRate)) : NaN;\n const rate = Number.isFinite(rateRaw) && rateRaw > 0 ? rateRaw : null;\n const num = (d: unknown, fallback: number) => {\n if (d == null || d === \"\") return fallback;\n const n = parseFloat(String(d).replace(\",\", \".\"));\n return Number.isFinite(n) && n >= 0 ? n : fallback;\n };\n const std = num(site?.providerMembershipStandardUsd, 10);\n const gold = num(site?.providerMembershipGoldUsd, 10);\n const prem = num(site?.providerMembershipPremiumPerBusinessUsd, 10);\n const tryMul = (usd: number) => (rate != null ? Math.round(usd * rate * 100) / 100 : null);\n return {\n usdTryRate: rate,\n usdTryUpdatedAt: site?.usdTryUpdatedAt ? site.usdTryUpdatedAt.toISOString() : null,\n standardUsdMonthly: std,\n goldUsdMonthly: gold,\n premiumPerBusinessUsdMonthly: prem,\n standardTryMonthly: tryMul(std),\n goldTryMonthly: tryMul(gold),\n premiumPerBusinessTryMonthly: tryMul(prem),\n businessMembershipTryDaily: 10,\n businessMembershipTryAnnual: 3650,\n /** Tek işletme için tahmini Premium aylık (Gold + 1× işletme premium USD), TRY */\n premiumBundleTryMonthlyOneBusiness: rate != null ? Math.round((gold + prem) * rate * 100) / 100 : null,\n };\n}\n\n/** Panel oturumu: istemci `x-vendor-email` ile hem yetkili hem işletme e-postası doğrulanır */\nfunction sqlVendorSessionEmailMatches(headerEmail: string) {\n const e = String(headerEmail).trim();\n return sql`(\n LOWER(TRIM(COALESCE(owner_email, ''))) = LOWER(TRIM(${e}))\n OR LOWER(TRIM(COALESCE(email, ''))) = LOWER(TRIM(${e}))\n )`;\n}\n\n/** `notes` içinde satır bazlı `anahtar: değer` güncelle (ör. qr_menu_public:1) */\nfunction mergeVendorNoteLine(notes: string | null | undefined, key: string, value: string): string {\n const lines = String(notes ?? \"\")\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter(Boolean);\n const prefix = `${key.toLowerCase()}:`;\n const kept = lines.filter((l) => !l.toLowerCase().startsWith(prefix));\n kept.push(`${key}: ${value}`);\n return kept.join(\"\\n\");\n}\n\nfunction vendorNoteFlagOn(notes: string | null | undefined, key: string): boolean {\n const raw = String(notes ?? \"\");\n const esc = key.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const re = new RegExp(`(?:^|\\n)${esc}\\\\s*:\\\\s*(\\\\S+)`, \"i\");\n const m = raw.match(re);\n const v = (m?.[1] ?? \"\").toLowerCase();\n return v === \"1\" || v === \"true\" || v === \"on\" || v === \"yes\";\n}\n\nconst r = (res: unknown): Row[] => ((res as { rows?: Row[] }).rows ?? res) as Row[];\n\ntype VendorSession = {\n id: number;\n email: string;\n};\n\ntype VendorJwtPayload = jwt.JwtPayload & {\n typ?: string;\n vendorId?: number;\n email?: string;\n};\n\ntype CourierSession = {\n id: number;\n phone: string;\n};\n\ntype CourierJwtPayload = jwt.JwtPayload & {\n typ?: string;\n courierId?: number;\n phone?: string;\n};\n\nfunction signVendorSessionToken(vendorId: number, email: string): string {\n return jwt.sign(\n { typ: \"vendor\", vendorId, email },\n getSessionSecret(),\n { expiresIn: \"30d\", audience: \"yekpare:vendor\", issuer: \"yekpare-api\", subject: String(vendorId) },\n );\n}\n\nfunction verifyVendorSessionToken(req: Request): VendorSession | null {\n const auth = String(req.headers.authorization ?? \"\").trim();\n const token = auth.toLowerCase().startsWith(\"bearer \") ? auth.slice(7).trim() : \"\";\n if (!token) return null;\n try {\n const decoded = jwt.verify(token, getSessionSecret(), {\n audience: \"yekpare:vendor\",\n issuer: \"yekpare-api\",\n }) as VendorJwtPayload;\n const id = Number(decoded.vendorId ?? decoded.sub);\n const email = String(decoded.email ?? \"\").trim();\n if (decoded.typ !== \"vendor\" || !Number.isFinite(id) || id <= 0 || !email) return null;\n return { id, email };\n } catch {\n return null;\n }\n}\n\nfunction requireVendorSession(req: Request, res: Response, next: NextFunction): void {\n const path = (req.path || \"\").replace(/^\\/providers\\/?/, \"/\");\n if (path === \"/geliver/webhook\") {\n next();\n return;\n }\n const session = verifyVendorSessionToken(req);\n if (!session) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n req.headers[\"x-vendor-id\"] = String(session.id);\n req.headers[\"x-vendor-email\"] = session.email;\n next();\n}\n\nfunction signCourierSessionToken(courierId: number, phone: string): string {\n return jwt.sign(\n { typ: \"courier\", courierId, phone },\n getSessionSecret(),\n { expiresIn: \"30d\", audience: \"yekpare:courier\", issuer: \"yekpare-api\", subject: String(courierId) },\n );\n}\n\nfunction verifyCourierSessionToken(req: Request): CourierSession | null {\n const auth = String(req.headers.authorization ?? \"\").trim();\n const token = auth.toLowerCase().startsWith(\"bearer \") ? auth.slice(7).trim() : \"\";\n if (!token) return null;\n try {\n const decoded = jwt.verify(token, getSessionSecret(), {\n audience: \"yekpare:courier\",\n issuer: \"yekpare-api\",\n }) as CourierJwtPayload;\n const id = Number(decoded.courierId ?? decoded.sub);\n const phone = String(decoded.phone ?? \"\").trim();\n if (decoded.typ !== \"courier\" || !Number.isFinite(id) || id <= 0 || !phone) return null;\n return { id, phone };\n } catch {\n return null;\n }\n}\n\nfunction requireCourierSession(req: Request, res: Response, next: NextFunction): void {\n const session = verifyCourierSessionToken(req);\n if (!session) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n req.headers[\"x-courier-id\"] = String(session.id);\n req.headers[\"x-courier-phone\"] = session.phone;\n next();\n}\n\nfunction courierPhoneFromSession(req: Request): string {\n return String(req.headers[\"x-courier-phone\"] ?? \"\").trim();\n}\n\nasync function vendorSessionOwnsRow(vendorId: unknown, vendorEmail: unknown): Promise {\n if (!vendorId || !vendorEmail) return false;\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n return Boolean(own);\n}\n\n/* — Yardımcı: slug üret — */\nfunction slugify(str: string): string {\n return str.toLowerCase()\n .replace(/ğ/g, \"g\").replace(/ü/g, \"u\").replace(/ş/g, \"s\")\n .replace(/ı/g, \"i\").replace(/ö/g, \"o\").replace(/ç/g, \"c\")\n .replace(/[^a-z0-9]+/g, \"-\").replace(/^-|-$/g, \"\");\n}\n\nfunction noteField(notes: string | null | undefined, key: string): string {\n const raw = String(notes || \"\");\n const m = raw.match(new RegExp(`${key}:([^|\\\\n]+)`));\n return m?.[1]?.trim() || \"\";\n}\n\nfunction upsertNoteField(notes: string | null | undefined, key: string, value: string | null | undefined): string {\n const raw = String(notes || \"\");\n const parts = raw.split(\"|\").map((x) => x.trim()).filter(Boolean);\n const filtered = parts.filter((p) => !p.toLowerCase().startsWith(`${key.toLowerCase()}:`));\n const v = String(value || \"\").trim();\n if (v) filtered.push(`${key}:${v}`);\n return filtered.join(\" | \");\n}\n\nfunction maskSecret(v: string): string {\n if (!v) return \"\";\n if (v.length <= 8) return \"****\";\n return `${v.slice(0, 4)}...${v.slice(-4)}`;\n}\n\nexport async function importMenuIntoVendor(vendorId: number, menu: Array<{ name: string; category?: string | null; description?: string | null; price?: number | null; imageUrl?: string | null; options?: Array<{ name: string; required?: boolean; multiple?: boolean; choices: Array<{ name: string; price?: number }> }> }>) {\n const catMap = new Map();\n let categories = 0;\n let items = 0;\n let options = 0;\n for (const m of menu) {\n const catName = String(m.category || \"Diğer\").trim() || \"Diğer\";\n const catKey = catName.toLowerCase();\n let catId = catMap.get(catKey);\n if (!catId) {\n const q = r(await db.execute(sql`\n INSERT INTO vendor_menu_categories (vendor_id, name, position, active)\n VALUES (${vendorId}, ${catName}, 999, true)\n ON CONFLICT DO NOTHING\n RETURNING id\n `));\n if (q[0]?.id) catId = Number(q[0].id);\n if (!catId) {\n const ex = r(await db.execute(sql`\n SELECT id FROM vendor_menu_categories\n WHERE vendor_id = ${vendorId} AND LOWER(name) = LOWER(${catName})\n LIMIT 1\n `));\n if (ex[0]?.id) catId = Number(ex[0].id);\n }\n if (catId) {\n catMap.set(catKey, catId);\n categories++;\n }\n }\n if (!catId || !m.name) continue;\n const ins = r(await db.execute(sql`\n INSERT INTO vendor_menu_items (vendor_id, menu_category_id, name, description, price, image_url, active)\n VALUES (${vendorId}, ${catId}, ${m.name}, ${m.description || null}, ${m.price != null ? String(m.price) : \"0\"}, ${m.imageUrl || null}, true)\n ON CONFLICT DO NOTHING\n RETURNING id\n `));\n let itemId: number | null = ins[0]?.id ? Number(ins[0].id) : null;\n if (!itemId) {\n const ex = r(await db.execute(sql`\n SELECT id, menu_category_id, image_url FROM vendor_menu_items\n WHERE vendor_id = ${vendorId} AND LOWER(name) = LOWER(${m.name})\n LIMIT 1\n `));\n if (ex[0]?.id) {\n itemId = Number(ex[0].id);\n const img = String(m.imageUrl ?? \"\").trim() || null;\n await db.execute(sql`\n UPDATE vendor_menu_items SET\n menu_category_id = ${catId},\n description = COALESCE(${m.description || null}, description),\n price = COALESCE(${m.price != null ? String(m.price) : null}, price),\n image_url = CASE WHEN ${img} IS NOT NULL AND ${img} <> '' THEN ${img} ELSE image_url END,\n updated_at = now()\n WHERE id = ${itemId} AND vendor_id = ${vendorId}\n `);\n }\n }\n if (itemId) items++;\n if (itemId && m.options?.length) {\n for (const og of m.options) {\n if (!og.name || !og.choices?.length) continue;\n await db.execute(sql`\n INSERT INTO vendor_item_options (menu_item_id, name, required, multiple, choices)\n VALUES (\n ${itemId},\n ${og.name},\n ${Boolean(og.required)},\n ${Boolean(og.multiple)},\n ${JSON.stringify(og.choices)}::jsonb\n )\n `);\n options++;\n }\n }\n }\n return { categories, items, options };\n}\n\ntype ExternalMenuImportInput = {\n sourceUrl?: string;\n menu?: ImportedMenuItem[];\n selectedCategories?: string[];\n city?: string;\n district?: string;\n neighborhood?: string;\n address?: string;\n phone?: string;\n lat?: number | null;\n lng?: number | null;\n rating?: number | null;\n reviewCount?: number | null;\n platform?: ExternalPlatform | string;\n name?: string;\n prefetchedHtml?: string;\n prefetchedMenuJson?: unknown;\n};\n\nfunction pickPrefetchedHtml(body: Record | undefined): string | undefined {\n const html = String(body?.prefetchedHtml ?? \"\").trim();\n if (html.length > 400) return html;\n if (html.startsWith(\"{\") && /menu_categories|productCategories|menuCategories/i.test(html)) return html;\n return undefined;\n}\n\nfunction pickPrefetchedMenuJson(body: Record | undefined): unknown {\n const raw = body?.prefetchedMenuJson;\n if (raw && typeof raw === \"object\") return raw;\n if (typeof raw === \"string\" && raw.trim().startsWith(\"{\")) {\n try {\n return JSON.parse(raw);\n } catch {\n return undefined;\n }\n }\n return undefined;\n}\n\nexport async function runExternalMenuImportForVendor(vendorId: number, input: ExternalMenuImportInput) {\n let imported: ImportedVendorData;\n const prefetchedHtml = String(input.prefetchedHtml ?? \"\").trim();\n const hasPrefetchedHtml =\n prefetchedHtml.length > 400 ||\n (prefetchedHtml.startsWith(\"{\") && /menu_categories|productCategories|menuCategories/i.test(prefetchedHtml));\n\n if (hasPrefetchedHtml) {\n const sourceUrl = String(input.sourceUrl || \"\").trim();\n if (!sourceUrl) throw new Error(\"sourceUrl zorunlu\");\n imported = await importVendorFromExternalUrl(sourceUrl, {\n prefetchedHtml,\n prefetchedMenuJson: input.prefetchedMenuJson,\n });\n } else if (Array.isArray(input.menu) && input.menu.length > 0) {\n imported = {\n platform: (input.platform as ExternalPlatform) || \"unknown\",\n sourceUrl: String(input.sourceUrl || \"\").trim(),\n name: String(input.name || \"\").trim(),\n description: null,\n phone: input.phone ?? null,\n address: input.address ?? null,\n city: input.city ?? null,\n district: input.district ?? null,\n neighborhood: input.neighborhood ?? null,\n lat: input.lat ?? null,\n lng: input.lng ?? null,\n rating: input.rating ?? null,\n reviewCount: input.reviewCount ?? null,\n isOpen: true,\n menu: input.menu,\n };\n } else {\n const sourceUrl = String(input.sourceUrl || \"\").trim();\n if (!sourceUrl) throw new Error(\"sourceUrl zorunlu\");\n imported = await importVendorFromExternalUrl(sourceUrl, {\n prefetchedHtml: input.prefetchedHtml,\n prefetchedMenuJson: input.prefetchedMenuJson,\n });\n }\n const menuToImport = filterMenuSelection(imported.menu, input.selectedCategories);\n const [vendor] = r(await db.execute(sql`SELECT id, notes FROM vendors WHERE id = ${vendorId} LIMIT 1`));\n if (!vendor) throw new Error(\"İşletme bulunamadı\");\n const neighborhood = input.neighborhood || imported.neighborhood || null;\n const notes = [\n String(vendor.notes ?? \"\"),\n `source_platform:${imported.platform}`,\n imported.sourceUrl ? `source_url:${imported.sourceUrl}` : \"\",\n imported.sourceId ? `source_id:${imported.sourceId}` : \"\",\n neighborhood ? `neighborhood:${neighborhood}` : \"\",\n ]\n .filter(Boolean)\n .join(\" | \");\n const ratingVal =\n imported.rating != null && imported.rating > 0 && imported.rating <= 5 ? imported.rating : null;\n const reviewCountVal =\n imported.reviewCount != null && imported.reviewCount >= 0 ? Math.round(imported.reviewCount) : null;\n const mergedAddress = buildMergedAddress({\n address: input.address || imported.address,\n neighborhood,\n });\n await db.execute(sql`\n UPDATE vendors SET\n notes = ${notes},\n working_hours = COALESCE(${imported.workingHours || null}, working_hours),\n image_url = COALESCE(${imported.imageUrl || null}, image_url),\n cover_url = COALESCE(${imported.coverUrl || null}, cover_url),\n phone = COALESCE(${input.phone || imported.phone || null}, phone),\n address = COALESCE(${mergedAddress || null}, address),\n city = COALESCE(${input.city || imported.city || null}, city),\n district = COALESCE(${input.district || imported.district || null}, district),\n lat = COALESCE(${Number.isFinite(Number(input.lat)) ? Number(input.lat) : (imported.lat ?? null)}, lat),\n lng = COALESCE(${Number.isFinite(Number(input.lng)) ? Number(input.lng) : (imported.lng ?? null)}, lng),\n rating = COALESCE(${ratingVal}, rating),\n review_count = COALESCE(${reviewCountVal}, review_count),\n updated_at = now()\n WHERE id = ${vendorId}\n `);\n const menuStats = await importMenuIntoVendor(vendorId, menuToImport);\n return { imported, menuStats, menuToImport, preview: buildExternalMenuPreview({ ...imported, menu: menuToImport }) };\n}\n\nfunction sanitizeRegistrationHost(raw: unknown): string | null {\n const s = String(raw ?? \"\")\n .trim()\n .toLowerCase();\n if (!s || s.length > 200) return null;\n if (!/^[a-z0-9.-]+$/.test(s)) return null;\n return s;\n}\n\n/* ────────────────────────────────────────────────────────\n PUBLIC — Başvuru gönder\n POST /api/providers/apply\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/apply\", async (req, res): Promise => {\n const {\n businessName, providerType, providerSubtype,\n ownerName, ownerEmail, phone, address, city, district,\n lat, lng, description, docKimlik, docVergi, docImza,\n revenueModel: revenueModelRaw, commissionRatePct: commissionRateRaw,\n registrationHost: registrationHostBody,\n } = req.body as Record;\n\n const registrationHost =\n sanitizeRegistrationHost(registrationHostBody) ??\n sanitizeRegistrationHost(\n typeof req.headers[\"x-forwarded-host\"] === \"string\"\n ? req.headers[\"x-forwarded-host\"].split(\",\")[0]?.trim()\n : undefined,\n ) ??\n sanitizeRegistrationHost(typeof req.headers.host === \"string\" ? req.headers.host.split(\":\")[0] : undefined);\n\n const registrationNote = registrationHost\n ? `Kayıt kaynağı: ${registrationHost} alan adından işletme başvurusu (Yekpare).`\n : null;\n\n if (!businessName || !providerType || !ownerName || !ownerEmail || !phone) {\n res.status(400).json({ error: \"Zorunlu alanlar eksik\" });\n return;\n }\n\n const businessNameStr = String(businessName).trim();\n const providerTypeStr = String(providerType).trim();\n\n const revenueModel = normalizeRevenueModel(revenueModelRaw ?? \"subscription\");\n let commissionRatePct: number | null = null;\n if (revenueModel === \"commission\") {\n const r = parseFloat(String(commissionRateRaw ?? \"\").replace(\",\", \".\"));\n if (!Number.isFinite(r) || r <= 0 || r > 100) {\n res.status(400).json({\n error: \"Komisyon modelinde geçerli bir komisyon oranı zorunludur (%1 ile %100 arası)\",\n });\n return;\n }\n commissionRatePct = r;\n }\n\n /* Benzersiz slug oluştur */\n let baseSlug = slugify(businessNameStr);\n if (!baseSlug) baseSlug = \"isletme\";\n const existing = r(await db.execute(sql`SELECT slug FROM vendors WHERE slug LIKE ${baseSlug + \"%\"} ORDER BY slug`));\n const slug = existing.length === 0 ? baseSlug : `${baseSlug}-${existing.length + 1}`;\n\n /* Vendor tipini belirle */\n const vendorType = providerTypeStr === \"siparis\" ? \"delivery\"\n : providerTypeStr === \"alisveris\" ? \"ecommerce\"\n : providerTypeStr;\n\n try {\n const rows = r(await db.execute(sql`\n INSERT INTO vendors (\n name, slug, description, vendor_type, phone, email, address, city, district,\n lat, lng, owner_name, owner_email, whatsapp,\n provider_type, provider_subtype, application_status,\n doc_kimlik, doc_vergi, doc_imza,\n revenue_model, commission_rate_pct,\n notes,\n active, is_open, featured, rating, review_count\n ) VALUES (\n ${businessNameStr}, ${slug}, ${description || null}, ${vendorType},\n ${phone}, ${ownerEmail}, ${address || null}, ${city || null}, ${district || null},\n ${lat ? Number(lat) : null}, ${lng ? Number(lng) : null},\n ${ownerName}, ${ownerEmail}, ${phone},\n ${providerTypeStr}, ${providerSubtype || null}, 'pending',\n ${docKimlik || null}, ${docVergi || null}, ${docImza || null},\n ${revenueModel}, ${commissionRatePct},\n ${registrationNote},\n false, false, false, 0, 0\n ) RETURNING id, slug, name\n `));\n\n const newVendor = rows[0];\n\n /* Admin'e WhatsApp bildirimi — fire and forget */\n notifyAdminWhatsApp({\n eventType: \"Yeni Servis Sağlayıcı Başvurusu\",\n details: [\n `🏢 ${businessNameStr}`,\n `👤 ${ownerName} (${ownerEmail})`,\n `📞 ${phone}`,\n `📍 ${city || \"\"} ${district || \"\"}`.trim(),\n `🏷️ ${PROVIDER_TYPE_LABELS[providerTypeStr] ?? providerTypeStr}`,\n revenueModel === \"commission\"\n ? `💳 Gelir: Komisyon (%${commissionRatePct})`\n : `💳 Gelir: Abonelik`,\n registrationNote ? `🌐 ${registrationNote}` : \"\",\n ].filter(Boolean).join(\"\\n\"),\n panelUrl: `https://turk.eco/admin/servis-saglayicilar`,\n }).catch(() => {});\n\n const welcomeWa = [\n `Yekpare — Merhaba ${ownerName},`,\n ``,\n `${businessName} başvurunuz alındı. Kaydınız başarıyla oluşturulmuştur.`,\n ``,\n `Hesabınızın aktifleşmesi için kimlik, vergi levhası ve imza sirküsü gibi işletme belgelerinizi eksikse tamamlayın; başvuru formunda yüklediyseniz yönetici onayını bekleyin.`,\n ``,\n `Teşekkürler — Yekpare`,\n ].join(\"\\n\");\n void trySendSiteOutboundWhatsApp(String(phone), welcomeWa).catch(() => {});\n\n res.json({ success: true, vendor: newVendor });\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n res.status(500).json({ error: \"Kayıt oluşturulamadı\", detail: msg });\n }\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — Tüm başvuruları listele (durum filtresi ile)\n GET /api/admin/providers?status=pending|approved|rejected&type=...\n──────────────────────────────────────────────────────── */\nrouter.get(\"/admin/providers\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { status, type, q } = req.query as Record;\n const statusFilter = typeof status === \"string\" && status.trim() ? status.trim() : null;\n const typeFilter = typeof type === \"string\" && type.trim() ? type.trim() : null;\n\n const rows = r(await db.execute(\n sql`\n SELECT\n v.id, v.name, v.slug, v.vendor_type, v.provider_type, v.provider_subtype,\n v.application_status, v.rejection_reason,\n v.owner_name, v.owner_email, v.phone, v.email,\n v.address, v.city, v.district, v.lat, v.lng,\n v.description, v.image_url, v.cover_url,\n v.active, v.is_open, v.featured, v.rating, v.review_count,\n v.doc_kimlik, v.doc_vergi, v.doc_imza, v.verified_at,\n v.notes, v.created_at, v.updated_at,\n v.revenue_model, v.commission_rate_pct,\n v.payout_bank_holder, v.payout_bank_iban, v.payout_bank_branch,\n v.geliver_api_token, v.geliver_sender_zip, v.geliver_sender_mahalle, v.geliver_auto_ship_on_order,\n (SELECT COUNT(*)::int FROM delivery_orders WHERE vendor_id = v.id) as order_count,\n (SELECT COUNT(*)::int FROM vendor_menu_items WHERE vendor_id = v.id) as product_count\n FROM vendors v\n WHERE (${statusFilter} IS NULL OR v.application_status = ${statusFilter})\n AND (${typeFilter} IS NULL OR v.provider_type = ${typeFilter} OR v.vendor_type = ${typeFilter})\n ORDER BY v.created_at DESC\n `\n ));\n\n const withGeliver = rows.map((raw) => {\n const v = raw as Row;\n const tok = String(v.geliver_api_token ?? \"\").trim();\n const { geliver_api_token: _t, ...rest } = v;\n return {\n ...rest,\n geliver_api_token_masked: tok ? maskSecret(tok) : \"\",\n geliver_configured: Boolean(tok),\n } as Row;\n });\n\n let result = withGeliver;\n if (q) {\n const lq = q.toLowerCase();\n result = withGeliver.filter((v) => {\n const id = Number((v as Row).id);\n return (\n String(v.name || \"\").toLowerCase().includes(lq) ||\n String(v.slug || \"\").toLowerCase().includes(lq) ||\n String(v.owner_name || \"\").toLowerCase().includes(lq) ||\n String(v.owner_email || \"\").toLowerCase().includes(lq) ||\n String(v.email || \"\").toLowerCase().includes(lq) ||\n String(v.city || \"\").toLowerCase().includes(lq) ||\n String(id).includes(lq)\n );\n });\n }\n\n res.json(result);\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — Onayla\n PUT /api/admin/providers/:id/approve\n──────────────────────────────────────────────────────── */\nrouter.put(\"/admin/providers/:id/approve\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n\n const tempPassword = vendorApprovalTempPassword();\n const passwordHash = await bcrypt.hash(tempPassword, 10);\n\n const rows = r(await db.execute(sql`\n UPDATE vendors\n SET application_status = 'approved', active = true, is_open = true,\n verified_at = NOW(), updated_at = NOW(),\n password_hash = ${passwordHash}\n WHERE id = ${id}\n RETURNING id, name, slug, lat, lng, image_url, city, district, address, phone,\n vendor_type, provider_type, provider_subtype, owner_name, owner_email\n `));\n\n const v = rows[0];\n if (!v) { res.status(404).json({ error: \"Bulunamadı\" }); return; }\n\n /* Haritada premium pin olarak ekle / güncelle */\n if (v.lat && v.lng) {\n await db.execute(sql`\n INSERT INTO map_businesses (\n name, address, city, district, phone, lat, lng,\n is_premium, is_active, vendor_id,\n photo_url, category_id, created_at\n ) VALUES (\n ${v.name}, ${v.address || \"\"}, ${v.city || \"Kırşehir\"},\n ${v.district || null}, ${v.phone || null},\n ${v.lat}, ${v.lng},\n true, true, ${id},\n ${v.image_url || null}, 1, NOW()\n )\n ON CONFLICT (vendor_id) DO UPDATE SET\n name = EXCLUDED.name, is_premium = true, is_active = true,\n lat = EXCLUDED.lat, lng = EXCLUDED.lng, updated_at = NOW()\n `).catch(() => {});\n }\n\n const baseUrl = sitePublicOrigin();\n\n /* Aktivasyon e-postası gönder */\n let emailSent = false;\n if (v.owner_email) {\n const mail = buildActivationEmail({\n vendorName: v.name as string,\n ownerName: (v.owner_name as string) || \"İşletme Sahibi\",\n email: v.owner_email as string,\n password: tempPassword,\n loginUrl: `${baseUrl}/servis-saglayici-giris`,\n });\n const result = await sendEmail({ to: v.owner_email as string, subject: mail.subject, html: mail.html, text: mail.text });\n emailSent = result.sent;\n }\n\n /* Vendor'a WhatsApp onay bildirimi — fire and forget */\n const waResult = await notifyVendorWhatsApp({\n vendorId: id,\n eventType: \"new_request\",\n details: [\n `🎉 *${v.name}* işletmenizin başvurusu onaylandı!`,\n `📧 Giriş e-postanız: ${v.owner_email || \"kayıtlı e-posta\"}`,\n `🔑 Varsayılan şifreniz: ${tempPassword}`,\n `⚠️ Güvenliğiniz için şifrenizi değiştirin.`,\n ].join(\"\\n\"),\n panelUrl: `${baseUrl}/servis-saglayici-giris`,\n });\n\n res.json({ success: true, vendor: v, emailSent, whatsapp: waResult });\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — İşletme şifresini sıfırla\n POST /api/admin/providers/:id/set-password\n──────────────────────────────────────────────────────── */\nrouter.post(\"/admin/providers/:id/set-password\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenanceAny(req, res, [\"servis_saglayicilar\", \"turizm\"])) return;\n const id = Number(req.params.id);\n const { password } = req.body as { password?: string };\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n if (!password || password.length < 4) { res.status(400).json({ error: \"Şifre en az 4 karakter olmalı\" }); return; }\n\n try {\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS password_hash text`);\n const hash = await bcrypt.hash(password, 10);\n await db.execute(sql`UPDATE vendors SET password_hash = ${hash}, updated_at = NOW() WHERE id = ${id}`);\n res.json({ success: true });\n } catch (e) {\n console.error(\"[set-password]\", e);\n res.status(500).json({ success: false, error: \"Şifre kaydedilemedi\", detail: String(e) });\n }\n});\n\nrouter.post(\"/admin/providers/:id/panel-session\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenanceAny(req, res, [\"servis_saglayicilar\", \"turizm\"])) return;\n const id = Number(req.params.id);\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, name, slug, vendor_type, provider_type, provider_subtype, owner_email, email, active, is_open\n FROM vendors\n WHERE id = ${id}\n LIMIT 1\n `));\n const v = rows[0];\n if (!v) { res.status(404).json({ error: \"İşletme bulunamadı\" }); return; }\n const loginEmail = String(v.owner_email ?? \"\").trim() || String(v.email ?? \"\").trim();\n if (!loginEmail) { res.status(400).json({ error: \"Bu işletmede giriş e-postası yok\" }); return; }\n attachProviderPanelRoute(v);\n const token = signVendorSessionToken(Number(v.id), loginEmail);\n res.json({\n success: true,\n token,\n vendor: {\n ...v,\n login_email: loginEmail,\n },\n session: {\n id: Number(v.id),\n email: loginEmail,\n name: String(v.name ?? \"\"),\n token,\n panelKind: v.panel_kind,\n panelPath: v.panel_route,\n },\n });\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — Reddet\n PUT /api/admin/providers/:id/reject\n──────────────────────────────────────────────────────── */\nrouter.put(\"/admin/providers/:id/reject\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const { reason } = req.body;\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n\n const rows = r(await db.execute(sql`\n UPDATE vendors\n SET application_status = 'rejected', active = false,\n rejection_reason = ${reason || null}, updated_at = NOW()\n WHERE id = ${id}\n RETURNING id, name\n `));\n\n const v = rows[0];\n\n /* Vendor'a WhatsApp red bildirimi — fire and forget */\n const waResult = await notifyVendorWhatsApp({\n vendorId: id,\n eventType: \"new_request\",\n details: [\n `❌ *${v?.name ?? \"İşletme\"}* başvurunuz incelendi.`,\n reason ? `📋 Sebep: ${reason}` : \"\",\n `Eksiklerinizi tamamlayıp yeniden başvurabilirsiniz.`,\n ].filter(Boolean).join(\"\\n\"),\n panelUrl: `https://turk.eco/isletme-basvuru`,\n });\n\n res.json({ success: true, vendor: v, whatsapp: waResult });\n});\n\n/* ────────────────────────────────────────────────────────\n ADMIN — Notlar güncelle\n PUT /api/admin/providers/:id/notes\n──────────────────────────────────────────────────────── */\nrouter.put(\"/admin/providers/:id/notes\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const { notes } = req.body;\n await db.execute(sql`UPDATE vendors SET notes = ${notes || null}, updated_at = NOW() WHERE id = ${id}`);\n res.json({ success: true });\n});\n\nrouter.patch(\"/admin/providers/:id/geliver-settings\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n await ensureGeliverIntegrationColumns();\n\n const body = req.body as Record;\n const geliverApiToken = body[\"geliverApiToken\"];\n const geliverSenderZip = body[\"geliverSenderZip\"];\n const geliverSenderMahalle = body[\"geliverSenderMahalle\"];\n const geliverAutoShipOnOrder = body[\"geliverAutoShipOnOrder\"];\n\n const curRows = r(await db.execute(sql`\n SELECT geliver_api_token, geliver_sender_zip, geliver_sender_address_id, geliver_auto_ship_on_order,\n geliver_sender_mahalle\n FROM vendors WHERE id = ${id} LIMIT 1\n `));\n const cur = curRows[0];\n if (!cur) { res.status(404).json({ error: \"Bulunamadı\" }); return; }\n\n let nextToken = cur.geliver_api_token != null ? String(cur.geliver_api_token) : null;\n if (geliverApiToken !== undefined) {\n const t = String(geliverApiToken ?? \"\").trim();\n nextToken = t.length ? t : null;\n }\n\n const prevZ = cur.geliver_sender_zip != null ? String(cur.geliver_sender_zip) : null;\n let nextZip = prevZ;\n if (geliverSenderZip !== undefined) {\n const z = String(geliverSenderZip ?? \"\").trim().replace(/\\D/g, \"\").slice(0, 5);\n nextZip = z.length ? z : null;\n }\n\n const prevM = normGeliverMahalle(cur.geliver_sender_mahalle);\n let nextM = prevM;\n if (geliverSenderMahalle !== undefined) {\n nextM = normGeliverMahalle(geliverSenderMahalle);\n }\n\n let nextSender = cur.geliver_sender_address_id != null ? String(cur.geliver_sender_address_id) : null;\n if (geliverApiToken !== undefined && !String(geliverApiToken ?? \"\").trim()) {\n nextSender = null;\n }\n\n const zipChanged = geliverSenderZip !== undefined && String(nextZip ?? \"\") !== String(prevZ ?? \"\");\n const mahalleChanged = geliverSenderMahalle !== undefined && String(nextM ?? \"\") !== String(prevM ?? \"\");\n if (zipChanged || mahalleChanged) nextSender = null;\n\n let nextAuto = Boolean(cur.geliver_auto_ship_on_order);\n if (geliverAutoShipOnOrder !== undefined) nextAuto = Boolean(geliverAutoShipOnOrder);\n\n await db.execute(sql`\n UPDATE vendors SET\n geliver_api_token = ${nextToken},\n geliver_sender_zip = ${nextZip},\n geliver_sender_address_id = ${nextSender},\n geliver_sender_mahalle = ${nextM},\n geliver_auto_ship_on_order = ${nextAuto},\n updated_at = NOW()\n WHERE id = ${id}\n `);\n\n res.json({\n success: true,\n geliver_api_token_masked: nextToken ? maskSecret(nextToken) : \"\",\n geliver_configured: Boolean(nextToken),\n geliver_sender_zip: nextZip,\n geliver_sender_mahalle: nextM,\n geliver_auto_ship_on_order: nextAuto,\n });\n});\n\nrouter.patch(\"/admin/providers/:id/revenue-model\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n if (!id) { res.status(400).json({ error: \"Geçersiz ID\" }); return; }\n const { revenueModel: rmRaw, commissionRatePct: crRaw } = req.body as Record;\n const model = normalizeRevenueModel(rmRaw ?? \"subscription\");\n let rate: string | null = null;\n if (model === \"commission\") {\n const r = parseFloat(String(crRaw ?? \"\").replace(\",\", \".\"));\n if (!Number.isFinite(r) || r <= 0 || r > 100) {\n res.status(400).json({ error: \"Komisyon oranı 1 ile 100 arasında olmalıdır\" });\n return;\n }\n rate = String(r);\n }\n await db.execute(sql`\n UPDATE vendors SET\n revenue_model = ${model},\n commission_rate_pct = ${rate},\n updated_at = NOW()\n WHERE id = ${id}\n `);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER AUTH — E-posta ile giriş\n POST /api/providers/forgot-password\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/forgot-password\", async (req, res): Promise => {\n const { email, whatsapp } = req.body as { email?: string; whatsapp?: string };\n const emailTrim = String(email ?? \"\").trim();\n const waTrim = String(whatsapp ?? \"\").trim();\n if (!emailTrim && !waTrim) {\n res.status(400).json({ error: \"E-posta veya WhatsApp numarası girin.\" });\n return;\n }\n\n let v: Row | undefined;\n if (emailTrim) {\n const rows = r(await db.execute(sql`\n SELECT id, name, owner_name, owner_email, email, whatsapp, phone, callmebot_key FROM vendors\n WHERE ${sqlVendorSessionEmailMatches(emailTrim)} AND application_status = 'approved'\n LIMIT 1\n `));\n v = rows[0];\n } else {\n const variants = phoneDigitsVariants(waTrim);\n if (variants.length === 0) {\n res.status(400).json({ error: \"Geçerli bir telefon numarası girin.\" });\n return;\n }\n const orParts = variants.flatMap((variant) => [\n sql`regexp_replace(COALESCE(v.whatsapp,''), '[^0-9]', '', 'g') = ${variant}`,\n sql`regexp_replace(COALESCE(v.phone,''), '[^0-9]', '', 'g') = ${variant}`,\n ]);\n const orClause = sql.join(orParts, sql` OR `);\n const rows = r(await db.execute(sql`\n SELECT id, name, owner_name, owner_email, email, whatsapp, phone, callmebot_key FROM vendors v\n WHERE v.application_status = 'approved' AND (${orClause})\n ORDER BY v.id DESC\n LIMIT 1\n `));\n v = rows[0];\n }\n\n /* Güvenlik: hesap yoksa da success döndür */\n if (!v) {\n res.json({ success: true });\n return;\n }\n\n const token = crypto.randomBytes(32).toString(\"hex\");\n const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString();\n await db.execute(sql`\n UPDATE vendors SET pw_reset_token = ${token}, pw_reset_expires_at = ${expiresAt}\n WHERE id = ${Number(v.id)}\n `);\n\n const baseUrl = sitePublicOrigin();\n const resetUrl = `${baseUrl}/sifre-yenile?token=${token}`;\n\n let emailSent = false;\n const mail = buildPasswordResetEmail({\n ownerName: (v.owner_name as string) || \"İşletme Sahibi\",\n vendorName: v.name as string,\n resetUrl,\n });\n const toAddr = String(v.owner_email || v.email || \"\").trim();\n if (toAddr) {\n const result = await sendEmail({ to: toAddr, subject: mail.subject, html: mail.html, text: mail.text });\n emailSent = result.sent;\n }\n\n const waMsg = `Yekpare — Şifre sıfırlama\\n\\nBağlantıyı açın (yaklaşık 30 dk):\\n${resetUrl}\\n\\nBu isteği siz yapmadıysanız bu mesajı yok sayın.`;\n const waKey = String(v.callmebot_key ?? \"\").trim();\n const destRaw = String(v.whatsapp ?? v.phone ?? \"\").trim();\n let whatsappSent = false;\n if (destRaw) {\n if (waKey) {\n const waPhone = digitsForWhatsAppLink(destRaw);\n if (waPhone) {\n const u = `https://api.callmebot.com/whatsapp.php?phone=${encodeURIComponent(waPhone)}&text=${encodeURIComponent(waMsg)}&apikey=${encodeURIComponent(waKey)}`;\n try {\n const wr = await fetch(u, { signal: AbortSignal.timeout(8000) });\n whatsappSent = wr.ok;\n } catch {\n whatsappSent = false;\n }\n }\n }\n if (!whatsappSent) {\n const r2 = await trySendSiteOutboundWhatsApp(destRaw, waMsg);\n whatsappSent = r2.sent;\n }\n }\n\n res.json({\n success: true,\n emailSent,\n whatsappSent,\n ...(process.env[\"NODE_ENV\"] !== \"production\" ? { _devLink: resetUrl } : {}),\n });\n});\n\n/* ────────────────────────────────────────────────────────\n POST /api/providers/reset-password\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/reset-password\", async (req, res): Promise => {\n const { token, password } = req.body as { token?: string; password?: string };\n if (!token || !password) { res.status(400).json({ error: \"Token ve şifre gerekli\" }); return; }\n if (password.length < 6) { res.status(400).json({ error: \"Şifre en az 6 karakter olmalı\" }); return; }\n\n const rows = r(await db.execute(sql`\n SELECT id, pw_reset_expires_at FROM vendors WHERE pw_reset_token = ${token} LIMIT 1\n `));\n const v = rows[0];\n if (!v) { res.status(400).json({ error: \"Geçersiz sıfırlama linki.\" }); return; }\n if (!v.pw_reset_expires_at || new Date(v.pw_reset_expires_at as string) < new Date()) {\n res.status(400).json({ error: \"Sıfırlama linkinin süresi dolmuş. Lütfen tekrar talep edin.\" }); return;\n }\n\n const hash = await bcrypt.hash(password, 10);\n await db.execute(sql`\n UPDATE vendors SET password_hash = ${hash}, pw_reset_token = NULL, pw_reset_expires_at = NULL\n WHERE id = ${Number(v.id)}\n `);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n POST /api/providers/login\n Body: { email, password }\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/login\", async (req, res): Promise => {\n const { email, password, captchaToken, captchaAnswer } = req.body as {\n email?: string;\n password?: string;\n captchaToken?: string;\n captchaAnswer?: string;\n };\n if (!verifyLoginMathCaptcha(String(captchaToken ?? \"\"), String(captchaAnswer ?? \"\"))) {\n res.status(400).json({ error: \"Güvenlik doğrulaması hatalı veya süresi doldu.\" });\n return;\n }\n const emailTrim = String(email ?? \"\").trim();\n if (!emailTrim) { res.status(400).json({ error: \"E-posta gerekli\" }); return; }\n if (!password) { res.status(400).json({ error: \"Şifre gerekli\" }); return; }\n\n /** Aynı e-postayla birden fazla `vendors` satırı varsa `created_at` yerine `updated_at` ile seç; admin şifre sıfırlaması güncellenen satırda kalır. */\n const rows = r(await db.execute(sql`\n SELECT id, name, slug, vendor_type, provider_type, provider_subtype,\n application_status, rejection_reason, owner_name, owner_email,\n email,\n phone, city, active, image_url, cover_url, verified_at, notes, password_hash,\n linked_map_business_id, membership_tier\n FROM vendors\n WHERE ${sqlVendorSessionEmailMatches(emailTrim)}\n ORDER BY updated_at DESC NULLS LAST, id DESC\n LIMIT 1\n `));\n\n const v = rows[0];\n if (!v) {\n res.status(404).json({ error: \"Bu e-posta ile kayıtlı işletme bulunamadı.\" });\n return;\n }\n\n const storedHash = v.password_hash as string | null;\n if (!storedHash) {\n res.status(403).json({ error: \"Bu hesap için şifre tanımlanmamış. Lütfen yönetici ile iletişime geçin.\" });\n return;\n }\n const passwordOk = await bcrypt.compare(String(password), storedHash);\n if (!passwordOk) {\n res.status(401).json({ error: \"Şifre hatalı.\" });\n return;\n }\n\n const loginEmail =\n String(v.owner_email ?? \"\").trim() ||\n String(v.email ?? \"\").trim() ||\n String(email).trim();\n delete v.password_hash;\n (v as Record).login_email = loginEmail;\n attachProviderPanelRoute(v);\n\n const token = signVendorSessionToken(Number(v.id), loginEmail);\n res.json({ success: true, token, vendor: v });\n});\n\nrouter.use(\"/providers\", requireVendorSession);\n\n/* ────────────────────────────────────────────────────────\n PROVIDER AUTH — Mevcut oturumu doğrula\n GET /api/providers/me\n Header: Authorization: Bearer \n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/me\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n\n const rows = r(await db.execute(sql`\n SELECT id, name, slug, vendor_type, provider_type, provider_subtype,\n application_status, rejection_reason, owner_name, owner_email,\n phone, email, address, city, district, lat, lng, description, about_html,\n image_url, cover_url, whatsapp, working_hours,\n active, is_open, featured, rating, review_count,\n doc_kimlik, doc_vergi, doc_imza, verified_at, notes,\n revenue_model, commission_rate_pct,\n payout_bank_holder, payout_bank_iban, payout_bank_branch,\n geliver_api_token, geliver_sender_address_id, geliver_sender_zip, geliver_sender_mahalle, geliver_auto_ship_on_order,\n geliver_organization_id,\n paytr_merchant_id, paytr_merchant_key, paytr_merchant_salt, paytr_test_mode,\n iyzico_api_key, iyzico_secret_key, iyzico_sandbox, preferred_tr_gateway,\n linked_map_business_id, membership_tier, about_html,\n theme_key, theme_config,\n created_at, updated_at\n FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `));\n\n const v = rows[0];\n if (!v) { res.status(401).json({ error: \"Geçersiz oturum\" }); return; }\n\n const iyzicoConfigured = Boolean(String(v.iyzico_api_key ?? \"\").trim());\n const paytrMerchantIdRaw = String(v.paytr_merchant_id ?? \"\").trim();\n const paytrConfigured = Boolean(\n paytrMerchantIdRaw && String(v.paytr_merchant_key ?? \"\").trim() && String(v.paytr_merchant_salt ?? \"\").trim(),\n );\n delete v.paytr_merchant_id;\n delete v.paytr_merchant_key;\n delete v.paytr_merchant_salt;\n delete v.iyzico_api_key;\n delete v.iyzico_secret_key;\n (v as Record).paytr_merchant_id_masked = paytrMerchantIdRaw ? `****${paytrMerchantIdRaw.slice(-4)}` : \"\";\n (v as Record).paytr_configured = paytrConfigured;\n (v as Record).iyzico_configured = iyzicoConfigured;\n\n const geliverTok = String(v.geliver_api_token ?? \"\").trim();\n delete v.geliver_api_token;\n (v as Record).geliver_api_token_masked = geliverTok ? maskSecret(geliverTok) : \"\";\n\n await ensureVendorThemeColumns();\n /* Sipariş sayısı ve ürün sayısı */\n const stats = r(await db.execute(sql`\n SELECT\n (SELECT COUNT(*)::int FROM delivery_orders WHERE vendor_id = ${Number(vendorId)}) as order_count,\n (SELECT COUNT(*)::int FROM vendor_menu_items WHERE vendor_id = ${Number(vendorId)}) as product_count,\n (SELECT COUNT(*)::int FROM delivery_orders WHERE vendor_id = ${Number(vendorId)} AND status = 'pending') as pending_orders\n `));\n\n const [siteRow] = await db.select().from(siteSettingsTable).limit(1);\n (v as Record).membership_pricing = membershipPricingFromSiteRow(siteRow);\n\n await ensureVendorCustomDomainsTable();\n (v as Record).custom_domains = await listVendorDomains(Number(vendorId));\n attachProviderPanelRoute(v);\n\n res.json({ success: true, vendor: { ...v, ...(stats[0] || {}) } });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Profil güncelle\n PUT /api/providers/profile\n──────────────────────────────────────────────────────── */\nrouter.put(\"/providers/profile\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n\n const {\n name,\n phone,\n address,\n city,\n district,\n description,\n aboutHtml,\n whatsapp,\n workingHours,\n callmebotKey,\n imageUrl,\n coverUrl,\n contactEmail,\n ownerEmail,\n } = req.body as Record;\n\n const hasImage = Object.prototype.hasOwnProperty.call(req.body, \"imageUrl\");\n const hasCover = Object.prototype.hasOwnProperty.call(req.body, \"coverUrl\");\n const hasContact = Object.prototype.hasOwnProperty.call(req.body, \"contactEmail\");\n const hasOwnerEm = Object.prototype.hasOwnProperty.call(req.body, \"ownerEmail\");\n const hasAboutHtml = Object.prototype.hasOwnProperty.call(req.body, \"aboutHtml\");\n const imageVal = hasImage\n ? (imageUrl == null ? null : String(imageUrl).trim() || null)\n : undefined;\n const coverVal = hasCover ? (coverUrl == null ? null : String(coverUrl).trim() || null) : undefined;\n const emailVal = hasContact ? (contactEmail == null ? null : String(contactEmail).trim() || null) : undefined;\n const ownerEmVal = hasOwnerEm ? (ownerEmail == null ? null : String(ownerEmail).trim() || null) : undefined;\n const aboutHtmlVal = hasAboutHtml\n ? aboutHtml == null\n ? null\n : String(aboutHtml).trim().slice(0, 60_000) || null\n : undefined;\n\n const rows = r(await db.execute(sql`\n UPDATE vendors\n SET\n name = COALESCE(${name || null}, name),\n phone = COALESCE(${phone || null}, phone),\n address = COALESCE(${address || null}, address),\n city = COALESCE(${city || null}, city),\n district = COALESCE(${district || null}, district),\n description = COALESCE(${description || null}, description),\n about_html = CASE WHEN ${hasAboutHtml}::boolean THEN ${hasAboutHtml ? aboutHtmlVal : null} ELSE about_html END,\n whatsapp = COALESCE(${whatsapp || null}, whatsapp),\n callmebot_key = COALESCE(${callmebotKey !== undefined ? (callmebotKey || null) : null}, callmebot_key),\n working_hours = COALESCE(${workingHours ? JSON.stringify(workingHours) : null}, working_hours),\n image_url = CASE WHEN ${hasImage}::boolean THEN ${hasImage ? imageVal : null} ELSE image_url END,\n cover_url = CASE WHEN ${hasCover}::boolean THEN ${hasCover ? coverVal : null} ELSE cover_url END,\n email = CASE WHEN ${hasContact}::boolean THEN ${hasContact ? emailVal : null} ELSE email END,\n owner_email = CASE WHEN ${hasOwnerEm}::boolean THEN ${hasOwnerEm ? ownerEmVal : null} ELSE owner_email END,\n updated_at = NOW()\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n RETURNING id, name, phone, address, city, district, description, about_html, whatsapp, callmebot_key, working_hours,\n image_url, cover_url, email, owner_email\n `));\n\n if (!rows[0]) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n res.json({ success: true, vendor: rows[0] });\n});\n\nrouter.put(\"/providers/payout-bank\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n\n const { payoutBankHolder, payoutBankIban, payoutBankBranch } = req.body as Record;\n const holder = String(payoutBankHolder ?? \"\").trim();\n const ibanRaw = sanitizeTrIban(String(payoutBankIban ?? \"\"));\n const branch = String(payoutBankBranch ?? \"\").trim() || null;\n\n if (!holder || !ibanRaw) {\n res.status(400).json({ error: \"Hesap sahibi adı ve IBAN zorunludur\" });\n return;\n }\n\n const check = r(await db.execute(sql`\n SELECT id, revenue_model FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `))[0];\n if (!check) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n if (String(check.revenue_model ?? \"subscription\") !== \"commission\") {\n res.status(400).json({ error: \"Banka bilgisi yalnızca komisyon modelindeki işletmeler için kullanılır\" });\n return;\n }\n\n await db.execute(sql`\n UPDATE vendors SET\n payout_bank_holder = ${holder},\n payout_bank_iban = ${ibanRaw},\n payout_bank_branch = ${branch},\n updated_at = NOW()\n WHERE id = ${Number(vendorId)}\n `);\n\n res.json({ success: true });\n});\n\nrouter.put(\"/providers/geliver-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n await ensureGeliverIntegrationColumns();\n\n const body = req.body as Record;\n const geliverApiToken = body[\"geliverApiToken\"];\n const geliverSenderZip = body[\"geliverSenderZip\"];\n const geliverSenderMahalle = body[\"geliverSenderMahalle\"];\n const geliverSenderAddressId = body[\"geliverSenderAddressId\"];\n const geliverOrganizationId = body[\"geliverOrganizationId\"];\n const geliverAutoShipOnOrder = body[\"geliverAutoShipOnOrder\"];\n\n const rows = r(await db.execute(sql`\n SELECT id, geliver_api_token, geliver_sender_zip, geliver_sender_address_id, geliver_auto_ship_on_order,\n geliver_sender_mahalle, geliver_organization_id\n FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `));\n const cur = rows[0];\n if (!cur) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n let nextToken = cur.geliver_api_token != null ? String(cur.geliver_api_token) : null;\n if (geliverApiToken !== undefined) {\n const t = String(geliverApiToken ?? \"\").trim();\n nextToken = t.length ? t : null;\n }\n\n const prevZ = cur.geliver_sender_zip != null ? String(cur.geliver_sender_zip) : null;\n let nextZip = prevZ;\n if (geliverSenderZip !== undefined) {\n const z = String(geliverSenderZip ?? \"\").trim().replace(/\\D/g, \"\").slice(0, 5);\n nextZip = z.length ? z : null;\n }\n\n const prevM = normGeliverMahalle(cur.geliver_sender_mahalle);\n let nextM = prevM;\n if (geliverSenderMahalle !== undefined) {\n nextM = normGeliverMahalle(geliverSenderMahalle);\n }\n\n let nextSender = cur.geliver_sender_address_id != null ? String(cur.geliver_sender_address_id) : null;\n if (geliverSenderAddressId !== undefined) {\n const s = String(geliverSenderAddressId ?? \"\").trim();\n nextSender = s.length ? s : null;\n }\n\n const zipChanged = geliverSenderZip !== undefined && String(nextZip ?? \"\") !== String(prevZ ?? \"\");\n const mahalleChanged = geliverSenderMahalle !== undefined && String(nextM ?? \"\") !== String(prevM ?? \"\");\n if (zipChanged || mahalleChanged) nextSender = null;\n\n const prevOrg = cur.geliver_organization_id != null ? String(cur.geliver_organization_id) : null;\n let nextOrg = prevOrg;\n if (geliverOrganizationId !== undefined) {\n const o = String(geliverOrganizationId ?? \"\").trim();\n nextOrg = o.length ? o : null;\n }\n\n if (geliverApiToken !== undefined && !String(geliverApiToken ?? \"\").trim()) {\n nextSender = null;\n nextOrg = null;\n }\n\n let nextAuto = Boolean(cur.geliver_auto_ship_on_order);\n if (geliverAutoShipOnOrder !== undefined) nextAuto = Boolean(geliverAutoShipOnOrder);\n\n await db.execute(sql`\n UPDATE vendors SET\n geliver_api_token = ${nextToken},\n geliver_sender_zip = ${nextZip},\n geliver_sender_address_id = ${nextSender},\n geliver_sender_mahalle = ${nextM},\n geliver_organization_id = ${nextOrg},\n geliver_auto_ship_on_order = ${nextAuto},\n updated_at = NOW()\n WHERE id = ${Number(vendorId)}\n `);\n\n res.json({\n success: true,\n geliverApiTokenMasked: nextToken ? maskSecret(nextToken) : \"\",\n geliverSenderZip: nextZip,\n geliverSenderMahalle: nextM,\n geliverSenderAddressId: nextSender,\n geliverOrganizationId: nextOrg,\n geliverAutoShipOnOrder: nextAuto,\n });\n});\n\n/**\n * POST /api/providers/geliver/manual-shipment\n * Siparişe bağlı olmayan manuel gönderi: yalnızca teklifler (etiket Geliver panelinde).\n */\nrouter.post(\"/providers/geliver/manual-shipment\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n await ensureGeliverIntegrationColumns();\n\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const body = req.body as Record;\n const rec = body.recipient as Record | undefined;\n if (!rec || typeof rec !== \"object\") {\n res.status(400).json({ error: \"Alıcı bilgisi (recipient) gerekli.\" });\n return;\n }\n const name = String(rec.name ?? \"\").trim();\n const phone = String(rec.phone ?? \"\").trim();\n const address1 = String(rec.address1 ?? \"\").trim();\n const city = String(rec.city ?? \"\").trim();\n const district = String(rec.district ?? \"\").trim();\n const neighborhood = String(rec.neighborhood ?? rec.mahalle ?? \"\").trim();\n if (!name || !phone || !address1 || !city) {\n res.status(400).json({ error: \"Alıcı: ad, telefon, adres (sokak/satır), şehir zorunlu.\" });\n return;\n }\n if (!district) {\n res.status(400).json({ error: \"İlçe seçilmeli.\" });\n return;\n }\n if (!neighborhood) {\n res.status(400).json({ error: \"Mahalle seçilmeli (il–ilçe–mahalle listesinden).\" });\n return;\n }\n\n const lengthCm = String(body.lengthCm ?? \"\").trim();\n const widthCm = String(body.widthCm ?? \"\").trim();\n const heightCm = String(body.heightCm ?? \"\").trim();\n const weightKg = String(body.weightKg ?? \"\").trim();\n if (!lengthCm || !widthCm || !heightCm || !weightKg) {\n res.status(400).json({ error: \"Paket: boy, en, yükseklik (cm) ve ağırlık (kg) zorunlu.\" });\n return;\n }\n\n const zipRaw = rec.zip != null ? String(rec.zip).trim().replace(/\\D/g, \"\").slice(0, 5) : \"\";\n const emailRaw = rec.email != null ? String(rec.email).trim() : \"\";\n const reference = body.reference != null ? String(body.reference).trim() : \"\";\n const senderAddressIdRaw = body.senderAddressId != null ? String(body.senderAddressId).trim() : \"\";\n\n const result = await createGeliverManualShipment({\n vendorId: Number(vendorId),\n senderAddressId: senderAddressIdRaw || undefined,\n recipient: {\n name,\n phone,\n address1,\n city,\n district: district || \"Merkez\",\n neighborhood,\n zip: zipRaw || undefined,\n email: emailRaw || undefined,\n },\n lengthCm,\n widthCm,\n heightCm,\n weightKg,\n reference: reference || undefined,\n });\n\n if (!result.ok) {\n res.status(400).json({ error: result.error });\n return;\n }\n res.json({\n success: true,\n purchaseMode: \"manual\",\n shipmentId: result.shipmentId,\n shipment: result.shipment ?? null,\n });\n});\n\n/** Geliver — şehir listesi (API token ile, bkz. https://docs.geliver.io/docs/home ) */\nrouter.get(\"/providers/geliver/cities\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const r0 = await geliverApiListCitiesForVendor(Number(vendorId));\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data });\n});\n\n/** Geliver — ilçe listesi */\nrouter.get(\"/providers/geliver/districts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const cityCode = typeof req.query.cityCode === \"string\" ? req.query.cityCode.trim() : \"\";\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!cityCode) {\n res.status(400).json({ error: \"cityCode zorunlu\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const r0 = await geliverApiListDistrictsForVendor(Number(vendorId), cityCode);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data });\n});\n\nrouter.get(\"/providers/geliver/shipments\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const page = Math.max(1, parseInt(String(req.query.page ?? \"1\"), 10) || 1);\n const limit = Math.min(50, Math.max(1, parseInt(String(req.query.limit ?? \"20\"), 10) || 20));\n const r0 = await geliverApiListShipmentsForVendor(Number(vendorId), { page, limit });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, ...((r0.envelope as Record) ?? {}) });\n});\n\nrouter.get(\"/providers/geliver/shipment/:shipmentId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const shipmentId = String(req.params.shipmentId ?? \"\").trim();\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!shipmentId) {\n res.status(400).json({ error: \"shipmentId gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const r0 = await geliverApiGetShipmentForVendor(Number(vendorId), shipmentId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, shipment: r0.shipment });\n});\n\nrouter.patch(\"/providers/geliver/shipment/:shipmentId/package\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const shipmentId = String(req.params.shipmentId ?? \"\").trim();\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!shipmentId) {\n res.status(400).json({ error: \"shipmentId gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const b = req.body as Record;\n const lengthCm = String(b.lengthCm ?? \"\").trim();\n const widthCm = String(b.widthCm ?? \"\").trim();\n const heightCm = String(b.heightCm ?? \"\").trim();\n const weightKg = String(b.weightKg ?? \"\").trim();\n if (!lengthCm || !widthCm || !heightCm || !weightKg) {\n res.status(400).json({ error: \"Boy, en, yükseklik (cm) ve ağırlık (kg) zorunlu.\" });\n return;\n }\n const r0 = await geliverApiUpdateShipmentPackageForVendor(Number(vendorId), shipmentId, {\n lengthCm,\n widthCm,\n heightCm,\n weightKg,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, shipment: r0.shipment });\n});\n\nrouter.post(\"/providers/geliver/shipment/:shipmentId/accept-offer\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const shipmentId = String(req.params.shipmentId ?? \"\").trim();\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!shipmentId) {\n res.status(400).json({ error: \"shipmentId gerekli\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const body = (req.body ?? {}) as Record;\n const offerId = String(body.offerId ?? body.offerID ?? body.offer_id ?? \"\").trim();\n if (!offerId) {\n res.status(400).json({ error: \"offerId (teklif kimliği) zorunlu\" });\n return;\n }\n try {\n logger.info({ vendorId, shipmentId, offerId }, \"[geliver] accept-offer route start\");\n // Hosting / proxy 30 sn’de kestiğinde kullanıcı “Application failed to respond” görüyordu;\n // 28 sn’lik kendi bütçemizle açık bir mesaj dönelim.\n const r0 = await Promise.race([\n geliverApiAcceptOfferForVendor(Number(vendorId), offerId, shipmentId),\n new Promise<{ ok: false; error: string }>((resolve) =>\n setTimeout(\n () =>\n resolve({\n ok: false,\n error:\n \"Geliver yanıtı zaman aşımına uğradı. Birkaç saniye sonra «API'den yenile» deyip tekrar deneyin; satın alma muhtemelen Geliver tarafında işlendi.\",\n }),\n 28_000,\n ),\n ),\n ]);\n if (!r0.ok) {\n logger.warn(\n { vendorId, shipmentId, offerId, error: r0.error, debug: \"debug\" in r0 ? r0.debug : undefined },\n \"[geliver] accept-offer failed\",\n );\n const manualUrl = \"manualPurchaseUrl\" in r0 ? r0.manualPurchaseUrl : undefined;\n const manualHint = \"manualPurchaseHint\" in r0 ? r0.manualPurchaseHint : undefined;\n res.status(400).json({\n error: r0.error,\n debug: \"debug\" in r0 ? r0.debug : undefined,\n ...(typeof manualUrl === \"string\" && manualUrl ? { manualPurchaseUrl: manualUrl } : {}),\n ...(typeof manualHint === \"string\" && manualHint ? { manualPurchaseHint: manualHint } : {}),\n });\n return;\n }\n const isPending = \"pending\" in r0 && r0.pending === true;\n logger.info(\n {\n vendorId,\n shipmentId,\n offerId,\n transactionId: r0.transactionId,\n hasTracking: Boolean(r0.trackingNumber),\n hasLabel: Boolean(r0.labelUrl),\n pending: isPending,\n statusCode: \"statusCode\" in r0 ? r0.statusCode ?? null : null,\n },\n \"[geliver] accept-offer success\",\n );\n // Sipariş satırı bu shipmentId ile bağlıysa Geliver alanlarını anında senkronla.\n // Pending’de label/tracking henüz boş olabilir; status 'offer_accepted' kalır, ileride yenileme/webhook doldurur.\n const tracking = r0.trackingNumber != null ? String(r0.trackingNumber).trim() : \"\";\n const label = r0.labelUrl != null ? String(r0.labelUrl).trim() : \"\";\n const txId = r0.transactionId != null ? String(r0.transactionId).trim() : \"\";\n const nextStatus = tracking || label ? \"label_created\" : \"offer_accepted\";\n await db\n .execute(sql`\n UPDATE delivery_orders\n SET geliver_tracking_number = ${tracking || null},\n geliver_label_url = ${label || null},\n geliver_transaction_id = ${txId || null},\n geliver_status = ${nextStatus},\n geliver_last_error = NULL,\n updated_at = NOW()\n WHERE vendor_id = ${Number(vendorId)}\n AND geliver_shipment_id = ${shipmentId}\n `)\n .catch((err) => logger.warn({ err, vendorId, shipmentId }, \"[geliver] accept-offer sync delivery_orders failed\"));\n res.json(\n geliverJsonSafe({\n success: true,\n pending: isPending,\n message: \"message\" in r0 ? r0.message ?? null : null,\n statusCode: \"statusCode\" in r0 ? r0.statusCode ?? null : null,\n transactionId: r0.transactionId,\n trackingNumber: r0.trackingNumber,\n labelUrl: r0.labelUrl,\n debug: \"debug\" in r0 ? r0.debug ?? null : null,\n }) as Record,\n );\n } catch (e: unknown) {\n logger.error({ err: e, vendorId, shipmentId }, \"[geliver] accept-offer route\");\n res.status(500).json({ error: e instanceof Error ? e.message : \"Etiket yanıtı işlenemedi\" });\n }\n});\n\nrouter.put(\"/providers/open-status\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n const isOpen = Boolean((req.body as Record)?.isOpen);\n try {\n await db.execute(sql`\n UPDATE vendors\n SET is_open = ${isOpen}, updated_at = NOW()\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n `);\n res.json({ success: true, isOpen });\n } catch (e) {\n res.status(500).json({ error: String(e) });\n }\n});\n\n/** Bakiye — [Geliver organizations balance](https://docs.geliver.io/docs/home); organizationId sorguda veya işletme kaydında */\nrouter.get(\"/providers/geliver/balance\", async (req, res): Promise => {\n try {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const vid = Number(vendorId);\n if (!Number.isFinite(vid) || vid <= 0) {\n res.status(400).json({ error: \"Geçersiz işletme oturumu.\" });\n return;\n }\n const qOrg = typeof req.query.organizationId === \"string\" ? req.query.organizationId.trim() : \"\";\n const qShipmentHint =\n typeof req.query.shipmentId === \"string\" ? req.query.shipmentId.trim() : \"\";\n const rows = r(\n await db.execute(sql`\n SELECT geliver_organization_id FROM vendors WHERE id = ${vid} LIMIT 1\n `),\n );\n const orgId = qOrg || String(rows[0]?.geliver_organization_id ?? \"\").trim();\n // Upstream proxy timeout’una takılmamak için kendi 14 sn’lik bütçemiz: süre dolarsa kullanıcıya açık mesaj.\n const r0 = await Promise.race([\n geliverApiGetBalanceForVendor(vid, orgId, qShipmentHint ? { hintShipmentId: qShipmentHint } : undefined),\n new Promise<{ ok: false; error: string }>((resolve) =>\n setTimeout(\n () =>\n resolve({\n ok: false,\n error:\n \"Geliver bakiye sorgusu zaman aşımına uğradı. Geliver tarafı yavaş yanıt verdi; birkaç saniye sonra tekrar deneyin.\",\n }),\n 14_000,\n ),\n ),\n ]);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n const balPayload: Record = { success: true, balance: r0.balance };\n if (\"resolvedOrganizationId\" in r0 && r0.resolvedOrganizationId) {\n balPayload.resolvedOrganizationId = r0.resolvedOrganizationId;\n }\n res.json(geliverJsonSafe(balPayload) as Record);\n } catch (e: unknown) {\n logger.error({ err: e }, \"[geliver] balance route\");\n res.status(500).json({ error: e instanceof Error ? e.message : \"Bakiye yanıtı işlenemedi\" });\n }\n});\n\nrouter.get(\"/providers/geliver/addresses\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const role = String(req.query.role ?? \"sender\").toLowerCase();\n const isRecipient = role === \"recipient\" || role === \"customer\" || role === \"alıcı\";\n const page = Math.max(1, parseInt(String(req.query.page ?? \"1\"), 10) || 1);\n const limit = Math.min(50, Math.max(1, parseInt(String(req.query.limit ?? \"30\"), 10) || 30));\n const r0 = await geliverApiListAddressesForVendor(Number(vendorId), {\n isRecipientAddress: isRecipient,\n page,\n limit,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({\n success: true,\n data: r0.data,\n totalRows: r0.totalRows,\n page: r0.page,\n limit: r0.limit,\n totalPages: r0.totalPages,\n });\n});\n\nrouter.get(\"/providers/geliver/address/:addressId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const addressId = String(req.params.addressId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!addressId) {\n res.status(400).json({ error: \"addressId gerekli\" });\n return;\n }\n const r0 = await geliverApiGetAddressForVendor(Number(vendorId), addressId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, address: r0.address });\n});\n\nrouter.post(\"/providers/geliver/sender-address\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const name = String(b.name ?? \"\").trim();\n const email = String(b.email ?? \"\").trim();\n const phone = String(b.phone ?? \"\").trim();\n const address1 = String(b.address1 ?? \"\").trim();\n const cityName = String(b.cityName ?? b.city ?? \"\").trim();\n const cityCode = String(b.cityCode ?? \"\").trim();\n const districtName = String(b.districtName ?? b.district ?? \"\").trim();\n const districtID = Number(b.districtID ?? b.districtId ?? NaN);\n const zip = String(b.zip ?? \"\").trim().replace(/\\D/g, \"\").slice(0, 5);\n const shortName = b.shortName != null ? String(b.shortName).trim() : \"\";\n const address2 = b.address2 != null ? String(b.address2).trim() : undefined;\n if (!name || !email || !phone || !address1 || !cityName || !cityCode || !districtName || !Number.isFinite(districtID)) {\n res.status(400).json({ error: \"Gönderici: ad, e-posta, telefon, address1, şehir, cityCode, ilçe adı, districtID (Geliver), posta kodu zorunlu.\" });\n return;\n }\n if (!zip) {\n res.status(400).json({ error: \"Posta kodu (zip) zorunludur.\" });\n return;\n }\n const r0 = await geliverApiCreateSenderAddressForVendor(Number(vendorId), {\n name,\n email,\n phone,\n address1,\n address2,\n countryCode: \"TR\",\n cityName,\n cityCode,\n districtName,\n districtID,\n zip,\n shortName: shortName || undefined,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, address: r0.address });\n});\n\nrouter.delete(\"/providers/geliver/address/:addressId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const addressId = String(req.params.addressId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!addressId) {\n res.status(400).json({ error: \"addressId gerekli\" });\n return;\n }\n const r0 = await geliverApiDeleteAddressForVendor(Number(vendorId), addressId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n await db.execute(sql`\n UPDATE vendors SET\n geliver_sender_address_id = CASE WHEN geliver_sender_address_id = ${addressId} THEN NULL ELSE geliver_sender_address_id END,\n updated_at = NOW()\n WHERE id = ${Number(vendorId)}\n `);\n res.json({ success: true });\n});\n\nrouter.get(\"/providers/geliver/webhooks\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const r0 = await geliverApiListWebhooksForVendor(Number(vendorId));\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data });\n});\n\nrouter.post(\"/providers/geliver/webhooks\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const url = String(b.url ?? \"\").trim();\n const type = b.type != null ? String(b.type).trim() : undefined;\n if (!url) {\n res.status(400).json({ error: \"url zorunlu\" });\n return;\n }\n const r0 = await geliverApiCreateWebhookForVendor(Number(vendorId), { url, type });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, raw: r0.raw });\n});\n\nrouter.delete(\"/providers/geliver/webhooks/:webhookId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const webhookId = String(req.params.webhookId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!webhookId) {\n res.status(400).json({ error: \"webhookId gerekli\" });\n return;\n }\n const r0 = await geliverApiDeleteWebhookForVendor(Number(vendorId), webhookId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true });\n});\n\nrouter.post(\"/providers/geliver/webhooks/test\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const url = String(b.url ?? \"\").trim();\n const type = String(b.type ?? \"\").trim();\n if (!url || !type) {\n res.status(400).json({ error: \"url ve type zorunlu\" });\n return;\n }\n const r0 = await geliverApiTestWebhookForVendor(Number(vendorId), { url, type });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, raw: r0.raw });\n});\n\n/**\n * Geliver webhook alıcı endpoint'i.\n * Geliver burayı `shipment_status_changed`, `TRACK_UPDATED` vb. olaylar için POST'lar.\n * Hangi shipment veya tracking değişti onu bulup `delivery_orders`'i güncelliyoruz.\n *\n * Ham gövde + imza doğrulama; `app.ts` içinde `express.json` öncesinde kayıtlıdır.\n */\nfunction geliverWebhookHeader(headers: Record, name: string): string {\n const direct = headers[name];\n if (typeof direct === \"string\" && direct.trim()) return direct.trim();\n const lower = headers[name.toLowerCase()];\n if (typeof lower === \"string\" && lower.trim()) return lower.trim();\n return \"\";\n}\n\nfunction geliverWebhookSecretFromAuth(authHeader: string): string {\n const auth = authHeader.trim();\n if (auth.toLowerCase().startsWith(\"bearer \")) return auth.slice(7).trim();\n return auth;\n}\n\nfunction geliverWebhookNormalizeSignature(sig: string): string {\n const s = sig.trim();\n const m = /^sha256=(.+)$/i.exec(s);\n return (m?.[1] ?? s).trim();\n}\n\nfunction geliverWebhookSafeEqual(a: string, b: string): boolean {\n const ba = Buffer.from(a, \"utf8\");\n const bb = Buffer.from(b, \"utf8\");\n if (ba.length !== bb.length) return false;\n return crypto.timingSafeEqual(ba, bb);\n}\n\n/** Geliver SDK: HMAC-SHA256(secret, `${timestamp}.${rawBody}`) ↔ X-Signature; yedek: paylaşımlı secret başlığı. */\nfunction verifyGeliverWebhook(req: Request, rawBody: Buffer): boolean {\n const secret = getGeliverWebhookSecret();\n if (!secret) {\n logger.warn(\"[geliver:webhook] GELIVER_WEBHOOK_SECRET yapılandırılmamış — istek reddedildi\");\n return false;\n }\n\n const headers = req.headers as Record;\n const signature = geliverWebhookHeader(headers, \"x-signature\");\n const timestamp = geliverWebhookHeader(headers, \"x-timestamp\");\n\n if (signature && timestamp) {\n const tsNum = Number(timestamp);\n if (!Number.isFinite(tsNum)) return false;\n const tsMs = tsNum > 1e12 ? tsNum : tsNum * 1000;\n if (Math.abs(Date.now() - tsMs) > 5 * 60 * 1000) return false;\n\n const payload = `${timestamp}.${rawBody.toString(\"utf8\")}`;\n const expectedHex = crypto.createHmac(\"sha256\", secret).update(payload).digest(\"hex\");\n const expectedB64 = crypto.createHmac(\"sha256\", secret).update(payload).digest(\"base64\");\n const normalized = geliverWebhookNormalizeSignature(signature);\n if (geliverWebhookSafeEqual(normalized, expectedHex)) return true;\n if (geliverWebhookSafeEqual(normalized, expectedB64)) return true;\n return false;\n }\n\n const providedSecret =\n geliverWebhookHeader(headers, \"x-geliver-webhook-secret\")\n || geliverWebhookHeader(headers, \"x-webhook-secret\")\n || geliverWebhookSecretFromAuth(geliverWebhookHeader(headers, \"authorization\"));\n if (providedSecret && geliverWebhookSafeEqual(providedSecret, secret)) return true;\n\n return false;\n}\n\nasync function ingestGeliverWebhookEvent(\n body: Record,\n headers: Record,\n): Promise {\n // Olay tipini ve shipmentId'yi gevşek biçimde topla.\n const flat = (() => {\n try { return JSON.stringify(body).slice(0, 4000); } catch { return \"\"; }\n })();\n const pickStr = (...vals: unknown[]): string => {\n for (const v of vals) {\n if (typeof v === \"string\" && v.trim()) return v.trim();\n }\n return \"\";\n };\n const data = (body.data && typeof body.data === \"object\" ? (body.data as Record) : null);\n const shipmentId = pickStr(\n body.shipmentId, body.shipmentID, body.shipment_id,\n data?.shipmentId, data?.shipmentID, data?.shipment_id,\n (body.shipment as { id?: string } | undefined)?.id,\n );\n const trackingNumber = pickStr(\n body.trackingNumber, body.tracking_number,\n data?.trackingNumber, data?.tracking_number,\n );\n const labelUrl = pickStr(\n body.labelUrl, body.label_url,\n data?.labelUrl, data?.label_url,\n );\n const statusCode = pickStr(\n body.statusCode, body.status,\n data?.statusCode, data?.status,\n );\n const eventType = pickStr(body.type, body.event, body.eventType);\n logger.info(\n {\n eventType,\n shipmentId,\n trackingNumber: trackingNumber ? \"[set]\" : \"\",\n labelUrl: labelUrl ? \"[set]\" : \"\",\n statusCode,\n contentType: headers[\"content-type\"],\n bodyPreview: flat.slice(0, 500),\n },\n \"[geliver:webhook] inbound\",\n );\n if (!shipmentId) return;\n\n try {\n if (trackingNumber || labelUrl) {\n const patch: Partial = {\n geliverStatus: \"purchased\",\n updatedAt: new Date(),\n };\n if (trackingNumber) patch.geliverTrackingNumber = trackingNumber;\n if (labelUrl) patch.geliverLabelUrl = labelUrl;\n await db\n .update(deliveryOrdersTable)\n .set(patch)\n .where(eq(deliveryOrdersTable.geliverShipmentId, shipmentId));\n return;\n }\n\n if (/shipped|TRACK_UPDATED|in[_ ]?transit|delivered/i.test(`${eventType} ${statusCode}`)) {\n await db\n .update(deliveryOrdersTable)\n .set({ geliverStatus: \"shipped\", updatedAt: new Date() })\n .where(eq(deliveryOrdersTable.geliverShipmentId, shipmentId));\n }\n } catch (err) {\n logger.warn({ err, shipmentId }, \"[geliver:webhook] db update failed\");\n }\n}\n\nexport async function handleGeliverWebhook(req: Request, res: Response): Promise {\n const buf = req.body;\n if (!Buffer.isBuffer(buf)) {\n res.status(400).json({ error: \"Raw body gerekli\" });\n return;\n }\n\n if (!verifyGeliverWebhook(req, buf)) {\n logger.warn(\"[geliver:webhook] imza/secret doğrulanamadı — istek reddedildi\");\n res.status(401).json({ error: \"Webhook doğrulanamadı\" });\n return;\n }\n\n let body: Record;\n try {\n body = JSON.parse(buf.toString(\"utf8\")) as Record;\n } catch {\n res.status(400).json({ error: \"Geçersiz JSON\" });\n return;\n }\n\n res.status(200).json({ ok: true });\n try {\n await ingestGeliverWebhookEvent(body, req.headers as Record);\n } catch (err) {\n logger.warn({ err }, \"[geliver:webhook] handler failed\");\n }\n}\n\n// Geliver bazen test panelinde `GET` ile sağlık kontrolü yapabilir - 200 OK dönelim.\nrouter.get(\"/providers/geliver/webhook\", (_req, res) => {\n res.status(200).json({ ok: true, service: \"geliver-webhook\", ts: new Date().toISOString() });\n});\n\nrouter.get(\"/providers/geliver/parcel-templates\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const r0 = await geliverApiListParcelTemplatesForVendor(Number(vendorId));\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data });\n});\n\nrouter.post(\"/providers/geliver/parcel-templates\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const r0 = await geliverApiCreateParcelTemplateForVendor(Number(vendorId), b);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, template: r0.template });\n});\n\nrouter.delete(\"/providers/geliver/parcel-templates/:templateId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const templateId = String(req.params.templateId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!templateId) {\n res.status(400).json({ error: \"templateId gerekli\" });\n return;\n }\n const r0 = await geliverApiDeleteParcelTemplateForVendor(Number(vendorId), templateId);\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true });\n});\n\nrouter.get(\"/providers/geliver/prices\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const length = String(req.query.length ?? \"\").trim();\n const width = String(req.query.width ?? \"\").trim();\n const height = String(req.query.height ?? \"\").trim();\n const weight = String(req.query.weight ?? \"\").trim();\n if (!length || !width || !height || !weight) {\n res.status(400).json({ error: \"length, width, height, weight sorgu parametreleri zorunlu.\" });\n return;\n }\n const r0 = await geliverApiListPricesForVendor(Number(vendorId), {\n length,\n width,\n height,\n weight,\n distanceUnit: typeof req.query.distanceUnit === \"string\" ? req.query.distanceUnit : undefined,\n massUnit: typeof req.query.massUnit === \"string\" ? req.query.massUnit : undefined,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, raw: r0.raw });\n});\n\nrouter.get(\"/providers/geliver/provider-accounts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const r0 = await geliverApiListProviderAccountsForVendor(Number(vendorId));\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, data: r0.data, envelope: r0.envelope ?? null });\n});\n\nrouter.post(\"/providers/geliver/provider-accounts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n const b = req.body as Record;\n const username = String(b.username ?? \"\").trim();\n const name = String(b.name ?? \"\").trim();\n const providerCode = String(b.providerCode ?? \"\").trim();\n const version = parseInt(String(b.version ?? \"1\"), 10) || 1;\n if (!username || !name || !providerCode) {\n res.status(400).json({ error: \"username, name, providerCode zorunlu\" });\n return;\n }\n const password = b.password != null ? String(b.password) : undefined;\n const r0 = await geliverApiCreateProviderAccountForVendor(Number(vendorId), {\n username,\n password,\n name,\n providerCode,\n version,\n isActive: b.isActive !== false,\n isPublic: Boolean(b.isPublic),\n sharable: Boolean(b.sharable),\n isDynamicPrice: Boolean(b.isDynamicPrice),\n parameters:\n b.parameters && typeof b.parameters === \"object\"\n ? (b.parameters as Record)\n : undefined,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true, account: r0.account });\n});\n\nrouter.delete(\"/providers/geliver/provider-accounts/:accountId\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n const accountId = String(req.params.accountId ?? \"\").trim();\n if (!(await vendorSessionOwnsRow(vendorId, vendorEmail))) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n if (!accountId) {\n res.status(400).json({ error: \"accountId gerekli\" });\n return;\n }\n const isDel =\n req.query.isDeleteAccountConnection === \"1\" || String(req.query.isDeleteAccountConnection ?? \"\") === \"true\";\n const r0 = await geliverApiDeleteProviderAccountForVendor(Number(vendorId), accountId, {\n isDeleteAccountConnection: isDel,\n });\n if (!r0.ok) {\n res.status(400).json({ error: r0.error });\n return;\n }\n res.json({ success: true });\n});\n\nrouter.put(\"/providers/tr-gateway-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS paytr_merchant_id TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS paytr_merchant_key TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS paytr_merchant_salt TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS paytr_test_mode BOOLEAN NOT NULL DEFAULT true`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS iyzico_api_key TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS iyzico_secret_key TEXT`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS iyzico_sandbox BOOLEAN NOT NULL DEFAULT true`);\n await db.execute(sql`ALTER TABLE vendors ADD COLUMN IF NOT EXISTS preferred_tr_gateway TEXT`);\n\n const check = r(\n await db.execute(sql`\n SELECT id, revenue_model FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!check) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n if (normalizeRevenueModel(check.revenue_model) !== \"subscription\") {\n res.status(400).json({\n error: \"Bu bağlantı yalnızca abonelik gelir modelindeki işletmeler içindir.\",\n });\n return;\n }\n\n const body = req.body as Record;\n\n const [cur] = r(\n await db.execute(sql`\n SELECT paytr_merchant_id, paytr_merchant_key, paytr_merchant_salt, paytr_test_mode,\n iyzico_api_key, iyzico_secret_key, iyzico_sandbox, preferred_tr_gateway\n FROM vendors WHERE id = ${Number(vendorId)} LIMIT 1\n `),\n );\n if (!cur) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const pick = (k: string, prev: unknown): unknown =>\n k in body ? (body[k] === null || body[k] === \"\" ? null : body[k]) : prev;\n\n const paytrMerchantId = pick(\"paytrMerchantId\", cur.paytr_merchant_id);\n let paytrMerchantKey = pick(\"paytrMerchantKey\", cur.paytr_merchant_key);\n let paytrMerchantSalt = pick(\"paytrMerchantSalt\", cur.paytr_merchant_salt);\n let iyzicoApiKey = pick(\"iyzicoApiKey\", cur.iyzico_api_key);\n let iyzicoSecretKey = pick(\"iyzicoSecretKey\", cur.iyzico_secret_key);\n if (body[\"paytrMerchantKey\"] === \"***\") paytrMerchantKey = cur.paytr_merchant_key;\n if (body[\"paytrMerchantSalt\"] === \"***\") paytrMerchantSalt = cur.paytr_merchant_salt;\n if (body[\"iyzicoApiKey\"] === \"***\") iyzicoApiKey = cur.iyzico_api_key;\n if (body[\"iyzicoSecretKey\"] === \"***\") iyzicoSecretKey = cur.iyzico_secret_key;\n\n const paytrTestMode =\n body[\"paytrTestMode\"] !== undefined ? Boolean(body[\"paytrTestMode\"]) : Boolean(cur.paytr_test_mode !== false);\n const iyzicoSandbox =\n body[\"iyzicoSandbox\"] !== undefined ? Boolean(body[\"iyzicoSandbox\"]) : Boolean(cur.iyzico_sandbox !== false);\n\n let preferred: string | null =\n body[\"preferredTrGateway\"] !== undefined\n ? String(body[\"preferredTrGateway\"] ?? \"\").trim().toLowerCase() || null\n : (cur.preferred_tr_gateway as string | null);\n if (preferred && preferred !== \"paytr\" && preferred !== \"iyzico\") preferred = null;\n\n await db.execute(sql`\n UPDATE vendors SET\n paytr_merchant_id = ${paytrMerchantId != null ? String(paytrMerchantId).trim() || null : null},\n paytr_merchant_key = ${paytrMerchantKey != null ? String(paytrMerchantKey) : null},\n paytr_merchant_salt = ${paytrMerchantSalt != null ? String(paytrMerchantSalt) : null},\n paytr_test_mode = ${paytrTestMode},\n iyzico_api_key = ${iyzicoApiKey != null ? String(iyzicoApiKey).trim() || null : null},\n iyzico_secret_key = ${iyzicoSecretKey != null ? String(iyzicoSecretKey) : null},\n iyzico_sandbox = ${iyzicoSandbox},\n preferred_tr_gateway = ${preferred},\n updated_at = NOW()\n WHERE id = ${Number(vendorId)}\n `);\n\n const pid = String(paytrMerchantId ?? \"\").trim();\n res.json({\n success: true,\n paytrMerchantIdMasked: pid ? `****${pid.slice(-4)}` : \"\",\n paytrTestMode,\n iyzicoSandbox,\n preferredTrGateway: preferred,\n });\n});\n\nrouter.post(\"/providers/subscription-renewal\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n const { startDate, endDate, paymentMethod, receiptUrl } = req.body as {\n startDate?: string; endDate?: string; paymentMethod?: \"bank_transfer\" | \"stripe\"; receiptUrl?: string;\n };\n if (!startDate || !endDate) { res.status(400).json({ error: \"Başlangıç ve bitiş tarihi zorunlu\" }); return; }\n if ((paymentMethod || \"bank_transfer\") === \"bank_transfer\" && !String(receiptUrl || \"\").trim()) {\n res.status(400).json({ error: \"Havale için dekont zorunlu\" }); return;\n }\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `));\n if (!own[0]) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_subscription_requests (\n vendor_id, start_date, end_date, payment_method, receipt_url, status\n ) VALUES (\n ${Number(vendorId)}, ${startDate}, ${endDate}, ${(paymentMethod || \"bank_transfer\")}, ${String(receiptUrl || \"\").trim() || null}, 'pending'\n )\n RETURNING id\n `));\n if (!rows[0]) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n res.json({ success: true, requestId: rows[0].id });\n});\n\nrouter.get(\"/providers/integrations\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, notes FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `));\n const row = rows[0];\n if (!row) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const notes = String(row.notes || \"\");\n const openai = noteField(notes, \"openai_api_key\");\n const gemini = noteField(notes, \"gemini_api_key\");\n const googleAi = noteField(notes, \"google_ai_api_key\");\n const deepseek = noteField(notes, \"deepseek_api_key\");\n const model = noteField(notes, \"openai_model\") || \"gpt-4o-mini\";\n const waEnabled = noteField(notes, \"whatsapp_enabled\") !== \"0\";\n const waNewOrder = noteField(notes, \"whatsapp_feature_new_order\") !== \"0\";\n const waCustomer = noteField(notes, \"whatsapp_feature_customer_contact\") !== \"0\";\n const waStatus = noteField(notes, \"whatsapp_feature_order_status\") === \"1\";\n const waBulk = noteField(notes, \"whatsapp_feature_bulk_marketing\") === \"1\";\n res.json({\n success: true,\n integrations: {\n openaiApiKeyMasked: openai ? maskSecret(openai) : \"\",\n geminiApiKeyMasked: gemini ? maskSecret(gemini) : \"\",\n googleAiApiKeyMasked: googleAi ? maskSecret(googleAi) : \"\",\n deepseekApiKeyMasked: deepseek ? maskSecret(deepseek) : \"\",\n openaiModel: model,\n whatsappEnabled: waEnabled,\n whatsappFeatureNewOrder: waNewOrder,\n whatsappFeatureCustomerContact: waCustomer,\n whatsappFeatureOrderStatus: waStatus,\n whatsappFeatureBulkMarketing: waBulk,\n },\n });\n});\n\nrouter.patch(\"/providers/integrations\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const b = req.body as {\n openaiApiKey?: string;\n openaiModel?: string;\n geminiApiKey?: string;\n googleAiApiKey?: string;\n deepseekApiKey?: string;\n whatsappEnabled?: boolean;\n whatsappFeatureNewOrder?: boolean;\n whatsappFeatureCustomerContact?: boolean;\n whatsappFeatureOrderStatus?: boolean;\n whatsappFeatureBulkMarketing?: boolean;\n };\n const rows = r(await db.execute(sql`\n SELECT id, notes FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `));\n const row = rows[0];\n if (!row) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n let notes = String(row.notes || \"\");\n if (b.openaiApiKey !== undefined) notes = upsertNoteField(notes, \"openai_api_key\", b.openaiApiKey);\n if (b.geminiApiKey !== undefined) notes = upsertNoteField(notes, \"gemini_api_key\", b.geminiApiKey);\n if (b.googleAiApiKey !== undefined) notes = upsertNoteField(notes, \"google_ai_api_key\", b.googleAiApiKey);\n if (b.deepseekApiKey !== undefined) notes = upsertNoteField(notes, \"deepseek_api_key\", b.deepseekApiKey);\n if (b.openaiModel !== undefined) notes = upsertNoteField(notes, \"openai_model\", b.openaiModel);\n if (b.whatsappEnabled !== undefined) notes = upsertNoteField(notes, \"whatsapp_enabled\", b.whatsappEnabled ? \"1\" : \"0\");\n if (b.whatsappFeatureNewOrder !== undefined) notes = upsertNoteField(notes, \"whatsapp_feature_new_order\", b.whatsappFeatureNewOrder ? \"1\" : \"0\");\n if (b.whatsappFeatureCustomerContact !== undefined) notes = upsertNoteField(notes, \"whatsapp_feature_customer_contact\", b.whatsappFeatureCustomerContact ? \"1\" : \"0\");\n if (b.whatsappFeatureOrderStatus !== undefined) notes = upsertNoteField(notes, \"whatsapp_feature_order_status\", b.whatsappFeatureOrderStatus ? \"1\" : \"0\");\n if (b.whatsappFeatureBulkMarketing !== undefined) notes = upsertNoteField(notes, \"whatsapp_feature_bulk_marketing\", b.whatsappFeatureBulkMarketing ? \"1\" : \"0\");\n await db.execute(sql`UPDATE vendors SET notes = ${notes}, updated_at = NOW() WHERE id = ${Number(vendorId)}`);\n res.json({ success: true });\n});\n\n/** Panel: sipariş WhatsApp bildirimini dene (CallMeBot + numara kontrolü). */\nrouter.post(\"/providers/me/whatsapp-order-test\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const own = r(\n await db.execute(sql`\n SELECT id FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n )[0];\n if (!own) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const waResult = await notifyVendorWhatsApp({\n vendorId: Number(vendorId),\n eventType: \"new_order\",\n details: [\n \"Bu bir *test bildirimidir*.\",\n \"Gerçek siparişlerde de aynı kanal kullanılır.\",\n `Zaman: ${new Date().toLocaleString(\"tr-TR\", { timeZone: \"Europe/Istanbul\" })}`,\n ].join(\"\\n\"),\n });\n\n if (waResult.sent) {\n res.json({ success: true, sent: true, message: \"Test mesajı WhatsApp numaranıza gönderildi.\" });\n return;\n }\n\n res.status(400).json({\n success: false,\n sent: false,\n link: waResult.link ?? null,\n error:\n \"Otomatik mesaj gönderilemedi. İşletme WhatsApp numarasını kaydedin, «Siparişi WhatsApp'tan al» seçeneğini açın ve CallMeBot'u bu numarayla etkinleştirin (veya platform CallMeBot anahtarı tanımlı olsun).\",\n });\n});\n\n/* — Mağaza blogu (panel) — */\n\nrouter.get(\"/providers/me/blog-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id, slug, name FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT enabled FROM vendor_blog_settings WHERE vendor_id = ${Number(vendorId)} LIMIT 1\n `));\n res.json({\n success: true,\n enabled: Boolean(rows[0]?.enabled),\n vendorSlug: String(own.slug || \"\"),\n vendorName: String(own.name || \"\"),\n });\n});\n\nrouter.put(\"/providers/me/blog-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const enabled = Boolean((req.body as { enabled?: boolean })?.enabled);\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await db.execute(sql`\n INSERT INTO vendor_blog_settings (vendor_id, enabled, updated_at)\n VALUES (${Number(vendorId)}, ${enabled}, NOW())\n ON CONFLICT (vendor_id) DO UPDATE SET enabled = ${enabled}, updated_at = NOW()\n `);\n res.json({ success: true });\n});\n\nrouter.get(\"/providers/me/blog-posts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, slug, title, excerpt, cover_image_url, published, published_at, created_at, updated_at\n FROM vendor_blog_posts WHERE vendor_id = ${Number(vendorId)}\n ORDER BY updated_at DESC NULLS LAST, id DESC\n LIMIT 200\n `));\n res.json({ success: true, posts: rows });\n});\n\nrouter.post(\"/providers/me/blog-posts\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const b = req.body as {\n title?: string;\n slug?: string;\n excerpt?: string;\n coverImageUrl?: string;\n contentJson?: unknown;\n published?: boolean;\n };\n const title = String(b.title ?? \"\").trim().slice(0, 300);\n let slug = String(b.slug ?? \"\").trim().toLowerCase().slice(0, 120);\n if (!title) { res.status(400).json({ error: \"Başlık zorunlu\" }); return; }\n if (!slug) slug = slugify(title);\n slug = slug.replace(/[^a-z0-9-]/g, \"-\").replace(/-+/g, \"-\").replace(/^-|-$/g, \"\") || `yazi-${Date.now()}`;\n const excerpt = String(b.excerpt ?? \"\").trim().slice(0, 2000) || null;\n const cover = String(b.coverImageUrl ?? \"\").trim().slice(0, 2000) || null;\n const published = b.published === true;\n const contentArr = Array.isArray(b.contentJson) ? b.contentJson : [];\n const contentJson = JSON.stringify(contentArr).slice(0, 500_000);\n const pubAt = published ? new Date().toISOString() : null;\n try {\n const ins = r(await db.execute(sql`\n INSERT INTO vendor_blog_posts (vendor_id, slug, title, excerpt, cover_image_url, content_json, published, published_at, updated_at)\n VALUES (\n ${Number(vendorId)}, ${slug}, ${title}, ${excerpt}, ${cover},\n ${contentJson}::jsonb, ${published}, ${pubAt}, NOW()\n )\n RETURNING id, slug\n `));\n res.status(201).json({ success: true, id: ins[0]?.id, slug: ins[0]?.slug });\n } catch {\n res.status(400).json({ error: \"Slug kullanımda veya kayıt başarısız.\" });\n }\n});\n\nrouter.patch(\"/providers/me/blog-posts/:id\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const id = parseInt(req.params.id, 10);\n if (!Number.isFinite(id)) { res.status(400).json({ error: \"Geçersiz id\" }); return; }\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const cur = r(await db.execute(sql`\n SELECT id, slug, published FROM vendor_blog_posts WHERE id = ${id} AND vendor_id = ${Number(vendorId)} LIMIT 1\n `))[0];\n if (!cur) { res.status(404).json({ error: \"Yazı yok\" }); return; }\n const b = req.body as {\n title?: string;\n slug?: string;\n excerpt?: string;\n coverImageUrl?: string;\n contentJson?: unknown;\n published?: boolean;\n };\n let slug = cur.slug ? String(cur.slug) : \"\";\n if (b.slug !== undefined) {\n slug = String(b.slug).trim().toLowerCase().slice(0, 120);\n slug = slug.replace(/[^a-z0-9-]/g, \"-\").replace(/-+/g, \"-\").replace(/^-|-$/g, \"\");\n }\n const title = b.title !== undefined ? String(b.title).trim().slice(0, 300) : undefined;\n const excerpt = b.excerpt !== undefined ? (String(b.excerpt).trim().slice(0, 2000) || null) : undefined;\n const cover = b.coverImageUrl !== undefined ? (String(b.coverImageUrl).trim().slice(0, 2000) || null) : undefined;\n const published = typeof b.published === \"boolean\" ? b.published : undefined;\n let contentSql = sql``;\n if (b.contentJson !== undefined) {\n const contentArr = Array.isArray(b.contentJson) ? b.contentJson : [];\n const contentJson = JSON.stringify(contentArr).slice(0, 500_000);\n contentSql = sql`, content_json = ${contentJson}::jsonb`;\n }\n await db.execute(sql`\n UPDATE vendor_blog_posts SET\n title = COALESCE(${title ?? null}, title),\n slug = COALESCE(${b.slug !== undefined ? slug : null}, slug),\n excerpt = COALESCE(${excerpt !== undefined ? excerpt : null}, excerpt),\n cover_image_url = COALESCE(${cover !== undefined ? cover : null}, cover_image_url),\n updated_at = NOW()\n ${contentSql}\n WHERE id = ${id} AND vendor_id = ${Number(vendorId)}\n `);\n if (published === true) {\n await db.execute(sql`\n UPDATE vendor_blog_posts SET published = true, published_at = COALESCE(published_at, NOW()), updated_at = NOW()\n WHERE id = ${id} AND vendor_id = ${Number(vendorId)}\n `);\n } else if (published === false) {\n await db.execute(sql`\n UPDATE vendor_blog_posts SET published = false, published_at = NULL, updated_at = NOW()\n WHERE id = ${id} AND vendor_id = ${Number(vendorId)}\n `);\n }\n res.json({ success: true });\n});\n\nrouter.delete(\"/providers/me/blog-posts/:id\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const id = parseInt(req.params.id, 10);\n await ensureVendorBlogTables();\n const own = r(await db.execute(sql`\n SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `))[0];\n if (!own) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await db.execute(sql`DELETE FROM vendor_blog_posts WHERE id = ${id} AND vendor_id = ${Number(vendorId)}`);\n res.json({ success: true });\n});\n\nrouter.post(\"/providers/ai/generate\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { purpose = \"about\", title = \"\", keywords = \"\" } = req.body as { purpose?: string; title?: string; keywords?: string };\n const rows = r(await db.execute(sql`\n SELECT id, name, city, district, address, vendor_type, notes\n FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1\n `));\n const row = rows[0];\n if (!row) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const notes = String(row.notes || \"\");\n let apiKey = noteField(notes, \"openai_api_key\");\n let geminiKey = noteField(notes, \"gemini_api_key\");\n let deepseekKey = noteField(notes, \"deepseek_api_key\");\n const [siteAi] = await db\n .select({\n openaiApiKey: siteSettingsTable.openaiApiKey,\n openaiModel: siteSettingsTable.openaiModel,\n geminiApiKey: siteSettingsTable.geminiApiKey,\n deepseekApiKey: siteSettingsTable.deepseekApiKey,\n })\n .from(siteSettingsTable)\n .limit(1);\n if (!String(apiKey ?? \"\").trim()) apiKey = String(siteAi?.openaiApiKey ?? \"\").trim();\n const model =\n (noteField(notes, \"openai_model\") || String(siteAi?.openaiModel ?? \"\").trim() || \"gpt-4o-mini\").trim() || \"gpt-4o-mini\";\n if (!String(geminiKey ?? \"\").trim()) geminiKey = String(siteAi?.geminiApiKey ?? \"\").trim();\n if (!String(geminiKey ?? \"\").trim()) geminiKey = noteField(notes, \"google_ai_api_key\");\n if (!String(deepseekKey ?? \"\").trim()) deepseekKey = String(siteAi?.deepseekApiKey ?? \"\").trim();\n\n if (purpose === \"about\") {\n const gen = await generateTurkishVendorAboutDetailed(\n {\n openaiApiKey: apiKey || null,\n openaiModel: model,\n geminiApiKey: geminiKey || null,\n deepseekApiKey: deepseekKey || null,\n },\n {\n name: String(row.name || title || \"İşletme\"),\n city: row.city != null ? String(row.city) : null,\n district: row.district != null ? String(row.district) : null,\n address: row.address != null ? String(row.address) : null,\n vendorType: row.vendor_type != null ? String(row.vendor_type) : null,\n },\n );\n if (!gen.text) {\n res.status(400).json({\n error: gen.hasAnyKey\n ? `AI metni üretilemedi: ${gen.errors.join(\" | \").slice(0, 450)}`\n : \"AI metni üretilemedi: OpenAI, Gemini veya DeepSeek anahtarı tanımlı değil (işletme Entegrasyonlar veya site Genel Ayarlar).\",\n });\n return;\n }\n res.json({ success: true, text: gen.text, provider: gen.usedProvider });\n return;\n }\n\n const prompt = `Türkçe, kısa ve satış odaklı ürün açıklaması yaz. Ürün: ${title}. Anahtar kelimeler: ${keywords || \"-\"}.`;\n\n if (String(apiKey ?? \"\").trim()) {\n try {\n const rr = await fetch(\"https://api.openai.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: \"system\", content: \"Sen Türkçe içerik üreten yardımcı bir asistansın.\" },\n { role: \"user\", content: prompt },\n ],\n temperature: 0.6,\n }),\n });\n const d = (await rr.json().catch(() => ({}))) as Record;\n const text = String((d as { choices?: Array<{ message?: { content?: string } }> }).choices?.[0]?.message?.content || \"\").trim();\n if (rr.ok && text) {\n res.json({ success: true, text });\n return;\n }\n } catch {\n /* Gemini’ye düş */\n }\n }\n\n if (String(geminiKey ?? \"\").trim()) {\n try {\n const { callGeminiChat } = await import(\"../lib/aiChatProviders.js\");\n const g = await callGeminiChat(\n String(geminiKey),\n \"Sen Türkçe içerik üreten yardımcı bir asistansın.\",\n prompt,\n 0.55,\n );\n if (g.text) {\n res.json({ success: true, text: g.text });\n return;\n }\n } catch {\n /* DeepSeek’e düş */\n }\n }\n\n if (String(deepseekKey ?? \"\").trim()) {\n try {\n const rr = await fetch(\"https://api.deepseek.com/chat/completions\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${deepseekKey}` },\n body: JSON.stringify({\n model: \"deepseek-chat\",\n messages: [\n { role: \"system\", content: \"Sen Türkçe içerik üreten yardımcı bir asistansın.\" },\n { role: \"user\", content: prompt },\n ],\n temperature: 0.55,\n }),\n });\n const d = (await rr.json().catch(() => ({}))) as { choices?: Array<{ message?: { content?: string } }> };\n const text = String(d?.choices?.[0]?.message?.content || \"\").trim();\n if (rr.ok && text) {\n res.json({ success: true, text });\n return;\n }\n } catch {\n /* noop */\n }\n }\n\n res.status(400).json({\n error:\n \"OpenAI, Gemini veya DeepSeek API anahtarı yok veya üretim başarısız. Genel Ayarlar → Entegrasyonlar veya işletme paneli API anahtarlarını kontrol edin.\",\n });\n});\n\nrouter.get(\"/admin/providers/subscription-requests\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const status = String(req.query.status || \"pending\");\n const rows = r(await db.execute(sql`\n SELECT\n r.id, r.vendor_id, r.start_date, r.end_date, r.payment_method, r.receipt_url,\n r.status, r.admin_note, r.processed_at, r.created_at,\n v.name as vendor_name, v.slug as vendor_slug, v.owner_name, v.owner_email, v.city, v.district\n FROM vendor_subscription_requests r\n JOIN vendors v ON v.id = r.vendor_id\n WHERE ${status === \"all\" ? sql`1=1` : sql`r.status = ${status}`}\n ORDER BY r.created_at DESC\n LIMIT 300\n `));\n res.json(rows);\n});\n\nrouter.get(\"/admin/providers/subscription-expiring\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const days = Math.max(1, Math.min(90, Number(req.query.days || 30)));\n const rows = r(await db.execute(sql`\n SELECT\n v.id as vendor_id,\n v.name as vendor_name,\n v.slug as vendor_slug,\n v.owner_name,\n v.owner_email,\n v.city,\n v.district,\n MAX(r.end_date) as end_date,\n MAX(r.start_date) as start_date\n FROM vendors v\n JOIN vendor_subscription_requests r ON r.vendor_id = v.id AND r.status = 'approved'\n GROUP BY v.id, v.name, v.slug, v.owner_name, v.owner_email, v.city, v.district\n HAVING MAX(r.end_date) >= CURRENT_DATE\n AND MAX(r.end_date) <= (CURRENT_DATE + (${days} * INTERVAL '1 day'))\n ORDER BY MAX(r.end_date) ASC\n LIMIT 300\n `));\n res.json(rows);\n});\n\nrouter.patch(\"/admin/providers/subscription-requests/:id/approve\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const adminNote = String((req.body as any)?.adminNote || \"\").trim() || null;\n const reqRows = r(await db.execute(sql`\n SELECT id, vendor_id, start_date, end_date, payment_method, receipt_url\n FROM vendor_subscription_requests\n WHERE id = ${id} AND status = 'pending'\n LIMIT 1\n `));\n const row = reqRows[0];\n if (!row) { res.status(404).json({ error: \"Talep bulunamadı veya zaten işlenmiş\" }); return; }\n await db.execute(sql`\n UPDATE vendor_subscription_requests\n SET status = 'approved', admin_note = ${adminNote}, processed_at = NOW(), updated_at = NOW()\n WHERE id = ${id}\n `);\n await db.execute(sql`\n UPDATE vendors\n SET status = 'active',\n notes = CONCAT(\n COALESCE(notes, ''),\n CASE WHEN COALESCE(notes, '') = '' THEN '' ELSE ' | ' END,\n 'sub_start:', ${String(row.start_date)},\n ' | sub_end:', ${String(row.end_date)},\n ' | sub_payment:', ${String(row.payment_method)},\n ' | sub_receipt:', ${String(row.receipt_url || \"-\")},\n ' | sub_approved_at:', TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI')\n ),\n updated_at = NOW()\n WHERE id = ${Number(row.vendor_id)}\n `);\n res.json({ success: true });\n});\n\nrouter.patch(\"/admin/providers/subscription-requests/:id/reject\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const adminNote = String((req.body as any)?.adminNote || \"\").trim() || null;\n const row = r(await db.execute(sql`SELECT id FROM vendor_subscription_requests WHERE id = ${id} AND status = 'pending' LIMIT 1`))[0];\n if (!row) { res.status(404).json({ error: \"Talep bulunamadı veya zaten işlenmiş\" }); return; }\n await db.execute(sql`\n UPDATE vendor_subscription_requests\n SET status = 'rejected', admin_note = ${adminNote}, processed_at = NOW(), updated_at = NOW()\n WHERE id = ${id}\n `);\n res.json({ success: true });\n});\n\nrouter.patch(\"/admin/providers/subscription-requests/:id/note\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const adminNote = String((req.body as any)?.adminNote || \"\").trim() || null;\n const row = r(await db.execute(sql`SELECT id FROM vendor_subscription_requests WHERE id = ${id} LIMIT 1`))[0];\n if (!row) { res.status(404).json({ error: \"Talep bulunamadı\" }); return; }\n await db.execute(sql`\n UPDATE vendor_subscription_requests\n SET admin_note = ${adminNote}, updated_at = NOW()\n WHERE id = ${id}\n `);\n res.json({ success: true });\n});\n\n/** Admin: onaylı abonelik kaydı oluşturur (yıl doldu / manuel uzatma). Bitiş, başlangıç + ay sayısı ile hesaplanır. */\nrouter.post(\"/admin/providers/:id/subscription-manual-extend\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const vendorId = Number(req.params.id);\n if (!Number.isFinite(vendorId) || vendorId <= 0) {\n res.status(400).json({ error: \"Geçersiz işletme id\" });\n return;\n }\n const months = Math.max(1, Math.min(60, Math.floor(Number((req.body as { months?: unknown })?.months ?? 12))));\n const adminNote = String((req.body as { adminNote?: unknown })?.adminNote ?? \"\").trim() || null;\n\n const exists = r(await db.execute(sql`SELECT id FROM vendors WHERE id = ${vendorId} LIMIT 1`))[0];\n if (!exists) {\n res.status(404).json({ error: \"İşletme bulunamadı\" });\n return;\n }\n\n const maxRow = r(\n await db.execute(sql`\n SELECT MAX(end_date)::text as max_end\n FROM vendor_subscription_requests\n WHERE vendor_id = ${vendorId} AND status = 'approved'\n `),\n )[0];\n const maxEndStr = maxRow?.max_end != null ? String(maxRow.max_end) : \"\";\n\n const today = new Date();\n today.setUTCHours(0, 0, 0, 0);\n let start = new Date(today);\n if (maxEndStr) {\n const parts = maxEndStr.split(\"-\").map((x) => parseInt(x, 10));\n if (parts.length === 3 && parts.every((n) => Number.isFinite(n))) {\n const maxEnd = new Date(Date.UTC(parts[0], parts[1] - 1, parts[2]));\n if (!Number.isNaN(maxEnd.getTime())) {\n const dayAfter = new Date(maxEnd);\n dayAfter.setUTCDate(dayAfter.getUTCDate() + 1);\n if (dayAfter.getTime() > start.getTime()) start = dayAfter;\n }\n }\n }\n\n const end = new Date(start);\n end.setUTCMonth(end.getUTCMonth() + months);\n end.setUTCDate(end.getUTCDate() - 1);\n\n const startStr = start.toISOString().slice(0, 10);\n const endStr = end.toISOString().slice(0, 10);\n const append = ` | admin_extend:${startStr}→${endStr} | at:${new Date().toISOString().slice(0, 16)}`;\n\n await db.execute(sql`\n INSERT INTO vendor_subscription_requests (\n vendor_id, start_date, end_date, payment_method, receipt_url, status, admin_note, processed_at, updated_at\n ) VALUES (\n ${vendorId},\n ${startStr}::date,\n ${endStr}::date,\n 'admin_manual',\n null,\n 'approved',\n ${adminNote},\n NOW(),\n NOW()\n )\n `);\n\n await db.execute(sql`\n UPDATE vendors\n SET notes = CONCAT(COALESCE(notes, ''), ${append}),\n updated_at = NOW()\n WHERE id = ${vendorId}\n `);\n\n res.json({ success: true, start_date: startStr, end_date: endStr, vendor_id: vendorId, months });\n});\n\n/* ────────────────────────────────────────────────────────\n PATCH /api/providers/service-settings (Masa & Rezervasyon)\n──────────────────────────────────────────────────────── */\nrouter.patch(\"/providers/service-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const body = req.body as Record;\n const { tableServiceEnabled, reservationEnabled, reservationAutoConfirm, tableSections, qrMenuPublic } = body;\n const vid = Number(vendorId);\n const vendorRows = r(\n await db.execute(sql`\n SELECT vendor_type, provider_type, provider_subtype, notes\n FROM vendors\n WHERE id = ${vid} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `),\n );\n const vendorRow = vendorRows[0];\n if (!vendorRow) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n const tableAllowed = canVendorUseTableService(vendorRow);\n const effectiveTableServiceEnabled = tableAllowed && Boolean(tableServiceEnabled);\n const effectiveQrMenuPublic = tableAllowed && Boolean(qrMenuPublic);\n let mergedNotes: string | undefined;\n if (typeof qrMenuPublic === \"boolean\") {\n const prevNotes = vendorRow.notes != null ? String(vendorRow.notes) : \"\";\n mergedNotes = mergeVendorNoteLine(prevNotes, \"qr_menu_public\", effectiveQrMenuPublic ? \"1\" : \"0\");\n }\n await db.execute(sql`\n UPDATE vendors SET\n table_service_enabled = ${effectiveTableServiceEnabled},\n reservation_enabled = ${Boolean(reservationEnabled)},\n reservation_auto_confirm = ${Boolean(reservationAutoConfirm)},\n table_sections = ${effectiveTableServiceEnabled && tableSections !== undefined ? (tableSections === null ? null : JSON.stringify(tableSections)) : null},\n updated_at = NOW()\n WHERE id = ${vid} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n `);\n if (mergedNotes !== undefined) {\n await db.execute(sql`\n UPDATE vendors SET notes = ${mergedNotes}, updated_at = NOW()\n WHERE id = ${vid} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n `);\n }\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n GET /api/providers/service-settings\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/service-settings\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n if (!vendorId) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT vendor_type, provider_type, provider_subtype, table_service_enabled, reservation_enabled, reservation_auto_confirm, table_sections, notes\n FROM vendors WHERE id = ${Number(vendorId)} LIMIT 1\n `));\n const row: Row = rows[0] ?? {};\n const tableAllowed = canVendorUseTableService(row);\n res.json({\n tableServiceEnabled: tableAllowed && Boolean(row.table_service_enabled),\n reservationEnabled: Boolean(row.reservation_enabled),\n reservationAutoConfirm: Boolean(row.reservation_auto_confirm),\n tableSections: tableAllowed && row.table_sections ? JSON.parse(String(row.table_sections)) : [],\n qrMenuPublic: tableAllowed && vendorNoteFlagOn(row.notes != null ? String(row.notes) : \"\", \"qr_menu_public\"),\n });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürün & Kategori yardımcı: vendor sahipliği doğrula\n──────────────────────────────────────────────────────── */\nasync function verifyVendor(vendorId: string | string[] | undefined, vendorEmail: string | string[] | undefined): Promise {\n if (!vendorId || !vendorEmail) return null;\n const rows = r(await db.execute(sql`SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1`));\n return rows[0] ? Number(vendorId) : null;\n}\n\nasync function getVendorTypeForId(vid: number): Promise {\n const rows = r(\n await db.execute(sql`SELECT provider_type, vendor_type FROM vendors WHERE id = ${vid} LIMIT 1`),\n ) as Array<{ provider_type: string | null; vendor_type: string | null }>;\n const row = rows[0];\n return String(row?.provider_type || row?.vendor_type || \"\").toLowerCase();\n}\n\nasync function verifyVendorOwnsMenuItem(vid: number, itemId: number): Promise {\n const rows = r(await db.execute(sql`SELECT id FROM vendor_menu_items WHERE id = ${itemId} AND vendor_id = ${vid} LIMIT 1`));\n return Boolean(rows[0]);\n}\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürünleri + kategorileri listele\n GET /api/providers/products\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/products\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const [categories, items] = await Promise.all([\n db.execute(sql`\n SELECT id, name, position FROM vendor_menu_categories\n WHERE vendor_id = ${vid} AND active = true\n ORDER BY position, name\n `),\n db.execute(sql`\n SELECT mi.id, mi.name, mi.description, mi.price, mi.sale_price,\n mi.image_url, mi.active, mi.is_popular, mi.is_vegan, mi.is_spicy,\n mi.stock, mi.menu_category_id, mi.ecommerce_category_id,\n mc.name as category_name\n FROM vendor_menu_items mi\n LEFT JOIN vendor_menu_categories mc ON mc.id = mi.menu_category_id\n WHERE mi.vendor_id = ${vid} AND mi.active = true\n ORDER BY mc.position NULLS LAST, mi.name\n `),\n ]);\n\n res.json({ success: true, categories: r(categories), items: r(items) });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Yeni ürün ekle\n POST /api/providers/products\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/products\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const {\n name, price, salePrice, description, imageUrl, menuCategoryId, isPopular, isVegan, isSpicy, stock,\n ecommerceCategoryId, customCategoryName,\n } = req.body;\n if (!name || price === undefined) { res.status(400).json({ error: \"name ve price zorunlu\" }); return; }\n\n const vendorType = await getVendorTypeForId(vid);\n const resolved = await resolveProductCategoryForVendor({\n vendorId: vid,\n vendorType,\n ecommerceCategoryId: ecommerceCategoryId ? Number(ecommerceCategoryId) : null,\n customCategoryName: customCategoryName ? String(customCategoryName) : null,\n menuCategoryId: menuCategoryId ? Number(menuCategoryId) : null,\n });\n\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_menu_items (\n vendor_id, name, price, sale_price, description, image_url,\n menu_category_id, ecommerce_category_id, is_popular, is_vegan, is_spicy, stock, active\n )\n VALUES (\n ${vid}, ${name}, ${Number(price)}, ${salePrice ? Number(salePrice) : null},\n ${description || null}, ${imageUrl || null},\n ${resolved.menuCategoryId},\n ${resolved.ecommerceCategoryId},\n ${!!isPopular}, ${!!isVegan}, ${!!isSpicy},\n ${stock !== undefined ? Number(stock) : null}, true\n )\n RETURNING id, name, price, sale_price, description, image_url, menu_category_id, ecommerce_category_id, is_popular, active\n `));\n res.status(201).json({ success: true, item: rows[0] });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürün güncelle\n PUT /api/providers/products/:id\n──────────────────────────────────────────────────────── */\nrouter.put(\"/providers/products/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const itemId = Number(req.params.id);\n /* Ownership check */\n const own = r(await db.execute(sql`SELECT id FROM vendor_menu_items WHERE id = ${itemId} AND vendor_id = ${vid} LIMIT 1`));\n if (!own[0]) { res.status(403).json({ error: \"Bu ürün size ait değil\" }); return; }\n\n const { name, price, salePrice, description, imageUrl, menuCategoryId, isPopular, isVegan, isSpicy, stock, ecommerceCategoryId, customCategoryName } = req.body;\n\n const vendorType = await getVendorTypeForId(vid);\n let resolvedMenuCatId = menuCategoryId !== undefined ? (menuCategoryId ? Number(menuCategoryId) : null) : undefined;\n let resolvedEcomCatId: number | null | undefined = undefined;\n if (ecommerceCategoryId !== undefined || customCategoryName !== undefined) {\n const resolved = await resolveProductCategoryForVendor({\n vendorId: vid,\n vendorType,\n ecommerceCategoryId: ecommerceCategoryId ? Number(ecommerceCategoryId) : null,\n customCategoryName: customCategoryName ? String(customCategoryName) : null,\n menuCategoryId: menuCategoryId ? Number(menuCategoryId) : null,\n });\n resolvedMenuCatId = resolved.menuCategoryId;\n resolvedEcomCatId = resolved.ecommerceCategoryId;\n }\n\n const newPrice = price !== undefined ? Number(price) : null;\n const newSalePrice = salePrice !== undefined ? (salePrice ? Number(salePrice) : null) : undefined;\n const newDescription = description !== undefined ? (description || null) : null;\n const newImageUrl = imageUrl !== undefined ? (imageUrl || null) : null;\n const newCatId = resolvedMenuCatId !== undefined ? resolvedMenuCatId : (menuCategoryId !== undefined ? (menuCategoryId ? Number(menuCategoryId) : null) : null);\n const newEcomCatId = resolvedEcomCatId !== undefined ? resolvedEcomCatId : null;\n const newIsPopular = isPopular !== undefined ? !!isPopular : null;\n const newIsVegan = isVegan !== undefined ? !!isVegan : null;\n const newIsSpicy = isSpicy !== undefined ? !!isSpicy : null;\n const newStock = stock !== undefined ? Number(stock) : null;\n\n const rows = r(await db.execute(sql`\n UPDATE vendor_menu_items\n SET name = COALESCE(${name || null}, name),\n price = COALESCE(${newPrice}, price),\n sale_price = CASE WHEN ${newSalePrice !== undefined} THEN ${newSalePrice ?? null} ELSE sale_price END,\n description = COALESCE(${newDescription}, description),\n image_url = COALESCE(${newImageUrl}, image_url),\n menu_category_id = COALESCE(${newCatId}, menu_category_id),\n ecommerce_category_id = CASE WHEN ${resolvedEcomCatId !== undefined} THEN ${newEcomCatId ?? null} ELSE ecommerce_category_id END,\n is_popular = COALESCE(${newIsPopular}, is_popular),\n is_vegan = COALESCE(${newIsVegan}, is_vegan),\n is_spicy = COALESCE(${newIsSpicy}, is_spicy),\n stock = COALESCE(${newStock}, stock),\n updated_at = NOW()\n WHERE id = ${itemId} AND vendor_id = ${vid}\n RETURNING id, name, price, sale_price, description, image_url, menu_category_id, is_popular, active\n `));\n res.json({ success: true, item: rows[0] });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürün sil (soft)\n DELETE /api/providers/products/:id\n──────────────────────────────────────────────────────── */\nrouter.delete(\"/providers/products/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const itemId = Number(req.params.id);\n await db.execute(sql`UPDATE vendor_menu_items SET active = false WHERE id = ${itemId} AND vendor_id = ${vid}`);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Toplu ürün sil (soft)\n POST /api/providers/products/bulk-delete { ids: number[] }\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/products/bulk-delete\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const idsRaw = (req.body as { ids?: unknown }).ids;\n if (!Array.isArray(idsRaw) || idsRaw.length === 0) {\n res.status(400).json({ success: false, error: \"ids dizisi gerekli\" });\n return;\n }\n const ids = [...new Set(idsRaw.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0))];\n if (ids.length === 0) {\n res.status(400).json({ success: false, error: \"Geçerli ürün seçilmedi\" });\n return;\n }\n if (ids.length > 500) {\n res.status(400).json({ success: false, error: \"Tek seferde en fazla 500 ürün silinebilir\" });\n return;\n }\n\n const updated = await db\n .update(vendorMenuItemsTable)\n .set({ active: false, updatedAt: new Date() })\n .where(and(eq(vendorMenuItemsTable.vendorId, vid), inArray(vendorMenuItemsTable.id, ids)))\n .returning({ id: vendorMenuItemsTable.id });\n\n res.json({ success: true, deleted: updated.length });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Kategori listele\n GET /api/providers/categories\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/categories\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const rows = r(await db.execute(sql`SELECT id, name, position, is_custom, ecommerce_category_id FROM vendor_menu_categories WHERE vendor_id = ${vid} AND active = true ORDER BY position, name`));\n res.json({ success: true, categories: rows });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Global alışveriş kategori ağacı\n GET /api/providers/ecommerce-categories\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/ecommerce-categories\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const tree = await getEcommerceCategoryTree();\n res.json({ success: true, tree });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Ürün varyasyonları\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/item-options/:itemId\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const itemId = Number(req.params.itemId);\n if (!await verifyVendorOwnsMenuItem(vid, itemId)) { res.status(403).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`SELECT * FROM vendor_item_options WHERE menu_item_id = ${itemId} ORDER BY id`));\n res.json({ success: true, options: rows });\n});\n\nrouter.post(\"/providers/item-options\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { menuItemId, name, required, multiple, choices } = req.body ?? {};\n const itemId = Number(menuItemId);\n if (!itemId || !name || !Array.isArray(choices) || !choices.length) {\n res.status(400).json({ error: \"menuItemId, name ve choices zorunlu\" });\n return;\n }\n if (!await verifyVendorOwnsMenuItem(vid, itemId)) { res.status(403).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_item_options (menu_item_id, name, required, multiple, choices)\n VALUES (${itemId}, ${String(name)}, ${Boolean(required)}, ${Boolean(multiple)}, ${JSON.stringify(choices)}::jsonb)\n RETURNING *\n `));\n res.status(201).json({ success: true, option: rows[0] });\n});\n\nrouter.put(\"/providers/item-options/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const optId = Number(req.params.id);\n const { name, required, multiple, choices } = req.body ?? {};\n const own = r(await db.execute(sql`\n SELECT o.id FROM vendor_item_options o\n JOIN vendor_menu_items mi ON mi.id = o.menu_item_id\n WHERE o.id = ${optId} AND mi.vendor_id = ${vid}\n LIMIT 1\n `));\n if (!own[0]) { res.status(403).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n UPDATE vendor_item_options SET\n name = COALESCE(${name ?? null}, name),\n required = COALESCE(${required !== undefined ? Boolean(required) : null}, required),\n multiple = COALESCE(${multiple !== undefined ? Boolean(multiple) : null}, multiple),\n choices = COALESCE(${choices ? JSON.stringify(choices) : null}::jsonb, choices)\n WHERE id = ${optId}\n RETURNING *\n `));\n res.json({ success: true, option: rows[0] });\n});\n\nrouter.delete(\"/providers/item-options/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const optId = Number(req.params.id);\n const own = r(await db.execute(sql`\n SELECT o.id FROM vendor_item_options o\n JOIN vendor_menu_items mi ON mi.id = o.menu_item_id\n WHERE o.id = ${optId} AND mi.vendor_id = ${vid}\n LIMIT 1\n `));\n if (!own[0]) { res.status(403).json({ error: \"Yetkisiz\" }); return; }\n await db.execute(sql`DELETE FROM vendor_item_options WHERE id = ${optId}`);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Yeni kategori ekle\n POST /api/providers/categories\n──────────────────────────────────────────────────────── */\nrouter.post(\"/providers/categories\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const { name } = req.body;\n if (!name) { res.status(400).json({ error: \"Kategori adı zorunlu\" }); return; }\n\n const posRes = r(await db.execute(sql`SELECT COALESCE(MAX(position),0)+1 as pos FROM vendor_menu_categories WHERE vendor_id = ${vid}`));\n const pos = Number((posRes[0] as Row)?.pos ?? 1);\n const vendorType = await getVendorTypeForId(vid);\n const isShop = [\"alisveris\", \"ecommerce\"].includes(vendorType);\n\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_menu_categories (vendor_id, name, position, is_custom)\n VALUES (${vid}, ${name}, ${pos}, ${isShop})\n RETURNING id, name, position, is_custom, ecommerce_category_id\n `));\n res.status(201).json({ success: true, category: rows[0] });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Kategori sil (soft)\n DELETE /api/providers/categories/:id\n──────────────────────────────────────────────────────── */\nrouter.delete(\"/providers/categories/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const catId = Number(req.params.id);\n await db.execute(sql`UPDATE vendor_menu_categories SET active = false WHERE id = ${catId} AND vendor_id = ${vid}`);\n res.json({ success: true });\n});\n\n/* ────────────────────────────────────────────────────────\n PROVIDER — Siparişleri listele\n GET /api/providers/orders\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/orders\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) { res.status(401).json({ error: \"Kimlik doğrulama gerekli\" }); return; }\n\n const check = r(await db.execute(sql`SELECT id FROM vendors WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))} LIMIT 1`));\n if (!check[0]) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const orders = r(await db.execute(sql`\n SELECT o.id, o.order_number, o.customer_name, o.customer_phone,\n o.customer_address, o.customer_city, o.customer_district,\n o.customer_postal_code, o.customer_email, o.vendor_note,\n o.subtotal, o.delivery_fee, o.total, o.status,\n o.payment_method, o.payment_status, o.notes, o.items,\n o.driver_name, o.driver_phone, o.estimated_time,\n o.assigned_usta_id, o.usta_name,\n o.assigned_servis_id, o.servis_name,\n o.order_source, o.created_by_staff,\n o.platform_commission_amount, o.commission_base_amount,\n o.commission_rate_pct_snapshot, o.revenue_model_snapshot,\n o.geliver_shipment_id, o.geliver_tracking_number, o.geliver_label_url,\n o.geliver_transaction_id, o.geliver_status, o.geliver_last_error,\n o.created_at, o.updated_at\n FROM delivery_orders o\n WHERE o.vendor_id = ${Number(vendorId)}\n ORDER BY o.created_at DESC\n LIMIT 100\n `));\n\n res.json({ success: true, orders });\n});\n\nrouter.post(\"/providers/orders/:id/geliver-shipment\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await ensureGeliverIntegrationColumns();\n\n const orderId = Number(req.params.id);\n if (!Number.isFinite(orderId)) {\n res.status(400).json({ error: \"Geçersiz sipariş\" });\n return;\n }\n\n const force = String(req.query.force ?? \"\") === \"1\";\n const mode = String(req.query.mode ?? \"draft\").toLowerCase();\n\n if (mode === \"label\") {\n const result = await createGeliverShipmentForOrder({ orderId, vendorId: vid, force });\n if (!result.ok) {\n res.status(400).json({ error: result.error ?? \"geliver_failed\" });\n return;\n }\n res.json({ success: true });\n return;\n }\n\n const draft = await createGeliverDraftShipmentForOrder({ orderId, vendorId: vid, force });\n if (!draft.ok) {\n res.status(400).json({ error: draft.error ?? \"geliver_failed\" });\n return;\n }\n res.json({ success: true, shipmentId: draft.shipmentId, shipment: draft.shipment });\n});\n\n/* ────────────────────────────────────────────────────────\n PATCH /api/providers/orders/:id/status\n──────────────────────────────────────────────────────── */\nrouter.patch(\"/providers/orders/:id/status\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n\n const orderId = Number(req.params.id);\n const { status, estimatedTime: estimatedTimeRaw, vendorNote } = req.body as {\n status: string;\n estimatedTime?: number | string;\n vendorNote?: string;\n };\n\n const curr = r(await db.execute(sql`\n SELECT status FROM delivery_orders WHERE id = ${orderId} AND vendor_id = ${vid} LIMIT 1\n `));\n if (!curr[0]) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n\n const gate = deliveryStatusTransitionAllowed(\"vendor\", curr[0].status as string, status);\n if (!gate.ok) {\n res.status(400).json({ error: gate.error });\n return;\n }\n\n const tsField: Record = {\n confirmed: \"confirmed_at\", preparing: \"prepared_at\",\n picked_up: \"picked_up_at\", delivered: \"delivered_at\", cancelled: \"cancelled_at\",\n };\n const col = tsField[status];\n\n /* estimatedTime ve vendorNote sadece confirmed adımında yazılır */\n const etSql =\n status === \"confirmed\" && estimatedTimeRaw != null && estimatedTimeRaw !== \"\" && Number.isFinite(Number(estimatedTimeRaw))\n ? sql`, estimated_time = ${Number(estimatedTimeRaw)}`\n : sql.raw(\"\");\n const vnSql = (status === \"confirmed\" && vendorNote !== undefined)\n ? sql`, vendor_note = ${String(vendorNote)}`\n : sql.raw(\"\");\n\n const updated = r(await db.execute(sql`\n UPDATE delivery_orders\n SET status = ${status}, updated_at = now()\n ${col ? sql.raw(`, ${col} = now()`) : sql.raw(\"\")}\n ${etSql}\n ${vnSql}\n WHERE id = ${orderId} AND vendor_id = ${vid}\n RETURNING id, status, estimated_time, vendor_note, updated_at\n `));\n\n const fromStatus = curr[0].status as string;\n const etNum = estimatedTimeRaw != null && estimatedTimeRaw !== \"\" ? Number(estimatedTimeRaw) : NaN;\n const evNote =\n status === \"confirmed\" && Number.isFinite(etNum) && etNum > 0\n ? etNum >= 1440\n ? `Kargoya verilme: yaklaşık ${Math.round(etNum / 1440)} gün`\n : `Tahmini teslimat: ${Math.round(etNum)} dk`\n : null;\n await db.insert(deliveryOrderStatusEventsTable).values({\n orderId,\n fromStatus,\n toStatus: status,\n source: \"vendor_panel\",\n note: evNote,\n }).catch(() => {});\n\n if (status === \"confirmed\") {\n const [fullOrder] = await db\n .select()\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.vendorId, vid)))\n .limit(1);\n const [venRow] = await db\n .select({ name: vendorsTable.name, callmebotKey: vendorsTable.callmebotKey })\n .from(vendorsTable)\n .where(eq(vendorsTable.id, vid))\n .limit(1);\n if (fullOrder && venRow) {\n const track = fullOrder.orderNumber;\n const gTrack = fullOrder.geliverTrackingNumber?.trim();\n void notifyCustomerOrderWhatsApp({\n vendorId: vid,\n customerName: fullOrder.customerName,\n customerPhone: fullOrder.customerPhone,\n orderNumber: track,\n event: \"confirmed\",\n vendorNote: vendorNote != null ? String(vendorNote) : null,\n geliverTracking: gTrack || null,\n }).catch(() => {});\n const em = String((fullOrder as { customerEmail?: string | null }).customerEmail ?? \"\").trim();\n if (em) {\n const html = `

Merhaba ${escapeHtmlLite(fullOrder.customerName)},

\n

Siparişiniz işleme alındı.

\n

Takip numaranız: ${escapeHtmlLite(track)}

\n${gTrack ? `

Kargo takip: ${escapeHtmlLite(gTrack)}

` : \"\"}\n${vendorNote ? `

İşletme notu: ${escapeHtmlLite(String(vendorNote))}

` : \"\"}\n

${escapeHtmlLite(venRow.name)}

`;\n const textLines = [\n `Merhaba ${fullOrder.customerName},`,\n `Siparişiniz işleme alındı.`,\n `Takip: ${track}`,\n gTrack ? `Kargo: ${gTrack}` : \"\",\n vendorNote ? `Not: ${String(vendorNote).trim()}` : \"\",\n venRow.name,\n ].filter(Boolean).join(\"\\n\");\n void sendEmail({\n to: em,\n subject: `Siparişiniz alındı — ${venRow.name}`,\n html,\n text: textLines,\n }).catch(() => {});\n }\n }\n }\n\n res.json({ success: true, order: updated[0] });\n});\n\n/* — Kurye Yönetimi (Vendor) ──────────────────────────────────── */\n\nrouter.get(\"/providers/couriers\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, name, phone, active, created_at FROM vendor_couriers\n WHERE vendor_id = ${vid} ORDER BY id ASC\n `));\n res.json(rows);\n});\n\nrouter.post(\"/providers/couriers\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { name, phone, password } = req.body as { name: string; phone: string; password?: string };\n if (!name || !phone) { res.status(400).json({ error: \"Ad ve telefon zorunlu\" }); return; }\n const rawPassword = password?.trim() || phone.trim();\n const hash = await bcrypt.hash(rawPassword, 10);\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_couriers (vendor_id, name, phone, password_hash)\n VALUES (${vid}, ${name}, ${phone}, ${hash})\n RETURNING id, name, phone, active, created_at\n `));\n res.status(201).json({ ...rows[0], _defaultPassword: rawPassword });\n});\n\nrouter.delete(\"/providers/couriers/:id\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n await db.execute(sql`\n DELETE FROM vendor_couriers WHERE id = ${Number(req.params.id)} AND vendor_id = ${vid}\n `);\n res.json({ success: true });\n});\n\n/* Vendor assigns a courier to an order */\nrouter.patch(\"/providers/orders/:id/assign-courier\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { courierId } = req.body as { courierId: number };\n const [courier] = r(await db.execute(sql`\n SELECT name, phone FROM vendor_couriers WHERE id = ${Number(courierId)} AND vendor_id = ${vid} LIMIT 1\n `));\n if (!courier) { res.status(404).json({ error: \"Kurye bulunamadı\" }); return; }\n await db.execute(sql`\n UPDATE delivery_orders SET driver_name = ${courier.name as string}, driver_phone = ${courier.phone as string},\n updated_at = now() WHERE id = ${Number(req.params.id)} AND vendor_id = ${vid}\n `);\n res.json({ success: true, driverName: courier.name, driverPhone: courier.phone });\n});\n\n/* — Kurye Paneli (Public) ──────────────────────────────────── */\n\n/* Kurye login — phone + password */\nrouter.post(\"/courier/login\", async (req, res): Promise => {\n const { phone, password } = req.body as { phone?: string; password?: string };\n if (!phone || !password) { res.status(400).json({ error: \"Telefon ve şifre zorunlu\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, name, phone, active, password_hash\n FROM vendor_couriers WHERE phone = ${phone.trim()} LIMIT 1\n `));\n if (!rows[0]) { res.status(401).json({ error: \"Telefon numarası kayıtlı değil\" }); return; }\n const courier = rows[0];\n if (!courier.active) { res.status(401).json({ error: \"Hesabınız aktif değil. İşletme sahibiyle iletişime geçin.\" }); return; }\n const storedHash = courier.password_hash as string | null;\n if (!storedHash) {\n res.status(403).json({ error: \"Bu kurye hesabı için şifre tanımlanmamış. İşletme panelinden şifre atayın.\" });\n return;\n }\n const ok = await bcrypt.compare(password, storedHash);\n if (!ok) { res.status(401).json({ error: \"Şifre yanlış\" }); return; }\n const token = signCourierSessionToken(Number(courier.id), String(courier.phone));\n res.json({ id: courier.id, name: courier.name, phone: courier.phone, token });\n});\n\nrouter.use(\"/courier\", requireCourierSession);\n\n/* Kurye şifre değiştirme */\nrouter.patch(\"/courier/change-password\", async (req, res): Promise => {\n const { currentPassword, newPassword } = req.body as { currentPassword?: string; newPassword?: string };\n const phone = courierPhoneFromSession(req);\n if (!phone || !currentPassword || !newPassword) { res.status(400).json({ error: \"Eksik bilgi\" }); return; }\n if (newPassword.length < 4) { res.status(400).json({ error: \"Yeni şifre en az 4 karakter olmalı\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT id, active, password_hash FROM vendor_couriers WHERE phone = ${phone.trim()} LIMIT 1\n `));\n if (!rows[0]) { res.status(404).json({ error: \"Kurye bulunamadı\" }); return; }\n const courier = rows[0];\n if (!courier.active) { res.status(401).json({ error: \"Hesabınız aktif değil\" }); return; }\n const storedHash = courier.password_hash as string | null;\n const ok = storedHash ? await bcrypt.compare(currentPassword, storedHash) : false;\n if (!ok) { res.status(401).json({ error: \"Mevcut şifre yanlış\" }); return; }\n const newHash = await bcrypt.hash(newPassword, 10);\n await db.execute(sql`UPDATE vendor_couriers SET password_hash = ${newHash} WHERE id = ${courier.id as number}`);\n res.json({ success: true });\n});\n\n/* Kurye phone ile login → kendine atanmış siparişleri görür */\nrouter.get(\"/courier/orders\", async (req, res): Promise => {\n const phone = courierPhoneFromSession(req);\n if (!phone) { res.status(400).json({ error: \"Telefon zorunlu\" }); return; }\n const rows = r(await db.execute(sql`\n SELECT o.*, v.name as vendor_name, v.address as vendor_address, v.phone as vendor_phone\n FROM delivery_orders o\n JOIN vendors v ON v.id = o.vendor_id\n WHERE o.driver_phone = ${phone}\n AND o.status NOT IN ('cancelled')\n ORDER BY o.created_at DESC LIMIT 50\n `));\n res.json(rows);\n});\n\n/* Kurye sipariş durumunu günceller: picked_up → delivered + proof photo */\nrouter.patch(\"/courier/orders/:id/status\", async (req, res): Promise => {\n const { status, proofUrl } = req.body as { status: string; proofUrl?: string };\n const phone = courierPhoneFromSession(req);\n if (!phone || !status) { res.status(400).json({ error: \"Eksik alan\" }); return; }\n\n const curr = r(await db.execute(sql`\n SELECT id, status FROM delivery_orders WHERE id = ${Number(req.params.id)} AND driver_phone = ${phone} LIMIT 1\n `));\n if (!curr[0]) { res.status(404).json({ error: \"Sipariş bulunamadı veya bu siparişe atanmış değilsiniz\" }); return; }\n\n const fromCourier = curr[0].status as string;\n const gate = deliveryStatusTransitionAllowed(\"courier\", fromCourier, status);\n if (!gate.ok) {\n res.status(400).json({ error: gate.error });\n return;\n }\n\n const tsField: Record = { picked_up: \"picked_up_at\", on_the_way: \"picked_up_at\", delivered: \"delivered_at\" };\n const col = tsField[status];\n const orderId = Number(req.params.id);\n\n await db.execute(sql`\n UPDATE delivery_orders\n SET status = ${status}, updated_at = now()\n ${col ? sql.raw(`, ${col} = now()`) : sql.raw(\"\")}\n ${proofUrl && status === \"delivered\" ? sql`, delivery_proof_url = ${proofUrl}` : sql.raw(\"\")}\n WHERE id = ${orderId} AND driver_phone = ${phone}\n `);\n\n await db.insert(deliveryOrderStatusEventsTable).values({\n orderId,\n fromStatus: fromCourier,\n toStatus: status,\n source: \"courier_panel\",\n note: null,\n }).catch(() => {});\n\n res.json({ success: true, status });\n});\n\n/* Kurye canlı konum ping */\nrouter.post(\"/courier/location\", async (req, res): Promise => {\n const { orderId, lat, lng, accuracy, heading, speed } = req.body as {\n phone?: string; orderId?: number; lat?: number; lng?: number; accuracy?: number; heading?: number; speed?: number;\n };\n const courierPhone = courierPhoneFromSession(req);\n const latNum = Number(lat);\n const lngNum = Number(lng);\n if (!courierPhone || !Number.isFinite(latNum) || !Number.isFinite(lngNum)) {\n res.status(400).json({ error: \"phone, lat, lng zorunlu\" }); return;\n }\n\n const rows = r(await db.execute(sql`\n SELECT id, active FROM vendor_couriers WHERE phone = ${courierPhone} LIMIT 1\n `));\n if (!rows[0]) { res.status(404).json({ error: \"Kurye bulunamadı\" }); return; }\n if (!rows[0].active) { res.status(401).json({ error: \"Kurye pasif\" }); return; }\n const courierId = Number(rows[0].id);\n\n let orderIdNum: number | null = null;\n if (orderId !== undefined && orderId !== null && Number.isFinite(Number(orderId))) {\n const check = r(await db.execute(sql`\n SELECT id FROM delivery_orders\n WHERE id = ${Number(orderId)} AND driver_phone = ${courierPhone}\n LIMIT 1\n `));\n if (check[0]) orderIdNum = Number(check[0].id);\n }\n if (!orderIdNum) {\n const activeOrder = r(await db.execute(sql`\n SELECT id FROM delivery_orders\n WHERE driver_phone = ${courierPhone}\n AND status IN ('picked_up', 'on_the_way')\n ORDER BY updated_at DESC\n LIMIT 1\n `));\n if (activeOrder[0]) orderIdNum = Number(activeOrder[0].id);\n }\n\n const rowsIns = r(await db.execute(sql`\n INSERT INTO driver_locations (order_id, courier_id, courier_phone, lat, lng, accuracy, heading, speed, source)\n VALUES (\n ${orderIdNum},\n ${courierId},\n ${courierPhone},\n ${latNum},\n ${lngNum},\n ${Number.isFinite(Number(accuracy)) ? Number(accuracy) : null},\n ${Number.isFinite(Number(heading)) ? Number(heading) : null},\n ${Number.isFinite(Number(speed)) ? Number(speed) : null},\n 'courier_panel'\n )\n RETURNING id, order_id, lat, lng, created_at\n `));\n\n res.json({ success: true, location: rowsIns[0] ?? null });\n});\n\n/* — Mesajlaşma (Vendor Panel & Kurye Panel) ──────────────────── */\n\n/* Vendor: sipariş mesajlarını listele */\nrouter.get(\"/providers/orders/:id/messages\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const orderId = Number(req.params.id);\n /* Siparişin bu vendor'a ait olduğunu doğrula */\n const [order] = await db.select({ id: deliveryOrdersTable.id })\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.vendorId, vid)))\n .limit(1);\n if (!order) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n const msgs = await db.select().from(orderMessagesTable)\n .where(eq(orderMessagesTable.orderId, orderId))\n .orderBy(orderMessagesTable.createdAt);\n res.json(msgs);\n});\n\n/* Vendor: mesaj gönder */\nrouter.post(\"/providers/orders/:id/messages\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const { senderName, message } = req.body as { senderName: string; message: string };\n if (!message?.trim()) { res.status(400).json({ error: \"Mesaj boş olamaz\" }); return; }\n const orderId = Number(req.params.id);\n const [order] = await db.select({ id: deliveryOrdersTable.id })\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.vendorId, vid)))\n .limit(1);\n if (!order) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n const [msg] = await db.insert(orderMessagesTable).values({\n orderId, senderType: \"vendor\",\n senderName: senderName || \"İşletme\", message: message.trim(),\n }).returning();\n res.status(201).json(msg);\n});\n\n/* Kurye: mesajları listele */\nrouter.get(\"/courier/orders/:id/messages\", async (req, res): Promise => {\n const orderId = Number(req.params.id);\n const phone = courierPhoneFromSession(req);\n const [order] = await db.select({ id: deliveryOrdersTable.id })\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.driverPhone, phone)))\n .limit(1);\n if (!order) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n const msgs = await db.select().from(orderMessagesTable)\n .where(eq(orderMessagesTable.orderId, orderId))\n .orderBy(orderMessagesTable.createdAt);\n res.json(msgs);\n});\n\n/* Kurye: mesaj gönder */\nrouter.post(\"/courier/orders/:id/messages\", async (req, res): Promise => {\n const { senderName, message } = req.body as { senderName: string; message: string };\n if (!message?.trim()) { res.status(400).json({ error: \"Mesaj boş olamaz\" }); return; }\n const orderId = Number(req.params.id);\n const phone = courierPhoneFromSession(req);\n const [order] = await db.select({ id: deliveryOrdersTable.id })\n .from(deliveryOrdersTable)\n .where(and(eq(deliveryOrdersTable.id, orderId), eq(deliveryOrdersTable.driverPhone, phone)))\n .limit(1);\n if (!order) { res.status(404).json({ error: \"Sipariş bulunamadı\" }); return; }\n const [msg] = await db.insert(orderMessagesTable).values({\n orderId, senderType: \"courier\",\n senderName: senderName || \"Kurye\", message: message.trim(),\n }).returning();\n res.status(201).json(msg);\n});\n\n/* Vendor: dış platform URL'sinden menü önizleme (kaydetmez) */\nrouter.post(\"/providers/import/external-menu/preview\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const body = req.body as Record;\n const sourceUrl = String(body.sourceUrl ?? \"\").trim();\n if (!sourceUrl) { res.status(400).json({ error: \"sourceUrl zorunlu\" }); return; }\n try {\n const imported = await importVendorFromExternalUrl(sourceUrl, {\n prefetchedHtml: pickPrefetchedHtml(body),\n prefetchedMenuJson: pickPrefetchedMenuJson(body),\n });\n res.json({ success: true, ...externalMenuPreviewPayload(imported) });\n } catch (e: any) {\n res.status(500).json({ error: e?.message || \"Menü önizleme başarısız\" });\n }\n});\n\n/* Vendor: dış platform URL'sinden kendi menüsünü içe aktar */\nrouter.post(\"/providers/import/external-menu\", async (req, res): Promise => {\n const vid = await verifyVendor(req.headers[\"x-vendor-id\"], req.headers[\"x-vendor-email\"]);\n if (!vid) { res.status(401).json({ error: \"Yetkisiz\" }); return; }\n const body = req.body as Record;\n const sourceUrl = String(body.sourceUrl ?? \"\").trim();\n const selectedCategories = Array.isArray(body.selectedCategories)\n ? (body.selectedCategories as unknown[]).map((c) => String(c)).filter(Boolean)\n : undefined;\n const menu = Array.isArray(body.menu) ? (body.menu as ImportedMenuItem[]) : undefined;\n if (!sourceUrl && !(menu && menu.length > 0)) {\n res.status(400).json({ error: \"sourceUrl veya menu zorunlu\" });\n return;\n }\n try {\n const { imported, menuStats } = await runExternalMenuImportForVendor(vid, {\n sourceUrl: sourceUrl || undefined,\n prefetchedHtml: pickPrefetchedHtml(body),\n prefetchedMenuJson: pickPrefetchedMenuJson(body),\n menu,\n selectedCategories,\n city: body.city != null ? String(body.city) : undefined,\n district: body.district != null ? String(body.district) : undefined,\n neighborhood: body.neighborhood != null ? String(body.neighborhood) : undefined,\n address: body.address != null ? String(body.address) : undefined,\n phone: body.phone != null ? String(body.phone) : undefined,\n lat: body.lat != null ? Number(body.lat) : undefined,\n lng: body.lng != null ? Number(body.lng) : undefined,\n rating: body.rating != null ? Number(body.rating) : undefined,\n reviewCount: body.reviewCount != null ? Number(body.reviewCount) : undefined,\n platform: body.platform != null ? String(body.platform) : undefined,\n name: body.name != null ? String(body.name) : undefined,\n });\n res.json({\n success: true,\n vendorId: vid,\n importedMeta: {\n platform: imported.platform,\n itemCount: menuStats.items,\n rating: imported.rating ?? null,\n reviewCount: imported.reviewCount ?? null,\n },\n menuStats,\n });\n } catch (e: any) {\n res.status(500).json({ error: e?.message || \"Menü içe aktarma başarısız\" });\n }\n});\n\n/* — Admin: Vendor Kategori Yönetimi ──────────────────────────── */\n\nrouter.get(\"/admin/vendor-categories\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const rows = r(await db.execute(sql`\n SELECT id, name, slug, icon, image_url, position, active, super_category, created_at\n FROM vendor_categories ORDER BY position ASC, id ASC\n `));\n res.json(rows);\n});\n\nrouter.post(\"/admin/vendor-categories\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { name, slug, icon, imageUrl, position, superCategory } = req.body as Record;\n if (!name || !slug) { res.status(400).json({ error: \"Ad ve slug zorunlu\" }); return; }\n const rows = r(await db.execute(sql`\n INSERT INTO vendor_categories (name, slug, icon, image_url, position, super_category)\n VALUES (${name as string}, ${slug as string}, ${(icon as string) || null}, ${(imageUrl as string) || null},\n ${Number(position) || 0}, ${(superCategory as string) || \"siparis\"})\n RETURNING id, name, slug, icon, image_url, position, active, super_category, created_at\n `));\n res.status(201).json(rows[0]);\n});\n\nrouter.put(\"/admin/vendor-categories/:id\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { name, slug, icon, imageUrl, position, superCategory, active } = req.body as Record;\n const parts: string[] = [];\n const vals: (string | number | boolean | null)[] = [];\n if (name !== undefined) { parts.push(`name = $${parts.length+1}`); vals.push(name as string); }\n if (slug !== undefined) { parts.push(`slug = $${parts.length+1}`); vals.push(slug as string); }\n if (icon !== undefined) { parts.push(`icon = $${parts.length+1}`); vals.push((icon as string) || null); }\n if (imageUrl !== undefined) { parts.push(`image_url = $${parts.length+1}`); vals.push((imageUrl as string) || null); }\n if (position !== undefined) { parts.push(`position = $${parts.length+1}`); vals.push(Number(position) || 0); }\n if (superCategory !== undefined) { parts.push(`super_category = $${parts.length+1}`);vals.push(superCategory as string); }\n if (active !== undefined) { parts.push(`active = $${parts.length+1}`); vals.push(Boolean(active)); }\n if (parts.length === 0) { res.status(400).json({ error: \"Güncellenecek alan yok\" }); return; }\n vals.push(Number(req.params.id));\n const { pool } = await import(\"@workspace/db\");\n const r2 = await pool.query(\n `UPDATE vendor_categories SET ${parts.join(\", \")} WHERE id = $${parts.length+1} RETURNING id, name, slug, icon, image_url, position, active, super_category, created_at`,\n vals\n );\n res.json(r2.rows[0] ?? {});\n});\n\nrouter.delete(\"/admin/vendor-categories/:id\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n await db.execute(sql`DELETE FROM vendor_categories WHERE id = ${Number(req.params.id)}`);\n res.json({ success: true });\n});\n\n/* — Mağaza vitrin teması ve özel alan — */\n\nrouter.get(\"/providers/storefront/theme\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n await ensureVendorThemeColumns();\n\n const rows = r(await db.execute(sql`\n SELECT id, slug, vendor_type, provider_type, provider_subtype, theme_key, theme_config\n FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `));\n const v = rows[0];\n if (!v) {\n res.status(401).json({ error: \"Geçersiz oturum\" });\n return;\n }\n\n const vendorCtx = {\n vendorType: String(v.vendor_type ?? \"\"),\n providerType: String(v.provider_type ?? \"\"),\n providerSubtype: String(v.provider_subtype ?? \"\"),\n };\n const themeKey = String(v.theme_key ?? \"\").trim() || resolveDefaultThemeKey(vendorCtx);\n const savedRaw = v.theme_config;\n const saved =\n savedRaw && typeof savedRaw === \"object\" && !Array.isArray(savedRaw)\n ? (savedRaw as Record)\n : typeof savedRaw === \"string\"\n ? (() => {\n try {\n return JSON.parse(savedRaw) as Record;\n } catch {\n return {};\n }\n })()\n : {};\n\n const domains = await listVendorDomains(Number(vendorId));\n const available = themesForVendor(vendorCtx);\n const internalStorefrontPath = resolveVendorStorefrontPath({ slug: String(v.slug ?? \"\"), ...vendorCtx });\n const approvedDomain = domains.find((d) => d.status === \"approved\" || d.verified_at);\n const shortPath = resolveVendorCustomDomainShortPath({\n id: Number(vendorId),\n name: undefined,\n slug: String(v.slug ?? \"\"),\n });\n const storefrontPath = approvedDomain?.domain\n ? `https://${approvedDomain.domain}${shortPath}`\n : internalStorefrontPath;\n\n res.json({\n success: true,\n themeKey,\n themeConfig: mergeThemeConfig(themeKey, saved),\n defaultThemeKey: resolveDefaultThemeKey(vendorCtx),\n storefrontPath,\n internalStorefrontPath,\n shortPath,\n availableThemes: available,\n allThemes: VENDOR_THEME_CATALOG,\n domains,\n ...readVendorNavMenuFromThemeConfig(saved),\n });\n});\n\nrouter.put(\"/providers/storefront/theme\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n await ensureVendorThemeColumns();\n\n const body = req.body as Record;\n const hasThemeKey = Object.prototype.hasOwnProperty.call(body, \"themeKey\");\n const hasThemeConfig = Object.prototype.hasOwnProperty.call(body, \"themeConfig\");\n const hasNavMenuEnabled = Object.prototype.hasOwnProperty.call(body, \"navMenuEnabled\");\n const hasNavMenuItems = Object.prototype.hasOwnProperty.call(body, \"navMenuItems\");\n const hasStripMenuEnabled = Object.prototype.hasOwnProperty.call(body, \"stripMenuEnabled\");\n const hasStripMenuItems = Object.prototype.hasOwnProperty.call(body, \"stripMenuItems\");\n\n const current = r(await db.execute(sql`\n SELECT id, vendor_type, provider_type, provider_subtype, theme_key, theme_config\n FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `))[0];\n if (!current) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const vendorCtx = {\n vendorType: String(current.vendor_type ?? \"\"),\n providerType: String(current.provider_type ?? \"\"),\n providerSubtype: String(current.provider_subtype ?? \"\"),\n };\n\n let nextKey = String(current.theme_key ?? \"\").trim() || resolveDefaultThemeKey(vendorCtx);\n if (hasThemeKey) {\n const raw = String(body.themeKey ?? \"\").trim().toLowerCase();\n const allowed = themesForVendor(vendorCtx);\n if (!allowed.some((t) => t.key === raw)) {\n res.status(400).json({ error: \"Bu mağaza türü için tema seçilemez\" });\n return;\n }\n nextKey = raw;\n }\n\n let nextConfig: Record = {};\n const prevRaw = current.theme_config;\n if (prevRaw && typeof prevRaw === \"object\" && !Array.isArray(prevRaw)) {\n nextConfig = { ...(prevRaw as Record) };\n } else if (typeof prevRaw === \"string\") {\n try {\n nextConfig = JSON.parse(prevRaw) as Record;\n } catch {\n nextConfig = {};\n }\n }\n\n if (hasThemeConfig && body.themeConfig && typeof body.themeConfig === \"object\") {\n const def = themeDefByKey(nextKey);\n const incoming = body.themeConfig as Record;\n for (const slot of def?.slots ?? []) {\n if (Object.prototype.hasOwnProperty.call(incoming, slot.key)) {\n const val = incoming[slot.key];\n nextConfig[slot.key] = val == null ? \"\" : String(val).trim().slice(0, 4000);\n }\n }\n }\n\n if (hasNavMenuEnabled) {\n nextConfig.navMenuEnabled = body.navMenuEnabled === true;\n }\n\n if (hasNavMenuItems) {\n const rawItems = Array.isArray(body.navMenuItems) ? body.navMenuItems : [];\n const cleaned = cleanVendorNavMenuItems(\n rawItems.map((item, index) => {\n const o = item && typeof item === \"object\" && !Array.isArray(item) ? (item as Record) : {};\n return {\n id: String(o.id ?? `vnav-${index + 1}`),\n label: String(o.label ?? \"\"),\n href: String(o.href ?? \"#\"),\n enabled: o.enabled === false ? false : true,\n };\n }),\n );\n if (cleaned?.length) nextConfig.navMenuItems = cleaned;\n else delete nextConfig.navMenuItems;\n }\n\n if (hasStripMenuEnabled) {\n nextConfig.stripMenuEnabled = body.stripMenuEnabled === true;\n }\n\n if (hasStripMenuItems) {\n const rawItems = Array.isArray(body.stripMenuItems) ? body.stripMenuItems : [];\n const cleaned = cleanVendorNavMenuItems(\n rawItems.map((item, index) => {\n const o = item && typeof item === \"object\" && !Array.isArray(item) ? (item as Record) : {};\n return {\n id: String(o.id ?? `vstrip-${index + 1}`),\n label: String(o.label ?? \"\"),\n href: String(o.href ?? \"#\"),\n enabled: o.enabled === false ? false : true,\n };\n }),\n );\n if (cleaned?.length) nextConfig.stripMenuItems = cleaned;\n else delete nextConfig.stripMenuItems;\n }\n\n const rows = r(await db.execute(sql`\n UPDATE vendors\n SET theme_key = ${nextKey},\n theme_config = ${JSON.stringify(nextConfig)}::jsonb,\n updated_at = NOW()\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n RETURNING theme_key, theme_config\n `));\n\n res.json({\n success: true,\n themeKey: String(rows[0]?.theme_key ?? nextKey),\n themeConfig: mergeThemeConfig(nextKey, nextConfig),\n ...readVendorNavMenuFromThemeConfig(nextConfig),\n });\n});\n\nrouter.put(\"/providers/storefront/domains\", async (req, res): Promise => {\n const vendorId = req.headers[\"x-vendor-id\"];\n const vendorEmail = req.headers[\"x-vendor-email\"];\n if (!vendorId || !vendorEmail) {\n res.status(401).json({ error: \"Kimlik doğrulama gerekli\" });\n return;\n }\n await ensureVendorCustomDomainsTable();\n\n const check = r(await db.execute(sql`\n SELECT id, name, slug FROM vendors\n WHERE id = ${Number(vendorId)} AND ${sqlVendorSessionEmailMatches(String(vendorEmail))}\n LIMIT 1\n `))[0];\n if (!check) {\n res.status(401).json({ error: \"Yetkisiz\" });\n return;\n }\n\n const rawList = (req.body as { domains?: unknown }).domains;\n if (!Array.isArray(rawList)) {\n res.status(400).json({ error: \"domains dizisi gerekli\" });\n return;\n }\n\n const normalized: string[] = [];\n const seen = new Set();\n for (const item of rawList.slice(0, 3)) {\n const d = normalizeVendorDomain(String(item ?? \"\"));\n if (!d || seen.has(d)) continue;\n seen.add(d);\n normalized.push(d);\n }\n\n for (const d of normalized) {\n const conflict = r(await db.execute(sql`\n SELECT vendor_id FROM vendor_custom_domains\n WHERE lower(trim(domain)) = ${d} AND vendor_id <> ${Number(vendorId)}\n LIMIT 1\n `))[0];\n if (conflict) {\n res.status(409).json({ error: `${d} başka bir mağazaya bağlı` });\n return;\n }\n }\n\n await db.execute(sql`DELETE FROM vendor_custom_domains WHERE vendor_id = ${Number(vendorId)}`);\n\n let pos = 1;\n for (const d of normalized) {\n await db.execute(sql`\n INSERT INTO vendor_custom_domains (vendor_id, domain, position, status, verified_at, requested_at, updated_at)\n VALUES (${Number(vendorId)}, ${d}, ${pos}, 'pending', NULL, NOW(), NOW())\n `);\n pos += 1;\n }\n\n if (normalized.length > 0) {\n notifyAdminWhatsApp({\n eventType: \"Servis Sağlayıcı Domain Talebi\",\n details: [\n `🏢 ${String(check.name ?? \"Servis sağlayıcı\")}`,\n `🆔 Vendor ID: ${Number(vendorId)}`,\n `🔗 ${normalized.join(\", \")}`,\n `🌐 DNS: ns1.vercel-dns.com / ns2.vercel-dns.com`,\n `📌 Admin panelden onay bekliyor`,\n ].join(\"\\n\"),\n }).catch(() => {});\n }\n\n const domains = await listVendorDomains(Number(vendorId));\n res.json({ success: true, domains, message: \"Domain talebiniz admin onayına gönderildi\" });\n});\n\nrouter.get(\"/admin/vendor-domain-requests\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n await ensureVendorCustomDomainsTable();\n const rows = r(await db.execute(sql`\n SELECT d.id, d.vendor_id, d.domain, d.position, d.status, d.admin_note, d.verified_at,\n d.requested_at, d.approved_at, d.rejected_at,\n v.name AS vendor_name, v.slug AS vendor_slug, v.vendor_type, v.provider_type, v.provider_subtype\n FROM vendor_custom_domains d\n JOIN vendors v ON v.id = d.vendor_id\n ORDER BY\n CASE WHEN d.status = 'pending' THEN 0 WHEN d.status = 'approved' THEN 1 ELSE 2 END,\n d.requested_at DESC,\n d.id DESC\n LIMIT 200\n `));\n res.json({ success: true, requests: rows });\n});\n\nrouter.put(\"/admin/vendor-domain-requests/:id\", async (req, res): Promise => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n await ensureVendorCustomDomainsTable();\n const id = Number(req.params.id);\n const body = req.body as { action?: unknown; adminNote?: unknown };\n const action = String(body.action ?? \"\").trim().toLowerCase();\n const adminNote = String(body.adminNote ?? \"\").trim().slice(0, 1000) || null;\n if (!Number.isFinite(id) || id <= 0) {\n res.status(400).json({ error: \"Geçersiz kayıt\" });\n return;\n }\n if (action !== \"approve\" && action !== \"reject\") {\n res.status(400).json({ error: \"action approve veya reject olmalı\" });\n return;\n }\n\n const rows = r(await db.execute(sql`\n UPDATE vendor_custom_domains\n SET status = ${action === \"approve\" ? \"approved\" : \"rejected\"},\n admin_note = ${adminNote},\n verified_at = ${action === \"approve\" ? sql`NOW()` : sql`NULL`},\n approved_at = ${action === \"approve\" ? sql`NOW()` : sql`NULL`},\n rejected_at = ${action === \"reject\" ? sql`NOW()` : sql`NULL`},\n updated_at = NOW()\n WHERE id = ${id}\n RETURNING id, vendor_id, domain, status, verified_at, admin_note, approved_at, rejected_at\n `));\n const row = rows[0];\n if (!row) {\n res.status(404).json({ error: \"Talep bulunamadı\" });\n return;\n }\n res.json({ success: true, request: row });\n});\n\nexport default router;\n"}}}}