All files / services inquiry.service.ts

100% Statements 30/30
100% Branches 24/24
100% Functions 3/3
100% Lines 30/30

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(", ")}`,
			);
		}
	}
}