298 lines
8.7 KiB
TypeScript
298 lines
8.7 KiB
TypeScript
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"]),
|
|
});
|