Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/ahenkbt_haberler.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
397 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nfunction normGeliverMahalle(v: unknown): string | null {\n if (v == null) return null;\n const s = String(v).trim();\n return s.length ? s : null;\n}\n\n/* Auto-migrate: şifre sütunları */\n(async () => {\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<string, string> = {\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<string, string> = {\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<string, unknown>;\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<boolean> {\n if (!vendorId || !vendorEmail) return false;\n const own = r(\n await db.execute<Row>(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<string, number>();\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<string, unknown> | 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<string, unknown> | 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<Row>(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<void> => {\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<string, unknown>;\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<Row>(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<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { status, type, q } = req.query as Record<string, string>;\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<Row>(\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<void> => {\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<Row>(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<Row>(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<void> => {\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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const { notes } = req.body;\n await db.execute<Row>(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<void> => {\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<string, unknown>;\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<Row>(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<void> => {\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<string, unknown>;\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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<string, unknown>).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 <signed vendor token>\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/me\", async (req, res): Promise<void> => {\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<Row>(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<string, unknown>).paytr_merchant_id_masked = paytrMerchantIdRaw ? `****${paytrMerchantIdRaw.slice(-4)}` : \"\";\n (v as Record<string, unknown>).paytr_configured = paytrConfigured;\n (v as Record<string, unknown>).iyzico_configured = iyzicoConfigured;\n\n const geliverTok = String(v.geliver_api_token ?? \"\").trim();\n delete v.geliver_api_token;\n (v as Record<string, unknown>).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<Row>(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<string, unknown>).membership_pricing = membershipPricingFromSiteRow(siteRow);\n\n await ensureVendorCustomDomainsTable();\n (v as Record<string, unknown>).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<void> => {\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<string, unknown>;\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<Row>(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<void> => {\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<string, string | undefined>;\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<Row>(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<void> => {\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<string, unknown>;\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<Row>(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<void> => {\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<Row>(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<string, unknown>;\n const rec = body.recipient as Record<string, unknown> | 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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<string, unknown>) ?? {}) });\n});\n\nrouter.get(\"/providers/geliver/shipment/:shipmentId\", async (req, res): Promise<void> => {\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<Row>(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<void> => {\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<Row>(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<string, unknown>;\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<void> => {\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<Row>(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<string, unknown>;\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<string, unknown>,\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<void> => {\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<string, unknown>)?.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<void> => {\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<Row>(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<string, unknown> = { 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<string, unknown>);\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<void> => {\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<void> => {\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<void> => {\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<string, unknown>;\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<void> => {\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<void> => {\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<void> => {\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<string, unknown>;\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<void> => {\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<void> => {\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<string, unknown>;\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<string, unknown>, 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<string, unknown>;\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<string, unknown>,\n headers: Record<string, unknown>,\n): Promise<void> {\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<string, unknown>) : 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<typeof deliveryOrdersTable.$inferInsert> = {\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<void> {\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<string, unknown>;\n try {\n body = JSON.parse(buf.toString(\"utf8\")) as Record<string, unknown>;\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<string, unknown>);\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<void> => {\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<void> => {\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<string, unknown>;\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<void> => {\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<void> => {\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<void> => {\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<void> => {\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<string, unknown>;\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<string, unknown>)\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<void> => {\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<void> => {\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<Row>(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<string, unknown>;\n\n const [cur] = r(\n await db.execute<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<string, unknown>;\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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const status = String(req.query.status || \"pending\");\n const rows = r(await db.execute<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<string, unknown>;\n const { tableServiceEnabled, reservationEnabled, reservationAutoConfirm, tableSections, qrMenuPublic } = body;\n const vid = Number(vendorId);\n const vendorRows = r(\n await db.execute<Row>(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<void> => {\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<Row>(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<number | null> {\n if (!vendorId || !vendorEmail) return null;\n const rows = r(await db.execute<Row>(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<string> {\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<boolean> {\n const rows = r(await db.execute<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<void> => {\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<Row>(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<void> => {\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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<void> => {\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<Row>(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<string, string> = {\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<Row>(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 = `<p>Merhaba ${escapeHtmlLite(fullOrder.customerName)},</p>\n<p>Siparişiniz işleme alındı.</p>\n<p><strong>Takip numaranız:</strong> ${escapeHtmlLite(track)}</p>\n${gTrack ? `<p><strong>Kargo takip:</strong> ${escapeHtmlLite(gTrack)}</p>` : \"\"}\n${vendorNote ? `<p><strong>İşletme notu:</strong> ${escapeHtmlLite(String(vendorNote))}</p>` : \"\"}\n<p>${escapeHtmlLite(venRow.name)}</p>`;\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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\n const phone = courierPhoneFromSession(req);\n if (!phone) { res.status(400).json({ error: \"Telefon zorunlu\" }); return; }\n const rows = r(await db.execute<Row>(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<void> => {\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<Row>(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<string, string> = { 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<void> => {\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<Row>(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<Row>(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<Row>(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<Row>(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<void> => {\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<void> => {\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<void> => {\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<void> => {\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<void> => {\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<string, unknown>;\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<void> => {\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<string, unknown>;\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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const rows = r(await db.execute<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { name, slug, icon, imageUrl, position, superCategory } = req.body as Record<string, string | number>;\n if (!name || !slug) { res.status(400).json({ error: \"Ad ve slug zorunlu\" }); return; }\n const rows = r(await db.execute<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { name, slug, icon, imageUrl, position, superCategory, active } = req.body as Record<string, string | number | boolean>;\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<void> => {\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<void> => {\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<Row>(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<string, unknown>)\n : typeof savedRaw === \"string\"\n ? (() => {\n try {\n return JSON.parse(savedRaw) as Record<string, unknown>;\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<void> => {\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<string, unknown>;\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<Row>(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<string, unknown> = {};\n const prevRaw = current.theme_config;\n if (prevRaw && typeof prevRaw === \"object\" && !Array.isArray(prevRaw)) {\n nextConfig = { ...(prevRaw as Record<string, unknown>) };\n } else if (typeof prevRaw === \"string\") {\n try {\n nextConfig = JSON.parse(prevRaw) as Record<string, unknown>;\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<string, unknown>;\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<string, unknown>) : {};\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<string, unknown>) : {};\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<Row>(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<void> => {\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<Row>(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<string>();\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<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n await ensureVendorCustomDomainsTable();\n const rows = r(await db.execute<Row>(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<void> => {\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<Row>(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, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\nfunction normGeliverMahalle(v: unknown): string | null {\n if (v == null) return null;\n const s = String(v).trim();\n return s.length ? s : null;\n}\n\n/* Auto-migrate: şifre sütunları */\n(async () => {\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<string, string> = {\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<string, string> = {\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<string, unknown>;\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<boolean> {\n if (!vendorId || !vendorEmail) return false;\n const own = r(\n await db.execute<Row>(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<string, number>();\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<string, unknown> | 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<string, unknown> | 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<Row>(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<void> => {\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<string, unknown>;\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<Row>(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<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { status, type, q } = req.query as Record<string, string>;\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<Row>(\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<void> => {\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<Row>(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<Row>(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<void> => {\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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const id = Number(req.params.id);\n const { notes } = req.body;\n await db.execute<Row>(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<void> => {\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<string, unknown>;\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<Row>(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<void> => {\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<string, unknown>;\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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<string, unknown>).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 <signed vendor token>\n──────────────────────────────────────────────────────── */\nrouter.get(\"/providers/me\", async (req, res): Promise<void> => {\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<Row>(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<string, unknown>).paytr_merchant_id_masked = paytrMerchantIdRaw ? `****${paytrMerchantIdRaw.slice(-4)}` : \"\";\n (v as Record<string, unknown>).paytr_configured = paytrConfigured;\n (v as Record<string, unknown>).iyzico_configured = iyzicoConfigured;\n\n const geliverTok = String(v.geliver_api_token ?? \"\").trim();\n delete v.geliver_api_token;\n (v as Record<string, unknown>).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<Row>(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<string, unknown>).membership_pricing = membershipPricingFromSiteRow(siteRow);\n\n await ensureVendorCustomDomainsTable();\n (v as Record<string, unknown>).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<void> => {\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<string, unknown>;\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<Row>(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<void> => {\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<string, string | undefined>;\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<Row>(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<void> => {\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<string, unknown>;\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<Row>(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<void> => {\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<Row>(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<string, unknown>;\n const rec = body.recipient as Record<string, unknown> | 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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<string, unknown>) ?? {}) });\n});\n\nrouter.get(\"/providers/geliver/shipment/:shipmentId\", async (req, res): Promise<void> => {\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<Row>(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<void> => {\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<Row>(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<string, unknown>;\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<void> => {\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<Row>(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<string, unknown>;\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<string, unknown>,\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<void> => {\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<string, unknown>)?.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<void> => {\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<Row>(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<string, unknown> = { 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<string, unknown>);\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<void> => {\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<void> => {\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<void> => {\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<string, unknown>;\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<void> => {\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<void> => {\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<void> => {\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<string, unknown>;\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<void> => {\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<void> => {\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<string, unknown>;\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<string, unknown>, 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<string, unknown>;\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<string, unknown>,\n headers: Record<string, unknown>,\n): Promise<void> {\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<string, unknown>) : 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<typeof deliveryOrdersTable.$inferInsert> = {\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<void> {\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<string, unknown>;\n try {\n body = JSON.parse(buf.toString(\"utf8\")) as Record<string, unknown>;\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<string, unknown>);\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<void> => {\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<void> => {\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<string, unknown>;\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<void> => {\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<void> => {\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<void> => {\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<void> => {\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<string, unknown>;\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<string, unknown>)\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<void> => {\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<void> => {\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<Row>(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<string, unknown>;\n\n const [cur] = r(\n await db.execute<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<string, unknown>;\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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const status = String(req.query.status || \"pending\");\n const rows = r(await db.execute<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<string, unknown>;\n const { tableServiceEnabled, reservationEnabled, reservationAutoConfirm, tableSections, qrMenuPublic } = body;\n const vid = Number(vendorId);\n const vendorRows = r(\n await db.execute<Row>(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<void> => {\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<Row>(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<number | null> {\n if (!vendorId || !vendorEmail) return null;\n const rows = r(await db.execute<Row>(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<string> {\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<boolean> {\n const rows = r(await db.execute<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<void> => {\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<Row>(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<void> => {\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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<Row>(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<void> => {\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<void> => {\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<Row>(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<string, string> = {\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<Row>(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 = `<p>Merhaba ${escapeHtmlLite(fullOrder.customerName)},</p>\n<p>Siparişiniz işleme alındı.</p>\n<p><strong>Takip numaranız:</strong> ${escapeHtmlLite(track)}</p>\n${gTrack ? `<p><strong>Kargo takip:</strong> ${escapeHtmlLite(gTrack)}</p>` : \"\"}\n${vendorNote ? `<p><strong>İşletme notu:</strong> ${escapeHtmlLite(String(vendorNote))}</p>` : \"\"}\n<p>${escapeHtmlLite(venRow.name)}</p>`;\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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\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<Row>(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<void> => {\n const phone = courierPhoneFromSession(req);\n if (!phone) { res.status(400).json({ error: \"Telefon zorunlu\" }); return; }\n const rows = r(await db.execute<Row>(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<void> => {\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<Row>(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<string, string> = { 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<void> => {\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<Row>(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<Row>(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<Row>(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<Row>(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<void> => {\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<void> => {\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<void> => {\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<void> => {\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<void> => {\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<string, unknown>;\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<void> => {\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<string, unknown>;\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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const rows = r(await db.execute<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { name, slug, icon, imageUrl, position, superCategory } = req.body as Record<string, string | number>;\n if (!name || !slug) { res.status(400).json({ error: \"Ad ve slug zorunlu\" }); return; }\n const rows = r(await db.execute<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n const { name, slug, icon, imageUrl, position, superCategory, active } = req.body as Record<string, string | number | boolean>;\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<void> => {\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<void> => {\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<Row>(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<string, unknown>)\n : typeof savedRaw === \"string\"\n ? (() => {\n try {\n return JSON.parse(savedRaw) as Record<string, unknown>;\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<void> => {\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<string, unknown>;\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<Row>(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<string, unknown> = {};\n const prevRaw = current.theme_config;\n if (prevRaw && typeof prevRaw === \"object\" && !Array.isArray(prevRaw)) {\n nextConfig = { ...(prevRaw as Record<string, unknown>) };\n } else if (typeof prevRaw === \"string\") {\n try {\n nextConfig = JSON.parse(prevRaw) as Record<string, unknown>;\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<string, unknown>;\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<string, unknown>) : {};\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<string, unknown>) : {};\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<Row>(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<void> => {\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<Row>(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<string>();\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<Row>(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<void> => {\n if (!denyUnlessAdminMaintenance(req, res, \"servis_saglayicilar\")) return;\n await ensureVendorCustomDomainsTable();\n const rows = r(await db.execute<Row>(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<void> => {\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<Row>(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"}}}}