Test sentinels
Inputs that give a known outcome with a test key, so every screen can be built and tested without a real code, a real receipt, Seebait or TRA.
Test keys only
sentinel_with_live_key. A sentinel is not a real code: it contains a 0, which no minted code does.The samples use $UTC_API_URL and $UTC_API_KEY (Quickstart). The same tables drive createFakeClient in @utc/gateway-client/fake, so a frontend's own tests need no server.
Code sentinels
Send one as code on POST /api/v1/submissions, with the key of test-native. Each phone and sentinel pair behaves like a real code: submit the same pair again and you get the first submission back with replayed: true, unless it was rejected. Change the phone for a fresh one.
| Code | state | message_key | Entries | What it stands for |
|---|---|---|---|---|
TEST-0-ENTERED | accepted | entered | 1 | Accepted |
TEST-0-INSTANT-WIN | accepted | instant_win | 1 | Accepted with an instant prizeinstant_win: TZS 2,000 airtime |
TEST-0-INVALID | rejected | invalid | 0 | Code not recognised |
TEST-0-ALREADY-USED | rejected | already_used | 0 | Code used by someone else |
TEST-0-DAILY-LIMIT | rejected | daily_limit | 0 | Phone reached the daily limit |
TEST-0-BLOCKED | rejected | blocked | 0 | Phone blocked |
TEST-0-UNKNOWN | unknown | checking | 0 | Upstream timed out; stays unknown |
TEST-0-SETTLES | pending | checking | 0 | Pending, then accepted after 10 seconds: use it to test pollingthen accepted / entered when polled after 10 s |
TEST-0-ENTERED: Accepted
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-ENTERED","consent":true}'TEST-0-INSTANT-WIN: Accepted with an instant prize
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-INSTANT-WIN","consent":true}'TEST-0-INVALID: Code not recognised
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-INVALID","consent":true}'TEST-0-ALREADY-USED: Code used by someone else
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-ALREADY-USED","consent":true}'TEST-0-DAILY-LIMIT: Phone reached the daily limit
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-DAILY-LIMIT","consent":true}'TEST-0-BLOCKED: Phone blocked
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-BLOCKED","consent":true}'TEST-0-UNKNOWN: Upstream timed out; stays unknown
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-UNKNOWN","consent":true}'TEST-0-SETTLES: Pending, then accepted after 10 seconds: use it to test polling
curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-SETTLES","consent":true}'Any other code starting TEST-0- is invalid. On the code check, TEST-0-INVALID reads invalid, TEST-0-ALREADY-USED reads already_used, and every other sentinel reads valid.
Receipt sentinels
For a receipt campaign: use the key of test-receipt. Start a typed receipt whose code is the sentinel, then submit with its id. A photo whose qr_payload (or whose QR, when we decode it) holds the sentinel does the same.
export UTC_API_KEY="$(grep '^UTC_TEST_KEY_TEST_RECEIPT=' .env.local | cut -d= -f2)"| Receipt code | state | Review status | message_key | Entries | What it stands for |
|---|---|---|---|---|---|
TEST0VERIFIED1 | accepted | verified | entered | 1 | TRA returns one qualifying bottle |
TEST0VERIFIED3 | accepted | verified | entered | 3 | TRA returns three qualifying bottles |
TEST0PENDING | pending | under_review | receipt_received | 0 | TRA did not answer in time; the re-check job finishes it |
TEST0UNREADABLE | pending | under_review | receipt_unreadable | 0 | QR could not be read from the photo |
TEST0WRONGSTORE | rejected | declined | receipt_wrong_store | 0 | Receipt from another merchant |
TEST0OUTSIDE | rejected | declined | receipt_outside_window | 0 | Receipt dated outside the window |
TEST0NOPRODUCT | rejected | declined | receipt_no_qualifying_product | 0 | No participating product on the receipt |
TEST0DUPLICATE | rejected | declined | receipt_already_used | 0 | Receipt already entered |
TEST0REVIEW | pending | under_review | receipt_received | 0 | A brand line that matches no product: held for a person |
TEST0VERIFIED1: TRA returns one qualifying bottle
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0VERIFIED1","time":"10:15"}'TEST0VERIFIED3: TRA returns three qualifying bottles
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0VERIFIED3","time":"10:15"}'TEST0PENDING: TRA did not answer in time; the re-check job finishes it
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0PENDING","time":"10:15"}'TEST0UNREADABLE: QR could not be read from the photo
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0UNREADABLE","time":"10:15"}'TEST0WRONGSTORE: Receipt from another merchant
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0WRONGSTORE","time":"10:15"}'TEST0OUTSIDE: Receipt dated outside the window
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0OUTSIDE","time":"10:15"}'TEST0NOPRODUCT: No participating product on the receipt
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0NOPRODUCT","time":"10:15"}'TEST0DUPLICATE: Receipt already entered
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0DUPLICATE","time":"10:15"}'TEST0REVIEW: A brand line that matches no product: held for a person
curl -s -X POST "$UTC_API_URL/api/v1/receipts" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"typed","code":"TEST0REVIEW","time":"10:15"}'Then submit with the receipt
Copy id from the reply into RECEIPT_ID, then submit. A receipt belongs to one submission, so start a new receipt for each try, and use a new phone or the earlier pending or accepted submission is replayed.
export RECEIPT_ID=<the id from the reply>curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-ENTERED","consent":true,"receipt_id":"'"$RECEIPT_ID"'"}'A typed code that is not a sentinel is recorded as receipt_received and waits for a person: a test key never reaches TRA. A code that starts TEST0 but is not in the table is 422 validation_failed.
Seeded test campaigns
pnpm dev:api seeds these under the brand UTC Test, so window states can be tested without waiting. Each has its own test key, kept in ./.env.local; on staging, ops run pnpm seed --env staging and hand the keys out.
| Campaign | What it is | status | A TEST-0-ENTERED submission | Key in ./.env.local |
|---|---|---|---|---|
test-native | Codes we mint, no receipt. The quickstart's campaign. | live | accepted, entered | UTC_TEST_KEY_TEST_NATIVE |
test-receipt | A code and a receipt, with Spier's stores, products and rules, and a window that never closes. | live | 422 receipt_required without a receipt_id | UTC_TEST_KEY_TEST_RECEIPT |
test-seebait | Crown codes checked by Seebait. The code check answers unsupported. | live | 500 internal_error today: Seebait submissions are not yet served | UTC_TEST_KEY_TEST_SEEBAIT |
test-not-started | A window that opens in 2099. | not_started | rejected, campaign_not_started | UTC_TEST_KEY_TEST_NOT_STARTED |
test-ended | A window that closed in 2020. | ended | rejected, campaign_ended | UTC_TEST_KEY_TEST_ENDED |
test-paused | Paused by ops. | paused | rejected, campaign_paused | UTC_TEST_KEY_TEST_PAUSED |
To try one, switch key and ask for the campaign, then submit:
export UTC_API_KEY="$(grep '^UTC_TEST_KEY_TEST_PAUSED=' .env.local | cut -d= -f2)"curl -s "$UTC_API_URL/api/v1/campaign" \
-H "Authorization: Bearer $UTC_API_KEY"curl -s -X POST "$UTC_API_URL/api/v1/submissions" \
-H "Authorization: Bearer $UTC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"phone":"0712345678","code":"TEST-0-ENTERED","consent":true}'