All files / lib/validators leads.schema.ts

100% Statements 5/5
100% Branches 0/0
100% Functions 0/0
100% Lines 5/5

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          3x                                                             3x                                       3x             3x         3x            
import { z } from "zod";
import { internationalPhoneSchema } from "@interioring/utils/validation/phone";
import { customerNameSchema } from "@interioring/utils/validation/customer-name";
import { freeTextSchema } from "@interioring/utils/validation/free-text";
 
export const createLeadSchema = z.object({
	// Shared schema rejects numeric-only / single-char / too-short. Marketplace
	// inquiries already use the same validator — keeping CRM in sync here so the
	// rule never drifts (audit found the gap during issue #563 fix). Layer a
	// `.max(200)` on top to preserve the existing column-aware ceiling.
	customerName: customerNameSchema.pipe(
		z.string().max(200, "Customer name is too long"),
	),
	// CRM leads support multiple countries (the AddLeadModal in Portal exposes
	// a country-code selector). Use the international validator here, not the
	// Indian-only one.
	phone: internationalPhoneSchema,
	email: z.string().email("Please enter a valid email").optional().or(z.literal("")),
	// Free-text location: must have ≥1 letter when present so "12345" no longer
	// passes. Allows digits ("HSR Layout 5th Block") via requireLetter+digits.
	location: freeTextSchema({
		minLen: 2,
		requireLetter: true,
		maxLen: 200,
	}),
	leadSourceId: z.number().int().positive("Please select a lead source"),
	projectType: z.string().max(100).optional().or(z.literal("")),
	budgetRange: z.string().max(100).optional().or(z.literal("")),
	requirement: z.string().max(2000, "Requirement notes are too long").optional().or(z.literal("")),
});
 
// Partial-update schema for PATCH /:proId/crm/leads/:leadId. Same field rules
// as create, but every field is optional. Issue #563 audit caught that the
// PATCH path bypassed validation entirely (raw `c.req.json()`), allowing a pro
// to update an existing lead's name/location to junk values via the lead-edit
// UI — defeating the create-time fix.
export const updateLeadSchema = z.object({
	customerName: customerNameSchema
		.pipe(z.string().max(200, "Customer name is too long"))
		.optional(),
	phone: internationalPhoneSchema.optional(),
	email: z.string().email("Please enter a valid email").optional().or(z.literal("")),
	location: freeTextSchema({
		minLen: 2,
		requireLetter: true,
		maxLen: 200,
	}),
	projectType: z.string().max(100).optional().or(z.literal("")),
	budgetRange: z.string().max(100).optional().or(z.literal("")),
	requirement: z
		.string()
		.max(2000, "Requirement notes are too long")
		.optional()
		.or(z.literal("")),
});
 
export const changeStageSchema = z.object({
	stageId: z.number().int().positive(),
	orderValuePaise: z.number().int().min(0).optional(),
	lossReason: z.string().max(500).optional().or(z.literal("")),
	lossReasonCategory: z.string().max(100).optional().or(z.literal("")),
});
 
export const contactMethodSchema = z.object({
	method: z.string().min(1, "Contact method is required"),
	note: z.string().max(1000).optional().or(z.literal("")),
});
 
export const valueSchema = z.object({
	valuePaise: z.number().int().min(0, "Value must be non-negative"),
});
 
export type CreateLeadInput = z.infer<typeof createLeadSchema>;
export type UpdateLeadInput = z.infer<typeof updateLeadSchema>;