Example 1: a chain's own loyalty system
Part 1 of 3 · 1c. How the data travels: loyalty and telecom plan integrations
A retail chain runs its own loyalty system, outside iVendNext: its members, their points and today's offers live there. On every sale the till should show the customer's points and offer, the member's reward should enter the sale before payment, and after payment the chain's loyalty system should hear what was earned.
Back: the overviewNext: Example 2
Built on sample S22 (customer-owned loyalty). On its own, S22's service is the loyalty engine: its tiers, cash back and ledger are kept as records in the chain's iVendNext, and it calls nothing outside. Its opt-in adapter (service/outside.py) adds the outside hop: when the chain's settings record names its loyalty system's address, the Member key also asks that system for the customer's points and offer, and each sale's earning is posted to it after payment. With the address empty, S22 behaves exactly as shipped. The tier the till prices is S22's own: its daily job moves each member to the customer group the year's spend reaches. The adapter shows the chain's own points and offer and keeps the chain's system told; it does not move a customer's group to a tier the chain's system decides (that would be one more call, from your service to the chain's iVendNext, on S22's pattern). So in this example the reward that reaches the sale is S22's own: the tier's discount (a promotion the till prices) and Member cash spent from S22's ledger. The chain's points and today's offer are shown to the cashier, not applied. To let the chain's own system decide the reward, compose the same doors: your service moves the customer's group to the tier the chain's system names (so the till's promotion prices it), and the wallet's what may this member spend asks the chain's system for its balance. Both are calls you add on S22's pattern; no new door is needed.
The steps
- A sale is rung up. The cashier puts the customer on the sale.
- The reward enters the sale before payment, priced by the till itself. The member's tier is a customer group, and each tier's discount is an ordinary promotion for that group, which the till prices as the lines go on, online or offline. The customer sees the discount on the sale. No call is made for it.
- The cashier presses Member. The till asks your service, which asks the chain's loyalty system for the customer's points and today's offer, and the till shows them in a message: the tier, the cash back, what reaches the next tier, the customer's points and the offer. The message adds nothing to the sale.
- At Pay, the member may spend the cash back (Member cash): the till's payments app asks your service, through the wallet door, what this member may spend on this sale, and takes it as a payment.
- After payment, the event feed tells your service. It writes the cash back earned to the ledger (a record in the chain's iVendNext, named by its unique key) and posts the earning to the chain's loyalty system, once. A return or a cancellation later takes back its share, in the ledger and in the chain's loyalty system alike.
The picture
Every hop of the loyalty link
| # | From | To | Endpoint | Data that crosses | Signed with | Kept where |
|---|---|---|---|---|---|---|
| 1 | Your service | The chain's iVendNext | POST /api/method/ivendnext_mpos.api.feed.events (the event feed's door, pulled every 30 seconds or each minute) |
the chain's new customers, sales and returns, as events | the app's own user's key (Authorization: token …) |
your note of handled events (a cache); the records stay in the chain's iVendNext |
| 2 | The till | The chain's iVendNext | the till's own call when the cashier presses Member | the sale's lines, the customer | the cashier's session | nothing |
| 3 | The chain's iVendNext | Your service | POST https://<your service>/lyl/member (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, no payment |
X-iVendNext-Signature: HMAC-SHA256 with this chain's key secret, made at install |
nothing |
| 4 | Your service | The chain's iVendNext | GET /api/resource/PP LYL Member/<customer>, the ledger and plans lists, GET /api/resource/PP LYL Settings/Default |
the member, the balance, the plans, the chain's loyalty system's address and key | the app's own user's key | the chain's iVendNext (its own records) |
| 5 | Your service | The chain's loyalty system | POST <its address>/v1/members/lookup (the stand-in's names; a real system has its own) |
the customer's id | this chain's own loyalty key, read in hop 4 | the loyalty system |
| 6 | Your service | The till (through the chain's iVendNext) | the answer to hop 3 | {"message": {"title", "body", "level"}}: the tier, the cash back, the customer's points, the offer |
— (the answer to a signed call) | shown on the till; nothing on the sale |
| 7 | The chain's iVendNext | Your service | POST https://<your service>/paygw/1/preview and /authorise (the wallet door, Member cash) |
the member, the amount due, the payment's reference | the wallet's own secret | a Redeem ledger row in the chain's iVendNext |
| 8 | Your service | The chain's iVendNext | hop 1's feed brings sale booked; GET /api/resource/POS Invoice/<sale>, then POST /api/resource/PP LYL Ledger |
the sale's totals and lines; the Earn row, named POS Invoice:<sale>:earn |
the app's own user's key | the chain's iVendNext (the ledger) |
| 9 | Your service | The chain's loyalty system | POST <its address>/v1/earn with Idempotency-Key: POS Invoice:<sale>:earn; a return or a cancellation posts what it takes back, under its own row's key (…:<return>:return, …:<sale>:cancel) |
the customer, the sale, its value, the cash back earned (negative for a return or a cancellation), the currency | this chain's own loyalty key | the loyalty system |
The member's discount (step 2) crosses no hop: it is a promotion in the chain's iVendNext, priced by the till.
The loyalty account for each chain
The chain keeps its loyalty system's address and key in its own iVendNext, in the app's settings record (PP LYL Settings, record Default: Your own loyalty system's address and key). Only the System Manager and the app's own role read that record; a cashier never does. Your service reads it at the moment of each call, with the key of the chain whose signature the call carried, at that chain's own address.
Why one chain's account is never used for another: your service never chooses an account. The call names its chain and is signed with that chain's secret; the chain's record in your shops store gives that chain's address and key; that key can only read that chain's settings, which hold that chain's loyalty account. There is no path from a call of chain A to chain B's account.
Two chains, same app, same second
Two cashiers at two chains press Member in the same second. They are two separate calls, answered by two copies of your service at once (a hosted function runs up to 10 copies). What differs per call:
| 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 Member key secret | chain B's |
| The key your service reads iVendNext with | chain A's app user's key | chain B's |
| The loyalty account used | chain A's (its own settings) | chain B's (its 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: nothing about a sale or a member. It keeps the shops store (one row per chain: its address, the secrets its install handed over, its app user's key) and each chain's note of handled feed events, in a folder per chain. Everything else is read again at each call. S22's own tests hold this with two chains sharing a customer's name (One service for many retailers).
When the loyalty link fails
- The loyalty system is slow or down. The Member key answers anyway (S22 waits at most 0.8 seconds for it, inside the till's 3), with the tier and cash back from the chain's iVendNext and "The loyalty system is not answering." Nothing on the sale changes: the discount was the till's. After payment, the post of the earning fails, so the feed event stays undone and is tried again on the next pass, with the same idempotency key.
- The till is offline. The member's discount is still priced (the promotion is on the till). The Member key and Member cash are dimmed. The sale syncs when the till is back, and the feed then brings it to your service like any other.
- The loyalty system answers after the sale is already paid. Nothing waits for it: the sale was never held for it. The earning reaches it on a later pass; the idempotency key makes a late or repeated post count once.
- Posting the earning fails after payment. The Earn row is already in the chain's iVendNext (the ledger is the record). If the loyalty system did not answer, the post is tried again on the next pass, with the same key. If it refused for good (it does not know the member, or the key was withdrawn: a refusal other than too many requests), the refusal is written to your service's log by its status code and the event is done; it is not retried every pass. Fix the cause, then post the sale again from the ledger.
Run the loyalty example yourself
The tests run the chain's loyalty system as a stand-in web server on your laptop:
python3 -m unittest discover -s samples/s22_customer_loyalty/tests -p 'test_*.py'
tests/test_adapter.py runs the chain's loyalty system as a stand-in web server on your laptop (tests/standin_loyalty.py) and drives the Member key and the earning through it: switched off, nothing is called; switched on, the customer's points and offer appear and each sale is posted once; with the loyalty system down, the key says so and the feed retries. The canaries break each rule and expect a test to fail:
python3 samples/s22_customer_loyalty/tests/canaries.py test_adapter
To watch the hops against your own test site: start the stand-in (port 8156, key chain-loyalty-key), put http://127.0.0.1:8156 and that key in PP LYL Settings (Your own loyalty system's address and key), and run S22's service in local mode (IVN_LOCAL=1: plain http is accepted only to this machine, and only then), as its README says:
python3 samples/s22_customer_loyalty/tests/standin_loyalty.py
IVN_LOCAL=1 python3 samples/s22_customer_loyalty/service/app.py samples/s22_customer_loyalty/service/.env.local