All files / src/lib validation.ts

100% Statements 16/16
100% Branches 18/18
100% Functions 5/5
100% Lines 15/15

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                                                                          51x                         77x 77x                         18x 13x 13x                                   81x 81x 13x   68x 7x   61x               54x 54x                         11x    
// Validation utilities for forms and API requests
import { parseIndianPhone } from "@interioring/utils/validation/phone/core";
import {
	type CustomerNameError,
	validateCustomerName as sharedValidateCustomerName,
} from "@interioring/utils/validation/customer-name";
 
// Type for incoming inquiry request body (before validation)
export interface InquiryRequestBody {
	proId?: string;
	projectId?: string;
	customerName?: string;
	customerPhone?: string;
	customerEmail?: string;
	requirement?: string;
	budget?: string;
	type?: string;
	requirementType?: string;
	requirementDescription?: string;
	sourceType?: string;
	sourcePage?: string;
}
 
// Type for validated inquiry request
export interface ValidatedInquiryRequest {
	proId: string;
	customerName: string;
	customerPhone: string;
	projectId?: string;
	customerEmail?: string;
	requirement?: string;
	budget?: string;
}
 
// Homeowner wording. The shared messages ("Customer name is required") are
// written for pros entering a client in the Portal and for the API, so the
// marketplace keeps the rule but says it to the person whose name it is.
const NAME_ERROR_MESSAGES: Record<CustomerNameError, string> = {
	required: "Please enter your name.",
	too_short: "Please enter your name (at least 2 letters).",
	invalid_chars:
		"Your name can only contain letters, spaces, apostrophes, periods and hyphens.",
};
 
/**
 * Validates a customer name. Thin wrapper over `@interioring/utils` that
 * preserves the existing string-or-null return contract for marketplace
 * form callers that show `result.message` directly in error UI.
 */
export function validateCustomerName(name: string): string | null {
	const error = sharedValidateCustomerName(name);
	return error ? NAME_ERROR_MESSAGES[error] : null;
}
 
/**
 * Type guard to validate required inquiry fields
 */
export function isValidInquiryRequest(
	body: unknown,
): body is InquiryRequestBody & {
	proId: string;
	customerName: string;
	customerPhone: string;
} {
	if (typeof body !== "object" || body === null) return false;
	const b = body as Record<string, unknown>;
	return (
		typeof b.proId === "string" &&
		b.proId.length > 0 &&
		typeof b.customerName === "string" &&
		b.customerName.length > 0 &&
		typeof b.customerPhone === "string" &&
		b.customerPhone.length > 0
	);
}
 
/**
 * Validate Indian phone number format. Returns an error message if invalid,
 * or null if valid. Backed by libphonenumber-js (via @interioring/phone) so it
 * accepts any Indian mobile format the user might type — with or without
 * country code, with or without spaces/dashes — and rejects landlines for
 * the marketplace inquiry flow (WhatsApp/SMS only deliver to mobiles).
 */
export function validateIndianPhoneNumber(phone: string): string | null {
	const parsed = parseIndianPhone(phone);
	if (!parsed) {
		return "Enter a valid 10-digit Indian mobile number.";
	}
	if (parsed.type !== "mobile") {
		return "Phone number must be a mobile (we use WhatsApp/SMS to confirm inquiries).";
	}
	return null;
}
 
/**
 * Extract digits from phone number string. Strips +91, spaces, and other
 * formatting via the canonical parser so callers always see consistent input.
 */
export function extractPhoneDigits(phone: string): string {
	const parsed = parseIndianPhone(phone);
	return parsed ? parsed.e164.slice(3) : phone.replace(/\D/g, "");
}
 
/**
 * The value a 10-digit mobile box should hold after an input event, typed or
 * pasted. A pasted "90000 10701", "+91 90000 10701" or "090000 10701" becomes
 * "9000010701"; a partial number keeps its digits so typing still works.
 *
 * The box must not cap its raw length at 10 (`maxLength={10}`): the browser
 * truncates a paste BEFORE this runs, so "90000 10701" arrived as
 * "90000 1070" and lost its last digit (#1085 QA).
 */
export function toMobileInputDigits(value: string): string {
	return extractPhoneDigits(value).slice(0, 10);
}