Rocky_Mountain_Vending/lib/convex-service.ts

216 lines
5.5 KiB
TypeScript

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<string, string>
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 cachedServerConvexClient: ConvexHttpClient | null | undefined
function getServerConvexClient() {
if (cachedServerConvexClient !== undefined) {
return cachedServerConvexClient
}
const convexUrl = process.env.CONVEX_URL || process.env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
cachedServerConvexClient = null
return cachedServerConvexClient
}
const client = new ConvexHttpClient(convexUrl)
const adminKey = process.env.CONVEX_SELF_HOSTED_ADMIN_KEY
if (adminKey) {
client.setAdminAuth(adminKey)
}
cachedServerConvexClient = client
return cachedServerConvexClient
}
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<TData>(label: string, query: Promise<TData>, fallback: TData): Promise<TData> {
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<Product[]> {
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<Product[]> {
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<Product | null> {
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<Manual[]> {
if (!hasConvexUrl()) {
return []
}
const client = getServerConvexClient()
if (!client) {
return []
}
const manuals = await safeFetchQuery("manuals.list", client.query(LIST_MANUALS, {}), [] as ConvexManualDoc[])
return (manuals as ConvexManualDoc[]).map(mapConvexManual)
}
export async function getConvexManualDashboard() {
if (!hasConvexUrl()) {
return null
}
const client = getServerConvexClient()
if (!client) {
return null
}
return await safeFetchQuery("manuals.dashboard", client.query(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<ConvexAdminOrder[]> {
if (!hasConvexUrl()) {
return []
}
return await safeFetchQuery(
"orders.listAdmin",
fetchQuery(api.orders.listAdmin, {
status: options?.status,
search: options?.search,
}),
[] as ConvexAdminOrder[],
)
}