Secure Account Data
By the end of this lesson, you'll be able to:
- Apply Module 28's WITH USER_MODE and CRUD/FLS checks to the banking objects
- Prevent a withdrawal from making Balance__c negative
- Prevent transactions against a Frozen or Closed account
Prerequisites: "Schedule Nightly Reconciliation (Batch + Scheduled)"
Field-level security on Balance__c
public with sharing class BankAccountService {
public static List<Bank_Account__c> getAccountsFor(Id userId) {
return [
SELECT Id, Balance__c, Status__c
FROM Bank_Account__c
WHERE OwnerId = :userId
WITH USER_MODE
];
}
}
public with sharing class BankAccountService {—with sharing, not left to the default.public static List<Bank_Account__c> getAccountsFor(Id userId) {— the method signature.return [— opens the query.SELECT Id, Balance__c, Status__c— includes the sensitiveBalance__cfield.FROM Bank_Account__c— the object.WHERE OwnerId = :userId— scoped to a specific user's own accounts.WITH USER_MODE— Module 28's field- and object-level enforcement, applied automatically.];— closes the query.}— closes the method.}— closes the class.
with sharing and WITH USER_MODE are Module 28's exact security mechanisms — for a banking system, enforcing that a user can only see accounts and balances they're actually permitted to see isn't optional polish, it's the whole point of the security lesson finally mattering in a domain where the stakes are obvious.
Rejecting an overdraft in a before-insert validation trigger
trigger TransactionValidation on Transaction__c (before insert) {
Set<Id> accountIds = new Set<Id>();
for (Transaction__c t : Trigger.new) accountIds.add(t.Bank_Account__c);
Map<Id, Bank_Account__c> accounts = new Map<Id, Bank_Account__c>(
[SELECT Id, Balance__c, Status__c FROM Bank_Account__c WHERE Id IN :accountIds]
);
for (Transaction__c t : Trigger.new) {
Bank_Account__c acc = accounts.get(t.Bank_Account__c);
if (acc.Status__c != 'Active') {
t.addError('This account is not active and cannot accept transactions.');
} else if (t.Type__c == 'Withdrawal' && t.Amount__c > acc.Balance__c) {
t.addError('Insufficient funds for this withdrawal.');
}
}
}
trigger TransactionValidation on Transaction__c (before insert) {— a separate trigger, deliberately kept apart from Lesson 2'safter insertone.Set<Id> accountIds = new Set<Id>();— collects the distinct account Ids involved.for (Transaction__c t : Trigger.new) accountIds.add(t.Bank_Account__c);— one loop, no query yet.- (blank line)
Map<Id, Bank_Account__c> accounts = new Map<Id, Bank_Account__c>(— aMapconstructed directly from a query's results.[SELECT Id, Balance__c, Status__c FROM Bank_Account__c WHERE Id IN :accountIds]— exactly one query, for every involved account at once.);— closes the map constructor.- (blank line)
for (Transaction__c t : Trigger.new) {— a second loop, now with the accounts already in hand.Bank_Account__c acc = accounts.get(t.Bank_Account__c);— looks up this transaction's account from the map — no additional query.if (acc.Status__c != 'Active') {— the status check.t.addError('This account is not active and cannot accept transactions.');— blocks the record entirely.} else if (t.Type__c == 'Withdrawal' && t.Amount__c > acc.Balance__c) {— the overdraft check.t.addError('Insufficient funds for this withdrawal.');— blocks the record entirely.}— closes theif/else if.}— closes theforloop.}— closes the trigger.
This is a separate before insert trigger, deliberately kept apart from Lesson 2's balance-update after insert trigger — Module 24's separation-of-concerns principle, applied here so that validation logic and balance-update logic can each be tested (Lesson 7) and reasoned about independently.
Why validation happens before, not after, the balance update
before insert runs, and can call addError() to block the record entirely, before Lesson 2's after insert balance-update trigger ever executes. This ordering guarantee means an invalid overdraft transaction never reaches the balance-update logic at all — it's rejected outright, exactly the fail-safe design a banking system requires.
Exercise
As a comment, explain why the overdraft check belongs in a before-insert trigger rather than the after-insert trigger from Lesson 2.
Show hint
Think about what addError() does and when a record needs to be rejected relative to the balance update.
Secure Account Data Quiz
My Notes
Log in to keep private notes on this lesson.
Questions about this lesson
No questions yet — be the first to ask.
Log in to ask a question about this lesson.
Summary
This lesson layers Module 28's security discipline and genuine business-rule validation onto the banking objects — enforcing field-level security on sensitive financial data, and rejecting transactions that would violate the account's own rules.