CALL URL RECEIPT LAB — 2026-11-06

This complete local exercise sends a fictional Dispo-shaped GET event into a
durable SQLite receipt ledger. It does not call VICIdial, place a call, connect
to a CRM, or install a production service. Python 3.8 or later on Linux/macOS
with sqlite3 is required.
The standard-library HTTP server is deliberately bound to 127.0.0.1 only.

Save receiver.py, send-example.py, selftest.py and event-fictional.json together
in a private working directory. Keep native-dispo-url.txt as the later mapping
reference. Do not use real customer data.

1. Run the network-independent self-test:
   python3 -B selftest.py
   It uses real loopback HTTP with a temporary SQLite database. Seven cases
   cover store, identical replay, wrong token, missing identity, unexpanded
   variable, changed same-key payload, and replay after a receiver restart.
   Temporary test state is removed by the self-test after the assertions.

2. Create a new persistent exercise state directory:
   python3 -B receiver.py init
   Expected: a new mode-0700 call-url-state directory, private token.txt and
   events.sqlite3. Existing state is never overwritten. The secret is not
   printed. A setup failure requires inspecting your private directory/port;
   choose a new --state directory rather than discarding an existing ledger.

3. In terminal A start the local receiver:
   python3 -B receiver.py serve
   Expected: listening on 127.0.0.1:8765. Keep this terminal open.

4. In terminal B, from the same directory:
   python3 -B send-example.py
   Expected first run: HTTP 201 {"result": "stored"}
   Run the same command again: HTTP 200 {"result": "duplicate"}
   python3 -B receiver.py inspect
   Expected: exactly one row with source TRAINING_A, call_id 1700000000.42,
   event dispo, lead_id 900001 and dispo SALE. These are invented identifiers.

5. Stop only this receiver using Ctrl+C in terminal A, start serve again,
   then repeat send-example.py in terminal B. It must still return duplicate
   and inspect must still show one row. Do not run init again to simulate a
   restart: that would create a different ledger, not test persistence.

6. The self-test also proves that changing dispo for the same source/call/event
   produces 409 conflict without overwriting the original. A corrected status
   needs a separately designed revision policy; it is not silently ignored.

The receipt key is (source, call_id, event), with a canonical payload hash.
The SQLite transaction commits before an acknowledgement is sent. This makes
receipt insertion durable across the tested process restart; it does not prove
exactly-once delivery, arbitrary power-loss recovery, or exactly-once CRM writes.
No worker or CRM side effect is hidden in this example. The ledger is the output.

OPTIONAL VICIDIAL VALIDATION — separate, not executed by this package
Only an approved single-server synthetic lab should configure the included
native-dispo-url.txt in Campaigns -> the test campaign -> Dispo Call URL.
Substitute the generated token privately. A loopback address refers to the
VICIdial webserver because the Dispo request originates there; it does not refer
to your laptop. The receiver must run on that same lab host for this exact URL.
First confirm the installed build substitutes uniqueid, lead_id and dispo for
your call mode. An empty or unexpanded value is rejected. source TRAINING_A is
a single-server fixture namespace, not a collision-proof cluster identity.

Save the old field value. Use a synthetic disposition and independently compare
the resulting receipt with the matching call/disposition record. A local
send-example result proves the receiver path only, not VICIdial triggering.
Restore the old field after the test and stop only this receiver. Keep the
private ledger for review; rotate/revoke the test token before repurposing it.

BOUNDARIES
The token is a replayable query credential. It appears in the saved native URL
and can appear in upstream URL logs even though this receiver suppresses its
own request logs. Never place CRM credentials, notes, names or phone numbers in
that URL. This HTTP loopback server is not a public webhook endpoint. A real
deployment needs a reviewed authenticated/TLS ingress, restricted access/logs,
secret rotation, backup/recovery, bounded retry/reconciliation and a durable
downstream job design. Do not expose this teaching server on 0.0.0.0.

Primary references:
https://www.vicidial.org/docs/CALL_URL_FEATURES.txt
https://docs.python.org/3/library/http.server.html
https://docs.python.org/3/library/sqlite3.html
