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 | 1x 1x 1x 1x 1x 3x 3x 1x 9x 9x 9x 9x 1x 8x 8x 2x 6x 6x 1x 5x 1x 4x 4x 1x 3x 3x 2x 2x 2x 7x | // Public Free-Consultation Booking Routes (API Key Protected)
//
// Bookings from the standalone /book-consultation page on the marketplace.
// Like /landing-leads these carry no proId — the support team is emailed the
// details and coordinates the call manually. A homeowner may have only one
// active consultation at a time (PRD P0).
import { parseIndianPhone } from "@interioring/utils/validation/phone";
import { Hono } from "hono";
import type { Dal } from "../../dal";
import { fireConsultationConfirmation } from "../../lib/communication/whatsapp-consultation";
import { ConflictError, ValidationError } from "../../lib/errors";
import { fireInternalNotification } from "../../lib/internal-notifications";
import { handleError, success } from "../../lib/response";
import { rateLimit } from "../../middleware/rate-limit.middleware";
type Env = {
Bindings: CloudflareBindings;
Variables: {
dal: Dal;
};
};
const DETAILS_MAX_LENGTH = 1000;
const SOURCE_PAGE_MAX_LENGTH = 500;
// PRD copy shown when a phone already has an active consultation request.
const ACTIVE_CONSULTATION_MESSAGE =
"You already have a consultation request in progress. Our team will be in touch shortly.";
const consultations = new Hono<Env>();
// Same budget as the inquiry/landing-lead limiter: 5 per 10 minutes per IP.
const consultationRateLimit = rateLimit({
windowMs: 10 * 60 * 1000,
max: 5,
devMax: 1000,
message: "Too many submissions. Please try again in 10 minutes.",
});
// Mint a human-facing booking reference, e.g. CONS-A1B2C3D4.
function generateReference(): string {
const slug = crypto.randomUUID().replace(/-/g, "").slice(0, 8).toUpperCase();
return `CONS-${slug}`;
}
// Submit a free-consultation booking
consultations.post("/", consultationRateLimit, async (c) => {
try {
const dal = c.get("dal");
const body = await c.req.json();
// Phone — required, must be a valid Indian mobile (WhatsApp/SMS delivery).
if (!body.phone) {
throw new ValidationError("phone is required");
}
const parsed = parseIndianPhone(String(body.phone));
if (!parsed || parsed.type !== "mobile") {
throw new ValidationError(
"phone must be a valid 10-digit Indian mobile number",
);
}
const phone = parsed.e164;
// Details — optional free text.
if (body.details && String(body.details).length > DETAILS_MAX_LENGTH) {
throw new ValidationError(
`details must be ${DETAILS_MAX_LENGTH} characters or fewer`,
);
}
if (
body.sourcePage &&
String(body.sourcePage).length > SOURCE_PAGE_MAX_LENGTH
) {
throw new ValidationError(
`sourcePage must be ${SOURCE_PAGE_MAX_LENGTH} characters or fewer`,
);
}
// One active consultation per phone (PRD P0).
const activeCount =
await dal.consultationBookings.countActiveByPhone(phone);
if (activeCount >= 1) {
throw new ConflictError(ACTIVE_CONSULTATION_MESSAGE);
}
const reference = generateReference();
const booking = await dal.consultationBookings.create({
reference,
phone,
details: body.details ? String(body.details).trim() : null,
// Trusted homeowner id from the marketplace SSR proxy (already
// session-validated there), when the booker is logged in.
homeownerId:
body.homeownerId && typeof body.homeownerId === "string"
? body.homeownerId
: null,
sourcePage: body.sourcePage ? String(body.sourcePage) : null,
});
// Best-effort email to support — never blocks or fails the response.
fireInternalNotification(c, dal, {
event: "consultation_lead_internal",
bookingId: booking.id,
reference: booking.reference,
phone: booking.phone,
details: booking.details,
sourcePage: booking.sourcePage,
});
// Best-effort WhatsApp confirmation to the homeowner.
fireConsultationConfirmation(c, dal, booking.phone, booking.reference);
return success(c, { id: booking.id, reference: booking.reference }, 201);
} catch (error) {
return handleError(c, error);
}
});
export default consultations;
|