sfb2b-review Skill
Code review standards, quality checks, and best practices for {{clientName}} B2B Commerce project.
Review Checklist
Comprehensive checklist for all code reviews. Use this systematically for every PR.
Architecture Alignment
- • Components follow Service, Selector, Domain (SSD) pattern
- • Correct layer usage:
- •Service Layer: Business logic, orchestration, external integrations
- •Selector Layer: SOQL queries, data retrieval only
- •Domain Layer: Business rules, validation, data manipulation
- •Controller Layer: API exposure only
- • LWC components don't contain business logic (logic in Apex service layer)
- • No circular dependencies between modules
- • Integration points use Named Credentials (no hardcoded endpoints)
- • Async operations use proper patterns (queueable, batch, scheduled)
Example of correct layering:
// CONTROLLER LAYER - Apex Controller
public with sharing class {{projectPrefix}}_ProductController {
@AuraEnabled(cacheable=true)
public static List<Product2> getProductsForStore(String storeId) {
// Immediately delegates to service layer
return {{projectPrefix}}_ProductService.getProductsForStore(storeId);
}
}
// SERVICE LAYER - Business Logic
public with sharing class {{projectPrefix}}_ProductService {
public static List<Product2> getProductsForStore(String storeId) {
// Complex logic, integrations
Set<Id> validProductIds = {{projectPrefix}}_ProductSelector.getActiveProductsForStore(storeId);
List<Product2> products = {{projectPrefix}}_ProductSelector.getProductsByIds(validProductIds);
// Apply business rules
{{projectPrefix}}_ProductDomain.applyDiscounts(products);
{{projectPrefix}}_ProductDomain.validateInventory(products);
return products;
}
}
// SELECTOR LAYER - Data Access Only
public with sharing class {{projectPrefix}}_ProductSelector {
public static Set<Id> getActiveProductsForStore(String storeId) {
return new Map<Id, Product2>([
SELECT Id FROM Product2
WHERE IsActive = true
AND StoreId__c = :storeId
]).keySet();
}
}
// DOMAIN LAYER - Business Rules
public with sharing class {{projectPrefix}}_ProductDomain {
public static void applyDiscounts(List<Product2> products) {
// Apply buyer-group-specific discounts
for (Product2 prod : products) {
// Discount logic
}
}
}
Naming Conventions
- • All custom Apex classes/interfaces start with
{{projectPrefix}}_prefix - • LWC component naming:
- •Use camelCase with leading lowercase:
{{projectPrefixLower}}ProductCard - •Parent folder same as component name
- •Descriptive names reflecting component purpose
- •Use camelCase with leading lowercase:
- • Class naming follows pattern:
- •Service:
{{projectPrefix}}_[Entity]Service - •Selector:
{{projectPrefix}}_[Entity]Selector - •Domain:
{{projectPrefix}}_[Entity]Domain - •Controller:
{{projectPrefix}}_[Entity]Controller - •Test:
{{projectPrefix}}_[Entity][Type]Test
- •Service:
- • Variables use camelCase
- • Constants use UPPER_SNAKE_CASE
- • Boolean variables start with
isorhas
// Correct naming
public class {{projectPrefix}}_OrderService {
private static final Integer MAX_ORDER_ITEMS = 500;
private static final String ERROR_MSG_INVALID_ORDER = 'Order is invalid';
public static Boolean isOrderValid(Order order) { ... }
private Boolean hasDiscountApplied = false;
}
// Incorrect naming
public class OrderService { ... } // Missing {{projectPrefix}}_ prefix
public class getOrderInfo() { ... } // Should be camelCase for methods
private String maxItems; // Should be MAX_ITEMS for constant
Test Coverage Requirements
- • Apex: >85% code coverage for submitted class
- • Jest: >80% coverage for LWC component
- • Both statement AND branch coverage verified
- • All public methods have tests
- • All error paths tested
- • Coverage report attached to PR
Verification commands:
# Apex coverage
sf apex run test --class-names {{projectPrefix}}_OrderServiceTest --code-coverage
# Jest coverage
npm test -- --coverage --collectCoverageFrom='force-app/**/component.js'
Security Checks
- •
No hardcoded credentials (API keys, passwords, endpoints)
- •
CRUD/FLS checks present in all queries:
apexpublic class {{projectPrefix}}_OrderSelector { public static List<Order> getOrdersByAccount(Id accountId) { // CRUD check if (!Order.sObjectType.getDescribe().isAccessible()) { throw new SecurityException('No access to Order'); } // FLS check SObjectAccessDecision decision = Security.stripInaccessible( AccessLevel.USER_MODE, [SELECT Id, OrderNumber, Amount FROM Order WHERE AccountId = :accountId] ); return (List<Order>)decision.getRecords(); } } - •
LWC prevents XSS: uses
textContentinstead ofinnerHTMLjavascript// Secure element.textContent = userInput; // Vulnerable element.innerHTML = userInput;
- •
All external API calls use Named Credentials
- •
Query parameters sanitized
- •
Sharing rules respected in all operations
- •
No
with sharingomitted without justification - •
PII fields not logged or exposed
- •
Third-party integrations validate responses
Performance Checks
- •
No SOQL queries in loops:
apex// BAD for (Order order : orders) { Order fullOrder = [SELECT Id, Amount FROM Order WHERE Id = :order.Id]; // Query in loop! } // GOOD Set<Id> orderIds = new Map<Id, Order>(orders).keySet(); List<Order> fullOrders = [SELECT Id, Amount FROM Order WHERE Id IN :orderIds]; - •
Bulkified operations (handles 200+ records)
- •
Lazy loading implemented for lists/details
- •
No synchronous callouts in critical paths
- •
Proper indexing on frequently queried fields
- •
Collection operations optimized (avoid nested loops)
- •
Governor limits monitored
Performance anti-patterns to flag:
// BAD: Multiple separate queries
for (Account acc : accounts) {
List<Contact> contacts = [SELECT Id FROM Contact WHERE AccountId = :acc.Id];
List<Opportunity> opps = [SELECT Id FROM Opportunity WHERE AccountId = :acc.Id];
}
// GOOD: Single bulkified query
Map<Id, List<Contact>> contactsByAccountId = new Map<Id, List<Contact>>();
for (Contact con : [SELECT AccountId FROM Contact WHERE AccountId IN :accountIds]) {
if (!contactsByAccountId.containsKey(con.AccountId)) {
contactsByAccountId.put(con.AccountId, new List<Contact>());
}
contactsByAccountId.get(con.AccountId).add(con);
}
Accessibility (WCAG 2.1 AA)
- •
ARIA labels on interactive elements
html<!-- Good --> <lightning-button aria-label="Add product to cart" variant="brand"> Add to Cart </lightning-button> <!-- Bad --> <button>+</button>
- •
Semantic HTML used (buttons for actions, links for navigation)
- •
Color not sole means of conveying information
- •
Form labels associated with inputs
- •
Keyboard navigation fully supported (Tab, Enter, Escape)
- •
Focus visible on all interactive elements
- •
Images have alt text
- •
Headings use correct hierarchy (h1 -> h2 -> h3)
Accessibility test template:
test('has proper ARIA labels for accessibility', async () => {
element.product = { Name: 'Test Product', Id: '001xx000003DHY' };
await element.updateComplete;
const button = element.shadowRoot.querySelector('[data-test-id="add-to-cart"]');
expect(button.getAttribute('aria-label')).toBe('Add Test Product to cart');
});
test('is fully keyboard navigable', async () => {
// Tab through all interactive elements
const button = element.shadowRoot.querySelector('button');
button.focus();
expect(document.activeElement).toBe(element.shadowRoot.host);
// Fire keyboard events
const event = new KeyboardEvent('keydown', { key: 'Enter' });
button.dispatchEvent(event);
// Verify action occurred
});
Integration Safety
- • Named Credentials used for all external endpoints
- • Retry logic present for callouts (exponential backoff)
- • Error handling graceful (no stack traces exposed)
- • Timeout values appropriate (not too long, not too short)
- • Integration points have circuit breaker pattern (fail gracefully)
- • Webhook/callback validations in place
- • Webhook signatures verified
Named Credential usage:
// CORRECT: Using Named Credential
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:{{projectPrefix}}_Middleware_Prod/orders');
req.setMethod('POST');
Http http = new Http();
HttpResponse res = http.send(req);
// INCORRECT: Hardcoded endpoint
req.setEndpoint('https://api.internal.com/orders?key=abc123');
Documentation
- • All public methods have JSDoc/Apex comments:
apex
/** * @description Retrieves active products for the specified store * @param storeId The WebStore ID * @return List of active products filtered by store * @throws SecurityException if user lacks Order access */ public static List<Product2> getProductsForStore(String storeId) { // Implementation } - • Complex logic explained with inline comments
- • Edge cases documented
- • PR description is comprehensive
- • Linked issues/tickets have clear context
- • Breaking changes clearly noted
Automated Checks
PMD Apex Rules Configuration
.pmd/rulesets/apex.xml
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="{{projectPrefix}} Apex Rules"
xmlns="http://pmd.sf.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/2.0.0
http://pmd.sf.net/ruleset_2_0_0.xsd">
<description>PMD Rules for {{clientName}} B2B Commerce</description>
<!-- Code style -->
<rule ref="category/apex/style.xml/FieldNamingConventions">
<properties>
<property name="prefix" value="m_"/>
<property name="exclusions" value="serialVersionUID"/>
</properties>
</rule>
<!-- Design -->
<rule ref="category/apex/design.xml/ExcessiveParameterList">
<properties>
<property name="threshold" value="4"/>
</properties>
</rule>
<rule ref="category/apex/design.xml/ExcessiveClassLength">
<properties>
<property name="threshold" value="1000"/>
</properties>
</rule>
<!-- Performance -->
<rule ref="category/apex/performance.xml/OperationWithLimitsInLoop">
<priority>1</priority>
</rule>
<rule ref="category/apex/performance.xml/AvoidSoqlInLoops">
<priority>1</priority>
</rule>
<!-- Best Practices -->
<rule ref="category/apex/bestpractices.xml/AvoidGlobalModifier">
<priority>3</priority>
</rule>
<rule ref="category/apex/bestpractices.xml/ApexAssertionsShouldIncludeMessage">
<priority>3</priority>
</rule>
</ruleset>
Run PMD:
sf scanner run --engine pmdapex --target force-app/ --format json > pmd-report.json
ESLint Rules for LWC
.eslintrc.json
{
"extends": ["eslint:recommended", "plugin:@lwc/recommended"],
"parserOptions": {
"ecmaVersion": 2020
},
"env": {
"browser": true,
"es2020": true,
"jest": true
},
"rules": {
"no-var": "error",
"prefer-const": "error",
"no-console": "warn",
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@lwc/lwc/no-invalid-dom-query-selector": "error",
"@lwc/lwc/valid-api": "error",
"@lwc/lwc/no-document-query": "error",
"no-restricted-globals": ["error", "window"],
"@lwc/lwc/no-dupe-class-fields": "error"
}
}
Run ESLint:
npm run lint npm run lint -- --fix # Auto-fix issues
Prettier Configuration
.prettierrc
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"printWidth": 100,
"arrowParens": "always",
"overrides": [
{
"files": "*.apex",
"options": {
"parser": "apex",
"tabWidth": 4
}
}
]
}
Run Prettier:
npm run format npm run format:check
SFDX Scanner Rules
Create configuration: .sfdx-scanner-config.json
{
"rules": [
{
"engine": "pmdapex",
"enabled": true,
"categories": ["Performance", "Security", "Design"]
},
{
"engine": "retire",
"enabled": true
}
],
"target": ["force-app/"]
}
Run scanner:
sf scanner run --config .sfdx-scanner-config.json --format json
PR Template
Create .github/pull_request_template.md
## Summary [Brief description of what this PR accomplishes - 1-2 sentences] ## Workstream - Epic/Feature: [e.g., "B2B Cart Management"] - Story: [e.g., "As a buyer, I want to modify cart quantities"] - Task: [e.g., "Implement cart update endpoint"] ## Gap Analysis Items [List any outstanding tasks or considerations] - [ ] Integration testing pending - [ ] Performance benchmarking required - [ ] Third-party API validation needed ## Type of Change - [ ] Bug fix (non-breaking change) - [ ] New feature (non-breaking change) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Refactoring (no functional change) - [ ] Security fix ## Testing **Manual Testing Steps:** 1. Navigate to product listing page 2. Select a product and add to cart 3. Update quantity in cart 4. Verify total price recalculates correctly 5. Proceed to checkout and complete order **Test Coverage:** - Jest Coverage: 85% (statements, branches, lines) - Apex Coverage: 92% (includes error paths) - Manual QA: [Pass/Fail] ## Screenshots [For UI changes, include before/after screenshots] ## Checklist ### Code Quality - [ ] Code follows project style guide - [ ] No hardcoded credentials or sensitive data - [ ] Comments explain complex logic - [ ] Variable/method names are clear and descriptive - [ ] No console.log or debug statements left ### Testing - [ ] Unit tests written and passing - [ ] Integration tests passing - [ ] All test scenarios documented - [ ] Negative test cases included - [ ] Code coverage >80% (LWC) / >85% (Apex) ### Security & Performance - [ ] CRUD/FLS checks included in queries - [ ] No SOQL in loops - [ ] Bulkified for 200+ records - [ ] No hardcoded endpoints (using Named Credentials) - [ ] Accessibility checks (WCAG 2.1 AA) ### Documentation - [ ] JSDoc/Apex comments on public methods - [ ] README updated if new setup steps required - [ ] Edge cases documented - [ ] Related issues/tickets linked ### Review Requirements - [ ] 2+ code reviewers assigned - [ ] No merge conflicts - [ ] Builds passing in CI/CD - [ ] Performance benchmarks met (if applicable) --- ## Related Issues Closes #[issue number] Related to #[issue number] ## Additional Context [Add any other context about the PR here]
Review Response Format
Severity Levels
Structure all review comments with severity levels:
CRITICAL - Must fix before merge
**CRITICAL: Security Issue**
This query doesn't check CRUD permissions before accessing Account data.
Suggested fix:
\`\`\`apex
if (!Account.sObjectType.getDescribe().isAccessible()) {
throw new SecurityException('No access to Account');
}
\`\`\`
MAJOR - Should fix before merge
**MAJOR: Performance Issue** This SOQL query inside a loop will cause governor limit errors with 200+ records. Suggested fix: - Move query outside loop - Use bulk API instead - See sfb2b-test skill for bulk testing examples
MINOR - Nice to have, not blocking
**MINOR: Code Style**
Method name should follow camelCase convention: `getProductInfo` instead of `getproductinfo`
Reference: {{projectPrefix}}_ naming conventions in sfb2b-review skill
SUGGESTION - Consider for future
**SUGGESTION: Maintainability** Consider extracting this calculation into a domain class for reusability. This is optional but would improve code reusability across the system.
Response Template
## Summary
This PR implements the cart quantity update feature for the B2B Commerce platform.
## Code Quality Review
- Architecture follows SSD pattern correctly
- Test coverage: 87% (exceeds 85% requirement)
- MAJOR: See performance concern below
## Issues Found
### 1. **CRITICAL: Security - CRUD Check Missing**
Lines 42-45 in `{{projectPrefix}}_CartService.cls`
The `updateCartItem` method queries CartItem without checking CRUD permissions.
**Fix Required:**
\`\`\`apex
public static void updateCartItem(Id cartItemId, Decimal newQuantity) {
if (!CartItem.sObjectType.getDescribe().isAccessible()) {
throw new SecurityException('Insufficient privileges');
}
// ... rest of method
}
\`\`\`
### 2. **MAJOR: Performance - SOQL in Loop**
Lines 58-62 in `{{projectPrefix}}_CartDomain.cls`
This loop will trigger SOQL in loop error:
\`\`\`apex
for (CartItem item : cartItems) {
Product2 prod = [SELECT UnitPrice FROM Product2 WHERE Id = :item.Product2Id]; // Query in loop
}
\`\`\`
**Fix Required:**
\`\`\`apex
Set<Id> productIds = new Map<Id, CartItem>(cartItems).keySet();
Map<Id, Product2> products = new Map<Id, Product2>([
SELECT Id, UnitPrice FROM Product2 WHERE Id IN :productIds
]);
for (CartItem item : cartItems) {
Product2 prod = products.get(item.Product2Id);
}
\`\`\`
### 3. **MINOR: Naming Convention**
Variable `qty` should be `quantity` for clarity.
## Approvals Needed
- [ ] 2+ reviewers
- [ ] Security team (for CRUD implementation)
- [ ] Performance review (verify bulkification)
## Ready to Merge?
**NO** - Critical security issue must be resolved first.
---
Approve after addressing CRITICAL and MAJOR items.
Anti-Patterns to Flag
Review all code for these problematic patterns:
1. God Classes (Single Responsibility Violation)
Pattern to reject:
// BAD: {{projectPrefix}}_OrderManager does everything
public class {{projectPrefix}}_OrderManager {
public static void createOrder() { }
public static void updateInventory() { }
public static void calculateTax() { }
public static void submitToERP() { }
public static void sendNotification() { }
public static void generateReport() { }
}
Correct structure:
// GOOD: Separated concerns
public class {{projectPrefix}}_OrderService { ... } // Creates/manages orders
public class {{projectPrefix}}_InventoryService { ... } // Inventory operations
public class {{projectPrefix}}_TaxService { ... } // Tax calculations
public class {{projectPrefix}}_ERPIntegration { ... } // ERP submission
public class {{projectPrefix}}_NotificationService { ... } // Notifications
2. Mixed Concerns in LWC
Pattern to reject:
// BAD: Component with business logic
export default class {{projectPrefixLower}}Order extends LightningElement {
handleAddToCart() {
// Complex business logic in component
const subtotal = this.cartItems.reduce((sum, item) => {
return sum + item.quantity * item.unitPrice;
}, 0);
const taxRate = this.getTaxRate(this.billingState);
const tax = subtotal * taxRate;
const total = subtotal + tax;
// ...
}
}
Correct pattern:
// GOOD: Call service, let Apex handle logic
import { {{projectPrefix}}_CartController } from '@salesforce/apex/{{projectPrefix}}_CartController';
export default class {{projectPrefixLower}}Order extends LightningElement {
async handleAddToCart(cartItem) {
// Delegate to service
const cartTotal = await {{projectPrefix}}_CartController.addItemToCart(cartItem);
this.displayTotal(cartTotal);
}
}
3. Hardcoded IDs and Endpoints
Pattern to reject:
// BAD: Hardcoded IDs and URLs
public class {{projectPrefix}}_ProductService {
private static final String STORE_ID = '0ZeQ30000000001CAA'; // Hardcoded ID
private static final String API_ENDPOINT = 'https://api.internal.com/products'; // Hardcoded
public static void syncProducts() {
HttpRequest req = new HttpRequest();
req.setEndpoint(API_ENDPOINT);
}
}
Correct pattern:
// GOOD: Use Named Credentials and Custom Metadata
public class {{projectPrefix}}_ProductService {
public static void syncProducts() {
{{projectPrefix}}_Config__mdt config = {{projectPrefix}}_Config__mdt.getInstance('{{projectPrefix}}_Prod');
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:{{projectPrefix}}_Middleware_Prod/products'); // Named Credential
WebStore store = {{projectPrefix}}_WebStoreSelector.getDefaultStore(); // Query vs hardcode
}
}
4. SOQL in Loops
Pattern to reject:
// BAD: Multiple queries in loop
for (Account acc : accounts) {
List<Contact> contacts = [SELECT Id, Name FROM Contact WHERE AccountId = :acc.Id];
List<Opportunity> opportunities = [SELECT Id, Amount FROM Opportunity WHERE AccountId = :acc.Id];
// Process
}
Correct pattern:
// GOOD: Single bulkified query
Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
Map<Id, List<Contact>> contactsByAccountId = new Map<Id, List<Contact>>();
for (Contact con : [SELECT AccountId, Name FROM Contact WHERE AccountId IN :accountIds]) {
if (!contactsByAccountId.containsKey(con.AccountId)) {
contactsByAccountId.put(con.AccountId, new List<Contact>());
}
contactsByAccountId.get(con.AccountId).add(con);
}
5. Missing Null Checks
Pattern to reject:
// BAD: No null checks
public static Decimal calculateOrderTotal(Order order) {
Decimal total = order.Amount + order.Tax; // order or Amount could be null
return total;
}
Correct pattern:
// GOOD: Defensive null checking
public static Decimal calculateOrderTotal(Order order) {
if (order == null || order.Amount == null) {
throw new IllegalArgumentException('Order and Amount required');
}
Decimal tax = order.Tax != null ? order.Tax : 0;
Decimal total = order.Amount + tax;
return total;
}
6. Synchronous Long-Running Callouts
Pattern to reject:
// BAD: Synchronous callout during transaction
public static void submitOrder(Id orderId) {
Order order = [SELECT Id, OrderNumber FROM Order WHERE Id = :orderId];
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:{{projectPrefix}}_Middleware_Prod/orders');
req.setMethod('POST');
HttpResponse res = http.send(req); // Blocks entire transaction
update order;
}
Correct pattern:
// GOOD: Queue async callout
public static void submitOrder(Id orderId) {
Order order = [SELECT Id, OrderNumber FROM Order WHERE Id = :orderId];
order.Status = 'Processing';
update order;
// Queue async job to prevent blocking
System.enqueueJob(new {{projectPrefix}}_OrderSubmissionQueueable(orderId));
}
// In separate queueable class
public class {{projectPrefix}}_OrderSubmissionQueueable implements Queueable, Database.AllowsCallouts {
private Id orderId;
public void execute(QueueableContext context) {
Http http = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:{{projectPrefix}}_Middleware_Prod/orders');
HttpResponse res = http.send(req); // Async, non-blocking
}
}
7. Unhandled Exceptions
Pattern to reject:
// BAD: No exception handling
public static List<Order> getOrders() {
return [SELECT Id, OrderNumber FROM Order LIMIT 1000]; // What if SOQL fails?
}
Correct pattern:
// GOOD: Graceful error handling
public static List<Order> getOrders() {
try {
return [SELECT Id, OrderNumber FROM Order LIMIT 1000];
} catch (QueryException e) {
System.debug(LoggingLevel.ERROR, 'Failed to retrieve orders: ' + e.getMessage());
throw new ApplicationException('Unable to retrieve orders', e);
}
}
Pre-Merge Checklist
Final checklist before approving and merging PR:
- • All automated checks passing (ESLint, PMD, Jest, Apex tests)
- • Code review approved by 2+ team members
- • No blocking comments remaining
- • Test coverage meets minimum requirements
- • Security review complete (if applicable)
- • Performance validation passed
- • Accessibility verified (if UI changes)
- • Documentation updated
- • No conflicts with target branch
- • CI/CD pipeline green
- • Linked issues updated to reflect completion
Quick Review Commands
# Run full review suite
npm run lint && npm test -- --coverage && sf scanner run
# Check specific file
npm run lint force-app/main/default/lwc/{{projectPrefixLower}}ProductCard/{{projectPrefixLower}}ProductCard.js
# Run Apex tests with coverage
sf apex run test --class-names {{projectPrefix}}_OrderServiceTest --code-coverage
# Generate PMD report
sf scanner run --engine pmdapex --format json > pmd-report.json
# Check test coverage
npm test -- --coverage --collectCoverageFrom='force-app/main/default/lwc/**/*.js'
Reference: Common Review Comments
### Must Always Include
- [ ] CRUD/FLS checks in queries
- [ ] SOQL bulkification (no loops with queries)
- [ ] Unit test coverage >80-85%
- [ ] JSDoc comments on public methods
- [ ] {{projectPrefix}}_ prefix on all custom classes
### Must Not Include
- [ ] Hardcoded credentials or endpoints
- [ ] console.log statements
- [ ] Synchronous callouts
- [ ] Commented-out code blocks
- [ ] TODO/FIXME without context