Example 2: a phone retailer's mobile plans from a telecom company
Part 2 of 3 · 1c. How the data travels: loyalty and telecom plan integrations
A phone retailer sells mobile plans from a telecom company. The plans and their prices are the telecom company's, and each retailer has its own account with it, at its own prices. The cashier must see the plans for the phone being sold, the plan's price must be a line on the sale before payment, the telecom company must confirm the plan for this customer before money moves, and after payment the plan must be activated and its number written back.
Back: Example 1Next: What both outside systems share
Built on sample S52 (phone plans from a telecom company), with a stand-in telecom company that runs on your laptop.
The steps
- The cashier scans the phone and opens the plan sheet (the Plans key).
- Your service asks the telecom company's API for the plans and prices for that phone and that customer, with this retailer's own telecom account, and the till shows them. Nothing is added to the sale.
- The cashier adds the chosen plan as its own item, priced from a price list your service keeps in step with the telecom company. No till door adds a priced line by itself: the till key, the capture at the scan and the partner page carry no money, and the price door only takes money off. So the plan is an ordinary item of the retailer's, and its price comes from the retailer's own price list, written by your service from the telecom company's prices for this account.
- At Pay, the app's check confirms the plan and its price with the telecom company for that customer: the plan fits a phone on the sale, the customer may take it, its price still holds. If they no longer hold, the sale is refused in plain words, and the cashier reads why. A sale the cashier rang with no customer reaches the check with the store's walk-in customer on it (the till puts its POS Profile's default customer there); the retailer names its walk-in customers in the app's settings, and a plan for one is refused: "A phone plan needs the customer on the sale. Add them, then press Pay."
- After payment, your service activates the plan with the telecom company and writes the activation number back: to a PP TEL Activation row of the app's own, named by the sale and its line, linked to the sale and the customer. Staff find it in the PP TEL Activation list, filtered by Sale (below), and in the sale's and the customer's Connections, Phone plans, where those forms open for them.

The picture
Every hop of the telecom link
| # | From | To | Endpoint | Data that crosses | Signed with | Kept where |
|---|---|---|---|---|---|---|
| 0a | Your service | The telecom company | GET /v1/plans (each pass: a minute on hosting) |
the account's plans: code, name, price, the phones each fits | this retailer's telecom account and key | the telecom company |
| 0b | Your service | The retailer's iVendNext | GET and PUT/POST /api/resource/Item Price on the settings' price list |
each plan's price, where it differs | the app's own user's key | the retailer's price list |
| 1 | The till | The retailer's iVendNext | the till's own call when the cashier presses Plans | the sale's lines, the customer | the cashier's session | nothing |
| 2 | The retailer's iVendNext | Your service | POST https://<your service>/tel/plans (a remote till key) |
sale_ref, store, POS Profile, company, currency, the cashier's login, the customer's id, each line's item code, quantity and unit. No price |
HMAC-SHA256 with this retailer's Plans secret | nothing |
| 3 | Your service | The retailer's iVendNext | GET /api/resource/PP TEL Settings/Default |
the retailer's telecom account and key, the plans' item group, the price list | the app's own user's key | the retailer's iVendNext |
| 4 | Your service | The telecom company | POST /v1/offers |
every item code on the sale (the telecom company says which are phones), the customer's id | this retailer's telecom account and key, read in hop 3 | nothing |
| 5 | Your service | The till (through the retailer's iVendNext) | the answer to hop 2 | {"message": …}: each plan's name, price and code |
— | shown on the till; nothing on the sale |
| 6 | The till | — | the cashier adds the plan's item | the plan, priced from the price list (hop 0b) | — | the sale, as a line |
| 7 | The retailer's iVendNext | Your service | POST https://<your service>/tel/check (the check before payment, check/1) |
the priced basket: each line's item code, item group, quantity, unit, price, amount, discount and captured values; the sale's totals; the customer and customer group; the store, POS Profile, POS day and terminal; the cashier's login; the price list, coupons and the tenders offered; the currency. No card data | HMAC-SHA256 with this retailer's check secret | the retailer's iVendNext stores the answer against the basket's fingerprint (your service keeps nothing): a basket changed after it is asked again |
| 8 | Your service | The telecom company | POST /v1/offers |
every item code on the sale other than the plans (the telecom company says which are phones), the customer's id | this retailer's telecom account | nothing |
| 9 | Your service | The retailer's iVendNext | the answer to hop 7 | {"verdict": "allow"} or {"verdict": "refuse", "reason": "…"} |
— | the till: the payment sheet, or the reason |
| 10 | Your service | The retailer's iVendNext | the event feed's door, then GET /api/resource/POS Invoice/<sale> |
sale booked; the sale's lines, item groups, serial numbers, customer | the app's own user's key | your note of handled events (a cache) |
| 11 | Your service | The telecom company | POST /v1/activations with Idempotency-Key: <retailer's site>/<sale>:<line> |
the plan, the phone, its serial number (each plan its own phone's, on a line of several), the customer, the sale's name | this retailer's telecom account | the telecom company |
| 12 | Your service | The retailer's iVendNext | POST, then PUT /api/resource/PP TEL Activation/<sale>:<line> |
Pending, then Activated with the number (or Failed with the telecom company's sentence) | the app's own user's key | the retailer's iVendNext, under the sale |
The telecom account for each retailer
The retailer keeps its telecom account and key in its own iVendNext, in the app's settings record PP TEL Settings (record Default), with the item group its plans are in and the price list they are sold from. A System Manager types them in Desk. Only the System Manager and the app's own role (PP TEL Integration) read the record; a cashier never does. The key is an ordinary text field, readable whole by those two roles: the app adds no encryption of its own. Your service reads it at the moment of each call (hop 3) with the key of the retailer whose signature the call carried, at that retailer's own address.
This is the route proved on the programme's test bench (run pp-20261028T170105Z-24298, the Technical notes): the app's own user, made by the install and holding only its own role, read the account and key whole through the record API, and a cashier could not read the record. Two routes that look shorter do not work: a password field read through the record API comes back masked, and reading a password whole (get_password) is for a System Manager, which the app's own user never is. The secrets the install hands your service at connect are the install's own (the till key's, the check's and the app user's key); a retailer cannot add its telecom key to them.
Why one retailer's account is never used for another: your service never chooses an account. The signature covers the call's body; the site header only picks which chain's secret checks it, so a call that names another chain's site fails the check. The account comes from the settings record, read with the key of the retailer the call's signature matched, at that retailer's address. The activation's idempotency key starts with the retailer's site, so the same sale name at two retailers is never one activation, however a telecom company scopes its keys.
Two chains, same app, same second
| Chain A's press | Chain B's press | |
|---|---|---|
The site the call names (X-iVendNext-Site) |
chain A's | chain B's |
| The secret that signs it | chain A's Plans secret | chain B's |
| The key your service reads iVendNext with | chain A's app user's key | chain B's |
| The telecom account used | account A, from chain A's own settings | account B, from chain B's own settings |
| The address the answer goes back to | chain A's till, as the answer to chain A's call | chain B's till |
What the service holds between calls: the shops store (one row per chain: on CitiXsys App Hosting the app's own encrypted table, on your own server a file only the app's user reads) and each chain's note of handled feed events. No account, price or sale is kept: a retailer that changes its telecom account in Desk is answered with the new one at its next press. S52's tests run two retailers at the same moment, 24 calls on threads released together, and check that every answer carries only its own account's prices and that the telecom company saw each account exactly as often as its own retailer called (tests/test_service.py, TwoRetailersAtOnce).
When the telecom link fails
- A wrong or withdrawn telecom key. Plans says the telecom company is not answering, Pay refuses a sale with a plan, and after payment the activation waits (Pending) and is tried again on every pass until the retailer fixes its key in PP TEL Settings; it is never written Failed for it.
- The telecom company is slow or down. Plans says "Telecom company not answering … Nothing was added to the sale." At Pay, a sale with a plan is refused in plain words ("The telecom company is not answering, so the plan can't be confirmed. Try again, or take the plan off the sale."), inside the till's 3 seconds. A sale without a plan is never held up: the telecom company is not asked.
- Your service is down, or the till is offline. The till cannot ask the check, so it applies the check's declared down answer to every sale on that till. S52 declares allow and flag: sales go on, and every sale on that till in that time is flagged on the Sales to review report, plan or not. The activation after payment still asks the telecom company. The Plans key is dimmed offline. (A check whose down answer is refuse stops every sale on the till while it cannot be asked: do not choose it for a check that only some sales need.)
- The telecom company's price moved after the price list was written. The check refuses with the new price; the price list follows on the next pass (a minute on hosting); the cashier takes the plan off and adds it again after that. The price is the account's, the same for every customer of this retailer: the check confirms with the telecom company that this customer may take the plan, and at this price. A telecom company that prices each customer differently needs its price on the line some other way: an extension point to request.
- The telecom company answers after the sale is already paid (an activation whose answer was lost or came too late). The activation row stays Pending and the feed event stays undone; the next pass asks again with the same idempotency key, and the telecom company answers with the same number. One activation, never two.
- The activation fails after payment (the telecom company refuses it). The row says Failed with the telecom company's sentence, listed under the sale, and nobody retries it. The money is not undone by the app: the store sorts it out with the customer (a return refunds the plan's line).
- A return, or an exchange. A return is not sent to the telecom company: cancelling a plan is the telecom company's own process. A plan sold in an exchange (a return that also sells new lines) is refused at Pay: "Sell a plan on its own sale, not in an exchange."
Run the telecom example yourself
The tests start the stand-in telecom company as a web server on your laptop and call the service the way the till signs its calls:
python3 -m unittest discover -s samples/s52_phone_plans/tests -p 'test_*.py'
python3 samples/s52_phone_plans/tests/canaries.py
The stand-in telecom company on its own (port 8154, two accounts, standin/accounts.json), then the service against your own test site, one retailer (the env file's names are in the sample's README):
python3 samples/s52_phone_plans/standin/telecom_api.py
python3 samples/s52_phone_plans/service/app.py samples/s52_phone_plans/service/.env.local
For two retailers on your laptop, run the service in the app template's local mode (IVN_LOCAL=1, the template's README) and connect two local sites.