Dentolize · Dynamic Clinic Seeder Walkthrough
On this pageBusiness viewTechnical view

Architecture & the hook seam

Business view

The seeder is one self-contained folder that builds a clinic in a fixed sequence of steps, from the company outward to its patients and their financial history. Its cleverest idea is a plug point — a "hook seam" — that lets the exact same files behave two ways:

  • On its own (this branch): it builds a complete clinic on the base product

schema. No accounting, no payroll ledger — just the operational data the app reads.

  • Alongside the accounting module (a separate, in-flight branch): the same

files gain a neighbor, accountingHooks.js, that switches on real general-ledger postings, HR/payroll seeding, and a reconciliation check.

The point of this design is merge safety. The shared files are copied byte-for-byte between the two branches. All the accounting-specific behavior lives in files that only exist on the accounting branch. So when accounting merges into this line later, nothing collides — the accounting behavior simply "turns on."

Technical view

One folder, one entry point

Everything lives under packages/server/src/generateServerData/dynamicSeed/. The entry point is index.js, run via yarn seed-dynamic. It orchestrates the pipeline and owns the top-level concerns: config parsing, the safety guard, idempotency, the step sequence, counter persistence, and the final summary/reconcile.

index.js         orchestrator (main)              index.js:51
config.js        params + DB target + safety      config.js:57, 105
countries.js     SA / AE / EG presets             countries.js:7
rng.js           seeded Faker toolkit             rng.js:15
catalog.js       inventory / assets / policy tiers catalog.js
provisionBase.js S-1  company + groups + catalogs provisionBase.js:45
setupBranches.js S-2  branches, treasuries, prices setupBranches.js:70
setupPeople.js   S-3  owner, doctors, staff       setupPeople.js:37
setupAssets.js   S-4  suppliers, stock, insurance setupAssets.js:14
generate.js      S-5  patients, visits, invoices  generate.js:245
money.js         money primitives (shared)        money.js
report.js        summary + credential block + cleanup report.js
hooks.js         the extension seam               hooks.js

The step-by-step behavior is documented in The seed pipeline.

The hook seam

hooks.js defines a defaults object whose methods reproduce the base (pre-accounting) behavior: legacy JSON enum-slot catalogs on the company, plain treasury writes, and no GL postings. At the bottom it tries to load an optional sibling:

// hooks.js:156-165
let impl = null
try {
  impl = require('./accountingHooks')
} catch (e) {
  const optionalMiss = e.code === 'MODULE_NOT_FOUND' &&
    String(e.message).includes('accountingHooks')
  if (!optionalMiss) throw e   // a broken accountingHooks must fail loudly
}
export const hooks = { ...defaults, ...(impl ? impl.accountingHooks : {}) }

Two important properties:

  1. Graceful absence, loud failure. A missing accountingHooks.js is the

normal generic case. But any other load error — a syntax error, or one of accountingHooks's own imports missing — re-throws, so a half-present accounting module can't silently degrade into wrong data.

  1. Merge over replace. The accounting implementation overrides only the

methods it cares about; everything else stays the shared default.

The generators never branch on "is accounting present?" — they just call hooks.postInvoiceCreated(...), hooks.injectCapital(...), etc. On this branch those are no-ops or plain operational writes; on the accounting branch they add GL legs. This is why the same generate.js produces a self-consistent clinic in both worlds.

The merge invariant

Both index.js:18-21 and hooks.js:12-13 carry the rule in a header comment: the shared files must stay byte-identical across branches, and accounting behavior may enter only through accountingHooks.js (plus its private helpers ledger.js, setupHr.js, accountingCoverage.js, reconcile.js, which don't exist here). Editing a shared file here without mirroring it there would break the no-conflict merge.

What the hooks cover

The default (base) hook surface, all in hooks.js:36-153:

HookBase behaviorAccounting behavior
companyCreateExtraslegacy JSON enum-slot catalogsCategory taxonomy columns
provisionCompanyModulesno-opCategory tree + Chart of Accounts
enableFeatureFlagsno-opturn on FEATURE_ACCOUNTING_MODULE
loadCategoryContextnull category idsreal Category rows
createSalaryProfileno-opSalary-Hub profile per user
injectCapitalincoming treasury txnDr Cash / Cr Capital
acquireAssetpaid equipment expenseFixedAsset + GL
post* (invoice/payment/expense/inventory/commission/wallet)no-opsGL legs
runHr / runCoveragereturn null (skipped)HR + payroll/claims/depreciation
reconcile{ ok: true }, "skipped"Δ0 GL reconciliation gate

The operational bookkeeping (subledger rows + balances the UI reads) always happens in money.js; only the GL leg is hook-gated. That separation is the whole reason both branches stay consistent — detailed in The money model.