Skip to content

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; the credit-card supervisor queue is documented in Credit-card payment approval.

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, 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).
  4. Validate the shared rules. AddBusinessRules on the base class registers the following, all evaluated when the caller reads IsValid:
  5. Rule_HasInitiateAuthority — the acting user must hold initiate authority for the paid amount ("Error: You are not authorized to initiate payments.").
  6. Rule_ValidExpenseType — an expense type is required; Rule_Payable blocks paying an expense typed as referral or justification.
  7. 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).
  8. 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.
  9. Rule_ValidPeriodEffDate — the service period must have a begin date before an expense can be paid.
  10. Rule_ValidUpdateStatus — a paid expense cannot be updated; Rule_ValidDelete blocks deletion for statuses outside the deletable set.
  11. Rule_ValidWriteOffReason / Rule_ValidWriteOffAmount — the adjust-bill reason and the adjust-bill amount must be supplied together.
  12. Rule_ValidDeniedReason / Rule_ValidDeniedStatus — a denied reason is required if and only if the status is denied.
  13. 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.
  14. Rule_HasAmendmentText — amendment/correction text is required when paying an amended or corrected expense.
  15. 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).
  16. 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.*
  17. 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.
  18. 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:
  19. initiate authority is insufficient for the paid amount (BR-PROC-005);
  20. the expense is an amendment whose adjustment amount changed and the user lacks approval authority;
  21. the expense is not an amendment but carries a non-zero adjustment amount;
  22. the expense carries a non-zero adjust-bill (write-off) amount;
  23. 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");
  24. the claim is federal or inmate jurisdiction;
  25. 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).
  26. 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:
  27. 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).
  28. 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.
  29. 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.
  30. 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.
  31. 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, Details). Amendments bind a typed null rather than zero for the original expense identifier, and a never-paid expense binds a typed null date.
  32. 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.
  33. 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, Details). AfterUpdate applies the row-count check on both statements and adds a DB contention error on failure.
  34. 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, Details). AfterDelete adds DeleteError with the DB contention message when the row count is not one.
  35. 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.
  36. Retrieve. A single expense is read by binding the expense identifier to the subclass retrieve — pk82500_eaip.GetPurchaseByExpenseId for a purchase (Details). 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). CurrentOnly() filters the materialized collection down to amendments plus non-amended originals, which is the view the user interface shows.
  37. 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.

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 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.

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