MoniWave

Testing

The sandbox runs a deterministic simulator instead of real mobile-money operators. Outcomes are chosen by the MSISDN you send, so you can rehearse every path — including the ones that are hard to trigger against a real operator — before touching real money.

This is deliberately better than a real operator sandbox: no flakiness, no waiting for a test phone, and the same result every run, which makes it safe to assert on in automated tests.

Magic MSISDNs

MSISDNOutcomeReason codeUse it to test
237690000001Succeeds on the first status checkThe happy path
237690000002Pending ~10 s, then succeedsYour polling loop — the realistic case
237690000003Failspayer_declinedCustomer rejects the prompt
237690000004Failsinsufficient_fundsCustomer's wallet is empty
237690000005Never completes, eventually expiresTimeout handling and expiry
237690000006Rejected at submission (never reaches pending)invalid_subscriberImmediate rejection
anything elseSucceeds on the first status checkConvenient default

These work identically for collections, payouts, and refunds.

Numbers are stable API behaviour — they will not be renumbered. Anything not in this table succeeds immediately, so arbitrary test numbers "just work".

Before going live, prove your integration survives each of these:

ScenarioHowWhat to verify
Successful payment237690000001Order fulfils exactly once
Slow payment237690000002You wait rather than assuming failure
Declined237690000003Order not fulfilled, customer sees a useful message
No response237690000005You handle expired and release the reservation
Invalid number237690000006Fails fast, does not hang
Retry after timeoutSame idempotency key twiceExactly one transaction created
Duplicate webhookReplay the same event idOrder not fulfilled twice
Forged webhookPOST unsigned JSON to your endpointRejected with 401

The last three catch the bugs that actually cost money in production, and none of them require a real operator to test.

Local development

Running the whole platform locally needs Docker:

cd modules/backend
dotnet run --project src/MoniWave.AppHost

This starts Postgres, the Service Bus emulator, blob storage, the simulator, the APIs, and orchestration. Then run DataSeed (from the Aspire dashboard, explicit start) to create a merchant and key.

ServiceURL
Public APIhttp://localhost:6003 (health: /v1/ping)
Panel APIhttp://localhost:6001
Simulatorhttp://localhost:6002

Seeded merchant mer_local, key sk_test_LocalDevOnly000000000000000000000.

Local behaviour is identical to the hosted sandbox — same magic numbers, same statuses, same webhook format.

Testing webhooks locally

Your endpoint must be reachable from the orchestration service and publicly routable — a webhook URL on localhost or a private address is refused, both when you save it and at delivery time (endpoint requirements).

So a local receiver alone will not work. Use a tunnelling tool (cloudflared tunnel, ngrok http, or similar) and register the public URL it gives you. There is deliberately no development bypass for this: a flag that relaxed the rule locally would be one misconfiguration away from relaxing it in production, and the rule exists because the delivery worker runs inside a private network.

Verifying the signature does not need a tunnel — that logic is pure, so unit-test it against a payload and secret directly rather than against a live delivery.

Verify signatures in tests too — webhooks.md has copy-paste verification code in Node, PHP, and Python. Testing that a valid signature passes proves little; also assert that a tampered body and a wrong secret are rejected.

Sandbox limits

  • Sandbox data is entirely separate from live: transactions, merchants, and keys do not carry over.
  • Sandbox money is not real; balances and float are simulated.
  • Rate limits are looser than live but not unlimited — batch load tests should be discussed with us first.