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"]), ebayListings: defineTable({ itemId: v.string(), title: v.string(), normalizedTitle: v.string(), price: v.string(), currency: v.string(), imageUrl: v.optional(v.string()), viewItemUrl: v.string(), condition: v.optional(v.string()), shippingCost: v.optional(v.string()), affiliateLink: v.string(), sourceQueries: v.array(v.string()), fetchedAt: v.number(), firstSeenAt: v.number(), lastSeenAt: v.number(), expiresAt: v.number(), active: v.boolean(), }) .index("by_itemId", ["itemId"]) .index("by_active", ["active"]) .index("by_expiresAt", ["expiresAt"]) .index("by_lastSeenAt", ["lastSeenAt"]), ebayPollState: defineTable({ key: v.string(), status: v.union( v.literal("idle"), v.literal("success"), v.literal("rate_limited"), v.literal("error"), v.literal("missing_config"), v.literal("skipped") ), lastSuccessfulAt: v.optional(v.number()), lastAttemptAt: v.optional(v.number()), nextEligibleAt: v.optional(v.number()), lastError: v.optional(v.string()), consecutiveFailures: v.number(), queryCount: v.number(), itemCount: v.number(), sourceQueries: v.array(v.string()), updatedAt: v.number(), }).index("by_key", ["key"]), 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") ) ), contactId: v.optional(v.id("contacts")), conversationId: v.optional(v.id("conversations")), 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"]), contacts: defineTable({ firstName: v.string(), lastName: v.string(), email: v.optional(v.string()), normalizedEmail: v.optional(v.string()), phone: v.optional(v.string()), normalizedPhone: v.optional(v.string()), company: v.optional(v.string()), tags: v.optional(v.array(v.string())), status: v.optional( v.union( v.literal("active"), v.literal("lead"), v.literal("customer"), v.literal("inactive") ) ), source: v.optional(v.string()), notes: v.optional(v.string()), ghlContactId: v.optional(v.string()), livekitIdentity: v.optional(v.string()), lastActivityAt: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_normalizedEmail", ["normalizedEmail"]) .index("by_normalizedPhone", ["normalizedPhone"]) .index("by_ghlContactId", ["ghlContactId"]) .index("by_lastActivityAt", ["lastActivityAt"]) .index("by_updatedAt", ["updatedAt"]), conversations: defineTable({ contactId: v.optional(v.id("contacts")), title: v.optional(v.string()), channel: v.union( v.literal("call"), v.literal("sms"), v.literal("chat"), v.literal("unknown") ), source: v.optional(v.string()), status: v.optional( v.union(v.literal("open"), v.literal("closed"), v.literal("archived")) ), direction: v.optional( v.union(v.literal("inbound"), v.literal("outbound"), v.literal("mixed")) ), startedAt: v.number(), endedAt: v.optional(v.number()), lastMessageAt: v.optional(v.number()), lastMessagePreview: v.optional(v.string()), unreadCount: v.optional(v.number()), summaryText: v.optional(v.string()), ghlConversationId: v.optional(v.string()), livekitRoomName: v.optional(v.string()), voiceSessionId: v.optional(v.id("voiceSessions")), createdAt: v.number(), updatedAt: v.number(), }) .index("by_contactId", ["contactId"]) .index("by_channel", ["channel"]) .index("by_status", ["status"]) .index("by_ghlConversationId", ["ghlConversationId"]) .index("by_livekitRoomName", ["livekitRoomName"]) .index("by_voiceSessionId", ["voiceSessionId"]) .index("by_lastMessageAt", ["lastMessageAt"]), conversationParticipants: defineTable({ conversationId: v.id("conversations"), contactId: v.optional(v.id("contacts")), role: v.optional( v.union( v.literal("contact"), v.literal("agent"), v.literal("system"), v.literal("unknown") ) ), displayName: v.optional(v.string()), phone: v.optional(v.string()), normalizedPhone: v.optional(v.string()), email: v.optional(v.string()), normalizedEmail: v.optional(v.string()), externalContactId: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_conversationId", ["conversationId"]) .index("by_contactId", ["contactId"]) .index("by_externalContactId", ["externalContactId"]), messages: defineTable({ conversationId: v.id("conversations"), contactId: v.optional(v.id("contacts")), direction: v.optional( v.union(v.literal("inbound"), v.literal("outbound"), v.literal("system")) ), channel: v.union( v.literal("call"), v.literal("sms"), v.literal("chat"), v.literal("unknown") ), source: v.optional(v.string()), messageType: v.optional(v.string()), body: v.string(), status: v.optional(v.string()), sentAt: v.number(), ghlMessageId: v.optional(v.string()), voiceTranscriptTurnId: v.optional(v.id("voiceTranscriptTurns")), voiceSessionId: v.optional(v.id("voiceSessions")), livekitRoomName: v.optional(v.string()), metadata: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_conversationId", ["conversationId"]) .index("by_contactId", ["contactId"]) .index("by_ghlMessageId", ["ghlMessageId"]) .index("by_voiceTranscriptTurnId", ["voiceTranscriptTurnId"]) .index("by_sentAt", ["sentAt"]), callArtifacts: defineTable({ conversationId: v.id("conversations"), contactId: v.optional(v.id("contacts")), source: v.optional(v.string()), recordingId: v.optional(v.string()), recordingUrl: v.optional(v.string()), recordingStatus: v.optional( v.union( v.literal("pending"), v.literal("starting"), v.literal("recording"), v.literal("completed"), v.literal("failed") ) ), transcriptionText: v.optional(v.string()), durationMs: v.optional(v.number()), startedAt: v.optional(v.number()), endedAt: v.optional(v.number()), ghlMessageId: v.optional(v.string()), voiceSessionId: v.optional(v.id("voiceSessions")), livekitRoomName: v.optional(v.string()), metadata: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_conversationId", ["conversationId"]) .index("by_contactId", ["contactId"]) .index("by_recordingId", ["recordingId"]) .index("by_voiceSessionId", ["voiceSessionId"]) .index("by_ghlMessageId", ["ghlMessageId"]), externalSyncState: defineTable({ provider: v.string(), entityType: v.string(), entityId: v.string(), cursor: v.optional(v.string()), checksum: v.optional(v.string()), status: v.optional( v.union( v.literal("pending"), v.literal("synced"), v.literal("failed"), v.literal("reconciled"), v.literal("mismatch") ) ), lastAttemptAt: v.optional(v.number()), lastSyncedAt: v.optional(v.number()), error: v.optional(v.string()), metadata: v.optional(v.string()), updatedAt: v.number(), }) .index("by_provider_entityType", ["provider", "entityType"]) .index("by_provider_entityType_entityId", ["provider", "entityType", "entityId"]), 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()), contactId: v.optional(v.id("contacts")), conversationId: v.optional(v.id("conversations")), 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"]), })