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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | 2x 25x 25x 25x 2x 2x 2x 2x 12x 2x 2x | // Onboarding Step Validation Schemas
import { z } from "zod";
import {
indianMobileSchema,
optionalIndianPhoneSchema,
} from "@interioring/utils/validation/phone";
import { freeTextSchema } from "@interioring/utils/validation/free-text";
// Step 1: Simplified Business Info (name + slug + city + business type + whatsapp + logo)
export const step1Schema = z.object({
businessName: z.string().min(2).max(100),
slug: z
.string()
.min(3)
.max(50)
.regex(
/^[a-z0-9-]+$/,
"Slug must be lowercase alphanumeric with hyphens only",
)
.refine(
(val) => /^[a-z]/.test(val),
"Must start with a letter",
)
.refine(
(val) => !val.startsWith("-") && !val.endsWith("-"),
"Slug cannot start or end with a hyphen",
)
.refine(
(val) => !val.includes("--"),
"Slug cannot contain consecutive hyphens",
),
cityId: z.string(),
businessTypeId: z.string().min(1, "Select a business type"),
whatsapp: indianMobileSchema,
logoUrl: z.string().optional().or(z.literal("")),
});
// Step 2: About Your Brand (skippable). Free-text fields require ≥1 letter
// when present so numeric-only "999" / "12345" no longer passes (audit during
// issue #563 found this gap).
export const step2Schema = z.object({
tagline: freeTextSchema({ minLen: 3, requireLetter: true, maxLen: 120 }),
about: freeTextSchema({ minLen: 10, requireLetter: true, maxLen: 1000 }),
logoUrl: z.string().optional().or(z.literal("")),
coverImage: z.string().optional().or(z.literal("")),
processDescription: freeTextSchema({
minLen: 10,
requireLetter: true,
maxLen: 500,
}),
});
// Step 3: Contact & Social (skippable)
export const step3Schema = z.object({
whatsapp: optionalIndianPhoneSchema,
email: z.string().email().max(200).optional().or(z.literal("")),
instagramHandle: z.string().max(50).optional(),
facebookUrl: z.string().url().max(200).optional().or(z.literal("")),
youtubeUrl: z.string().url().max(200).optional().or(z.literal("")),
googleBusinessUrl: z.string().url().max(200).optional().or(z.literal("")),
websiteUrl: z.string().url().max(200).optional().or(z.literal("")),
linkedinUrl: z.string().url().max(200).optional().or(z.literal("")),
whatsappBusinessNumber: optionalIndianPhoneSchema,
});
// Step 4: What You Do (Services, Materials & Brands)
export const step4Schema = z.object({
businessTypeId: z.string().min(1, "Select a primary business type"),
businessTypesSecondary: z.array(z.string()).optional(),
serviceCategoryIds: z.array(z.string()).optional(),
materialTagIds: z.array(z.string()).optional(),
brandsWorkWith: z.array(z.string()).optional(),
brandsOfficialPartner: z.array(z.string()).optional(),
});
// Step 5: Your Customers
export const step5Schema = z
.object({
customerSegmentId: z.string().min(1, "Select a primary customer segment"),
customerSegmentsSecondary: z.array(z.string()).optional(),
priceRangeMin: z
.number()
.positive("Minimum job cost is required")
.min(10000, "Minimum ₹10,000")
.max(100000000, "Maximum ₹10 crore"),
priceRangeMax: z
.number()
.positive("Maximum job cost is required")
.min(10000, "Minimum ₹10,000")
.max(100000000, "Maximum ₹10 crore")
.optional(),
pricingModel: z.enum([
"per_sqft",
"fixed_quote",
"cost_plus",
"daily_rate",
"package_based",
]),
})
.refine(
(data) =>
data.priceRangeMax === undefined ||
data.priceRangeMin <= data.priceRangeMax,
{
message:
"Minimum price must be less than or equal to maximum price",
path: ["priceRangeMin"],
},
);
// Step 6: How You Deliver
export const step6Schema = z.object({
factoryMade: z.boolean().optional(),
factoryType: z.enum(["own", "partner"]).optional(),
siteExecution: z.boolean().optional(),
labourOnly: z.boolean().optional(),
fullSiteExecution: z.boolean().optional(),
materialApproach: z
.enum(["we_provide", "customer_provides", "combination"])
.optional(),
billingModel: z
.enum(["sqft_rates", "fixed_quote", "combination"])
.optional(),
deliveryDaysMin: z.number().int().positive().optional(),
deliveryDaysMax: z.number().int().positive().nullable().optional(),
consultationFee: z.enum(["free", "paid", "adjustable"]).optional(),
warrantyYears: z.number().int().min(0).max(25).optional(),
hasAfterSalesService: z.boolean().optional(),
hasShowroom: z.boolean().optional(),
provides3DDesign: z.boolean().optional(),
});
// Step 7: Your First Project (skippable)
export const step7Schema = z.object({
projectCreated: z.boolean().optional(),
});
// Typed profileFields JSON column — covers all fields stored in pro.profileFields
export interface ProfileFields {
factoryMade?: boolean;
factoryType?: "own" | "partner";
siteExecution?: boolean;
labourOnly?: boolean;
fullSiteExecution?: boolean;
materialApproach?: "we_provide" | "customer_provides" | "combination";
billingModel?: "sqft_rates" | "fixed_quote" | "combination";
deliveryDaysMin?: number;
deliveryDaysMax?: number | null;
pricingModel?:
| "per_sqft"
| "fixed_quote"
| "cost_plus"
| "daily_rate"
| "package_based";
minProjectValue?: number;
consultationFee?: "free" | "paid" | "adjustable";
warrantyYears?: number;
hasAfterSalesService?: boolean;
hasShowroom?: boolean;
provides3DDesign?: boolean;
}
// Type exports
export type Step1Data = z.infer<typeof step1Schema>;
export type Step2Data = z.infer<typeof step2Schema>;
export type Step3Data = z.infer<typeof step3Schema>;
export type Step4Data = z.infer<typeof step4Schema>;
export type Step5Data = z.infer<typeof step5Schema>;
export type Step6Data = z.infer<typeof step6Schema>;
export type Step7Data = z.infer<typeof step7Schema>;
|