Nobody has started this yet — be first.
Business impact
POST /v1/payments has no protection against being accidentally called twice for the same logical request (a client's retry after a timeout, a double-tapped "pay" button, a load balancer replaying a request). Every call creates a brand-new payment, full stop -- exactly the kind of gap that turns a network blip into a real double-charge.
Problem
A payments.idempotency_key column (migration 000002), the Payment.IdempotencyKey field, the CreateParams.IdempotencyKey field, Store.Insert's wiring to persist it, and the HTTP handler reading the Idempotency-Key request header all already exist. Only Store.FindByIdempotencyKey -- the lookup that turns a retried request into "return what already happened" instead of "create a duplicate" -- is a panicking stub, so Service.Create panics (surfaced as 500) the moment any caller supplies an Idempotency-Key.
Current behavior
POST /v1/payments with an Idempotency-Key header returns 500, with a panic logged: Store.FindByIdempotencyKey not implemented.
Expected behavior
POST /v1/payments with an Idempotency-Key header should be safe to retry: calling it twice with the same key (and, in this implementation, any body) returns the SAME payment both times, never creating a second one.
Steps to reproduce
go run ./cmd/paystate
curl -s -X POST localhost:8080/v1/payments
-H 'Idempotency-Key: abc-123'
-d '{"amount_minor": 1000, "currency": "INR"}'
Why this matters
This mirrors the exact problem reliable/pkg/idempotency (a sibling project in this repo) solves -- "the same request, sent twice, must produce the same result, not double the effect." Here it's scoped down to a single lookup-before-insert, backed by the partial unique index payments_idempotency_key_idx (WHERE idempotency_key IS NOT NULL) as a last-resort guarantee even if two requests with the same key somehow raced past the lookup at the same instant.
Suggested approach
Look at Store.Get right above Store.FindByIdempotencyKey in internal/payment/store.go for the query/scan pattern -- you need the same thing with WHERE idempotency_key = $1 instead of WHERE id = $1, returning ErrNotFound on no match (exactly like Get does), which is what Service.Create already checks for via errors.Is(err, ErrNotFound) to decide "no existing payment, proceed to create" vs. "found one, return it."
Acceptance criteria
Verification
TEST_DATABASE_URL=postgresql://... go test ./tickets/tests/... -run TestTicket23 -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/idempotency-keyFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/idempotency-keySubmit 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.