Files
webdev-pipeline/convex/leads.ts

162 lines
4.6 KiB
TypeScript

import { v } from "convex/values";
import { normalizeListLimit } from "./domain";
import { mutation, query } from "./_generated/server";
export const create = mutation({
args: {
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()),
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()),
email: v.optional(v.string()),
emailSource: v.optional(v.string()),
contactPerson: v.optional(v.string()),
priority: v.optional(
v.union(
v.literal("high"),
v.literal("medium"),
v.literal("low"),
v.literal("defer"),
),
),
contactStatus: v.optional(
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"),
),
),
notes: v.optional(v.string()),
},
handler: async (ctx, args) => {
const now = Date.now();
return await ctx.db.insert("leads", {
...args,
priority: args.priority ?? "medium",
contactStatus: args.contactStatus ?? "new",
duplicateStatus: "unchecked",
blacklistStatus: "clear",
createdAt: now,
updatedAt: now,
});
},
});
export const get = query({
args: { id: v.id("leads") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
export const list = query({
args: {
campaignId: v.optional(v.id("campaigns")),
contactStatus: v.optional(
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"),
),
),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = normalizeListLimit(args.limit);
if (args.campaignId) {
const campaignId = args.campaignId;
return await ctx.db
.query("leads")
.withIndex("by_campaignId", (q) => q.eq("campaignId", campaignId))
.order("desc")
.take(limit);
}
if (args.contactStatus) {
const contactStatus = args.contactStatus;
return await ctx.db
.query("leads")
.withIndex("by_contactStatus", (q) =>
q.eq("contactStatus", contactStatus),
)
.order("desc")
.take(limit);
}
return await ctx.db.query("leads").order("desc").take(limit);
},
});
export const listFunnel = query({
args: {
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = normalizeListLimit(args.limit);
const leads = await ctx.db.query("leads").order("desc").take(limit);
return await Promise.all(
leads.map(async (lead) => {
const outreach = await ctx.db
.query("outreachRecords")
.withIndex("by_leadId", (q) => q.eq("leadId", lead._id))
.order("desc")
.take(1);
const latestOutreach = outreach[0] ?? null;
return {
id: lead._id,
companyName: lead.companyName,
niche: lead.niche ?? null,
address: lead.address ?? null,
city: lead.city ?? null,
postalCode: lead.postalCode ?? null,
priority: lead.priority,
contactStatus: lead.contactStatus,
blacklistStatus: lead.blacklistStatus,
email: lead.email ?? null,
phone: lead.phone ?? null,
contactPerson: lead.contactPerson ?? null,
websiteDomain: lead.websiteDomain ?? null,
outreach: latestOutreach
? {
approvalStatus: latestOutreach.approvalStatus,
sendStatus: latestOutreach.sendStatus,
responseStatus: latestOutreach.responseStatus,
salesStatus: latestOutreach.salesStatus,
}
: null,
};
}),
);
},
});