Solavel Solavel Docs

Plan & Feature Debugging

docs/admin-support/plan-feature-debugging.md

Audience: support engineers, admins Difficulty: admin only

What this covers

When a user says "I can't see X in the menu" or "I get 'Plan does not include feature' but I'm on Premium" — this page walks the chain from their plan through the entitlement snapshot, the org-level toggle, the route middleware, the policy, and the UI conditional. By the end you know which link in the chain broke.


The chain

client_subscription.plan_id  →  plan.feature_overrides
                              →  EntitlementResolver merges with feature catalog
                              →  feature_flags table (per-org override)
                              →  EnsureFinanceFeatureEnabled (route middleware)
                              →  Policy / @can / controller authorize()
                              →  Blade @feature / config('finance_features') in view

A user can be blocked at any of those seven steps. Walk it in this order — each step is cheaper to check than the one after.


Step 1 — Plan

/admin/clients > [client] > Subscription

  • Confirm the active subscription is what the user thinks. The subscription has status='active' and billing_cycle_ends_at in the future.
  • Confirm the plan is the right one (Free / Professional / Premium / Enterprise; or a bundle like Starter / Business / Enterprise).
  • For bundles, confirm the constituent project plans (finance + inventory) are at the right tier each.

If wrong here. Update the subscription. Have the user sign out and back in to refresh the entitlement snapshot.


Step 2 — Plan feature overrides

/admin/projects > [project] > Plans > [tier] > Feature Overrides

  • Each tier carries a map of feature_key => value.
  • For boolean features the value is true/false.
  • For limit features the value is a number or null (= unlimited).

Reference: the seeder is database/seeders/ProjectPlanSeeder.php — read tierBlueprints() and financeTrackerFeatureOverrides() for the generated defaults.

If wrong here. Edit the override and save. The seeder writes through plan_features and the resolver picks it up on next session.


Step 3 — Central feature catalog

config/features.catalog.php (parent app, 108 entries) is the authoritative list of feature keys + types + defaults. The seeder database/seeders/PlanFeatureSeeder.php reflects this catalog into the plan_features table.

A feature missing from this catalog will not be persisted.

Audit-flagged: 7 keys are referenced as feature: middleware in finance routes but are missing from the central catalog and the finance local config:

finance.retainers
projects.ai_assistant
projects.core
projects.members
projects.tasks
projects.time_approval
projects.time_tracking

If a user's complaint involves any of these keys, the real fix is to add them to config/features.catalog.php and finance_features.php.


Step 4 — Org-level toggle

Inside the tenant DB, the feature_flags table holds per-org overrides. Owner / Manager can edit at Settings > Features in the finance app.

A row keyed (organization_id, key) with enabled=false overrides the plan's true. So even with a Premium plan, an org can have a feature turned off.

How to check.

-- run in the tenant DB
SELECT key, enabled, value
FROM feature_flags
WHERE organization_id = ?
  AND key = 'tracker.fixed_assets';

If wrong here. Owner toggles in Settings > Features, or support clears the row.


Step 5 — Route middleware

The feature: route middleware (EnsureFinanceFeatureEnabled) reads:

  1. The org's feature_flags row, if any.
  2. The plan's resolved value (from the entitlements snapshot).
  3. The default in config/finance_features.php.

Whichever is most specific wins. If all three are missing, the middleware's behaviour on missing config keys was not fully audited — it likely soft-fails (treats as enabled or as disabled). For the 7 undeclared keys above, this means the route may be either always allowed or always blocked.

How to check. Reproduce the user's request, watch storage/logs/laravel.log for "Feature not enabled" — the middleware logs a line.


Step 6 — Policy / authorize() / perm:

Even if the feature gate passes, the user might not have the permission for the action. The two are independent.

  • feature: controls visibility of the area at all.
  • perm: (or a policy) controls whether the user in that role can perform the specific action.

Example. A Free-plan org has tracker.purchase_orders=false. The PO menu is hidden. Even an Owner cannot see it — feature gates beat role permissions.

Conversely: a Premium org has tracker.purchase_orders=true, but a Member without purchase_orders.create cannot create a PO. The PO list is visible; the New Purchase Order button is hidden.

Reference. reference/permission-matrix.


Step 7 — UI conditional

Even if the route is reachable and the user has permission, the UI might not render the menu item. Two patterns:

  • @feature('tracker.foo') Blade directive — gates a sidebar entry.
  • config('finance_features.features.foo.default') — used by the V2 menu (config/finance_menu_v2.php) to decide whether to show a panel.

The V2 menu file is the source of truth for what shows in the sidebar. Solabooks V2 sidebar is enabled when FINANCE_UI_V2=true.


Common debug paths

"I'm on Premium but I see 'Plan does not include feature' for Sales Orders."

Walk:

  1. Subscription → confirm Premium tier on finance project.
  2. Plan feature overrides → find tracker.sales_orders — should be true. If false, fix it.
  3. Note: Sales Orders also requires sales.use_sales_orders=true (a second flag). Both must be on.
  4. Org feature_flags → confirm sales.use_sales_orders is not overridden to false for the org.

"My new accountant can see invoices but the New button is missing."

This is a permission issue, not a feature issue. The Member finance role has sales.invoices.view but not sales.invoices.create by default. Add the permission per-user, or upgrade the user to Manager.

"I upgraded an hour ago and still see the Free limits."

Entitlements are snapshotted into the user's session at sign-in. Have the user sign out and back in. If the issue persists, run the parent command subscriptions:resync --client=<id>.

"Feature works for me but not for my colleague — same role, same org."

Check:

  • Is the colleague's session stale? (Last seen >24h ago in User Monitor — sign out / in.)
  • Does the colleague have a per-user permission override?
  • Are they in a different organization than they think they are? Check the org switcher.

Audit-flagged mismatches

From the Phase 1 audit:

  • 116 keys in solavel-finance/config/finance_features.php are not in the central catalog. Most are tracker-defaults that should live as plan overrides. Net effect: a feature defined here but absent from the central catalog is not seeded into plan_features — the resolver falls back to this file's default.
  • 33 keys in the central catalog are not in finance's features map. Limit-type entitlements (e.g. max_price_lists, receipt_ocr_scans) are usually fine here — they're enforced at controller level. Boolean ones could break if the route reads config('finance_features.features.<key>.default').
  • 130 of the 261 finance permissions are not enforced by any perm: middleware token at the route layer. They may be checked in policies, @can directives, or controllers — but if a user is granted one and finds it has no effect, this is why.

Related

Source: docs/admin-support/plan-feature-debugging.md ← All documentation