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 108 109 110 111 112 113 114 115 116 117 | 7x 19x 15x 15x 9x 1x 8x 2x 2x 1x 7x 7x 1x 6x 6x 1x 5x 5x 15x 15x 2x 13x 1x 12x 12x 1x 11x 1x 10x 1x | // Inquiry Service - Business Logic Layer
// Trimmed to create-only — pro/admin management removed (CRM replaces them).
// Marketplace inquiry submission still uses create().
import {
customerNameErrorMessage,
normalizeCustomerName,
validateCustomerName,
} from "@interioring/utils/validation/customer-name";
import { parseIndianPhone } from "@interioring/utils/validation/phone";
import type { Dal } from "../dal";
import type { Inquiry } from "../db/schema";
import { INQUIRY_TYPES } from "../db/schema";
import { NotFoundError, ValidationError } from "../lib/errors";
import { isValidEnum } from "../lib/utils";
export type CreateInquiryInput = {
proId: string;
projectId?: string;
type: string;
customerName: string;
customerPhone: string;
customerEmail?: string;
customerLocation?: string;
requirement?: string;
notes?: string;
sourcePage?: string;
requirementType?: string;
sourceType?: string;
// Set when the inquiry is submitted by a logged-in homeowner (Restrict Direct
// Contact). Links the inquiry to a `ho_users` account so it surfaces in the
// homeowner portal and can carry an on-platform message thread.
homeownerId?: string;
};
const MAX_ACTIVE_INQUIRIES_PER_PHONE = 3;
export class InquiryService {
constructor(private dal: Dal) {}
async create(input: CreateInquiryInput): Promise<Inquiry> {
this.validateInput(input);
// Verify pro exists
const pro = await this.dal.pros.findById(input.proId);
if (!pro) {
throw new NotFoundError("Pro", input.proId);
}
// Verify project if provided
if (input.projectId) {
const project = await this.dal.projects.findById(input.projectId);
if (!project) {
throw new NotFoundError("Project", input.projectId);
}
}
const parsedPhone = parseIndianPhone(input.customerPhone);
if (!parsedPhone) {
throw new ValidationError("Invalid Indian phone number");
}
// Enforce per-phone active inquiry cap
const activeCount = await this.dal.inquiries.countActiveByPhone(
parsedPhone.e164,
);
if (activeCount >= MAX_ACTIVE_INQUIRIES_PER_PHONE) {
throw new ValidationError(
`You've reached the limit of ${MAX_ACTIVE_INQUIRIES_PER_PHONE} active inquiries. A pro must close an existing inquiry before you can submit a new one.`,
);
}
const inquiry = await this.dal.inquiries.create({
proId: input.proId,
projectId: input.projectId,
homeownerId: input.homeownerId,
type: input.type as Inquiry["type"],
status: "new",
homeownerStatus: "submitted",
customerName: normalizeCustomerName(input.customerName),
customerPhone: parsedPhone.e164,
customerEmail: input.customerEmail?.trim(),
customerLocation: input.customerLocation?.trim(),
requirement: input.requirement?.trim(),
notes: input.notes?.trim(),
sourcePage: input.sourcePage,
requirementType: input.requirementType,
sourceType: input.sourceType,
});
return inquiry;
}
private validateInput(input: CreateInquiryInput): void {
const nameError = validateCustomerName(input.customerName ?? "");
if (nameError) {
throw new ValidationError(customerNameErrorMessage(nameError));
}
if (!input.customerPhone?.trim()) {
throw new ValidationError("Customer phone is required");
}
const parsed = parseIndianPhone(input.customerPhone);
if (!parsed || parsed.type !== "mobile") {
throw new ValidationError(
"Customer phone must be a valid 10-digit Indian mobile number",
);
}
if (!input.type) {
throw new ValidationError("Inquiry type is required");
}
if (!isValidEnum(input.type, INQUIRY_TYPES)) {
throw new ValidationError(
`Invalid inquiry type. Must be one of: ${INQUIRY_TYPES.join(", ")}`,
);
}
}
}
|