import { ConvexHttpClient } from "convex/browser" import { fetchQuery } from "convex/nextjs" import { makeFunctionReference } from "convex/server" import { api } from "@/convex/_generated/api" import { hasConvexUrl } from "@/lib/convex-config" import type { Product } from "@/lib/products/types" import type { Manual } from "@/lib/manuals-types" type ConvexProductDoc = { _id: string name: string description?: string price: number currency: string images: string[] metadata?: Record stripeProductId?: string stripePriceId?: string active: boolean } type ConvexManualDoc = { filename: string path: string manufacturer: string category: string size?: number lastModified?: number searchTerms?: string[] commonNames?: string[] thumbnailUrl?: string manualUrl?: string } const LIST_MANUALS = makeFunctionReference<"query">("manuals:list") const MANUALS_DASHBOARD = makeFunctionReference<"query">("manuals:dashboard") let cachedServerConvexAdminClient: ConvexHttpClient | null | undefined let cachedServerConvexPublicClient: ConvexHttpClient | null | undefined function getServerConvexClient(useAdminAuth: boolean) { const cacheKey = useAdminAuth ? "admin" : "public" const cachedClient = useAdminAuth ? cachedServerConvexAdminClient : cachedServerConvexPublicClient if (cachedClient !== undefined) { return cachedClient } const convexUrl = process.env.CONVEX_URL || process.env.NEXT_PUBLIC_CONVEX_URL if (!convexUrl) { if (useAdminAuth) { cachedServerConvexAdminClient = null return cachedServerConvexAdminClient } cachedServerConvexPublicClient = null return cachedServerConvexPublicClient } const client = new ConvexHttpClient(convexUrl) const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY if (useAdminAuth && adminKey) { ;( client as ConvexHttpClient & { setAdminAuth?: (token: string) => void } ).setAdminAuth?.(adminKey) } if (cacheKey === "admin") { cachedServerConvexAdminClient = client return cachedServerConvexAdminClient } cachedServerConvexPublicClient = client return cachedServerConvexPublicClient } async function queryManualsWithAuthFallback( label: string, queryRef: ReturnType>, fallback: TData ): Promise { const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY const adminClient = getServerConvexClient(true) if (adminClient) { try { return (await adminClient.query(queryRef, {})) as TData } catch (error) { console.error(`[convex-service] ${label} admin query failed`, error) if (!adminKey) { return fallback } } } const publicClient = getServerConvexClient(false) if (!publicClient) { return fallback } return await safeFetchQuery( `${label}.public`, publicClient.query(queryRef, {}), fallback ) } type ConvexOrderItem = { productId?: string | null productName: string price: number quantity: number priceId: string image?: string } export type ConvexAdminOrder = { id: string customerEmail: string customerName?: string totalAmount: number currency: string status: "pending" | "paid" | "fulfilled" | "cancelled" | "refunded" stripeSessionId: string | null paymentIntentId: string | null createdAt: string updatedAt: string shippingAddress?: { name?: string address?: string city?: string state?: string zipCode?: string country?: string } items: ConvexOrderItem[] } async function safeFetchQuery( label: string, query: Promise, fallback: TData ): Promise { try { return await query } catch (error) { console.error(`[convex-service] ${label} failed`, error) return fallback } } function mapConvexProduct(product: ConvexProductDoc): Product { return { id: product._id, stripeId: product.stripeProductId ?? product._id, name: product.name, description: product.description ?? null, price: product.price, currency: product.currency, images: product.images ?? [], metadata: product.metadata, priceId: product.stripePriceId, } } function mapConvexManual(manual: ConvexManualDoc): Manual { return { filename: manual.filename, path: manual.path, manufacturer: manual.manufacturer, category: manual.category, size: manual.size, lastModified: manual.lastModified ? new Date(manual.lastModified) : undefined, searchTerms: manual.searchTerms, commonNames: manual.commonNames, thumbnailUrl: manual.thumbnailUrl, } } export async function listConvexProducts(): Promise { if (!hasConvexUrl()) { return [] } const products = await safeFetchQuery( "products.listActive", fetchQuery(api.products.listActive, {}), [] as ConvexProductDoc[] ) return (products as ConvexProductDoc[]).map(mapConvexProduct) } export async function listConvexAdminProducts( search?: string ): Promise { if (!hasConvexUrl()) { return [] } const products = await safeFetchQuery( "products.listAdmin", fetchQuery(api.products.listAdmin, { search }), [] as ConvexProductDoc[] ) return (products as ConvexProductDoc[]).map(mapConvexProduct) } export async function getConvexProduct(id: string): Promise { if (!hasConvexUrl()) { return null } const products = await safeFetchQuery( "products.listAdmin", fetchQuery(api.products.listAdmin, {}), [] as ConvexProductDoc[] ) const match = (products as ConvexProductDoc[]).find((product) => { return product._id === id || product.stripeProductId === id }) return match ? mapConvexProduct(match) : null } export async function listConvexManuals(): Promise { if (!hasConvexUrl()) { return [] } const manuals = await queryManualsWithAuthFallback( "manuals.list", LIST_MANUALS, [] as ConvexManualDoc[] ) return (manuals as ConvexManualDoc[]).map(mapConvexManual) } export async function getConvexManualDashboard() { if (!hasConvexUrl()) { return null } return await queryManualsWithAuthFallback( "manuals.dashboard", MANUALS_DASHBOARD, null ) } export async function getConvexOrderMetrics() { if (!hasConvexUrl()) { return null } return await safeFetchQuery( "orders.getMetrics", fetchQuery(api.orders.getMetrics, {}), null ) } export async function listConvexOrders(options?: { status?: ConvexAdminOrder["status"] search?: string }): Promise { if (!hasConvexUrl()) { return [] } return await safeFetchQuery( "orders.listAdmin", fetchQuery(api.orders.listAdmin, { status: options?.status, search: options?.search, }), [] as ConvexAdminOrder[] ) }