From 7bb344293e3261aca5fc8b45f7aeb41a359a9389 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 6 Oct 2026 15:12:21 +0000 Subject: [PATCH 1/2] Landing: explain navigation billing (Medicare pays for coordination) A 'Get paid for coordination' section in plain language: what Medicare pays for, the three steps (log time, the month adds up, hand your biller a superbill), the three programs with their codes, and a superbill preview. Nav link, an Advocate seat chip, and the superbill command in the terminal sample. Co-Authored-By: Claude Opus 5.5 --- apps/web/public/index.html | 73 +++++++++++++++++++++++++++++++++++++- design/Main.dc.html | 71 ++++++++++++++++++++++++++++++++++++ design/build.py | 2 +- 3 files changed, 144 insertions(+), 2 deletions(-) diff --git a/apps/web/public/index.html b/apps/web/public/index.html index a7a925d..6220851 100644 --- a/apps/web/public/index.html +++ b/apps/web/public/index.html @@ -4,7 +4,7 @@ tleehealth: the practice that runs itself - + @@ -29,6 +29,7 @@
Product + Get paid Agents Import Developers @@ -140,6 +141,74 @@

+
+
+
GET PAID FOR COORDINATION
+

Medicare pays for the help your staff already gives.

+

Chasing a prior authorization. Booking the oncologist. Finding a ride to infusion. Since 2024 Medicare pays practices for this work, roughly $75 to $100 a month per patient for the first hour, depending on where you practice. Most practices never bill it, because nobody counts the minutes.

+
    +
  1. 01Your advocate logs the time. A staff member helps a patient, picks what they did and how long it took. The care plan keeps the goals and who owes what.
  2. +
  3. 02The month adds itself up. Minutes turn into billing codes per patient, and the caseload shows how close each one is to the next.
  4. +
  5. 03Hand your biller a superbill. One spreadsheet of what to claim, with anything that would get a claim rejected (missing consent, no qualifying visit, no NPI) held back and named.
  6. +
+
+
+
Illness navigation
+
One serious condition, like cancer or heart failure
+
G0023 · G0024
+
+
+
Community health
+
Housing, food or transport in the way of treatment
+
G0019 · G0022
+
+
+
Chronic care
+
Two or more long-term conditions, with a care plan
+
99490 · 99439
+
+
+

Your practice submits the claims; tleehealth keeps the record and does the math. Rates vary by locality and payer.

+
+ + +
+
+
Superbill · October
+
2 ready · 1 on hold
+
+
    +
  • + Rosa D.
    Illness navigation · 105 min
    + G0023 + G0024 + ready +
  • +
  • + Marcus O.
    Chronic care · 42 min
    + 99490 + 99439 + ready +
  • +
  • + Walt K.
    Community health · 64 min
    + G0019 + hold +
  • +
  • Walt K.: consent is over a year old; renew it
  • +
+
+
Rosa D. · progress to the next add-on15 min to go
+
+
+ Ready to bill: G0023 ×1 · G0024 ×1 · 99490 ×1 · 99439 ×1 + Download CSV +
+
+
+
+ +
@@ -197,6 +266,7 @@

Org manager Provider Staff + Advocate Always free: Patients Leads @@ -236,6 +306,7 @@

$ tleehealth login $ tleehealth schedule today --location mission-st $ tleehealth invite dr.lin@example.com --type provider +$ tleehealth superbill --month 2026-10 --csv $ tleehealth tui

diff --git a/design/Main.dc.html b/design/Main.dc.html index 59d4917..b901211 100644 --- a/design/Main.dc.html +++ b/design/Main.dc.html @@ -26,6 +26,7 @@
Product + Get paid Agents Import Developers @@ -137,6 +138,74 @@

+
+
+
GET PAID FOR COORDINATION
+

Medicare pays for the help your staff already gives.

+

Chasing a prior authorization. Booking the oncologist. Finding a ride to infusion. Since 2024 Medicare pays practices for this work, roughly $75 to $100 a month per patient for the first hour, depending on where you practice. Most practices never bill it, because nobody counts the minutes.

+
    +
  1. 01Your advocate logs the time. A staff member helps a patient, picks what they did and how long it took. The care plan keeps the goals and who owes what.
  2. +
  3. 02The month adds itself up. Minutes turn into billing codes per patient, and the caseload shows how close each one is to the next.
  4. +
  5. 03Hand your biller a superbill. One spreadsheet of what to claim, with anything that would get a claim rejected (missing consent, no qualifying visit, no NPI) held back and named.
  6. +
+
+
+
Illness navigation
+
One serious condition, like cancer or heart failure
+
G0023 · G0024
+
+
+
Community health
+
Housing, food or transport in the way of treatment
+
G0019 · G0022
+
+
+
Chronic care
+
Two or more long-term conditions, with a care plan
+
99490 · 99439
+
+
+

Your practice submits the claims; tleehealth keeps the record and does the math. Rates vary by locality and payer.

+
+ + +
+
+
Superbill · October
+
2 ready · 1 on hold
+
+
    +
  • + Rosa D.
    Illness navigation · 105 min
    + G0023 + G0024 + ready +
  • +
  • + Marcus O.
    Chronic care · 42 min
    + 99490 + 99439 + ready +
  • +
  • + Walt K.
    Community health · 64 min
    + G0019 + hold +
  • +
  • Walt K.: consent is over a year old; renew it
  • +
+
+
Rosa D. · progress to the next add-on15 min to go
+
+
+ Ready to bill: G0023 ×1 · G0024 ×1 · 99490 ×1 · 99439 ×1 + Download CSV +
+
+
+
+ +
@@ -194,6 +263,7 @@

Org manager Provider Staff + Advocate Always free: Patients Leads @@ -233,6 +303,7 @@

$ tleehealth login $ tleehealth schedule today --location mission-st $ tleehealth invite dr.lin@example.com --type provider +$ tleehealth superbill --month 2026-10 --csv $ tleehealth tui

diff --git a/design/build.py b/design/build.py index d0cb0bd..3069a78 100644 --- a/design/build.py +++ b/design/build.py @@ -13,7 +13,7 @@ PAGES = { 'Main.dc.html': ('index.html', 'tleehealth: the practice that runs itself', 'Telehealth and practice management with an AI agent that calls every patient. ' - 'Scheduling, prescriptions, labs and health-record import. $10 per seat or $199 for up to 1,000.', + 'Scheduling, prescriptions, labs, care plans and Medicare navigation billing. $10 per seat or $199 for up to 1,000.', 'https://tleehealth.com/'), } From d89296d4eb3f4cec59b2221693712c2f04d3c440 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 6 Oct 2026 15:16:25 +0000 Subject: [PATCH 2/2] Superbill: drop the midpoint option; PIN/CHI need their full time CMS, CY2024 PFS final rule (88 FR 78941): 'if a patient requires less than 60 minutes per month for PIN services, then their needs may be best suited to other types of care management services.' CMS also declined shorter CHI increments (88 FR 78925), and the CY2025/2026 rules kept the 60 minutes. Billing G0023 at 31 minutes would claim time not spent, so the ?rule=midpoint option, the UI select, the CLI --rule flag and the MCP rule argument are removed. CLI and MCP 0.3.1. Co-Authored-By: Claude Opus 5.5 --- apps/web/public/app/app.js | 13 ++++--------- apps/web/src/api.js | 12 ++++-------- apps/web/src/app.js | 2 +- apps/web/src/navigation.js | 35 ++++++++++++++--------------------- bun.lock | 6 +++--- packages/cli/package.json | 2 +- packages/cli/src/cli.js | 9 ++++----- packages/cli/src/client.js | 4 ++-- packages/mcp/package.json | 4 ++-- packages/mcp/src/server.js | 3 +-- test/client.test.js | 2 +- test/navigation.test.js | 20 ++++++++------------ 12 files changed, 45 insertions(+), 67 deletions(-) diff --git a/apps/web/public/app/app.js b/apps/web/public/app/app.js index 724d353..9e8bf43 100644 --- a/apps/web/public/app/app.js +++ b/apps/web/public/app/app.js @@ -1014,9 +1014,8 @@ async function viewCaseload(q) { async function viewSuperbill(q) { const month = q.get('month') ?? thisMonthISO(); - const rule = q.get('rule') === 'midpoint' ? 'midpoint' : 'full'; - const sb = await get(`/orgs/${state.orgId}/superbill?month=${month}&rule=${rule}`); - const link = (m, r = rule) => `/app/superbill?month=${m}${r === 'midpoint' ? '&rule=midpoint' : ''}`; + const sb = await get(`/orgs/${state.orgId}/superbill?month=${month}`); + const link = (m) => `/app/superbill?month=${m}`; const totals = Object.entries(sb.totals); const html = `
@@ -1025,8 +1024,7 @@ async function viewSuperbill(q) { ‹ This month › - - Download CSV + Download CSV
@@ -1050,16 +1048,13 @@ async function viewSuperbill(q) { }
One line per code, per patient, per month, for your biller or clearinghouse. Prices vary by locality, so none are shown. - Full time counts a unit only when its whole time is met. Midpoint applies the CPT rule (G0023 at 31 minutes); check that your payers accept it before using it. CCM always needs its full 20 minutes. + A code counts only when its full time is met: 60 minutes for G0023 (and G0019, G0140), 30 more for each add-on, 20 for CCM. Medicare does not allow rounding up from 31 minutes on these codes. On hold means a claim would be missing something: consent (renewed yearly for PIN and CHI), the initiating visit, the billing practitioner's NPI, or for CCM a care plan.
`; return { html, - bind() { - document.getElementById('rule').onchange = (e) => go(link(month, e.target.value)); - }, }; } diff --git a/apps/web/src/api.js b/apps/web/src/api.js index 5eb08e8..8eaafaa 100644 --- a/apps/web/src/api.js +++ b/apps/web/src/api.js @@ -714,7 +714,7 @@ async function carePlan(patientId) { } /** Enrollments with their minutes in a month, decorated with codes and blockers. */ -async function programRows(orgId, { start, end, month }, filter, rule = 'full') { +async function programRows(orgId, { start, end, month }, filter) { const sql = db(); const rows = await sql` select cp.*, p.name as patient, p.dob, bp.name as billing_provider, bp.npi as billing_npi, nv.name as navigator, @@ -726,7 +726,7 @@ async function programRows(orgId, { start, end, month }, filter, rule = 'full') where cp.org_id = ${orgId} ${filter(sql)} order by p.name nulls last, cp.program`; return rows.map((r) => ({ - ...nav.superbillRow(r, r.minutes, month, rule), + ...nav.superbillRow(r, r.minutes, month), status: r.status, navigator_id: r.navigator_id, billing_provider_id: r.billing_provider_id, @@ -736,7 +736,7 @@ async function programRows(orgId, { start, end, month }, filter, rule = 'full') })); } -api.get('/navigation/programs', (c) => c.json({ programs: nav.PROGRAMS, activities: nav.ACTIVITIES, time_rules: nav.TIME_RULES })); +api.get('/navigation/programs', (c) => c.json({ programs: nav.PROGRAMS, activities: nav.ACTIVITIES })); api.get('/orgs/:org/patients/:id/care-plan', async (c) => { const { me, user } = await member(c, c.req.param('org')); @@ -945,19 +945,16 @@ api.get('/orgs/:org/caseload', async (c) => { return c.json({ month: m.month, mine, caseload: rows }); }); -/** Everything billable in a month. ?rule=full|midpoint, ?format=csv. */ +/** Everything billable in a month. ?format=csv. Units need their full time (see navigation.js). */ api.get('/orgs/:org/superbill', async (c) => { const { me, user } = await member(c, c.req.param('org'), BILLERS); const m = monthOf(c); - const rule = c.req.query('rule') ?? 'full'; - if (!nav.TIME_RULES.includes(rule)) fail(400, 'rule must be full or midpoint'); // Every enrollment that overlapped the month and has time in it. const rows = ( await programRows( me.org_id, m, (sql) => sql`and cp.started_on < ${m.end} and (cp.ended_on is null or cp.ended_on >= ${m.start})`, - rule, ) ).filter((r) => r.minutes > 0); rows.sort((a, b) => Number(b.ready) - Number(a.ready)); @@ -972,7 +969,6 @@ api.get('/orgs/:org/superbill', async (c) => { for (const r of rows) if (r.ready) for (const l of r.lines) totals[l.code] = (totals[l.code] ?? 0) + l.units; return c.json({ month: m.month, - rule, rows, totals, ready: rows.filter((r) => r.ready).length, diff --git a/apps/web/src/app.js b/apps/web/src/app.js index 339f22f..e54e893 100644 --- a/apps/web/src/app.js +++ b/apps/web/src/app.js @@ -151,7 +151,7 @@ Authenticate with a session cookie or \`Authorization: Bearer th_live_...\` (cre - PATCH /orgs/:org/programs/:id consent (renews it), billing practitioner, end - POST /orgs/:org/programs/:id/time log navigation minutes ({"minutes", "activity", "note", "performed_on"}) - GET /orgs/:org/caseload?month=YYYY-MM enrollments, minutes toward G0023/G0024, G0019/G0022, 99490/99439, open tasks -- GET /orgs/:org/superbill?month=YYYY-MM&rule=full|midpoint&format=csv +- GET /orgs/:org/superbill?month=YYYY-MM&format=csv the month's billable codes per patient, ready or on hold with the reason ## Tools diff --git a/apps/web/src/navigation.js b/apps/web/src/navigation.js index a294c8a..018ccea 100644 --- a/apps/web/src/navigation.js +++ b/apps/web/src/navigation.js @@ -5,12 +5,14 @@ * they add up to. Prices vary by locality and change yearly, so none are kept * here; a superbill lists codes and units, and the biller prices them. * - * Time rules. "full" counts a unit only when its whole time is met: G0023 at 60 - * minutes, each G0024 at another 30. "midpoint" applies the CPT rule that a unit - * of time is attained once its midpoint is passed (G0023 at 31, G0024 at 16 more). - * Whether a practice's payer accepts the midpoint for these codes is the biller's - * call, so full is the default. CCM's codes say "at least 20 minutes" in the - * descriptor, so the midpoint never applies to them. + * A unit counts only when its whole time is met: G0023 at 60 minutes, each G0024 + * at another 30. The CPT "midpoint" rule (a unit at 31 minutes) does NOT apply to + * these codes. CMS, CY2024 PFS final rule, 88 FR 78941: "if a patient requires + * less than 60 minutes per month for PIN services, then their needs may be best + * suited to other types of care management services." CMS also declined shorter + * increments for CHI (88 FR 78925), and the CY2025/2026 rules kept the 60 minutes. + * CCM's descriptors say "at least 20 minutes". Never add a midpoint option back: + * it would bill Medicare for time not spent. */ export const PROGRAMS = { @@ -44,7 +46,6 @@ export const PROGRAMS = { first: { code: '99490', minutes: 20 }, addon: { code: '99439', minutes: 20, max: 2 }, yearlyConsent: false, - exactTime: true, needsCarePlan: true, about: 'Two or more chronic conditions expected to last 12 months or more, with a care plan.', }, @@ -63,29 +64,21 @@ export const ACTIVITIES = [ 'other', ]; -export const TIME_RULES = ['full', 'midpoint']; - -/** Minutes past which a unit of `minutes` counts under a rule. */ -function threshold(minutes, rule, exactTime) { - return rule === 'midpoint' && !exactTime ? Math.floor(minutes / 2) + 1 : minutes; -} - /** * What a month's minutes bill under a program: [{ code, units }], and how many * more minutes reach the next unit (null when the add-on is capped out). */ -export function unitsFor(program, totalMinutes, rule = 'full') { +export function unitsFor(program, totalMinutes) { const p = PROGRAMS[program]; if (!p) throw new Error(`unknown program ${program}`); const total = Math.max(0, Math.floor(totalMinutes || 0)); - const firstAt = threshold(p.first.minutes, rule, p.exactTime); + const firstAt = p.first.minutes; if (total < firstAt) return { lines: [], next: firstAt - total }; const lines = [{ code: p.first.code, units: 1 }]; - // Add-on units start counting after the first code's full time, even under the - // midpoint rule: the first unit has to be complete before the next one begins. + // Add-on units start counting after the first code's full time. const past = total - p.first.minutes; - const addAt = threshold(p.addon.minutes, rule, p.exactTime); + const addAt = p.addon.minutes; let addUnits = past < addAt ? 0 : 1 + Math.floor((past - addAt) / p.addon.minutes); if (p.addon.max != null) addUnits = Math.min(addUnits, p.addon.max); if (addUnits) lines.push({ code: p.addon.code, units: addUnits }); @@ -141,8 +134,8 @@ export function blockers(enrollment, month) { } /** One superbill row per enrollment with time this month. */ -export function superbillRow(enrollment, minutes, month, rule) { - const { lines, next } = unitsFor(enrollment.program, minutes, rule); +export function superbillRow(enrollment, minutes, month) { + const { lines, next } = unitsFor(enrollment.program, minutes); const blocked = blockers(enrollment, month); return { program_id: enrollment.id, diff --git a/bun.lock b/bun.lock index 9688b23..57a12ca 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,7 @@ }, "packages/cli": { "name": "@profullstack/tleehealth", - "version": "0.3.0", + "version": "0.3.1", "bin": { "tleehealth": "./bin/tleehealth.mjs", }, @@ -36,12 +36,12 @@ }, "packages/mcp": { "name": "@profullstack/tleehealth-mcp", - "version": "0.3.0", + "version": "0.3.1", "bin": { "tleehealth-mcp": "./bin/tleehealth-mcp.mjs", }, "dependencies": { - "@profullstack/tleehealth": "^0.3.0", + "@profullstack/tleehealth": "^0.3.1", }, }, "packages/payments": { diff --git a/packages/cli/package.json b/packages/cli/package.json index b6c9e16..07f4133 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/tleehealth", - "version": "0.3.0", + "version": "0.3.1", "description": "tleehealth from the terminal: the CLI, the front-desk TUI, and the API client the MCP server uses.", "type": "module", "license": "MIT", diff --git a/packages/cli/src/cli.js b/packages/cli/src/cli.js index 27969c7..6045258 100644 --- a/packages/cli/src/cli.js +++ b/packages/cli/src/cli.js @@ -3,7 +3,7 @@ * * tleehealth schedule [--date today|tomorrow|YYYY-MM-DD] [--json] * tleehealth caseload [--month YYYY-MM] [--all] [--json] - * tleehealth superbill [--month YYYY-MM] [--rule full|midpoint] [--csv] [--json] + * tleehealth superbill [--month YYYY-MM] [--csv] [--json] * tleehealth log-time PROGRAM_ID MINUTES [--activity A] [--note TEXT] [--date YYYY-MM-DD] * tleehealth health * tleehealth login [KEY] @@ -19,8 +19,7 @@ const HELP = `tleehealth: telehealth and practice management. tleehealth schedule [--date today|tomorrow|YYYY-MM-DD] [--json] the front-desk day tleehealth caseload [--month YYYY-MM] [--all] [--json] navigation patients and minutes this month - tleehealth superbill [--month YYYY-MM] [--rule full|midpoint] [--csv] - billable PIN/CHI/CCM codes for the month + tleehealth superbill [--month YYYY-MM] [--csv] billable PIN/CHI/CCM codes for the month tleehealth log-time PROGRAM_ID MINUTES [--activity A] [--note T] [--date YYYY-MM-DD] tleehealth health is the API up tleehealth login [KEY] save an API key @@ -70,7 +69,7 @@ export function formatCaseload(c) { } export function formatSuperbill(sb) { - const lines = [`superbill ${sb.month} (${sb.rule} time): ${sb.ready} ready, ${sb.held} on hold, ${sb.under} under threshold`]; + const lines = [`superbill ${sb.month}: ${sb.ready} ready, ${sb.held} on hold, ${sb.under} under threshold`]; for (const r of sb.rows) lines.push( `${(r.patient ?? '').padEnd(18)} ${r.program_name.padEnd(7)} ${String(r.minutes).padStart(4)} min ${(codes(r) || '-').padEnd(16)} ${r.ready ? 'ready' : r.lines.length ? `hold: ${r.blockers.join('; ')}` : `${r.next_unit_in} min short`}`, @@ -105,7 +104,7 @@ export async function main(argv = process.argv.slice(2)) { return 0; } case 'superbill': { - const sb = await superbill(auth, { org: flags.org, month: flags.month, rule: flags.rule, csv: Boolean(flags.csv) }); + const sb = await superbill(auth, { org: flags.org, month: flags.month, csv: Boolean(flags.csv) }); console.log(flags.csv ? sb.trimEnd() : flags.json ? JSON.stringify(sb, null, 2) : formatSuperbill(sb)); return 0; } diff --git a/packages/cli/src/client.js b/packages/cli/src/client.js index 6e08eab..57a8faf 100644 --- a/packages/cli/src/client.js +++ b/packages/cli/src/client.js @@ -95,9 +95,9 @@ export async function caseload(auth, { org, month, all } = {}) { } /** The month's billable care-management codes. csv: true returns the CSV text. */ -export async function superbill(auth, { org, month, rule = 'full', csv = false } = {}) { +export async function superbill(auth, { org, month, csv = false } = {}) { const id = await resolveOrg(auth, org); - return call(auth, `/api/v1/orgs/${id}/superbill?month=${monthParam(month)}&rule=${encodeURIComponent(rule)}${csv ? '&format=csv' : ''}`); + return call(auth, `/api/v1/orgs/${id}/superbill?month=${monthParam(month)}${csv ? '&format=csv' : ''}`); } /** Log navigation minutes against a program enrollment. */ diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 44fa844..8c05554 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/tleehealth-mcp", - "version": "0.3.0", + "version": "0.3.1", "description": "MCP server for tleehealth.com: read the practice schedule, the navigation caseload and superbill, and log navigation time from any MCP client.", "type": "module", "license": "MIT", @@ -11,6 +11,6 @@ "bin": { "tleehealth-mcp": "./bin/tleehealth-mcp.mjs" }, "exports": { ".": "./src/server.js" }, "files": ["bin", "src", "README.md"], - "dependencies": { "@profullstack/tleehealth": "^0.3.0" }, + "dependencies": { "@profullstack/tleehealth": "^0.3.1" }, "keywords": ["mcp", "telehealth", "practice-management", "scheduling"] } diff --git a/packages/mcp/src/server.js b/packages/mcp/src/server.js index a2462a2..c3a6bfb 100644 --- a/packages/mcp/src/server.js +++ b/packages/mcp/src/server.js @@ -35,12 +35,11 @@ const TOOLS = [ { name: 'get_superbill', description: - "The month's billable care-management codes per patient: program, condition, billing practitioner and NPI, minutes, codes and units, and ready / on hold (with the reason) / under threshold. Owners, org managers and providers only. rule 'midpoint' applies the CPT midpoint time rule; 'full' (default) needs each unit's whole time.", + "The month's billable care-management codes per patient: program, condition, billing practitioner and NPI, minutes, codes and units, and ready / on hold (with the reason) / under threshold. Owners, org managers and providers only. A code counts only when its full time is met (60 minutes for G0023, 30 per add-on, 20 for CCM).", inputSchema: { type: 'object', properties: { month: { type: 'string', description: 'YYYY-MM; default this month' }, - rule: { type: 'string', enum: ['full', 'midpoint'] }, csv: { type: 'boolean', description: 'return the CSV for a biller instead of JSON' }, org: { type: 'string' }, }, diff --git a/test/client.test.js b/test/client.test.js index 891ead6..b8a185e 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -29,7 +29,7 @@ describe('cli', () => { expect(c).toContain('caseload 2026-10 (mine)'); expect(c).toContain('G0023 + G0024'); expect(c).toContain('25 min to next'); - const sb = formatSuperbill({ month: '2026-10', rule: 'full', ready: 1, held: 0, under: 0, rows: [row], totals: { G0023: 1, G0024: 1 } }); + const sb = formatSuperbill({ month: '2026-10', ready: 1, held: 0, under: 0, rows: [row], totals: { G0023: 1, G0024: 1 } }); expect(sb).toContain('ready to bill: G0023 x1, G0024 x1'); }); }); diff --git a/test/navigation.test.js b/test/navigation.test.js index c3e0945..f08ca2d 100644 --- a/test/navigation.test.js +++ b/test/navigation.test.js @@ -13,18 +13,16 @@ describe('units', () => { expect(unitsFor('pin', 150).lines[1]).toEqual({ code: 'G0024', units: 3 }); }); - test('PIN under the midpoint rule: G0023 at 31, G0024 at 16 past the full hour', () => { - expect(unitsFor('pin', 30, 'midpoint').lines).toEqual([]); - expect(unitsFor('pin', 31, 'midpoint')).toEqual({ lines: [{ code: 'G0023', units: 1 }], next: 45 }); - expect(unitsFor('pin', 75, 'midpoint').lines).toHaveLength(1); - expect(unitsFor('pin', 76, 'midpoint').lines[1]).toEqual({ code: 'G0024', units: 1 }); - expect(unitsFor('pin', 106, 'midpoint').lines[1]).toEqual({ code: 'G0024', units: 2 }); + test('no rounding up: 31 minutes of PIN bills nothing, 76 bills no add-on (CMS, 88 FR 78941)', () => { + expect(unitsFor('pin', 31)).toEqual({ lines: [], next: 29 }); + expect(unitsFor('pin', 76).lines).toEqual([{ code: 'G0023', units: 1 }]); + expect(unitsFor('chi', 59).lines).toEqual([]); }); - test('CCM needs full time under either rule, and 99439 caps at two', () => { - expect(unitsFor('ccm', 19, 'midpoint').lines).toEqual([]); + test('CCM needs its full 20 minutes, and 99439 caps at two', () => { + expect(unitsFor('ccm', 19).lines).toEqual([]); expect(unitsFor('ccm', 20).lines).toEqual([{ code: '99490', units: 1 }]); - expect(unitsFor('ccm', 40, 'midpoint').lines[1]).toEqual({ code: '99439', units: 1 }); + expect(unitsFor('ccm', 40).lines[1]).toEqual({ code: '99439', units: 1 }); expect(unitsFor('ccm', 200)).toEqual({ lines: [{ code: '99490', units: 1 }, { code: '99439', units: 2 }], next: null }); }); @@ -216,9 +214,7 @@ describe.skipIf(!configured())('navigation api', () => { expect(sb.body.ready).toBe(1); expect(sb.body.totals).toEqual({ G0023: 1 }); expect(sb.body.rows[0].billing_npi).toBe('1234567893'); - const mid = await owner.get(`/orgs/${orgId}/superbill?month=${month}&rule=midpoint`); - expect(mid.body.rows[0].next_unit_in).toBe(11); // 60 + 16 - 65 - expect((await owner.get(`/orgs/${orgId}/superbill?month=${month}&rule=loose`)).status).toBe(400); + expect(sb.body.rows[0].next_unit_in).toBe(25); // G0024 at 90 const csv = await owner.get(`/orgs/${orgId}/superbill?month=${month}&format=csv`); expect(csv.headers.get('content-type')).toContain('text/csv'); expect(csv.body).toContain('Rosa Diaz,1951-07-04,PIN,Stage III colon cancer');