feat(intake): structured per-service intake questions

Services declare up to 8 intake fields (text/yesno, required) stored
as JSONB; shared/intake.go is the single normalization point — slug
keys, label/dup/unknown-key rules, yes/no canonicalization. Booking
create validates answers against the service declaration (missing
required -> 400), unknown keys are dropped before persist.

Answers surface on UpcomingBooking and CustomerBookingView; the
services page gains a compact editor and the public form renders
declared questions under contact details. Verified live end-to-end.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-18 18:04:32 +02:00
1 parent ad60167a31
commit 33b6e2a2a6
20 files changed
+598 -41

No files matched your search

+42
View File
@@ -2044,6 +2044,21 @@ components:
type: array
items:
$ref: "#/components/schemas/PublicStaff"
IntakeField:
type: object
required: [key, label, type]
properties:
key:
type: string
description: Slug key (lowercase alnum + underscore); JSON key in booking intake answers
label:
type: string
description: Customer-facing question text
type:
type: string
enum: [text, yesno]
required:
type: boolean
PublicService:
type: object
required: [id, name, durationMinutes, priceCents]
@@ -2057,6 +2072,10 @@ components:
type: integer
priceCents:
type: integer
intakeFields:
type: array
items:
$ref: "#/components/schemas/IntakeField"
PublicStaff:
type: object
required: [id, displayName]
@@ -2094,6 +2113,11 @@ components:
format: email
notes:
type: string
intake:
type: object
additionalProperties:
type: string
description: Structured intake answers keyed by the service's declared intakeFields keys
startsAt:
type: string
format: date-time
@@ -2190,6 +2214,11 @@ components:
type: string
notes:
type: string
intake:
type: object
additionalProperties:
type: string
description: Structured intake answers keyed by the service's declared intakeFields keys
classSessionId:
type: string
format: uuid
@@ -2233,6 +2262,10 @@ components:
type: string
notes:
type: string
intake:
type: object
additionalProperties:
type: string
waitlistPosition:
type: integer
description: 1-based queue spot for waitlisted class bookings; absent otherwise
@@ -2619,6 +2652,11 @@ components:
priceCents:
type: integer
description: Price in the tenant's currency minor units
intakeFields:
type: array
items:
$ref: "#/components/schemas/IntakeField"
description: Structured intake questions asked at booking time (max 8)
createdAt:
type: string
format: date-time
@@ -2636,6 +2674,10 @@ components:
type: integer
priceCents:
type: integer
intakeFields:
type: array
items:
$ref: "#/components/schemas/IntakeField"
StaffMember:
type: object
required: [id, tenantId, displayName, active]