goldnead/statamic-offers
Statamic Offers is a Laravel package that adds offer management to Statamic CMS. Editors can create and organize offers in the control panel, while developers can surface them in Antlers templates for promotions, deals, or listings.
What this release actually is. v1.18.0 is a different kind of change from the one we assessed at v1.15.0, and the two should not be conflated. The prior release was an additive money read-model (Basket::firstPaymentCent()), a presentation-layer fix with no behaviour change. This one introduces a new domain surface: the funnels map now stores offers as objects — type: offer, id = handle, number = seats accepted in the period — and the package now declares a hard dependency on statamic-brand-context ^1.17. That is a data-shape change plus a dependency change in the same release, which is materially more invasive than the last one.
Version gap. We are reviewing 1.15.0 → 1.18.0 with no recorded assessment for 1.16 or 1.17. The brand-context floor of ^1.17 strongly implies coordinated releases we have not read. Before this goes on a roadmap, someone must diff the changelog for 1.16 and 1.17; assuming 1.18.0 is the whole story is exactly how an unlisted breaking change reaches production.
Architecture fit. Coherent for a Statamic addon: the funnels map becomes a typed reference registry rather than loose identifiers, offers are addressed by handle (content-friendly, stable, human-readable), and the seat count is stored alongside the reference rather than derived elsewhere. The type discriminator is the right call — it implies further referencable types later without another schema break. Two caveats. First, this package is now explicitly a Statamic addon with a Statamic addon dependency, so it no longer sits cleanly in a plain Laravel service; any headless or API-only consumer inherits the Statamic runtime and content model. Second, number = seats accepted in the period introduces a quantity into a package we previously treated as a money formatter. Seats are billable units. That is the seam to watch.
Integration feasibility. Medium, and gated on dependency resolution rather than code. The implementation itself is likely small — read the new object shape, render it. The work is (a) resolving statamic-brand-context ^1.17 against whatever is currently pinned, (b) auditing any code that reads funnel map entries assuming the old shape, and (c) deciding whether seat counts now feed the "due today" calculation. Step (a) is the schedule risk: an addon-to-addon constraint can force a cascading upgrade of Statamic, brand context, and anything else sharing those transitive constraints, and Composer will not negotiate.
Technical risk. Medium, with the prior invariant still live and two new ones added:
firstPaymentCent() must account for number or the "due today" line will be correct for one seat and wrong for many. Nothing in the notes says the two are connected. Assume they are not connected until proven, and if they are not, that is a product decision to schedule.type. A discriminator invites new types. Anything we write that switches exhaustively on type: offer will fail closed on the next one.firstPaymentCent() / charge-path dual source of truth is untouched by this release and remains our most expensive latent defect.Key questions.
statamic-brand-context ^1.17 force a Statamic or PHP upgrade, and what else in our dependency graph shares those constraints?number seats feed pricing, entitlements, or both — and does firstPaymentCent() include seat quantity today?type values planned, and is the registry extensible by us?Stack fit. Statamic addon within a Laravel app. Two distinct surfaces now: the money read-model we scoped at 1.15 (checkout "due today") and the funnels/offers map (content-side configuration). Treat them as separate workstreams with separate owners, because they fail differently — one is a silent arithmetic drift, the other is a content-shape break. Server-rendered Statamic templates consume both; API consumers should still receive firstPaymentCent() as a precomputed integer and should receive funnel/offer data resolved server-side, never re-derived client-side.
Migration path. Do not treat this as a Composer bump. Sequence: pin 1.18.0 on a branch, let Composer resolve statamic-brand-context ^1.17, and inspect the full lockfile diff before touching application code. Diff the 1.16/1.17 changelogs. Audit every consumer of funnel map data. Only then decide whether 1.18.0 is a maintenance upgrade or a feature we adopt — they have different timelines and should be separable.
Compatibility.
number against the quantity on live subscriptions before enabling anything downstream.netCent() remains unchanged; re-run the 1.15 parity harness to confirm the money path did not move while we were upgrading around it.Sequencing.
statamic-brand-context ^1.17 on a branch; review lockfile and platform requirements; estimate cascade.{seats} × {trials, coupons, setup fees, bumps} before any UI change.Maintenance. Two invariants now, not one. The first is unchanged: firstPaymentCent() must equal the gateway capture, defended by a parity test and a PR checklist for coupons, trials, setup fees, and bumps. The second is new: the funnels map schema and the type registry must be versioned, and any consumer that switches on type must fail open with a visible error rather than a blank. Add brand context to the dependency watch list — a Renovate/Dependabot rule plus a named owner, because an addon we do not own can now break a checkout-adjacent package.
Support. The money-side benefit from 1.15 still holds: one canonical figure to quote. This release adds a support surface rather than removing one — "why does this funnel show three seats," "why did my offer disappear," "why is this brand priced differently." Those are configuration and entitlement questions, not billing disputes, and they belong to whoever owns funnel setup. Brief them, and give them a read of the funnels map in the control panel before the release lands, not after.
Scaling. The seat count is the scaling concern. number per offer per period implies a computation or a stored counter; either it is recomputed on read (potentially expensive across a long funnel list) or it is stored and can drift. Ask which, and ask what happens at period rollover — a batch job that resets counters is a new operational dependency, and a missed run means wrong entitlements for every customer simultaneously. Offer resolution through brand context may also add per-brand cache segments; confirm the Stache invalidates correctly when brand context changes.
Failure modes to watch.
type arrives in a patch release and our exhaustive switch renders nothing.Ramp-up. Roughly a day for a Laravel/Statamic engineer to implement the funnels-map rendering; the estimate is dominated by dependency resolution, which is unpredictable and not the engineer's to control. Two briefings are required before work starts: the billing owner on whether seats are billable, and the content/funnel owner on the new object shape and what existing entries need to become. Do not let the checkout team infer the seats-to-price relationship from the code, and do not let anyone treat 1.16 and 1.17 as covered by this review.
How can I help you explore Laravel packages today?