# Business Rules

## Evidence Labels

Rules marked Observed are enforced or documented in code, tests, or README. Rules marked Inferred describe likely business intent behind implementation behavior.

| ID | Rule | Enforcement point | Exceptions or notes | Evidence quality |
| --- | --- | --- | --- | --- |
| BR-001 | Source Odin report must be an `.xlsx` workbook. | `inspect_inputs` in `src/odin_lunchtab/managed.py` | Other extensions are rejected. | Observed |
| BR-002 | Lunchtab users export must be a `.csv` file. | `inspect_inputs` in `src/odin_lunchtab/managed.py` | Other extensions are rejected. | Observed |
| BR-003 | Odin rows require account type, ID number, deposits, spendings, balance, and student name. | `extract_odin_report` in `src/odin_lunchtab/workflow.py` | Missing fields become malformed exceptions. | Observed |
| BR-004 | Odin student names must use `Surname, FirstName` format. | `split_student_name` in `src/odin_lunchtab/workflow.py` | Invalid names become malformed exceptions. | Observed |
| BR-005 | Monetary values must parse to finite decimals. | `_money_text`, `_decimal`, and `_decimal_or_none` in workflow modules | Invalid values block or are excluded depending on workflow stage. | Observed |
| BR-006 | Lunchtab users export must include `FirstName`, `PreferredName`, `Surname`, `LoginBarcode`, `DefaultFamilyCode`, and `DefaultFamilyBalanceAmount`. | `REQUIRED_LUNCHTAB_HEADERS` in `src/odin_lunchtab/managed.py` | Profile-specific target fields can add required headers. | Observed |
| BR-007 | `OdinBalanceAmount` is inserted immediately after `DefaultFamilyBalanceAmount` in the transfer CSV. | `run_workflow` in `src/odin_lunchtab/workflow.py` | The original Lunchtab rows are preserved with the added balance column. | Observed |
| BR-008 | A duplicate Odin account identity is not auto-matched. | `match_balances_detailed` in `src/odin_lunchtab/workflow.py` | Reason is `duplicate Odin ID Number`. | Observed |
| BR-009 | A crosswalk match must resolve to exactly one Lunchtab target and pass name validation. | `match_balances_detailed` in `src/odin_lunchtab/workflow.py` | Stale, duplicate, or name-failed crosswalks become exceptions. | Observed |
| BR-010 | Identifier matches must pass surname and first/preferred-name validation. | `names_compatible` in `src/odin_lunchtab/workflow.py` | Surnames normalize exactly; first names can match by compatible prefix. | Observed |
| BR-011 | Matching profiles can target LoginBarcode, ExternalId, or EmailUsername only. | `APPROVED_TARGET_FIELDS` in `src/odin_lunchtab/profiles.py` | Unsupported targets are rejected. | Observed |
| BR-012 | Email username matching uses the complete local part before a single `@`, and optional domains must be exact and valid. | `parse_email_address`, `valid_email_domain`, and `_email_candidate_allowed` | Malformed or disallowed emails do not produce candidates. | Observed |
| BR-013 | A matching profile cannot contain duplicate enabled rule names or duplicate crosswalk source identities. | `MatchingProfile.validate` in `src/odin_lunchtab/profiles.py` | Validation raises an error. | Observed |
| BR-014 | The Legacy Default profile is read-only and cannot be modified or deleted. | `save_profile` and `delete_profile` in `src/odin_lunchtab/profiles.py` | It preserves exact ID-to-LoginBarcode matching plus name fallback. | Observed |
| BR-015 | Multiple matching rules resolving to different Lunchtab users are quarantined. | `match_balances_detailed` in `src/odin_lunchtab/workflow.py` | Reason is `matching rules resolved to different Lunchtab users`. | Observed |
| BR-016 | Multiple Odin records matching one Lunchtab user are quarantined. | `match_balances_detailed` in `src/odin_lunchtab/workflow.py` | Prevents multiple balance writes to one user row. | Observed |
| BR-017 | `no Lunchtab match` records are excluded from the focused manual-review exception file. | `run_workflow` in `src/odin_lunchtab/workflow.py` | They remain in the full exceptions report. | Observed |
| BR-018 | Reconciliation audit control must satisfy valid source dollars equals matched dollars plus valid exception dollars. | `write_reconciliation_audit_control` in `src/odin_lunchtab/audit_control.py` | A failure raises an error in `run_workflow`. | Observed |
| BR-019 | Managed runs publish only after output files and manifest are created. | `run_managed_workflow`, `run_initial_balances_workflow`, and `run_manual_reconciliation_workflow` | Staging folders are removed on failure. | Observed |
| BR-020 | Manual reconciliation edited transfer must have the same headers and row count as the original transfer. | `audit_manual_reconciliation` in `src/odin_lunchtab/manual_reconciliation.py` | Structural mismatch is rejected. | Observed |
| BR-021 | Filling a previously blank `OdinBalanceAmount` is treated as a manual resolution. | `audit_manual_reconciliation` | Severity is `REVIEW`. | Observed |
| BR-022 | Invalid manual balance amounts block downstream use. | `audit_manual_reconciliation` | Summary status becomes `BLOCKED`. | Observed |
| BR-023 | Changing automated balances, family codes, identity fields, or other fields requires review. | `audit_manual_reconciliation` | Summary status becomes `REVIEW` unless invalid amounts block. | Observed |
| BR-024 | InitialBalances export must contain exactly `FamilyName,FamilyCode,Amount` in order. | `inspect_initial_balances_inputs` in `src/odin_lunchtab/initial_balances.py` | Noncanonical headers are rejected. | Observed |
| BR-025 | InitialBalances transfer aggregates populated Odin balances by case-sensitive `DefaultFamilyCode`. | `_analyze_initial_balances` in `src/odin_lunchtab/initial_balances.py` | Multiple source rows can update one family. | Observed |
| BR-026 | Blank target InitialBalances amounts are treated as zero. | `_decimal(..., blank_as_zero=True)` | Only when parsing target amount. | Observed |
| BR-027 | InitialBalances import file is omitted when blocking exceptions exist. | `process_initial_balances` in `src/odin_lunchtab/initial_balances.py` | Audit and exception artifacts are still produced. | Observed |
| BR-028 | InitialBalances control totals must pass before publication. | `process_initial_balances` and `write_initial_balances_audit_control` | Failure raises an integrity error. | Observed |
| BR-029 | CSV reads support UTF-8, Windows-1252, and BOM-marked UTF-16, and reject binary-looking or corrupted text. | `_decode_csv_bytes` and `_validate_csv_text` in `src/odin_lunchtab/workflow.py` | Generated CSVs use UTF-8 with BOM. | Observed |
| BR-030 | Source files are not modified by the application. | README and workflow behavior | Outputs are written to separate result folders. | Observed |
| BR-031 | Logs and manifests should avoid student-level row data and absolute source paths. | README and manifest structures in `managed.py` and `initial_balances.py` | File hashes and names are stored for provenance. | Observed |
| BR-032 | Staff should use generated imports only after reviewing summaries and resolving blocking exceptions. | README and run summary next-action text | Operational approval outside the app remains a human activity. | Inferred |
