iVendNextDevelopers Request a sandbox

Approvals by workflow, with a nightly export — the recipe

A price change: requested, approved, applied

A price-change request is made, approved, then applied once by an outside service, and the price row changes.

This recipe moves a request — a price change, a write-off — through Requested → Approved → Applied, with the right person at each step, and writes a file of the day's requests every night. It is for a partner or a retailer's head office. Reference implementation: samples/s11_approvals/ (a manifest, an outside service, tests).

It uses a native workflow on the partner's own record type; the workflow never submits, cancels or changes a price itself. An outside service you run makes the change, once, after the approval.

When to use it. When a change to the shop's data needs a second person's yes before it happens.

Steps

  1. Roles first. A manifest refuses a role the shop does not have. The sample names three: PP APR Requester, PP APR Approver, PP APR Applier. Use the shop's own manager roles in a real roll-out. ⚠ Never give one person both the requester's and the approver's role. The two-person rule rests on that, not on the workflow (see Know these limits).

  2. Write the manifest (samples/s11_approvals/manifest.json). It holds:

    • one record type, PP APR Price Change Request (item, price list, new price, reason), with change tracking on;
    • a numbering rule;
    • one workflow: four states (Requested, Approved, Rejected, Applied, all drafts) and four transitions, each allowed to one role. The approver approves, or rejects (from Requested, or from Approved — the way out of a request that was tampered with). The applier applies. No transition has a condition (the validator refuses code), and self-approval is off: the installer writes it off. iVendFramework's own default is on, so a workflow made by hand in Desk has it on. It guards the Approve action only.
    • the rows: the requester reads, raises and writes (Desk needs write to fill in a new request); the approver and the applier read and write (the workflow's actions save the request); the shop's System Manager reads, reports, exports and prints, and cannot change or delete the trail through this row. A state's "who may edit" is a screen rule only: over the record API anyone with write can change a request in any state, which is why the service checks the history (step 5).

    ⚠ Write every right out — all nine (read, write, create, delete, report, export, share, print, email). A permission row that leaves one out gets it ON. The manifest checker now refuses a row that leaves a right out, or writes one as anything but 0 or 1.

  3. Install it (tools/manifest/apply.py install; recipes/manifest.md). Uninstall exports the requests first. A reinstall leaves the schema exactly as the first install did.

  4. Give the service's role exactly one right outside the request type: read and write on Item Price. Write all nine rights out (read, write on, the other seven off), and give nothing on any other type. Then fence its user to the price lists it serves: a User Permission for the service's user on Price List, one per list, with Apply To All Document Types off and Applicable For Item Price. Without it the key can write every price row in the shop. Left on all document types (the form's default), the fence would also hide requests for other lists from the service and from the export, silently.

  5. Run the outside service (samples/s11_approvals/approvals.py apply) with its own key, as often as you like (every minute, or on a trigger). For each Approved request, oldest first, it:

    1. reads the change history;
    2. writes the price row only if it is not already the approved price;
    3. takes the workflow's Apply action, which moves the request to Applied.

    It can be killed at any moment and run twice. A rerun after a crash between the price and the action finds the price done and only takes the action. An Applied request has no further action. If the workflow action fails after the price was written, the report says "THE PRICE IS ALREADY WRITTEN" and the next run takes the action.

    The applier key is narrow: read and write on the request type, and write on the price rows of its own lists — nothing else. It reads a request's change history through the request (the form-load call checks the key's right on that request). So it needs no right on the shop's change-history records, which also hold other record types' old and new values. It cannot edit an item, read customers, sales or the change history, or raise a request of its own.

    It refuses, and names why: a request whose item, price list or price changed at any time after it was raised — by the approver before approving, by anyone between the approver's look and the click, by anyone after; a request approved by the person recorded as raising it; a request with ten or more recorded changes (the shop shows only the latest ten, so the whole history cannot be checked).

    ⚠ Know these limits before you ship:

    • The approval binds this service only. Any other role with write on price rows changes a price without it: the shop's own price-master roles, an AI assistant started with writes (S10), anyone in Desk. Take write on Item Price away from every role but the service's, or the approval is a suggestion.
    • A leaked service key can change any price row of its lists, or mark an Approved request Applied without writing the price. The service itself only writes the one an approved request names.
    • The shop's self-approval check covers only the Approve action. A person holding both roles who writes the state straight in is accepted by the shop (measured); the service then refuses the request, because who raised it is set once and cannot be changed (measured). Your own code that reads approvals must make the same check, so keep the rule in step 1: no one holds both roles.

    In Desk, one request taken one step at a time by each person's own key:

    requested

    A requester asks: the item, the price list, the new price, the reason.

    approved

    The approver approves.

    applied

    The service applies it, once.

    the trail

    The trail: who approved, and that the service applied it.

    the price row

    The shop's price row, now at the approved price.

  6. Schedule the export (approvals.py export --day <date> --out <folder>). It writes one CSV of the day's requests (created or last changed that day), with who made, approved and applied each. The same data always gives the same bytes. The day is the shop's day (System Settings → time zone), not your server's. Run it just after the shop's midnight for the day that ended, for example, on a server in the shop's zone:

    5 0 * * * cd /opt/partner-kit/samples/s11_approvals && BASE=https://shop.example AP_KEY=… AP_SECRET=… python3 approvals.py export --day $(TZ=Africa/Johannesburg date -d yesterday +\%F) --out /var/exports/approvals (on macOS: TZ=Africa/Johannesburg date -v-1d +\%F).

    Run it from the kit's own folder layout: approvals.py loads the kit's client from tools/feed_client, two folders up. Name the shop's zone in TZ=: the line then exports the shop's previous day, whatever your server's zone. The day runs to its very last instant, fractions included. A request changed again on a later day is no longer in the earlier day's file if the export runs late. Each row shows the request as it is now; the approver is blank for a request with ten or more changes.

Editing a request after it is approved

The shop does not stop it. The workflow's "who may edit in this state" is a screen rule, not an API one: anyone with write on the request type — the requester, the approver, the applier — can change a request in any state. An edit after Applied changes the record, not the price, and the export then shows the edited figure. So the safeguard is the service's. It reads the request's change history and will not apply a request whose item, price list or price changed after it was raised. It names the change and leaves the request Approved. The approver then Rejects it (a transition from Approved), and the requester raises a fresh one.

What is refused, and why

Technical notes

Notes moved from the steps

What was proven where

This page in the kit