All files / routes/pro/onboarding completion.routes.ts

87.5% Statements 35/40
100% Branches 23/23
100% Functions 2/2
85.71% Lines 30/35

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                                              1x     1x 9x 9x 9x 9x     9x 8x 1x       7x 1x               6x 6x 6x 6x 6x   6x 2x             4x   4x 9x 3x 3x 2x       4x                 4x   4x 4x                                   4x         4x          
// Onboarding Completion Routes
import { Hono } from "hono";
import type { Dal } from "../../../dal";
import { BadRequestError, NotFoundError } from "../../../lib/errors";
import { handleError, success } from "../../../lib/response";
import { requireUser } from "../../../lib/utils";
import { requireProAccess } from "../../../middleware";
import type { Services } from "../../../services";
import { flattenProForOnboarding } from "./utils";
import { logger } from "../../../lib/logger";
 
type Env = {
	Bindings: CloudflareBindings;
	Variables: {
		user: { id: string; name: string; email: string } | null;
		session: unknown;
		dal: Dal;
		services: Services;
		proId: string;
		proRole: string;
	};
};
 
const completionRouter = new Hono<Env>();
 
// Complete onboarding
completionRouter.post("/complete", requireProAccess, async (c) => {
	try {
		const dal = c.get("dal");
		const user = requireUser(c.get("user"));
		const proId = c.get("proId");
 
		// Get current pro to check required fields
		const pro = await dal.pros.findById(proId);
		if (!pro) {
			throw new NotFoundError("Pro not found");
		}
 
		// Idempotency: if already completed, return early without re-sending emails
		if (pro.onboardingStatus === "completed") {
			return success(c, {
				completed: true,
				pro: flattenProForOnboarding(pro as Record<string, unknown>),
				message: "Onboarding already completed",
			});
		}
 
		// Validate required fields are set
		const errors: string[] = [];
		if (!pro.businessName) errors.push("Business name is required");
		if (!pro.slug) errors.push("URL slug is required");
		if (!pro.cityId) errors.push("City is required");
		if (!pro.businessTypeId) errors.push("Business type is required");
 
		if (errors.length > 0) {
			throw new BadRequestError(
				`Cannot complete onboarding: ${errors.join(", ")}`,
			);
		}
 
		// Sync whatsapp fields: ensure both columns have the number if
		// either was set during onboarding (safeguard for step 3 data)
		const whatsappSync: Record<string, unknown> = {};
		const effectiveWhatsapp =
			pro.whatsapp || pro.whatsappBusinessNumber || null;
		if (effectiveWhatsapp) {
			if (!pro.whatsapp) whatsappSync.whatsapp = effectiveWhatsapp;
			if (!pro.whatsappBusinessNumber)
				whatsappSync.whatsappBusinessNumber = effectiveWhatsapp;
		}
 
		// Mark onboarding as complete
		const updatedPro = await dal.pros.update(proId, {
			...whatsappSync,
			onboardingStatus: "completed",
			onboardingStep: 7,
			onboardingCompletedAt: new Date(),
			userUpdated: user.id,
			dateUpdated: new Date(),
		});
 
		c.executionCtx.waitUntil(
			(async () => {
				try {
					const { CommunicationGateway } = await import(
						"../../../lib/communication/gateway"
					);
					const { OnboardingCompleteHandler } = await import(
						"../../../lib/communication/handlers/onboarding-complete.handler"
					);
					const gateway = new CommunicationGateway(dal, c.env);
					const handler = new OnboardingCompleteHandler(gateway);
					await handler.handle({
						email: user.email,
						userName: user.name,
					});
				} catch (err) {
					logger.error("[ONBOARDING] Failed to send welcome email:", err);
				}
			})(),
		);
 
		return success(c, {
			completed: true,
			pro: flattenProForOnboarding(updatedPro as Record<string, unknown>),
		});
	} catch (err) {
		return handleError(c, err);
	}
});
 
export default completionRouter;