Onboarding step 3 shell (#92)
CI / ci (push) Successful in 2m19s

Co-authored-by: dev_agent <dev_agent@cubecraftlabs.com>
This commit was merged in pull request #92.
This commit is contained in:
2026-10-06 17:57:25 -04:00
committed by approver_agent
parent 0a5db6a9e6
commit 6a3f1f3936
10 changed files with 503 additions and 12 deletions
+11
View File
@@ -68,6 +68,11 @@ type SettingsPatch struct {
SuggestionsTime *string
ChatAutosendDow *int
ChatAutosendTime *string
// OnboardingCompleted, when true, records that the household finished
// onboarding: onboarding_completed_at becomes the time of the update,
// unless it is already set, when the first time is kept. Onboarding is
// never undone, so false is invalid.
OnboardingCompleted *bool
}
// Problem is one invalid field of a SettingsPatch, named by its JSON key.
@@ -208,6 +213,9 @@ func (s *SettingsService) Update(ctx context.Context, patch SettingsPatch, updat
setString(&p.SuggestionsTime, patch.SuggestionsTime)
setDow(&p.ChatAutosendDow, patch.ChatAutosendDow)
setString(&p.ChatAutosendTime, patch.ChatAutosendTime)
if patch.OnboardingCompleted != nil && !p.OnboardingCompletedAt.Valid {
p.OnboardingCompletedAt = sql.NullString{String: p.UpdatedAt, Valid: true}
}
row, err = q.UpsertHouseholdSettings(ctx, p)
if err != nil {
return fmt.Errorf("household: update settings: %w", err)
@@ -291,6 +299,9 @@ func (p SettingsPatch) validate() error {
fail(d.field, fmt.Sprintf("invalid day of the week %d (want 0-6, Sunday = 0)", *d.value))
}
}
if p.OnboardingCompleted != nil && !*p.OnboardingCompleted {
fail("onboarding_completed", "onboarding cannot be undone (want true)")
}
if len(problems) > 0 {
return &ValidationError{Problems: problems}
}
+34
View File
@@ -217,6 +217,39 @@ func TestUpdateAppliesPatchAndRecordsUpdatedBy(t *testing.T) {
}
}
func TestUpdateCompletesOnboardingOnce(t *testing.T) {
t.Parallel()
ctx := context.Background()
svc := seededService(t)
got, err := svc.Update(ctx, SettingsPatch{OnboardingCompleted: ptr(true)}, "sam")
if err != nil {
t.Fatal(err)
}
if got.OnboardingCompletedAt == nil || *got.OnboardingCompletedAt != got.UpdatedAt {
t.Fatalf("onboarding_completed_at = %v, want the update time %s", got.OnboardingCompletedAt, got.UpdatedAt)
}
first := *got.OnboardingCompletedAt
// Completing again keeps the first time.
got, err = svc.Update(ctx, SettingsPatch{OnboardingCompleted: ptr(true)}, "alex")
if err != nil {
t.Fatal(err)
}
if got.OnboardingCompletedAt == nil || *got.OnboardingCompletedAt != first {
t.Errorf("second completion: onboarding_completed_at = %v, want %s", got.OnboardingCompletedAt, first)
}
// Other changes leave it alone.
got, err = svc.Update(ctx, SettingsPatch{DinnerTime: ptr("18:00")}, "sam")
if err != nil {
t.Fatal(err)
}
if got.OnboardingCompletedAt == nil || *got.OnboardingCompletedAt != first {
t.Errorf("after another change: onboarding_completed_at = %v, want %s", got.OnboardingCompletedAt, first)
}
}
func TestUpdateRequiresMember(t *testing.T) {
t.Parallel()
ctx := context.Background()
@@ -259,6 +292,7 @@ func TestUpdateValidation(t *testing.T) {
{"dow 7", SettingsPatch{PlanDraftDow: ptr(7)}, []string{"plan_draft_dow"}},
{"dow -1", SettingsPatch{SuggestionsDow: ptr(-1)}, []string{"suggestions_dow"}},
{"dow 100", SettingsPatch{ChatAutosendDow: ptr(100)}, []string{"chat_autosend_dow"}},
{"onboarding undone", SettingsPatch{OnboardingCompleted: ptr(false)}, []string{"onboarding_completed"}},
{
"every problem is reported",
SettingsPatch{Timezone: ptr("Nowhere"), DinnerTime: ptr("5pm"), PlanDraftDow: ptr(9)},