fix(flags): close CRM/accounting feature-gating gaps from the audit

A sweep of every CRM/accounting toggle found surfaces still reachable
with their flag OFF. Adds a shared requireFeatureFlag middleware (the two
existing per-file copies predate it) and closes the gaps:

- Hours logging: only createEntry checked the flag — edit/delete/bill and
  the list/summary routes were permission-only. Gate all six
  /hour-entries routes on the hoursLogging master so a disabled feature
  can't be read, mutated, or invoiced via a direct API hit.
- Installment plans: PUT /deals/:uuid/installment-plan mutates invoices
  but wasn't bills-gated; add requireFeatureFlag('bills').
- Customer invoice PDF: /invoices/:id/pdf lacked the feature_bills check
  the list + quotes routes have. Also fixes the quotes-PDF gate, which
  read req.customer.feature_quotes (never populated → silent no-op).
- Customer contracts: /contracts + /contracts/:id/pdf were gated by
  neither the master nor a per-customer column.

Per-customer contracts override (the missing counterpart):
- Migration 131 adds customer_accounts.feature_contracts, default TRUE so
  existing customers keep their Contracts tab (preserve-visuals).
- Effective resolver now contractsMaster AND feature_contracts; admin
  detail page gains the toggle; service/validator/serializer wired.

Cleanups:
- Drop stale `taxReport` from the sidebar's Clients-reveal list (Tax moved
  to Accounting); add the missing `projects` so it mirrors the context
  derivation.
- SettingsPage tab-snap effect now depends on flags.accounting.
- Fix stale taxReport "forced off when bills off" comment (it's accounting).
This commit is contained in:
Luca
2026-06-16 13:12:26 +02:00
parent 873be910a5
commit 03fa3d8296
11 changed files with 141 additions and 17 deletions
@@ -0,0 +1,34 @@
/**
* Migration 131: per-customer `feature_contracts` override on customer_accounts.
*
* Contracts was master-only — every active customer saw the Contracts tab
* whenever the global `contracts` feature flag was on. This adds a per-customer
* toggle to match feature_calendar / feature_quotes / feature_bills /
* feature_hours_logging, so an admin can hide Contracts for an individual
* customer.
*
* PRESERVE-VISUALS: unlike the opt-in quotes/bills columns (default false),
* contracts is currently opt-OUT (everyone has it), so the column defaults
* TRUE. Adding a NOT NULL column with a default backfills existing rows to
* true on both SQLite and Postgres, so no customer loses their Contracts tab
* on upgrade. The effective resolver becomes
* `contractsMaster && truthy(feature_contracts)`.
*
* Idempotent: guarded by hasColumn.
*/
exports.up = async function (knex) {
const has = await knex.schema.hasColumn('customer_accounts', 'feature_contracts');
if (!has) {
await knex.schema.alterTable('customer_accounts', (table) => {
table.boolean('feature_contracts').notNullable().defaultTo(true);
});
}
};
exports.down = async function (knex) {
if (await knex.schema.hasColumn('customer_accounts', 'feature_contracts')) {
await knex.schema.alterTable('customer_accounts', (table) => {
table.dropColumn('feature_contracts');
});
}
};