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_cdset to'WRITTEN_OFF'; offsettingWRITE_OFFtypecash_receiptcreated with an auto-approved worksheet - Recovery of a completed write-off:
billing_item_detail.write_off_status_cdreverted; reversal worksheet created for GL posting - Viewing the status timeline and comment history for a packet
Not covered (documented separately):
billing_item_detailwrite-off fields and the REV/PAY split — see Billing Items Workflow- Cash receipt worksheet lifecycle used for the offsetting write-off receipt — see Worksheets Workflow
- AR Aging reports that surface write-off-eligible receivables — see AR Aging Workflow
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_historycovering 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
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
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 withbilling_item_detail_amtbelow $100.00 are not eligible.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 inDRAFTstatus withpacket_status_cd = 'DRAFT'andcurrent_approver_role = null.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_receivablerows. Each receivable must have aneligibility_criteria_cdassigned (AGED,UNCOLLECTIBLE,BANKRUPTCY, orAGENT_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.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_packetorpacket_receivableentity.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_cdadvances to'SUBMITTED'andcurrent_approver_roleis set to'AGENT'.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 advancespacket_status_cdthroughAPPROVED_AGENT→APPROVED_DH→APPROVED_VP, then toCOMPLETE(if total < $50,000), or continues toAPPROVED_CFO→COMPLETE(if total $50,000–$250,000), orAPPROVED_CFO→APPROVED_MD→COMPLETE(if total > $250,000). Every action is recorded inpacket_status_history.Rejection and resubmission — Any approver can reject, setting
packet_status_cdto 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 fromAGENTlevel.Write-off execution (COMPLETE) — On final approval,
billing_item_detail.write_off_status_cdis set to'WRITTEN_OFF'andwrite_off_dtis populated for each packet receivable. An offsettingcash_receiptof typeWRITE_OFFis created, along with an auto-approved worksheet containing applications for all written-off receivables.write_off_packet.cash_receipt_idis populated with a link to this receipt, which is visible in the packet header as a link to the worksheet.Recovery — If the write-off needs to be reversed (e.g., the buyer eventually pays), an approver can initiate recovery. This sets
packet_status_cdto'RECOVERED'(terminal state), revertsbilling_item_detail.write_off_status_cd, populatesrecovered_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 DRAFT → SUBMITTED.
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_AGENT → APPROVED_DH → APPROVED_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
| Operation | Foundation Doc | Purpose in This Workflow |
|---|---|---|
searchPackets | Search Packets | Load all packets for the packet list page with client name, status, totals, and eligibility. Supports filter criteria for status, client, and date range. |
getPacketWithDetails | Get Packet With Details | Load 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. |
getReceivablesByPacket | Get Receivables By Packet | Load 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. |
getHistoryWithUsersByPacket | Get Status History With Users | Load the full packet_status_history audit trail with user names for the status timeline display and approval history view. |
searchEligibleReceivables | Search Eligible Receivables | Find 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. |
getPacketsPendingApproval | Get Packets Pending Approval | Retrieve packets where current_approver_role matches the approver's role. Drives the Approval Dashboard table. |
isPacketNameUnique | Check Packet Name Unique | Validates uniqueness of write_off_packet.packet_name at create and rename time. |
isReceivableInAnyPacket | Check Receivable In Packet | Checks whether a billing_item_detail_id is already in an active packet before allowing it to be added. |
getFilesForEntity | TODO: Document in foundation/queries/write-offs.md | Retrieves uploaded file records linked to a write_off_packet or packet_receivable entity for display in the documents dialogs. |
4.2 Procedures Used
| Operation | Foundation Doc | Trigger in This Workflow |
|---|---|---|
createPacket | Create Packet | User submits the "Create New Write-Off Packet" form with a unique name and selected client. |
updatePacket | Update Packet | User edits the packet name (inline edit) or sets/clears the packet-level eligibility selector in the header. |
deletePacket | Delete Packet | User clicks the trash icon on a DRAFT packet in the packet list and confirms deletion. |
bulkAddReceivablesToPacket | Add Receivable to Packet | User selects receivables in the Add Receivables dialog and clicks "Add to Packet." |
removeReceivableFromPacket | Remove Receivable from Packet | User clicks the trash icon on a receivable row (single) or selects multiple and clicks "Delete Selected." |
updateReceivableEligibilityCriteria | Update Eligibility | User changes the eligibility dropdown on a single receivable row. |
bulkUpdateBlankEligibility | Bulk Update Eligibility | User sets the packet-level eligibility in the header, propagating the code to all receivables with a blank eligibility_criteria_cd. |
recalculatePacketAggregates | Recalculate Aggregates | Triggered automatically after any add, remove, or eligibility update on receivables to keep total_commission_amt and receivable_count current. |
submitPacket | Submit Packet | User clicks "Submit for Approval" on a DRAFT packet after validation passes. |
approvePacket (via PacketApprovalService) | Approve Packet | Approver clicks the approve icon on the Approval Dashboard and confirms in the approve dialog. |
rejectPacket (via PacketApprovalService) | Reject Packet | Approver clicks the reject icon on the Approval Dashboard and provides a required rejection reason. |
resubmitPacket | Resubmit Packet | User clicks "Resubmit for Approval" on a packet in any REJECTED_* status. |
executeWriteOff | Execute Write-Off | Called 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 Packet | Approver triggers recovery on a COMPLETE packet with a required reason. Creates a reversal worksheet. |
addPacketComment | TODO: Document in foundation/procedures/write-offs.md | User opens the Comments dialog and saves a new note against the packet. |
addReceivableComment | TODO: Document in foundation/procedures/write-offs.md | User clicks the comment icon on a receivable row and saves a note. |
uploadFile | TODO: Document in foundation/procedures/write-offs.md | User 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_nameunique constraint).
Procedure reference: Create Packet
Steps:
- User navigates to the packet list (
/write-offs/packets) and clicks "Add Packet." - User enters a unique packet name and searches for/selects a client.
- User clicks "Create Packet."
- System validates: non-empty name, client selected, name is unique.
- Packet is created with
packet_status_cd = 'DRAFT',total_commission_amt = '0.00',receivable_count = 0. - A
packet_status_historyrecord is written withaction_cd = 'CREATE'andto_status_cd = 'DRAFT'. - User is redirected to the new packet's detail page.
Postconditions:
write_off_packet.packet_status_cd = 'DRAFT'write_off_packet.current_approver_role = nullwrite_off_packet.total_commission_amt = '0.00'write_off_packet.receivable_count = 0- One
packet_status_historyrow withaction_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 anyREJECTED_*status. - Receivable is REV-type (
billing_item_detail_type_cd = 'REV'). - Receivable has
current_item_ind = trueandopen_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:
- User clicks "Search Receivables" on the packet detail page.
- The Add Receivables dialog opens, auto-loading eligible open REV receivables for the packet's client (filtered to exclude items already in the packet).
- User selects one or more receivables using checkboxes in the results table.
- User clicks "Add N to Packet."
- System bulk-adds selected items as
packet_receivablerows witheligibility_criteria_cd = ''(blank; user must set). total_commission_amtandreceivable_countare recalculated and saved towrite_off_packet.
Postconditions:
- One
packet_receivablerow per added receivable, withwrite_off_packet_idlinked andeligibility_criteria_cd = ''. write_off_packet.total_commission_amtandreceivable_countupdated.
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 (
DRAFTorREJECTED_*).
Procedure reference: Update Receivable Eligibility
Steps (per receivable):
- User selects an eligibility code from the dropdown in the receivable row:
AGED,UNCOLLECTIBLE,BANKRUPTCY, orAGENT_REQUEST. - System saves
packet_receivable.eligibility_criteria_cdand triggers aggregate recalculation.
Steps (bulk via packet header):
- User selects an eligibility code from the "Eligibility" selector in the packet header.
- System saves the code to
write_off_packet.packet_eligibility_criteria_cd. - System bulk-updates all
packet_receivablerows with blankeligibility_criteria_cdto the selected code. - Aggregate recalculation runs; the receivables table reloads silently.
Postconditions:
packet_receivable.eligibility_criteria_cdset to the selected code for affected rows.write_off_packet.packet_eligibility_criteria_cdset (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
canEditfor 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):
- User clicks "Documents" in the packet header.
- The Packet Documents dialog opens, showing existing files.
- User clicks "Upload Document" and selects one or more files.
- Files are uploaded and linked to the
write_off_packetentity. - The document count badge updates.
Steps (receivable-level):
- User clicks the paperclip icon on a receivable row.
- The Receivable Documents dialog opens.
- User clicks "Upload Document" and selects files.
- Files are uploaded and linked to the
packet_receivableentity. - 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_receivablerows haveeligibility_criteria_cdset (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:
- User clicks "Submit for Approval" on the packet detail page.
- System runs the full submission validation.
- On validation pass:
packet_status_cd→'SUBMITTED';current_approver_role→'AGENT';submitted_dtandsubmitted_by_user_idpopulated. - A
packet_status_historyrecord is written withaction_cd = 'SUBMIT'andapprover_role = 'AGENT'. - 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_dtandsubmitted_by_user_idpopulated.- One
packet_status_historyrow withaction_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_cdis one of:SUBMITTED,RESUBMITTED,APPROVED_AGENT,APPROVED_DH,APPROVED_VP,APPROVED_CFO,APPROVED_MD.write_off_packet.current_approver_rolematches the approver's role.- User has
canApproveWorksheetspermission.
Procedure reference: Approve Packet
Steps:
- Approver navigates to
/write-offs/approvalsand selects their role. - Approver sees packets pending their approval in the table.
- Approver clicks the approve icon (checkmark) on the desired packet.
- The Approve dialog opens; approver optionally enters a comment.
- Approver clicks "Approve."
- Status machine determines next status based on current status and
total_commission_amtvs thresholds. packet_status_cdadvances;current_approver_roleadvances to the next role (or null if complete).- If next status is
COMPLETE: write-off is executed (receivables markedWRITTEN_OFF, cash receipt created);completed_dtandcompleted_by_user_idpopulated. - A
packet_status_historyrecord is written withaction_cd = 'APPROVE'andapprover_roleset.
Postconditions:
write_off_packet.packet_status_cdadvanced (e.g.,'SUBMITTED'→'APPROVED_AGENT').write_off_packet.current_approver_roleupdated.- On
COMPLETE:billing_item_detail.write_off_status_cd = 'WRITTEN_OFF';write_off_packet.cash_receipt_idpopulated;completed_dtandcompleted_by_user_idpopulated. - One
packet_status_historyrow withaction_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_cdis an approvable status.current_approver_rolematches the rejecting user's role.- User has
canApproveWorksheetspermission. - Rejection reason is non-empty.
Procedure reference: Reject Packet
Steps:
- Approver clicks the reject icon (red X) on the packet in the Approval Dashboard.
- The Reject dialog opens; approver enters a required rejection reason.
- Approver clicks "Reject."
packet_status_cdis set to the role-specific rejected status (e.g.,'REJECTED_VP').current_approver_roleis cleared.write_off_packet.rejected_dt,rejected_by_user_id, andrejection_reasonpopulated.- A
packet_status_historyrecord is written withaction_cd = 'REJECT'andcomment_text= reason. - Packet returns to an editable state.
Postconditions:
write_off_packet.packet_status_cdset to the correspondingREJECTED_*status.write_off_packet.current_approver_role = null.write_off_packet.rejected_dt,rejected_by_user_id,rejection_reasonpopulated.- One
packet_status_historyrow withaction_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_cdis anyREJECTED_*status.
Procedure reference: Resubmit Packet
Steps:
- User views the rejected packet detail page and sees "Resubmit for Approval" button.
- User clicks "Resubmit for Approval."
packet_status_cd→'SUBMITTED';current_approver_role→'AGENT'.rejected_dt,rejected_by_user_id,rejection_reasonare cleared.- A
packet_status_historyrecord is written withaction_cd = 'RESUBMIT'. - 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_historyrow withaction_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
canApproveWorksheetspermission. - Recovery reason is non-empty.
Procedure reference: Recover Packet
Steps:
- Approver initiates recovery via the approval service (recovery action on a
COMPLETEpacket). - Recovery reason is required and entered.
billing_item_detail.write_off_status_cdreverted (to'NOT_WRITTEN_OFF');write_off_packet_idcleared;recovered_dtpopulated for each packet receivable.packet_status_cd→'RECOVERED';recovered_dtandrecovered_by_user_idpopulated on the packet.- A reversal worksheet is created to post the offsetting GL entries.
- A
packet_status_historyrecord is written withaction_cd = 'RECOVER'andcomment_text= reason.
Postconditions:
write_off_packet.packet_status_cd = 'RECOVERED'(terminal — no further transitions).write_off_packet.recovered_dtandrecovered_by_user_idpopulated.billing_item_detail.write_off_status_cdreverted for each packet receivable;recovered_dtpopulated.- Reversal worksheet created for GL pickup.
- One
packet_status_historyrow withaction_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'orREJECTED_*. - Receivable is currently in the packet.
Procedure reference: Remove Receivable from Packet
Steps:
- User clicks the trash icon on a receivable row (single) or selects multiple rows and clicks "Delete Selected N."
- A confirmation dialog appears.
- User confirms deletion.
packet_receivablerow is deleted (cascading to any receivable-level documents).- Aggregate recalculation updates
total_commission_amtandreceivable_counton the packet.
Postconditions:
packet_receivablerow removed.write_off_packet.total_commission_amtandreceivable_countrecalculated.
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
| Action | CASH_MANAGER | CASH_PROCESSOR | SETTLEMENT_APPROVER | AGENT | DEPT_HEAD | VP_CLIENT_ACCT | CFO | MD | IT |
|---|---|---|---|---|---|---|---|---|---|
| Create packet | Yes | Yes | — | — | — | — | — | — | Yes |
| Add / remove receivables | Yes | Yes | — | — | — | — | — | — | Yes |
| Set eligibility criteria | Yes | Yes | — | — | — | — | — | — | Yes |
| Upload documents | Yes | Yes | — | — | — | — | — | — | Yes |
| Submit packet | Yes | Yes | — | — | — | — | — | — | Yes |
| Delete draft packet | Yes | Yes | — | — | — | — | — | — | Yes |
| Approve at Agent level | — | — | — | Yes | — | — | — | — | Yes |
| Approve at Dept Head level | — | — | — | — | Yes | — | — | — | Yes |
| Approve at VP level | — | — | Yes | — | — | Yes | — | — | Yes |
| Approve at CFO level | — | — | — | — | — | — | Yes | — | Yes |
| Approve at MD level | — | — | — | — | — | — | — | Yes | Yes |
| Reject at any level | — | — | — | Yes | Yes | Yes | Yes | Yes | Yes |
| Resubmit after rejection | Yes | Yes | — | — | — | — | — | — | Yes |
| Recover completed write-off | — | — | Yes | — | — | — | — | — | Yes |
| View packets (read-only) | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
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'orREJECTED_*). - 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
| Source | Data Provided | Mechanism |
|---|---|---|
| Billing Items workflow | REV-type billing_item_detail records eligible for write-off; billing_item_detail_amt, due dates, client/buyer/deal context | FK lookup via packet_receivable.billing_item_detail_id → billing_item_detail.billing_item_detail_id |
| AR Aging reports | Identification of aged receivables (180+ days outstanding) recommended for the AGED eligibility code | Read: billing_item.billing_item_aging_dt and billing_item_detail queried in AR Aging report; surfaces to users for write-off nomination |
| Deals and Parties | Client name, buyer name, deal name, department | FK joins through billing_item to party, deal, department when enriching packet receivable display data |
7.2 Downstream
| Consumer | Data Consumed | Mechanism |
|---|---|---|
| General Ledger (via write-off worksheet) | Write-off GL entries posted via the auto-approved WRITE_OFF cash receipt worksheet created on packet completion | write_off_packet.cash_receipt_id → cash_receipt → cash_receipt_worksheet → GL batch jobs |
| General Ledger reversal | Recovery GL entries via the reversal worksheet created on packet recovery | WriteOffCashReceiptService.createReversalWorksheet creates a new worksheet for GL pickup |
| CECL Reporting | Written-off receivables excluded from credit loss calculations via billing_item_detail.exclude_from_cecl_ind = true | Read: exclude_from_cecl_ind in regulatory reporting queries |
| AR Aging | Written-off receivables excluded from aging reports via write_off_status_cd | Read: billing_item_detail.write_off_status_cd in AR aging report queries |
| Billing Items workflow | Write-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 recovery | Direct 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:
searchPackets— Search 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 / Column | Source | Editable? | Condition |
|---|---|---|---|
| Breadcrumb "Write-Off Packets" | Static | No | Always visible |
| "Add Packet" button | — | — | Always visible; navigates to /write-offs/packets/new |
Packets Table Region
Displays all write-off packets in a sortable data table.
| Field / Column | Source | Editable? | Condition |
|---|---|---|---|
| Packet Name | write_off_packet.packet_name | No | Always visible |
| Client | party.party_name via write_off_packet.client_id | No | Always visible |
| Amount | write_off_packet.total_commission_amt | No | Always visible; formatted as currency |
| Receivables | write_off_packet.receivable_count | No | Always visible |
| Status | write_off_packet.packet_status_cd | No | Always visible; displayed as a styled badge |
| Eligibility | write_off_packet.packet_eligibility_criteria_cd | No | Displays the code or "—" if null |
| Created | write_off_packet.created_dt | No | Always visible; formatted as date |
| View icon | — | — | Always visible; navigates to packet detail |
| Delete icon | — | — | Visible 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 / Column | Source | Editable? | Condition |
|---|---|---|---|
| Packet Name | User input → write_off_packet.packet_name | Yes | Always visible; required field |
| Client | Client search → write_off_packet.client_id | Yes | Always visible; required field |
| Create Packet button | — | — | Always visible; disabled when isSubmitting = true |
| Cancel button | — | — | Always 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:
getPacketWithDetails— Get Packet With Details — loads the full packet with all user names and timestamps.getReceivablesByPacket— Get Receivables By Packet — loads the receivables table.getCurrentUser— loads current user for display name andcanApproveWorksheetsflag.
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 / Column | Source | Editable? | Condition |
|---|---|---|---|
| Packet Name | write_off_packet.packet_name | Yes (inline) | Editable when canEdit (DRAFT or REJECTED_*); read-only otherwise |
| Status Badge | write_off_packet.packet_status_cd | No | Always visible |
| Client | party.party_name via write_off_packet.client_id | No | Always visible |
| Eligibility selector | write_off_packet.packet_eligibility_criteria_cd | Yes (dropdown + clear) | Editable when canEdit; read-only text otherwise |
| Comments button + count badge | Comment count for write_off_packet entity | No | Always visible; opens Comments dialog |
| Documents button + count badge | File count for write_off_packet entity | No | Always visible; opens Documents dialog |
| Total Amount | write_off_packet.total_commission_amt | No | Always visible; formatted as currency |
| Receivables count | write_off_packet.receivable_count | No | Always visible |
| Submit for Approval button | — | — | Visible when packet_status_cd = 'DRAFT'; disabled when receivable_count < 1 |
| Resubmit for Approval button | — | — | Visible when packet_status_cd is any REJECTED_* |
| Approve button | — | — | Visible when canApproveWorksheets = true and packet is in an approvable status |
| Reject button | — | — | Visible when canApproveWorksheets = true and packet is in an approvable status |
| Metadata: Created | write_off_packet.created_dt + created_by_user_name | No | Always visible |
| Metadata: Submitted | write_off_packet.submitted_dt + submitted_by_user_name | No | Visible when submitted_dt is non-null |
| Metadata: Last Saved | write_off_packet.updated_dt + updated_by_user_name | No | Always visible |
| Metadata: Approved | write_off_packet.completed_dt + completed_by_user_name | No | Visible when completed_dt is non-null |
| Metadata: Rejected | write_off_packet.rejected_dt + rejected_by_user_name | No | Visible when rejected_dt is non-null; includes rejection history icon |
| Metadata: Recovered | write_off_packet.recovered_dt + recovered_by_user_name | No | Visible when recovered_dt is non-null |
| Worksheet link | write_off_packet.cash_receipt_id → latest worksheet lookup | No | Visible 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
canApproveWorksheetsand packet is in an approvable status. - Worksheet link appears only after packet reaches
COMPLETEand write-off cash receipt has been created. - Rejection history icon visible only when
rejection_reasonis 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 / Column | Source | Editable? | Condition |
|---|---|---|---|
| Select checkbox | — | Yes | Visible only when canEdit |
| Deal | deal.deal_name via billing_item.deal_id | No | Always visible |
| Client | party.party_name via billing_item.client_id | No | Always visible |
| Buyer | party.party_name via billing_item.buyer_id | No | Always visible |
| Billing Item | billing_item.billing_item_name | No | Always visible |
| Amount (REV) | billing_item_detail.billing_item_detail_amt where billing_item_detail_type_cd = 'REV' | No | Always visible; formatted as currency |
| Due Date | billing_item.billing_item_due_dt | No | Always visible |
| Current | Computed: amount where days past due <= 0 | No | Always visible |
| 1-30 Days | Computed: amount where 1 <= days past due <= 30 | No | Always visible |
| 31-60 Days | Computed: amount where 31 <= days past due <= 60 | No | Always visible |
| 61-90 Days | Computed: amount where 61 <= days past due <= 90 | No | Always visible |
| 90+ Days | Computed: amount where days past due > 90 | No | Always visible |
| Eligibility | packet_receivable.eligibility_criteria_cd | Yes (dropdown) | Editable as dropdown + clear button when canEdit; read-only text otherwise |
| Comment | packet_receivable comment count | Yes (dialog) | Always visible; comment icon with count badge opens CommentIconWithDialog |
| Documents | File count for packet_receivable entity | Yes (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
canEditand 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 wherecurrent_approver_rolematches the selected role.
Role Selector Region
Allows the approver to select which role they are acting as.
| Field / Column | Source | Editable? | Condition |
|---|---|---|---|
| Approver Role dropdown | User selection; values: AGENT, DEPT_HEAD, VP_CLIENT_ACCT, CFO, MD | Yes | Always 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 / Column | Source | Editable? | Condition |
|---|---|---|---|
| Packet Name | write_off_packet.packet_name | No | Always visible |
| Client | party.party_name via write_off_packet.client_id | No | Always visible |
| Amount | write_off_packet.total_commission_amt | No | Always visible; formatted as currency |
| Receivables | write_off_packet.receivable_count | No | Always visible |
| Submitted | write_off_packet.submitted_dt | No | Formatted as date; "—" if null |
| Status | write_off_packet.packet_status_cd | No | Displayed as badge |
| View icon | — | — | Always visible; navigates to packet detail |
| Approve icon | — | — | Always visible for rows in this filtered table |
| Reject icon | — | — | Always visible for rows in this filtered table |
Grid features:
- Sortable columns: standard table display
- Filters: Filtered at query level by
current_approver_rolematching 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
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 : RecoverWrite-Off Execution Sequence
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 completedNOTE
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
| Document | Relationship |
|---|---|
| Write-Offs Data Model | Defines all tables used in this workflow: write_off_packet, packet_receivable, packet_status_history, packet_document |
| Write-Offs Queries | Specifies all data retrieval operations: search, enriched detail fetch, eligible receivable search, pending approval lookup, history queries |
| Write-Offs Procedures | Specifies all data mutation operations: create, update, delete, add receivables, submit, approve, reject, resubmit, execute write-off, recover |
| Billing Items Data Model | packet_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 Workflow | Upstream: billing items and REV details are the source receivables that populate write-off packets |
| Worksheets Workflow | Downstream: 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 Workflow | AR 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
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