feat: add lead qualification workflow
This commit is contained in:
@@ -29,7 +29,7 @@ export type ReviewQueueItem = {
|
||||
detail: string;
|
||||
};
|
||||
|
||||
export type LeadPriority = "high" | "medium" | "low" | "defer";
|
||||
export type LeadPriority = "high" | "medium" | "low" | "defer" | "blocked";
|
||||
|
||||
export type LeadContactStatus =
|
||||
| "new"
|
||||
@@ -41,6 +41,11 @@ export type LeadContactStatus =
|
||||
| "do_not_contact";
|
||||
|
||||
export type LeadBlacklistStatus = "clear" | "blocked";
|
||||
export type LeadDuplicateStatus =
|
||||
| "unchecked"
|
||||
| "unique"
|
||||
| "possible_duplicate"
|
||||
| "duplicate";
|
||||
|
||||
export type OutreachApprovalStatus = "draft" | "approved" | "rejected";
|
||||
export type OutreachSendStatus = "not_sent" | "queued" | "sent" | "failed";
|
||||
@@ -151,14 +156,15 @@ export const leadFunnelStages: LeadFunnelStage[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const priorityLabels: Record<LeadPriority, string> = {
|
||||
export const leadPriorityLabels: Record<LeadPriority, string> = {
|
||||
high: "Hoch",
|
||||
medium: "Mittel",
|
||||
low: "Niedrig",
|
||||
defer: "Zurückstellen",
|
||||
blocked: "Gesperrt",
|
||||
};
|
||||
|
||||
const contactStatusLabels: Record<LeadContactStatus, string> = {
|
||||
export const leadContactStatusLabels: Record<LeadContactStatus, string> = {
|
||||
new: "Neu",
|
||||
missing_contact: "Kontakt fehlt",
|
||||
audit_ready: "Audit bereit",
|
||||
@@ -168,6 +174,61 @@ const contactStatusLabels: Record<LeadContactStatus, string> = {
|
||||
do_not_contact: "Nicht kontaktieren",
|
||||
};
|
||||
|
||||
export const leadDuplicateStatusLabels: Record<LeadDuplicateStatus, string> = {
|
||||
unchecked: "Noch nicht geprüft",
|
||||
unique: "Einzigartig",
|
||||
possible_duplicate: "Möglicher Doppelter",
|
||||
duplicate: "Duplikat",
|
||||
};
|
||||
|
||||
export const leadBlacklistStatusLabels: Record<LeadBlacklistStatus, string> = {
|
||||
clear: "Offen",
|
||||
blocked: "Gesperrt",
|
||||
};
|
||||
|
||||
export const leadPriorityOptions: LeadPriority[] = [
|
||||
"high",
|
||||
"medium",
|
||||
"low",
|
||||
"defer",
|
||||
"blocked",
|
||||
];
|
||||
|
||||
export const leadContactStatusOptions: LeadContactStatus[] = [
|
||||
"new",
|
||||
"missing_contact",
|
||||
"audit_ready",
|
||||
"outreach_ready",
|
||||
"contacted",
|
||||
"replied",
|
||||
"do_not_contact",
|
||||
];
|
||||
|
||||
export const leadDuplicateStatusOptions: LeadDuplicateStatus[] = [
|
||||
"unchecked",
|
||||
"unique",
|
||||
"possible_duplicate",
|
||||
"duplicate",
|
||||
];
|
||||
|
||||
export const leadBlacklistStatusOptions: LeadBlacklistStatus[] = ["clear", "blocked"];
|
||||
|
||||
export function getLeadPriorityLabel(priority: LeadPriority): string {
|
||||
return leadPriorityLabels[priority];
|
||||
}
|
||||
|
||||
export function getLeadContactStatusLabel(status: LeadContactStatus): string {
|
||||
return leadContactStatusLabels[status];
|
||||
}
|
||||
|
||||
export function getLeadDuplicateStatusLabel(status: LeadDuplicateStatus): string {
|
||||
return leadDuplicateStatusLabels[status];
|
||||
}
|
||||
|
||||
export function getLeadBlacklistStatusLabel(status: LeadBlacklistStatus): string {
|
||||
return leadBlacklistStatusLabels[status];
|
||||
}
|
||||
|
||||
export function toLeadFunnelCard(lead: LeadFunnelInput): LeadFunnelCard {
|
||||
return {
|
||||
id: lead.id,
|
||||
@@ -175,8 +236,8 @@ export function toLeadFunnelCard(lead: LeadFunnelInput): LeadFunnelCard {
|
||||
company: lead.companyName,
|
||||
niche: lead.niche ?? "Nische offen",
|
||||
location: formatLeadLocation(lead),
|
||||
priorityLabel: priorityLabels[lead.priority],
|
||||
contactStatusLabel: contactStatusLabels[lead.contactStatus],
|
||||
priorityLabel: getLeadPriorityLabel(lead.priority),
|
||||
contactStatusLabel: getLeadContactStatusLabel(lead.contactStatus),
|
||||
nextAction: getLeadNextAction(lead),
|
||||
websiteDomain: lead.websiteDomain,
|
||||
contactDetail: formatContactDetail(lead),
|
||||
@@ -198,6 +259,7 @@ function getLeadFunnelStageId(lead: LeadFunnelInput): LeadFunnelStageId {
|
||||
if (
|
||||
lead.blacklistStatus === "blocked" ||
|
||||
lead.priority === "defer" ||
|
||||
lead.priority === "blocked" ||
|
||||
lead.contactStatus === "do_not_contact"
|
||||
) {
|
||||
return "deferred";
|
||||
|
||||
@@ -228,6 +228,13 @@ type GooglePlaceDisplayName =
|
||||
text?: string;
|
||||
};
|
||||
|
||||
type GooglePlaceContactEmailSource = {
|
||||
email: string;
|
||||
emailSource?: string | null;
|
||||
contactPerson?: string | null;
|
||||
isBusinessContactAddress?: boolean;
|
||||
};
|
||||
|
||||
type GooglePlaceApiPlace = {
|
||||
id?: string;
|
||||
displayName?: GooglePlaceDisplayName;
|
||||
@@ -254,6 +261,11 @@ export type GooglePlaceCandidate = {
|
||||
websiteUrl: string | null;
|
||||
websiteDomain: string | null;
|
||||
phone: string | null;
|
||||
email?: string | null;
|
||||
emailSource?: string | null;
|
||||
contactPerson?: string | null;
|
||||
isBusinessContactAddress?: boolean;
|
||||
contactEmails?: GooglePlaceContactEmailSource[];
|
||||
rating: number | null;
|
||||
userRatingCount: number | null;
|
||||
businessStatus: string | null;
|
||||
@@ -297,6 +309,163 @@ function normalizeWebsiteDomain(input?: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
const GENERIC_BUSINESS_EMAIL_LOCAL_PARTS = new Set([
|
||||
"info",
|
||||
"kontakt",
|
||||
"hello",
|
||||
"hallo",
|
||||
"office",
|
||||
"post",
|
||||
"service",
|
||||
"team",
|
||||
"anfrage",
|
||||
]);
|
||||
|
||||
export function normalizeText(value?: string | null) {
|
||||
return value?.trim().toLowerCase().replace(/\s+/g, " ") ?? "";
|
||||
}
|
||||
|
||||
export function normalizeEmailAddress(value?: string | null) {
|
||||
const valueTrimmed = value?.trim().toLowerCase();
|
||||
|
||||
if (!valueTrimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [localPart, domain] = valueTrimmed.split("@");
|
||||
|
||||
if (!localPart || !domain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9._%+-]+$/.test(localPart)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!/^[^\s@]+\.[^\s@]+$/.test(domain)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return valueTrimmed;
|
||||
}
|
||||
|
||||
export type UsableContactEmail = {
|
||||
email: string;
|
||||
emailSource: string | null;
|
||||
contactPerson: string | null;
|
||||
};
|
||||
|
||||
type ParsedContactEmail = {
|
||||
email: string;
|
||||
emailSource: string | null;
|
||||
contactPerson: string | null;
|
||||
isBusinessContactAddress: boolean;
|
||||
isGeneric: boolean;
|
||||
};
|
||||
|
||||
type ContactEmailRuleInput = {
|
||||
email: string;
|
||||
emailSource?: string | null;
|
||||
contactPerson?: string | null;
|
||||
isBusinessContactAddress?: boolean;
|
||||
};
|
||||
|
||||
export function getUsableContactEmailFromEntries(
|
||||
entries: ContactEmailRuleInput[] | undefined,
|
||||
) {
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedEntries: ParsedContactEmail[] = [];
|
||||
|
||||
for (const emailEntry of entries) {
|
||||
const normalized = normalizeEmailAddress(emailEntry.email);
|
||||
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
|
||||
parsedEntries.push({
|
||||
email: normalized,
|
||||
emailSource: emailEntry.emailSource ?? null,
|
||||
contactPerson: emailEntry.contactPerson ?? null,
|
||||
isBusinessContactAddress: emailEntry.isBusinessContactAddress === true,
|
||||
isGeneric: isGenericBusinessEmail(normalized),
|
||||
});
|
||||
}
|
||||
|
||||
const generic = parsedEntries.find((entry) => entry.isGeneric);
|
||||
if (generic) {
|
||||
return {
|
||||
email: generic.email,
|
||||
emailSource: generic.emailSource,
|
||||
contactPerson: generic.contactPerson,
|
||||
};
|
||||
}
|
||||
|
||||
const named = parsedEntries.find((entry) => entry.isBusinessContactAddress);
|
||||
if (!named) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
email: named.email,
|
||||
emailSource: named.emailSource,
|
||||
contactPerson: named.contactPerson,
|
||||
};
|
||||
}
|
||||
|
||||
function getCandidateEmailMetadata(candidate: GooglePlaceCandidate) {
|
||||
const emails: GooglePlaceContactEmailSource[] = [];
|
||||
|
||||
if (candidate.email) {
|
||||
emails.push({
|
||||
email: candidate.email,
|
||||
emailSource: candidate.emailSource,
|
||||
contactPerson: candidate.contactPerson,
|
||||
isBusinessContactAddress: candidate.isBusinessContactAddress,
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(candidate.contactEmails)) {
|
||||
emails.push(...candidate.contactEmails);
|
||||
}
|
||||
|
||||
return emails;
|
||||
}
|
||||
|
||||
export function getCandidateEmailValues(candidate: GooglePlaceCandidate) {
|
||||
return getCandidateEmailMetadata(candidate)
|
||||
.map((entry) => normalizeEmailAddress(entry.email))
|
||||
.filter((value): value is string => value !== null);
|
||||
}
|
||||
|
||||
function splitEmailLocalPart(email: string) {
|
||||
const [localPart] = email.split("@");
|
||||
|
||||
return localPart?.split("+")[0] ?? "";
|
||||
}
|
||||
|
||||
function isGenericBusinessEmail(email: string) {
|
||||
const normalizedLocalPart = splitEmailLocalPart(email).toLowerCase();
|
||||
|
||||
return GENERIC_BUSINESS_EMAIL_LOCAL_PARTS.has(normalizedLocalPart);
|
||||
}
|
||||
|
||||
export function getUsableContactEmail(
|
||||
candidate: GooglePlaceCandidate,
|
||||
): UsableContactEmail | null {
|
||||
return getUsableContactEmailFromEntries(
|
||||
getCandidateEmailMetadata(candidate).map((entry) => ({
|
||||
email: entry.email,
|
||||
emailSource: entry.emailSource,
|
||||
contactPerson: entry.contactPerson,
|
||||
isBusinessContactAddress: entry.isBusinessContactAddress,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizePlacesResponse(
|
||||
response: GooglePlacesApiResponse,
|
||||
fetchedAt: number,
|
||||
@@ -333,6 +502,10 @@ export function normalizePlacesResponse(
|
||||
export type ExistingLeadLike = {
|
||||
googlePlaceId?: string | null;
|
||||
websiteDomain?: string | null;
|
||||
email?: string | null;
|
||||
companyName?: string | null;
|
||||
address?: string | null;
|
||||
phone?: string | null;
|
||||
};
|
||||
|
||||
export type BlacklistRow = {
|
||||
@@ -342,20 +515,25 @@ export type BlacklistRow = {
|
||||
};
|
||||
|
||||
export type BlacklistLookupValue = {
|
||||
type: "domain" | "phone" | "company" | "google_place_id";
|
||||
type: "domain" | "email" | "phone" | "company" | "google_place_id";
|
||||
normalizedValue: string;
|
||||
};
|
||||
|
||||
function normalizeDomain(value?: string | null) {
|
||||
export function normalizeDomain(value?: string | null) {
|
||||
return value?.trim().toLowerCase().replace(/^www\./, "") ?? "";
|
||||
}
|
||||
|
||||
function normalizePhone(value?: string | null) {
|
||||
export function normalizePhone(value?: string | null) {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return value.replace(/\D+/g, "");
|
||||
const digits = value.replace(/\D+/g, "");
|
||||
if (digits.startsWith("00")) {
|
||||
return digits.slice(2);
|
||||
}
|
||||
|
||||
return digits;
|
||||
}
|
||||
|
||||
function uniqueLookupValues(values: BlacklistLookupValue[]) {
|
||||
@@ -375,6 +553,8 @@ function uniqueLookupValues(values: BlacklistLookupValue[]) {
|
||||
export function getBlacklistLookupValues(
|
||||
candidate: GooglePlaceCandidate,
|
||||
): BlacklistLookupValue[] {
|
||||
const emailAddresses = getCandidateEmailValues(candidate);
|
||||
|
||||
return uniqueLookupValues([
|
||||
{
|
||||
type: "google_place_id",
|
||||
@@ -386,7 +566,7 @@ export function getBlacklistLookupValues(
|
||||
},
|
||||
{
|
||||
type: "company",
|
||||
normalizedValue: normalizeDomain(candidate.businessName),
|
||||
normalizedValue: normalizeText(candidate.businessName),
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
@@ -396,6 +576,10 @@ export function getBlacklistLookupValues(
|
||||
type: "phone",
|
||||
normalizedValue: normalizeDomain(candidate.phone),
|
||||
},
|
||||
...emailAddresses.map((email) => ({
|
||||
type: "email" as const,
|
||||
normalizedValue: email ?? "",
|
||||
})),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -405,25 +589,57 @@ export function isDuplicateCandidate(
|
||||
): boolean {
|
||||
const candidatePlaceId = normalizeDomain(candidate.placeId);
|
||||
const candidateDomain = normalizeDomain(candidate.websiteDomain);
|
||||
const candidateEmails = getCandidateEmailValues(candidate);
|
||||
|
||||
return existing.some((entry) => {
|
||||
const entryPlaceId = normalizeDomain(entry.googlePlaceId);
|
||||
const entryDomain = normalizeDomain(entry.websiteDomain);
|
||||
const entryEmail = normalizeEmailAddress(entry.email);
|
||||
|
||||
return (
|
||||
(candidatePlaceId && entryPlaceId === candidatePlaceId) ||
|
||||
(candidateDomain && entryDomain === candidateDomain)
|
||||
(candidateDomain && entryDomain === candidateDomain) ||
|
||||
candidateEmails.some(
|
||||
(candidateEmail) => candidateEmail && entryEmail === candidateEmail,
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function isProbableDuplicateCandidate(
|
||||
candidate: GooglePlaceCandidate,
|
||||
existing: ExistingLeadLike[],
|
||||
): boolean {
|
||||
const candidateCompany = normalizeText(candidate.businessName);
|
||||
const candidateAddress = normalizeText(candidate.address);
|
||||
const candidatePhone = normalizePhone(candidate.phone);
|
||||
|
||||
return existing.some((entry) => {
|
||||
const entryCompany = normalizeText(entry.companyName);
|
||||
const entryAddress = normalizeText(entry.address);
|
||||
const entryPhone = normalizePhone(entry.phone);
|
||||
|
||||
const isSameCompanyAndAddress =
|
||||
candidateCompany &&
|
||||
candidateAddress &&
|
||||
entryCompany &&
|
||||
entryAddress &&
|
||||
candidateCompany === entryCompany &&
|
||||
candidateAddress === entryAddress;
|
||||
|
||||
const isSamePhone = candidatePhone && entryPhone && candidatePhone === entryPhone;
|
||||
|
||||
return isSameCompanyAndAddress || isSamePhone;
|
||||
});
|
||||
}
|
||||
|
||||
export function getBlacklistMatches(
|
||||
candidate: GooglePlaceCandidate,
|
||||
blacklistRows: BlacklistRow[],
|
||||
) {
|
||||
const candidatePlaceId = normalizeDomain(candidate.placeId);
|
||||
const candidateDomain = normalizeDomain(candidate.websiteDomain);
|
||||
const candidateCompany = normalizeDomain(candidate.businessName);
|
||||
const candidateCompany = normalizeText(candidate.businessName);
|
||||
const candidatePhone = normalizePhone(candidate.phone);
|
||||
|
||||
return blacklistRows.filter((row) => {
|
||||
@@ -446,6 +662,10 @@ export function getBlacklistMatches(
|
||||
(row.normalizedValue === candidatePhone ||
|
||||
normalizePhone(row.value) === candidatePhone)
|
||||
);
|
||||
case "email":
|
||||
return getCandidateEmailValues(candidate).some(
|
||||
(candidateEmail) => candidateEmail === row.normalizedValue,
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { GooglePlaceCandidate } from "./lead-discovery-google";
|
||||
import {
|
||||
normalizePhone,
|
||||
normalizeText,
|
||||
getUsableContactEmail,
|
||||
type GooglePlaceCandidate,
|
||||
} from "./lead-discovery-google";
|
||||
import type { Id } from "../convex/_generated/dataModel";
|
||||
|
||||
type AgentRunLike = {
|
||||
status: string;
|
||||
@@ -12,8 +18,16 @@ type LeadDiscoveryCounterInput = {
|
||||
};
|
||||
|
||||
type LeadDiscoveryContactInput = {
|
||||
websiteDomain?: string | null;
|
||||
phone?: string | null;
|
||||
usableEmail?: string | null;
|
||||
};
|
||||
|
||||
export type LeadDiscoveryPriority = "high" | "medium" | "low" | "defer" | "blocked";
|
||||
|
||||
type LeadDiscoveryPriorityInput = {
|
||||
isBlacklisted?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
hasWebsite?: boolean;
|
||||
hasWebsiteSignal?: boolean;
|
||||
};
|
||||
|
||||
type LeadDiscoveryLeadRecordInput<TCampaignId extends string, TRunId extends string> = {
|
||||
@@ -70,7 +84,7 @@ export function buildLeadDiscoveryCounters(input: LeadDiscoveryCounterInput) {
|
||||
export function getLeadDiscoveryContactStatus(
|
||||
input: LeadDiscoveryContactInput,
|
||||
) {
|
||||
if (input.websiteDomain || input.phone) {
|
||||
if (input.usableEmail) {
|
||||
return "new";
|
||||
}
|
||||
|
||||
@@ -81,6 +95,14 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
TCampaignId extends string,
|
||||
TRunId extends string,
|
||||
>(input: LeadDiscoveryLeadRecordInput<TCampaignId, TRunId>) {
|
||||
type LeadDiscoveryDuplicateStatus =
|
||||
| "unchecked"
|
||||
| "unique"
|
||||
| "possible_duplicate"
|
||||
| "duplicate";
|
||||
|
||||
const usableEmail = getUsableContactEmail(input.candidate);
|
||||
|
||||
const lead: {
|
||||
campaignId: TCampaignId;
|
||||
discoveryRunId: TRunId;
|
||||
@@ -100,9 +122,21 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
websiteUrl?: string;
|
||||
websiteDomain?: string;
|
||||
phone?: string;
|
||||
priority: "medium";
|
||||
normalizedGooglePlaceId?: string;
|
||||
normalizedEmail?: string;
|
||||
normalizedPhone?: string;
|
||||
normalizedCompanyName?: string;
|
||||
normalizedAddress?: string;
|
||||
email?: string;
|
||||
emailSource?: string;
|
||||
contactPerson?: string;
|
||||
priorityReason?: string;
|
||||
duplicateReason?: string;
|
||||
duplicateOfLeadId?: Id<"leads">;
|
||||
blacklistReason?: string;
|
||||
priority: LeadDiscoveryPriority;
|
||||
contactStatus: "new" | "missing_contact";
|
||||
duplicateStatus: "unique";
|
||||
duplicateStatus: LeadDiscoveryDuplicateStatus;
|
||||
blacklistStatus: "clear";
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
@@ -119,8 +153,7 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
sourceFetchedAt: input.candidate.sourceFetchedAt,
|
||||
priority: "medium",
|
||||
contactStatus: getLeadDiscoveryContactStatus({
|
||||
websiteDomain: input.candidate.websiteDomain,
|
||||
phone: input.candidate.phone,
|
||||
usableEmail: usableEmail?.email,
|
||||
}),
|
||||
duplicateStatus: "unique",
|
||||
blacklistStatus: "clear",
|
||||
@@ -136,6 +169,21 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
const websiteUrl = optionalString(input.candidate.websiteUrl);
|
||||
const websiteDomain = optionalString(input.candidate.websiteDomain);
|
||||
const phone = optionalString(input.candidate.phone);
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
const normalizedCompanyName = normalizeText(input.candidate.businessName);
|
||||
const normalizedAddress = normalizeText(input.candidate.address);
|
||||
|
||||
if (normalizedCompanyName !== "") {
|
||||
lead.normalizedCompanyName = normalizedCompanyName;
|
||||
}
|
||||
|
||||
if (normalizedAddress !== "") {
|
||||
lead.normalizedAddress = normalizedAddress;
|
||||
}
|
||||
|
||||
if (normalizedPhone !== "") {
|
||||
lead.normalizedPhone = normalizedPhone;
|
||||
}
|
||||
|
||||
if (googleMapsUrl !== undefined) {
|
||||
lead.googleMapsUrl = googleMapsUrl;
|
||||
@@ -161,6 +209,55 @@ export function buildLeadDiscoveryLeadRecord<
|
||||
if (phone !== undefined) {
|
||||
lead.phone = phone;
|
||||
}
|
||||
if (usableEmail) {
|
||||
lead.normalizedEmail = usableEmail.email;
|
||||
lead.email = usableEmail.email;
|
||||
if (usableEmail.emailSource !== null) {
|
||||
lead.emailSource = usableEmail.emailSource;
|
||||
}
|
||||
if (usableEmail.contactPerson !== null) {
|
||||
lead.contactPerson = usableEmail.contactPerson;
|
||||
}
|
||||
} else {
|
||||
lead.contactStatus = "missing_contact";
|
||||
}
|
||||
|
||||
return lead;
|
||||
}
|
||||
|
||||
export function getLeadDiscoveryPriority(
|
||||
input: LeadDiscoveryPriorityInput,
|
||||
): { priority: LeadDiscoveryPriority; reason: string } {
|
||||
if (input.isBlacklisted) {
|
||||
return {
|
||||
priority: "blocked",
|
||||
reason: "Lead ist auf der Sperrliste.",
|
||||
};
|
||||
}
|
||||
|
||||
if (input.isDuplicate) {
|
||||
return {
|
||||
priority: "defer",
|
||||
reason: "Dublettenprüfung oder Reviewpause.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!input.hasWebsite) {
|
||||
return {
|
||||
priority: "high",
|
||||
reason: "Kein Website-Indikator vorhanden.",
|
||||
};
|
||||
}
|
||||
|
||||
if (input.hasWebsiteSignal) {
|
||||
return {
|
||||
priority: "low",
|
||||
reason: "Website vorhanden: geringer Kontaktaufwand.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
priority: "medium",
|
||||
reason: "Standardpriorität.",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user