Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | 25x 2x 10x 10x 2x 8x 2x 6x 6x 14x 6x 3x 2x 2x 1x 1x 1x 3x 5x 5x 1x 4x 1x 3x 1x 2x 2x 1x 1x 4x 4x 1x 3x 1x 2x 2x 1x 1x | // Lead Sources Service - Business Logic Layer
import type { Dal } from "../../dal";
import type { LeadSource } from "../../db/schema";
import {
ForbiddenError,
NotFoundError,
ValidationError,
ConflictError,
} from "../../lib/errors";
export class LeadSourcesService {
constructor(private dal: Dal) {}
async getSources(
proId: string,
includeInactive = false,
): Promise<LeadSource[]> {
return this.dal.leadSources.findByProId(proId, includeInactive);
}
async createSource(
proId: string,
input: { name: string; icon?: string; color?: string },
): Promise<LeadSource> {
const name = input.name.trim();
if (!name || name.length > 50) {
throw new ValidationError(
"Source name must be between 1 and 50 characters",
);
}
if (input.color && !/^#[0-9A-Fa-f]{3,6}$/.test(input.color)) {
throw new ValidationError(
"Color must be a valid hex color (e.g. #FF6B35)",
);
}
// Check for duplicate name (including inactive sources)
const existing = await this.dal.leadSources.findByProId(proId, true);
const duplicate = existing.find(
(s) => s.name.toLowerCase() === name.toLowerCase(),
);
if (duplicate) {
// If the duplicate is inactive, reactivate it instead of creating a new one
if (!duplicate.isActive) {
const reactivated = await this.dal.leadSources.reactivate(duplicate.id);
if (!reactivated) {
throw new NotFoundError("Lead source", String(duplicate.id));
}
return reactivated;
}
throw new ConflictError(`Source "${name}" already exists`);
}
return this.dal.leadSources.create({
proId,
name,
icon: input.icon,
color: input.color,
isSystem: false,
});
}
async deactivateSource(
proId: string,
sourceId: number,
): Promise<LeadSource> {
const source = await this.dal.leadSources.findById(sourceId);
if (!source) {
throw new NotFoundError("Lead source", String(sourceId));
}
if (source.proId !== proId) {
throw new ForbiddenError("Source does not belong to this pro");
}
// Cannot deactivate the system Marketplace source
if (source.isSystem) {
throw new ValidationError("System sources cannot be deactivated");
}
const updated = await this.dal.leadSources.deactivate(sourceId);
if (!updated) {
throw new NotFoundError("Lead source", String(sourceId));
}
return updated;
}
async reactivateSource(
proId: string,
sourceId: number,
): Promise<LeadSource> {
const source = await this.dal.leadSources.findById(sourceId);
if (!source) {
throw new NotFoundError("Lead source", String(sourceId));
}
if (source.proId !== proId) {
throw new ForbiddenError("Source does not belong to this pro");
}
const updated = await this.dal.leadSources.reactivate(sourceId);
if (!updated) {
throw new NotFoundError("Lead source", String(sourceId));
}
return updated;
}
}
|