507 lines
16 KiB
TypeScript
507 lines
16 KiB
TypeScript
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("running"),
|
|
v.literal("pending"),
|
|
v.literal("synced"),
|
|
v.literal("failed"),
|
|
v.literal("missing_config"),
|
|
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"]),
|
|
})
|