416 lines
13 KiB
TypeScript
416 lines
13 KiB
TypeScript
import { defineSchema, defineTable } from "convex/server";
|
|
import { v } from "convex/values";
|
|
import { tables as authTables } from "./betterAuth/schema";
|
|
import {
|
|
RUN_EVENT_LEVELS,
|
|
RUN_STATUSES,
|
|
RUN_TYPES,
|
|
} from "./domain";
|
|
|
|
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"),
|
|
v.literal("blocked"),
|
|
);
|
|
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 websiteEnrichmentPageKind = v.union(
|
|
v.literal("homepage"),
|
|
v.literal("contact"),
|
|
v.literal("impressum"),
|
|
v.literal("services"),
|
|
v.literal("about"),
|
|
v.literal("team"),
|
|
v.literal("other"),
|
|
);
|
|
const runType = v.union(...RUN_TYPES.map((type) => v.literal(type)));
|
|
const runStatus = v.union(...RUN_STATUSES.map((status) => v.literal(status)));
|
|
const runEventLevel = v.union(
|
|
...RUN_EVENT_LEVELS.map((level) => v.literal(level)),
|
|
);
|
|
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()),
|
|
geocodedAt: v.optional(v.number()),
|
|
geocodingPlaceId: v.optional(v.string()),
|
|
geocodingFormattedAddress: v.optional(v.string()),
|
|
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,
|
|
countryCode: v.optional(v.string()),
|
|
country: v.optional(v.string()),
|
|
lastRunAt: v.optional(v.number()),
|
|
nextRunAt: v.optional(v.union(v.number(), v.null())),
|
|
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")),
|
|
discoveryRunId: v.optional(v.id("agentRuns")),
|
|
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()),
|
|
normalizedGooglePlaceId: v.optional(v.string()),
|
|
googleMapsUrl: v.optional(v.string()),
|
|
googlePrimaryType: v.optional(v.string()),
|
|
googleTypes: v.optional(v.array(v.string())),
|
|
googleRating: v.optional(v.number()),
|
|
googleUserRatingCount: v.optional(v.number()),
|
|
googleBusinessStatus: v.optional(v.string()),
|
|
sourceProvider: v.optional(v.literal("google_places")),
|
|
sourceFetchedAt: v.optional(v.number()),
|
|
websiteUrl: v.optional(v.string()),
|
|
websiteDomain: v.optional(v.string()),
|
|
phone: v.optional(v.string()),
|
|
normalizedEmail: v.optional(v.string()),
|
|
normalizedPhone: v.optional(v.string()),
|
|
normalizedCompanyName: v.optional(v.string()),
|
|
normalizedAddress: v.optional(v.string()),
|
|
email: v.optional(v.string()),
|
|
emailSource: v.optional(v.string()),
|
|
contactPerson: v.optional(v.string()),
|
|
priorityReason: v.optional(v.string()),
|
|
contactStatusReason: v.optional(v.string()),
|
|
duplicateReason: v.optional(v.string()),
|
|
blacklistReason: v.optional(v.string()),
|
|
duplicateOfLeadId: v.optional(v.id("leads")),
|
|
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_discoveryRunId", ["discoveryRunId"])
|
|
.index("by_contactStatus", ["contactStatus"])
|
|
.index("by_normalizedEmail", ["normalizedEmail"])
|
|
.index("by_normalizedPhone", ["normalizedPhone"])
|
|
.index("by_normalizedCompanyName_and_normalizedAddress", [
|
|
"normalizedCompanyName",
|
|
"normalizedAddress",
|
|
])
|
|
.index("by_normalizedGooglePlaceId", ["normalizedGooglePlaceId"])
|
|
.index("by_googlePlaceId", ["googlePlaceId"])
|
|
.index("by_websiteDomain", ["websiteDomain"])
|
|
.index("by_normalizedCompanyName", ["normalizedCompanyName"])
|
|
.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"]),
|
|
|
|
websiteCrawlPages: defineTable({
|
|
leadId: v.id("leads"),
|
|
runId: v.optional(v.id("agentRuns")),
|
|
sourceUrl: v.string(),
|
|
finalUrl: v.string(),
|
|
pageKind: websiteEnrichmentPageKind,
|
|
title: v.optional(v.string()),
|
|
metaDescription: v.optional(v.string()),
|
|
headings: v.array(v.string()),
|
|
visibleTextExcerpt: v.optional(v.string()),
|
|
hasContactFormSignal: v.boolean(),
|
|
hasContactCtaSignal: v.boolean(),
|
|
createdAt: v.number(),
|
|
})
|
|
.index("by_leadId", ["leadId"])
|
|
.index("by_runId", ["runId"])
|
|
.index("by_leadId_and_createdAt", ["leadId", "createdAt"]),
|
|
|
|
websiteCrawlLinks: defineTable({
|
|
leadId: v.id("leads"),
|
|
runId: v.optional(v.id("agentRuns")),
|
|
pageUrl: v.string(),
|
|
href: v.string(),
|
|
text: v.optional(v.string()),
|
|
isInternal: v.boolean(),
|
|
isBroken: v.optional(v.boolean()),
|
|
createdAt: v.number(),
|
|
})
|
|
.index("by_leadId", ["leadId"])
|
|
.index("by_runId", ["runId"]),
|
|
|
|
websiteEmailCandidates: defineTable({
|
|
leadId: v.id("leads"),
|
|
runId: v.optional(v.id("agentRuns")),
|
|
email: v.string(),
|
|
normalizedEmail: v.string(),
|
|
emailSource: v.string(),
|
|
sourceUrl: v.string(),
|
|
contactPerson: v.optional(v.string()),
|
|
isBusinessContactAddress: v.boolean(),
|
|
isGeneric: v.boolean(),
|
|
accepted: v.boolean(),
|
|
createdAt: v.number(),
|
|
})
|
|
.index("by_leadId", ["leadId"])
|
|
.index("by_normalizedEmail", ["normalizedEmail"])
|
|
.index("by_runId", ["runId"]),
|
|
|
|
websiteCrawlScreenshots: defineTable({
|
|
leadId: v.id("leads"),
|
|
runId: v.optional(v.id("agentRuns")),
|
|
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_leadId", ["leadId"])
|
|
.index("by_runId", ["runId"])
|
|
.index("by_storageId", ["storageId"]),
|
|
|
|
websiteTechnicalChecks: defineTable({
|
|
leadId: v.id("leads"),
|
|
runId: v.optional(v.id("agentRuns")),
|
|
sourceUrl: v.string(),
|
|
finalUrl: v.optional(v.string()),
|
|
usesHttps: v.boolean(),
|
|
missingTitle: v.boolean(),
|
|
missingMetaDescription: v.boolean(),
|
|
hasVisibleContactPath: v.boolean(),
|
|
brokenInternalLinkCount: v.number(),
|
|
createdAt: v.number(),
|
|
})
|
|
.index("by_leadId", ["leadId"])
|
|
.index("by_runId", ["runId"]),
|
|
|
|
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", ["type"])
|
|
.index("by_type_and_status", ["type", "status"])
|
|
.index("by_type_and_status_and_leadId", ["type", "status", "leadId"])
|
|
.index("by_campaignId_and_updatedAt", ["campaignId", "updatedAt"])
|
|
.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"]),
|
|
});
|