Externalize audit pipeline services
This commit is contained in:
350
convex/leads.ts
350
convex/leads.ts
@@ -3,7 +3,13 @@ import { v } from "convex/values";
|
||||
import { getUsableContactEmailFromEntries } from "../lib/lead-discovery-google";
|
||||
import { normalizeListLimit } from "./domain";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import {
|
||||
internalMutation,
|
||||
internalQuery,
|
||||
mutation,
|
||||
query,
|
||||
} from "./_generated/server";
|
||||
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
||||
|
||||
type LeadDoc = Doc<"leads">;
|
||||
|
||||
@@ -37,6 +43,74 @@ type LeadReviewPatch = {
|
||||
contactPerson?: string;
|
||||
};
|
||||
|
||||
type LeadReviewUpdateArgs = {
|
||||
id: Id<"leads">;
|
||||
priority?: LeadDoc["priority"];
|
||||
priorityReason?: string;
|
||||
contactStatus?: LeadDoc["contactStatus"];
|
||||
contactStatusReason?: string;
|
||||
notes?: string;
|
||||
duplicateStatus?: LeadDoc["duplicateStatus"];
|
||||
duplicateReason?: string;
|
||||
blacklistStatus?: LeadDoc["blacklistStatus"];
|
||||
blacklistReason?: string;
|
||||
duplicateOfLeadId?: Id<"leads">;
|
||||
applyBlacklist?: boolean;
|
||||
reviewEmail?: string;
|
||||
reviewEmailSource?: string;
|
||||
reviewContactPerson?: string;
|
||||
reviewIsBusinessContactAddress?: boolean;
|
||||
};
|
||||
|
||||
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 reviewUpdateArgs = {
|
||||
id: v.id("leads"),
|
||||
priority: v.optional(leadPriority),
|
||||
priorityReason: v.optional(v.string()),
|
||||
contactStatus: v.optional(leadContactStatus),
|
||||
contactStatusReason: v.optional(v.string()),
|
||||
notes: v.optional(v.string()),
|
||||
duplicateStatus: v.optional(leadDuplicateStatus),
|
||||
duplicateReason: v.optional(v.string()),
|
||||
blacklistStatus: v.optional(leadBlacklistStatus),
|
||||
blacklistReason: v.optional(v.string()),
|
||||
duplicateOfLeadId: v.optional(v.id("leads")),
|
||||
applyBlacklist: v.optional(v.boolean()),
|
||||
reviewEmail: v.optional(v.string()),
|
||||
reviewEmailSource: v.optional(v.string()),
|
||||
reviewContactPerson: v.optional(v.string()),
|
||||
reviewIsBusinessContactAddress: v.optional(v.boolean()),
|
||||
};
|
||||
|
||||
const requireOperator = async (ctx: MutationCtx | QueryCtx) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (!identity) {
|
||||
throw new Error("Nicht autorisiert.");
|
||||
}
|
||||
};
|
||||
|
||||
function buildReviewContactPatch(args: {
|
||||
email?: string;
|
||||
emailSource?: string;
|
||||
@@ -88,6 +162,91 @@ function buildReviewContactPatch(args: {
|
||||
});
|
||||
}
|
||||
|
||||
async function reviewUpdateLead(ctx: MutationCtx, args: LeadReviewUpdateArgs) {
|
||||
const lead = await ctx.db.get(args.id);
|
||||
|
||||
if (!lead) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const patch: LeadReviewPatch = {
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
if (args.priority !== undefined) {
|
||||
patch.priority = args.priority;
|
||||
}
|
||||
if (args.priorityReason !== undefined) {
|
||||
patch.priorityReason = args.priorityReason;
|
||||
}
|
||||
if (args.contactStatus !== undefined) {
|
||||
patch.contactStatus = args.contactStatus;
|
||||
}
|
||||
if (args.contactStatusReason !== undefined) {
|
||||
patch.contactStatusReason = args.contactStatusReason;
|
||||
}
|
||||
if (args.notes !== undefined) {
|
||||
patch.notes = args.notes;
|
||||
}
|
||||
if (args.duplicateStatus !== undefined) {
|
||||
patch.duplicateStatus = args.duplicateStatus;
|
||||
}
|
||||
if (args.duplicateReason !== undefined) {
|
||||
patch.duplicateReason = args.duplicateReason;
|
||||
}
|
||||
if (args.duplicateOfLeadId !== undefined) {
|
||||
patch.duplicateOfLeadId = args.duplicateOfLeadId;
|
||||
}
|
||||
|
||||
if (args.applyBlacklist) {
|
||||
patch.blacklistStatus = "blocked";
|
||||
if (args.blacklistReason !== undefined) {
|
||||
patch.blacklistReason = args.blacklistReason;
|
||||
} else if (lead.blacklistReason === undefined) {
|
||||
patch.blacklistReason = "Manuell in der Review als Sperrgrund gesetzt.";
|
||||
}
|
||||
if (args.priority === undefined || args.priority !== "blocked") {
|
||||
patch.priority = "blocked";
|
||||
}
|
||||
} else if (args.applyBlacklist === false && args.blacklistStatus !== undefined) {
|
||||
patch.blacklistStatus = args.blacklistStatus;
|
||||
patch.blacklistReason = args.blacklistReason;
|
||||
} else if (args.blacklistStatus !== undefined) {
|
||||
patch.blacklistStatus = args.blacklistStatus;
|
||||
patch.blacklistReason = args.blacklistReason;
|
||||
}
|
||||
|
||||
const reviewContactPatch = buildReviewContactPatch({
|
||||
email: args.reviewEmail,
|
||||
emailSource: args.reviewEmailSource,
|
||||
contactPerson: args.reviewContactPerson,
|
||||
isBusinessContactAddress: args.reviewIsBusinessContactAddress,
|
||||
explicitContactStatus: args.contactStatus !== undefined,
|
||||
currentContactStatus: lead.contactStatus,
|
||||
});
|
||||
|
||||
if (reviewContactPatch?.patch) {
|
||||
Object.assign(patch, reviewContactPatch.patch);
|
||||
}
|
||||
|
||||
if (
|
||||
reviewContactPatch !== null &&
|
||||
reviewContactPatch.setContactStatus !== undefined &&
|
||||
args.contactStatus === undefined
|
||||
) {
|
||||
patch.contactStatus = reviewContactPatch.setContactStatus;
|
||||
}
|
||||
|
||||
if (args.blacklistReason !== undefined && patch.blacklistStatus === undefined) {
|
||||
patch.blacklistStatus = "blocked";
|
||||
patch.blacklistReason = args.blacklistReason;
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.id, patch);
|
||||
return args.id;
|
||||
}
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
campaignId: v.optional(v.id("campaigns")),
|
||||
@@ -116,44 +275,20 @@ export const create = mutation({
|
||||
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"),
|
||||
v.literal("blocked"),
|
||||
),
|
||||
),
|
||||
priority: v.optional(leadPriority),
|
||||
priorityReason: v.optional(v.string()),
|
||||
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"),
|
||||
),
|
||||
),
|
||||
contactStatus: v.optional(leadContactStatus),
|
||||
contactStatusReason: v.optional(v.string()),
|
||||
duplicateStatus: v.optional(
|
||||
v.union(
|
||||
v.literal("unchecked"),
|
||||
v.literal("unique"),
|
||||
v.literal("possible_duplicate"),
|
||||
v.literal("duplicate"),
|
||||
),
|
||||
),
|
||||
duplicateStatus: v.optional(leadDuplicateStatus),
|
||||
duplicateReason: v.optional(v.string()),
|
||||
blacklistReason: v.optional(v.string()),
|
||||
duplicateOfLeadId: v.optional(v.id("leads")),
|
||||
blacklistStatus: v.optional(v.union(v.literal("clear"), v.literal("blocked"))),
|
||||
blacklistStatus: v.optional(leadBlacklistStatus),
|
||||
normalizedGooglePlaceId: v.optional(v.string()),
|
||||
notes: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await requireOperator(ctx);
|
||||
const now = Date.now();
|
||||
|
||||
return await ctx.db.insert("leads", {
|
||||
@@ -174,136 +309,29 @@ export const create = mutation({
|
||||
});
|
||||
|
||||
export const reviewUpdate = mutation({
|
||||
args: {
|
||||
id: v.id("leads"),
|
||||
priority: v.optional(
|
||||
v.union(
|
||||
v.literal("high"),
|
||||
v.literal("medium"),
|
||||
v.literal("low"),
|
||||
v.literal("defer"),
|
||||
v.literal("blocked"),
|
||||
),
|
||||
),
|
||||
priorityReason: v.optional(v.string()),
|
||||
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"),
|
||||
),
|
||||
),
|
||||
contactStatusReason: v.optional(v.string()),
|
||||
notes: v.optional(v.string()),
|
||||
duplicateStatus: v.optional(
|
||||
v.union(
|
||||
v.literal("unchecked"),
|
||||
v.literal("unique"),
|
||||
v.literal("possible_duplicate"),
|
||||
v.literal("duplicate"),
|
||||
),
|
||||
),
|
||||
duplicateReason: v.optional(v.string()),
|
||||
blacklistStatus: v.optional(v.union(v.literal("clear"), v.literal("blocked"))),
|
||||
blacklistReason: v.optional(v.string()),
|
||||
duplicateOfLeadId: v.optional(v.id("leads")),
|
||||
applyBlacklist: v.optional(v.boolean()),
|
||||
reviewEmail: v.optional(v.string()),
|
||||
reviewEmailSource: v.optional(v.string()),
|
||||
reviewContactPerson: v.optional(v.string()),
|
||||
reviewIsBusinessContactAddress: v.optional(v.boolean()),
|
||||
},
|
||||
args: reviewUpdateArgs,
|
||||
handler: async (ctx, args) => {
|
||||
const lead = await ctx.db.get(args.id);
|
||||
await requireOperator(ctx);
|
||||
return await reviewUpdateLead(ctx, args);
|
||||
},
|
||||
});
|
||||
|
||||
if (!lead) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const patch: LeadReviewPatch = {
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
if (args.priority !== undefined) {
|
||||
patch.priority = args.priority;
|
||||
}
|
||||
if (args.priorityReason !== undefined) {
|
||||
patch.priorityReason = args.priorityReason;
|
||||
}
|
||||
if (args.contactStatus !== undefined) {
|
||||
patch.contactStatus = args.contactStatus;
|
||||
}
|
||||
if (args.contactStatusReason !== undefined) {
|
||||
patch.contactStatusReason = args.contactStatusReason;
|
||||
}
|
||||
if (args.notes !== undefined) {
|
||||
patch.notes = args.notes;
|
||||
}
|
||||
if (args.duplicateStatus !== undefined) {
|
||||
patch.duplicateStatus = args.duplicateStatus;
|
||||
}
|
||||
if (args.duplicateReason !== undefined) {
|
||||
patch.duplicateReason = args.duplicateReason;
|
||||
}
|
||||
if (args.duplicateOfLeadId !== undefined) {
|
||||
patch.duplicateOfLeadId = args.duplicateOfLeadId;
|
||||
}
|
||||
|
||||
if (args.applyBlacklist) {
|
||||
patch.blacklistStatus = "blocked";
|
||||
if (args.blacklistReason !== undefined) {
|
||||
patch.blacklistReason = args.blacklistReason;
|
||||
} else if (lead.blacklistReason === undefined) {
|
||||
patch.blacklistReason = "Manuell in der Review als Sperrgrund gesetzt.";
|
||||
}
|
||||
if (args.priority === undefined || args.priority !== "blocked") {
|
||||
patch.priority = "blocked";
|
||||
}
|
||||
} else if (args.applyBlacklist === false && args.blacklistStatus !== undefined) {
|
||||
patch.blacklistStatus = args.blacklistStatus;
|
||||
patch.blacklistReason = args.blacklistReason;
|
||||
} else if (args.blacklistStatus !== undefined) {
|
||||
patch.blacklistStatus = args.blacklistStatus;
|
||||
patch.blacklistReason = args.blacklistReason;
|
||||
}
|
||||
|
||||
const reviewContactPatch = buildReviewContactPatch({
|
||||
email: args.reviewEmail,
|
||||
emailSource: args.reviewEmailSource,
|
||||
contactPerson: args.reviewContactPerson,
|
||||
isBusinessContactAddress: args.reviewIsBusinessContactAddress,
|
||||
explicitContactStatus: args.contactStatus !== undefined,
|
||||
currentContactStatus: lead.contactStatus,
|
||||
});
|
||||
|
||||
if (reviewContactPatch?.patch) {
|
||||
Object.assign(patch, reviewContactPatch.patch);
|
||||
}
|
||||
|
||||
if (
|
||||
reviewContactPatch !== null &&
|
||||
reviewContactPatch.setContactStatus !== undefined &&
|
||||
args.contactStatus === undefined
|
||||
) {
|
||||
patch.contactStatus = reviewContactPatch.setContactStatus;
|
||||
}
|
||||
|
||||
if (args.blacklistReason !== undefined && patch.blacklistStatus === undefined) {
|
||||
patch.blacklistStatus = "blocked";
|
||||
patch.blacklistReason = args.blacklistReason;
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.id, patch);
|
||||
return args.id;
|
||||
export const reviewUpdateInternal = internalMutation({
|
||||
args: reviewUpdateArgs,
|
||||
handler: async (ctx, args) => {
|
||||
return await reviewUpdateLead(ctx, args);
|
||||
},
|
||||
});
|
||||
|
||||
export const get = query({
|
||||
args: { id: v.id("leads") },
|
||||
handler: async (ctx, args) => {
|
||||
await requireOperator(ctx);
|
||||
return await ctx.db.get(args.id);
|
||||
},
|
||||
});
|
||||
|
||||
export const getInternal = internalQuery({
|
||||
args: { id: v.id("leads") },
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.id);
|
||||
@@ -313,20 +341,11 @@ export const get = query({
|
||||
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"),
|
||||
),
|
||||
),
|
||||
contactStatus: v.optional(leadContactStatus),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await requireOperator(ctx);
|
||||
const limit = normalizeListLimit(args.limit);
|
||||
|
||||
if (args.campaignId) {
|
||||
@@ -360,6 +379,7 @@ export const listFunnel = query({
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await requireOperator(ctx);
|
||||
const limit = normalizeListLimit(args.limit);
|
||||
const leads = await ctx.db.query("leads").order("desc").take(limit);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user