mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
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:
1 parent
ad60167a31
commit
33b6e2a2a6
20 files changed
+598
-41
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
"bookra/apps/backend/internal/db"
|
||||
"bookra/apps/backend/internal/domain"
|
||||
"bookra/apps/backend/internal/notifications"
|
||||
"bookra/apps/backend/internal/shared"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
@@ -200,6 +201,16 @@ func (s *Service) DeleteLocation(ctx context.Context, principal domain.Principal
|
||||
// SERVICE CATALOG MANAGEMENT
|
||||
// ============================================
|
||||
|
||||
// normalizeIntakeFields validates declared intake fields via the shared
|
||||
// normalizer and wraps failures in ErrInvalidInput for the API layer.
|
||||
func normalizeIntakeFields(fields []domain.IntakeField) (string, error) {
|
||||
raw, err := shared.NormalizeIntakeFields(fields)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %s", ErrInvalidInput, err.Error())
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func serviceToDomain(rec db.ServiceRecord) domain.Service {
|
||||
return domain.Service{
|
||||
ID: rec.ID,
|
||||
@@ -209,6 +220,7 @@ func serviceToDomain(rec db.ServiceRecord) domain.Service {
|
||||
BufferBeforeMinutes: rec.BufferBeforeMinutes,
|
||||
BufferAfterMinutes: rec.BufferAfterMinutes,
|
||||
PriceCents: rec.PriceCents,
|
||||
IntakeFields: shared.ParseIntakeFields(rec.IntakeFields),
|
||||
CreatedAt: rec.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -260,6 +272,10 @@ func (s *Service) CreateService(ctx context.Context, principal domain.Principal,
|
||||
if err := validateServiceValues(name, req.DurationMinutes, req.BufferBeforeMinutes, req.BufferAfterMinutes, req.PriceCents); err != nil {
|
||||
return domain.Service{}, err
|
||||
}
|
||||
intakeJSON, err := normalizeIntakeFields(req.IntakeFields)
|
||||
if err != nil {
|
||||
return domain.Service{}, err
|
||||
}
|
||||
|
||||
rec, err := s.repo.CreateService(ctx, db.CreateServiceParams{
|
||||
TenantID: membership.Tenant.ID,
|
||||
@@ -268,6 +284,7 @@ func (s *Service) CreateService(ctx context.Context, principal domain.Principal,
|
||||
BufferBeforeMinutes: req.BufferBeforeMinutes,
|
||||
BufferAfterMinutes: req.BufferAfterMinutes,
|
||||
PriceCents: req.PriceCents,
|
||||
IntakeFields: intakeJSON,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Service{}, err
|
||||
@@ -303,6 +320,13 @@ func (s *Service) UpdateService(ctx context.Context, principal domain.Principal,
|
||||
if req.PriceCents != nil {
|
||||
params.PriceCents = req.PriceCents
|
||||
}
|
||||
if req.IntakeFields != nil {
|
||||
intakeJSON, err := normalizeIntakeFields(*req.IntakeFields)
|
||||
if err != nil {
|
||||
return domain.Service{}, err
|
||||
}
|
||||
params.IntakeFields = &intakeJSON
|
||||
}
|
||||
|
||||
// Validate against the merged result so partial updates cannot produce an
|
||||
// invalid record (e.g. setting duration to 0 while keeping the name).
|
||||
|
||||
Reference in new issue
Block a user