Rocky_Mountain_Vending/convex/schema.ts

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"]),
})