Nobody has started this yet — be first.
Business impact
The settled -> charged_back transition currently has no guard at all -- any settled payment can be charged back at any time, no matter how long ago it settled. Real card/UPI networks enforce something similar -- disputes have a filing deadline -- and without this guard the payment machine allows a materially unrealistic sequence that a real integration partner would reject.
Problem
chargebackWithinDisputeWindowGuard and the exported payment.ChargebackDisputeWindow variable now exist and are wired into the transition table, but the guard itself is a stub that unconditionally rejects every chargeback, regardless of SettledAt.
Current behavior
A chargeback fired shortly after settlement (well within the dispute window) is rejected anyway, because the guard stub rejects unconditionally.
Expected behavior
A chargeback should only be allowed within ChargebackDisputeWindow (default 180 days) of SettledAt.
Steps to reproduce
go test ./tickets/tests/... -run TestTicket15 -v
A chargeback fired 1 hour after settlement (well within the 4320h default window) is currently rejected with "payment: dispute window has expired, chargeback no longer allowed" -- it should succeed.
Why this matters
Guard in pkg/statemachine is exactly the mechanism for this: a function that inspects the data being transitioned (*TxnCtx, here) and decides whether the transition is currently allowed. This is a good, realistic example of a time-based guard, as opposed to the existing positiveAmountGuard's pure data-validity check.
Suggested approach
Look at positiveAmountGuard right above chargebackWithinDisputeWindowGuard in internal/payment/machine.go for the guard function shape (func(context.Context, any) (bool, error), type-asserting data.(*TxnCtx)). You'll need c.Payment.SettledAt (a *time.Time -- it could theoretically be nil if the guard is somehow invoked before settlement, though the transition table means it never should be in practice; decide what your guard does in that case) and time.Since(*c.Payment.SettledAt) compared against the package-level ChargebackDisputeWindow variable declared right above it.
Acceptance criteria
Verification
go test ./tickets/tests/... -run TestTicket15 -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/chargeback-dispute-windowFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/chargeback-dispute-windowSubmit 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.