import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; const campaignStatus = v.union(v.literal("active"), v.literal("paused")); const leadPriority = v.union( v.literal("high"), v.literal("medium"), v.literal("low"), v.literal("defer"), ); const leadContactStatus = v.union( v.literal("new"), v.literal("missing_contact"), v.literal("audit_ready"), v.literal("outreach_ready"), v.literal("contacted"), v.literal("replied"), v.literal("do_not_contact"), ); const leadDuplicateStatus = v.union( v.literal("unchecked"), v.literal("unique"), v.literal("possible_duplicate"), v.literal("duplicate"), ); const leadBlacklistStatus = v.union(v.literal("clear"), v.literal("blocked")); const auditStatus = v.union( v.literal("draft"), v.literal("approved"), v.literal("published"), v.literal("deactivated"), ); const outreachStrategy = v.union( v.literal("call_first"), v.literal("email_first"), v.literal("defer"), v.literal("do_not_contact"), ); const outreachApprovalStatus = v.union( v.literal("draft"), v.literal("approved"), v.literal("rejected"), ); const outreachSendStatus = v.union( v.literal("not_sent"), v.literal("queued"), v.literal("sent"), v.literal("failed"), ); const outreachResponseStatus = v.union( v.literal("none"), v.literal("manual_reply_recorded"), v.literal("no_interest"), v.literal("follow_up_needed"), ); const outreachSalesStatus = v.union( v.literal("follow_up_planned"), v.literal("follow_up_sent"), v.literal("reply_received"), v.literal("not_interested"), v.literal("later"), v.literal("meeting_scheduled"), v.literal("proposal_requested"), v.literal("proposal_sent"), v.literal("won"), v.literal("lost"), v.literal("do_not_pursue"), ); const blacklistType = v.union( v.literal("domain"), v.literal("email"), v.literal("phone"), v.literal("company"), v.literal("google_place_id"), ); const runType = v.union( v.literal("campaign"), v.literal("lead_discovery"), v.literal("audit"), v.literal("outreach"), v.literal("lifecycle"), ); const runStatus = v.union( v.literal("pending"), v.literal("running"), v.literal("succeeded"), v.literal("failed"), v.literal("canceled"), ); const runEventLevel = v.union( v.literal("info"), v.literal("warning"), v.literal("error"), ); const screenshotViewport = v.union(v.literal("desktop"), v.literal("mobile")); const settingsValue = v.union(v.string(), v.number(), v.boolean(), v.null()); const auditMetricSummary = v.object({ performanceScore: v.optional(v.number()), accessibilityScore: v.optional(v.number()), bestPracticesScore: v.optional(v.number()), seoScore: v.optional(v.number()), notes: v.optional(v.array(v.string())), }); const playwrightSummary = v.object({ pagesVisited: v.number(), contactLinksFound: v.number(), formsFound: v.number(), notes: v.optional(v.array(v.string())), }); const eventDetail = v.object({ label: v.string(), value: v.string(), source: v.optional(v.string()), }); export default defineSchema({ campaigns: defineTable({ name: v.string(), categoryMode: v.union(v.literal("preset"), v.literal("custom")), category: v.string(), customSearchTerm: v.optional(v.string()), postalCode: v.string(), region: v.optional(v.string()), latitude: v.optional(v.number()), longitude: v.optional(v.number()), radiusKm: v.number(), maxNewLeadsPerRun: v.number(), maxAuditsPerRun: v.number(), recurrence: v.union( v.literal("manual"), v.literal("daily"), v.literal("weekly"), v.literal("monthly"), ), status: campaignStatus, lastRunAt: v.optional(v.number()), nextRunAt: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_status", ["status"]) .index("by_nextRunAt", ["nextRunAt"]) .index("by_status_and_nextRunAt", ["status", "nextRunAt"]), leads: defineTable({ campaignId: v.optional(v.id("campaigns")), companyName: v.string(), niche: v.optional(v.string()), address: v.optional(v.string()), city: v.optional(v.string()), postalCode: v.optional(v.string()), googlePlaceId: v.optional(v.string()), googleMapsUrl: v.optional(v.string()), websiteDomain: v.optional(v.string()), phone: v.optional(v.string()), email: v.optional(v.string()), emailSource: v.optional(v.string()), contactPerson: v.optional(v.string()), priority: leadPriority, contactStatus: leadContactStatus, duplicateStatus: leadDuplicateStatus, blacklistStatus: leadBlacklistStatus, notes: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_campaignId", ["campaignId"]) .index("by_contactStatus", ["contactStatus"]) .index("by_googlePlaceId", ["googlePlaceId"]) .index("by_websiteDomain", ["websiteDomain"]) .index("by_priority_and_contactStatus", ["priority", "contactStatus"]), audits: defineTable({ leadId: v.id("leads"), status: auditStatus, slug: v.string(), checkedDomain: v.string(), checkedPages: v.array(v.string()), pageSpeedSummary: v.optional(auditMetricSummary), playwrightSummary: v.optional(playwrightSummary), textFindings: v.optional(v.array(v.string())), skillSummaries: v.optional( v.array( v.object({ name: v.string(), purpose: v.string(), summary: v.string(), }), ), ), multimodalSummary: v.optional(v.string()), internalSummary: v.optional(v.string()), publicSummary: v.optional(v.string()), publicBody: v.optional(v.string()), ctaType: v.optional(v.string()), publishedAt: v.optional(v.number()), reviewDueAt: v.optional(v.number()), deactivatedAt: v.optional(v.number()), createdAt: v.number(), updatedAt: v.number(), }) .index("by_leadId", ["leadId"]) .index("by_slug", ["slug"]) .index("by_status", ["status"]) .index("by_status_and_reviewDueAt", ["status", "reviewDueAt"]), auditScreenshots: defineTable({ auditId: v.id("audits"), storageId: v.id("_storage"), viewport: screenshotViewport, sourceUrl: v.string(), capturedAt: v.number(), width: v.number(), height: v.number(), mimeType: v.string(), createdAt: v.number(), }) .index("by_auditId", ["auditId"]) .index("by_auditId_and_viewport", ["auditId", "viewport"]) .index("by_storageId", ["storageId"]), outreachRecords: defineTable({ leadId: v.id("leads"), auditId: v.optional(v.id("audits")), strategy: outreachStrategy, phoneScript: v.optional(v.string()), emailSubject: v.optional(v.string()), emailBody: v.optional(v.string()), followUpDraft: v.optional(v.string()), approvalStatus: outreachApprovalStatus, sendStatus: outreachSendStatus, sentAt: v.optional(v.number()), responseStatus: outreachResponseStatus, salesStatus: outreachSalesStatus, createdAt: v.number(), updatedAt: v.number(), }) .index("by_leadId", ["leadId"]) .index("by_auditId", ["auditId"]) .index("by_approvalStatus", ["approvalStatus"]) .index("by_sendStatus", ["sendStatus"]), blacklistEntries: defineTable({ type: blacklistType, value: v.string(), normalizedValue: v.string(), note: v.optional(v.string()), createdAt: v.number(), }) .index("by_type_and_normalizedValue", ["type", "normalizedValue"]) .index("by_normalizedValue", ["normalizedValue"]), agentRuns: defineTable({ type: runType, campaignId: v.optional(v.id("campaigns")), leadId: v.optional(v.id("leads")), auditId: v.optional(v.id("audits")), status: runStatus, startedAt: v.optional(v.number()), finishedAt: v.optional(v.number()), currentStep: v.optional(v.string()), errorSummary: v.optional(v.string()), counters: v.optional( v.object({ leadsFound: v.number(), leadsCreated: v.number(), auditsCreated: v.number(), outreachPrepared: v.number(), errors: v.number(), }), ), createdAt: v.number(), updatedAt: v.number(), }) .index("by_status", ["status"]) .index("by_type_and_status", ["type", "status"]) .index("by_campaignId_and_status", ["campaignId", "status"]) .index("by_auditId", ["auditId"]), agentRunEvents: defineTable({ runId: v.id("agentRuns"), level: runEventLevel, message: v.string(), details: v.optional(v.array(eventDetail)), createdAt: v.number(), }) .index("by_runId_and_createdAt", ["runId", "createdAt"]) .index("by_level", ["level"]), settings: defineTable({ key: v.string(), value: settingsValue, description: v.optional(v.string()), createdAt: v.number(), updatedAt: v.number(), }).index("by_key", ["key"]), });