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 }:

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:

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".