Skip to main content

Cancel Transaction

Overview

Cancel a PENDING transaction before it is settled. This command intelligently reverses all entity changes (holds, balance reserves, state transitions) that were made when the transaction was created.

When to Use

  • ✅ Customer changes their mind before approval
  • ✅ Operational error detected before settlement
  • ✅ Transaction no longer needed
  • ✅ Cleanup of stale pending transactions

When NOT to Use


Endpoint

POST /api/bpm/cmd/CancelTransactionCommand

Request Body

FieldTypeRequiredDescription
transactionKeystringYesUnique transaction reference (e.g., "TRX-2026-001234")
reasonstringNoReason for cancellation (recommended for audit trail)

Example Request

{
"transactionKey": "TRX-2026-001234",
"reason": "Customer requested cancellation before approval"
}

Response Format

Success Response

{
"success": true,
"message": "Transaction cancelled successfully. Reversed 3 entity changes.",
"code": "SUCCESS",
"data": {
"transactionKey": "TRX-2026-001234",
"oldState": "PENDING",
"newState": "CANCELLED",
"reversedChanges": 3,
"reason": "Customer requested cancellation before approval"
}
}

Error Response - Already Settled

{
"success": false,
"message": "Cannot cancel transaction. Current state: SETTLED. Only PENDING or HOLD transactions can be cancelled.",
"code": "INVALID_STATE_TRANSITION"
}

Error Response - Not Found

{
"success": false,
"message": "Transaction not found: TRX-2026-999999",
"code": "TRANSACTION_NOT_FOUND"
}

What Gets Reversed

When you cancel a transaction, the system automatically reverses:

1. Hold Amounts

BEFORE: Account balance = 50,000, Hold = 10,000
AFTER: Account balance = 50,000, Hold = 0

2. Balance Reserves

BEFORE: Available balance = 40,000 (50,000 - 10,000 hold)
AFTER: Available balance = 50,000

3. Account State Changes

BEFORE: Account state = PENDING_APPROVAL
AFTER: Account state = ACTIVE (restored to original)

4. Loan Account Changes

BEFORE: Principal due = 10,000 (reserved for payment)
AFTER: Principal due = 0 (reservation removed)

State Transition Diagram


Usage Examples

Example 1: Cancel Customer Deposit

const response = await fetch('/api/bpm/cmd/CancelTransactionCommand', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
transactionKey: 'TRX-2026-001234',
reason: 'Customer provided incorrect account number'
})
});

const data = await response.json();
if (data.success) {
console.log(`Cancelled successfully. Reversed ${data.data.reversedChanges} changes.`);
}

Example 2: Bulk Cancellation of Old Pending Transactions

// Find old pending transactions
const oldPending = await getTransactions({
status: [0], // PENDING
endDate: new Date(Date.now() - 30*24*60*60*1000).toISOString() // Older than 30 days
});

// Cancel each one
for (const txn of oldPending.data.items) {
await fetch('/api/bpm/cmd/CancelTransactionCommand', {
method: 'POST',
body: JSON.stringify({
transactionKey: txn.transactionKey,
reason: 'Automatic cleanup of stale pending transaction'
})
});
}

Comparison with Other Commands

FeatureCancel TransactionReject TransactionReverse Transaction
Valid StatesPENDING, HOLDPENDING (in workflow)SETTLED
Use CaseSimple cancellationWorkflow rejectionUndo completed txn
Requires CategoryNoYes (rejection category)Yes (reversal category)
Generates ReportNoYes (if FRAUD/AML)Yes (reversal journal)
Balance ImpactReleases holdsReleases holdsRestores balance

Security & Permissions

Required Permissions

  • Role: ManageTransactions or CancelTransactions
  • Scope: Can cancel own transactions; managers can cancel any

Audit Trail

All cancellations are logged with:

  • User who cancelled
  • Timestamp
  • Cancellation reason
  • Reversed entity changes
  • Original transaction details

Best Practices

✅ DO

  • ✅ Always provide a clear reason for cancellation
  • ✅ Verify transaction state before cancelling
  • ✅ Notify customer if applicable
  • ✅ Document operational errors

❌ DON'T

  • ❌ Don't use for settled transactions (use reverse instead)
  • ❌ Don't cancel without checking impact
  • ❌ Don't skip reason field
  • ❌ Don't cancel as part of normal workflow (use reject instead)


Testing

describe('Cancel Transaction', () => {
it('should cancel pending transaction', async () => {
// Create pending transaction
const txn = await createDeposit({ amount: 10000, requireApproval: true });
expect(txn.state).toBe('PENDING');

// Cancel it
const cancel = await cancelTransaction({
transactionKey: txn.transactionKey,
reason: 'Test cancellation'
});

expect(cancel.success).toBe(true);
expect(cancel.data.newState).toBe('CANCELLED');
});

it('should not cancel settled transaction', async () => {
const txn = await createDeposit({ amount: 10000, requireApproval: false });
expect(txn.state).toBe('SETTLED');

const cancel = await cancelTransaction({ transactionKey: txn.transactionKey });
expect(cancel.success).toBe(false);
expect(cancel.message).toContain('Cannot cancel');
});
});