Categories, Feature Flags & List Framework
This page covers three cross-cutting pieces that don't belong to accounting, payroll, or HR specifically, but that the rest of this PR depends on.
Business view
- Categories is a new, generic tagging system. Before this PR, things like expense types, income types, appointment types, and salary-adjustment types were each a separate hard-coded list stored as JSON on the company record — hard to extend and impossible to relate to each other. Categories replaces that with one flexible model that covers expenses, payments, income, inventory, appointments, salary adjustments, and (reserved for later) taxes, users, assets, patients, and rooms. Crucially, an expense category can now carry an accounting classification (which P&L bucket it rolls into) directly, instead of the accounting engine having to guess from a free-text label.
- Feature Flags control who sees the new accounting module, and can target specific companies by rule (country, tier, beta status, and so on) rather than an all-or-nothing switch — so this can be rolled out gradually.
- List & filter framework upgrades are what let the mobile app's list screens filter by the new Category system and show grouped/subtotaled views (e.g., "expenses grouped by category, with a running total per group") instead of just a flat list.
Technical view
Feature Flags
packages/clinic-mobile/src/context/featureFlagContext.js — useFeatureFlag(key, fallback) runs a tenantActiveFeatureFlags(tenantId) query, cache-first, and fails closed to fallback while loading or on error (so a flag-gated screen stays hidden, never briefly flashes visible, during the loading window).
This isn't a hardcoded switch — it's a small rule engine. FeatureFlag (packages/prisma/schema.prisma:8024-8033) has key, a global active kill switch, and positiveRules/negativeRules parsed by a real expression DSL (packages/server/src/utils/featureFlagDSL.js, built on jsep) supporting Rule(field, operator, value) combined with AND/OR/NOT against Company fields like country, tier, isBeta. evaluateFeatureFlagRules (packages/server/src/utils/featureFlagUtils.js:89) evaluates it per company. In the sandbox this documentation was captured from, FEATURE_ACCOUNTING_MODULE is seeded with positiveRules: null, which evaluates true unconditionally — explicitly commented in the seed script as sandbox-only, not part of the production rollout config.
Worth knowing: clinic-web does not use the same hook as clinic-mobile. Web imports useFeatureFlag from a separate package, @dentolize/clinic-web-canary, whose FeatureFlagsProvider runs the same query plus a live GraphQL subscription that refetches on flag change — so a flag flip can update the web UI without a reload, while mobile only re-evaluates from cache on the next hook call. Two independently-built implementations of the same concept, worth keeping in sync if one changes.
Categories
Category (schema.prisma:8982-9051) / SubCategory (:9072-9099) — type (CategoryType, :8921-8933: EXPENSE | PAYMENT | INCOME | INVENTORY | APPOINTMENT | SALARY_ADJUSTMENT | USER | ASSET | TAX | PATIENT | ROOM), systemName (a stable key for protected/system rows that the posting engine keys off of — not the display name, which clinics can freely rename), mainType (an ExpenseMainType accounting roll-up for EXPENSE categories: cost-of-revenue, operating expense, tax, or other), forceSubCat, and an optional link to an appointment procedure (for auto-invoicing).
The posting engine reads Category.systemName directly — resolveCategorySystemName (packages/server/src/accounting/posting/expense.js:151) uses it to route doctor-commission and lab-payable postings correctly, explicitly replacing an older, broken string-equality check the code comment says "never matched." This is a real fix bundled into the migration, not just a rename.
Not every CategoryType has a UI yet. Both CategoriesScreen.js (mobile) and its web equivalent (settings/Account/CompanyTypes.js) only expose six of the eleven types: Appointment, Expense, Income, Inventory, Payment, Salary Adjustment. TAX, USER, ASSET, PATIENT, and ROOM are defined in the schema and even have a partially-built form branch (isTax) sitting unreachable in the new-category form, but there's no menu entry that reaches them — reserved for a future phase.
Inventory items are the clearest example of the migration in practice: NewInventoryItemScreen.js now sources its item-type picker from GET_CATEGORIES(type: INVENTORY) instead of the old company.inventoryTypes JSON, and a new appointment-category multi-select lets an inventory item auto-consume when a matching appointment type is confirmed.
List & filter framework
AggregatedListScreen.js (packages/clinic-mobile/src/common/list/AggregatedListScreen.js) is a generic "group by" list screen — given a query name and a groupBy dimension, it renders any registered list grouped with subtotals, reusing the existing CommonList component. This PR registers 12 new list types onto it (incomes, expenses, expense payments, salary adjustments, treasuries, lab orders, quotations, transactions, appointment feedbacks, inventory items, inventory sub-items, inventory locations) — all financial or inventory-adjacent, giving the mobile app a lightweight "grouped report" view alongside the dedicated web accounting reports.
queryFilters.js and FiltersScreen.js were reworked so the "type" filter on affected list screens (created appointments, expenses, incomes, salary adjustments, inventory items/sub-items/locations/transactions) reads from the new Category system instead of the legacy per-company JSON type lists. This migration is partial: payments/expensePayments filters still read the old company.paymentTypes JSON — they were not migrated to Category in this PR, per an explicit comment left in the code acknowledging the gap.