Skip to content

Write-Offs Workflow

1. Executive Summary

Purpose

The write-offs workflow manages the process of identifying, documenting, approving, and executing write-offs for uncollectable receivables within the UTA Client Payments system. When a buyer fails to pay and a receivable is deemed uncollectable — whether through aging, buyer bankruptcy, exhausted collection efforts, or agent judgment — UTA forgoes its commission revenue on that receivable. The workflow groups one or more REV-type billing item details into a write-off packet, routes the packet through a multi-level approval chain scaled to the total dollar amount, executes the write-off against the general ledger upon final approval, and provides a recovery path if circumstances change. Only REV (commission) detail lines are ever written off; the PAY (client payout) obligation is unaffected because the company absorbs the loss on its own commission.

Scope

Covered:

  • Creating and managing write-off packets (write_off_packet)
  • Searching for and adding eligible REV-type receivables to a packet (packet_receivable)
  • Assigning eligibility criteria to each receivable
  • Attaching supporting documents at the packet and receivable level
  • Submitting a packet for approval and navigating the multi-level approval chain (Agent → Dept Head → VP Client Accounting → CFO → MD)
  • Amount-based routing: packets under $50,000 complete at VP level; $50,000–$250,000 require CFO; over $250,000 require MD
  • Rejecting and resubmitting a packet after corrections
  • Write-off execution on final approval: billing_item_detail.write_off_status_cd set to 'WRITTEN_OFF'; offsetting WRITE_OFF type cash_receipt created with an auto-approved worksheet
  • Recovery of a completed write-off: billing_item_detail.write_off_status_cd reverted; reversal worksheet created for GL posting
  • Viewing the status timeline and comment history for a packet

Not covered (documented separately):

Key Objectives

  • Provide a controlled, auditable approval path before any receivable is written off, ensuring appropriate financial oversight scaled to the dollar amount involved.
  • Enforce the REV-only constraint: PAY obligations to clients remain unchanged regardless of collectability; only UTA's commission is written off.
  • Produce a complete, immutable audit trail via packet_status_history covering every approval, rejection, and recovery action.
  • Create the necessary GL artifacts (offsetting cash receipt and worksheet on approval; reversal worksheet on recovery) so that write-off impacts are accurately reflected in the general ledger.

2. Process Overview

mermaid
flowchart TD
    A[Agent identifies uncollectable REV receivables] --> B[Create Write-Off Packet]
    B --> C[Add eligible REV receivables]
    C --> D[Assign eligibility criteria per receivable]
    D --> E[Attach supporting documents]
    E --> F{Ready to submit?}
    F -->|Yes - all criteria set| G[Submit Packet]
    F -->|No - missing criteria| D
    G --> H[Agent Review]
    H --> I{Agent decision}
    I -->|Approve| J[Dept Head Review]
    I -->|Reject| R1[REJECTED_AGENT]
    R1 --> RE[User corrects and Resubmits]
    RE --> H
    J --> K{Dept Head decision}
    K -->|Approve| L[VP Client Accounting Review]
    K -->|Reject| R2[REJECTED_DH]
    R2 --> RE
    L --> M{VP decision}
    M -->|Approve + total < $50K| N[COMPLETE]
    M -->|Approve + total >= $50K| O[CFO Review]
    M -->|Reject| R3[REJECTED_VP]
    R3 --> RE
    O --> P{CFO decision}
    P -->|Approve + total <= $250K| N
    P -->|Approve + total > $250K| Q[MD Review]
    P -->|Reject| R4[REJECTED_CFO]
    R4 --> RE
    Q --> S{MD decision}
    S -->|Approve| N
    S -->|Reject| R5[REJECTED_MD]
    R5 --> RE
    N --> T[Write-Off Executed: billing_item_detail WRITTEN_OFF + WRITE_OFF cash_receipt created]
    T --> U{Recovery needed?}
    U -->|Yes| V[RECOVERED: reversal worksheet created]
    U -->|No| W[Terminal: COMPLETE]

Walkthrough

  1. Identify eligible receivables — A user (typically an agent or cash manager) identifies REV-type billing item details with outstanding balances that are deemed uncollectable. The receivable must be current_item_ind = true, open_item_ind = true, write_off_status_cd = 'NOT_WRITTEN_OFF', and not already assigned to another active packet. Receivables with billing_item_detail_amt below $100.00 are not eligible.

  2. Create the packet — The user creates a new write_off_packet, providing a unique name and selecting the client. All receivables in a packet must belong to this single client. The packet is created in DRAFT status with packet_status_cd = 'DRAFT' and current_approver_role = null.

  3. Add receivables and set eligibility — From the packet detail page, the user opens the Search Receivables dialog to find eligible open REV receivables for the client. Selected receivables are bulk-added as packet_receivable rows. Each receivable must have an eligibility_criteria_cd assigned (AGED, UNCOLLECTIBLE, BANKRUPTCY, or AGENT_REQUEST) before the packet can be submitted. A packet-level eligibility can be set in the header, which auto-populates all receivables that currently have a blank eligibility. Packet aggregates (total_commission_amt, receivable_count) are recalculated each time receivables are added or removed.

  4. Attach supporting documents — Users upload supporting documents at the packet level (shared across all receivables) via the Documents button in the packet header, or at the individual receivable level via the paperclip icon on each receivable row. Documents are stored via the file upload service linked to the write_off_packet or packet_receivable entity.

  5. Submit for approval — When all receivables have eligibility criteria, the user clicks "Submit for Approval." Validation runs: the packet must have at least one receivable; all receivables must be REV-type; all receivables must have eligibility_criteria_cd; no receivable may be in another active packet; all receivables must belong to the packet's client. On success, packet_status_cd advances to 'SUBMITTED' and current_approver_role is set to 'AGENT'.

  6. Multi-level approval chain — Approvers access the Approval Dashboard (/write-offs/approvals) filtered by their role. Each approver can approve (with an optional comment) or reject (with a required reason). Approval advances packet_status_cd through APPROVED_AGENTAPPROVED_DHAPPROVED_VP, then to COMPLETE (if total < $50,000), or continues to APPROVED_CFOCOMPLETE (if total $50,000–$250,000), or APPROVED_CFOAPPROVED_MDCOMPLETE (if total > $250,000). Every action is recorded in packet_status_history.

  7. Rejection and resubmission — Any approver can reject, setting packet_status_cd to the role-specific rejected status (e.g., REJECTED_VP). The user can make corrections to the packet (which is editable again in rejected statuses) and click "Resubmit for Approval," which resets the approval chain from AGENT level.

  8. Write-off execution (COMPLETE) — On final approval, billing_item_detail.write_off_status_cd is set to 'WRITTEN_OFF' and write_off_dt is populated for each packet receivable. An offsetting cash_receipt of type WRITE_OFF is created, along with an auto-approved worksheet containing applications for all written-off receivables. write_off_packet.cash_receipt_id is populated with a link to this receipt, which is visible in the packet header as a link to the worksheet.

  9. Recovery — If the write-off needs to be reversed (e.g., the buyer eventually pays), an approver can initiate recovery. This sets packet_status_cd to 'RECOVERED' (terminal state), reverts billing_item_detail.write_off_status_cd, populates recovered_dt, and creates a reversal worksheet to post the offsetting GL entries. After recovery, the packet and all its receivables are read-only.


3. Business Rules

3.1 REV-Only Write-Off Constraint

Business rule: Only billing_item_detail records with billing_item_detail_type_cd = 'REV' may be written off. PAY-type details are never written off because the client's payout obligation persists regardless of collectability — UTA absorbs the loss on its own commission. Attempting to add a PAY-type receivable to a packet is rejected.

Foundation reference: Write-Offs Data Model — Business Validation

Workflow context: The Search Receivables dialog filters the eligible receivable list to items with a positive REV balance (revBalance > 0). The system uses each item's revId (the billing_item_detail_id for the REV detail) when adding to the packet. The submission validator also enforces this rule and will block submission if a non-REV receivable is present.


3.2 Single-Packet Exclusivity

Business rule: A given billing_item_detail may belong to at most one active write-off packet at a time. "Active" means any packet_status_cd other than 'RECOVERED'. Attempting to add a receivable that is already in an active packet is rejected at add-time.

Foundation reference: Write-Offs Data Model — Business Validation


3.3 Client Consistency

Business rule: All receivables within a single packet must belong to the same client identified by write_off_packet.client_id. Receivables from a different client may not be mixed into the same packet.

Foundation reference: Write-Offs Data Model — Business Validation

Workflow context: The Search Receivables dialog automatically scopes its search to the packet's client (clientId), preventing cross-client additions. The submission validator checks this rule as a final guard before DRAFTSUBMITTED.


3.4 Eligibility Criteria Requirement

Business rule: Every packet_receivable must have a non-null eligibility_criteria_cd before the packet can be submitted. Valid codes are AGED, UNCOLLECTIBLE, BANKRUPTCY, and AGENT_REQUEST.

Foundation reference: Write-Offs Data Model — Business Validation and Write-Offs Procedures — Submission

Workflow context: The submission validation blocks progression if any receivable has a blank eligibility, surfacing the error "Receivable must have eligibility criteria" per receivable. A bulk eligibility update is available: setting the packet-level eligibility in the header applies the selected code to all receivables that currently have a blank eligibility_criteria_cd.


3.5 Minimum Amount Threshold

Business rule: Receivables with billing_item_detail_amt below $100.00 are not eligible for write-off. This prevents the approval workflow overhead for trivially small amounts.

Foundation reference: Write-Offs Data Model — Business Validation


3.6 Amount-Based Approval Routing

Business rule: The depth of the approval chain is determined by write_off_packet.total_commission_amt. Packets under $50,000 require only Agent, Dept Head, and VP Client Accounting. Packets $50,000–$250,000 also require CFO approval. Packets over $250,000 additionally require Managing Director approval.

Foundation reference: Write-Offs Data Model — Approval Amount Thresholds

Workflow context: The total_commission_amt is denormalized on write_off_packet and recalculated whenever receivables are added or removed. Approvers only see packets where current_approver_role matches their role. The status machine evaluates the total at each approval step to determine the next state.


3.7 Rejection Requires a Reason

Business rule: Any approver who rejects a packet must provide a non-empty rejection reason. The reason is stored in write_off_packet.rejection_reason and in packet_status_history.comment_text.

Foundation reference: Write-Offs Data Model — Packet Status Lifecycle

Workflow context: The Reject dialog has a required "Rejection Reason" text area; the Reject button remains disabled until text is entered. The rejection history is displayed in the packet header's "Rejected" metadata field, with a comment icon that opens the full rejection history dialog.


3.8 Packet Editability

Business rule: A packet is editable (receivables can be added or removed, eligibility criteria can be changed, the packet name can be changed) only when packet_status_cd = 'DRAFT' or the status is any REJECTED_* status. Once submitted, the packet is read-only until it is rejected back.

Foundation reference: Write-Offs Procedures — Packet Lifecycle

Workflow context: The "Search Receivables" button, eligibility dropdowns, trash icons, and editable packet name are visible only when the packet is in an editable status. The header eligibility selector and the bulk update function are also gated on canEdit.


3.9 Delete Restricted to Draft

Business rule: A packet can only be deleted when packet_status_cd = 'DRAFT'. Submitted, approved, rejected, complete, or recovered packets cannot be deleted.

Foundation reference: Write-Offs Procedures — Packet Lifecycle

Workflow context: The delete (trash) icon in the packet list is visible only when packetStatusCd === 'DRAFT'. Deletion cascades to remove all associated packet_receivable, packet_status_history, and packet_document rows.


3.10 Recovery Only from COMPLETE

Business rule: A write-off can only be recovered when packet_status_cd = 'COMPLETE'. Recovery reverts billing_item_detail.write_off_status_cd and creates a reversal worksheet. Recovered packets enter the terminal RECOVERED state and cannot be further modified.

Foundation reference: Write-Offs Data Model — Transition COMPLETE → RECOVERED

Workflow context: The recovery action is gated on packet_status_cd = 'COMPLETE' and requires the same canApproveWorksheets permission as approval. Recovery reason is required.


3.11 Write-Off Execution Side Effects

Business rule: When a packet reaches COMPLETE, the write-off is executed: each billing_item_detail.write_off_status_cd is set to 'WRITTEN_OFF', write_off_dt is populated, write_off_packet_id is linked, and exclude_from_cecl_ind is set to true. An offsetting WRITE_OFF type cash_receipt is created along with an auto-approved worksheet and applications for all written-off receivables. write_off_packet.completed_dt and completed_by_user_id are populated.

Foundation reference: Write-Offs Data Model — Transition to COMPLETE and Write-Offs Procedures — Write-Off Execution

Workflow context: After the packet reaches COMPLETE, the header shows a "Worksheet: #N" link pointing to the auto-created write-off worksheet. This link is resolved by looking up the latest worksheet for the cash_receipt_id stored on the packet.

IMPORTANT

In the PoC implementation, the approval chain is simplified: WriteOffPacketService.approvePacket allows approving from DRAFT or SUBMITTED directly to an APPROVED state (bypassing the granular APPROVED_AGENTAPPROVED_DHAPPROVED_VP chain). The full multi-level chain is implemented in PacketApprovalService and the status machine but the simplified path is used by the packet detail page's "Approve" button. Production must route all approvals through PacketApprovalService with proper role validation.

NOTE

PoC Artifact: Documentation validation (validateDocumentation) is disabled in the PoC to simplify the demo workflow. Production must re-enable this rule so that each receivable has supporting documentation — either packet-level (when use_packet_document_ind = true) or receivable-level (packet_document rows with a packet_receivable_id).


4. Data Access & Operations References

4.1 Queries Used

OperationFoundation DocPurpose in This Workflow
searchPacketsSearch PacketsLoad all packets for the packet list page with client name, status, totals, and eligibility. Supports filter criteria for status, client, and date range.
getPacketWithDetailsGet Packet With DetailsLoad a single packet with enriched user name fields (created by, updated by, submitted by, completed by, rejected by, recovered by) for the packet detail header.
getReceivablesByPacketGet Receivables By PacketLoad all packet_receivable rows enriched with billing item, deal, buyer, and client data for the receivables table. Also used to count existing receivables when opening the Add Receivables dialog.
getHistoryWithUsersByPacketGet Status History With UsersLoad the full packet_status_history audit trail with user names for the status timeline display and approval history view.
searchEligibleReceivablesSearch Eligible ReceivablesFind open, current, REV-type billing item details for the client that are not yet in any active packet. Used by the Add Receivables dialog. Filters: current_item_ind = true, open_item_ind = true, REV balance > 0.
getPacketsPendingApprovalGet Packets Pending ApprovalRetrieve packets where current_approver_role matches the approver's role. Drives the Approval Dashboard table.
isPacketNameUniqueCheck Packet Name UniqueValidates uniqueness of write_off_packet.packet_name at create and rename time.
isReceivableInAnyPacketCheck Receivable In PacketChecks whether a billing_item_detail_id is already in an active packet before allowing it to be added.
getFilesForEntityTODO: Document in foundation/queries/write-offs.mdRetrieves uploaded file records linked to a write_off_packet or packet_receivable entity for display in the documents dialogs.

4.2 Procedures Used

OperationFoundation DocTrigger in This Workflow
createPacketCreate PacketUser submits the "Create New Write-Off Packet" form with a unique name and selected client.
updatePacketUpdate PacketUser edits the packet name (inline edit) or sets/clears the packet-level eligibility selector in the header.
deletePacketDelete PacketUser clicks the trash icon on a DRAFT packet in the packet list and confirms deletion.
bulkAddReceivablesToPacketAdd Receivable to PacketUser selects receivables in the Add Receivables dialog and clicks "Add to Packet."
removeReceivableFromPacketRemove Receivable from PacketUser clicks the trash icon on a receivable row (single) or selects multiple and clicks "Delete Selected."
updateReceivableEligibilityCriteriaUpdate EligibilityUser changes the eligibility dropdown on a single receivable row.
bulkUpdateBlankEligibilityBulk Update EligibilityUser sets the packet-level eligibility in the header, propagating the code to all receivables with a blank eligibility_criteria_cd.
recalculatePacketAggregatesRecalculate AggregatesTriggered automatically after any add, remove, or eligibility update on receivables to keep total_commission_amt and receivable_count current.
submitPacketSubmit PacketUser clicks "Submit for Approval" on a DRAFT packet after validation passes.
approvePacket (via PacketApprovalService)Approve PacketApprover clicks the approve icon on the Approval Dashboard and confirms in the approve dialog.
rejectPacket (via PacketApprovalService)Reject PacketApprover clicks the reject icon on the Approval Dashboard and provides a required rejection reason.
resubmitPacketResubmit PacketUser clicks "Resubmit for Approval" on a packet in any REJECTED_* status.
executeWriteOffExecute Write-OffCalled automatically during approvePacket when the packet transitions to COMPLETE. Updates billing_item_detail.write_off_status_cd and creates the offsetting cash receipt and worksheet.
recoverPacket (via PacketApprovalService)Recover PacketApprover triggers recovery on a COMPLETE packet with a required reason. Creates a reversal worksheet.
addPacketCommentTODO: Document in foundation/procedures/write-offs.mdUser opens the Comments dialog and saves a new note against the packet.
addReceivableCommentTODO: Document in foundation/procedures/write-offs.mdUser clicks the comment icon on a receivable row and saves a note.
uploadFileTODO: Document in foundation/procedures/write-offs.mdUser uploads a supporting document at the packet or receivable level via the Documents dialogs.

5. Key User Actions

5.1 Create Write-Off Packet

Preconditions:

  • User is authenticated.
  • A client exists in the system.
  • The intended packet name does not already exist (write_off_packet.packet_name unique constraint).

Procedure reference: Create Packet

Steps:

  1. User navigates to the packet list (/write-offs/packets) and clicks "Add Packet."
  2. User enters a unique packet name and searches for/selects a client.
  3. User clicks "Create Packet."
  4. System validates: non-empty name, client selected, name is unique.
  5. Packet is created with packet_status_cd = 'DRAFT', total_commission_amt = '0.00', receivable_count = 0.
  6. A packet_status_history record is written with action_cd = 'CREATE' and to_status_cd = 'DRAFT'.
  7. User is redirected to the new packet's detail page.

Postconditions:

  • write_off_packet.packet_status_cd = 'DRAFT'
  • write_off_packet.current_approver_role = null
  • write_off_packet.total_commission_amt = '0.00'
  • write_off_packet.receivable_count = 0
  • One packet_status_history row with action_cd = 'CREATE'

UI trigger: "Add Packet" button. Visible always on the packet list page.


5.2 Add Receivables to Packet

Preconditions:

  • Packet packet_status_cd = 'DRAFT' or any REJECTED_* status.
  • Receivable is REV-type (billing_item_detail_type_cd = 'REV').
  • Receivable has current_item_ind = true and open_item_ind = true.
  • Receivable write_off_status_cd = 'NOT_WRITTEN_OFF'.
  • Receivable is not already in another active packet.
  • Receivable billing_item_detail_amt >= $100.00.

Procedure reference: Add Receivable to Packet

Steps:

  1. User clicks "Search Receivables" on the packet detail page.
  2. The Add Receivables dialog opens, auto-loading eligible open REV receivables for the packet's client (filtered to exclude items already in the packet).
  3. User selects one or more receivables using checkboxes in the results table.
  4. User clicks "Add N to Packet."
  5. System bulk-adds selected items as packet_receivable rows with eligibility_criteria_cd = '' (blank; user must set).
  6. total_commission_amt and receivable_count are recalculated and saved to write_off_packet.

Postconditions:

  • One packet_receivable row per added receivable, with write_off_packet_id linked and eligibility_criteria_cd = ''.
  • write_off_packet.total_commission_amt and receivable_count updated.

UI trigger: "Search Receivables" button. Visible when packetStatus === 'DRAFT'. Within the dialog, the "Add N to Packet" button is enabled only when at least one checkbox is selected.


5.3 Set Eligibility Criteria

Preconditions:

  • Packet is in an editable status (DRAFT or REJECTED_*).

Procedure reference: Update Receivable Eligibility

Steps (per receivable):

  1. User selects an eligibility code from the dropdown in the receivable row: AGED, UNCOLLECTIBLE, BANKRUPTCY, or AGENT_REQUEST.
  2. System saves packet_receivable.eligibility_criteria_cd and triggers aggregate recalculation.

Steps (bulk via packet header):

  1. User selects an eligibility code from the "Eligibility" selector in the packet header.
  2. System saves the code to write_off_packet.packet_eligibility_criteria_cd.
  3. System bulk-updates all packet_receivable rows with blank eligibility_criteria_cd to the selected code.
  4. Aggregate recalculation runs; the receivables table reloads silently.

Postconditions:

  • packet_receivable.eligibility_criteria_cd set to the selected code for affected rows.
  • write_off_packet.packet_eligibility_criteria_cd set (bulk path).

UI trigger (per receivable): Dropdown in the Eligibility column. Editable as dropdown when canEdit; read-only text otherwise. A clear (X) button appears next to the dropdown when a value is set.

UI trigger (bulk): "Eligibility" dropdown in the packet header. Visible and editable when canEdit.


5.4 Upload Supporting Documents

Preconditions:

  • Packet is accessible at any status for viewing documents; upload requires canEdit for the packet-level dialog.
  • File size must not exceed 25 MB per file.
  • Accepted formats: PDF, DOC, DOCX, XLS, XLSX, CSV, JPG, JPEG, PNG, GIF, TXT.

Procedure reference: TODO: Document in foundation/procedures/write-offs.md

Steps (packet-level):

  1. User clicks "Documents" in the packet header.
  2. The Packet Documents dialog opens, showing existing files.
  3. User clicks "Upload Document" and selects one or more files.
  4. Files are uploaded and linked to the write_off_packet entity.
  5. The document count badge updates.

Steps (receivable-level):

  1. User clicks the paperclip icon on a receivable row.
  2. The Receivable Documents dialog opens.
  3. User clicks "Upload Document" and selects files.
  4. Files are uploaded and linked to the packet_receivable entity.
  5. The document count badge on the paperclip icon updates.

Postconditions:

  • Files stored via the file upload service, linked to the respective entity.
  • Document count badges reflect the new total.

UI trigger (packet): "Documents" button with count badge in the packet header. Visible always. Upload button inside the dialog visible only when canEdit.

UI trigger (receivable): Paperclip icon with count badge in the "Documents" column. Visible always. Upload button inside the dialog visible only when canEdit.


5.5 Submit Packet for Approval

Preconditions:

  • packet_status_cd = 'DRAFT'.
  • receivable_count >= 1.
  • All packet_receivable rows have eligibility_criteria_cd set (non-null, non-empty).
  • All receivables are REV-type.
  • All receivables belong to write_off_packet.client_id.
  • No receivable is in another active packet.

Procedure reference: Submit Packet

Steps:

  1. User clicks "Submit for Approval" on the packet detail page.
  2. System runs the full submission validation.
  3. On validation pass: packet_status_cd'SUBMITTED'; current_approver_role'AGENT'; submitted_dt and submitted_by_user_id populated.
  4. A packet_status_history record is written with action_cd = 'SUBMIT' and approver_role = 'AGENT'.
  5. The header updates to show "Submitted" status; packet becomes read-only.

Postconditions:

  • write_off_packet.packet_status_cd = 'SUBMITTED'
  • write_off_packet.current_approver_role = 'AGENT'
  • write_off_packet.submitted_dt and submitted_by_user_id populated.
  • One packet_status_history row with action_cd = 'SUBMIT'.

UI trigger: "Submit for Approval" button. Visible when packet_status_cd = 'DRAFT'. Disabled when receivable_count < 1.


5.6 Approve Packet (Per Approval Level)

Preconditions:

  • packet_status_cd is one of: SUBMITTED, RESUBMITTED, APPROVED_AGENT, APPROVED_DH, APPROVED_VP, APPROVED_CFO, APPROVED_MD.
  • write_off_packet.current_approver_role matches the approver's role.
  • User has canApproveWorksheets permission.

Procedure reference: Approve Packet

Steps:

  1. Approver navigates to /write-offs/approvals and selects their role.
  2. Approver sees packets pending their approval in the table.
  3. Approver clicks the approve icon (checkmark) on the desired packet.
  4. The Approve dialog opens; approver optionally enters a comment.
  5. Approver clicks "Approve."
  6. Status machine determines next status based on current status and total_commission_amt vs thresholds.
  7. packet_status_cd advances; current_approver_role advances to the next role (or null if complete).
  8. If next status is COMPLETE: write-off is executed (receivables marked WRITTEN_OFF, cash receipt created); completed_dt and completed_by_user_id populated.
  9. A packet_status_history record is written with action_cd = 'APPROVE' and approver_role set.

Postconditions:

  • write_off_packet.packet_status_cd advanced (e.g., 'SUBMITTED''APPROVED_AGENT').
  • write_off_packet.current_approver_role updated.
  • On COMPLETE: billing_item_detail.write_off_status_cd = 'WRITTEN_OFF'; write_off_packet.cash_receipt_id populated; completed_dt and completed_by_user_id populated.
  • One packet_status_history row with action_cd = 'APPROVE'.

UI trigger: Approve icon (green checkmark) per row in the Pending Approvals table. Enabled when the packet row is visible (filtered to matching approver role).


5.7 Reject Packet

Preconditions:

  • packet_status_cd is an approvable status.
  • current_approver_role matches the rejecting user's role.
  • User has canApproveWorksheets permission.
  • Rejection reason is non-empty.

Procedure reference: Reject Packet

Steps:

  1. Approver clicks the reject icon (red X) on the packet in the Approval Dashboard.
  2. The Reject dialog opens; approver enters a required rejection reason.
  3. Approver clicks "Reject."
  4. packet_status_cd is set to the role-specific rejected status (e.g., 'REJECTED_VP'). current_approver_role is cleared.
  5. write_off_packet.rejected_dt, rejected_by_user_id, and rejection_reason populated.
  6. A packet_status_history record is written with action_cd = 'REJECT' and comment_text = reason.
  7. Packet returns to an editable state.

Postconditions:

  • write_off_packet.packet_status_cd set to the corresponding REJECTED_* status.
  • write_off_packet.current_approver_role = null.
  • write_off_packet.rejected_dt, rejected_by_user_id, rejection_reason populated.
  • One packet_status_history row with action_cd = 'REJECT'.
  • Packet is editable (canEdit = true).

UI trigger: Reject icon (red X) per row in the Pending Approvals table. The "Reject" button in the dialog is disabled until rejection reason is non-empty.


5.8 Resubmit Packet After Rejection

Preconditions:

  • packet_status_cd is any REJECTED_* status.

Procedure reference: Resubmit Packet

Steps:

  1. User views the rejected packet detail page and sees "Resubmit for Approval" button.
  2. User clicks "Resubmit for Approval."
  3. packet_status_cd'SUBMITTED'; current_approver_role'AGENT'.
  4. rejected_dt, rejected_by_user_id, rejection_reason are cleared.
  5. A packet_status_history record is written with action_cd = 'RESUBMIT'.
  6. Approval chain restarts from Agent level.

Postconditions:

  • write_off_packet.packet_status_cd = 'SUBMITTED'
  • write_off_packet.current_approver_role = 'AGENT'
  • Rejection fields cleared.
  • One packet_status_history row with action_cd = 'RESUBMIT'.

UI trigger: "Resubmit for Approval" button. Visible when packetStatus.startsWith('REJECTED_').


5.9 Recover a Completed Write-Off

Preconditions:

  • packet_status_cd = 'COMPLETE'.
  • User has canApproveWorksheets permission.
  • Recovery reason is non-empty.

Procedure reference: Recover Packet

Steps:

  1. Approver initiates recovery via the approval service (recovery action on a COMPLETE packet).
  2. Recovery reason is required and entered.
  3. billing_item_detail.write_off_status_cd reverted (to 'NOT_WRITTEN_OFF'); write_off_packet_id cleared; recovered_dt populated for each packet receivable.
  4. packet_status_cd'RECOVERED'; recovered_dt and recovered_by_user_id populated on the packet.
  5. A reversal worksheet is created to post the offsetting GL entries.
  6. A packet_status_history record is written with action_cd = 'RECOVER' and comment_text = reason.

Postconditions:

  • write_off_packet.packet_status_cd = 'RECOVERED' (terminal — no further transitions).
  • write_off_packet.recovered_dt and recovered_by_user_id populated.
  • billing_item_detail.write_off_status_cd reverted for each packet receivable; recovered_dt populated.
  • Reversal worksheet created for GL pickup.
  • One packet_status_history row with action_cd = 'RECOVER'.
  • Packet and all receivables permanently read-only.

UI trigger: Recovery action available for packets with packet_status_cd = 'COMPLETE', restricted to users with canApproveWorksheets.


5.10 Remove Receivable from Packet

Preconditions:

  • Packet packet_status_cd = 'DRAFT' or REJECTED_*.
  • Receivable is currently in the packet.

Procedure reference: Remove Receivable from Packet

Steps:

  1. User clicks the trash icon on a receivable row (single) or selects multiple rows and clicks "Delete Selected N."
  2. A confirmation dialog appears.
  3. User confirms deletion.
  4. packet_receivable row is deleted (cascading to any receivable-level documents).
  5. Aggregate recalculation updates total_commission_amt and receivable_count on the packet.

Postconditions:

  • packet_receivable row removed.
  • write_off_packet.total_commission_amt and receivable_count recalculated.

UI trigger: Trash icon per row and "Delete Selected" bulk button. Both visible only when canEdit (packet_status_cd === 'DRAFT').


6. Permissions & Role-Based Access

ActionCASH_MANAGERCASH_PROCESSORSETTLEMENT_APPROVERAGENTDEPT_HEADVP_CLIENT_ACCTCFOMDIT
Create packetYesYesYes
Add / remove receivablesYesYesYes
Set eligibility criteriaYesYesYes
Upload documentsYesYesYes
Submit packetYesYesYes
Delete draft packetYesYesYes
Approve at Agent levelYesYes
Approve at Dept Head levelYesYes
Approve at VP levelYesYesYes
Approve at CFO levelYesYes
Approve at MD levelYesYes
Reject at any levelYesYesYesYesYesYes
Resubmit after rejectionYesYesYes
Recover completed write-offYesYes
View packets (read-only)YesYesYesYesYesYesYesYesYes

NOTE

In the PoC implementation, approval access is gated on the canApproveWorksheets flag from the user session rather than on formal role codes. The production system must implement proper role-based checks aligned with the approval chain roles (AGENT, DEPT_HEAD, VP_CLIENT_ACCT, CFO, MD).

Field-level restrictions:

  • Packet name is editable inline only when canEdit (packet_status_cd = 'DRAFT' or REJECTED_*).
  • Eligibility selectors (both header and per-row) are editable only when canEdit.
  • Document upload buttons are visible only when canEdit; document viewing and download are available at all statuses.
  • Comments can be added at any packet status; available to all users with packet access.
  • The "Documents," "Comments," status timeline, and metadata bar are readable by all users with packet access.

7. Integration Points

7.1 Upstream

SourceData ProvidedMechanism
Billing Items workflowREV-type billing_item_detail records eligible for write-off; billing_item_detail_amt, due dates, client/buyer/deal contextFK lookup via packet_receivable.billing_item_detail_idbilling_item_detail.billing_item_detail_id
AR Aging reportsIdentification of aged receivables (180+ days outstanding) recommended for the AGED eligibility codeRead: billing_item.billing_item_aging_dt and billing_item_detail queried in AR Aging report; surfaces to users for write-off nomination
Deals and PartiesClient name, buyer name, deal name, departmentFK joins through billing_item to party, deal, department when enriching packet receivable display data

7.2 Downstream

ConsumerData ConsumedMechanism
General Ledger (via write-off worksheet)Write-off GL entries posted via the auto-approved WRITE_OFF cash receipt worksheet created on packet completionwrite_off_packet.cash_receipt_idcash_receiptcash_receipt_worksheet → GL batch jobs
General Ledger reversalRecovery GL entries via the reversal worksheet created on packet recoveryWriteOffCashReceiptService.createReversalWorksheet creates a new worksheet for GL pickup
CECL ReportingWritten-off receivables excluded from credit loss calculations via billing_item_detail.exclude_from_cecl_ind = trueRead: exclude_from_cecl_ind in regulatory reporting queries
AR AgingWritten-off receivables excluded from aging reports via write_off_status_cdRead: billing_item_detail.write_off_status_cd in AR aging report queries
Billing Items workflowWrite-off fields on billing_item_detail (write_off_status_cd, write_off_dt, write_off_packet_id, exclude_from_cecl_ind, recovered_dt) updated on write-off and recoveryDirect field updates via PacketApprovalService.executeWriteOff and executeRecovery

7.3 External Integrations

No external integrations for this workflow. The write-off workflow operates entirely within the internal Client Payments system. The offsetting cash receipt and reversal worksheet are processed through the standard GL batch jobs, which are internal integrations covered by the Worksheets workflow.


8. Functional Screen Requirements

8.1 Write-Off Packets List

Route: /write-offs/packets

Data loading:

  • searchPacketsSearch Packets — loads all packets with client name, status, total commission amount, receivable count, eligibility, and created date.

Header Region

Navigation breadcrumb and the "Add Packet" action button.

Field / ColumnSourceEditable?Condition
Breadcrumb "Write-Off Packets"StaticNoAlways visible
"Add Packet" buttonAlways visible; navigates to /write-offs/packets/new

Packets Table Region

Displays all write-off packets in a sortable data table.

Field / ColumnSourceEditable?Condition
Packet Namewrite_off_packet.packet_nameNoAlways visible
Clientparty.party_name via write_off_packet.client_idNoAlways visible
Amountwrite_off_packet.total_commission_amtNoAlways visible; formatted as currency
Receivableswrite_off_packet.receivable_countNoAlways visible
Statuswrite_off_packet.packet_status_cdNoAlways visible; displayed as a styled badge
Eligibilitywrite_off_packet.packet_eligibility_criteria_cdNoDisplays the code or "—" if null
Createdwrite_off_packet.created_dtNoAlways visible; formatted as date
View iconAlways visible; navigates to packet detail
Delete iconVisible only when packet_status_cd = 'DRAFT'; opens delete confirmation dialog

Grid features:

  • Sortable columns: Packet Name, Client, Amount, Receivables, Status, Created
  • Filters: Column header controls via DataTable
  • Row selection: None
  • Pagination: DataTable default

Conditional display:

  • Delete confirmation dialog appears when trash icon clicked; dismissed on confirm or cancel.
  • Toast success shown on successful deletion.

8.2 Create Write-Off Packet Form

Route: /write-offs/packets/new

Data loading:

  • None on page entry. Client search is a dynamic lookup triggered by user input.

Form Region

Single-step creation form for a new write-off packet.

Field / ColumnSourceEditable?Condition
Packet NameUser input → write_off_packet.packet_nameYesAlways visible; required field
ClientClient search → write_off_packet.client_idYesAlways visible; required field
Create Packet buttonAlways visible; disabled when isSubmitting = true
Cancel buttonAlways visible; navigates to /write-offs/packets

Conditional display:

  • "Create Packet" button disabled while form is submitting.
  • Toast error shown if packet name is empty, client is not selected, or name already exists.

8.3 Packet Detail

Route: /write-offs/packets/[id]

Data loading:

  • getPacketWithDetailsGet Packet With Details — loads the full packet with all user names and timestamps.
  • getReceivablesByPacketGet Receivables By Packet — loads the receivables table.
  • getCurrentUser — loads current user for display name and canApproveWorksheets flag.

Header Region

Displays packet identity, status badge, key metrics, action buttons, and a metadata bar. Inline editing and dialog interactions are available for editable fields.

Field / ColumnSourceEditable?Condition
Packet Namewrite_off_packet.packet_nameYes (inline)Editable when canEdit (DRAFT or REJECTED_*); read-only otherwise
Status Badgewrite_off_packet.packet_status_cdNoAlways visible
Clientparty.party_name via write_off_packet.client_idNoAlways visible
Eligibility selectorwrite_off_packet.packet_eligibility_criteria_cdYes (dropdown + clear)Editable when canEdit; read-only text otherwise
Comments button + count badgeComment count for write_off_packet entityNoAlways visible; opens Comments dialog
Documents button + count badgeFile count for write_off_packet entityNoAlways visible; opens Documents dialog
Total Amountwrite_off_packet.total_commission_amtNoAlways visible; formatted as currency
Receivables countwrite_off_packet.receivable_countNoAlways visible
Submit for Approval buttonVisible when packet_status_cd = 'DRAFT'; disabled when receivable_count < 1
Resubmit for Approval buttonVisible when packet_status_cd is any REJECTED_*
Approve buttonVisible when canApproveWorksheets = true and packet is in an approvable status
Reject buttonVisible when canApproveWorksheets = true and packet is in an approvable status
Metadata: Createdwrite_off_packet.created_dt + created_by_user_nameNoAlways visible
Metadata: Submittedwrite_off_packet.submitted_dt + submitted_by_user_nameNoVisible when submitted_dt is non-null
Metadata: Last Savedwrite_off_packet.updated_dt + updated_by_user_nameNoAlways visible
Metadata: Approvedwrite_off_packet.completed_dt + completed_by_user_nameNoVisible when completed_dt is non-null
Metadata: Rejectedwrite_off_packet.rejected_dt + rejected_by_user_nameNoVisible when rejected_dt is non-null; includes rejection history icon
Metadata: Recoveredwrite_off_packet.recovered_dt + recovered_by_user_nameNoVisible when recovered_dt is non-null
Worksheet linkwrite_off_packet.cash_receipt_id → latest worksheet lookupNoVisible when cash_receipt_id is non-null; navigates to worksheet detail

Conditional display:

  • Inline editing of packet name enabled only when canEdit.
  • Eligibility dropdown replaced with read-only text when not canEdit.
  • "Submit for Approval" button disabled when receivable_count < 1.
  • Approve and Reject buttons visible only when user has canApproveWorksheets and packet is in an approvable status.
  • Worksheet link appears only after packet reaches COMPLETE and write-off cash receipt has been created.
  • Rejection history icon visible only when rejection_reason is non-null.
  • Document upload button inside the Documents dialog enabled only when canEdit.

Receivables Table Region

Displays all receivables in the packet with aging buckets, eligibility, comments, and documents per row.

Field / ColumnSourceEditable?Condition
Select checkboxYesVisible only when canEdit
Dealdeal.deal_name via billing_item.deal_idNoAlways visible
Clientparty.party_name via billing_item.client_idNoAlways visible
Buyerparty.party_name via billing_item.buyer_idNoAlways visible
Billing Itembilling_item.billing_item_nameNoAlways visible
Amount (REV)billing_item_detail.billing_item_detail_amt where billing_item_detail_type_cd = 'REV'NoAlways visible; formatted as currency
Due Datebilling_item.billing_item_due_dtNoAlways visible
CurrentComputed: amount where days past due <= 0NoAlways visible
1-30 DaysComputed: amount where 1 <= days past due <= 30NoAlways visible
31-60 DaysComputed: amount where 31 <= days past due <= 60NoAlways visible
61-90 DaysComputed: amount where 61 <= days past due <= 90NoAlways visible
90+ DaysComputed: amount where days past due > 90NoAlways visible
Eligibilitypacket_receivable.eligibility_criteria_cdYes (dropdown)Editable as dropdown + clear button when canEdit; read-only text otherwise
Commentpacket_receivable comment countYes (dialog)Always visible; comment icon with count badge opens CommentIconWithDialog
DocumentsFile count for packet_receivable entityYes (dialog)Always visible; paperclip icon with count badge opens receivable documents dialog
Actions (trash)Visible only when canEdit

Grid features:

  • Sortable columns: Deal, Client, Buyer, Amount (REV)
  • Filters: None (no global filter on receivables table)
  • Row selection: Multi-select checkbox column visible when canEdit; drives "Delete Selected" bulk action
  • Pagination: Disabled (all receivables in packet shown)

Conditional display:

  • "Search Receivables" button visible only when canEdit.
  • "Delete Selected (N)" bulk delete button visible only when canEdit and at least one row is selected.
  • Individual trash icon per row visible only when canEdit.
  • Eligibility dropdown replaced with read-only text when not canEdit.
  • Document upload button inside the receivable documents dialog enabled only when canEdit.
  • Empty state message varies: includes "Click Search Receivables to add some" when canEdit.

8.4 Approval Dashboard

Route: /write-offs/approvals

Data loading:

  • getPacketsPendingApproval(selectedRole)Get Packets Pending Approval — loads packets where current_approver_role matches the selected role.

Role Selector Region

Allows the approver to select which role they are acting as.

Field / ColumnSourceEditable?Condition
Approver Role dropdownUser selection; values: AGENT, DEPT_HEAD, VP_CLIENT_ACCT, CFO, MDYesAlways visible; defaults to AGENT

Conditional display:

  • Changing the role selection triggers a reload of the pending packets table.

Pending Packets Table Region

Displays all packets awaiting approval for the currently selected approver role.

Field / ColumnSourceEditable?Condition
Packet Namewrite_off_packet.packet_nameNoAlways visible
Clientparty.party_name via write_off_packet.client_idNoAlways visible
Amountwrite_off_packet.total_commission_amtNoAlways visible; formatted as currency
Receivableswrite_off_packet.receivable_countNoAlways visible
Submittedwrite_off_packet.submitted_dtNoFormatted as date; "—" if null
Statuswrite_off_packet.packet_status_cdNoDisplayed as badge
View iconAlways visible; navigates to packet detail
Approve iconAlways visible for rows in this filtered table
Reject iconAlways visible for rows in this filtered table

Grid features:

  • Sortable columns: standard table display
  • Filters: Filtered at query level by current_approver_role matching selected role
  • Row selection: None
  • Pagination: None

Conditional display:

  • Empty state message shown when no packets are pending for the selected role.
  • Approve dialog contains optional comment field.
  • Reject dialog contains required rejection reason field; Reject button disabled until reason is non-empty.

9. Additional Diagrams

Packet Status State Machine

mermaid
stateDiagram-v2
    [*] --> DRAFT : Packet created
    DRAFT --> SUBMITTED : Submit
    SUBMITTED --> APPROVED_AGENT : Agent approves
    SUBMITTED --> REJECTED_AGENT : Agent rejects
    APPROVED_AGENT --> APPROVED_DH : Dept Head approves
    APPROVED_AGENT --> REJECTED_DH : Dept Head rejects
    APPROVED_DH --> APPROVED_VP : VP approves
    APPROVED_DH --> REJECTED_VP : VP rejects
    APPROVED_VP --> COMPLETE : total_commission_amt < $50K
    APPROVED_VP --> APPROVED_CFO : total_commission_amt >= $50K
    APPROVED_VP --> REJECTED_CFO : CFO rejects
    APPROVED_CFO --> COMPLETE : total_commission_amt <= $250K
    APPROVED_CFO --> APPROVED_MD : total_commission_amt > $250K
    APPROVED_CFO --> REJECTED_MD : MD rejects
    APPROVED_MD --> COMPLETE : MD approves
    REJECTED_AGENT --> SUBMITTED : Resubmit
    REJECTED_DH --> SUBMITTED : Resubmit
    REJECTED_VP --> SUBMITTED : Resubmit
    REJECTED_CFO --> SUBMITTED : Resubmit
    REJECTED_MD --> SUBMITTED : Resubmit
    COMPLETE --> RECOVERED : Recover

Write-Off Execution Sequence

mermaid
sequenceDiagram
    participant Approver
    participant PacketApprovalService
    participant StatusMachine
    participant BillingItemDetail
    participant WriteOffCashReceiptService
    participant CashReceipt

    Approver->>PacketApprovalService: approvePacket(packetId, role, comment)
    PacketApprovalService->>StatusMachine: getNextStatusAfterApproval(currentStatus, totalAmt)
    StatusMachine-->>PacketApprovalService: nextStatus = COMPLETE, isComplete = true
    PacketApprovalService->>BillingItemDetail: UPDATE write_off_status_cd = WRITTEN_OFF per receivable
    PacketApprovalService->>PacketApprovalService: completePacket (completed_dt, completed_by_user_id)
    PacketApprovalService->>WriteOffCashReceiptService: createWriteOffCashReceipt(packetId)
    WriteOffCashReceiptService->>CashReceipt: INSERT cash_receipt (receiptTypeCd = WRITE_OFF)
    WriteOffCashReceiptService->>CashReceipt: INSERT cash_receipt_worksheet (worksheetStatusCd = A)
    WriteOffCashReceiptService->>CashReceipt: INSERT cash_receipt_application per receivable
    WriteOffCashReceiptService-->>PacketApprovalService: cashReceiptId, applicationCount
    PacketApprovalService->>PacketApprovalService: UPDATE write_off_packet.cash_receipt_id
    PacketApprovalService-->>Approver: success - Packet approved and write-off completed

NOTE

PoC Artifact: The PoC executeRecovery method sets billing_item_detail.write_off_status_cd back to 'NOT_WRITTEN_OFF' rather than 'RECOVERED'. The data model specifies 'RECOVERED' as the post-recovery status. Production must set write_off_status_cd = 'RECOVERED' on recovery so the field accurately records the write-off history.


10. Cross-References

DocumentRelationship
Write-Offs Data ModelDefines all tables used in this workflow: write_off_packet, packet_receivable, packet_status_history, packet_document
Write-Offs QueriesSpecifies all data retrieval operations: search, enriched detail fetch, eligible receivable search, pending approval lookup, history queries
Write-Offs ProceduresSpecifies all data mutation operations: create, update, delete, add receivables, submit, approve, reject, resubmit, execute write-off, recover
Billing Items Data Modelpacket_receivable.billing_item_detail_id references billing_item_detail. Write-off fields on billing_item_detail (write_off_status_cd, write_off_packet_id, write_off_dt, exclude_from_cecl_ind, recovered_dt) are owned by the Billing Items model and updated as side effects of write-off execution and recovery
Billing Items WorkflowUpstream: billing items and REV details are the source receivables that populate write-off packets
Worksheets WorkflowDownstream: the offsetting WRITE_OFF cash receipt uses a standard worksheet structure (auto-approved at status 'A'); the reversal worksheet on recovery follows the standard worksheet model
AR Aging WorkflowAR aging reports identify aged receivables (180+ days) that are candidates for the AGED eligibility criteria; written-off receivables are excluded from aging via write_off_status_cd

11. Gherkin Scenarios

gherkin
Feature: Write-Offs - Packet Creation and Setup

  Scenario: Create a write-off packet and add aged receivables
    Given an agent identifies three open REV billing item details for client "Taylor Reed"
    And each detail has billing_item_detail_amt >= $100.00
    And each detail has write_off_status_cd = 'NOT_WRITTEN_OFF'
    And none of the details belong to another active write_off_packet
    When the agent creates a new write_off_packet named "Taylor Reed Q1 Write-Off" for client Taylor Reed
    Then write_off_packet.packet_status_cd = 'DRAFT'
    And write_off_packet.current_approver_role is null
    And write_off_packet.receivable_count = 0
    And a packet_status_history row exists with action_cd = 'CREATE' and to_status_cd = 'DRAFT'
    When the agent adds all three billing_item_detail records via the Search Receivables dialog
    Then write_off_packet.receivable_count = 3
    And three packet_receivable rows exist with eligibility_criteria_cd = '' (blank)
    When the agent sets the packet-level eligibility to 'AGED'
    Then write_off_packet.packet_eligibility_criteria_cd = 'AGED'
    And all three packet_receivable.eligibility_criteria_cd = 'AGED'

Feature: Write-Offs - Submission Validation

  Scenario: Submission blocked when receivables are missing eligibility criteria
    Given a write_off_packet in DRAFT status with two packet_receivable rows
    And one packet_receivable has eligibility_criteria_cd = 'AGED'
    And the other packet_receivable has eligibility_criteria_cd = '' (blank)
    When the user attempts to submit the packet
    Then submission fails with error "Receivable must have eligibility criteria"
    And write_off_packet.packet_status_cd remains 'DRAFT'

  Scenario: Submission blocked when packet is empty
    Given a write_off_packet with packet_status_cd = 'DRAFT' and receivable_count = 0
    When the user views the packet detail page
    Then the "Submit for Approval" button is disabled
    And no submission can be triggered

  Scenario: Submission succeeds when all validation passes
    Given a write_off_packet in DRAFT status with one packet_receivable
    And the packet_receivable has eligibility_criteria_cd = 'UNCOLLECTIBLE'
    And the receivable has billing_item_detail_type_cd = 'REV'
    When the user clicks "Submit for Approval"
    Then write_off_packet.packet_status_cd = 'SUBMITTED'
    And write_off_packet.current_approver_role = 'AGENT'
    And write_off_packet.submitted_dt is populated
    And a packet_status_history row exists with action_cd = 'SUBMIT' and to_status_cd = 'SUBMITTED'

  Scenario: Adding a receivable already in another active packet is rejected
    Given billing_item_detail_id = 12345 is already a packet_receivable in an active write_off_packet
    When a user attempts to add billing_item_detail_id = 12345 to a different packet
    Then the add operation fails with error "Receivable is already in another active packet"
    And no new packet_receivable row is created for billing_item_detail_id = 12345

Feature: Write-Offs - Approval Chain

  Scenario: Full approval chain completes write-off for packet under $50,000
    Given a write_off_packet named "Studio Debt Under 50K" with total_commission_amt = 45000.00
    And packet_status_cd = 'SUBMITTED' and current_approver_role = 'AGENT'
    When the Agent approves the packet with comment "Verified with collections team"
    Then packet_status_cd = 'APPROVED_AGENT' and current_approver_role = 'DEPT_HEAD'
    And a packet_status_history row exists with action_cd = 'APPROVE' and approver_role = 'AGENT'
    When the Department Head approves the packet
    Then packet_status_cd = 'APPROVED_DH' and current_approver_role = 'VP_CLIENT_ACCT'
    When the VP Client Accounting approves the packet
    Then packet_status_cd = 'COMPLETE' and current_approver_role is null
    And billing_item_detail.write_off_status_cd = 'WRITTEN_OFF' for all packet receivables
    And billing_item_detail.write_off_dt is populated for each receivable
    And billing_item_detail.exclude_from_cecl_ind = true for each receivable
    And a cash_receipt with receiptTypeCd = 'WRITE_OFF' is created
    And write_off_packet.cash_receipt_id is populated
    And write_off_packet.completed_dt is populated
    And a packet_status_history row exists with action_cd = 'APPROVE' and approver_role = 'VP_CLIENT_ACCT' and to_status_cd = 'COMPLETE'

  Scenario: Approval chain routes to CFO for packet between $50,000 and $250,000
    Given a write_off_packet with total_commission_amt = 120000.00
    And packet_status_cd = 'APPROVED_DH'
    When the VP Client Accounting approves the packet
    Then packet_status_cd = 'APPROVED_VP'
    And current_approver_role = 'CFO'
    When the CFO approves the packet
    Then packet_status_cd = 'COMPLETE'
    And write_off_packet.cash_receipt_id is populated

  Scenario: Approval chain routes to MD for packet over $250,000
    Given a write_off_packet with total_commission_amt = 300000.00
    And packet_status_cd = 'APPROVED_VP'
    When the CFO approves the packet
    Then packet_status_cd = 'APPROVED_CFO'
    And current_approver_role = 'MD'
    When the Managing Director approves the packet
    Then packet_status_cd = 'COMPLETE'
    And all packet receivables have billing_item_detail.write_off_status_cd = 'WRITTEN_OFF'

Feature: Write-Offs - Rejection and Resubmission

  Scenario: VP rejects packet requiring corrections
    Given a write_off_packet named "Disputed Commission Q2" with packet_status_cd = 'APPROVED_DH'
    And current_approver_role = 'VP_CLIENT_ACCT'
    When the VP Client Accounting rejects the packet with reason "Missing court documentation for BANKRUPTCY receivables"
    Then packet_status_cd = 'REJECTED_VP'
    And write_off_packet.current_approver_role is null
    And write_off_packet.rejection_reason contains "Missing court documentation for BANKRUPTCY receivables"
    And write_off_packet.rejected_dt is populated
    And a packet_status_history row exists with action_cd = 'REJECT' and comment_text = "Missing court documentation for BANKRUPTCY receivables"
    And the packet is editable (canEdit = true)

  Scenario: User corrects and resubmits a rejected packet
    Given a write_off_packet with packet_status_cd = 'REJECTED_VP'
    And the user has uploaded required court documentation for the BANKRUPTCY receivable
    When the user clicks "Resubmit for Approval"
    Then packet_status_cd = 'SUBMITTED'
    And current_approver_role = 'AGENT'
    And write_off_packet.rejection_reason is cleared
    And write_off_packet.rejected_dt is cleared
    And a packet_status_history row exists with action_cd = 'RESUBMIT' and to_status_cd = 'SUBMITTED'

  Scenario: Reject dialog requires a non-empty rejection reason
    Given a write_off_packet in an approvable status
    And the approver opens the Reject dialog
    When the approver leaves the rejection reason field empty
    Then the "Reject" button remains disabled
    And no rejection action is submitted

Feature: Write-Offs - Recovery

  Scenario: Recovering a completed write-off when buyer eventually pays
    Given a write_off_packet named "Sony Music Q3 Write-Off" with packet_status_cd = 'COMPLETE'
    And billing_item_detail.write_off_status_cd = 'WRITTEN_OFF' for all packet receivables
    And billing_item_detail.exclude_from_cecl_ind = true for all packet receivables
    When an authorized approver initiates recovery with reason "Buyer settled outstanding balance in full"
    Then write_off_packet.packet_status_cd = 'RECOVERED'
    And write_off_packet.recovered_dt is populated
    And billing_item_detail.write_off_status_cd is reverted for each receivable
    And billing_item_detail.recovered_dt is populated for each receivable
    And a reversal worksheet is created for GL posting
    And a packet_status_history row exists with action_cd = 'RECOVER' and comment_text = "Buyer settled outstanding balance in full"
    And the packet is permanently read-only

  Scenario: Recovery is blocked on non-complete packets
    Given a write_off_packet with packet_status_cd = 'SUBMITTED'
    When an approver attempts to recover the packet
    Then recovery fails with error "Only completed packets can be recovered"
    And packet_status_cd remains 'SUBMITTED'

  Scenario: Recovered packet cannot be further modified
    Given a write_off_packet with packet_status_cd = 'RECOVERED'
    When a user attempts to add a receivable to the packet
    Then the add operation fails with error "Cannot add receivables to packet in RECOVERED status"
    And no packet_receivable row is created

Confidential. For internal use only.