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

+7 -4
View File
@@ -12,7 +12,7 @@ func (r *PGRepository) ListBookingsByTenantBetween(ctx context.Context, tenantID
rows, err := r.pool.Query(ctx, `
SELECT id, tenant_id, service_id, class_session_id, staff_id, location_id,
customer_name, customer_email, customer_phone, starts_at, ends_at, status, reference,
COALESCE(manage_token, ''), COALESCE(notes, ''), created_at
COALESCE(manage_token, ''), COALESCE(notes, ''), COALESCE(intake::text, '{}'), created_at
FROM bookings
WHERE tenant_id = $1 AND starts_at < $3 AND ends_at > $2
ORDER BY starts_at ASC
@@ -41,6 +41,7 @@ func (r *PGRepository) ListBookingsByTenantBetween(ctx context.Context, tenantID
&record.Reference,
&record.ManageToken,
&record.Notes,
&record.Intake,
&record.CreatedAt,
); err != nil {
return nil, err
@@ -56,7 +57,7 @@ func (r *PGRepository) ListBookingsBySession(ctx context.Context, sessionID stri
rows, err := r.pool.Query(ctx, `
SELECT id, tenant_id, service_id, class_session_id, staff_id, location_id,
customer_name, customer_email, customer_phone, starts_at, ends_at, status, reference,
COALESCE(manage_token, ''), COALESCE(notes, ''), created_at
COALESCE(manage_token, ''), COALESCE(notes, ''), COALESCE(intake::text, '{}'), created_at
FROM bookings
WHERE class_session_id = $1
ORDER BY created_at ASC
@@ -85,6 +86,7 @@ func (r *PGRepository) ListBookingsBySession(ctx context.Context, sessionID stri
&record.Reference,
&record.ManageToken,
&record.Notes,
&record.Intake,
&record.CreatedAt,
); err != nil {
return nil, err
@@ -100,9 +102,9 @@ func (r *PGRepository) CreateBooking(ctx context.Context, params CreateBookingPa
INSERT INTO bookings (
tenant_id, service_id, class_session_id, staff_id, location_id,
booking_mode, customer_name, customer_email, customer_phone, starts_at, ends_at,
status, reference, manage_token, notes
status, reference, manage_token, notes, intake
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16::jsonb)
RETURNING id, reference, status, manage_token
`,
params.TenantID,
@@ -120,6 +122,7 @@ func (r *PGRepository) CreateBooking(ctx context.Context, params CreateBookingPa
params.Reference,
params.ManageToken,
params.Notes,
params.Intake,
).Scan(&created.ID, &created.Reference, &created.Status, &created.ManageToken)
return created, err
}