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 | 1x 18x 1x 17x 17x 17x 17x 11x 1x 10x 10x 18x 18x 10x 10x 10x 3x 10x 10x 10x 10x 10x 1x 1x 1x 12x 12x 12x 12x 12x 12x 11x 4x 8x 6x 8x 2x | // Onboarding Step Save Routes
//
// Onboarding is ONE screen, and step 1 is the only step that exists.
//
// This file used to implement all seven cases of `handleStepSave` plus a
// sequential gate that refused step N until step N-1 had been recorded. None of
// cases 2-7 ever ran: the portal has only ever posted step 1 (see
// `apps/portal/src/pages/onboarding/index.tsx`) and the e2e suite only ever
// posted step 1. They were live dead code and were deleted in the portal
// redesign. The fields they wrote are all editable from the profile screens.
//
// `pros.onboarding_step` is NO LONGER WRITTEN — not here and not by
// `completion.routes.ts`. The column deliberately still exists: seeded and
// historical rows carry values and `GET /onboarding/status` still reads it for
// its `currentStep` field. Dropping it needs a migration plus a sweep of those
// readers, which is a separate change; removing the writes first is the
// reversible half. `onboardingStatus` (not the step number) is what decides
// whether onboarding is required — see `onboardingRequired` in
// `routes/pro/profile.routes.ts`.
import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import { z } from "zod";
import type { Dal } from "../../../dal";
import { BadRequestError } from "../../../lib/errors";
import { handleError, success } from "../../../lib/response";
import { requireUser } from "../../../lib/utils";
import { requireProAccess } from "../../../middleware";
import type { Services } from "../../../services";
import { type Step1Data, step1Schema } from "./schemas";
import { flattenProForOnboarding } from "./utils";
type Env = {
Bindings: CloudflareBindings;
Variables: {
user: { id: string; name: string; email: string } | null;
session: unknown;
dal: Dal;
services: Services;
proId: string;
proRole: string;
};
};
// The only step there is.
const ONLY_STEP = 1;
// Save the one onboarding step. Exported so it can be unit-tested directly;
// the step number is still a parameter (and still validated) because the route
// shape `/step/:stepNumber` is a published contract.
export async function handleStepSave(
stepNumber: number,
body: unknown,
dal: Dal,
proId: string,
userId: string,
) {
if (stepNumber !== ONLY_STEP) {
throw new BadRequestError(`Invalid step number: ${stepNumber}`);
}
// Step 1: Simplified Business Info
const data = step1Schema.parse(body) as Step1Data;
const updateData: Record<string, unknown> = {};
updateData.businessName = data.businessName;
// Validate slug uniqueness
const existingPro = await dal.pros.findBySlug(data.slug);
if (existingPro && existingPro.id !== proId) {
throw new BadRequestError("This slug is already taken");
}
updateData.slug = data.slug;
// Empty string means "other" city — store as null
updateData.cityId = data.cityId || null;
updateData.businessTypeId = data.businessTypeId;
// Sync whatsapp to both columns
if (data.whatsapp) {
updateData.whatsapp = data.whatsapp;
updateData.whatsappBusinessNumber = data.whatsapp;
}
if (data.logoUrl !== undefined) {
updateData.logoUrl = data.logoUrl || null;
}
// Mark onboarding as started. `onboardingStep` is deliberately not written
// — see the file header.
updateData.onboardingStatus = "in_progress";
updateData.userUpdated = userId;
updateData.dateUpdated = new Date();
// Update pro (DAL takes 2 args: id, data)
const pro = await dal.pros.update(proId, updateData);
return pro;
}
const stepsRouter = new Hono<Env>();
// Save a step's data. Only step 1 exists, so anything else is a 400 at the
// edge rather than a silently-accepted no-op.
const saveStepParamsSchema = z.object({
stepNumber: z.coerce.number().int().min(ONLY_STEP).max(ONLY_STEP),
});
stepsRouter.post(
"/step/:stepNumber",
requireProAccess,
zValidator("param", saveStepParamsSchema),
async (c) => {
try {
const dal = c.get("dal");
const user = requireUser(c.get("user"));
const proId = c.get("proId");
const { stepNumber } = c.req.valid("param");
// Parse request body
const body = await c.req.json();
// Handle step save with validation
const pro = await handleStepSave(stepNumber, body, dal, proId, user.id);
return success(c, {
step: stepNumber,
saved: true,
pro: flattenProForOnboarding(pro as Record<string, unknown>),
});
} catch (err) {
// Handle Zod validation errors
if (err instanceof z.ZodError) {
return handleError(
c,
new BadRequestError(
`Validation failed: ${err.issues.map((e) => e.message).join(", ")}`,
),
);
}
return handleError(c, err);
}
},
);
export default stepsRouter;
|