Research basis (read first): development/projects/dental_app/fp17_uda (Matthew's FP17/UDA research transcript, 2026-09-09) — the four-stage claim lifecycle, netting of patient charges, UDA/band matrix, "highest band wins", charge-exempt CoTs, continuation and the two-month rule.
Builds on:fp17-nhs-accounting-model(M1–M6 shipped, live /app) andfp17-course-billing-redesign(P1/P2). Corrections here supersede those specs where they differ.
Decisions D1–D7 confirmed by Matthew 2026-09-09 ("go with your recommendations").
Testing is deliberately paused until the claim/UDA model matches real FP17 practice end-to-end. What exists is correct but coarse: the banding engine returns plain bands 1/2/3 (no 2a/2b/2c subdivision), the UDA map is static {1:1, 2:3, 3:12}, urgent carries no UDA credit, charge-exempt CoTs do not exist, there is no continuation/two-month-rule logic, superannuation is refused in posting, and there is no contract-performance (mid-year 96% / year-end clawback) view. This round fills those gaps so the final test pass covers the whole system.
dental/fp17.py::band_for_course — iterates COMPLETED chart entries across the course; band 3 tokens / procedure-code band 3 → 3; is_extracted or RCT token or any non-Band-1 surface token → 2; else 1. Returns int or None (caller falls back to the appointment's planned band).dental/models.py::UDA_VALUE_BY_BAND = {1:1, 2:3, 3:12}; uda_value_for_band(nhs_band) does int(nhs_band) — so FP17Claim.nhs_band is currently int-like; claim rows hold 1/2/3.fp17.get_band_charge resolves NHSBandCharge (England, date-anchored; 2026/27: 1=£27.90, 2=£76.60, 3=£332.10, URGENT=£27.90); urgent falls back to the band-1 rate.uda.py derives UDAs from accepted/transmitted claims via uda_value_for_band; delivered_fee_for_claims sums total_nhs_fee (Leg B fee basis, date-anchored statutory band charge).NHSContract (urgent_uda_value, default 1.2) — historic flat 1.2 is the demo default; contracts may set their own. Charge stays £27.90 (band-1 rate).1, 2A, 2B, 2C, 3, U (urgent) and exempt-catalogue codes X_ARREST (arrest of bleeding), X_SUTURES (removal of sutures), X_REPAIR_BRIDGE, X_REPAIR_DENTURE, X_FLUORIDE (nurse-applied fluoride varnish). Display labels for the UI (claims list/detail, filters): "Band 1", "Band 2a", "Band 2b", "Band 2c", "Band 3", "Urgent", and the exempt descriptions. FP17Claim.nhs_band migrates int → CharField; existing rows re-derived (below).
Extend band_for_course (keep its COMPLETED-only rule and the fallback to the planned band) to return a band code. Cascade, highest band wins (research page §"Complete CoT and UDA Rule Matrix" + pseudo-code):
_BAND3_TOKENS — crown/bridge/denture/veneer/inlay/splint — or procedure-code band 3) → 3._RCT_TOKENS / root-canal procedure code) on a permanent molar → 2C. Molar FDI teeth: 16,17,18,26,27,28,36,37,38,46,47,48.2B. Restorative teeth = distinct tooth_numbers with a completed filling/restoration surface (existing "not crown/sealant/fluoride/endo" token rule), a fissure sealant, a preformed crown, or is_extracted.2A.1 (examination/diagnostics/scale-polish/advice only).Urgent: a course whose appointments are all EMERGENCY-type (no planned CHECKUP/FILLING etc.) closes as U (existing urgent charge £27.90; new UDA credit per D2). Where the engine previously returned int, keep a thin compatibility shim (int only for 1/2/3) or update all callers — OpenCode's call; no caller may crash on a sub-band code. Extend _band_for_procedure/FP17TreatmentCode so a treatment code can pin an exact band code (add band_code or map band 2 codes by tooth/type context).
Charge + fee basis resolution: get_band_charge strips the sub-band suffix (2A/2B/2C → parent 2, £76.60) — or NHSBandCharge gains sub-band rows pointing at the same amount; either way the date-anchored £76.60 holds for all of 2a/b/c. total_nhs_fee for U = £27.90 (existing), for sub-bands = £76.60.
Replace the static int map with code-keyed values: 1→1, 2A→3, 2B→5, 2C→7, 3→12, U→contract.urgent_uda_value (default 1.2), X_ARREST→1.2, X_REPAIR_BRIDGE→1.2, X_SUTURES→1, X_REPAIR_DENTURE→1, X_FLUORIDE→0.5. uda_value_for_band reads the map + contract; UDAs become Decimal-capable end-to-end (delivered_udas, dashboard, associate pay, statement match — 0.5/1.2 arithmetic must not lose precision). Exempt codes need the claim's practice/contract context to resolve U — pass through where the claim is available. ACCEPTED credit path, uda dashboard and M6 pay flow through unchanged apart from the new values.
Closing a course whose COMPLETED treatment is only exempt-catalogue items produces an FP17 claim with the exempt band code, £0 patient charge, £0 fee basis — exemption of the patient is NOT required (these are £0 by nature). Exempt claims still transmit/accept and credit UDAs toward the contract; in the three-way match they contribute the UDA side only (no clawback line, no fee-basis line). Validation must accept £0 charge without patient_exemption. UI: claim detail shows the exempt CoT description instead of a band charge; claims list shows £0.00 + code. Prescription-only (chart marker for Rx issue, no course close claim) → activity log only, no claim (D3).
now > course.completed_at + 2 months (validation at the transmit action, clear message citing the NHSBSA deadline). Claims list shows an age indicator (e.g. "overdue" badge) when past the window. Demo data is recent — no friction expected.Performer superannuation is tracked on accepted/reconciled claims (pay profile superannuation_percent, seeded 8.4%) and appears on the associate pay statement as a deduction line with the net payable. When associate pay is posted and superannuation > 0, the journal gains the 4th leg: Dr Associate Fees (gross) / Cr Bank (net) / Cr Superannuation Payable — remove the current refusal. ERPNext: extend the NHS-account provisioning (provision_erpnext_config --provision-nhs-accounts) to also ensure a Superannuation Payable account (Liabilities), idempotent. Month-end statement journal (F3b) stays 3-leg. Provisioning must be live before any re-sync.
Read-only card/view per contract period: delivered accepted UDAs YTD (existing uda module), contract target (NHSContract uda_target), % achieved; mid-year flag at 30 Sep (below 96% = warning); projected year-end shortfall and a labelled clawback estimate = shortfall_udas × (ACV ÷ uda_target). Estimates labelled as such — no posting, no automation.
veripath_dental AND test-client-dental.FP17Claim.nhs_band → CharField; existing rows re-derived where a course + COMPLETED charts exist (re-run the extended band_for_course), else 2 → 2A (conservative — keeps recorded £76.60 fee basis valid). Expected: Fatima 232 (tooth 26 filling) → 2A; no other demo claims exist.2A, £76.60, 3 UDAs — identical to today's plain-band-2 numbers. The UDA differences only appear for molar-endo (2C), ≥3-tooth (2B) and lab (3) cases, which the live test pass can chart fresh. No seed change required; optionally a future seed story can add a 2C/3 closed course to show 7/12 UDA credit (not this round).feature/dental-nhs-accounting (baseline clean before U-round edits).veripath_dental + test-client-dental; restart dental_app + dental_celery_worker; verify healthy.provision_erpnext_config --provision-nhs-accounts on practice 58 (remember --realm test-client-dental) so Superannuation Payable exists before any posting test.Private flow unchanged; deposit semantics / patient-portal Path B untouched; real EDI/Compass integration NOT in scope (mock gateway remains); no per-claim NHS invoice (unchanged); patient charge for 2a/b/c stays £76.60; two-month rule is validation only (no auto-claims/closes); exempt CoTs never charge; no changes to the ERPNext UI-trim or the GP app. Doc pass (Hermes, one page at a time, after the round): fp17-claim-submission, uda-auditing, invoicing, finance intro (+ sub-band/exempt/continuation content), co-payment-splitting retire.