import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; const orderStatus = v.union( v.literal("pending"), v.literal("paid"), v.literal("fulfilled"), v.literal("cancelled"), v.literal("refunded"), ); export default defineSchema({ products: defineTable({ name: v.string(), description: v.optional(v.string()), price: v.number(), currency: v.string(), images: v.array(v.string()), metadata: v.optional(v.record(v.string(), v.string())), stripeProductId: v.optional(v.string()), stripePriceId: v.optional(v.string()), active: v.boolean(), featured: v.optional(v.boolean()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_active", ["active"]) .index("by_stripeProductId", ["stripeProductId"]), orders: defineTable({ customerEmail: v.string(), customerName: v.optional(v.string()), status: orderStatus, totalAmount: v.number(), currency: v.string(), stripeSessionId: v.optional(v.string()), stripePaymentIntentId: v.optional(v.string()), shippingAddress: v.optional( v.object({ name: v.optional(v.string()), address: v.optional(v.string()), city: v.optional(v.string()), state: v.optional(v.string()), zipCode: v.optional(v.string()), country: v.optional(v.string()), }), ), createdAt: v.number(), updatedAt: v.number(), }) .index("by_createdAt", ["createdAt"]) .index("by_status", ["status"]) .index("by_stripeSessionId", ["stripeSessionId"]) .index("by_customerEmail", ["customerEmail"]), orderItems: defineTable({ orderId: v.id("orders"), productId: v.optional(v.id("products")), stripeProductId: v.optional(v.string()), stripePriceId: v.string(), productName: v.string(), image: v.optional(v.string()), price: v.number(), quantity: v.number(), createdAt: v.number(), }).index("by_orderId", ["orderId"]), manuals: defineTable({ filename: v.string(), path: v.string(), manufacturer: v.string(), category: v.string(), size: v.optional(v.number()), lastModified: v.optional(v.number()), searchTerms: v.optional(v.array(v.string())), commonNames: v.optional(v.array(v.string())), thumbnailUrl: v.optional(v.string()), manualUrl: v.optional(v.string()), hasParts: v.optional(v.boolean()), assetSource: v.optional(v.string()), sourcePath: v.optional(v.string()), sourceSite: v.optional(v.string()), sourceDomain: v.optional(v.string()), siteVisibility: v.optional(v.array(v.string())), importBatch: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_manufacturer", ["manufacturer"]) .index("by_category", ["category"]) .index("by_path", ["path"]), manualCategories: defineTable({ name: v.string(), slug: v.string(), description: v.optional(v.string()), icon: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }).index("by_slug", ["slug"]), leadSubmissions: defineTable({ type: v.union(v.literal("contact"), v.literal("requestMachine")), status: v.union(v.literal("pending"), v.literal("delivered"), v.literal("failed")), idempotencyKey: v.optional(v.string()), firstName: v.string(), lastName: v.string(), email: v.string(), phone: v.string(), company: v.optional(v.string()), intent: v.optional(v.string()), message: v.optional(v.string()), source: v.optional(v.string()), page: v.optional(v.string()), url: v.optional(v.string()), employeeCount: v.optional(v.string()), machineType: v.optional(v.string()), machineCount: v.optional(v.string()), serviceTextConsent: v.optional(v.boolean()), marketingTextConsent: v.optional(v.boolean()), consentVersion: v.optional(v.string()), consentCapturedAt: v.optional(v.string()), consentSourcePage: v.optional(v.string()), marketingConsent: v.optional(v.boolean()), termsAgreement: v.optional(v.boolean()), usesendStatus: v.optional( v.union( v.literal("pending"), v.literal("sent"), v.literal("synced"), v.literal("failed"), v.literal("skipped"), ), ), ghlStatus: v.optional( v.union( v.literal("pending"), v.literal("sent"), v.literal("synced"), v.literal("failed"), v.literal("skipped"), ), ), error: v.optional(v.string()), deliveredAt: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_type", ["type"]) .index("by_status", ["status"]) .index("by_createdAt", ["createdAt"]) .index("by_idempotencyKey", ["idempotencyKey"]), adminUsers: defineTable({ email: v.string(), name: v.optional(v.string()), role: v.union(v.literal("admin")), active: v.boolean(), createdAt: v.number(), updatedAt: v.number(), lastLoginAt: v.optional(v.number()), }).index("by_email", ["email"]), adminSessions: defineTable({ adminUserId: v.id("adminUsers"), tokenHash: v.string(), expiresAt: v.number(), createdAt: v.number(), }) .index("by_tokenHash", ["tokenHash"]) .index("by_adminUserId", ["adminUserId"]), siteSettings: defineTable({ key: v.string(), value: v.string(), description: v.optional(v.string()), updatedAt: v.number(), }).index("by_key", ["key"]), syncJobs: defineTable({ kind: v.string(), status: v.union( v.literal("pending"), v.literal("running"), v.literal("completed"), v.literal("failed"), ), message: v.optional(v.string()), metadata: v.optional(v.string()), startedAt: v.optional(v.number()), completedAt: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_kind", ["kind"]) .index("by_status", ["status"]), voiceSessions: defineTable({ roomName: v.string(), participantIdentity: v.string(), siteUrl: v.optional(v.string()), pathname: v.optional(v.string()), pageUrl: v.optional(v.string()), source: v.optional(v.string()), startedAt: v.number(), endedAt: v.optional(v.number()), callStatus: v.optional( v.union( v.literal("started"), v.literal("completed"), v.literal("failed"), ), ), transcriptTurnCount: v.optional(v.number()), agentAnsweredAt: v.optional(v.number()), linkedLeadId: v.optional(v.string()), leadOutcome: v.optional( v.union( v.literal("none"), v.literal("contact"), v.literal("requestMachine"), ), ), handoffRequested: v.optional(v.boolean()), handoffReason: v.optional(v.string()), summaryText: v.optional(v.string()), notificationStatus: v.optional( v.union( v.literal("pending"), v.literal("sent"), v.literal("failed"), v.literal("disabled"), ), ), notificationSentAt: v.optional(v.number()), notificationError: v.optional(v.string()), recordingDisclosureAt: v.optional(v.number()), recordingStatus: v.optional( v.union( v.literal("pending"), v.literal("starting"), v.literal("recording"), v.literal("completed"), v.literal("failed"), ), ), recordingId: v.optional(v.string()), recordingUrl: v.optional(v.string()), recordingError: v.optional(v.string()), metadata: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_roomName", ["roomName"]) .index("by_participantIdentity", ["participantIdentity"]) .index("by_source", ["source"]) .index("by_source_startedAt", ["source", "startedAt"]) .index("by_startedAt", ["startedAt"]), voiceTranscriptTurns: defineTable({ sessionId: v.id("voiceSessions"), roomName: v.string(), participantIdentity: v.string(), role: v.union(v.literal("user"), v.literal("assistant"), v.literal("system")), kind: v.optional(v.string()), text: v.string(), isFinal: v.optional(v.boolean()), language: v.optional(v.string()), source: v.optional(v.string()), createdAt: v.number(), }) .index("by_sessionId", ["sessionId"]) .index("by_roomName", ["roomName"]) .index("by_createdAt", ["createdAt"]), });