diff --git a/pkg/api/settlement.go b/pkg/api/settlement.go index 75c36bb..1093757 100644 --- a/pkg/api/settlement.go +++ b/pkg/api/settlement.go @@ -165,10 +165,9 @@ func (a *API) handleConfirmIssue(w http.ResponseWriter, r *http.Request) { return } settled, err := a.Store.Mutate(idea.ID, true, func(i *store.Idea) error { - if !store.CanTransition(i.Status, store.StatusSettled) { - return &store.ValidationError{Msg: "cannot settle an idea in status " + i.Status} + if err := i.TransitionTo(store.StatusSettled, "settle"); err != nil { + return err } - i.Status = store.StatusSettled i.IssueURL = in.IssueURL return nil }) diff --git a/pkg/api/wave2.go b/pkg/api/wave2.go index 1bb5541..1479fee 100644 --- a/pkg/api/wave2.go +++ b/pkg/api/wave2.go @@ -177,11 +177,8 @@ func (a *API) handleOffer(w http.ResponseWriter, r *http.Request) { if o := i.OfferTo(rp.RepoID); o != nil && o.Status != store.OfferDeclined { return &store.ValidationError{Msg: "already offered to this repo"} } - if i.Status != store.StatusOffered { - if !store.CanTransition(i.Status, store.StatusOffered) { - return &store.ValidationError{Msg: "cannot offer an idea in status " + i.Status} - } - i.Status = store.StatusOffered + if err := i.TransitionTo(store.StatusOffered, "offer"); err != nil { + return err } if o := i.OfferTo(rp.RepoID); o != nil { o.Status = store.OfferPending @@ -218,11 +215,8 @@ func (a *API) offerExternal(w http.ResponseWriter, idea *store.Idea, repoID stri if o := i.OfferTo(repoID); o != nil && o.Status != store.OfferDeclined { return &store.ValidationError{Msg: "already offered to this repo"} } - if i.Status != store.StatusOffered { - if !store.CanTransition(i.Status, store.StatusOffered) { - return &store.ValidationError{Msg: "cannot offer an idea in status " + i.Status} - } - i.Status = store.StatusOffered + if err := i.TransitionTo(store.StatusOffered, "offer"); err != nil { + return err } if o := i.OfferTo(repoID); o != nil { o.Status = store.OfferPending @@ -405,9 +399,7 @@ func (a *API) handleDecide(w http.ResponseWriter, r *http.Request) { return nil // other repos are still considering it } } - if store.CanTransition(i.Status, store.StatusDeclined) { - i.Status = store.StatusDeclined - } + i.TryTransition(store.StatusDeclined) return nil }) if err != nil { @@ -437,15 +429,14 @@ func (a *API) accept(w http.ResponseWriter, r *http.Request, idea *store.Idea, r return } updated, err := a.Store.Mutate(idea.ID, true, func(i *store.Idea) error { - if !store.CanTransition(i.Status, store.StatusAccepted) { - return &store.ValidationError{Msg: "cannot accept an idea in status " + i.Status} + if err := i.TransitionTo(store.StatusAccepted, "accept"); err != nil { + return err } if o := i.OfferTo(rp.RepoID); o != nil { now := timeNow() o.Status = store.OfferAccepted o.DecidedAt = &now } - i.Status = store.StatusAccepted i.TargetRepo = rp.RepoID return nil }) @@ -489,10 +480,9 @@ func (a *API) legacySettle(w http.ResponseWriter, r *http.Request, updated *stor return } settled, err := a.Store.Mutate(updated.ID, true, func(i *store.Idea) error { - if !store.CanTransition(i.Status, store.StatusSettled) { - return &store.ValidationError{Msg: "cannot settle an idea in status " + i.Status} + if err := i.TransitionTo(store.StatusSettled, "settle"); err != nil { + return err } - i.Status = store.StatusSettled i.IssueURL = issueURL return nil }) diff --git a/pkg/store/store.go b/pkg/store/store.go index 12cf9ea..4ae3374 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -185,6 +185,34 @@ func (i *Idea) HasPassed(repoID string) bool { return false } +// TransitionTo advances the idea to status to, enforcing the lifecycle +// state machine. action names the attempted move (e.g. "offer", "accept", +// "settle") for the returned error message. A no-op (to == i.Status) always +// succeeds. Callers that previously duplicated the CanTransition-then-set +// pattern inline should call this instead so the invariant lives in one +// place. +func (i *Idea) TransitionTo(to, action string) error { + if i.Status == to { + return nil + } + if !CanTransition(i.Status, to) { + return &ValidationError{Msg: "cannot " + action + " an idea in status " + i.Status} + } + i.Status = to + return nil +} + +// TryTransition sets the idea's status to to if the lifecycle state machine +// allows it, reporting whether the transition happened. Used where a +// disallowed transition is a silent no-op rather than a hard error. +func (i *Idea) TryTransition(to string) bool { + if !CanTransition(i.Status, to) { + return false + } + i.Status = to + return true +} + // ErrNotFound is returned when an idea does not exist. var ErrNotFound = errors.New("store: idea not found")