D72 — Real per-module entitlement features: payments→features edge + the shared CoreLevelCatalog tier home (partial)
Architecture decision record, relocated verbatim from the retired single-file
docs/tenancy/decision log. Status, thematic clusters, and how to record a new ADR: the decision log index.
The first pass of the entitlements rollout (docs/tracking/feature-entitlements-rollout.md items 1+2) wires
REAL features into REAL domain flows — one of each of the five feature types — alongside the still-present
demo scaffolding (both coexist; features:sync realizes both; the demo teardown is a later pass once real
coverage of all five types + tests exist).
Declare-from-the-owning-module. Two domain modules now contribute features the same way Team declared the boolean worked example:
- Team adds
team.seats(LIMIT; NOT-FINAL placeholder caps starter 3 / growth 10 / elite 50). A seat is consumed when an invite is accepted (the member becomes active — mirrored by the deactivaterelease- reactivate re-
consume), andInviteTeamMemberCommandis gated on it so the bus refuses an invite at the cap (QUOTA_EXCEEDED) BEFORE the email is sent (consume-on-accept, gate-on-invite: the seat is the real active member; the atomic cap still blocks an accept that would overshoot).team.advanced-rolesbecomes a REAL check:Create/UpdateTenantRoleCommandgate on it (top tier only → FEATURE_UNAVAILABLE below elite).
- reactivate re-
- Payments adds
payments.monthly-orders(QUOTA/monthly),payments.order-fee(METER, unitorder), andpayments.payout-volume-fee(STEPPED, cumulative payout value) viaPaymentsFeatureCatalog(NOT-FINAL placeholder magnitudes).CreateOrderCommandgates on the quota + meters one order + the per-order fee;InitiatePayoutCommandgates on the stepped fee + advances cumulative volume by the payoutamount. Consumption is recorded INSIDE the handler's bus transaction (after the local row write), so a rollback also unwinds usage. This adds the payments→features dependency edge (the Team→Features precedent): payments depends on the entitlements engine; Features must not depend back on it (ModuleBoundaryTestpayments block dropsModules\Featuresfrom its forbidden list, mirroring the Team allowance).
Shared tier home. The commercial tiers (starter/growth/elite) moved out of the demo catalog into
Modules\Features\Support\CoreLevelCatalog, registered once from FeaturesServiceProvider — so the tiers'
metadata survives the eventual demo teardown. DemoFeatureCatalog::registerLevels() delegates to it (idempotent).
Test-tenant provisioning. Because gating/metering is fail-closed on a tenant with no level/jurisdiction,
the TenantFactory default is now a fully-provisioned elite/ZA tenant (the two columns only the Features
resolver reads); the Features-engine + tier-gating tests override explicitly. This keeps every command flow
that now touches an entitlement green without editing each test's tenant setup.
⚠️ Known production-blocking gap: that
elite/ZAfactory default MASKS a real gap — a self-registered tenant has NULLlevel/jurisdictionand no self-serve/operator path to set them (provisioning is the deferred rollout item 3), so it is hard-locked out of every gated onboarding-reachable flow.jurisdictioncannot be auto-defaulted (compliance). See the "⚠️ Known gap — production tenant provisioning" subsection indocs/tracking/feature-entitlements-rollout.md.