TenantAtlas/specs/247-plans-entitlements-billing-readiness/research.md
ahmido e222845a36
Some checks failed
Main Confidence / confidence (push) Failing after 53s
247: plans entitlements billing readiness (#287)
Automated commit and PR created by Copilot per user request.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #287
2026-04-27 17:35:04 +00:00

7.3 KiB

Research: Plans, Entitlements & Billing Readiness

Date: 2026-04-27
Branch: 247-plans-entitlements-billing-readiness

Decision 1: Persist workspace commercial truth in existing workspace_settings

  • Decision: Store the first-slice workspace commercial truth through explicit WorkspaceSetting keys in an entitlements domain, reusing SettingsRegistry, SettingsResolver, and SettingsWriter.
  • Rationale: The repo already has validated, audited workspace-scoped settings persistence and a singleton workspace settings page. Reusing that path keeps the slice narrow, keeps audit behavior consistent, and avoids inventing a billing or account persistence model.
  • Alternatives considered:
    • New plans, subscriptions, or customer_accounts tables: rejected because the spec explicitly forbids broad billing/account scope.
    • One nested JSON blob for all entitlement fields: rejected because explicit keys better fit existing page save/reset patterns, validation, and audit attribution.

Decision 2: Keep the plan-profile catalog code-owned and bounded

  • Decision: Represent plan-profile defaults as a small code-owned catalog with one code-owned default profile and bounded named profile identifiers, not as operator-editable data.
  • Rationale: The first slice needs deterministic defaults when no workspace-specific selection exists, but it does not need a management UI, a billing backoffice, or a pricing model. Code-owned defaults are the narrowest current-release truth.
  • Alternatives considered:
    • Database-backed plan catalog: rejected because there is no current product workflow for editing plans.
    • External billing/provider sync: rejected because the spec explicitly excludes payment providers and subscription lifecycle work.

Decision 3: Introduce one bounded WorkspaceEntitlementResolver

  • Decision: Add one bounded resolver that projects effective entitlement decisions from plan defaults, workspace overrides, override rationale, and current usage.
  • Rationale: Existing settings helpers resolve raw setting values but do not answer the operator question the feature actually needs: what is the effective value, where did it come from, why is it overridden, what is current usage, and may this action proceed now?
  • Alternatives considered:
    • Rebuild the logic independently on WorkspaceSettings, ManagedTenantOnboardingWizard, and each review-pack entry surface: rejected because it would immediately create wording drift and inconsistent enforcement.
    • Extend SettingsResolver to absorb entitlement-specific usage logic: rejected because that would over-specialize a generic settings utility.

Decision 4: Keep hard enforcement at the existing mutation and run-start boundaries

  • Decision: Enforce onboarding entitlement in ManagedTenantOnboardingWizard::canCompleteOnboarding() and completeOnboarding(), and enforce review-pack entitlement inside ReviewPackService::generate() and generateFromReview(), while UI surfaces render the same decision state ahead of action execution.
  • Rationale: Review-pack generation already fans out through several Filament actions, but those surfaces converge on ReviewPackService. Putting hard enforcement at the service boundary prevents bypass. Onboarding completion is already owned by the wizard page and should remain there.
  • Alternatives considered:
    • UI-only disabling on each action surface: rejected because it would not protect direct Livewire action execution.
    • A second cross-cutting action framework for entitlement checks: rejected because the slice only needs one bounded business decision path, not a new platform hook system.

Decision 5: Preserve explicit RBAC versus business-state semantics

  • Decision: Keep 404 for non-members and wrong-plane actors, keep 403 for members missing capability, and model entitlement denial as a visible business-state block for otherwise authorized actors.
  • Rationale: The repo constitution already distinguishes membership isolation from capability denial. Entitlements are neither. Treating entitlement denial as 403 or 404 would erase the operator-visible truth this slice exists to provide.
  • Alternatives considered:
    • Hide blocked actions completely: rejected because the spec requires operator-visible rationale.
    • Return 403 for entitlement denial: rejected because it conflates product policy with authorization.

Decision 6: Keep system visibility read-only on the existing workspace directory page

  • Decision: Expose the resolved plan profile, entitlement values, source, and last-changed attribution on App\Filament\System\Pages\Directory\ViewWorkspace and its existing Blade view, with no system-plane mutation control.
  • Rationale: Platform support needs visibility into current workspace commercial truth, but introducing a second mutation plane would immediately create duplicate truth and cross-plane drift.
  • Alternatives considered:
    • New system resource or admin-like settings page: rejected because the first slice is explicitly read-only on /system.
    • Linking support users back to /admin without any local visibility: rejected because it keeps support dependent on plane switching and tribal knowledge.

Decision 7: Keep review-pack shared OperationRun UX unchanged when entitled

  • Decision: Preserve existing OperationUxPresenter, OperationRunLinks, dedupe behavior, and queued background generation semantics whenever review-pack generation is entitled.
  • Rationale: The feature is about whether generation is allowed, not about rebuilding review-pack run UX. The right insertion point is before run creation, not inside the shared run lifecycle.
  • Alternatives considered:
    • Localize new review-pack blocked/queued UX per surface: rejected because the repo already centralizes the run-start UX.
    • Add a new entitlement-specific notification family: rejected because blocked attempts should stop quietly with truthful local action messaging and no new run.

Decision 8: Prove the slice with focused Sail/Pest unit and feature coverage only

  • Decision: Cover the new resolver/profile defaults with unit tests and prove settings, onboarding, review-pack gating, and system visibility with focused feature tests run through Sail.
  • Rationale: The business risk is decision correctness and action enforcement, not browser layout or broad workflow orchestration. Unit plus feature lanes are enough to prove the slice without dragging in heavy-governance or browser cost.
  • Alternatives considered:
    • Browser tests: rejected because no browser-only interaction or layout risk is introduced.
    • Heavy-governance suite expansion: rejected because the scope is bounded and local to existing surfaces.

Decision 9: Leave Filament panel registration, global search, and assets unchanged

  • Decision: Do not add panels, providers, global-search resources, or new Filament asset registrations as part of this slice.
  • Rationale: The feature is workspace-first entitlement truth inside existing admin and system surfaces. Filament infrastructure changes would widen scope without helping the first release.
  • Alternatives considered:
    • New commercial panel or system sub-panel: rejected because the slice reuses current surfaces.
    • Asset-backed custom billing UI components: rejected because native Filament components and the existing system Blade page are sufficient.