Skip to content

Class Name: SafeUpdateService

Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/SafeUpdateService.cls

API Name: SafeUpdateService Type: Service (Invocable) Test Coverage: To be determined

Business Purpose

The SafeUpdateService class provides a Flow-invocable method for performing partial-success DML updates with automatic error logging. This service enables Flows to: - Update records without failing the entire transaction on individual record errors - Automatically log failures to Flow_Error_Log__c for monitoring - Continue processing valid records when some records have validation errors - Return success/failure status to Flow for decision logic

This supports resilient Flow automation that handles data quality issues gracefully.

Class Overview

  • Scope/Sharing: with sharing - Respects record-level security
  • Key Responsibilities:
  • Perform partial-success DML updates
  • Log failed records to Flow_Error_Log__c
  • Return error status to Flow
  • Support multiple object types

Inner Classes

RequestWrapper

public class RequestWrapper {
    @InvocableVariable(required=true)
    public List<SObject> recordsToUpdate;

    @InvocableVariable(required=false)
    public String flowName;
}

Fields: - recordsToUpdate (List, required): Records to update - flowName (String, optional): Flow name for error log context

Public Methods

safeUpdateRecords

@InvocableMethod(label='Safe Update Records')
public static List<Boolean> safeUpdateRecords(List<RequestWrapper> requests)

Purpose: Updates records with partial success, logging errors without failing transaction.

Parameters: - requests (List) - List of update requests from Flow

Returns: List<Boolean> - One boolean per request indicating if ANY errors occurred

Business Logic:

  1. Partial Success Update (line 10):
    Database.SaveResult[] results = Database.update(req.recordsToUpdate, false);
    
  2. false parameter enables partial success (allOrNone=false)
  3. Individual record failures don't stop others

  4. Error Detection (lines 12-32):

  5. Iterates through SaveResult array
  6. For each failed record:

    • Extracts record Id and object type
    • Populates object-specific lookup fields (lines 20-24)
    • Creates Flow_Error_Log__c record
    • Sets hasErrors flag
  7. Object-Specific Lookups (lines 20-24):

    if (objectName == 'Subscription__c') { errorLogRecord.SubscriptionId__c = recordId; }
    else if (objectName == 'Membership__c') { errorLogRecord.MembershipId__c = recordId; }
    else if (objectName == 'Account') { errorLogRecord.AccountId__c = recordId; }
    else if (objectName == 'Order') { errorLogRecord.Order__c = recordId; }
    else if (objectName == 'OrderSummary') { errorLogRecord.Order_Summary__c = recordId; }
    

  8. Populates lookup fields for common object types
  9. Objects not in list only get generic ObjectName__c

  10. Error Log Insertion (lines 35-37):

    if (!errorLogs.isEmpty()) {
        insert errorLogs;
    }
    

  11. Inserts all error logs in single DML operation
  12. No error handling if log insertion fails

  13. Return Value Construction (lines 39-43):

    List<Boolean> resultsToReturn = new List<Boolean>();
    for (Integer i = 0; i < requests.size(); i++) {
        resultsToReturn.add(hasErrors);
    }
    return resultsToReturn;
    

  14. BUG: Returns same hasErrors value for ALL requests
  15. Should track errors per request, not globally

Issues/Concerns: - 🚨 CRITICAL BUG (lines 40-42): Returns same boolean for all requests - If ANY request has errors, ALL return true - Cannot distinguish which specific request failed - Should track hasErrors per request - ⚠️ No Error Log Insert Handling: Line 36 insert can fail silently - ⚠️ Global Error Flag: Single hasErrors for all requests (line 6) - ⚠️ Limited Object Support: Only 5 object types have lookup fields - ✅ Partial Success: Properly uses allOrNone=false - ✅ Bulk Error Logging: Single insert for all error logs

Correct Implementation:

@InvocableMethod(label='Safe Update Records')
public static List<Boolean> safeUpdateRecords(List<RequestWrapper> requests) {
    List<Flow_Error_Log__c> errorLogs = new List<Flow_Error_Log__c>();
    List<Boolean> resultsToReturn = new List<Boolean>();

    for (RequestWrapper req : requests) {
        Boolean hasErrors = false;

        Database.SaveResult[] results = Database.update(req.recordsToUpdate, false);

        for (Integer i = 0; i < results.size(); i++) {
            if (!results[i].isSuccess()) {
                hasErrors = true;
                for (Database.Error err : results[i].getErrors()) {
                    String recordId = (String)req.recordsToUpdate[i].get('Id');
                    String objectName = req.recordsToUpdate[i].getSObjectType().getDescribe().getName();

                    Flow_Error_Log__c errorLogRecord = new Flow_Error_Log__c();
                    if (objectName == 'Subscription__c') { errorLogRecord.SubscriptionId__c = recordId; }
                    else if (objectName == 'Membership__c') { errorLogRecord.MembershipId__c = recordId; }
                    else if (objectName == 'Account') { errorLogRecord.AccountId__c = recordId; }
                    else if (objectName == 'Order') { errorLogRecord.Order__c = recordId; }
                    else if (objectName == 'OrderSummary') { errorLogRecord.Order_Summary__c = recordId; }

                    errorLogRecord.ObjectName__c = objectName;
                    errorLogRecord.ErrorMessage__c = err.getMessage();
                    errorLogRecord.FlowRunDateTime__c = System.now();
                    errorLogRecord.FlowName__c = req.flowName;
                    errorLogs.add(errorLogRecord);
                }
            }
        }

        resultsToReturn.add(hasErrors);
    }

    if (!errorLogs.isEmpty()) {
        try {
            insert errorLogs;
        } catch (DmlException e) {
            System.debug(LoggingLevel.ERROR, 'Failed to insert error logs: ' + e.getMessage());
        }
    }

    return resultsToReturn;
}

Flow Usage Example:

Flow: Update Membership Records

Get Records: memberships (Membership__c collection)

Loop: For each membership
  Assignment: Update membership fields

Action: Safe Update Records
  recordsToUpdate: {!memberships}
  flowName: "Update Membership Records"

Store Output: hasErrors

Decision: Check if errors occurred
  If hasErrors = true
    Send Email Alert to Admin
  Else
    Display Success Message

Dependencies

Salesforce Objects

  • Flow_Error_Log__c (Custom Object)
  • Fields: ObjectName__c, ErrorMessage__c, FlowRunDateTime__c, FlowName__c, SubscriptionId__c, MembershipId__c, AccountId__c, Order__c, Order_Summary__c
  • Access: Create (INSERT)

Custom Settings/Metadata

  • None

Other Classes

  • Similar to: DailyMembershipUtil.safeUpdate()

Design Patterns

  1. Invocable Method Pattern: Flow integration
  2. Partial Success DML: allOrNone=false for resilient updates
  3. Error Aggregation: Collects errors before bulk insert
  4. Object-Type Switching: Handles multiple object types dynamically

Governor Limits Considerations

Current Impact (Per Transaction)

  • DML Statements: 2 per request (1 update + 1 error log insert)
  • DML Rows: Variable based on records and failures
  • SOQL Queries: 0
  • Heap Size: Proportional to number of errors

Scalability Analysis

  • Bulk Error Logging: Single insert for all errors
  • ⚠️ Per-Request Processing: Processes each request separately (not bulkified across requests)
  • ⚠️ No Maximum Validation: Doesn't validate total records across requests

Error Handling

Exception Types Thrown

  • None - Method doesn't throw exceptions

Exception Types Caught

  • None - No try-catch blocks

Error Handling Gaps

  1. Silent Error Log Failure: Line 36 insert can fail without notification
  2. No DML Exception Handling: Update failures not caught
  3. No Input Validation: Doesn't check for null/empty inputs

Security Considerations

Sharing Model

  • WITH SHARING: Respects record-level security
  • Updates only records user has access to

FLS Considerations

  • ⚠️ No FLS Validation: Doesn't check field-level security
  • Relies on Database.update to enforce security

Test Class Requirements

@IsTest
public class SafeUpdateServiceTest {

    @TestSetup
    static void setup() {
        Account acc = new Account(Name = 'Test Account');
        insert acc;

        List<Membership__c> memberships = new List<Membership__c>();
        for (Integer i = 0; i < 5; i++) {
            memberships.add(new Membership__c(
                Account_Name__c = acc.Id,
                Status__c = 'Active',
                Start_Date__c = Date.today()
            ));
        }
        insert memberships;
    }

    @IsTest
    static void testSafeUpdateRecords_Success() {
        List<Membership__c> memberships = [SELECT Id, Status__c FROM Membership__c];
        for (Membership__c m : memberships) {
            m.Status__c = 'Expired';
        }

        SafeUpdateService.RequestWrapper req = new SafeUpdateService.RequestWrapper();
        req.recordsToUpdate = memberships;
        req.flowName = 'Test Flow';

        Test.startTest();
        List<Boolean> results = SafeUpdateService.safeUpdateRecords(new List<SafeUpdateService.RequestWrapper>{req});
        Test.stopTest();

        Assert.areEqual(1, results.size(), 'Should return one result');
        Assert.isFalse(results[0], 'Should have no errors');

        List<Flow_Error_Log__c> logs = [SELECT Id FROM Flow_Error_Log__c];
        Assert.isTrue(logs.isEmpty(), 'Should have no error logs');
    }

    @IsTest
    static void testSafeUpdateRecords_PartialFailure() {
        List<Membership__c> memberships = [SELECT Id FROM Membership__c];

        // Create one invalid update (missing required field)
        Membership__c invalidMembership = new Membership__c(Id = memberships[0].Id);
        // Assume Status__c is required, set to null

        SafeUpdateService.RequestWrapper req = new SafeUpdateService.RequestWrapper();
        req.recordsToUpdate = new List<Membership__c>{invalidMembership};
        req.flowName = 'Test Partial Failure';

        Test.startTest();
        List<Boolean> results = SafeUpdateService.safeUpdateRecords(new List<SafeUpdateService.RequestWrapper>{req});
        Test.stopTest();

        // Due to bug, this tests current behavior
        Assert.isTrue(results[0], 'Should have errors');

        List<Flow_Error_Log__c> logs = [SELECT Id, ErrorMessage__c FROM Flow_Error_Log__c];
        Assert.isFalse(logs.isEmpty(), 'Should have error logs');
    }
}

Changes & History

Date Author Description
Unknown Original Developer Initial implementation
(Current) - Documentation added

Pre-Go-Live Concerns

🚨 CRITICAL

  • Return Value Bug (lines 40-42): Returns same boolean for all requests
  • Fix to track errors per request separately
  • Breaking change for existing Flows

HIGH

  • Silent Error Log Failure: No try-catch on error log insert (line 36)
  • Add error handling to prevent silent failures
  • Log to System.debug if insert fails

MEDIUM

  • Limited Object Support: Only 5 object types have lookup fields
  • Add more object types or make configurable
  • No Input Validation: Doesn't validate null/empty inputs
  • Add validation for recordsToUpdate

LOW

  • Global Error Flag: Single hasErrors for all requests (design issue fixed in recommended code)

Maintenance Notes

📋 Monitoring Recommendations

  • Query Flow_Error_Log__c daily for failures
  • Track which Flows have highest error rates
  • Monitor object types with most failures

🔧 Future Enhancement Opportunities

  1. Configurable Object Mappings: Use custom metadata for object-to-lookup mappings
  2. Return Detailed Results: Return list of failed record IDs per request
  3. Retry Logic: Add automatic retry for transient errors
  4. Bulk Optimization: Bulkify across multiple requests

⚠️ Breaking Change Risks

  • Fixing return value bug changes Flow behavior
  • Adding exceptions changes error handling model
  • DailyMembershipUtil.safeUpdate(): Similar pattern
  • Flow_Error_Log__c: Error logging target
  • Flow Automations: Multiple flows use this service

Business Owner

Primary Contact: Salesforce Development Team Technical Owner: Salesforce Development Team Last Reviewed: [Date]


Documentation Status: ✅ Complete Code Review Status: 🚨 CRITICAL - Return value bug affects all callers Test Coverage: Test class needed