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');
});
});