Skip to the page
Sections

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.

Warning:

Test keys only

A sentinel sent with a live key is 422 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.

Codestatemessage_keyEntriesWhat it stands for
TEST-0-ENTEREDacceptedentered1Accepted
TEST-0-INSTANT-WINacceptedinstant_win1Accepted with an instant prizeinstant_win: TZS 2,000 airtime
TEST-0-INVALIDrejectedinvalid0Code not recognised
TEST-0-ALREADY-USEDrejectedalready_used0Code used by someone else
TEST-0-DAILY-LIMITrejecteddaily_limit0Phone reached the daily limit
TEST-0-BLOCKEDrejectedblocked0Phone blocked
TEST-0-UNKNOWNunknownchecking0Upstream timed out; stays unknown
TEST-0-SETTLESpendingchecking0Pending, then accepted after 10 seconds: use it to test pollingthen accepted / entered when polled after 10 s

TEST-0-ENTERED: Accepted

curl
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
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
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
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
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
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
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
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.

Switch to the receipt test campaign's key (local)
export UTC_API_KEY="$(grep '^UTC_TEST_KEY_TEST_RECEIPT=' .env.local | cut -d= -f2)"
Receipt codestateReview statusmessage_keyEntriesWhat it stands for
TEST0VERIFIED1acceptedverifiedentered1TRA returns one qualifying bottle
TEST0VERIFIED3acceptedverifiedentered3TRA returns three qualifying bottles
TEST0PENDINGpendingunder_reviewreceipt_received0TRA did not answer in time; the re-check job finishes it
TEST0UNREADABLEpendingunder_reviewreceipt_unreadable0QR could not be read from the photo
TEST0WRONGSTORErejecteddeclinedreceipt_wrong_store0Receipt from another merchant
TEST0OUTSIDErejecteddeclinedreceipt_outside_window0Receipt dated outside the window
TEST0NOPRODUCTrejecteddeclinedreceipt_no_qualifying_product0No participating product on the receipt
TEST0DUPLICATErejecteddeclinedreceipt_already_used0Receipt already entered
TEST0REVIEWpendingunder_reviewreceipt_received0A brand line that matches no product: held for a person

TEST0VERIFIED1: TRA returns one qualifying bottle

1. Start 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":"TEST0VERIFIED1","time":"10:15"}'

TEST0VERIFIED3: TRA returns three qualifying bottles

1. Start 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":"TEST0VERIFIED3","time":"10:15"}'

TEST0PENDING: TRA did not answer in time; the re-check job finishes it

1. Start 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":"TEST0PENDING","time":"10:15"}'

TEST0UNREADABLE: QR could not be read from the photo

1. Start 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":"TEST0UNREADABLE","time":"10:15"}'

TEST0WRONGSTORE: Receipt from another merchant

1. Start 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":"TEST0WRONGSTORE","time":"10:15"}'

TEST0OUTSIDE: Receipt dated outside the window

1. Start 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":"TEST0OUTSIDE","time":"10:15"}'

TEST0NOPRODUCT: No participating product on the receipt

1. Start 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

1. Start 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":"TEST0DUPLICATE","time":"10:15"}'

TEST0REVIEW: A brand line that matches no product: held for a person

1. Start 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":"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.

2. Keep the id
export RECEIPT_ID=<the id from the reply>
3. Submit
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.

CampaignWhat it isstatusA TEST-0-ENTERED submissionKey in ./.env.local
test-nativeCodes we mint, no receipt. The quickstart's campaign.liveaccepted, enteredUTC_TEST_KEY_TEST_NATIVE
test-receiptA code and a receipt, with Spier's stores, products and rules, and a window that never closes.live422 receipt_required without a receipt_idUTC_TEST_KEY_TEST_RECEIPT
test-seebaitCrown codes checked by Seebait. The code check answers unsupported.live500 internal_error today: Seebait submissions are not yet servedUTC_TEST_KEY_TEST_SEEBAIT
test-not-startedA window that opens in 2099.not_startedrejected, campaign_not_startedUTC_TEST_KEY_TEST_NOT_STARTED
test-endedA window that closed in 2020.endedrejected, campaign_endedUTC_TEST_KEY_TEST_ENDED
test-pausedPaused by ops.pausedrejected, campaign_pausedUTC_TEST_KEY_TEST_PAUSED

To try one, switch key and ask for the campaign, then submit:

For example, the paused campaign (local)
export UTC_API_KEY="$(grep '^UTC_TEST_KEY_TEST_PAUSED=' .env.local | cut -d= -f2)"
curl
curl -s "$UTC_API_URL/api/v1/campaign" \
  -H "Authorization: Bearer $UTC_API_KEY"
curl
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}'