# Customer form — adding the questions that feed the hair-system suggestion **Route:** `/branch-portal/consultation/customer-form` · **Written 14 Sep 2026** Measured against the live form and the deployed `customerSchema` chunk on the same day. Read §1 before planning the work. It changes what this form can show. --- ## 1. The customer form cannot show a product shortlist. Here is the proof. The engine refuses to suggest until **seven** inputs are present. This is `REQUIRED_FOR_SUGGESTION` in `shp-suggest.js`, not a guess: | Required input | Who can answer it | |---|---| | `priority` | **Customer** | | `density` | **Customer** | | `hair_length` (wanted on top) | **Customer** | | `hairpiece_type` | Stylist — toupee / hair patch (frontal) / hair patch (crown) / full cap wig is trade vocabulary | | `baldness_level` | Stylist — a Norwood shape picker, judged by eye | | `hair_texture` | Stylist — observed | | `remaining_thickness` | Stylist — observed volume, not a preference | Four of the seven are the stylist's observations. A client filling this in before the appointment **cannot** complete the set, so `shpSuggest()` returns `{ ok: false, missing: [...] }` every time. **So do not build a suggestion panel on this form.** Build the questions. The payoff is on the hairstylist form, where the panel now fires immediately with three of seven answers already in — instead of the stylist collecting them while the client sits there. If a shortlist must appear on the customer's screen, that is a different product decision and needs Mehmet, not a dev: it would mean recommending a specific hair system before anyone has looked at the client's scalp. ## 2. What to add One new section, after **Experience & Daily Routine**, matching the existing section pattern exactly. Six questions, all single-choice, all using the option-button component the form already uses. > **Reuse the existing option-button component. Do not restyle it.** > Measured on the live form, an option button is > `btn btn-lg font-normal capitalize p-7 rounded-xl w-full max-sm:w-full` > — 18px/400, background `#F8F8F8`, 1px border `#EDEDED`, 12px radius, 58px tall. > Its row is `flex gap-4 flex-wrap` (16px gap). The question prompt above it is > `text-left text-xl font-semibold` (20px/600) and the section heading is > `font-semibold text-2xl` (24px/600). Reusing the component keeps the selected > state correct without anyone copying CSS. ### Section heading **What you're looking for** ### 2.1 `priority` — required > **What matters most to you?** | Option shown | Stored | |---|---| | The most natural look | `natural` | | A balance of both | `balanced` | | Something that lasts | `durable` | This is the engine's single heaviest input — it is weighted ×3. If only one question from this list gets built, build this one. ### 2.2 `density` > **How full would you like it to look?** | Option shown | Stored | |---|---| | Light | `light` | | Medium | `Medium` | | Full | `Heavy` | **`density` is an array column** (`P().of(...)` in the schema, JSON-stringified on the wire). Send a single-element array: `["Medium"]`. The full enum is `light`, `Light-Med`, `Medium`, `Med-Heavy`, `Heavy`. **Measured caveat — do not expect this to change the products offered.** Swept across 24 client profiles and all five density values, the shortlist changed on **2 of 24**. The engine does read it (`flags.densityWant` moves `light` -> `full` correctly), but it only reaches the output at the `light` / `Light-Med` end. `Medium`, `Med-Heavy` and `Heavy` produced an identical shortlist in every profile tried: ``` light -> M304,HD111 Light-Med -> M304,HD111 Medium -> M100,M108,HD111 Med-Heavy -> M100,M108,HD111 Heavy -> M100,M108,HD111 ``` So of the three options above, **Medium and Full are the same answer as far as the suggestion is concerned.** Ask it anyway - it is real information for the fitting, and the stylist should see it - but nobody should expect it to move the shortlist, and it is not worth making required. If that is unsatisfying, the question to put to the team is whether the matrix *should* weigh density more heavily; that is a scoring change, not a form change. ### 2.3 `hair_length` — the field whose meaning changes > **How long do you want it on top?** | Option shown | Stored | |---|---| | Short — about 1.5–2″ | `short` | | 3–6 inches | `medium` | | Longer than 6 inches | `long` | | Not sure yet | `not sure` | **Read this carefully — it is the one migration risk in the whole change.** `hair_length` already exists. Its current options are `Short (6-10")`, `Medium (10-14")`, `Long (14-18")`, `Extra Long (longer than 18")` — extension ranges, on a form where the *shortest* choice already exceeds what most men wear on top. It also has a live off-by-one: the fourth button renders **"Extra Long"** but stores **`not sure`**, because the enum only holds `short | medium | long | not sure`. Replacing the option set means `short` / `medium` / `long` change meaning without changing value. Rows written before and after are indistinguishable by value — only by date. So: - **Stamp a cutover date** and treat `hair_length` on earlier consultations as unknown. The engine already supports this: `SHP_CONFIG.hairLengthCutover`. Set it to the deploy date. - A `very long` value exists in the suggestion engine for 12″+. Adding it to the enum is optional for the customer form — "Longer than 6 inches" is enough detail from a client — but the hairstylist form uses it. "Not sure yet" is offered here and **deliberately not offered on the stylist form**. A client filling this in alone should be able to say they don't know; a stylist sitting with the client should not. It reads as unanswered either way, so the panel names it rather than assuming. ### 2.4 `preferred_maintenance_location` > **Who will look after it?** | Option shown | Stored | |---|---| | Your salon | `At our salon` | | My own hairstylist | `Customer's Hairstylist` | | Myself, at home | `At home` | | Not sure yet | `Unsure` | Feeds the experience gate: salon-maintained unlocks the more delicate bases. ### 2.5 `is_client_willing_to_shave_head` > **Would you have the area shaved if it gave a better fit?** | Option shown | Stored | |---|---| | Yes | `yes` | | Not sure | `unsure` | | No | `no` | The enum also holds `not needed`, which is a stylist's judgement rather than a client's answer — leave it off this form. ### 2.6 `has_allergies_or_sensitivities` > **Any allergies or sensitivities to hair products or adhesives?** | Option shown | Stored | |---|---| | Yes | `yes` | | Not sure | `not sure` | | No | `no` | Health data. It is already covered by the consent text this form shows first ("health details you choose to share, such as allergies"), so no extra consent is needed — but it must not appear in any marketing export. ## 3. Not adding: `base_type` The original spec wanted "base type if they already know it". **There is no `base_type` column in the schema** — verified against the deployed `customerSchema` chunk on 14 Sep 2026. It would need a new column, most clients cannot answer it, and on the hairstylist form the equivalent question is filled 3% of the time. Skip it. The rule that depends on it (R-BASE) stays unbuilt until someone decides it is worth a column. ## 4. What the stylist form still needs Unchanged by this work, and still the blocker on the suggestion firing at all: - `remaining_thickness` — a genuinely new field. **Does not exist in the schema.** - `hair_length` — its option set replaced, as §2.3. With those two plus the customer's three, all seven inputs are present and the panel suggests. ## 5. Field reference | Column | Type | Notes | |---|---|---| | `priority` | string enum | `natural` \| `balanced` \| `durable` | | `density` | **array** of string enum | JSON-stringified on the wire | | `hair_length` | string enum | exists; option set changes meaning — see §2.3 | | `preferred_maintenance_location` | string enum | exists on the hairstylist form | | `is_client_willing_to_shave_head` | string enum | exists on the hairstylist form | | `has_allergies_or_sensitivities` | string enum | exists on the hairstylist form | | `remaining_thickness` | string enum | **new column**, hairstylist form | | `base_type` | — | **does not exist**, not being added | Four of the six already exist as columns — they are currently collected on the hairstylist form. Moving them earlier is a form change, not a schema change. ## 6. One thing to check before you start `priority`, `preferred_maintenance_location`, `is_client_willing_to_shave_head` and `has_allergies_or_sensitivities` are **already asked on the hairstylist form**. Adding them here means deciding whether the stylist still sees them: - **Recommended:** show them on the stylist form pre-filled from the customer's answer, editable. The stylist can correct a client who misread a question, and nothing is asked twice from scratch. - Do not simply remove them from the stylist form — an online consultation with no customer-form step would then lose them entirely.