Files
webdev-pipeline/convex/schema.ts

300 lines
8.7 KiB
TypeScript

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { tables as authTables } from "./betterAuth/schema";
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({
...authTables,
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"]),
});