Nobody has started this yet — be first.
Business impact
GuardRejectedError.Reason should hold the rejecting guard's error message (e.g. "payment: amount_minor must be positive") so that 422 responses from the HTTP API actually explain why a transition was rejected, not just that it was. Right now that reason is silently discarded, leaving callers and API consumers with a rejection and no explanation.
Problem
Before this ticket's scaffolding, a guard that rejected a transition by returning (false, someErr) had someErr propagate directly out of Fire -- bypassing GuardRejectedError entirely. Fire now consistently wraps every guard rejection into a *GuardRejectedError, which has a new Reason string field and an Unwrap() error method -- but the actual extraction, guardRejectReason(err) string in statemachine.go, is a stub that always returns "", discarding whatever message the guard's error carried.
Current behavior
GuardRejectedError.Reason is empty, even though the rejecting guard (e.g. positiveAmountGuard) returned a perfectly good error message.
Expected behavior
GuardRejectedError.Reason should hold the rejecting guard's error message when the guard returned a non-nil error, and "" when it rejected with (false, nil) and no explanation. GuardRejectedError.Error() already includes Reason in its message when non-empty -- no handler changes needed once this is fixed.
Steps to reproduce
go test ./tickets/tests/... -run TestTicket24 -v
Fire a transition guarded by positiveAmountGuard with a negative amount; the resulting *statemachine.GuardRejectedError.Reason is currently "" instead of "payment: amount_minor must be positive".
Why this matters
This is a small, focused piece of a slightly bigger change: Fire's guard-handling branch was restructured so a rejection is always a *GuardRejectedError (never a bare, unwrapped guard error), while still preserving errors.Is/errors.As access to the original error via Unwrap. guardRejectReason is the one piece of that restructuring left unfinished.
Suggested approach
Look at guardRejectReason's doc comment in pkg/statemachine/statemachine.go -- it already says what it should do.
Acceptance criteria
Verification
go test ./tickets/tests/... -run TestTicket24 -v && go test ./pkg/statemachine/... -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/guard-rejected-reasonFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/guard-rejected-reasonSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign indocker composepaystate/paystate/go.modreplace reliable => ../reliabledocker compose up --buildpaystate/context: ..docker-compose.ymlCREATED_TIMEOUTAUTHORIZED_TIMEOUTWork the tickets in tickets/docs/ (ticket-01 through ticket-25); each names one Go test in tickets/tests/:
TEST_DATABASE_URL=postgresql://... go test ./tickets/tests/... -run TestTicket01 -v # a single ticket
TEST_DATABASE_URL=postgresql://... ./tickets/run_tickets.sh # all 25, clean pass/fail summary
Tickets that don't touch Postgres (02, 03, 04, 05, 06, 14, 15, 24) run fine without TEST_DATABASE_URL set; the rest report FAIL with a "TEST_DATABASE_URL not set" message if it's missing. make test-race runs the full go test -race -p 1 ./... suite the same way.
Level 2
Implement a feature
Extend the system within its own patterns.