Metadata-Version: 2.4
Name: ksero-payer-rules
Version: 0.3.0
Summary: Payer-specific rules and validation for insurance card OCR
Home-page: https://github.com/DV1-321/ksero-payer-rules
Author: David Fergins
Keywords: insurance ocr healthcare payer eligibility intake
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Healthcare Industry
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Text Processing
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: author
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: keywords
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-python
Dynamic: summary

![PyPI](https://img.shields.io/pypi/v/ksero-payer-rules)
![Python](https://img.shields.io/pypi/pyversions/ksero-payer-rules)
![License](https://img.shields.io/github/license/DV1-321/ksero-payer-rules)

# Ksero Payer Rules

Payer-specific rules and validation for medical, dental, and vision insurance cards.

Reads the text off a card — usually from OCR — and returns the carrier, the
specialty, and the identifiers, with a format check on each.

**The contract: every field is either found in the text or left `None`.** Nothing
here invents an identifier, and nothing reports a field as validated when it was
never read. For an intake product feeding a claim, a plausible wrong member ID is
worse than an empty one: an empty field gets queried by a person, a wrong one
gets billed, denied, and chased.

## Installation

```bash
pip install ksero-payer-rules
```

## Usage

```python
from ksero_payer_rules import extract_fields, validate_fields

text = """
UnitedHealthcare Choice Plus
Member ID: 123456789
Group Number: IB7654
"""

fields = validate_fields(extract_fields(text))
# {
#   'payer': 'UnitedHealthcare',
#   'specialty': 'medical',
#   'member_id': '123456789',
#   'group_number': 'IB7654',
#   'plan_name': 'Choice Plus',
#   'policy_holder_name': None,
#   'member_id_validation': True,
#   'group_number_validation': True,
#   'extra': {},
# }
```

A card the reader cannot make out returns the fields it could not find as `None`,
and the validations as `False`:

```python
extract_fields("UnitedHealthcare")
# {'payer': 'UnitedHealthcare', 'member_id': None, 'group_number': None, ...}
```

## What it does

- **Carrier detection** on word boundaries, so "Guardian: Jane Doe" on a
  paediatric card names a parent rather than a dental carrier.
- **Specialty detection** (medical / dental / vision) from benefit words first
  and carrier names second, so a MetLife *Vision* card is not read as dental.
- **Identifier extraction** for member ID, group number, plan name and policy
  holder, with a shape test that rejects labels and stray words — `Group No.:`
  yields the number after it, and `GROUP HEALTH PLAN` yields nothing.
- **Format validation** per carrier for UnitedHealthcare, VSP, Anthem/BCBS,
  Medicare, TRICARE and CHAMPVA, and a shape check for everyone else.

## API

| Function | Returns |
| --- | --- |
| `extract_fields(text)` | the full field dict |
| `validate_fields(data)` | the same dict plus `member_id_validation` / `group_number_validation` |
| `apply_payer_rules(payer, text, baseline)` | `baseline` filled in; `payer` may be `None` |
| `detect_payer(text)` | canonical carrier name, or `None` |
| `detect_specialty(text)` | `"medical"`, `"dental"`, `"vision"` or `"unknown"` |
| `extract_member_id(text)` | the identifier, or `None` |
| `extract_group_number(text)` | the group number, or `None` |
| `extract_plan_name(text)` | the plan, or `None` |
| `extract_policy_holder(text)` | the subscriber name, or `None` |
| `extract_dependents(text)` | `[{"type": "Spouse", "name": "M BROWN"}, ...]` |
| `extract_copays(text)` | `{"exam_copay": "$10", "materials_copay": "$25"}` |
| `normalize_date(value)` | ISO date, the original if date-shaped, else `None` |
| `validate_member_id(payer, member_id)` | `bool` |
| `validate_group_number(payer, group_number)` | `bool` |
| `check_member_id(payer, member_id)` | `{"is_valid", "reason", "score_bonus"}` |
| `check_group_number(payer, group_number)` | same shape |

Carrier-specific reading is applied where the general rule would be wrong: the
Medicare Beneficiary Identifier has its own shape, TRICARE and CHAMPVA carry a
DoD/beneficiary number, and copays are only read off a vision card — a dollar
figure after the word "exam" on a medical card is not a materials copay.

## Development

```bash
pip install -e ".[dev]"
pytest
```

## Upgrading to 0.3.0

The payer rules, specialty detection, identifier validation and date handling
were duplicated in the `ksero-ai` application. Both copies drifted and each bug
had to be found twice, so the richer behaviour — Medicare MBI shape, TRICARE and
CHAMPVA identifiers, dependents, VSP plan names and copays — moved here and the
application now re-exports from this package.

One behaviour changed: `detect_specialty` returns `"unknown"` for text it cannot
place, where 0.2.0 returned `"medical"`. Guessing a specialty asserts something
about a card nothing was read from, and it left a confidence score unable to tell
a medical card from an unreadable one.

## Upgrading from 0.1.x

**0.1.0 and 0.1.1 do not work.** Both shipped `specialy.py` while `__init__.py`
imported `.specialty`, so `import ksero_payer_rules` raised `ModuleNotFoundError`
on every install. Behind that, `apply_payer_rules` was a placeholder that
returned fixed identifiers — any text containing "UHC" came back with member ID
`999999876` — and the package's only test asserted those same constants, so it
passed while the library fabricated data.

0.2.0 fixes the import, replaces the placeholder with real extraction, and adds
a suite that reads values out of card text instead of restating them.

## License

MIT
