Auto zeroing the Medicare Medicaid patient balance

Hi @stephenwaite, and @kojiromike ,

I have this request. I was hoping to get a read from you on the project plan scoped out by Warp. Can you read over it and let me know if anything jumps out at you as don’t do that?

  1. Confirm environment and payer IDs
  • Validate which database to use during implementation and testing. You noted a local morton database is current; please confirm DB name to connect during sprint.
  • Verify the insurance company records that represent Medicare of Oklahoma and Medicaid of Oklahoma in insurance_companies. Capture all identifiers and aliases:
    • Medicare aliases: SKOK0, names containing Medicare.
    • Medicaid aliases: SMOK0 if applicable, names containing Medicaid.
  • Document site id and session setup needed for CLI or background scripts per your rule: if not using browser, set ignoreAuth and site_id in session.
  1. Introduce a new PSR-4 class: CrossoverReconciliationService
  • Location: src/Billing/CrossoverReconciliationService.php
  • Namespace: OpenEMR\Billing
  • Responsibility: Detect crossover claims and perform automatic reconciliation across both ERA and manual posting workflows.
  • Design: Single-responsibility class, no UI, reusable in both sl_eob_process.php and sl_eob_invoice.php.
  • Public interface:
    • detectPayers(patientId, encounterId, svcDate): returns primary and secondary payer classification (Medicare, Medicaid, other).
    • isMedicare(payerIdOrName), isMedicaid(payerIdOrName): alias-aware helpers.
    • aggregateMedicareAdjustments(eraServiceLine): returns combined amount and a memo string listing codes and amounts.
    • postAggregatedMedicareAdjustment(params): calls SLEOB::arPostAdjustment with combined amount and memo.
    • computeLineBalance(patientId, encounterId, code, modifier): fee minus existing payments and adjustments for that code line.
    • shouldAutoWriteOffMedicaid(eraClaimContext): true when Medicaid PR sum equals 0 and previous primary is Medicare.
    • autoWriteOffRemainingForMedicaid(patientId, encounterId, sessionId, code, modifier, reason, debug): posts remaining balance as adjustment with payer_type 2 using SLEOB::arPostAdjustment.
    • guardAlreadyPosted(patientId, encounterId, code, modifier, signature): prevent duplicate adjustments by searching ar_activity memo containing a unique signature.
  • Support types and constants:
    • Reason constants: MEDICARE_AGGREGATED_ADJ, MEDICAID_SECONDARY_AUTO_WO.
    • Rounding tolerance constant: 0.01.
  1. Configuration for payer alias detection
  • Add a small configuration facility in CrossoverReconciliationService that loads a payer alias map at runtime:
    • Default aliases: Medicare: SMOKO, name contains Medicare. Medicaid: SKOKO if applicable, name contains Medicaid.
  • Allow override via list_options table group payer_alias_map so admins can add new aliases (e.g., Medicare Advantage payers) without code changes.
  • Implement helper findPayerTypeByCompanyRow(row) using alias map and case-insensitive name matching.
  1. Medicare aggregated adjustment capture (ERA)
  • In sl_eob_process.php, inside the callback that processes service line CAS segments for Medicare claims:
    • Sum all CO group adjustment amounts for the line (e.g., CO-45, CO-253, CO-237). Do not include PR group entries (deductible, coins, copay).
    • Build a memo such as: Medicare adjustments aggregated: CO-45 6.41, CO-253 1.22, CO-237 6.02, total 13.65.
    • Call CrossoverReconciliationService::postAggregatedMedicareAdjustment with:
      • payer_type 1, codetype from line, code and modifier parsed from SVC, session id, amount equal to the sum, memo above, allowed_amount if available.
  • Important: avoid the literal phrase adjust code 45 in the memo so existing SLEOB automatic write-off logic does not override the supplied amount.
  • Ensure existing individual CO adjustment postings are suppressed for Medicare so adjustments are not double-posted. Keep PR entries as comments or zero-amount notes as currently designed.
  1. Medicaid automatic write-off when PR equals zero (ERA)
  • In sl_eob_process.php after posting Medicaid payments for a service line:
    • Compute patient responsibility sum from CAS PR group for the Medicaid ERA line. If absent, treat as 0.
    • Qualify for auto write-off if:
      • Payer is Medicaid per alias map.
      • Previous primary payer for the encounter is Medicare, detected via detectPayers or prior ar_activity entries with payer_type 1.
      • Medicaid patient responsibility total equals 0.00.
    • For the affected code and modifier:
      • Use computeLineBalance to get the remaining balance after all payments and existing adjustments.
      • If the remaining balance is greater than the rounding tolerance, post an adjustment with:
        • payer_type 2
        • amount equal to the remaining balance
        • codetype from the line
        • memo: AUTO-MCD-WO signature, example: Medicaid secondary auto write-off (PT RESP 0.00), remainder 8.99
        • guardAlreadyPosted to ensure idempotency if ERA reprocessed.
  • This zeros out the patient balance in the 99 percent case where Medicaid PR is zero.
  1. Exception flagging for the 1 percent
  • If Medicaid PR is greater than 0.00, or if the remaining balance after payment is negative or unexpectedly large:
    • Do not auto-write-off.
    • Create a zero-amount comment adjustment with payer_type 2 and memo: REVIEW NEEDED: Medicaid PR not zero or mismatch. Include computed figures for quick audit.
    • Optionally append a line to form_encounter.billing_note to surface the exception in UI.
  • Provide a small helper method flagException(patientId, encounterId, code, modifier, message).
  1. Manual posting integration in sl_eob_invoice.php
  • Add a lightweight UI hint under Insurance or Invoice Actions:
    • Checkbox: PT RESP is 0.00 per remit (default checked for Medicaid). Name: form_ptresp_zero.
  • After the existing foreach that posts payments and adjustments:
    • If current payer_type equals 2 (Medicaid) and form_ptresp_zero equals true:
      • For each code line present in form_line:
        • Compute the remainder via computeLineBalance.
        • Post auto write-off using CrossoverReconciliationService::autoWriteOffRemainingForMedicaid.
  • Show an info alert if auto write-offs were applied or if exceptions were flagged.
  • Preserve current behavior when the checkbox is unchecked.
  1. Payer detection in manual workflow
  • Use SLEOB::arGetPayerID(pid, svcdate, level) to fetch payer ids for levels 1 and 2.
  • Pass the payer name or id through CrossoverReconciliationService::isMedicaid, isMedicare to decide behaviors above.
  • This avoids any UI dependency on names and keeps logic centralized.
  1. List options for reason tracking
  • Seed list_options entries for list_id adjreason:
    • option_id MEDICARE_AGGREGATED, title Medicare adjustments aggregated, option_value 1.
    • option_id MEDICAID_SECONDARY_AUTO, title Medicaid secondary auto write-off, option_value 4.
  • Use these as memo prefixes when posting adjustments so reports can filter on them. No schema change required.
  1. Balance computation details
  • computeLineBalance executes:
    • Get fee from billing where pid equals patient, encounter equals encounter, code equals code, modifier equals modifier, activity equals 1, limit 1.
    • Get sums of pay_amount and adj_amount from ar_activity for the same keys where deleted is null.
    • remainder equals fee minus pay_sum minus adj_sum.
    • If the absolute remainder is less than the tolerance, treat it as zero.
  • If no exact modifier match exists, optionally fall back to a code-only match if a single active billing line exists for that code on the encounter.
  1. Idempotency and concurrency guards
  • guardAlreadyPosted signs auto adjustments with a stable memo token:
    • For Medicare: AUTO-MCR-AGG code modifier sessionId.
    • For Medicaid: AUTO-MCD-WO code modifier sessionId.
  • Before inserting, query ar_activity for an existing memo containing the token to prevent duplicates.
  • Wrap posting in a transaction per service method. Use sqlBeginTrans, sqlCommitTrans as in SLEOB.
  1. Unit and integration tests
  • Create PHPUnit unit tests for CrossoverReconciliationService:
    • Given Medicare ERA lines with CO-45, CO-253, and CO-237 amounts, assert aggregate equals sum and memo format correct.
    • Given Medicaid PR zero and partial payment, assert write-off equals remainder and a single ar_activity adjustment entry is created.
    • Exception case where PR greater than zero asserts review flag is added and no write-off is posted.
  • Add integration test stubs for sl_eob_process flow with mock ParseERA outputs.
  1. Configuration toggle and rollout
  • Add a Billing Global setting:
    • Auto-reconcile Medicaid secondary when PR equals zero (default ON).
    • Aggregate Medicare CO adjustments into a single entry (default ON).
  • Respect settings in both ERA and manual code paths so the feature can be turned off if needed during rollout.
  1. Historical correction utility (optional)
  • Provide a CLI script scripts/crossover_reconcile_backfill.php that:
    • Scans encounters with payer_type sequence Medicare then Medicaid, where encounter balance is small and Medicaid PR is zero per ERA data if available.
    • Offers a dry-run and an apply mode to create missing auto write-offs.
  • Only use if needed, since most were manually corrected.
  1. Acceptance criteria with your example
  • Medicare remit example:
    • Posts Medicare payment 45.56.
    • Creates one adjustment entry with amount 12.82 or 13.65 depending on the exact totals shown in remit, with memo listing CO-45, CO-253, CO-237.
    • Leaves PR coins amount for crossover as outstanding.
  • Medicaid remit example:
    • Posts Medicaid payment 5.37.
    • Detects Medicaid PR 0.00.
    • Auto posts write-off for the remaining balance on that code (example remainder 8.99), payer_type 2, memo Medicaid secondary auto write-off (PT RESP 0.00).
    • Final patient balance for that line equals zero.
  • Exceptions are flagged rather than written off when PR is greater than zero.
  1. Deployment steps
  • Implement class and code changes in a feature branch.
  • composer dump-autoload -o
  • Add list_options seed via upgrade or on first use if not present.
  • Test with local morton database using sample encounters that reflect both Medicare and Medicaid remits.
  • Enable the two global toggles and run ERA processing on a test remit, then verify ledger and balances.
  • Review sl_eob_invoice.php manual path by posting a Medicaid secondary payment and confirming automatic write-off applies when checkbox is enabled.
  1. Open questions to confirm before coding
  • Reason text and code: Is the memo text Medicaid secondary auto write-off acceptable, or would you prefer a specific internal reason like Morton for reporting consistency?
  • Global toggles: OK to add two new Billing settings as described?
  • Manual UI: Is adding a PT RESP is 0.00 per remit checkbox acceptable, default checked when payer is Medicaid?
  • Payer aliases: Please confirm the final alias list for Medicare and Medicaid so we can preseed payer_alias_map accordingly.
  • Database name: Confirm the exact database to use for development and test runs.
1 Like

hi @juggernautsei, adding something to the 835 posting script will probably be the easiest solution.

1 Like

@stephenwaite I noticed that the ar_activity table does not have a column for allowed_amount. Is there a reason why it is not there? Can it be added for future compliance in healthcare billing?