Decision record + build spec — 2026-09-08 (Matthew + Hermes), following Matthew's Gemini research on how NHS dental money actually flows (research captures: fp17_claims and fp17_claims_submissions). Corrects the accounting shape of fp17-course-billing-redesign — its D4 posting treatment and its deferred "Phase 3" are the subjects here. Decisions F1–F5 + F3b all confirmed by Matthew 2026-09-08; raised to OpenCode via the from-hermes inbox note (top, 2026-09-08).
The redesign (P1+P2, commit 9d8bded4) correctly moved the charge point to course completion and
correctly removed the invented two-invoice split (NHS_REIMBURSABLE £76.60 "NHS invoice" + PATIENT_COPAY
£18.40 co-pay). But it left the NHS money-flow modelled like private business:
- The NHS patient charge (statutory band charge) is raised as a local Invoice (type TRANSACTION) and
mirrored to ERPNext as a Sales Invoice on the patient customer, posting to the SAME default income
account as private fees (config.default_income_account, e.g. "4010 - Fee Income"). In reality that
money is NOT practice income: the practice collects it at reception and the NHSBSA claws the same
amount back out of the next monthly contract payment. Booking it as fee income overstates earnings
unless the contract side offsets it.
- There is no representation of the NHS side of a completed course (the contract income the claim
earns via UDAs / the monthly schedule). Removing the old split removed the second leg; the
placeholder fee basis (total_nhs_fee = statutory band charge) is not reconciled to anything.
- No contract/schedule machinery exists (the old "Phase 3", now understood as core NHS accounting):
fixed monthly contract payments, UDA credit on acceptance, DCS127/129 month-end schedule
reconciliation (the three-way match), clawback awareness, associate (performer) pay.
- Some labels/fields conflate the two legs: FP17Claim.fee_amount (claims-list "Fee" column) is the
sum of patient-charge invoices (Leg A money), while uda.delivered_fee_for_claims presents itself as
"delivered NHS fee value" but mostly sums that same patient-charge money.
Private treatment is FINE as-is: standard invoicing is exactly right (research says so explicitly).
The private ERPNext leg shipped in P2 (PRIVATE invoices → patient Sales Invoice → Payment Entry) needs
NO change.
====================================================================
====================================================================
For every completed NHS course of treatment the practice's books hold TWO distinct components:
Leg A — Patient charge: the statutory band charge (£76.60 band 2, 2026/27) or £0-exempt, collected
locally at reception. It is NOT incremental income — the NHS claws it back from the monthly
contract payment. If a paying patient never settles, the practice still loses the money
(clawback happens regardless) → uncollected-charge debt monitor needed.
Leg B — NHS income: earned through claim acceptance → UDA credit against the contract. Cash arrives
as the fixed monthly contract payment (1/12 ACV), net of clawbacks + superannuation, per the
month-end NHSBSA schedule (DCS127/129). There is NO invoice to the NHS and no per-claim NHS
payment. The claim's fee basis (total_nhs_fee) is the per-claim record of Leg B that the
schedule reconciles against.
Financial events, in order:
- Course close → FP17 DRAFT + patient charge due (Leg A receivable, or £0-exempt) + claim fee basis
recorded (Leg B).
- Patient pays at reception → receipt; Leg A settles; cash recorded.
- Claim validated → transmitted → ACCEPTED → UDA credit (Leg B accrues at claim level).
- Month-end: schedule (DCS127/129-style statement) entered → three-way match (UDAs credited vs the
app's claims; clawback vs Leg A collected) → claims marked RECONCILED; ERPNext journal:
Dr Bank (net paid) + Dr NHS-charge clearing (clawback) / Cr NHS Contract Income (gross).
- Associate pay: per reconciled UDA × performer rate − lab share − superannuation.
====================================================================
====================================================================
- dental/services/billing.py close_course: NHS → FP17Claim created with patient_charge, total_nhs_fee
(= _nhs_fee_for_course → get_band_charge(band, on_date=charge_date) — a documented PLACEHOLDER fee
basis), exemption fields; non-exempt → PENDING DentalTransaction + local Invoice
(InvoiceType.TRANSACTION, label "Patient Charge"). Private → price-list PENDING tx per completed
appointment + Invoice (InvoiceType.PRIVATE, "Private Treatment").
- dental/invoice_models.py Invoice: InvoiceType enum still carries retired NHS_REIMBURSABLE /
PATIENT_COPAY / SEATS alongside TRANSACTION / PRIVATE. Rows carry erpnext_invoice_name /
erpnext_synced / status DRAFT→SENT→PAID, fp17_claim FK.
- integrations/tasks.py sync_invoice_to_erpnext: one ERPNext Sales Invoice per local Invoice on the
patient customer; item income_account = config.default_income_account (single account for BOTH NHS
patient charges and private); retired types skipped. sync_payment_to_erpnext: Payment Entry on the
selected invoice at invoice.total_amount (417 fix), tx.erpnext_synced + invoice PAID on success.
- dental/uda.py: delivered_udas_for_claims / delivered_fee_for_claims (sums claim.fee_amount, falling
back total_nhs_fee) — feeds the UDA dashboard £ card shipped with the P3 £ piece.
- FP17Claim fields: appointment, course, practice, performer, nhs_band, patient_charge, total_nhs_fee,
fee_amount, patient_exemption, exemption_evidence_seen, status lifecycle DRAFT→…→ACCEPTED/REJECTED.
====================================================================
====================================================================
ERPNext-side: NHS patient-charge Sales Invoices (local type TRANSACTION, claim-linked) post to a
dedicated NHS patient-charge account — a clearing/liability account (money collected on the NHS's
behalf, clawed back later) — NOT to config.default_income_account. Private invoices keep fee income.
- sync_invoice_to_erpnext selects the income account BY invoice type: claim-linked TRANSACTION →
nhs_patient_charge_account (new config value); PRIVATE → default_income_account (unchanged).
- Local Invoice TRANSACTION label "Patient Charge" → "NHS Patient Charge" so ledgers read correctly
(badge text only; code value unchanged).
- New ERPNext accounts (idempotent provisioning — management command + provision/bootstrap guard,
OpenCode; executed on the live dental site during re-prep): "NHS Patient Charges (Clearing)" —
liability-side, e.g. 2xxx; plus "NHS Contract Income" (M4 needs it) e.g. 4xxx.
- DentalERPNextConfig gains nhs_patient_charge_account (+ nhs_contract_income_account, + bank account
reference for M4 journal) with defaults and a clear error if unset at sync time.
- Test: claim-linked TRANSACTION syncs to the NHS-charge account; PRIVATE to fee income; both submit;
Payment Entry path unregressed.
The old "split" returns as dual-leg DATA on every completed NHS claim, reconciled at schedule level —
NOT as two Sales Invoices and NOT as any invoice to an "NHS customer":
- Leg A = patient_charge (+ tx/invoice) — exists.
- Leg B = total_nhs_fee — CONFIRMED: stays the date-anchored statutory band charge
(get_band_charge(band, on_date=charge_date)), explicitly the fee basis, READ-ONLY after transmit,
carried onto the accepted/schedule records.
- Claims list "Fee" column + FP17Claim.fee_amount semantics: fee_amount currently = sum of patient
invoices (Leg A). Re-define so the two legs are never conflated — column renamed "Patient charge"
(shows due/collected Leg A); NHS fee basis (Leg B) shown on claim detail. fee_amount either removed
or re-purposed to Leg A only (OpenCode call; no UI may label it "NHS fee").
- uda.delivered_fee_for_claims must sum total_nhs_fee (Leg B) — NOT fee_amount — so the UDA dashboard
£ card means "NHS fee basis of delivered claims"; docstring fixed. (Card intent = what the NHS owes
via the contract, distinct from patient money collected.)
Extend the uda module so each practice/tenant has its NHS contract data (annual contract value →
monthly payment, UDA target, per-band UDA values / fee scale) and ACCEPTED claims deterministically
credit the performer/practice UDA ledger. Claims carry performer + date-anchored fee basis already;
delivered/reconciled UDA + fee aggregations per performer per period must be queryable (M6 needs it).
New module (models + manager UI + tasks), v1:
- Contract/Schedule: per practice, monthly NHS statement record — period, gross contract payment,
total patient-charge clawback, superannuation, net paid. v1 = manual entry by the practice manager
(file import of real DCS127/129 = LATER, out of scope this round).
- Three-way match per accepted claim: NHS fee basis (Leg B) vs UDAs credited vs the claim's patient
charge (Leg A) appearing in the clawback total; mark each claim RECONCILED / UNMATCHED.
- Monitors (list views/filters on finance screens): "uncollected patient charges due for clawback"
(accepted claim, Leg A unpaid) and "disputed/audited exemptions" (exempt claims at retrospective-
clawback risk).
- ERPNext representation [F3b — recommended design, confirm]: monthly statement posts a Journal Entry
Dr Bank (net) + Dr NHS Patient Charges (Clearing) (clawback — drains the clearing account built up
by patient-charge payments) / Cr NHS Contract Income (gross). ERPNext then shows the full picture:
AR Patients (unpaid patient charges), the clearing liability, and the practice's real NHS revenue.
- Retire dead enum values NHS_REIMBURSABLE / PATIENT_COPAY (demo rows already CANCELLED/retired) —
remove or keep-for-history per OpenCode call; no UI may show them.
- Claims-list caption/column copy per M2; financial dashboard FP17 card wording checked against
Leg A/Leg B semantics.
- Continuation rules, 2-month-rule alerts, urgent/free-repair edges, £0-charge-but-UDA treatments:
still LATER phase (unchanged from the redesign spec — recorded, not built).
- Performer profile gains: target UDAs, UDA pay rate (£), lab-fee split %, superannuation rate.
- Month-end pay statement = reconciled (RECONCILED-status) UDAs × rate − lab share − superannuation;
never calculated on charted/claimed-but-unreconciled UDAs (research rule).
- Depends on M4's RECONCILED marking + M3's per-performer aggregations → build after M3/M4 land.
Coordinated reset after the build (Hermes runs it, precedent: seed_dental_demo --tenant
test-client-dental --reset):
- Provision the new ERPNext accounts on the live dental site + wire the config row (practice pk 58)
BEFORE the reset so nothing syncs to a stale account.
- Re-run the three course stories under the corrected model (Mei OPEN NHS band-2 close → claim +
NHS-charge invoice at £76.60 posting to clearing; Daniel private close £60 → fee income; Fatima
exempt close → £0 patient charge + £76.60 fee basis recorded) + at least one month-end statement
entry + three-way match to exercise M4, and an associate-pay statement for a performer to exercise
M6 (M4/M6 can be demoed with the reset's synthetic rows).
- Existing demo invoices ACC-SINV-2026-00029/30 posted to fee income: cancelled as part of the reset
(pre-change history; ERPNext residue cleared like the earlier cleanup).
- ABSORBS the OPEN seed-fix note (seed_dental_demo must yield a clean new-model baseline without
post-seed surgery — course-less claims, legacy PAID local-only tx, no REJECTED demo claims).
====================================================================
====================================================================
F1 Dedicated NHS-charge clearing account, selected by invoice type. CONFIRMED.
F2 Fee basis stays date-anchored statutory band charge. CONFIRMED.
F3 Schedule reconciliation built now (v1 manual; DCS file import later). CONFIRMED.
F3b ERPNext journal for the monthly statement (Dr Bank net + Dr clearing clawback / Cr NHS Contract
Income). CONFIRMED 2026-09-08.
F4 Associate pay module built now (after M4). CONFIRMED.
F5 Demo tenant reset after the build. CONFIRMED.
====================================================================
====================================================================
- M1 (account selection + config fields + provision command for ERPNext accounts + label).
- M2 (fee column/field redefinition + uda metric fix).
- M3 (contract data + UDA-credit verification/extension).
- M4 (schedule/statement model + three-way match + monitors + ERPNext journal task).
- M6 (performer schedule + pay statement on reconciled UDAs).
- M5 cleanup + seed re-work (M7's seed requirement).
Suite green + migrations on central veripath_dental + test-client-dental, then Hermes runs M7
(re-prep: accounts live + config wiring + reset + three-story demo + month-end exercise).
====================================================================
====================================================================
- Invoice sync account selection: claim-linked TRANSACTION → NHS-charge account; PRIVATE → fee
income; retired types skipped; ERPNext posts + submits.
- Payment Entry unregressed (amount = chosen invoice total — the 417 fix must hold across the account
change).
- fee_amount/column + delivered_fee_for_claims unit tests redefined (exempt claim: fee basis £76.60
present while patient charge £0).
- M4: statement entry, three-way match (matched/unmatched), RECONCILED marking, monitor queries,
journal task posts the right Dr/Cr and drains clearing by the clawback total.
- M6: pay statement = reconciled UDAs only; lab/superannuation arithmetic; performer with zero
reconciled UDAs pays £0.
- Migrations safe on central + tenant; enum cleanup without dangling references; existing suites stay
green (course-billing / payments / band-charges / invoice-sync / financial / uda).
====================================================================
====================================================================
- Private treatment flow UNCHANGED (invoice → ERPNext → Payment Entry is correct per research).
- No per-claim ERPNext invoice to an "NHS customer"; no resurrection of NHS_REIMBURSABLE/PATIENT_COPAY
two-invoice semantics.
- Deposit semantics, patient portal Path B, FP17PR exemption capture UI — untouched this round.
- ERPNext account provisioning is idempotent; live sites get accounts + config wiring before any
re-sync/reset (Hermes runs the live re-prep; OpenCode ships the tooling).
- No real patient data anywhere (dummy/synthetic only); no secrets/keys/internal addresses in any
output.
- Doc pass after build (Hermes, one page at a time): private-ledger-billing (likely fine),
co-payment-splitting (retired → rewrite/delete), invoicing, fp17-claim-submission, uda-auditing +
finance intro — now also covering NHS-charge clearing, schedule reconciliation and associate pay.
====================================================================
- Research: development/projects/dental_app/fp17_claims, fp17_claims_submissions
- Prior spec: development/projects/dental_app/fp17-course-billing-redesign (P1+P2 shipped 9d8bded4;
this round = its accounting corrections + P3-as-core)
- Handoff notes 2026-09-08 (from-hermes): redesign note (OPEN — superseded in part by this round);
private-income→ERPNext (leg confirmed correct); 417 bugs (fixed in P2 — must not regress); seed fix
(OPEN — absorbed into M7)