Cash Receipts Procedures
1. Executive Summary
Purpose
This document describes every data-mutation operation in the cash receipts domain: how cash_receipt, cash_receipt_split, cash_receipt_reference, and cash_receipt_adjustment records are created, updated, deleted, and voided. Each procedure is defined as a sequence of numbered steps specifying which records are read, which records are written, what field values are set, and what side-effects cascade to related tables. The procedures documented here are the behavioral specification from which the production system should be implemented.
Scope
This document covers the following procedures:
- Cash Receipt creation — manual entry and bank-transaction sync (Living Mirror)
- Cash Receipt update — inline field edits with posting-status guards
- Cash Receipt amount adjustment — split-level amount changes that recalculate the receipt total
- Cash Receipt voiding — zeroing out a receipt via the adjustment mechanism
- Cash Receipt deletion — hard-delete of receipt and child records
- Cash Receipt locking and unlocking — concurrency control
- Cash Receipt Split creation — default 1:1 split and additional carve-out splits
- Cash Receipt Split adjustment — changing a split's amount
- Cash Receipt Split update — notes, amount, and file-link changes
- Cash Receipt Split fund transfer — moving funds between splits atomically
- Cash Receipt Split deletion — with fund transfer and worksheet cleanup
- Cash Receipt Split combination — merging multiple splits into one
- Cash Receipt Split voiding — cancelling a split during receipt zeroing
- Cash Receipt Split tax warning update — storing withholding-tax warnings
- Cash Receipt Adjustment creation — bank fees and corrective adjustments
- Cash Receipt Adjustment deletion — reversing an unposted adjustment
- Cash Receipt Transfer — moving funds to a different bank account
- Write-Off Cash Receipt creation — system-generated receipt from packet approval
- Write-Off Reversal Worksheet — recovery of a written-off receipt
- Reset Receipts for GL Reposting — batch status reset
Reference documents:
- Data model: Cash Receipts Data Model
- Queries: Cash Receipts Queries
2. Key Procedures
2.1 Create Cash Receipt (Manual)
Operation: createCashReceipt
Trigger: User submits a new cash receipt form with amount, currency, bank account, and deposit date fields.
Input Parameters:
original_receipt_amt:decimal(15,2)(required) —cash_receipt.original_receipt_amt; must be greater than zerooriginal_currency_cd:varchar(50)(required) —cash_receipt.original_currency_cdcurrency_cd:varchar(50)(optional) —cash_receipt.currency_cd; defaults tooriginal_currency_cdwhen omittedfx_rate:real(conditionally required) —cash_receipt.fx_rate; required and must be > 0 whencurrency_cddiffers fromoriginal_currency_cdbank_account_id:integer(optional) —cash_receipt.bank_account_iddeposit_date:date(optional) —cash_receipt.deposit_datecash_receipt_ref:varchar(150)(optional) —cash_receipt.cash_receipt_refcash_receipt_comment:varchar(255)(optional) —cash_receipt.cash_receipt_commentactorId: String — identifier for the user performing the operation
Creates a new cash receipt from user input, computes derived amount fields, and auto-creates a default 1:1 split with a Draft worksheet.
Step 1. Validate Inputs
- Source: Caller-supplied parameters.
- Action: No database write. Validation only.
- Logic:
- Reject if
original_receipt_amtis missing, zero, or negative. - Reject if
original_currency_cdis missing. - Reject if
currency_cddiffers fromoriginal_currency_cdandfx_rateis missing or <= 0.
- Reject if
Step 2. Compute Derived Amounts
- Source: Caller-supplied
original_receipt_amt,fx_rate,currency_cd,original_currency_cd. - Action: No database write. In-memory calculation.
- Logic:
- If
currency_cddiffers fromoriginal_currency_cdandfx_rateis provided: setreceipt_amt = original_receipt_amt * fx_rate, rounded to 2 decimal places. - Otherwise: set
receipt_amt = original_receipt_amt. - Set
net_receipt_amt = receipt_amt(no adjustments yet on creation).
- If
Step 3. Insert Cash Receipt Record
- Source: Validated and computed values from steps 1–2.
- Action: INSERT into
cash_receipt. - Logic:
- Set
original_receipt_amt,original_currency_cd,currency_cd,fx_ratefrom input. - Set
receipt_amt,net_receipt_amtfrom step 2 computation. - Set
bank_account_id,deposit_date,cash_receipt_ref,cash_receipt_commentfrom input. - Set
posting_status_cd = 'U'(default — Unposted). - Set
receipt_type_cd = 'NORMAL'(default). - Set
created_by = actorId,updated_by = actorId,created_dt = NOW(),updated_dt = NOW().
- Set
Step 4. Create Default 1:1 Split
- Source: Newly inserted
cash_receipt_idandnet_receipt_amtfrom step 3. - Action: Delegates to the Create Default Split procedure (see 2.2 Create Default Split).
- Logic:
- Calls
createDefaultSplitwithcash_receipt_id,net_receipt_amt,actorId. - One
cash_receipt_splitand onecash_receipt_worksheetare created as a result.
- Calls
IMPORTANT
The split creation is not wrapped in a database transaction with the receipt insert. If split creation fails, the receipt exists without a split. Production implementations should wrap both operations in a single transaction.
Side-effects:
- Creates one
cash_receipt_splitrecord withsplit_amt = net_receipt_amtandsplit_status_cd = 'N'. - Creates one
cash_receipt_worksheetrecord withcash_receipt_worksheet_status_cd = 'D'andcurrent_item_ind = true.
Postconditions:
cash_receiptexists withposting_status_cd = 'U'andreceipt_type_cd = 'NORMAL'.- Exactly one
cash_receipt_splitexists for this receipt. - Exactly one
cash_receipt_worksheetexists in Draft status for that split.
2.2 Create Default Split
Operation: createDefaultSplit
Trigger: Called internally by createCashReceipt and syncFromBankTransaction immediately after inserting a new cash_receipt record.
Input Parameters:
cash_receipt_id:integer(required) —cash_receipt_split.cash_receipt_idreceipt_amt:decimal(15,2)(required) — amount to assign to the default split, taken fromcash_receipt.net_receipt_amtactorId: String — identifier for the user performing the operation
Creates the initial 1:1 split for a newly created receipt, plus a Draft worksheet attached to it.
Step 1. Verify Receipt Exists
- Source:
cash_receiptWHEREcash_receipt_id = :cash_receipt_id. - Action: SELECT only. No write.
- Logic:
- Reject if no row found.
Step 2. Check No Splits Exist
- Source: COUNT from
cash_receipt_splitWHEREcash_receipt_id = :cash_receipt_id. - Action: SELECT only. No write.
- Logic:
- If count > 0, abort with error "Default split already exists."
Step 3. Insert Cash Receipt Split
- Source:
cash_receipt_idandreceipt_amt. - Action: INSERT into
cash_receipt_split. - Logic:
- Set
cash_receipt_id = :cash_receipt_id. - Set
split_sequence = 1. - Set
split_amt = :receipt_amt. - Set
split_status_cd = 'N'(default — New). - Set
created_by = actorId,updated_by = actorId,created_dt = NOW(),updated_dt = NOW().
- Set
Step 4. Insert Draft Worksheet
- Source:
cash_receipt_split_idfrom step 3. - Action: INSERT into
cash_receipt_worksheet. - Logic:
- Set
cash_receipt_split_idfrom step 3. - Set
cash_receipt_worksheet_status_cd = 'D'. - Set
current_item_ind = true. - Set
created_by = actorId,updated_by = actorId.
- Set
Side-effects:
- None beyond the two records created in steps 3 and 4.
Postconditions:
- Exactly one
cash_receipt_splitexists for this receipt withsplit_amt = receipt_amtandsplit_status_cd = 'N'. - Exactly one
cash_receipt_worksheetexists for that split withcash_receipt_worksheet_status_cd = 'D'.
2.3 Sync from Bank Transaction (Living Mirror)
Operation: syncFromBankTransaction
Trigger: User clicks "Generate Cash Receipt" on a bank transaction row, or the bank ingestion service processes a new file that updates a previously ingested transaction's status.
Input Parameters:
bank_account_id:integer(required) —cash_receipt.bank_account_id; must not be nullbank_reference_id:varchar(100)(required) —cash_receipt.bank_ref_id; AcctSvcrRef from the bank; must not be nullbank_transaction_id:integer(optional) — FK tobank_transactionfor audit linkagestatus:varchar(20)(required) — bank entry status (PDNGorBOOK); maps tocash_receipt.entry_statusbooking_date:date(optional) —cash_receipt.booking_dateremittance_info:text(optional) —cash_receipt.remittance_info; snapshot, not updated after creationamount:decimal(15,2)(required) — transaction amount; maps tocash_receipt.original_receipt_amtcurrency:varchar(50)(required) — transaction currency; maps tocash_receipt.original_currency_cdfile_name:varchar(255)(optional) — source bank file name; maps tocash_receipt.filenameactorId: String — identifier for the user or system process performing the operation
Creates or updates a cash receipt from a bank transaction. If a receipt already exists for this bank reference, the procedure cascades the status change; if not, it creates a new receipt with a default split and Draft worksheet.
Step 1. Validate Required Fields
- Source: Caller-supplied parameters.
- Action: No database write. Validation only.
- Logic:
- Reject if
bank_account_idis null. - Reject if
bank_reference_idis null or empty.
- Reject if
Step 2. Look Up Existing Receipt by Composite Key
- Source:
cash_receiptWHEREbank_account_id = :bank_account_idANDbank_ref_id = :bank_reference_id. - Action: SELECT using the unique index
uq_cash_receipt_bank_ref. - Logic:
- If a row is found, proceed to step 3 (update branch).
- If no row is found, proceed to step 4 (create branch).
Step 3. Update Existing Receipt (Status Cascade)
- Source: Existing
cash_receiptrow from step 2. - Action: UPDATE
cash_receiptifentry_statushas changed; otherwise no write. - Logic:
- If
existing.entry_status != :status: UPDATEcash_receiptSETentry_status = :status,booking_date = :booking_date,bank_transaction_id = :bank_transaction_id,updated_by = actorId,updated_dt = NOW(). - If
existing.entry_status = :status: no mutation. Return the existing receipt as-is.
- If
Step 4. Create New Receipt (When None Exists)
- Source: Caller-supplied transaction data.
- Action: INSERT into
cash_receipt. - Logic:
- Set
entry_status = :status. - Set
bank_ref_id = :bank_reference_id. - Set
bank_transaction_id = :bank_transaction_id. - Set
booking_date = :booking_date. - Set
remittance_info = :remittance_info. - Set
bank_account_id = :bank_account_id. - Set
deposit_date = :booking_date(uses booking date as deposit date). - Set
cash_receipt_ref = :bank_reference_id. - Set
filename = :file_name. - Set
original_receipt_amt = :amount. - Set
receipt_amt = :amount(no FX conversion applied for bank-originated receipts). - Set
net_receipt_amt = :amount. - Set
original_currency_cd = :currency. - Set
currency_cd = :currency. - Set
posting_status_cd = 'U'. - Set
receipt_type_cd = 'NORMAL'.
- Set
NOTE
Creation is not blocked for BOOK status transactions. Intraday bank files can contain already-booked items. The system creates receipts regardless of entry status.
Side-effects:
- On new receipt creation: delegates to
createDefaultSplitto create onecash_receipt_splitand one Draftcash_receipt_worksheet. - On status update: no side-effects beyond the receipt row change.
Postconditions:
- A
cash_receiptexists for the combination(bank_account_id, bank_ref_id)withentry_statusreflecting the latest bank status. - On new creation: one
cash_receipt_splitand one Draftcash_receipt_worksheetalso exist.
2.4 Update Cash Receipt
Operation: updateCashReceipt
Trigger: User edits fields on the cash receipt detail form and saves changes.
Input Parameters:
cash_receipt_id:integer(required) —cash_receipt.cash_receipt_id- Any updatable field: type varies — field-level editability depends on
posting_status_cd(see business rules below) actorId: String — identifier for the user performing the operation
Updates mutable fields on an existing cash receipt, with field-level editability gated by the receipt's current posting_status_cd.
Step 1. Read Current Receipt
- Source:
cash_receiptWHEREcash_receipt_id = :cash_receipt_id. - Action: SELECT only. No write.
- Logic:
- Reject if no row found.
Step 2. Enforce Posting-Status Guards
- Source:
cash_receipt.posting_status_cdfrom step 1. - Action: No database write. Validation only.
- Logic:
- If
posting_status_cd = 'V'(Voided): onlycash_receipt_refandcash_receipt_commentmay appear in the update payload. Any other field triggers an error. - If
posting_status_cd = 'P'(Posted): onlycash_receipt_commentandnet_receipt_amt(via adjustment recalculation) may appear in the update payload. Any other field triggers an error. - If
posting_status_cd = 'U'(Unposted): all fields are editable.
- If
Step 3. Recalculate Derived Amounts (Conditional)
- Source: Updated
original_receipt_amt,fx_rate,currency_cd,original_currency_cdfrom payload; current record from step 1. - Action: No database write. In-memory recalculation.
- Logic:
- If
original_receipt_amt,fx_rate,currency_cd, ororiginal_currency_cdchanged andreceipt_amtwas not explicitly provided:- If
currency_cddiffers fromoriginal_currency_cdandfx_rateis present:receipt_amt = original_receipt_amt * fx_rate. - Otherwise:
receipt_amt = original_receipt_amt.
- If
net_receipt_amtis preserved from the current record unless explicitly provided in the payload (to retain existing adjustment effects).
- If
Step 4. Update Cash Receipt Record
- Source: Changed fields from payload, computed values from step 3.
- Action: UPDATE
cash_receiptSET changed fields,updated_by = actorId,updated_dt = NOW(). - Logic:
- Only the fields present in the update payload (plus
updated_by,updated_dt) are written.
- Only the fields present in the update payload (plus
Step 5. Cascade Split Amount (Conditional)
- Source: Updated
net_receipt_amt; count and state of splits for this receipt. - Action: UPDATE
cash_receipt_splitSETsplit_amt = :new_net_receipt_amt(conditional). - Logic:
- Only if the receipt amount changed AND the receipt has exactly one split AND that split has no worksheet.
- In all other cases, skip this step.
**PoC Artifact:**
The cascade to update the single split is implemented in the server action layer rather than the service layer. Production should move this logic into the service for transactional consistency.
Side-effects:
- When
net_receipt_amtchanges and the single-split cascade condition is met:cash_receipt_split.split_amtis updated to match.
Postconditions:
cash_receiptreflects the updated field values within the permissions allowed byposting_status_cd.
2.5 Adjust Receipt Amount
Operation: adjustReceiptAmount
Trigger: User uses the "Adjust Amount" action on a receipt and enters a new amount for a specific split.
Input Parameters:
cash_receipt_id:integer(required) —cash_receipt.cash_receipt_idsplit_id:integer(required) —cash_receipt_split.cash_receipt_split_id; the split whose amount is being changednew_split_amount:decimal(15,2)(required) — the desired new amount for the split; may be zerooriginal_receipt_amt:decimal(15,2)(optional) — override forcash_receipt.original_receipt_amtactorId: String — identifier for the user performing the operation
Changes a receipt's total by adjusting a specific split's amount and recalculating the receipt total from all splits. Zeroing all splits voids the receipt. This procedure replaces a separate "void receipt" action.
Step 1. Validate Receipt Is Not Voided
- Source:
cash_receiptWHEREcash_receipt_id = :cash_receipt_id. - Action: SELECT only. No write.
- Logic:
- Reject if
posting_status_cd = 'V'.
- Reject if
Step 2. Validate Split Belongs to Receipt
- Source: All splits with worksheets for the receipt.
- Action: SELECT only. No write.
- Logic:
- Locate the split with
cash_receipt_split_id = :split_idin the result set. - Reject if not found.
- Locate the split with
Step 3. Enforce Draft-Only Worksheet Constraint
- Source: Worksheet associated with the target split from step 2.
- Action: No write.
- Logic:
- If the split has a worksheet and
cash_receipt_worksheet_status_cd != 'D', abort with an error naming the current status. - This constraint is stricter than
adjustSplitAmount, which also permits Approved ('A') worksheets.
- If the split has a worksheet and
Step 4. Branch: Zero Amount with Multiple Splits
- Source:
new_split_amountfrom caller; count of splits from step 2 result. - Action: Conditional delegation.
- Logic:
- If
new_split_amount = 0AND more than one split exists:- Call
adjustSplitAmountwithnew_amount = '0'for the target split. - Call
deleteSplitwithout a target (since amount is now $0).
- Call
- Otherwise proceed to step 5.
- If
Step 5. Normal Adjustment
- Source:
new_split_amountfrom caller. - Action: Delegates to
adjustSplitAmountfor the target split. - Logic:
- See 2.7 Adjust Split Amount for the full validation and write sequence.
Step 6. Recalculate Receipt Total
- Source: Re-read all non-voided splits for the receipt after step 4 or 5.
- Action: SELECT only. No write at this step.
- Logic:
new_receipt_total = SUM(split_amt)across all remaining splits.
Step 7. Void Receipt If Total Is Zero
- Source:
new_receipt_totalfrom step 6. - Action: Conditional updates.
- Logic:
- If
new_receipt_total = 0:- For each remaining split, call
voidSplit(see 2.11 Void Split). - Add
posting_status_cd = 'V'to the receipt update in step 8.
- For each remaining split, call
- If
Step 8. Update Receipt Amounts
- Source:
new_receipt_totalfrom step 6;original_receipt_amtfrom caller (if provided). - Action: UPDATE
cash_receiptSETreceipt_amt = :new_receipt_total,net_receipt_amt = :new_receipt_total,updated_by = actorId,updated_dt = NOW(). - Logic:
- If
original_receipt_amtwas provided, also setoriginal_receipt_amt = :original_receipt_amt. - If
posting_status_cd = 'V'was flagged in step 7, also setposting_status_cd = 'V'.
- If
IMPORTANT
There is no standalone "void receipt" action. Voiding is the consequence of reducing the receipt total to zero. The split and adjustment records remain as an audit trail explaining why the receipt was cancelled.
Side-effects:
cash_receipt_split.split_amtis updated for the target split.- If total reaches zero: all remaining splits are set to
split_status_cd = 'V'; empty Draft worksheets are deleted. cash_receipt.receipt_amtandcash_receipt.net_receipt_amtare updated.- If total reaches zero:
cash_receipt.posting_status_cdis set to'V'.
Postconditions:
cash_receipt.net_receipt_amtequals the sum of all non-voided split amounts.- If zeroed out:
cash_receipt.posting_status_cd = 'V'and all splits havesplit_status_cd = 'V'.
2.6 Create Additional Split (Carve-Out)
Operation: createSplit
Trigger: User initiates a "Re-Split" action on a receipt and specifies a new split amount and source split.
Input Parameters:
cash_receipt_id:integer(required) —cash_receipt_split.cash_receipt_idamount:decimal(15,2)(required) —cash_receipt_split.split_amtfor the new split; must be > 0source_split_id:integer(required) —cash_receipt_split.cash_receipt_split_idof the split to carve fromnotes:text(optional) —cash_receipt_split.notesfor the new splitsource_file_upload_id:integer(optional) —cash_receipt_split.source_file_upload_idactorId: String — identifier for the user performing the operation
Creates a new split by carving funds from an existing source split. The source split's amount is reduced accordingly. If the source is fully emptied, it is automatically deleted.
Step 1. Validate Amount Is Positive
- Source: Caller-supplied
amount. - Action: No write.
- Logic:
- Reject if
amount <= 0or is not a valid number.
- Reject if
Step 2. Validate Source Split Can Be Modified
- Source:
cash_receipt_splitand associatedcash_receipt_worksheetforsource_split_id. - Action: No write.
- Logic:
- Apply the Split Modifiability rule (see 3.1 Split Modifiability).
- Reject if the source split is voided or its worksheet is in Applied, Settled, Returned, or Submitted status.
Step 3. Validate Sufficient Available Balance
- Source:
cash_receipt_split.split_amtand approved applications sum forsource_split_id. - Action: No write.
- Logic:
available_balance = split_amt - sum_of_approved_applications.- Reject if
available_balance < amount.
Step 4. Read Source Split
- Source:
cash_receipt_splitWHEREcash_receipt_split_id = :source_split_id. - Action: SELECT only.
- Logic:
- Capture
split_amt. - Calculate
new_source_amount = split_amt - amount.
- Capture
Step 5. Branch: Source Would Be Emptied (new_source_amount < 0.01)
- Source: Source worksheet status and applied amount.
- Action: Conditional creates and deletes.
- Logic:
- Validate that the source worksheet (if any) is in Draft status with no applications. If not, reject.
- INSERT
cash_receipt_splitwithsplit_amt = :amount,split_status_cd = 'N',parent_split_id = :source_split_id,split_sequence = MAX(split_sequence) + 1,notes,source_file_upload_id. - INSERT
cash_receipt_worksheetfor the new split withcash_receipt_worksheet_status_cd = 'D',current_item_ind = true. - DELETE
cash_receipt_worksheetfor the source split (if it exists). - DELETE
cash_receipt_splitWHEREcash_receipt_split_id = :source_split_id.
Step 6. Branch: Normal Carve-Out (Source Retains Funds)
- Source:
new_source_amountfrom step 4. - Action: UPDATE source split; INSERT new split and worksheet.
- Logic:
- UPDATE
cash_receipt_splitSETsplit_amt = :new_source_amount,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :source_split_id. - INSERT
cash_receipt_splitwithsplit_amt = :amount,split_status_cd = 'N',parent_split_id = :source_split_id,split_sequence = MAX + 1,notes,source_file_upload_id. - INSERT
cash_receipt_worksheetfor the new split withcash_receipt_worksheet_status_cd = 'D',current_item_ind = true.
- UPDATE
Step 7. Validate Split Totals (Sanity Check)
- Source:
cash_receipt.net_receipt_amt; SUM ofcash_receipt_split.split_amtfor the receipt. - Action: SELECT only. No write.
- Logic:
- Log a warning if
ABS(net_receipt_amt - SUM(split_amt)) >= 0.005.
- Log a warning if
Side-effects:
- Source split
split_amtis reduced (or source split is deleted if fully emptied). - New
cash_receipt_splitis created withsplit_status_cd = 'N'. - New
cash_receipt_worksheetis created in Draft status for the new split.
Postconditions:
- A new split exists with
split_amt = :amountand a Draft worksheet. - Total of all splits for the receipt still equals
cash_receipt.net_receipt_amtwithin tolerance of 0.005. - The source split either has a reduced amount or has been removed.
2.7 Adjust Split Amount
Operation: adjustSplitAmount
Trigger: User directly changes the amount on an individual split, typically via the split edit form.
Input Parameters:
split_id:integer(required) —cash_receipt_split.cash_receipt_split_idnew_amount:decimal(15,2)(required) — targetcash_receipt_split.split_amt; must be >= 0actorId: String — identifier for the user performing the operation
Changes a single split's amount directly. Used for amount corrections, not for transfers between splits.
Step 1. Validate Amount Is Non-Negative
- Source: Caller-supplied
new_amount. - Action: No write.
- Logic:
- Reject if
new_amount < 0or is not a valid number.
- Reject if
Step 2. Validate Split Can Be Modified
- Source:
cash_receipt_splitand associated worksheet forsplit_id. - Action: No write.
- Logic:
- Apply the Split Modifiability rule (see 3.1 Split Modifiability).
Step 3. Read Current Split
- Source:
cash_receipt_splitWHEREcash_receipt_split_id = :split_id. - Action: SELECT only.
- Logic:
- Capture
split_amt.
- Capture
Step 4. Validate Floor Constraint
- Source: Available balance for the split (from approved applications sum on the worksheet).
- Action: No write.
- Logic:
applied_amt = current_split_amt - available_balance.- Reject if
new_amount < applied_amt.
Step 5. Update Split Amount
- Source:
new_amountfrom caller. - Action: UPDATE
cash_receipt_splitSETsplit_amt = :new_amount,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :split_id. - Logic:
- No other fields are changed.
Side-effects:
- None beyond the
split_amtupdate.
Postconditions:
cash_receipt_split.split_amt = :new_amount.- The split amount is at or above the applied amount floor.
2.8 Update Split
Operation: updateSplit
Trigger: User edits split details (notes, amount, or document attachment) on the split form and saves.
Input Parameters:
split_id:integer(required) —cash_receipt_split.cash_receipt_split_idamount:decimal(15,2)(optional) — newcash_receipt_split.split_amtnotes:text(optional) —cash_receipt_split.notessource_file_upload_id:integer(optional) —cash_receipt_split.source_file_upload_idactorId: String — identifier for the user performing the operation
Updates split details. Notes-only updates are allowed even on voided splits; amount or file-link changes require the split to be modifiable.
Step 1. Validate Split Can Be Modified
- Source:
cash_receipt_splitand associated worksheet forsplit_id. - Action: No write.
- Logic:
- If only
notesis changing (noamountorsource_file_upload_id): allow even ifsplit_status_cd = 'V'(notes-only exception). - Otherwise apply the full Split Modifiability rule (see 3.1 Split Modifiability).
- If only
Step 2. Validate Amount Floor (Conditional)
- Source: Approved applications sum for the split's worksheet.
- Action: No write.
- Logic:
- Only if
amountis provided. - Reject if
amount < 0. - Reject if
amount < applied_amt(same floor constraint as 2.7 Adjust Split Amount).
- Only if
Step 3. Update Split
- Source: Provided field values.
- Action: UPDATE
cash_receipt_splitSET provided fields,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :split_id. - Logic:
- Only the fields included in the input are written.
Side-effects:
- None beyond the updated fields on
cash_receipt_split.
Postconditions:
cash_receipt_splitreflects the updatedamount,notes, and/orsource_file_upload_idas supplied.
2.9 Transfer Funds Between Splits
Operation: transferFunds
Trigger: User selects a source split, specifies a target split, and enters a transfer amount on the fund-transfer form.
Input Parameters:
from_split_id:integer(required) — sourcecash_receipt_split.cash_receipt_split_idto_split_id:integer(required) — targetcash_receipt_split.cash_receipt_split_idamount:decimal(15,2)(required) — transfer amount; must be > 0actorId: String — identifier for the user performing the operation
Atomically moves funds from one split to another within the same receipt. If the source is emptied, the system attempts to auto-delete it.
Step 1. Validate Amount Is Positive
- Source: Caller-supplied
amount. - Action: No write.
- Logic:
- Reject if
amount <= 0or is not a valid number.
- Reject if
Step 2. Validate Both Splits Can Be Modified
- Source: Each split and its associated worksheet.
- Action: No write.
- Logic:
- Apply Split Modifiability rule (see 3.1 Split Modifiability) independently for source and target.
- Reject with a labeled error message if either is blocked.
Step 3. Validate Source Has Sufficient Available Balance
- Source: Available balance for
from_split_id. - Action: No write.
- Logic:
available_balance >= amount.- Reject if insufficient.
Step 4. Read Both Splits
- Source:
cash_receipt_splitWHEREcash_receipt_split_id IN (:from_split_id, :to_split_id). - Action: SELECT only.
- Logic:
- Verify both rows exist.
- Reject if
from_split.cash_receipt_id != to_split.cash_receipt_id.
Step 5. Calculate New Amounts
- Source:
split_amtvalues from step 4. - Action: No database write. In-memory.
- Logic:
new_from_amount = from_split.split_amt - amount.new_to_amount = to_split.split_amt + amount.
Step 6. Perform Atomic Transfer
- Source: New amounts from step 5.
- Action: Two UPDATEs within a single database transaction.
- Logic:
- UPDATE
cash_receipt_splitSETsplit_amt = :new_from_amount,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :from_split_id. - UPDATE
cash_receipt_splitSETsplit_amt = :new_to_amount,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :to_split_id.
- UPDATE
IMPORTANT
Both updates execute within a single database transaction. If either fails, both are rolled back. This prevents funds from being lost between splits.
Step 7. Auto-Delete Empty Source (Conditional)
- Source:
new_from_amountfrom step 5; source worksheet status and applied amount. - Action: Conditional DELETEs.
- Logic:
- Only if
new_from_amount < 0.01AND more than one split exists for the receipt. - Check source worksheet: if it exists, it must be in Draft status with no applications.
- If deletable: DELETE
cash_receipt_worksheetfor the source (if exists), then DELETEcash_receipt_splitWHEREcash_receipt_split_id = :from_split_id. - If not deletable: source remains as a zero-amount split with its worksheet intact.
- Only if
Side-effects:
cash_receipt_split.split_amtis reduced onfrom_splitand increased onto_split.- If source is emptied and deletable: the source
cash_receipt_splitand its Draft worksheet are deleted.
Postconditions:
- Total of all splits for the receipt is unchanged (transfer is zero-sum).
to_split.split_amthas increased byamount;from_split.split_amthas decreased byamount(or source split no longer exists).
2.10 Delete Split
Operation: deleteSplit
Trigger: User selects a split and chooses the "Delete Split" action, optionally specifying a target split to receive any remaining funds.
Input Parameters:
split_id:integer(required) —cash_receipt_split.cash_receipt_split_idtarget_split_id:integer(conditionally required) — targetcash_receipt_split.cash_receipt_split_idto receive funds; required ifsplit_amt >= 0.01actorId: String — identifier for the user performing the operation
Removes a split from a receipt, transferring any remaining funds to a target split first if needed.
Step 1. Read Split
- Source:
cash_receipt_splitWHEREcash_receipt_split_id = :split_id. - Action: SELECT only.
- Logic:
- Reject if not found.
Step 2. Enforce Last-Split Protection
- Source: COUNT of splits for
split.cash_receipt_id. - Action: SELECT only. No write.
- Logic:
- Reject if count <= 1 (cannot delete the last split for a receipt).
Step 3. Transfer Funds (Conditional)
- Source:
split_amtfrom step 1;target_splitfromcash_receipt_split. - Action: UPDATE
cash_receipt_splitfor target split (conditional). - Logic:
- If
split_amt >= 0.01:- Reject if
target_split_idis not provided. - Read target split; reject if it belongs to a different receipt.
- UPDATE
cash_receipt_splitSETsplit_amt = target.split_amt + split.split_amt,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :target_split_id.
- Reject if
- If
Step 4. Check and Delete Worksheet
- Source:
cash_receipt_worksheetWHEREcash_receipt_split_id = :split_id. - Action: SELECT; conditional DELETE.
- Logic:
- If a worksheet exists:
- Reject if
cash_receipt_worksheet_status_cd != 'D'. - Reject if total applied for worksheet > 0.
- DELETE
cash_receipt_worksheetWHEREcash_receipt_worksheet_id = :worksheet_id.
- Reject if
- If a worksheet exists:
Step 5. Delete Split
- Source:
split_id. - Action: DELETE from
cash_receipt_splitWHEREcash_receipt_split_id = :split_id. - Logic:
cash_receipt_referencerows linked to this split are deleted first (cascade or explicit delete).
Side-effects:
- If split had funds:
target_split.split_amtis increased. - Any
cash_receipt_referencerecords for this split are deleted. - The Draft worksheet (if any) is deleted.
Postconditions:
cash_receipt_splitWHEREcash_receipt_split_id = :split_idno longer exists.- Total of all remaining splits for the receipt equals
cash_receipt.net_receipt_amt.
2.11 Void Split
Operation: voidSplit
Trigger: Called internally by adjustReceiptAmount when the receipt total reaches zero and all splits must be cancelled.
Input Parameters:
split_id:integer(required) —cash_receipt_split.cash_receipt_split_idactorId: String — identifier for the user or system process performing the operation
Cancels a split by setting its status to Void and amount to zero. Deletes the associated Draft worksheet if it has no applications.
Step 1. Read Split
- Source:
cash_receipt_splitWHEREcash_receipt_split_id = :split_id. - Action: SELECT only.
- Logic:
- Reject if not found.
Step 2. Clean Up Empty Draft Worksheet (Conditional)
- Source:
cash_receipt_worksheetWHEREcash_receipt_split_id = :split_id. - Action: Conditional DELETE.
- Logic:
- If a worksheet exists AND
cash_receipt_worksheet_status_cd = 'D'AND total applied for worksheet = 0:- DELETE
cash_receipt_worksheetWHEREcash_receipt_worksheet_id = :worksheet_id.
- DELETE
- If a worksheet exists AND
Step 3. Set Split to Void
- Source:
split_id. - Action: UPDATE
cash_receipt_splitSETsplit_status_cd = 'V',split_amt = '0.00',updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :split_id. - Logic:
- Both
split_status_cdandsplit_amtare updated in a single operation.
- Both
Side-effects:
- Empty Draft worksheet is deleted if it existed.
Postconditions:
cash_receipt_split.split_status_cd = 'V'andsplit_amt = '0.00'.- No Draft worksheet with zero applications exists for this split.
2.12 Combine Splits
Operation: combineSplits
Trigger: User selects two or more splits on the receipt detail and chooses the "Combine Splits" action.
Input Parameters:
split_ids:integer[](required) — array ofcash_receipt_split.cash_receipt_split_idvalues; minimum 2 elementsactorId: String — identifier for the user performing the operation
Merges two or more splits into a single split. All selected splits must have no worksheet. The split with the lowest sequence number is kept; others are deleted.
Step 1. Validate Minimum Count
- Source:
split_ids. - Action: No write.
- Logic:
- Reject if
split_idscontains fewer than 2 elements.
- Reject if
Step 2. Fetch All Splits
- Source:
cash_receipt_splitWHEREcash_receipt_split_id IN (:split_ids). - Action: SELECT only.
- Logic:
- Reject if the number of rows returned does not equal the number of IDs requested.
- Reject if all splits do not share the same
cash_receipt_id.
Step 3. Verify No Worksheets Exist
- Source:
cash_receipt_worksheetWHEREcash_receipt_split_id= each split ID. - Action: SELECT only. No write.
- Logic:
- For each split, check for any associated worksheet.
- Reject with a specific error identifying which split is blocked if any worksheet is found.
Step 4. Determine Keeper Split and Combined Values
- Source: Fetched splits from step 2.
- Action: No write. In-memory.
- Logic:
- Sort splits by
split_sequence ASC. The first split (lowest sequence) is the keeper. combined_amount = SUM(split_amt)across all selected splits.combined_notes = JOIN(non-empty notes values, ' | ').
- Sort splits by
Step 5. Update Keeper Split
- Source:
combined_amountandcombined_notesfrom step 4. - Action: UPDATE
cash_receipt_splitSETsplit_amt = :combined_amount,notes = :combined_notes,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :keeper_id. - Logic:
- No other fields on the keeper are changed.
Step 6. Delete Non-Keeper Splits
- Source: All split IDs excluding the keeper.
- Action: For each non-keeper split: DELETE
cash_receipt_referenceWHEREcash_receipt_split_id = :split_id; then DELETEcash_receipt_splitWHEREcash_receipt_split_id = :split_id. - Logic:
- References are deleted before the split to maintain referential integrity.
Side-effects:
- All non-keeper
cash_receipt_splitrecords are permanently deleted. - All
cash_receipt_referencerecords for non-keeper splits are deleted.
Postconditions:
- Only the keeper split remains; it holds the combined amount and concatenated notes.
- Total of all splits for the receipt still equals
cash_receipt.net_receipt_amt.
2.13 Update Split Tax Warning
Operation: updateSplitTaxWarning
Trigger: A party reference is added to a split and the tax service evaluates the party's withholding tax status, or the tax warning is explicitly cleared.
Input Parameters:
split_id:integer(required) —cash_receipt_split.cash_receipt_split_idwarning:object or null(required) — structured payload{ warnings: [...], calculatedDt, partyId, jurisdiction }, ornullto clearactorId: String — identifier for the user or system performing the operation
Stores or clears withholding-tax warning data on a split.
Step 1. Read Split
- Source:
cash_receipt_splitWHEREcash_receipt_split_id = :split_id. - Action: SELECT only.
- Logic:
- Reject if not found.
Step 2. Update Tax Warning Fields
- Source:
warningpayload from caller. - Action: UPDATE
cash_receipt_splitSETtax_warning_ind,tax_warning_json,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :split_id. - Logic:
- Set
tax_warning_ind = trueifwarningis not null andwarning.warningshas at least one entry; otherwisefalse. - Set
tax_warning_json = :warning(the full JSON payload), ornullto clear.
- Set
Side-effects:
- None beyond the updated fields on
cash_receipt_split.
Postconditions:
cash_receipt_split.tax_warning_indandcash_receipt_split.tax_warning_jsonreflect the current warning state.
2.14 Create Adjustment
Operation: createAdjustment
Trigger: User adds a bank fee or corrective adjustment on the receipt detail form.
Input Parameters:
cash_receipt_id:integer(required) —cash_receipt_adjustment.cash_receipt_idcash_receipt_split_id:integer(required) — the split to reduceadjustment_amt:decimal(15,2)(required) —cash_receipt_adjustment.adjustment_amt; must be > 0comment:varchar(255)(required) —cash_receipt_adjustment.comment; must be non-emptyactorId: String — identifier for the user performing the operation
Creates a fee or corrective adjustment on a receipt, reducing both the associated split's amount and the receipt's net amount.
Step 1. Validate Inputs
- Source: Caller-supplied parameters.
- Action: No write.
- Logic:
- Reject if
adjustment_amt <= 0. - Reject if
commentis empty.
- Reject if
Step 2. Validate Receipt Is Not Voided
- Source:
cash_receiptWHEREcash_receipt_id = :cash_receipt_id. - Action: SELECT only.
- Logic:
- Reject if
posting_status_cd = 'V'.
- Reject if
Step 3. Validate Split Belongs to Receipt
- Source:
cash_receipt_splitWHEREcash_receipt_split_id = :cash_receipt_split_id. - Action: SELECT only.
- Logic:
- Reject if
cash_receipt_idon the split does not match:cash_receipt_id.
- Reject if
Step 4. Validate Worksheet Status
- Source:
cash_receipt_worksheetfor the split. - Action: SELECT only.
- Logic:
- Reject if a worksheet exists and
cash_receipt_worksheet_status_cd != 'D'.
- Reject if a worksheet exists and
Step 5. Validate Adjustment Does Not Exceed Split
- Source:
cash_receipt_split.split_amt; approved applications sum from the worksheet. - Action: No write.
- Logic:
- Reject if
split_amt - adjustment_amt < 0. - If the split has a worksheet, also reject if
split_amt - adjustment_amt < applied_amt(with 0.005 tolerance).
- Reject if
Step 6. Insert Adjustment Record
- Source: Validated input values.
- Action: INSERT into
cash_receipt_adjustment. - Logic:
- Set
cash_receipt_id = :cash_receipt_id. - Set
adjustment_type_cd = 'ADJ'. - Set
adjustment_amt = :adjustment_amt. - Set
comment = :comment. - Set
posting_status_cd = 'U'. - Set
created_by = actorId,updated_by = actorId.
- Set
Step 7. Reduce Split Amount
- Source:
adjustment_amtfrom caller; currentsplit_amt. - Action: UPDATE
cash_receipt_splitSETsplit_amt = split_amt - :adjustment_amt,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :cash_receipt_split_id. - Logic:
- Direct arithmetic update.
Step 8. Recalculate Receipt Net Amount
- Source: SUM of
cash_receipt_adjustment.adjustment_amtWHEREcash_receipt_id = :cash_receipt_id. - Action: UPDATE
cash_receiptSETnet_receipt_amt = :calculated_net,updated_by = actorId,updated_dt = NOW(). - Logic:
net_receipt_amt = receipt_amt - SUM(adjustment_amt)across all adjustments for the receipt.
Side-effects:
cash_receipt_split.split_amtis decreased byadjustment_amt.cash_receipt.net_receipt_amtis decreased accordingly.
Postconditions:
cash_receipt_adjustmentrecord exists withposting_status_cd = 'U'.cash_receipt_split.split_amtreflects the reduced amount.cash_receipt.net_receipt_amt = cash_receipt.receipt_amt - SUM(adjustment_amt).
2.15 Delete Adjustment
Operation: deleteAdjustment
Trigger: User removes an adjustment from the receipt detail form.
Input Parameters:
cash_receipt_adjustment_id:integer(required) —cash_receipt_adjustment.cash_receipt_adjustment_idcash_receipt_id:integer(required) —cash_receipt.cash_receipt_idcash_receipt_split_id:integer(required) — the split to restoreactorId: String — identifier for the user performing the operation
Removes an unposted adjustment and restores the associated split's amount.
Step 1. Validate Receipt Is Not Voided
- Source:
cash_receiptWHEREcash_receipt_id = :cash_receipt_id. - Action: SELECT only.
- Logic:
- Reject if
posting_status_cd = 'V'.
- Reject if
Step 2. Validate Adjustment Can Be Deleted
- Source:
cash_receipt_adjustmentWHEREcash_receipt_adjustment_id = :cash_receipt_adjustment_id. - Action: SELECT only.
- Logic:
- Reject if not found.
- Reject if
posting_status_cd = 'P'(posted adjustments are immutable). - Reject if
adjustment_type_cd = 'TR'(transfer adjustments cannot be deleted).
Step 3. Restore Split Amount
- Source:
adjustment_amtfrom step 2. - Action: UPDATE
cash_receipt_splitSETsplit_amt = split_amt + :adjustment_amt,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :cash_receipt_split_id. - Logic:
- Reverses the reduction applied when the adjustment was created.
Step 4. Delete Adjustment Record
- Source:
cash_receipt_adjustment_id. - Action: DELETE from
cash_receipt_adjustmentWHEREcash_receipt_adjustment_id = :cash_receipt_adjustment_id. - Logic:
- Hard delete.
Step 5. Recalculate Receipt Net Amount
- Source: SUM of remaining
cash_receipt_adjustment.adjustment_amtfor the receipt. - Action: UPDATE
cash_receiptSETnet_receipt_amt = :recalculated_net,updated_by = actorId,updated_dt = NOW(). - Logic:
- Same formula as 2.14 Create Adjustment step 8.
Side-effects:
cash_receipt_split.split_amtis restored by the deleted adjustment amount.cash_receipt.net_receipt_amtis increased accordingly.
Postconditions:
cash_receipt_adjustmentWHEREcash_receipt_adjustment_id = :idno longer exists.cash_receipt.net_receipt_amtreflects the removal of the deleted adjustment.
2.16 Create Transfer (Inter-Bank)
Operation: createTransfer
Trigger: User initiates a "Transfer" action on a receipt, specifying a destination bank account and transfer amount.
Input Parameters:
cash_receipt_id:integer(required) — sourcecash_receipt.cash_receipt_idcash_receipt_split_id:integer(required) — source split to debittransfer_amt:decimal(15,2)(required) — amount to transfer; must be > 0dest_bank_account_id:integer(required) — destinationbank_account.bank_account_idactorId: String — identifier for the user performing the operation
Transfers funds from one receipt to a new receipt on a different bank account, with optional FX conversion.
Step 1. Validate Amount Is Positive
- Source: Caller-supplied
transfer_amt. - Action: No write.
- Logic:
- Reject if
transfer_amt <= 0.
- Reject if
Step 2. Validate Source Receipt Is Not Voided
- Source:
cash_receiptWHEREcash_receipt_id = :cash_receipt_id. - Action: SELECT only.
- Logic:
- Reject if
posting_status_cd = 'V'.
- Reject if
Step 3. Validate Destination Bank Account
- Source:
bank_accountWHEREbank_account_id = :dest_bank_account_id. - Action: SELECT only.
- Logic:
- Reject if not found or
active_ind = false.
- Reject if not found or
Step 4. Validate Source Split and Worksheet
- Source:
cash_receipt_splitand associated worksheet for:cash_receipt_split_id. - Action: SELECT only.
- Logic:
- Verify split belongs to the source receipt.
- Worksheet must be in Draft status or not exist.
Step 5. Validate Transfer Does Not Exceed Available Balance
- Source: Applied amount floor for the source split.
- Action: No write.
- Logic:
transfer_amt <= split_amt - applied_amt(with 0.005 tolerance).
Step 6. Compute FX Conversion
- Source: Currency codes from source receipt and destination bank account; current FX rates.
- Action: No write. In-memory.
- Logic:
fx_rate = dest_currency_rate / source_currency_rate(both relative to USD).dest_amount = transfer_amt * fx_rate.
Step 7. Create Transfer Adjustment on Source Receipt
- Source: Computed values from step 6; destination bank name.
- Action: INSERT into
cash_receipt_adjustment. - Logic:
- Set
cash_receipt_id = :cash_receipt_id. - Set
adjustment_type_cd = 'TR'. - Set
adjustment_amt = :transfer_amt. - Set
comment = 'Transferred to {dest_bank_name}'. - Set
posting_status_cd = 'U'.
- Set
Step 8. Reduce Source Split Amount
- Source:
transfer_amt. - Action: UPDATE
cash_receipt_splitSETsplit_amt = split_amt - :transfer_amt,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_split_id = :cash_receipt_split_id. - Logic:
- Direct arithmetic update.
Step 9. Recalculate Source Receipt Net Amount
- Source: SUM of adjustments for source receipt.
- Action: UPDATE
cash_receipton source. - Logic:
- Same formula as 2.14 Create Adjustment step 8.
Step 10. Destination Receipt
This will come in from the destination bank as a new deposit.
2.17 Lock / Unlock Cash Receipt
Operation: lockCashReceipt / unlockCashReceipt
Trigger: User opens a cash receipt for editing (lock) or closes/saves the form (unlock).
Input Parameters:
cash_receipt_id:integer(required) —cash_receipt.cash_receipt_iduser_id:integer(required for lock) —cash_receipt.locked_by_user_id; the user claiming the lockactorId: String — identifier for the user performing the operation
Provides optimistic concurrency control by recording which user is currently editing a receipt.
Step 1. Lock
- Source:
cash_receipt_id,user_id. - Action: UPDATE
cash_receiptSETlocked_by_user_id = :user_id,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_id = :cash_receipt_id. - Logic:
locked_by_user_idis set to the requesting user's ID.
Step 2. Unlock
- Source:
cash_receipt_id. - Action: UPDATE
cash_receiptSETlocked_by_user_id = NULL,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_id = :cash_receipt_id. - Logic:
locked_by_user_idis cleared toNULL.
Side-effects:
- None beyond the
locked_by_user_idfield change.
Postconditions:
- After lock:
cash_receipt.locked_by_user_id = :user_id. - After unlock:
cash_receipt.locked_by_user_id IS NULL.
2.18 Delete Cash Receipt
Operation: deleteCashReceipt
Trigger: User selects a receipt and chooses the "Delete" action.
Input Parameters:
cash_receipt_id:integer(required) —cash_receipt.cash_receipt_idactorId: String — identifier for the user performing the operation
Hard-deletes a cash receipt and its child records. Only succeeds if the receipt has no cash applications.
Step 1. Verify Receipt Exists
- Source:
cash_receiptWHEREcash_receipt_id = :cash_receipt_id. - Action: SELECT only.
- Logic:
- Reject if not found.
Step 2. Delete Receipt
- Source:
cash_receipt_id. - Action: DELETE from
cash_receiptWHEREcash_receipt_id = :cash_receipt_id. - Logic:
- If the delete affects zero rows (e.g., due to FK constraints from
cash_receipt_applicationrecords), return error "Cannot delete cash receipt with applications."
- If the delete affects zero rows (e.g., due to FK constraints from
**PoC Artifact:**
The PoC uses a simple hard-delete with no explicit cascade cleanup of splits, worksheets, or references before the receipt delete. Database FK constraints (ON DELETE CASCADE or RESTRICT) determine the actual behavior. Production should explicitly delete child records in the correct order or use a soft-delete pattern.
Side-effects:
- Cascades to related child records depending on FK constraint configuration.
Postconditions:
cash_receiptWHEREcash_receipt_id = :cash_receipt_idno longer exists.
2.19 Create Write-Off Cash Receipt
Operation: createWriteOffCashReceipt
Trigger: A write-off packet transitions to approved status, triggering the write-off service to create the corresponding receipt.
Input Parameters:
packet_id:varchar(required) —write_off_packet.write_off_packet_idactorId: String — identifier for the system process performing the operation
System-generated receipt created when a write-off packet is approved. Creates a receipt, split, auto-approved worksheet, and applications for all receivables in the packet.
Step 1. Fetch Packet Receivables
- Source:
packet_receivableWHEREwrite_off_packet_id = :packet_id. - Action: SELECT only.
- Logic:
- Collect all billing item references in the packet.
Step 2. Build Billing Item Map
- Source:
billing_itemandbilling_item_detailfor each receivable. - Action: SELECT only.
- Logic:
- For each billing item, fetch the REV detail balance and PAY detail balance.
Step 3. Calculate Total Receipt Amount
- Source: Billing item balances from step 2.
- Action: No write. In-memory.
- Logic:
total = SUM(rev_balance + pay_balance)across all billing items.
Step 4. Create Cash Receipt
- Source:
totalfrom step 3; currency from billing items. - Action: INSERT into
cash_receipt. - Logic:
- Set
posting_status_cd = 'U'. - Set
receipt_type_cd = 'WRITE_OFF'. - Set
original_receipt_amt = :total. - Set
receipt_amt = :total. - Set
net_receipt_amt = :total. - Set
currency_cdfrom billing items. - Set
cash_receipt_comment = 'Write-off for packet'.
- Set
Step 5. Link Packet to Receipt
- Source: New
cash_receipt_idfrom step 4. - Action: UPDATE
write_off_packetSETcash_receipt_id = :new_receipt_id. - Logic:
- Establishes the 1:1 link between packet and receipt.
Step 6. Create Split
- Source:
cash_receipt_idfrom step 4;totalfrom step 3. - Action: INSERT into
cash_receipt_split. - Logic:
- Set
split_amt = :total. - Set
split_status_cd = 'U'(Unposted — write-off splits are not subject to the standard N/S/A workflow). - Set
split_sequence = 1.
- Set
Step 7. Create Auto-Approved Worksheet
- Source:
cash_receipt_split_idfrom step 6. - Action: INSERT into
cash_receipt_worksheet; UPDATE to set approval metadata. - Logic:
- INSERT with
cash_receipt_worksheet_status_cd = 'A'(Approved — skips Draft/Applied/Settled for write-offs). - Set
current_item_ind = true. - UPDATE to set
approved_dt = NOW(),approved_by = actorId.
- INSERT with
Step 8. Create Applications for Each Billing Item
- Source: Billing item balances from step 2;
cash_receipt_worksheet_idfrom step 7. - Action: INSERT into
cash_receipt_applicationfor each balance > 0. - Logic:
- If REV balance >= 0.01: INSERT
cash_receipt_applicationwithbilling_item_detail_id = :rev_detail_id,cash_receipt_amt_applied = :rev_balance. - If PAY balance >= 0.01: INSERT
cash_receipt_applicationwithbilling_item_detail_id = :pay_detail_id,cash_receipt_amt_applied = :pay_balance.
- If REV balance >= 0.01: INSERT
Step 9. Close Billing Items
- Source: Each
billing_itemand its REV/PAY details. - Action: UPDATE
billing_itemandbilling_item_detailfor each item. - Logic:
- UPDATE
billing_itemSETopen_item_ind = false. - UPDATE
billing_item_detail(REV) SETwrite_off_status_cd = 'WRITTEN_OFF',write_off_packet_id = :packet_id,write_off_dt = NOW(),exclude_from_cecl_ind = true. - UPDATE
billing_item_detail(PAY) SETwrite_off_status_cd = 'WRITTEN_OFF',write_off_packet_id = :packet_id,write_off_dt = NOW(),exclude_from_cecl_ind = true.
- UPDATE
Side-effects:
write_off_packet.cash_receipt_idis set to the new receipt.billing_item.open_item_indis set tofalsefor all items in the packet.billing_item_detail.write_off_status_cdis set to'WRITTEN_OFF'for both REV and PAY details.
Postconditions:
- A
cash_receiptwithreceipt_type_cd = 'WRITE_OFF'exists and is linked to the packet. - An Approved worksheet with applications covering all written-off balances exists.
- All billing items in the packet are closed with
write_off_status_cd = 'WRITTEN_OFF'.
2.20 Create Write-Off Reversal Worksheet
Operation: createWriteOffReversalWorksheet
Trigger: A write-off packet is recovered (status changes to RECOVERED), triggering the write-off service to create a reversal worksheet.
Input Parameters:
packet_id:varchar(required) —write_off_packet.write_off_packet_idactorId: String — identifier for the system process performing the operation
Creates a reversal worksheet when a write-off packet is recovered, negating all original applications and reopening billing items.
Step 1. Find Cash Receipt for the Packet
- Source:
write_off_packetWHEREwrite_off_packet_id = :packet_id. - Action: SELECT only.
- Logic:
- Capture
cash_receipt_id.
- Capture
Step 2. Find Original Current Worksheet
- Source:
cash_receipt_worksheetjoined tocash_receipt_splitWHEREcash_receipt_id = :cash_receipt_idANDcurrent_item_ind = true. - Action: SELECT only.
- Logic:
- Capture
cash_receipt_worksheet_idand all associated applications.
- Capture
Step 3. Get Original Applications
- Source:
cash_receipt_applicationWHEREcash_receipt_worksheet_id = :original_worksheet_id. - Action: SELECT only.
- Logic:
- Collect all application records for reversal.
Step 4. Mark Original Worksheet as Non-Current
- Source:
original_worksheet_idfrom step 2. - Action: UPDATE
cash_receipt_worksheetSETcurrent_item_ind = false,updated_by = actorId,updated_dt = NOW()WHEREcash_receipt_worksheet_id = :original_worksheet_id. - Logic:
- Marks the original as historical.
Step 5. Create Reversal Worksheet
- Source:
cash_receipt_split_idfrom step 2. - Action: INSERT into
cash_receipt_worksheet; UPDATE to set approval and linkage metadata. - Logic:
- INSERT with
cash_receipt_worksheet_status_cd = 'A'(terminal — no further processing on the reversal). - Set
current_item_ind = false. - UPDATE to set
approved_dt = NOW(),approved_by = actorId,previous_worksheet_id = :original_worksheet_id.
- INSERT with
Step 6. Create Reversal Applications
- Source: Original applications from step 3;
reversal_worksheet_idfrom step 5. - Action: INSERT into
cash_receipt_applicationfor each original application. - Logic:
- Set
cash_receipt_worksheet_id = :reversal_worksheet_id. - Set
cash_receipt_amt_applied = -(original amount). - Set
reversal_of_application_id = :original_app_id. - Set
reversal_reason_cd = 'PACKET_RECOVERY'.
- Set
Step 7. Update Billing Item Detail Statuses
- Source: Each reversed application's
billing_item_detail_id. - Action: UPDATE
billing_item_detailfor each. - Logic:
- Set
write_off_status_cd = 'RECOVERED'. - Set
recovered_dt = NOW().
- Set
Step 8. Reopen Billing Items
- Source: Distinct
billing_item_idvalues from the reversed applications. - Action: UPDATE
billing_itemfor each unique item. - Logic:
- Set
open_item_ind = true.
- Set
Side-effects:
- Original worksheet's
current_item_indis set tofalse. billing_item_detail.write_off_status_cdis set to'RECOVERED'for all reversed details.billing_item.open_item_indis restored totrue.
Postconditions:
- A reversal worksheet exists with negated applications and
cash_receipt_worksheet_status_cd = 'A'. - All billing items from the original write-off are reopened with
write_off_status_cd = 'RECOVERED'.
3. Business Rules & Logic
3.1 Split Modifiability
Business rule: A split can be structurally modified (amount changed, funds transferred, deleted) only when it has no worksheet or when its worksheet is in Draft or Approved status. Voided splits cannot be modified except for notes-only updates. Splits whose worksheets are in Applied, Settled, Returned, or Submitted status cannot have their amounts changed.
Data-level enforcement:
- Read:
cash_receipt_split.split_status_cd;cash_receipt_worksheet.cash_receipt_worksheet_status_cdfor the split. - Guard: If
split_status_cd = 'V': block all modifications except notes-only updates (amountandsource_file_upload_idabsent from payload). If worksheet status is'P','T','R', or'S': block amount and structural changes. - Write: Allowed when
split_status_cd != 'V'AND (no worksheet ORcash_receipt_worksheet_status_cd IN ('D', 'A')).
3.2 Split Amount Floor
Business rule: A split's amount cannot be reduced below the total amount already applied to receivables on its worksheet. You cannot take money away from a split that has been applied.
Data-level enforcement:
- Read:
cash_receipt_split.split_amt; SUM ofcash_receipt_application.cash_receipt_amt_appliedon the split's currentcash_receipt_worksheet. - Guard: If
new_amount < applied_amt, reject the operation. - Write:
split_amtmay only be set to a value >=applied_amt.
3.3 Split Total Invariant
Business rule: Every dollar of a receipt must be accounted for in a split. The sum of all split amounts must equal the receipt's net amount. Splits cannot be created from thin air — they must be carved from existing splits.
Data-level enforcement:
- Read:
cash_receipt.net_receipt_amt; SUM ofcash_receipt_split.split_amtWHEREcash_receipt_id = :id. - Guard:
ABS(net_receipt_amt - SUM(split_amt)) >= 0.005indicates a violation; log a warning and investigate. - Write: All split create and modify procedures carve amounts from existing splits to maintain this invariant.
3.4 Last-Split Protection
Business rule: The last remaining split for a receipt cannot be deleted. At least one split must always exist to maintain the receipt structure.
Data-level enforcement:
- Read: COUNT of
cash_receipt_splitWHEREcash_receipt_id = :id. - Guard: If count <= 1, reject the deletion.
- Write: Not applicable — this is a guard-only rule.
3.5 Receipt Posting-Status Edit Guards
Business rule: Once a receipt has been posted to the General Ledger, most fields become immutable. Voided receipts are read-only except for administrative reference and comment fields.
Data-level enforcement:
- Read:
cash_receipt.posting_status_cd. - Guard:
- If
posting_status_cd = 'U': all fields editable. - If
posting_status_cd = 'P': onlycash_receipt_commentandnet_receipt_amt(via adjustment recalculation) are editable; any other field in the update payload triggers an error. - If
posting_status_cd = 'V': onlycash_receipt_refandcash_receipt_commentare editable; any other field triggers an error.
- If
- Write: Field-level guards enforced before the UPDATE.
3.6 Voided Receipt Block
Business rule: A voided receipt cannot have adjustments, transfers, or amount changes applied to it. Only reference and comment fields remain accessible.
Data-level enforcement:
- Read:
cash_receipt.posting_status_cd. - Guard: Any procedure that modifies
cash_receipt(except comment and reference updates) checksposting_status_cd != 'V'before proceeding. - Write: Not applicable — this is a guard-only rule.
3.7 Adjustment Immutability
Business rule: Adjustments cannot be deleted once they have been posted to the General Ledger. Transfer-type adjustments (adjustment_type_cd = 'TR') can never be deleted regardless of posting status.
Data-level enforcement:
- Read:
cash_receipt_adjustment.posting_status_cd;cash_receipt_adjustment.adjustment_type_cd. - Guard: If
posting_status_cd = 'P', reject deletion. Ifadjustment_type_cd = 'TR', reject deletion. - Write: Not applicable — this is a guard-only rule.
3.8 Bank Reference Deduplication
Business rule: A given bank reference ID can produce at most one cash receipt per bank account. When a bank file is re-processed or a status-update file arrives, the system updates the existing receipt rather than creating a duplicate.
Data-level enforcement:
- Read:
cash_receiptWHERE(bank_account_id, bank_ref_id)matches the incoming transaction. - Guard: Unique index
uq_cash_receipt_bank_refon(bank_account_id, bank_ref_id)prevents duplicate inserts at the database level. - Write:
syncFromBankTransactionuses the composite key lookup to decide between UPDATE (existing) and INSERT (new).
3.9 Write-Off Receipt Type Guard
Business rule: Write-off receipts (receipt_type_cd = 'WRITE_OFF') are created exclusively by the write-off approval workflow. Users cannot create them manually. These receipts follow a different worksheet lifecycle (auto-approved, no settlement required).
Data-level enforcement:
- Read:
cash_receipt.receipt_type_cd. - Guard: Manual receipt creation always sets
receipt_type_cd = 'NORMAL'. The'WRITE_OFF'code is only set programmatically bycreateWriteOffCashReceipt. - Write:
receipt_type_cd = 'WRITE_OFF'must only be set by the write-off service.
3.10 Currency Conversion Requirement
Business rule: When a receipt is recorded in a different working currency than the currency it was received in, an FX rate must be provided. The system will not guess at the conversion.
Data-level enforcement:
- Read:
cash_receipt.currency_cd;cash_receipt.original_currency_cd;cash_receipt.fx_rate. - Guard: If
currency_cd != original_currency_cdANDcurrency_cd IS NOT NULL: reject iffx_rateis missing or <= 0. - Write:
receipt_amt = original_receipt_amt * fx_ratewhen currencies differ.
4. Field Mapping & Transformation
4.1 Bank Transaction to Cash Receipt
When creating a cash receipt from a bank transaction (syncFromBankTransaction), fields are mapped as follows:
| Source Table.Field | Target Table.Field | Transform |
|---|---|---|
bank_transaction.status | cash_receipt.entry_status | Copied as-is (PDNG or BOOK) |
bank_transaction.bank_reference_id | cash_receipt.bank_ref_id | Copied as-is |
bank_transaction.bank_reference_id | cash_receipt.cash_receipt_ref | Same value used for receipt reference |
bank_transaction.bank_transaction_id | cash_receipt.bank_transaction_id | Copied as-is |
bank_transaction.booking_date | cash_receipt.booking_date | Copied as-is |
bank_transaction.booking_date | cash_receipt.deposit_date | Same value used for deposit date |
bank_transaction.remittance_info | cash_receipt.remittance_info | Snapshot at creation; not updated afterward |
bank_transaction.bank_account_id | cash_receipt.bank_account_id | Copied as-is |
bank_transaction.file_name | cash_receipt.filename | Copied as-is |
bank_transaction.amount | cash_receipt.original_receipt_amt | Copied as-is |
bank_transaction.amount | cash_receipt.receipt_amt | Same (no FX conversion for bank-originated receipts) |
bank_transaction.amount | cash_receipt.net_receipt_amt | Same (no adjustments on creation) |
bank_transaction.currency | cash_receipt.original_currency_cd | Copied as-is |
bank_transaction.currency | cash_receipt.currency_cd | Same (no conversion) |
| — | cash_receipt.posting_status_cd | Defaulted to 'U' |
| — | cash_receipt.receipt_type_cd | Defaulted to 'NORMAL' |
4.2 Net Receipt Amount Calculation
| Source Table.Field | Target Table.Field | Transform |
|---|---|---|
cash_receipt.receipt_amt | cash_receipt.net_receipt_amt | Base value before adjustments |
cash_receipt_adjustment.adjustment_amt (SUM) | cash_receipt.net_receipt_amt | Subtracted from receipt_amt; net_receipt_amt = receipt_amt - SUM(adjustment_amt) |
4.3 Receipt Amount from Currency Conversion
| Source Table.Field | Target Table.Field | Transform |
|---|---|---|
cash_receipt.original_receipt_amt | cash_receipt.receipt_amt | When currency_cd = original_currency_cd: copied as-is |
cash_receipt.original_receipt_amt * cash_receipt.fx_rate | cash_receipt.receipt_amt | When currency_cd != original_currency_cd: multiplied by FX rate, rounded to decimal(15,2) |
4.4 Transfer FX Conversion
| Source Table.Field | Target Table.Field | Transform |
|---|---|---|
Source cash_receipt.currency_cd rate (relative to USD) | — | Determines source exchange rate |
bank_account.currency_cd rate (relative to USD) for destination | — | Determines destination exchange rate |
Computed fx_rate = dest_rate / source_rate | cash_receipt.fx_rate (destination) | Applied to compute destination amount |
transfer_amt * fx_rate | cash_receipt.original_receipt_amt (destination) | Final destination receipt amount |
4.5 Write-Off Original to Reversal Applications
When createWriteOffReversalWorksheet creates reversal applications, each original application is negated:
| Source Table.Field | Target Table.Field | Transform |
|---|---|---|
cash_receipt_application.cash_receipt_worksheet_id | cash_receipt_application.cash_receipt_worksheet_id | Set to the new reversal worksheet ID |
cash_receipt_application.billing_item_detail_id | cash_receipt_application.billing_item_detail_id | Copied as-is |
cash_receipt_application.cash_receipt_amt_applied | cash_receipt_application.cash_receipt_amt_applied | Negated (-1 * original amount) |
cash_receipt_application.cash_receipt_application_id | cash_receipt_application.reversal_of_application_id | References the original application |
| — | cash_receipt_application.reversal_reason_cd | Set to 'PACKET_RECOVERY' |
5. Cross-References
| Document | Relationship |
|---|---|
| Cash Receipts Data Model | Schema definitions, status lifecycles, validation constraints, and code-master values for the four tables covered by these procedures. |
| Cash Receipts Queries | All read operations and computed values referenced by the procedures in this document. |
| Worksheets Procedures | Worksheet creation is a side-effect of split creation. Worksheet deletion is a prerequisite for split deletion. The worksheet lifecycle (D, P, T, A, R) gates split modifiability. |
| Write-Offs Procedures | Write-off packet approval triggers createWriteOffCashReceipt. Write-off recovery triggers createWriteOffReversalWorksheet. |
| Bank Ingestion Procedures | Bank file parsing feeds bank_transaction records to syncFromBankTransaction. |
| Accounting Procedures | The GL posting batch job reads unposted receipts and transitions posting_status_cd from 'U' to 'P'. resetReceiptsForGLReposting reverses this when needed. |
| Billing Items Procedures | createWriteOffCashReceipt and createWriteOffReversalWorksheet update billing_item.open_item_ind and billing_item_detail.write_off_status_cd. |
| Tax & Withholding Procedures | Tax warning calculations store results on splits via updateSplitTaxWarning. |