All files / src/lib consultation.ts

100% Statements 16/16
100% Branches 19/19
100% Functions 1/1
100% Lines 16/16

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                                    2x                                         15x 2x   13x     13x 1x   12x 12x 2x         10x 4x 1x   3x 1x         2x     8x                
// Free Consultation Booking — shared constants + validation.
//
// PHASE 1 (standalone): this module is the single source of truth for the
// booking form's server-side payload validation and the booking-reference
// generator. It is intentionally self-contained — it does NOT touch the
// `inquiries` table, the API worker, or WhatsApp. Those hooks are wired in
// later phases (see the endpoint TODO markers).
//
// Simplified quick-book form: we ask only for a phone number and an optional
// free-text box of details to discuss on the call. The richer project-intent
// fields (type/city/budget/help/time) from the PRD are deferred to a later
// iteration.
//
// Feature spec: docs/product/prd/functional/free-consultation-booking (v2).
 
import { validateIndianPhoneNumber } from "./validation";
 
// Max length of the free-text "details to discuss" box.
export const DETAILS_MAX_LENGTH = 1000;
 
export interface ConsultationBookingPayload {
	phone: string;
	details?: string;
}
 
export type ConsultationValidationResult =
	| { valid: true; data: ConsultationBookingPayload }
	| { valid: false; error: string };
 
/**
 * Server-authoritative validation for a consultation booking submission.
 * Only the phone number is required; `details` is optional free text.
 * Returns the normalized payload on success, or a single user-facing error
 * message on the first failure. Reuses the marketplace phone validator so the
 * rules match the existing inquiry flow exactly.
 */
export function validateConsultationBooking(
	body: unknown,
): ConsultationValidationResult {
	if (typeof body !== "object" || body === null) {
		return { valid: false, error: "Invalid request." };
	}
	const b = body as Record<string, unknown>;
 
	// Phone (Indian mobile) — required.
	if (typeof b.phone !== "string" || b.phone.trim().length === 0) {
		return { valid: false, error: "Please enter your phone number." };
	}
	const phoneError = validateIndianPhoneNumber(b.phone);
	if (phoneError) {
		return { valid: false, error: phoneError };
	}
 
	// Details — optional free text.
	let details: string | undefined;
	if (b.details !== undefined && b.details !== null && b.details !== "") {
		if (typeof b.details !== "string") {
			return { valid: false, error: "Details must be text." };
		}
		if (b.details.length > DETAILS_MAX_LENGTH) {
			return {
				valid: false,
				error: `Details must be ${DETAILS_MAX_LENGTH} characters or fewer.`,
			};
		}
		details = b.details.trim();
	}
 
	return {
		valid: true,
		data: {
			phone: b.phone.trim(),
			details,
		},
	};
}