Coupon journeys
The Store's side of a QR coupon journey (MeshWeaver.Plugins#2063). Design of record: QR Coupon Journeys. The link itself is an Essentials action link (MeshWeaver.Plugins#2060); this page is what a coupon link's Continue opens.
| Node | What it is |
|---|---|
ReviewRule |
The ActionHandlerRule claiming store.coupon-journey v1 for Review. 🚨 It ships disabled (enabled: false) with publishPermission: "Update" and no capabilities, so nothing can publish or follow a coupon journey until the owner enables it — and then only someone holding Update on Store/CouponJourney/Review may publish. The review page and its save are gated by the same live resolver (ResolveActionHandler), so a disabled rule also stops a recipient who navigates to Review?link=… directly. 🚨 Do NOT enable it before the legacy free-redemption path (CouponRedemption.CountRedemptions) reserves from the same slot ledger: until then the two ledgers can together over-issue a limited coupon. |
Review |
The review page (/Store/CouponJourney/Review?link={linkId}). |
What the review page does
It trusts nothing it was handed. As the viewer it checks that a person is signed in and may read
the link's page a/{linkId}. As the platform it reads the link's presentation and private
definition, the coupon, and any claim the viewer already holds, and decides
(CouponClaimRules.Decide), in this order: signed in; the link serves (revoked, expired, unknown and
unreadable all render the same line); the link is bound to this site; the journey reads; the coupon
can still be claimed (window, plan tier, bounded term, budget); an existing claim.
It shows the exact quote — the plan tier, the real benefit start and end dates (and that the term is reduced when the coupon's validity cuts it short), Due today: 0 — the method-storage consent when the journey requires a payment setup, and an optional renewal as not available on this site yet. Visiting, refreshing or previewing it writes nothing.
Save my claim re-decides everything as the platform, then reserves a redemption slot and creates the claim. 🚨 It grants no plan, installs no course, saves no payment method and charges nothing.
The claim and its consents
CouponClaims/{claimKey} (node type Store/CouponClaim), a platform-owned partition nobody is granted
anything on. The key is CouponJourneyRules.ClaimKey — site, recipient and coupon offer, never the
link — and the create of the node is the atomic arbiter of one claim per site, person and offer: a
second tab, a double click or a second QR link returns the claim the first one created.
The claim keeps the accepted quote and its hash, the phase (Prepared), the slot, and two separate
consent records, each { kind, textHash, textVersion, acceptedAt, acceptedBy, quoteHash }:
methodStorageConsent— present exactly when the journey requires a payment setup, over the journey's own consent text.recurringConsent— only for a selected renewal whose terms were quoted. This build quotes no renewal terms, so a selected renewal or a recurring consent is refused; the two are never merged into one record.
The redemption budget
There is no cluster-atomic counter (core: Expressing a Write → Status: a cross-hub fold can lose a
count), so the budget is reserved by creating slot nodes: CouponClaims/Slots/{CODE}/{n} for
n in 1…maxRedemptions. A listing of the existing slots only picks where to start
(CouponClaimRules.ProbeOrder); the first successful create is the reservation; all taken is
"no redemptions left". At most maxRedemptions slots can exist, and no two claims hold one. A
coupon with no limit takes no slot.
Known limits, stated rather than hidden:
- The ordinary free-redemption path still counts
redeemedon the coupon without slots; the reservation refuses once that count is exhausted, but the two paths are not yet one budget. - A slot is taken before the claim is created. A run that loses the claim create gives its slot back; a process that dies between the two leaves a slot held by no claim. Both failures can only issue FEWER redemptions, never more.
- Slots are not yet released on cancellation or expiry.
The link facts it reads
Store never compiles Essentials. The review page reads the link's presentation (linkId,
definition) and private definition (linkId, site, invocation, validFrom, expiresAt,
revokedAt, revision) as data (CouponJourneyLinkFacts) — a field-name contract with
Essentials' ActionLinkDefinition, read tolerantly: a missing field reads as "does not serve".