Skip to content

Releases: glueful/subscriptions

v2.3.0

Choose a tag to compare

@MichaelSowah MichaelSowah released this 18 Aug 01:46
a845c07

[2.3.0] - 2026-08-17

Added

  • Declares the Glueful schema manifest (migration descriptors, requires.extensions, structural
    verifier); requires framework >=1.79.0 for schema-on-enable participation. Migrations are now
    registered by the manifest, not by provider boot.

v2.2.0 — Checkout reservation and purchasability

Choose a tag to compare

@MichaelSowah MichaelSowah released this 04 Aug 17:55
9ac6e9b

Host-integration seams for self-serve provider checkout, building on 2.1's administrative seams. SubscriptionService::reserveCheckoutFor() creates idempotent, non-entitling, origination-bound reservations before provider I/O, with guarded replacement and exact-origination release. A new non_renewing status projects stop-renewal cancellations while preserving entitlements exactly until the paid period's end, with a never-resurrect guard for terminally canceled rows. The projector gains an additive outcome-returning entry point with deterministic rejection codes (including origination_mismatch), and the strict Payvia bridge writes durable accepted/rejected acknowledgements to payvia ^2.5's checkout-origination ledger after receipt commit. Plans gain per-gateway provider_identifiers (migration 008) — the sole checkout-purchasability authority, read through the typed PlanPurchasability::forGateway() projection; the legacy scalar provider_price_id remains correlation-compatibility only, and existing plans require explicit operator configuration to become purchasable. On PATCH, provider_identifiers is a full-map replacement. All additive; existing 2.0/2.1 callers unchanged.

v2.1.0 - Host integration seams

Choose a tag to compare

@MichaelSowah MichaelSowah released this 03 Aug 05:38
fcf8504

Four additive seams for host platforms embedding the subscriptions engine. SubscriptionService::currentForTenants() gives platform-authority callers a bounded, single-query bulk projection of tenant subscriptions. OverrideRepository::listForSubject() exposes full override rows including expiry and reason metadata for administrative UIs. SubscriptionSubjectDataPurger::countSubjectRows() provides non-mutating per-table counts that share the purge predicates exactly, for purge prepare/verify flows. SubscriptionSchemaReadiness::isReady() is the extension-owned schema readiness authority — a never-fatal probe of the complete 2.x shape for host degraded modes. No behavior changes for existing callers. Also amends the v2 spec's resolver wording: binding a host resolver enables memberships only when it positively resolves and validates user subjects.

v2.0.0 - Subject model: workspace subscriptions + user memberships

Choose a tag to compare

@MichaelSowah MichaelSowah released this 03 Aug 03:41
5637fdb

Subscriptions 2.0 generalizes the workspace-only 1.x lifecycle engine into two coexisting, never-crossing products on one payment/lifecycle engine: workspace subscriptions (tenant billing, unchanged for existing callers) and user memberships (a user's paid relationship to a single workspace — paywalls, member benefits, recurring content access).

⚠️ Upgrading from 1.x — the bridge is mandatory

A populated install cannot migrate directly from 1.3.1. Migration 006 refuses to run without the 1.4.0 preparation marker:

  1. Put subscription writes into a maintenance window.
  2. Install 1.4.0 and run php glueful subscriptions:prepare-v2 (idempotent).
  3. Install 2.0.0 and run php glueful migrate:run.

Fresh installs: subscriptions:plans:import-config is now a required install step — with an empty subscription_plans table every entitlement resolves empty (a loud default_plan_unresolvable diagnostic tells you exactly this).

Breaking changes

  • Subject-model schema (migration 006). Every domain row carries the (tenant_uuid, subject_type, subject_uuid) triple; plan_uuid becomes a NOT NULL reference; uniqueness is now one subscription per subject. A new subscription_provider_event_receipts table is the first claim authority for inbound provider events.
  • Direct subscription_overrides inserts must supply subject_type/subject_uuid — a 1.x-shaped insert silently matches nothing (a hand-written deny override would stop denying). Use the new supported writer: OverrideRepository::upsertForSubject() / deleteForSubject().
  • DB-authoritative catalog. Config plans are seeds only — no runtime overlay; new config plans require subscriptions:plans:import-config. Plan keys are immutable and scoped by (audience, owner_tenant_uuid); subscriptions reference immutable plan_uuids.
  • Projector rejection semantics. Deterministic rejections (missing_subject, invalid_subject, plan_scope_mismatch, subject_mismatch) commit an auditable receipt; UnmappedProviderSubscriptionException is retryable — webhook endpoints MUST let it surface as a retry-inducing response. Provider payloads are sanitized (closed allowlist) in both receipts and events, including historical rows during 006.
  • Stricter read-path validation. current('') and the …For() methods throw invalid subject on empty/incoherent identities where 1.x returned null.

New

  • Subject-aware core (currentFor/startFor/changePlanFor/cancelFor/reconcileFor(Subject …)) with the full 1.x tenant API preserved as a supported facade — existing integrations keep working.
  • Memberships are enabled by binding a SubjectResolverInterface — no new config keys. Includes MemberEntitlementResolver + require_member_entitlement middleware (fail-closed; rate tiers remain tenant-only).
  • Owner-fenced, receipts-first provider projection with deterministic race handling (same-plan idempotent, different-plan conflict) proven against real PostgreSQL savepoints (env-gated PostgresSavepointTest, SUBSCRIPTIONS_TEST_PG_*).
  • Strict payvia delivery lane (requires glueful/payvia >= 2.4): subscription events ride payvia's strict tagged lane with at-least-once delivery and genuine retry on the checkout race. With payvia ≤ 2.3 delivery degrades to the fault-isolated bus — unmapped events are then permanently lost, not retried (upgrade payvia to get the guarantee).
  • Tenant lifecycle mechanics: tenant-table registration, system-context discipline, and a host-neutral SubscriptionSubjectDataPurger for user- and workspace-scoped purges.
  • Console subject options (--subject-type/--subject-uuid) across subscriptions:show/set-plan/reconcile; scoped plans:* commands (--audience/--owner).

Compatibility

  • PHP ^8.3 · glueful/framework ^1.57 · payvia and tenancy remain optional (suggest); payvia ≥ 2.4 required only for the strict delivery guarantee.
  • The 1.x tenant service facade is not deprecated — it is the supported API for workspace billing.

Two notes before you publish: the body assumes payvia 2.4.0 is released first (the strict-lane bullet references it, and this repo's require-dev resolves ^2.4 from Packagist) — publish that release before or together with this one. And make sure the pushed v2.0.0 tag points at 26f6c52 (the local tag moved when we dropped the CI workflow; if you pushed an earlier tag, force-push the tag or re-tag remotely first).

v1.3.1

Choose a tag to compare

@MichaelSowah MichaelSowah released this 16 Jun 07:08
9fe378f

1.3.1 -- 2026-06-16

Fixed

  • Register migration paths during provider boot so migrate:run sees the
    subscriptions schema through the same CLI lifecycle used by other extension
    migrations.

v1.3.0

Choose a tag to compare

@MichaelSowah MichaelSowah released this 15 Jun 01:51
84728fc

1.3.0 -- 2026-06-14

Changed

  • Migrated OpenAPI documentation to the framework 1.57.0 reflect generator. Route
    documentation (summaries, query parameters, request-body fields and response codes)
    is now expressed as typed #[ApiOperation], #[QueryParam] and #[ApiResponse]
    attributes on the controller methods; the now-inert route-file docblocks were removed.
    Docs-only — no runtime behaviour changes.
  • Raised the minimum framework requirement to ^1.57.0.

v1.2.0

Choose a tag to compare

@MichaelSowah MichaelSowah released this 13 Jun 16:46
ff3a6ce

1.2.0 -- 2026-06-13

Added

  • Provider-agnostic subscription event projection. A new
    SubscriptionEventProjectorInterface + ProviderSubscriptionEvent DTO own all
    projection rules (claim-first idempotency, tenant relink, the status state
    machine, period/grace), and a ProviderStatePullerInterface drives reconcile.
    Third-party payment providers can now project subscription state and reconcile
    drift by mapping their events into the DTO and (optionally) binding their own
    puller -- with no payvia present and no subscriptions internals touched. See
    docs/BRING_YOUR_OWN_PROVIDER.md.

Fixed

  • Harden boot/registration against partial failures. Each independent
    registration step (migrations, command discovery, route loading, and the
    optional payvia event listener) is now wrapped in its own try/catch that logs
    a [Subscriptions] ... message and re-throws outside production -- so a single
    failing step degrades gracefully in production instead of aborting app boot,
    while still failing fast during development. The existing registerMeta guard
    is unchanged.
  • Cap plan description at 255 characters in the payload validator (matching the
    subscription_plans.description VARCHAR(255) column) on both the create and
    patch paths. An over-long description now raises a clean validation error (HTTP
    422) instead of a confusing 500 (strict MySQL) or silent truncation. Only
    description is capped; other nullableString fields are unaffected.
  • Fail closed when an entitlement value has an unrecognized type. Plan values are
    validated (bool | int >= 0 | null) but override values are JSON-decoded
    and unvalidated, so a malformed value could reach the checker. allows() now
    denies (and limit() returns 0) for any non-bool/non-numeric/non-null type --
    previously the JSON string "false" (bool)-coerced to a grant and an
    unrecognized type read as an unlimited (null) limit. The intentional
    null = unlimited/allow semantics are unchanged.
  • Derive the entitlement cache key from the resolved content rather than from
    updated_at timestamps. The key now folds in a stable hash of the
    resolved-plan inputs (status, plan_key, grace_ends_at) and of the active
    override map, so a status/plan downgrade or an override edit invalidates the
    cache immediately even when a writer fails to bump updated_at -- closing a
    window (up to the cache TTL) in which a downgraded tenant kept elevated
    entitlements. The active override map is now read once and reused for both the
    key and the merge (no extra query).
  • Only relink unlinked tenant subscriptions on provider subscription.created
    events. The provider-echoed metadata.tenant_uuid is now treated as a recovery
    HINT used solely to attach an as-yet-unlinked subscription -- never to move an
    existing link. A subscription.created naming a tenant whose row is already
    linked to a different provider subscription is logged as an anomaly
    (subscriptions.relink_conflict_skipped, no payload) and no-ops instead of
    silently stealing the link.
  • Do not resurrect a canceled subscription on a late or replayed
    subscription.created event. When the stored status is already canceled, the
    event is still recorded/claimed but no status change is projected, so a
    delayed creation event can no longer flip a terminal subscription back to
    active.

Changed

  • Read plan write payloads (store/update) from the JSON body and POST form
    only -- query-string params are no longer merged into the body. Validation
    already gated every field, so this is a logging-hygiene change: plan fields
    (e.g. entitlements/status) passed via the query string for a write are no
    longer copied into the request body (and thus access logs). importConfig
    reads its force/status query params explicitly and is unaffected. Callers
    that relied on passing plan write fields via the query string must move them
    into the request body.
  • Payvia is now a thin optional integration rather than the projection owner. The
    former PaymentProviderEventListener (which both adapted payvia's event shape
    AND owned the projection rules) is replaced by a generic
    SubscriptionEventProjector plus a thin PayviaSubscriptionEventBridge
    (event adapter) and PayviaProviderStatePuller (reconcile adapter), each
    registered only when payvia is installed. All subscriptions-owned payvia_*
    storage/options/config/CLI vocabulary is renamed to provider_*:
    provider_gateway, provider_customer_id, provider_subscription_id,
    provider_price_id (generalized from the 12-char payvia_priced_plan_uuid to
    a VARCHAR(191) provider price/plan identifier), and
    provider_logical_event_key; the event source value payvia_event becomes
    provider_event, the unique index uniq_subscriptions_payvia_sub becomes
    uniq_subscriptions_provider_sub, and the plan CLI flag --payvia-priced-plan
    becomes --provider-price-id. No backward-compat shim or dual-read (the
    extension is pre-release); the columns are renamed directly in the base
    migrations.
  • An unknown provider event type that maps to an existing subscription is now
    recorded (the idempotency claim is taken) with no projected status change,
    instead of being dropped before the claim. This keeps every delivered event
    auditable and idempotent even when its type is not in the handled set.

v1.1.1

Choose a tag to compare

@MichaelSowah MichaelSowah released this 11 Jun 17:48
de7dabe

1.1.1 -- 2026-06-11

Fixed

  • Load plan catalog, entitlement resolver, and subscription service factories
    through the framework extension service DSL so the provider boots through the
    real DefaultServicesLoader in production. (Also keeps the provider compatible
    with the framework 1.55.0 load-time non-instantiable-binding guard.)
  • Return denied entitlement and plan-management permission checks through the
    framework Response error envelope instead of raw/manual JSON responses.

Changed

  • Require glueful/framework ^1.55.0 (was ^1.54.0) as the security-hardened
    baseline. The entitlement seam and container-precedence fix this extension
    relies on shipped in 1.54.0; 1.55.0 adds the security/correctness hardening
    pass (permission-attribute enforcement, signed-URL fail-closed, fail-loud
    extension loading, etc.).

1.1.0 - Managed subscription plan catalog

Choose a tag to compare

@MichaelSowah MichaelSowah released this 11 Jun 00:45
ec727b2

1.1.0 -- 2026-06-10

Added

  • Managed subscription plan catalog with DB-backed plans, config fallback, HTTP
    management API, and CLI commands.

v1.0.0 - Initial release

Choose a tag to compare

@MichaelSowah MichaelSowah released this 10 Jun 22:39
914e3b5

1.0.0 -- 2026-06-10

Initial release.

Added

  • Entitlement checker over the core seam: DefaultEntitlementChecker
    implements framework-core Glueful\Entitlements\Contracts\EntitlementCheckerInterface
    and is bound over core's allow-all NullEntitlementChecker default (relies on
    the framework container-precedence fix; requires glueful/framework ^1.54.0).
    S3 value semantics: absent key denies; false/0 deny; true/explicit
    null allow unlimited; positive int is the limit; non-positive ints deny
    with limit() === 0 (allows() and limit() always agree).
  • Schema (3 tables at DEPENDENT priority): subscriptions (one current
    subscription per tenant, unique tenant_uuid, nullable payvia_* link
    columns, unique (payvia_gateway, payvia_subscription_id)),
    subscription_overrides (per-tenant entitlement overrides with optional
    expiry, unique (tenant_uuid, entitlement)), and subscription_events
    (audit log with DB-enforced per-gateway logical-event-key dedupe via unique
    (payvia_gateway, payvia_logical_event_key) -- multiple all-NULL rows allowed
    for manual/reconcile events).
  • Config plan catalog (config/subscriptions.php): default_plan, plans
    with entitlement maps, optional payvia_priced_plan links, grace_days,
    resolver cache settings, permissive_middleware, rate_tiers, and the
    opt-in reconcile scheduler flag.
  • Status-gated resolution with cache: EffectivePlanResolver (lapsed /
    incomplete / paused / expired-grace tenants downgrade to default_plan;
    past_due keeps paid access only while grace_ends_at is in the future;
    trials resolve plan-as-trialed; paused is accepted from payvia's
    provider-status vocabulary and treated as not entitled to paid features)
    • EntitlementResolver (catalog + overrides merge) with a
      naturally-keyed cache (tenant + catalog fingerprint + row timestamps -- any
      change invalidates by key). CacheStore is optional; zero-infra installs
      resolve uncached.
  • RequireEntitlement route middleware, fail-closed 403 with an
    entitlement error code; permissive_middleware opt-in allows requests with
    no tenant context. Registered under the require_entitlement alias
    (middleware-string form require_entitlement:<entitlement>).
    The #[RequireEntitlement] route attribute is NOT shipped in v1 -- the
    framework has no generic attribute->middleware bridge for extension
    attributes (B1); the attribute form is deferred until a sanctioned hook
    exists.
  • EntitlementTierResolver rate-limit bridge over the framework's default
    TierResolver (tier-flag mapping: boolean rate.tier.{tier} entitlements
    pick the bucket, TierManager config owns the numbers). Inert without
    tenancy; lookup failures degrade to the default resolver.
  • SubscriptionService lifecycle: current / start / changePlan /
    cancel (at period end via metadata flag, or immediate) / reconcile --
    works fully with NO payvia installed (free/trial/comp). Every transition
    appends a subscription_events row.
  • Conditional payvia listener (S7): when payvia's PaymentProviderEvent
    exists, PaymentProviderEventListener self-registers (lazy @serviceId)
    and projects normalized provider events onto subscription state --
    claim-first in ONE transaction (the event-row insert is the atomic gate), so
    duplicate/concurrent deliveries never re-project and past_due grace is set
    exactly once. A swallowed duplicate claim emits a debug-level log line
    (logger resolved defensively -- never a hard dependency) so a misclassified
    integrity error stays observable. Unmapped provider subscriptions no-op;
    subscription.created can recover the tenant link from provider metadata
    tenant_uuid.
  • Reconcile (soft payvia seam): pulls authoritative state through payvia's
    GatewaySubscriptionService::reconcile() only when the class exists
    (injectable puller seam for tests); applies status/period drift and appends a
    reconciled event with a NULL logical key. Drifting into past_due grants
    the same dunning grace as the event path (grace_ends_at = now + grace_days;
    an already-past_due row is never re-extended), and settling to active
    clears grace.
  • CLI: subscriptions:reconcile [--tenant=], subscriptions:show --tenant=, subscriptions:set-plan --tenant= --plan= (validates the plan
    against the catalog).

Tooling

  • PHPStan at level 6 via a committed phpstan.neon (composer analyze is
    config-driven); all array docblocks carry value types. PHPUnit suite and
    PSR-12 (phpcs) gates ship green.

Guarantees

  • Soft dependencies only: no payvia and no tenancy class is referenced without
    a class_exists guard; the package installs, boots, and passes its suite
    with neither package present.
  • tenant_uuid is an opaque external id (no FK) -- works with any tenant
    source, not just glueful/tenancy.
  • Entitlement checks are stateless reads (allow/deny + optional numeric
    limit); usage metering / quota consumption is a non-goal for v1 (roadmap:
    v1.1+).