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?
- 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.
- 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.
- 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.
- 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.
- 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.
- 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).
- 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.
- For each code line present in form_line:
- If current payer_type equals 2 (Medicaid) and form_ptresp_zero equals true:
- Show an info alert if auto write-offs were applied or if exceptions were flagged.
- Preserve current behavior when the checkbox is unchecked.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.