Repository navigation
v1.0.0 - Initial release
1.0.0 -- 2026-06-10
Initial release.
Added
- Entitlement checker over the core seam:
DefaultEntitlementChecker
implements framework-coreGlueful\Entitlements\Contracts\EntitlementCheckerInterface
and is bound over core's allow-allNullEntitlementCheckerdefault (relies on
the framework container-precedence fix; requiresglueful/framework ^1.54.0).
S3 value semantics: absent key denies;false/0deny;true/explicit
nullallow unlimited; positive int is the limit; non-positive ints deny
withlimit() === 0(allows()andlimit()always agree). - Schema (3 tables at DEPENDENT priority):
subscriptions(one current
subscription per tenant, uniquetenant_uuid, nullablepayvia_*link
columns, unique(payvia_gateway, payvia_subscription_id)),
subscription_overrides(per-tenant entitlement overrides with optional
expiry, unique(tenant_uuid, entitlement)), andsubscription_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, optionalpayvia_priced_planlinks,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 todefault_plan;
past_duekeeps paid access only whilegrace_ends_atis in the future;
trials resolve plan-as-trialed;pausedis 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).CacheStoreis optional; zero-infra installs
resolve uncached.
RequireEntitlementroute middleware, fail-closed 403 with an
entitlementerror code;permissive_middlewareopt-in allows requests with
no tenant context. Registered under therequire_entitlementalias
(middleware-string formrequire_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.EntitlementTierResolverrate-limit bridge over the framework's default
TierResolver(tier-flag mapping: booleanrate.tier.{tier}entitlements
pick the bucket,TierManagerconfig owns the numbers). Inert without
tenancy; lookup failures degrade to the default resolver.SubscriptionServicelifecycle: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 asubscription_eventsrow.- Conditional payvia listener (S7): when payvia's
PaymentProviderEvent
exists,PaymentProviderEventListenerself-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 andpast_duegrace 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.createdcan 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
reconciledevent with a NULL logical key. Drifting intopast_duegrants
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 toactive
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 analyzeis
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
aclass_existsguard; the package installs, boots, and passes its suite
with neither package present. tenant_uuidis an opaque external id (no FK) -- works with any tenant
source, not justglueful/tenancy.- Entitlement checks are stateless reads (allow/deny + optional numeric
limit); usage metering / quota consumption is a non-goal for v1 (roadmap:
v1.1+).