---
title: Automated bill audit orchestration
summary: Retrieves a bill, generates/enriches its audit data, drives bill/service edits, MCO determination, and the audit rule engine, then saves the result.
status: draft
owner: SAIF Doc Gen
last_reviewed: 2026-07-30
library: SAIF.Business.Claim.Bill.AutomatedAudit
reference_type: package
operation_id: OP-AA-01
trigger: Caller invokes BillAuditEngine.AuditBill(BillID, ForceCompleteReAudit, ReadOnlyMode)
---

# Automated bill audit orchestration

`BillAuditEngine.AuditBill` is the single entry point for the automated bill-audit workflow. It retrieves a bill and its prior audit (if any), validates the bill and its status, generates or refreshes audit-specific data by enriching every service line with claim, provider, and national-drug-code context, delegates to the bill-edit, service-edit, and MCO-determination operations, invokes the rule-based audit engine, and finally saves the result unless running in read-only mode.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-AA-01 |
| **Trigger** | Caller invokes `BillAuditEngine.AuditBill(inBillID, ForceCompleteReAudit, ReadOnlyMode)` |
| **Inputs** | `BillID` (Decimal), `ForceCompleteReAudit` (Boolean, default `False`), `ReadOnlyMode` (Boolean, default `False`), `IDataPortalInfo` supplied at engine construction |
| **Outputs** | `AuditResults` enum value in `AuditResult`; explanatory text in `AuditResultMessage`; populated `clsBillAudit` exposed as `ProcessedAuditData`; persisted `CBP_BILL`/audit data unless `ReadOnlyMode` is set; timing/diagnostic entries in `TrailMessages` |
| **Primary components** | `BillAuditEngine.AuditBill`, `BillAuditEngine.RetrieveBill`, `BillAuditEngine.GenerateAuditData`, `BillAuditEngine.SaveAuditData`, `BillAuditEngine.GetProvider`, `BillAuditEngine.GetClaim`, `BillAuditEngine.GetNationalDrugCode` |
| **Entry point** | `Business\SAIF.Business.Claim.Bill.AutomatedAudit\BillAuditEngine.vb` |

## Steps

1. **Reset run state.** Clear the audit trail and broken-rules collection, and record the requested `ReadOnlyMode` and `ForceCompleteReAudit` flags. Start an audit-trail group tagged `"Audit Bill (<BillID>)"` and log the mode flags and portal ID.
2. **Initialize result state.** Set `AuditResult = Success`, clear `AuditResultMessage`, clear any prior `AuditExceptions`, and clear `ProcessedAuditData`.
3. **Retrieve the bill.** `RetrieveBill(inBillID)` calls `clsBillAudit.GetBillAudit(DPI, inBillID)`.
   - Sub-step: `GetBillAudit` issues `IDataPortalInfo.DataAccessLayer.RetrieveObject` against a criteria keyed by `BillID` and returns the hydrated `clsBillAudit`, which lazily exposes `Claim`, `BillingProvider`, `MCOInformation`, `History`, and `PharmacyEligibilityRange`.

   > **Origin**: `clsBillAudit` extends `clsBill` and its persistence surface (`Save`, `OnSave`, `OnSaveComplete`, `BeforeUpdate`, `GenerateStatusRecord`) is implemented by [Bill CRUD](../../saif-business-claim-bill/operations/bill-crud.md) and [Audit and audited services](../../saif-business-claim-bill/operations/audit-and-audited-services.md) in `SAIF.Business.Claim.Bill`.
   - If `ReadOnlyMode` is requested, call `Bill.SetReadOnlyMode()` immediately after retrieval.
4. **Validate the bill.** If `Bill.IsValid` is `False`: set `AuditResult = InvalidBill` ([BR-001](../business-rules.md#br-001-bill-must-be-valid-for-audit)), copy `Bill.ErrorMessages` into `AuditResultMessage`, propagate the bill's broken rules onto the engine, clone the bill into `ProcessedAuditData`, and return `True` — no data is saved.
5. **Validate bill status.** Unless `ForceCompleteReAudit` is set, require `Bill.CurrentBillStatus` to be `Entered`, `Reviewed`, `Audited`, or `Approved` ([BR-002](../business-rules.md#br-002-bill-status-must-be-valid-for-audit)).
   - On any other status: set `AuditResult = InvalidStatus`, message `"Incorrect status for audit: <status>"`, clone the bill, and return `True`.
6. **Generate/enrich audit data (`GenerateAuditData`).**
   1. If the bill is already audited and this is not a forced re-audit, clear the prior audit's messages so stale messages are not shown alongside the new pass.
   2. Decide whether the previous audit is eligible for deletion and regeneration:
      - Not eligible (already audited, status is not `Entered`, and not forced): keep the existing audit data unchanged.
      - Eligible (forced re-audit on an already-audited bill, status `Entered`, or no prior audit): unless read-only, delete the prior audit (`DeletePreviousAudit` — deletes `Bill.Audit` and saves the deletion via `SaveChildObject`; returns `False` and aborts on error) and call `Bill.GenerateAudit()` to build a fresh audit header and audited-service rows.

         > **Origin**: `GenerateAudit`/`OnGenerateAudit`/`OnGenerateAuditService` are implemented on `clsBillAudit`, which overrides base behavior from `clsBill`/`clsAudit` in [Audit and audited services](../../saif-business-claim-bill/operations/audit-and-audited-services.md) (`SAIF.Business.Claim.Bill`). The override sets `Audit.AuditReceiptDate`, `Audit.PrintEobFlag` (based on a hard-coded non-EOB provider tax-ID list), and applies interpreter-service (`D0004`/`D0006`) minimum-quantity and 15-minute-rounding normalization to each newly generated audited service.
      - On `GenerateAudit()` failure: return `False` (propagates to step 7 below).
   3. If data generation succeeded so far, enrich the bill:
      - If `Bill.ClaimID` is populated, retrieve claim context via `GetClaim(ClaimID)`.
        - Sub-step: `GetClaim` calls `clsClaimData.GetData(DPI, ClaimID)`; if the returned claim `IsReplaced`, it re-retrieves using `Claim.ReplacedClaimID` and adds an informational message `"Claim <old> has been merged to <new>."`.

          > **Origin**: Claim retrieval and the merge/replacement chain are implemented by [Retrieve claim summary data](../../saif-business-view-claim/operations/claim-summary-and-detail-retrieval.md) in `SAIF.Business.View.Claim`.
      - If `Bill.ProviderLocationID` is populated, retrieve billing-provider context via `GetProvider(ProviderLocationID)`, caching results per provider ID for the run.

        > **Origin**: Provider location retrieval is implemented by [Retrieve provider location](../../saif-business-view-provider/operations/provider-location-retrieval.md) in `SAIF.BOF.Business.View.Provider`.
      - For each billed service, build a `clsBillAuditService` wrapper (`GetAuditService` locates or generates the matching audited-service row) and enrich it:
        - Resolve servicing/prescribing provider location via `GetProvider` when a non-zero location ID is present.
        - For pharmacy-type services, resolve the national drug record via `GetNationalDrugCode`, trying the existing `NationalDrugCode` first and falling back to `ServiceCode` for new records.

          > **Origin**: National drug code retrieval and price history are implemented by [National drug validation](../../saif-business-claim-bill/operations/national-drug-validation.md) in `SAIF.Business.Claim.Bill`.
        - For `InpatientHospital` bills, force each service's `ServiceDate`/`ServiceEndDate` to the bill's statement from/thru dates when those are populated.
      - For non-`InpatientHospital` bills with at least one service, recompute the bill's `StatementFromDate`/`StatementThruDate` as the min/max of the audited services' dates.
   4. Return `True` if enrichment completed; `False` propagates to the caller as a generation failure.
7. **Handle generation failure.** If step 6 returns `False`: set `AuditResult = FailAuditGeneration`, message `"Failed to generate audit data."`, clone the bill, and return `False` (this is one of the three cases where `AuditBill` reports failure to the caller).
8. **Guard against an empty bill.** If `Bill.Services.Count = 0`, call `BillAuditor.GenerateError(Bill, "Bill data - no services to audit")` (an informational audit message, not a fatal error at this point).
9. **Run bill edit validation.** Delegate to [Bill edit validation](bill-edit-validation.md) via `BillEditor.EditBill(Bill)` ([BR-003](../business-rules.md#br-003-bill-edits-must-pass)).
   - On failure: set `AuditResult = FailBillEdits`, message from `BillEditor.ErrorMessages`, and remove any audited-service rows that were newly generated for this run (`IsNew`) so they are not persisted with an invalid bill.
10. **Run service edit and adjustment per service, when bill edits pass.** For each item in `Bill.ServiceData`, delegate to [Service edit and adjustment](service-edit-and-adjustment.md):
    - Call `ServiceEditor.EditService(Bill, Service)`; a `False` result marks `PassServiceEdits = False` for this run.
    - If the service `IsAuditable` (`ReauditCode = "R"`): remove any prior adjustments for that service, then call `ServiceEditor.AdjustService(Bill, Service)`; a `False` result also marks `PassServiceEdits = False`.
    - Propagate broken rules from `ServiceEditor` to the engine after each service.
    - After all services: sum `Bill.Audit.TotalAllowedAmount` and `TotalMcoAdminFeeAmount` across `Bill.ServiceData` ([BR-004](../business-rules.md#br-004-service-edits-must-pass)).
11. **Determine whether there is anything to audit.** If `Bill.ServiceData.ForReAudit.Count > 0`:
    - If service edits failed: set `AuditResult = FailServiceEdits`.
    - Otherwise:
      - Unless `Bill.BillType = WorkerReimbursement`, delegate to [MCO determination](mco-determination.md) via `MCOManager.DetermineMCO(Bill)`.
      - Delegate to [Bill audit rule execution](bill-audit-rule-execution.md) via `BillAuditor.AuditBill(Bill)`.
        - On failure: set `AuditResult = FailAuditCheck`, message from `BillAuditor.ErrorMessages`.
12. **Propagate remaining broken rules.** Copy broken rules from `BillEditor` and `BillAuditor` onto the engine's rule collection.
13. **Save the result (`SaveAuditData`).**
    - If `ReadOnlyMode`: call `Bill.GenerateStatusRecord()` only (builds an in-memory `AUD` status row and routing target) and log `"Process Only Mode - Data Not Saved!"` — no database write occurs.
    - Otherwise, if `Bill.IsDirty` or `AuditedServiceCount = 0`, call `Bill.Save()`.

      > **Origin**: `Save()`/`OnSave()`/`OnSaveComplete()`/`BeforeUpdate()` cascade the bill header (`CBP_BILL` via an inline update binding `:BillTotalAmount`, `:ClaimID`, `:ClaimNumber`, `:ClaimCheckValue`, `:StatementFromDate`, `:StatementThruDate`, `:DRGCode`, `:UpdateUser`, `:UpdateDate`, `:BillID`* against [CBP_BILL](../database-tables.md#cbp_bill)) together with `Audit`, `StatusHistory`, and `Diagnosis` child collections, implemented by [Bill CRUD](../../saif-business-claim-bill/operations/bill-crud.md) and [Audit and audited services](../../saif-business-claim-bill/operations/audit-and-audited-services.md) in `SAIF.Business.Claim.Bill`. Before saving, `OnSave()` resets any non-blank `ReauditCode` back to a single blank so re-audited services are not repeatedly re-flagged on the next run.
    - On save failure: set `AuditResult = FailAuditSave`, message from `Bill.ErrorMessages` (one of the three cases where `AuditBill` reports failure to the caller).
14. **Clone and return.** Store the final bill state in `ProcessedAuditData` (`CloneBill`). The whole method body is wrapped in a `Try/Catch`; any unhandled exception sets `AuditResult = EnvironmentError`, captures the exception in `AuditExceptions`, and is followed by the `Finally` block stopping the audit-trail group. The method returns `False` only for `EnvironmentError`, `FailAuditGeneration`, or `FailAuditSave`; all other outcomes (including `InvalidBill`, `InvalidStatus`, `FailBillEdits`, `FailServiceEdits`, and `FailAuditCheck`) return `True`.*

## Process flow

```mermaid
flowchart TD
	Start([AuditBill invoked]) --> ResetState[Reset trail and rule state]
	ResetState --> Retrieve[RetrieveBill: clsBillAudit.GetBillAudit]
	Retrieve --> Valid{Bill.IsValid?}
	Valid -->|No| InvalidBill([AuditResult = InvalidBill;<br>return True, no save])
	Valid -->|Yes| StatusCheck{Status eligible<br>or ForceCompleteReAudit?}
	StatusCheck -->|No| InvalidStatus([AuditResult = InvalidStatus;<br>return True])
	StatusCheck -->|Yes| Generate[GenerateAuditData:<br>delete/regenerate audit,<br>enrich claim/provider/NDC]
	Generate -->|Failed| FailGen([AuditResult = FailAuditGeneration;<br>return False])
	Generate -->|Ok| BillEdits[Bill edit validation]
	BillEdits -->|Fatal error| FailBillEdits([AuditResult = FailBillEdits;<br>discard new audit rows])
	BillEdits -->|Ok| ServiceLoop[Service edit and adjustment<br>per service line]
	ServiceLoop --> ForAudit{ForReAudit.Count > 0?}
	ForAudit -->|No| Save
	ForAudit -->|Yes, failed| FailServiceEdits([AuditResult = FailServiceEdits])
	ForAudit -->|Yes, passed| MCO{BillType =<br>WorkerReimbursement?}
	MCO -->|No| MCODetermine[MCO determination]
	MCO -->|Yes| AuditRun
	MCODetermine --> AuditRun[Bill audit rule execution]
	AuditRun -->|Failed| FailAuditCheck([AuditResult = FailAuditCheck])
	AuditRun -->|Ok| Save[SaveAuditData]
	FailServiceEdits --> Save
	FailAuditCheck --> Save
	FailBillEdits --> Save
	Save -->|ReadOnly| StatusOnly([GenerateStatusRecord only;<br>no DB write])
	Save -->|Failed| FailSave([AuditResult = FailAuditSave;<br>return False])
	Save -->|Ok| Clone[Clone bill into ProcessedAuditData]
	StatusOnly --> Clone
	Clone --> Done([Return True/False<br>per AuditResult])
```

## Failure and edge-case behavior

- An invalid bill or ineligible status is not treated as a hard failure: `AuditBill` returns `True`, and callers must check `AuditResult` to detect these conditions.
- Any exception anywhere in the pipeline is caught, recorded in `AuditExceptions`, and reported as `EnvironmentError`; the audit-trail group is always stopped via `Finally` regardless of outcome.
- If `GenerateAuditData` fails partway through deleting the previous audit, the method returns `False` immediately without attempting to regenerate — a prior audit may already be deleted with no replacement saved.*
- `ReadOnlyMode` never calls `Bill.Save()`; only an in-memory `AUD` status record is produced, so no `ReadOnlyMode` run can persist changes even if later logic assumes a save occurred.
- Only `EnvironmentError`, `FailAuditGeneration`, and `FailAuditSave` cause `AuditBill` to return `False`; all business-rule failures return `True`, which can be surprising for callers that treat the boolean return as a simple success/failure flag.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [SQL catalog](../sql/index.md)
- [Database tables](../database-tables.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming conventions, or
> contextual evidence where direct confirmation was not available in source artifacts.

## Sources
- `Business\SAIF.Business.Claim.Bill.AutomatedAudit\BillAuditEngine.vb`
- `Business\SAIF.Business.Claim.Bill.AutomatedAudit\clsBillAudit.vb`
- `Business\SAIF.Business.Claim.Bill.AutomatedAudit\clsBillAuditService.vb`
- `Business\SAIF.Business.Claim.Bill.AutomatedAudit\AuditUtilities\AuditTrailManager.vb`
