Rocky_Mountain_Vending/convex/schema.ts

378 lines
11 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(),
normalizedPhone: v.optional(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"])
.index("by_normalizedPhone", ["normalizedPhone"]),
contactProfiles: defineTable({
normalizedPhone: v.string(),
displayName: v.optional(v.string()),
firstName: v.optional(v.string()),
lastName: v.optional(v.string()),
email: v.optional(v.string()),
company: v.optional(v.string()),
lastIntent: v.optional(v.string()),
lastLeadOutcome: v.optional(
v.union(
v.literal("none"),
v.literal("contact"),
v.literal("requestMachine")
)
),
lastSummaryText: v.optional(v.string()),
lastCallAt: v.optional(v.number()),
lastReminderAt: v.optional(v.number()),
reminderNotes: v.optional(v.string()),
source: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_normalizedPhone", ["normalizedPhone"])
.index("by_updatedAt", ["updatedAt"]),
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(),
callerPhone: v.optional(v.string()),
siteUrl: v.optional(v.string()),
pathname: v.optional(v.string()),
pageUrl: v.optional(v.string()),
source: v.optional(v.string()),
contactProfileId: v.optional(v.id("contactProfiles")),
contactDisplayName: v.optional(v.string()),
contactCompany: 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()),
reminderStatus: v.optional(
v.union(v.literal("none"), v.literal("scheduled"), v.literal("sameDay"))
),
reminderRequestedAt: v.optional(v.number()),
reminderStartAt: v.optional(v.number()),
reminderEndAt: v.optional(v.number()),
reminderCalendarEventId: v.optional(v.string()),
reminderCalendarHtmlLink: v.optional(v.string()),
reminderNote: v.optional(v.string()),
warmTransferStatus: v.optional(
v.union(
v.literal("none"),
v.literal("attempted"),
v.literal("connected"),
v.literal("failed"),
v.literal("fallback")
)
),
warmTransferTarget: v.optional(v.string()),
warmTransferAttemptedAt: v.optional(v.number()),
warmTransferConnectedAt: v.optional(v.number()),
warmTransferFailureReason: 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_callerPhone", ["callerPhone"])
.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"]),
})