Skip to main content

Withdrawal Transaction

Complete guide to withdrawal transactions in BankLingo V2, including hold management, state flow, and concurrent safety mechanisms.


Overview

A Withdrawal Transaction debits funds from a customer's deposit account. Unlike deposits, withdrawals require hold management to prevent overdrafts in concurrent scenarios.

Key Characteristics:

  • Two-Phase Balance Updates: AvailableBalance reduced immediately (PENDING), BookBalance reduced on completion
  • Hold Management: Temporary holds prevent concurrent withdrawals from exceeding available balance
  • Approval Workflow: Large withdrawals may require manager approval
  • Channel Support: Teller, ATM, POS, Online Banking
  • Reversal Support: Full reversal capability with hold release

Transaction Flow

Entities Impacted

Scenario 1: Cash Withdrawal via Teller

Customer withdraws NGN 5,000 from account with NGN 10,000 balance

PENDING Phase (Hold Placement):

{
transactionId: "TXN123456",
transactionState: "PENDING",
impactedEntities: [
{
entityType: "DepositAccount",
entityId: 456,
entityKey: "ACC001",
fieldName: "AvailableBalance",
oldValue: 10000.00,
newValue: 4950.00, // 10000 - 5000 - 50 (fee)
deltaAmount: -5050.00
},
{
entityType: "DepositAccount",
entityId: 456,
entityKey: "ACC001",
fieldName: "HoldAmount",
oldValue: 0.00,
newValue: 5050.00,
deltaAmount: +5050.00
},
// BookBalance unchanged in PENDING phase
{
entityType: "DepositAccount",
entityId: 456,
entityKey: "ACC001",
fieldName: "BookBalance",
oldValue: 10000.00,
newValue: 10000.00, // Unchanged
deltaAmount: 0.00
}
]
}
```text
**COMPLETED Phase** (Hold Release + BookBalance Update):
```typescript
{
transactionId: "TXN123456",
transactionState: "COMPLETED",
impactedEntities: [
{
entityType: "DepositAccount",
entityId: 456,
entityKey: "ACC001",
fieldName: "BookBalance",
oldValue: 10000.00,
newValue: 4950.00,
deltaAmount: -5050.00
},
{
entityType: "DepositAccount",
entityId: 456,
entityKey: "ACC001",
fieldName: "HoldAmount",
oldValue: 5050.00,
newValue: 0.00,
deltaAmount: -5050.00
},
{
entityType: "TellerTill",
entityId: 789,
entityKey: "TILL001",
fieldName: "CashBalance",
oldValue: 100000.00,
newValue: 95000.00, // Till disburses 5000
deltaAmount: -5000.00
},
{
entityType: "GLAccount",
entityKey: "2100-001", // Customer Deposits Liability
fieldName: "DebitAmount",
deltaAmount: +5000.00
},
{
entityType: "GLAccount",
entityKey: "1010-001", // Cash in Till
fieldName: "CreditAmount",
deltaAmount: +5000.00
},
{
entityType: "GLAccount",
entityKey: "4100-001", // Fee Income
fieldName: "CreditAmount",
deltaAmount: +50.00
}
]
}
```text
### Scenario 2: ATM Withdrawal

**Customer withdraws NGN 20,000 from ATM**

```typescript
{
transactionId: "ATM789012",
transactionState: "COMPLETED",
impactedEntities: [
{
entityType: "DepositAccount",
entityId: 456,
entityKey: "ACC001",
fieldName: "AvailableBalance",
oldValue: 50000.00,
newValue: 29800.00, // 50000 - 20000 - 200 (ATM fee)
deltaAmount: -20200.00
},
{
entityType: "DepositAccount",
entityId: 456,
entityKey: "ACC001",
fieldName: "BookBalance",
oldValue: 50000.00,
newValue: 29800.00,
deltaAmount: -20200.00
},
{
entityType: "ATMDevice",
entityId: 123,
entityKey: "ATM-BRN001",
fieldName: "CashBalance",
oldValue: 500000.00,
newValue: 480000.00,
deltaAmount: -20000.00
},
{
entityType: "GLAccount",
entityKey: "2100-001", // Customer Deposits Liability
fieldName: "DebitAmount",
deltaAmount: +20000.00
},
{
entityType: "GLAccount",
entityKey: "1015-001", // ATM Cash
fieldName: "CreditAmount",
deltaAmount: +20000.00
},
{
entityType: "GLAccount",
entityKey: "4100-002", // ATM Fee Income
fieldName: "CreditAmount",
deltaAmount: +200.00
}
]
}
```text
---

## Hold Management

### Why Hold Management?

**Problem**: Without holds, concurrent withdrawals can cause overdrafts:

```text
Account Balance: NGN 10,000
User initiates two withdrawals simultaneously:
- Withdrawal A: NGN 6,000
- Withdrawal B: NGN 6,000

Without holds:
1. Both check balance (10,000 ≥ 6,000) ✓
2. Both proceed
3. Final balance: -2,000 (OVERDRAFT!)
```text
**Solution**: Hold management prevents this:

```text
1. Withdrawal A: AvailableBalance = 10,000 - 6,000 = 4,000, HoldAmount = 6,000
2. Withdrawal B: Check AvailableBalance (4,000 < 6,000) ✗ REJECTED
```text
### Hold Lifecycle

```typescript
// 1. PENDING - Place Hold
account.AvailableBalance -= (amount + fees);
account.HoldAmount += (amount + fees);
// BookBalance unchanged

// 2. COMPLETED - Release Hold, Update BookBalance
account.BookBalance -= (amount + fees);
account.HoldAmount -= (amount + fees);
// AvailableBalance already updated

// 3. REJECTED - Release Hold
account.AvailableBalance += (amount + fees);
account.HoldAmount -= (amount + fees);
// BookBalance unchanged
```text
---

## Approval Workflow

### Auto-Approval Conditions

Withdrawal is auto-approved if **ALL** conditions met:
1. Amount ≤ Auto-Approval Limit (from product config)
2. Account state is ACTIVE
3. No fraud flags on account
4. No dormancy flags
5. Customer verification passed (PIN, biometric, etc.)

### Manual Approval Process

**PENDING → APPROVAL → COMPLETED**

```typescript
// 1. Create withdrawal in PENDING state
const withdrawal = {
state: "PENDING",
amount: 100000, // Above auto-approval limit
accountId: 456,
approvalRequired: true
};

// 2. Hold placed immediately
account.AvailableBalance -= 100000;
account.HoldAmount += 100000;

// 3. Notify approvers
await notificationService.NotifyApprovers({
transactionId: withdrawal.id,
amount: 100000,
accountNumber: "ACC001",
reason: "Amount exceeds auto-approval limit"
});

// 4. Approver decision
if (approved) {
// Release hold, update book balance, disburse cash
account.BookBalance -= 100000;
account.HoldAmount -= 100000;
till.CashBalance -= 100000;
withdrawal.state = "COMPLETED";
} else {
// Release hold only
account.AvailableBalance += 100000;
account.HoldAmount -= 100000;
withdrawal.state = "REJECTED";
}
```text
---

## Limits & Validation

### Validation Rules

| Rule | Check | Error Code | Message |
|------|-------|------------|---------|
| **Account State** | Account.State is ACTIVE | 05 | Account is not active |
| **Account Locked** | !Account.IsLocked | 05 | Account is locked |
| **Account Dormant** | !Account.IsDormant | 05 | Account is dormant |
| **Positive Amount** | Amount > 0 | 12 | Invalid amount |
| **Sufficient Balance** | Amount ≤ AvailableBalance | 51 | Insufficient funds |
| **Single Transaction Limit** | Amount ≤ Product.WithdrawalTransactionLimit | 61 | Amount exceeds withdrawal limit |
| **Daily Limit** | TodayWithdrawals + Amount ≤ Product.DailyWithdrawalLimit | 65 | Daily withdrawal limit exceeded |
| **Channel Restriction** | Channel in Product.AllowedChannels | 57 | Channel not allowed |
| **Minimum Balance** | BookBalance - Amount ≥ Product.MinimumBalance | 51 | Would violate minimum balance |

---

## Fee Calculation

### Fee Configuration

```yaml
withdrawalFees:
- channel: TELLER
feeType: FLAT
amount: 50.00
minAmount: 50.00
maxAmount: 50.00
applyTo: WITHDRAWAL_AMOUNT

- channel: ATM
feeType: PERCENTAGE
percentage: 1.0
minAmount: 100.00
maxAmount: 500.00
applyTo: WITHDRAWAL_AMOUNT

- channel: POS
feeType: TIERED
tiers:
- minAmount: 0
maxAmount: 5000
fee: 50.00
- minAmount: 5001
maxAmount: 20000
fee: 100.00
- minAmount: 20001
maxAmount: null
fee: 200.00

Calculation Logic

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


GL Posting

Journal Entries

Example 1: Cash Withdrawal via Teller

AccountDescriptionDebitCredit
2100-001Customer Deposits Liability5,000.00-
4100-001Withdrawal Fee Income-50.00
1010-001Cash in Till-4,950.00

Example 2: ATM Withdrawal

AccountDescriptionDebitCredit
2100-001Customer Deposits Liability20,000.00-
4100-002ATM Fee Income-200.00
1015-001ATM Cash-19,800.00

Example 3: Online Transfer Withdrawal

AccountDescriptionDebitCredit
2100-001Customer Deposits Liability10,000.00-
4100-003Transfer Fee Income-100.00
2200-001Payable to Beneficiary Bank-9,900.00

API Usage

Request

POST /api/v2/transactions/withdrawal
Content-Type: application/json
Authorization: Bearer {token}

{
"accountEncodedKey": "ACC001",
"amount": 5000.00,
"channelType": "TELLER",
"tillId": 789,
"narration": "Cash withdrawal",
"customerReference": "CRF123",
"verificationMethod": "PIN",
"verificationData": "encrypted_pin_hash"
}
```text
### Response - Success (Auto-Approved)

```json
{
"success": true,
"transactionKey": "TXN123456",
"transactionId": 789012,
"state": "COMPLETED",
"accountNumber": "0123456789",
"accountName": "John Doe",
"oldBalance": 10000.00,
"newBalance": 4950.00,
"withdrawalAmount": 5000.00,
"feeAmount": 50.00,
"totalDebit": 5050.00,
"transactionDate": "2025-12-28T10:30:00Z",
"tillBalance": 95000.00,
"impactRecordId": 456,
"message": "Withdrawal successful"
}
```text
### Response - Pending Approval

```json
{
"success": true,
"transactionKey": "TXN123457",
"transactionId": 789013,
"state": "PENDING",
"accountNumber": "0123456789",
"withdrawalAmount": 100000.00,
"feeAmount": 100.00,
"holdPlaced": 100100.00,
"availableBalance": 49900.00,
"approvalRequired": true,
"message": "Withdrawal pending approval. Hold placed on account."
}
```text
### Response - Error (Insufficient Funds)

```json
{
"success": false,
"errorCode": "51",
"errorMessage": "Insufficient funds",
"accountNumber": "0123456789",
"availableBalance": 3000.00,
"requestedAmount": 5000.00,
"shortfall": 2000.00
}
```text
---

## Error Codes

| Code | Message | Cause | Resolution |
|------|---------|-------|------------|
| 00 | Success | Transaction completed | None |
| 05 | Account locked/dormant | Account not in ACTIVE state | Contact branch to activate |
| 12 | Invalid amount | Amount ≤ 0 | Enter valid amount |
| 14 | Invalid account | Account not found | Verify account number |
| 51 | Insufficient funds | Amount > AvailableBalance | Check balance, reduce amount |
| 57 | Channel restricted | Channel not allowed for product | Use allowed channel |
| 61 | Amount exceeds limit | Amount > WithdrawalTransactionLimit | Reduce amount or request approval |
| 65 | Daily limit exceeded | Today's withdrawals + Amount > DailyLimit | Wait until next day |
| 91 | System error | Database/network failure | Retry or contact support |

---

## Narration Templates

### Customer Narration

```text
{ChannelType} Withdrawal - {Amount:N2} from {AccountNumber}
Ref: {TransactionReference}
```text
**Example**: `TELLER Withdrawal - 5,000.00 from 0123456789 Ref: TXN123456`

### Channel Narration

```text
{CustomerName} - {ChannelType} WDR {Amount:N2} - {BranchName}
```text
**Example**: `John Doe - TELLER WDR 5,000.00 - Marina Branch`

---

## Concurrent Safety

### Scenario: Simultaneous Withdrawals

**Setup**:
- Account Balance: NGN 10,000
- User A: Withdraw NGN 6,000
- User B: Withdraw NGN 6,000
- Both requests arrive within milliseconds

**Without Hold Management** (FAILS):
```sql
-- Transaction A
SELECT AvailableBalance FROM DepositAccount WHERE Id = 456; -- Returns 10,000
-- Check: 6,000 <= 10,000 ✓

-- Transaction B (concurrent)
SELECT AvailableBalance FROM DepositAccount WHERE Id = 456; -- Returns 10,000
-- Check: 6,000 <= 10,000 ✓

-- Transaction A commits
UPDATE DepositAccount SET BookBalance = 4,000 WHERE Id = 456;

-- Transaction B commits
UPDATE DepositAccount SET BookBalance = 4,000 WHERE Id = 456; -- OVERWRITES A!

-- Result: Only one withdrawal recorded, but both disbursed! OVERDRAFT!
```text
**With Hold Management** (SAFE):
```sql
-- Transaction A
BEGIN TRANSACTION;
SELECT AvailableBalance, HoldAmount, Version
FROM DepositAccount WITH (UPDLOCK, ROWLOCK)
WHERE Id = 456; -- Returns: 10000, 0, Version 5

-- Check: 6,000 <= 10,000 ✓
-- Place hold
UPDATE DepositAccount
SET AvailableBalance = 4000,
HoldAmount = 6000,
Version = 6
WHERE Id = 456 AND Version = 5; -- ✓ Succeeds

COMMIT;

-- Transaction B (concurrent, waits for Transaction A's lock)
BEGIN TRANSACTION;
SELECT AvailableBalance, HoldAmount, Version
FROM DepositAccount WITH (UPDLOCK, ROWLOCK)
WHERE Id = 456; -- Returns: 4000, 6000, Version 6

-- Check: 6,000 <= 4,000 ✗ REJECTED
ROLLBACK;

-- Result: Only Transaction A proceeds. Account protected!
```text
### Optimistic Locking with Version Field

:::warning[Code Removed]
**Implementation details removed for security.**

Contact support for implementation guidance.
:::text
---

## Best Practices

### For Tellers
1. **Verify Customer Identity**: Always verify ID and signature before processing
2. **Count Cash Carefully**: Count cash twice before disbursing
3. **Check Available Balance**: Show customer available balance (not book balance)
4. **Document Large Withdrawals**: Note purpose for withdrawals > threshold
5. **Handle Holds**: Explain pending transactions if customer disputes balance

### For Developers
1. **Always Use Holds**: Never skip hold placement for withdrawals
2. **Lock Accounts**: Use `WITH (UPDLOCK, ROWLOCK)` in SQL queries
3. **Check Version**: Always verify version field before updating
4. **Track All Impacts**: Record every field change in TransactionImpactRecord
5. **Handle Rejections**: Always release holds on rejection or failure
6. **Test Concurrency**: Write tests simulating simultaneous withdrawals

### For Operations
1. **Monitor Large Withdrawals**: Flag withdrawals > threshold for review
2. **Review Holds**: Investigate long-standing holds (> 24 hours)
3. **Reconcile Till**: Verify till balance matches disbursements
4. **Audit Limits**: Review and adjust withdrawal limits quarterly
5. **Track Failures**: Monitor withdrawal failure rates by channel

---

## Reversal Process

### Full Reversal Steps

```typescript
// Original Withdrawal
{
transactionId: "TXN123456",
amount: 5000.00,
feeAmount: 50.00,
state: "COMPLETED",
impacts: [
{ entity: "DepositAccount", field: "BookBalance", delta: -5050.00 },
{ entity: "TellerTill", field: "CashBalance", delta: -5000.00 }
]
}

// Reversal Transaction
{
transactionId: "REV123456",
originalTransactionId: "TXN123456",
amount: 5000.00,
feeAmount: 50.00,
state: "COMPLETED",
impacts: [
{ entity: "DepositAccount", field: "BookBalance", delta: +5050.00 }, // Opposite delta
{ entity: "TellerTill", field: "CashBalance", delta: +5000.00 } // Opposite delta
]
}

// Update Original Transaction
originalTransaction.state = "REVERSED";
originalTransaction.reversedDate = DateTime.UtcNow;
originalTransaction.reversalTransactionId = "REV123456";
```text
---

## V2 API Commands

BankLingo V2 provides the **InitiateWithdrawalCommand** for BPMCore integration.

### Command Overview

- **Command**: `InitiateWithdrawalCommand`
- **Implementation**: `CB.Administration.Api/Commands/BPMCore/DepositWithdrawal/AdministrationCoreDepositTransactionCommandHandlers.cs`
- **Purpose**: Initiate withdrawal transactions with hold management and approval workflow
- **BPM Integration**: Accepts parameters via `BpmUtil.GetPropertyValue()`
- **State Management**: PENDING → APPROVED → SETTLED

### Key Features

**Hold Management**:
- PENDING state places hold on available balance (prevents overdraft)
- Book balance unchanged until SETTLED
- Hold automatically released on approval/rejection
- Prevents concurrent withdrawals exceeding available funds

**Approval Workflow Integration**:
- Large withdrawals require supervisor approval
- Hold remains in place during approval process
- Rejection releases hold without balance change
- Approval proceeds to settlement with balance deduction

**Impact Tracking**:
- All account balance changes tracked via `TransactionImpactRecord`
- Hold placement/release tracked as separate impacts
- Delta-based tracking enables accurate concurrent transaction handling
- Impact records include: AccountId, EntityType, FieldName, OldValue, NewValue

**Concurrent Safety**:
- Optimistic locking with version checking
- Hold mechanism prevents double-spending
- Delta-based balance updates (not absolute values)
- Account-level locking during settlement

**Till Integration** (Teller Context):
- When `tillId` provided, till balance automatically decreased
- Cash withdrawals reduce till balance atomically
- Till validation prevents insufficient cash scenarios
- Dual locking on account + till for concurrent safety

For detailed teller withdrawal workflows, see [Teller Withdrawal Transaction](../../teller-transactions/product/teller-withdrawal-technical-flow).

---

## Implementation Checklist

### Database Requirements
- [ ] Add `AvailableBalance` column to DepositAccount (if not exists)
- [ ] Add `HoldAmount` column to DepositAccount
- [ ] Add `Version` column to DepositAccount for optimistic locking
- [ ] Create `TransactionImpactRecord` table
- [ ] Create `ImpactedEntity` table with foreign key to TransactionImpactRecord
- [ ] Add index on `ImpactedEntity(EntityType, EntityId)` for reversal lookups
- [ ] Add `State` column to WithdrawalTransaction table

### Code Requirements
- [ ] Implement `ProcessWithdrawalAsync` with hold placement
- [ ] Add validation for available balance (not book balance)
- [ ] Implement approval workflow with hold management
- [ ] Track all entity impacts in TransactionImpactRecord
- [ ] Implement hold release on rejection
- [ ] Add optimistic locking with version checking
- [ ] Create reversal handler with delta-based restoration
- [ ] Implement fee calculation by channel
- [ ] Add GL posting logic for withdrawals

### Testing Requirements
- [ ] Unit test: Hold placement on PENDING
- [ ] Unit test: Hold release on COMPLETED
- [ ] Unit test: Hold release on REJECTED
- [ ] Integration test: Concurrent withdrawals
- [ ] Integration test: Withdrawal + reversal = original balance
- [ ] Load test: 100 concurrent withdrawals on same account
- [ ] UI test: Teller withdrawal workflow end-to-end

### Documentation Requirements
- [ ] Update API documentation with hold management details
- [ ] Create teller training guide on hold behavior
- [ ] Document error codes and resolution steps
- [ ] Create runbook for stuck holds
- [ ] Document GL account mapping by channel

---

## Developer Resources

For API implementation details, see:
- [Initiate Withdrawal API - Developer Guide](../../developer/initiate-withdrawal)
- [Deposit Transactions API Reference](../../developer/)

---

**Next**: [Transfer Transaction →](../product/transfer-technical-flow)