---
title: Expense lifecycle and payment approval
summary: Creates, validates, amends, approves, and pays EAIP purchase and wage-subsidy expenses through the shared two-table expense base class.
status: draft
owner: SAIF Doc Gen
last_reviewed: 2026-07-30
library: SAIF.Business.Claim.EAIP
reference_type: package
operation_id: OP-EAIP-02
trigger: Caller creates or retrieves a purchase or wage subsidy on a service period, edits it, optionally amends or corrects it, then calls Save() which runs the approval gate
---

# Expense lifecycle and payment approval

Purchases and wage subsidies are two faces of one record. Both derive from the abstract `clsExpense`, which owns the shared `EAIP_EXPENSE` row — status, received date, paid amount, write-off, adjustment, amendment text — while the subclass owns the type-specific row in `EAIP_PURCHASE` or `EAIP_WAGE_SUBSIDY`. Because the validation set, the amendment/correction mechanism, the authority-based approval gate, and the save cascade are all implemented once on the base class, they are documented here as a single operation. The wage-subsidy-specific worksheet arithmetic is documented separately in [Wage subsidy worksheet and calculation](wage-subsidy-worksheet.md); the credit-card supervisor queue is documented in [Credit-card payment approval](credit-card-payment-approval.md).

| Field | Value |
| --- | --- |
| **Operation ID** | OP-EAIP-02 |
| **Trigger** | Caller invokes `clsPurchase.NewPurchase(dpi, period)` or `clsWageSubsidy.NewWageSubsidy(dpi, period)`, or retrieves the period's `Purchases` / `WageSubsidies` collections, edits the expense, optionally calls `AmendExpense`/`CorrectExpense`/`Approve`, then calls `Save()` |
| **Inputs** | `IDataPortalInfo` (acting user and payment authority), owning `clsPeriod`, expense type code, received date, paid amount, item cost, order/delivery dates, write-off code and amount, adjustment code and amount, denied code, amendment text, credit-card transaction date, `UpdateNum` |
| **Outputs** | Persisted `EAIP_EXPENSE` row plus the matching purchase or wage-subsidy row; appended expense-status history rows; status transitions to `Pay`, `PendingSupervisorApproval`, `Paid`, `Reimbursed`, `Amended`, or `Corrected`; warning messages explaining why supervisor approval is needed; DB contention errors when a row count is not one |
| **Primary components** | `clsExpense` (`Create`, `AddInsertUpdateParms`, `BeforeInsert`/`AfterInsert`, `BeforeUpdate`/`AfterUpdate`, `BeforeDelete`/`AfterDelete`, `CreateAmendment`, `CreateCorrection`, `Approve`, `NeedsApproval`, `CanPay`, `ValidatePayableExpense`, `OnSave`, `ResetPaidExpense`), `clsExpenseCollection(Of T)` (`CanPay`, `CanBill`, `ValidatePayableExpenses`, `Approve`, `Disapprove`, `Remove`, `CurrentOnly`), `clsPurchase`, `clsExpenseHistory` |
| **Entry point** | `Business\SAIF.Business.Claim.EAIP\Period\clsExpense.vb` |

## Steps

1. **Create the expense.** `NewPurchase(inDPI, inPeriod)` (or the wage-subsidy equivalent) builds a type-specific criteria and calls the portal's create, landing in `clsExpense.Create()`. That method takes the next value of `eaip_expense_seq`, sets the status code to the `New` code from the EAIP expense-status code table, sets the status date to now, records the create user and timestamp, and initializes the update info to an empty user with update number zero ([SQL-007](../sql/SQL-007.md), [EAIP_EXPENSE_SEQ](../database-tables.md#eaip_expense_seq)). The subclass then stamps the owning period identifier onto the new object.
2. **Edit the shared expense fields.** The caller sets the expense type code, received date, paid amount, expense text, and — where applicable — the write-off code and amount, the adjustment code, amount, and date, the denied code, and the amendment explanation text. Every setter routes through the change-detection helper so the object is marked dirty only on an actual change.
3. **Edit the purchase-specific fields.** For a purchase the caller additionally sets the item description, order date, delivery date, item cost, vendor bill reference, and credit-card transaction date. Setting the credit-card transaction date has a side effect: the credit-card user is set to the acting user's identifier in upper case when a date is supplied, and cleared when the date is removed. That pairing is what the supervisor credit-card queue later selects on ([BR-PROC-003](../business-rules.md#br-proc-003-return-only-credit-card-purchases-paid-for-the-selected-eaip-user)).
4. **Validate the shared rules.** `AddBusinessRules` on the base class registers the following, all evaluated when the caller reads `IsValid`:
   - `Rule_HasInitiateAuthority` — the acting user must hold initiate authority for the paid amount ("Error: You are not authorized to initiate payments.").
   - `Rule_ValidExpenseType` — an expense type is required; `Rule_Payable` blocks paying an expense typed as referral or justification.
   - `Rule_ValidPaidAmount` — a payable amount is required, with the message text supplied by the subclass ("Paid amount is required." for a purchase, "Subsidy amount is required." for a wage subsidy).
   - `Rule_HasReceivedDate`, `Rule_ValidReceivedDate`, `Rule_ValidFinalReceivedDate` — a received date is required, and both the received date and the last-information-received date must fall on or after the date of injury and not in the future.
   - `Rule_ValidPeriodEffDate` — the service period must have a begin date before an expense can be paid.
   - `Rule_ValidUpdateStatus` — a paid expense cannot be updated; `Rule_ValidDelete` blocks deletion for statuses outside the deletable set.
   - `Rule_ValidWriteOffReason` / `Rule_ValidWriteOffAmount` — the adjust-bill reason and the adjust-bill amount must be supplied together.
   - `Rule_ValidDeniedReason` / `Rule_ValidDeniedStatus` — a denied reason is required if and only if the status is denied.
   - `Rule_HasAdjustmentCode` — an adjustment reason is required when an adjustment amount exists, except for the conversion account `EAIPCONV`, which is bypassed so historical converted records validate.
   - `Rule_HasAmendmentText` — amendment/correction text is required when paying an amended or corrected expense.
5. **Validate the purchase-specific rules.** `clsPurchase` adds `Rule_ValidItemCost` (the paid amount may not exceed the item cost unless the expense is an amendment or is not in `Pay` status), `Rule_ValidOrderDate1` (the order date may not precede 01/01/2000), `Rule_ValidOrderDate` (the order date must fall inside the service dates unless the expense carries a non-zero adjust-bill amount), `Rule_CreditCardDate` (the credit-card transaction date must fall on or after the date of injury and not in the future), `Rule_ValidDescription` (an item description is required to pay), and `Rule_ValidModification` (a collection may not simultaneously hold amended, billable, and corrected purchases).
6. **Amend or correct.** `CreateAmendment()` and `CreateCorrection()` are identical in mechanism: enable copying, clone the expense, give the clone a fresh criteria and a fresh identifier through `Create()`, assign it a new GUID, point its `OriginalExpenseID` at the source expense, clear its paid date, and then flip the *source* expense to the `Amended` or `Corrected` status with a status date of now. The collection helpers `AmendExpense`/`CorrectExpense` do this and add the clone to the collection in one call. Amendment ordering is derived rather than stored: the sort order of an amendment is its original's sort order plus one-tenth, so amendments sort immediately beneath the expense they amend.*
7. **Compute the derived payment amounts.** `NetPaymentAmount` returns the adjustment amount for an amendment and the paid amount otherwise, and `AmountToBill` is `NetPaymentAmount + WriteOffAmount`. These two derived values, not the raw paid amount, are what the billing operation later accumulates.
8. **Run the item-level approval gate.** `CanPay()` short-circuits to true when the acting user holds both initiate and approval authority for the paid amount, or when the expense already carries supervisor approval. Otherwise it returns the negation of `NeedsApproval()`, which adds one warning per triggered condition and returns true if any fired:
   - initiate authority is insufficient for the paid amount ([BR-PROC-005](../business-rules.md#br-proc-005-expense-payments-over-the-user-authority-level-require-supervisor-approval));
   - the expense is an amendment whose adjustment amount changed and the user lacks approval authority;
   - the expense is not an amendment but carries a non-zero adjustment amount;
   - the expense carries a non-zero adjust-bill (write-off) amount;
   - the amount to bill is positive and the period expiration date is older than twelve months and thirty days ("past one year and 30 days");
   - the claim is federal or inmate jurisdiction;
   - the paid amount exceeds the expense type's limit amount — this one raises no message because the warning line is commented out, so the approval requirement appears without an explanation.*
   `clsPurchase` overrides `NeedsApproval` so that a purchase carrying a credit-card transaction date and sitting in `Pay` status always requires supervisor approval, before any of the base conditions are evaluated ([BR-PROC-006](../business-rules.md#br-proc-006-credit-card-purchases-always-require-supervisor-approval)).
9. **Run the collection-level approval gate.** `clsExpenseCollection(Of T).CanPay()` runs only when at least one item sits in `Pay` status and adds two further collection-wide conditions:
   - **Minimum payment total.** When the summed paid amount is below $100 the collection calls `CanBill`, an inline query that first sums `EAIP_BILLING.BILLED_AMOUNT` for the period and, if that is zero, sums `PAID_AMOUNT` from the sibling expense table (purchases when the collection holds wage subsidies, wage subsidies when it holds purchases) for expenses in `PAY` or `PAID` status. If neither path clears the threshold the collection warns "Supervisor approval is needed when payment total is less than $100.00." ([BR-PROC-009](../business-rules.md#br-proc-009-expense-payment-totals-under-100-require-supervisor-approval)).
   - **Expense-type limits.** Items in amendable, `Pay`, or `Billed` status are grouped by expense type; a group whose summed paid amount exceeds the type's limit amount (a limit of zero meaning "no limit") triggers "Supervisor approval needed when total payable amount greater than limit amount for the expense type." Equipment and worksite-modification types are treated as one combined group sharing the highest of their two limits.
   Finally the collection propagates the broken rules of every child whose own `CanPay` is false, so the caller sees the item-level reasons on the collection.
10. **Apply the gate on save.** `OnSave` calls `ValidatePayableExpense` on the item and `ValidatePayableExpenses` on the collection. The collection evaluates `CanPay()` exactly once, before any status changes, then demotes every unapproved `Pay` item to `PendingSupervisorApproval` when the gate failed. The item-level version adds an auto-completion path: an amendment sitting in `Pay` status with a zero adjustment amount is promoted straight to `Paid` with today's paid date, because nothing actually changed and no supervisor review is warranted.
11. **Approve.** `Approve()` acts only on an expense in `PendingSupervisorApproval`. It requires the acting user to hold approval authority for the paid amount; if not, it adds the warning "Amount to pay is over your authority level." and returns false, leaving the status untouched. On success it marks the expense supervisor-approved and resolves the terminal status:
    - amendment with a non-positive adjustment amount and a zero amount to bill → `Reimbursed` (nothing left to collect from DCBS);
    - amendment with a non-positive adjustment amount and a non-zero amount to bill → `Paid`, paid date today;
    - non-amendment whose payment amount is zero → `Paid`, paid date today;
    - otherwise → `Pay`, leaving the expense in the payment queue.
    The collection's `Approve()` runs this over every pending item and reports success only when nothing is left pending; `Disapprove()` pushes every pending item back to `New`.
12. **Insert.** For a purchase, `BeforeInsert` first calls the base implementation, which builds a request against `pk82500_eaip.eaip_expense_insert` and binds the shared expense columns plus the payment-instruction block (payee type, policy location, person identifier, payment text, attention line, special handling, issue date, payee name, and a four-line address). The subclass then appends a second request against `pk82500_eaip.eaip_purchase_insert` binding the purchase columns, so a single portal request carries both statements ([Details](../procedures/pk82500_eaip.md#eaip_expense_insert), [Details](../procedures/pk82500_eaip.md#eaip_purchase_insert)). Amendments bind a typed null rather than zero for the original expense identifier, and a never-paid expense binds a typed null date.
13. **Check the insert and cascade.** Both `AfterInsert` implementations read the package's `RowsAffected` output parameter. The base adds `InsertError` with `Insert Failed - <type name>` when it is not one and otherwise saves the expense-status history collection; the purchase override adds its own `Insert Failed - Purchase` for the purchase row. History rows (`clsExpenseHistory`, derived from `clsHistoryBase`) record the status code, status date, and denied code for each transition and are read back newest first.
14. **Update.** `BeforeUpdate` binds `LastUpdateNum` from the current update info, replaces the update info with the acting user, now, and the incremented number, then binds the rest — the same old-number-compared, new-number-written pattern used across the library — against `pk82500_eaip.eaip_expense_update` and, for a purchase, `pk82500_eaip.eaip_purchase_update` ([Details](../procedures/pk82500_eaip.md#eaip_expense_update), [Details](../procedures/pk82500_eaip.md#eaip_purchase_update)). `AfterUpdate` applies the row-count check on both statements and adds a DB contention error on failure.
15. **Delete.** `BeforeDelete` binds the expense identifier and the last update number to a delete statement whose name is composed from the subclass table name — `pk82500_eaip.eaip_purchase_delete` or `pk82500_eaip.eaip_wage_subsidy_delete` — while the base criteria also exposes the generic expense delete ([SQL-048](../sql/SQL-048.md), [Details](../procedures/pk82500_eaip.md#eaip_purchase_delete)). `AfterDelete` adds `DeleteError` with the DB contention message when the row count is not one.
16. **Remove an amendment in memory.** The collection's `Remove` override detects that the removed item was the last amendment of its original expense and, when so, restores the original to its pre-amendment state by reading the first history row whose status differs from the original's current status and copying that status code and status date back onto the original.
17. **Retrieve.** A single expense is read by binding the expense identifier to the subclass retrieve — `pk82500_eaip.GetPurchaseByExpenseId` for a purchase ([Details](../procedures/pk82500_eaip.md#getpurchasebyexpenseid)). A collection is read by binding the period identifier to the collection's retrieve statement, which the criteria receives at construction (`pk82500_eaip.EAIP_Purchase_Select` or `pk82500_eaip.eaip_wage_subsidy_select`), and materializing one typed item per row ([Details](../procedures/pk82500_eaip.md#eaip_purchase_select)). `CurrentOnly()` filters the materialized collection down to amendments plus non-amended originals, which is the view the user interface shows.
18. **Reset a stuck expense.** The shared `ResetPaidExpense(dpi, expenseId)` runs an inline non-query that sets the expense status code back to `NEW` for one expense identifier. The identifier is interpolated directly into the statement text rather than bound as a parameter, which makes this the one data-access path in the class that is not parameterized.

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
	Start([Caller opens a service period]) --> New{New expense<br>or existing?}
	New -->|New| Create[Create: eaip_expense_seq nextval,<br>status New, status date now]
	New -->|Existing| Load[Collection retrieve by period id<br>materializes purchases or wage subsidies]
	Create --> Edit[Caller edits type, amounts, dates,<br>write-off, adjustment, description]
	Load --> Edit
	Edit --> Mod{Amend or correct?}
	Mod -->|Yes| Clone[Clone expense, new id and guid,<br>OriginalExpenseID set,<br>source flipped to Amended or Corrected]
	Clone --> Edit
	Mod -->|No| Rules{Item and purchase rules pass?}
	Rules -->|No| Broken([Broken rules returned])
	Rules -->|Yes| Save[Save triggers OnSave]
	Save --> Gate[Collection CanPay:<br>minimum total, type limits,<br>child rule propagation]
	Gate --> Item[Item NeedsApproval:<br>authority, adjustment, write-off,<br>past one year, federal/inmate,<br>credit card]
	Item --> Need{Approval needed?}
	Need -->|Yes| Pending[Status set to<br>PendingSupervisorApproval]
	Need -->|No| Pay[Status stays Pay,<br>zero-adjustment amendments<br>auto-set to Paid]
	Pending --> Sup{Supervisor approves?}
	Sup -->|No authority| Warn([Warning: amount over<br>your authority level])
	Sup -->|Yes| Resolve[Approve resolves status to<br>Reimbursed, Paid, or Pay]
	Pay --> Persist
	Resolve --> Persist[BeforeInsert/BeforeUpdate bind<br>expense row then purchase or<br>wage subsidy row]
	Persist --> Rows{RowsAffected = 1<br>on both statements?}
	Rows -->|No| Err([Insert/Update error with<br>DB contention message])
	Rows -->|Yes| Hist[Expense status history appended]
	Hist --> Done([Expense persisted and<br>marked old])
```

## Failure and edge-case behavior

- Both the expense row and the subclass row are written in one portal request, but each is checked independently. A partial failure — the expense row updated and the purchase row not — surfaces as two different error messages on the same object and leaves the two tables out of step unless the surrounding transaction rolls back.*
- The authority checks depend on a payment user resolved from the payment library. When that user cannot be resolved, every authority-based rule and warning behaves as though the user has no authority, which pushes all payments into supervisor approval rather than failing loudly.*
  > **Origin**: Initiate and approval authority limits are implemented by `clsEaipPayment.GetPaymentUser` and the `PaymentUser` authority methods in `SAIF.Business.Claim.Payment` and executed on behalf of this operation. That library is documented in the previous format, so no per-operation page exists; see [Operations and processes](../../saif-business-claim-payment/operations-processes.md) in the `SAIF.Business.Claim.Payment` documentation set.
- The expense-type limit condition in `NeedsApproval` sets the approval flag without adding a warning message, so a user can be forced into supervisor approval with no explanation in the message collection.*
- The sub-$100 fallback query inspects the *sibling* expense table, not the current one, so a period whose only payable work is in the collection being saved can still fail the threshold even though the combined total across both tables would clear it.*
- `ResetPaidExpense` interpolates its expense identifier into the statement text instead of binding it. Any caller that passes unvalidated input reaches the database with that value inlined.
- Removing an amendment restores the original's status from the *first* history row whose status differs from the current one. When the original has a long history, the restored status is the most recent differing entry, which is the intended pre-amendment status only if no other status change intervened.*
- `Rule_ValidHasUnPaidExpenses` on the owning period is registered-out, so nothing stops a period from being inactivated while these expenses are still unpaid.
- Deletion is blocked by rule rather than by database constraint; an expense in a non-deletable status fails validation before the delete statement is reached.

## 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.EAIP\Period\clsExpense.vb`
- `Business\SAIF.Business.Claim.EAIP\Period\clsPurchase.vb`
- `Business\SAIF.Business.Claim.EAIP\Period\clsExpenseHistory.vb`
- `Business\SAIF.Business.Claim.EAIP\Period\clsHistoryBase.vb`
- `Business\SAIF.Business.Claim.EAIP\Period\clsPeriod.vb`
- `Business\SAIF.Business.Claim.EAIP\mdlCodeTables.vb`
