Skip to content

Class Name: I2C_QueueableRefund

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

API Name: I2C_QueueableRefund Type: Queueable (Asynchronous Processing) Test Coverage: Target 90%+

Business Purpose

The I2C_QueueableRefund class orchestrates the refund/void process for orders in the payment system. It serves as the entry point for processing financial reversals by: 1. Disabling auto-renewal on subscriptions being refunded 2. Identifying the original payment transaction 3. Determining whether to void (reverse) or refund the transaction based on timing and amount 4. Chaining additional queueable jobs (AttemptVoid or AttemptRefund) for payment processor interaction

This class implements the Chain of Responsibility pattern, passing refund processing to specialized handlers while managing business rules around subscription cancellation and transaction timing.

Class Overview

  • Scope/Sharing: with sharing - Respects record-level security
  • Implements:
  • Queueable - Asynchronous execution
  • Database.AllowsCallouts - Enables HTTP callouts for payment processing
  • Key Responsibilities:
  • Query refund order and related order items
  • Disable auto-renewal on subscriptions being refunded
  • Locate original payment transaction
  • Determine void vs refund eligibility
  • Chain payment processing queueable

Why Queueable?

This class uses the Queueable interface because: - Future Callouts: Payment voids/refunds require HTTP callouts to payment gateway - Chaining: Needs to queue additional jobs (AttemptVoid/AttemptRefund) sequentially - Data Passing: Can pass complex objects (Order, Transaction) to chained jobs - Monitoring: Provides better tracking via AsyncApexJob records

Private Instance Variables

orderId

private String orderId;

Purpose: Stores the refund Order record ID to be processed.

Type: String (should be Id for type safety)

Usage: Set in constructor (line 7), used in execute query (line 14)

skipVoid

private Boolean skipVoid;

Purpose: Flag indicating whether to skip void attempt and proceed directly to refund.

Business Logic: - true: Force refund even if transaction qualifies for void - false: Allow void if within 24-hour window and full amount

Usage: Set in constructor (line 8), checked in eligibility logic (line 37)

Use Cases for Skipping Void: - Manual refund requests requiring audit trail - Payment processor void unavailable - Policy requirements for refund instead of void

Constructor

I2C_QueueableRefund

public I2C_QueueableRefund(String processOrderId, Boolean dontAttemptVoid)

Purpose: Initializes the queueable with refund order ID and void preference.

Parameters: - processOrderId (String) - The refund Order record ID - dontAttemptVoid (Boolean) - If true, skip void and force refund

Issues/Concerns: - ⚠️ String vs Id: processOrderId should be Id type for type safety - ⚠️ Confusing Parameter Name: dontAttemptVoid double-negative is unclear - Better: forceRefund or skipVoidAttempt

Recommended Signature:

public I2C_QueueableRefund(Id refundOrderId, Boolean forceRefund) {
    this.orderId = refundOrderId;
    this.skipVoid = forceRefund;
}

Public Methods

execute()

public void execute(QueueableContext context)

Purpose: Executes the refund orchestration logic asynchronously.

Parameters: - context (QueueableContext) - Standard queueable context (unused)

Returns: void

Business Logic:

1. Query Refund Order (lines 14-15)

Order ord = [
    SELECT Id, RelatedOrderId, TotalAmount
    FROM Order
    WHERE Id = :orderId
    LIMIT 1
][0];

Fields Retrieved: - Id: Refund order identifier - RelatedOrderId: Link to original paid order - TotalAmount: Refund amount (negative value)

Issues: - ⚠️ Array Index Access: [0] throws exception if order not found - ⚠️ No Null Check: Assumes order always exists - ⚠️ Missing Fields: Doesn't query Status, Type, or other validation fields

2. Query Refund Order Items (lines 17-21)

List<OrderItem> ois = [
    SELECT Id, RelatedOrderItemId
    FROM OrderItem
    WHERE OrderId = :ord.Id
];
Set<Id> relatedOrderItemIds = new Set<Id>();
for(OrderItem oi : ois){
    relatedOrderItemIds.add(oi.RelatedOrderItemId);
}

Purpose: Identifies original order items being refunded.

Data Model: - Refund OrderRefund OrderItemsRelatedOrderItemIdOriginal OrderItems - Builds set of original order item IDs for subscription lookup

Issues: - ⚠️ No Null Check: RelatedOrderItemId could be null (line 20) - ⚠️ Unbounded Query: No LIMIT clause (could return thousands of items)

3. Disable Auto-Renewal on Subscriptions (lines 23-24)

Map<Id, Subscription__c> refundedSubs = new Map<Id, Subscription__c>([
    SELECT Id
    FROM Subscription__c
    WHERE Order_Product__c IN :relatedOrderItemIds
    AND Auto_Renew__c = true
]);
I2C_AutoRenewService.disableAutoRenew(refundedSubs.KeySet());

Business Rule: When an order product is refunded, disable auto-renewal on associated subscriptions.

Query Logic: - Finds subscriptions linked to original order items - Filters to only subscriptions with auto-renewal enabled - Passes subscription IDs to I2C_AutoRenewService for disabling

Issues: - ⚠️ Service Dependency: If I2C_AutoRenewService.disableAutoRenew() fails, entire queueable fails - ⚠️ No Error Handling: Failure to disable auto-renewal not caught - ✅ Efficient Query: Only queries subscriptions needing updates

4. Find Original Payment Transaction (lines 26-31)

ChargentOrders__Transaction__c lastSaleTransaction = [
    SELECT Id, ChargentOrders__Order__c, ChargentOrders__Amount__c, ChargentOrders__Gateway_Date__c
    FROM ChargentOrders__Transaction__c
    WHERE Order__c = :ord.RelatedOrderId
    AND ChargentOrders__Type__c = 'Charge'
    AND ChargentOrders__Response_Status__c = 'Approved'
    ORDER BY CreatedDate DESC
    LIMIT 1
];
Id lastSaleTransactionId = lastSaleTransaction.Id;

Purpose: Locates the most recent successful charge transaction for the original order.

Query Criteria: - Order__c = RelatedOrderId: Transaction on original paid order - Type = 'Charge': Exclude refunds, voids, authorizations - Response_Status = 'Approved': Only successful transactions - ORDER BY CreatedDate DESC: Get most recent

Issues: - ⚠️ No Null Check: Assumes transaction always exists (line 31) - ⚠️ Array Index Missing: Should use LIMIT 1 result access safely - ⚠️ Multiple Payments: Doesn't handle partial payments or multiple transactions - ⚠️ Unused Variable: lastSaleTransactionId assigned but never used (line 31)

5. Determine Void vs Refund (lines 34-41)

if (lastSaleTransaction.ChargentOrders__Amount__c == (-1 * ord.TotalAmount)
    && lastSaleTransaction.ChargentOrders__Gateway_Date__c > Datetime.now().addMinutes(-1440)
    && !skipVoid) {
    System.enqueueJob(new AttemptVoid(ord, lastSaleTransaction));
} else {
    System.enqueueJob(new AttemptRefund(ord, lastSaleTransaction));
}

Void Eligibility Criteria (all must be true): 1. Full Amount: Refund amount equals original charge amount (line 35) - Transaction.Amount = -1 × Refund.TotalAmount - Example: $100 charge → -$100 refund 2. Within 24 Hours: Transaction processed within last 1,440 minutes (line 36) - Uses Gateway_Date (processor timestamp, not Salesforce CreatedDate) - 1,440 minutes = 24 hours 3. Void Not Skipped: skipVoid flag is false (line 37)

Business Logic Rationale: - Voids: Reverse authorization before settlement (no fees, faster processing) - Only possible within payment processor's void window (typically 24 hours) - Must be full amount (partial voids not supported) - Refunds: Post-settlement reversal (may incur fees, slower processing) - Used for partial refunds or after void window expires

Chained Queueables: - AttemptVoid: Lines up void transaction with payment gateway - AttemptRefund: Lines up refund transaction with payment gateway

Issues: - ⚠️ Equality Comparison for Decimals: Line 35 uses == for currency comparison - Floating-point precision issues possible - Should use: Math.abs(lastSaleTransaction.ChargentOrders__Amount__c + ord.TotalAmount) < 0.01 - ⚠️ Hardcoded 1440 Minutes: Should be configurable (custom metadata) - ⚠️ Gateway Date vs Created Date: Uses processor timestamp which may differ from Salesforce time - ⚠️ No Queueable Limit Check: Doesn't verify queue depth before enqueuing - ⚠️ No Return Value: Cannot determine success/failure from caller

6. Error Handling (lines 43-48)

try {
    // Void/Refund chaining logic
} catch (Exception e) {
    System.debug('Error during refund/void processing: ' + e.getMessage());
}
} catch (Exception e) {
    System.debug('Error getting Transaction/Order: ' + e.getMessage());
}

Exception Layers: - Inner Try-Catch (line 43): Catches errors during void/refund chaining - Outer Try-Catch (line 46): Catches errors during queries and subscription disabling

Error Handling Issues: - ⚠️ Silent Failure: Only logs to System.debug (lost after 24 hours) - ⚠️ No Persistent Error Logging: Should insert Flow_Error_Log__c - ⚠️ No Retry Logic: Failed queueable not retried - ⚠️ No Notification: Operations team unaware of failures - ⚠️ Generic Exception: Doesn't differentiate error types (query vs callout vs DML)

Dependencies

Salesforce Objects

Order (Standard Object)

  • Fields Read: Id, RelatedOrderId, TotalAmount
  • Access: Read
  • Relationship: Refund Order → Original Order

OrderItem (Standard Object)

  • Fields Read: Id, RelatedOrderItemId
  • Access: Read
  • Relationship: Refund OrderItem → Original OrderItem

Subscription__c (Custom Object)

  • Fields Read: Id, Auto_Renew__c, Order_Product__c
  • Access: Read (via query), Write (via I2C_AutoRenewService)
  • Relationship: Subscription → OrderItem (Order_Product__c)

ChargentOrders__Transaction__c (Managed Package Object - Chargent)

  • Fields Read: Id, ChargentOrders__Order__c, ChargentOrders__Amount__c, ChargentOrders__Gateway_Date__c, ChargentOrders__Type__c, ChargentOrders__Response_Status__c
  • Access: Read
  • Package: ChargentOrders managed package
  • Relationship: Transaction → Order

Custom Settings/Metadata

  • None (void window hardcoded at 1,440 minutes)

Other Classes

  • I2C_AutoRenewService: Disables auto-renewal on subscriptions
  • AttemptVoid: Chained queueable for void processing (inner class or separate file)
  • AttemptRefund: Chained queueable for refund processing (inner class or separate file)

External Services

  • Payment Gateway: Indirectly via AttemptVoid/AttemptRefund callouts
  • Chargent Payment Processor: Managed package integration

Design Patterns

  1. Queueable Chaining: Sequences asynchronous jobs for multi-step processing
  2. Chain of Responsibility: Delegates to specialized handlers (Void vs Refund)
  3. Strategy Pattern: Selects processing strategy based on business rules
  4. Dependency Injection: Passes Order and Transaction objects to chained jobs
  5. Guard Clauses: Early validation via try-catch wrappers

Governor Limits Considerations

Current Impact (Per Execution)

  • SOQL Queries: 4 queries
  • Order query (line 14)
  • OrderItem query (line 17)
  • Subscription query (line 23)
  • Transaction query (line 27)
  • SOQL Rows: Variable (depends on order complexity)
  • DML Statements: Variable (via I2C_AutoRenewService)
  • Queueable Jobs: 1 chained job (AttemptVoid or AttemptRefund)
  • Callouts: 0 (deferred to chained job)

Queueable Limits

  • Maximum Depth: 5 queueable jobs per transaction chain
  • Current chain: I2C_QueueableRefundAttemptVoid/AttemptRefund = Depth 2
  • Safe unless AttemptVoid/AttemptRefund chain further
  • Stack Limit: 50 queueable jobs total per transaction
  • This class only enqueues 1 additional job

Scalability Analysis

  • Asynchronous Processing: Doesn't block user transactions
  • Single Job per Refund: One queueable per refund order
  • ⚠️ Unbounded OrderItem Query: Could retrieve thousands of items
  • ⚠️ No Bulkification: Processes one refund at a time (by design)
  • ⚠️ Subscription Query: Could match many subscriptions per order

Recommendations

  1. Add Query Limits: Prevent runaway queries for complex orders

    List<OrderItem> ois = [
        SELECT Id, RelatedOrderItemId
        FROM OrderItem
        WHERE OrderId = :ord.Id
        LIMIT 2000 // Reasonable upper bound
    ];
    

  2. Monitor Queue Depth: Check for queue exhaustion

    if (System.QueueableJobs.size() >= 50) {
        throw new QueueableException('Queueable job limit exceeded');
    }
    

Error Handling

Exception Types Thrown

  • None - All exceptions caught and logged

Exception Types Caught

  • Exception (line 43): Errors during void/refund chaining
  • Queueable instantiation failures
  • System.enqueueJob() failures
  • Exception (line 46): Errors during queries and data processing
  • QueryException: Order/Transaction not found
  • DmlException: Subscription update failures (via service)
  • NullPointerException: Null field access

Error Recovery Strategy

  • No Recovery: Errors logged but not retried
  • Silent Failure: Only System.debug logging
  • Manual Intervention: Operations team must monitor logs and retry manually

Monitoring Recommendations

// Query queueable job status
SELECT Id, Status, JobType, MethodName, CompletedDate, NumberOfErrors
FROM AsyncApexJob
WHERE ApexClass.Name = 'I2C_QueueableRefund'
  AND CreatedDate = TODAY
ORDER BY CreatedDate DESC

// Query for errors
SELECT Id, Status, ExtendedStatus, NumberOfErrors
FROM AsyncApexJob
WHERE ApexClass.Name IN ('I2C_QueueableRefund', 'AttemptVoid', 'AttemptRefund')
  AND Status IN ('Failed', 'Aborted')
  AND CreatedDate = LAST_N_DAYS:7
public void execute(QueueableContext context) {
    try {
        // Validate order exists
        List<Order> orders = [
            SELECT Id, RelatedOrderId, TotalAmount, Status
            FROM Order
            WHERE Id = :orderId
            LIMIT 1
        ];

        if (orders.isEmpty()) {
            logError('Order not found: ' + orderId);
            return;
        }

        Order ord = orders[0];

        // ... existing logic with null checks ...

        // Chain with limit check
        if (Limits.getQueueableJobs() >= Limits.getLimitQueueableJobs()) {
            logError('Queueable job limit reached');
            return;
        }

        if (shouldVoid) {
            System.enqueueJob(new AttemptVoid(ord, lastSaleTransaction));
        } else {
            System.enqueueJob(new AttemptRefund(ord, lastSaleTransaction));
        }

    } catch (Exception e) {
        logError('Refund processing failed: ' + e.getMessage() + '\\n' + e.getStackTraceString());
    }
}

private void logError(String message) {
    System.debug(LoggingLevel.ERROR, message);

    try {
        insert new Flow_Error_Log__c(
            FlowName__c = 'I2C_QueueableRefund',
            ErrorMessage__c = message,
            FlowRunDateTime__c = System.now(),
            ObjectName__c = 'Order',
            RecordId__c = orderId
        );
    } catch (Exception e) {
        System.debug(LoggingLevel.FATAL, 'Cannot log error: ' + e.getMessage());
    }
}

Security Considerations

Sharing Model

  • WITH SHARING: Respects record-level security
  • Implication: User who triggered refund must have access to:
  • Refund Order
  • Original Order
  • Related OrderItems
  • Subscriptions
  • Chargent Transactions

Financial Data Access

  • Sensitive Fields:
  • Order.TotalAmount: Refund amount
  • Transaction.Amount: Payment amount
  • Transaction.Gateway_Date: Processor timestamp
  • Access Control: Ensure queueable runs in appropriate user context
  • Audit Trail: No logging of who initiated refund or when

Best Practices

  1. Run as System: Use without sharing for financial processing classes
  2. Audit Trail: Log refund initiation user and timestamp
  3. FLS Checks: Validate field access before queries (if needed)

Test Class Requirements

Required Test Coverage

@IsTest
public class I2C_QueueableRefundTest {

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

        // Create original order
        Order originalOrder = new Order(
            AccountId = acc.Id,
            EffectiveDate = Date.today(),
            Status = 'Activated',
            TotalAmount = 100.00
        );
        insert originalOrder;

        // Create original order item
        Product2 prod = new Product2(Name = 'Test Product', IsActive = true);
        insert prod;

        PricebookEntry pbe = new PricebookEntry(
            Product2Id = prod.Id,
            Pricebook2Id = Test.getStandardPricebookId(),
            UnitPrice = 100.00,
            IsActive = true
        );
        insert pbe;

        OrderItem originalOI = new OrderItem(
            OrderId = originalOrder.Id,
            PricebookEntryId = pbe.Id,
            Quantity = 1,
            UnitPrice = 100.00
        );
        insert originalOI;

        // Create subscription with auto-renewal
        Subscription__c sub = new Subscription__c(
            Name = 'Test Subscription',
            Account__c = acc.Id,
            Order_Product__c = originalOI.Id,
            Auto_Renew__c = true,
            Status__c = 'Active'
        );
        insert sub;

        // Create refund order
        Order refundOrder = new Order(
            AccountId = acc.Id,
            EffectiveDate = Date.today(),
            Status = 'Draft',
            TotalAmount = -100.00,
            RelatedOrderId = originalOrder.Id
        );
        insert refundOrder;

        // Create refund order item
        OrderItem refundOI = new OrderItem(
            OrderId = refundOrder.Id,
            PricebookEntryId = pbe.Id,
            Quantity = 1,
            UnitPrice = -100.00,
            RelatedOrderItemId = originalOI.Id
        );
        insert refundOI;

        // Create Chargent transaction (approved charge)
        ChargentOrders__Transaction__c txn = new ChargentOrders__Transaction__c(
            Order__c = originalOrder.Id,
            ChargentOrders__Amount__c = 100.00,
            ChargentOrders__Type__c = 'Charge',
            ChargentOrders__Response_Status__c = 'Approved',
            ChargentOrders__Gateway_Date__c = Datetime.now().addMinutes(-60) // 1 hour ago
        );
        insert txn;
    }

    @IsTest
    static void testVoidEligibility_FullAmountWithin24Hours() {
        Order refundOrder = [
            SELECT Id
            FROM Order
            WHERE TotalAmount < 0
            LIMIT 1
        ];

        Test.startTest();
        System.enqueueJob(new I2C_QueueableRefund(refundOrder.Id, false));
        Test.stopTest();

        // Verify subscription auto-renewal disabled
        Subscription__c sub = [SELECT Auto_Renew__c FROM Subscription__c LIMIT 1];
        Assert.isFalse(sub.Auto_Renew__c, 'Auto-renewal should be disabled');

        // Verify queueable completed
        List<AsyncApexJob> jobs = [
            SELECT Status
            FROM AsyncApexJob
            WHERE ApexClass.Name = 'I2C_QueueableRefund'
        ];
        Assert.areEqual('Completed', jobs[0].Status, 'Queueable should complete');
    }

    @IsTest
    static void testRefundPath_PartialAmount() {
        // Update refund to partial amount
        Order refundOrder = [SELECT Id FROM Order WHERE TotalAmount < 0 LIMIT 1];
        refundOrder.TotalAmount = -50.00; // Partial refund
        update refundOrder;

        Test.startTest();
        System.enqueueJob(new I2C_QueueableRefund(refundOrder.Id, false));
        Test.stopTest();

        // Should take refund path (not void)
        // Verify via chained job (if observable)
    }

    @IsTest
    static void testRefundPath_Beyond24Hours() {
        // Update transaction to be older than 24 hours
        ChargentOrders__Transaction__c txn = [
            SELECT Id
            FROM ChargentOrders__Transaction__c
            LIMIT 1
        ];
        txn.ChargentOrders__Gateway_Date__c = Datetime.now().addMinutes(-1500); // 25 hours ago
        update txn;

        Order refundOrder = [SELECT Id FROM Order WHERE TotalAmount < 0 LIMIT 1];

        Test.startTest();
        System.enqueueJob(new I2C_QueueableRefund(refundOrder.Id, false));
        Test.stopTest();

        // Should take refund path (past void window)
    }

    @IsTest
    static void testSkipVoidFlag() {
        Order refundOrder = [SELECT Id FROM Order WHERE TotalAmount < 0 LIMIT 1];

        Test.startTest();
        System.enqueueJob(new I2C_QueueableRefund(refundOrder.Id, true)); // Force refund
        Test.stopTest();

        // Should take refund path even if void-eligible
    }

    @IsTest
    static void testNoSubscriptions() {
        // Delete subscriptions
        delete [SELECT Id FROM Subscription__c];

        Order refundOrder = [SELECT Id FROM Order WHERE TotalAmount < 0 LIMIT 1];

        Test.startTest();
        System.enqueueJob(new I2C_QueueableRefund(refundOrder.Id, false));
        Test.stopTest();

        // Should complete without errors
    }

    @IsTest
    static void testMissingTransaction() {
        // Delete transaction
        delete [SELECT Id FROM ChargentOrders__Transaction__c];

        Order refundOrder = [SELECT Id FROM Order WHERE TotalAmount < 0 LIMIT 1];

        Test.startTest();
        try {
            System.enqueueJob(new I2C_QueueableRefund(refundOrder.Id, false));
            Test.stopTest();
        } catch (Exception e) {
            Assert.isTrue(e.getMessage().contains('List has no rows'), 'Should fail on missing transaction');
        }
    }

    @IsTest
    static void testMultipleOrderItems() {
        // Add second order item
        Order originalOrder = [SELECT Id FROM Order WHERE RelatedOrderId = null LIMIT 1];
        Order refundOrder = [SELECT Id FROM Order WHERE RelatedOrderId != null LIMIT 1];

        Product2 prod2 = new Product2(Name = 'Second Product', IsActive = true);
        insert prod2;

        PricebookEntry pbe2 = new PricebookEntry(
            Product2Id = prod2.Id,
            Pricebook2Id = Test.getStandardPricebookId(),
            UnitPrice = 50.00,
            IsActive = true
        );
        insert pbe2;

        OrderItem originalOI2 = new OrderItem(
            OrderId = originalOrder.Id,
            PricebookEntryId = pbe2.Id,
            Quantity = 1,
            UnitPrice = 50.00
        );
        insert originalOI2;

        OrderItem refundOI2 = new OrderItem(
            OrderId = refundOrder.Id,
            PricebookEntryId = pbe2.Id,
            Quantity = 1,
            UnitPrice = -50.00,
            RelatedOrderItemId = originalOI2.Id
        );
        insert refundOI2;

        Test.startTest();
        System.enqueueJob(new I2C_QueueableRefund(refundOrder.Id, false));
        Test.stopTest();

        // Should handle multiple order items
    }
}

Test Data Requirements

  • Account: Standard account
  • Order: Original and refund orders with RelatedOrderId link
  • OrderItem: Original and refund items with RelatedOrderItemId link
  • Product2/PricebookEntry: For order items
  • Subscription__c: With Auto_Renew__c = true
  • ChargentOrders__Transaction__c: Approved charge transaction

Mocking Considerations

  • AttemptVoid/AttemptRefund: May need to mock if they make actual callouts
  • I2C_AutoRenewService: Mock if service has complex dependencies
  • Chargent Managed Package: Test in sandbox with Chargent installed

Changes & History

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

Pre-Go-Live Concerns

CRITICAL

  • No Null Checks on Queries: Lines 14, 27, 31 will throw exceptions if records not found
  • Add null checks and error logging
  • Handle missing orders/transactions gracefully
  • Silent Failures: All exceptions logged to System.debug only
  • Add persistent error logging to Flow_Error_Log__c
  • Send notifications to operations team

HIGH

  • Decimal Equality Comparison: Line 35 uses == for currency amounts
  • Replace with tolerance-based comparison: Math.abs(difference) < 0.01
  • Hardcoded 24-Hour Window: Line 36 hardcodes 1,440 minutes
  • Extract to custom metadata for flexibility
  • No Transaction Validation: Doesn't verify transaction is refundable
  • Check transaction hasn't already been refunded/voided

MEDIUM

  • String orderId: Constructor parameter should be Id type
  • Confusing Parameter Name: dontAttemptVoid is double-negative
  • Rename to forceRefund or skipVoidAttempt
  • No Queueable Limit Check: Doesn't verify queue depth before enqueuing
  • Unused Variable: lastSaleTransactionId assigned but never used (line 31)

LOW

  • No Context Logging: Doesn't log who initiated refund or when
  • No Audit Trail: No record of void vs refund decision logic

Maintenance Notes

📋 Monitoring Recommendations

  • Queueable Status: Query AsyncApexJob daily for failures
  • Subscription Auto-Renewal: Verify subscriptions disabled after refunds
  • Void vs Refund Distribution: Track which path is most common
  • Gateway Transaction Sync: Ensure Salesforce transactions match gateway

🔧 Future Enhancement Opportunities

  1. Persistent Error Logging: Add Flow_Error_Log__c inserts
  2. Configurable Void Window: Move 24-hour limit to custom metadata
  3. Partial Void Support: Handle partial voids if gateway supports
  4. Audit Trail: Log refund decision logic and user context
  5. Return Receipt: Return status to calling code for monitoring

⚠️ Breaking Change Risks

  • Changing constructor signature requires updates to all callers
  • Modifying void eligibility logic changes financial processing behavior
  • Renaming AttemptVoid/AttemptRefund classes breaks queueable chain
  • I2C_AutoRenewService: Disables subscription auto-renewal
  • AttemptVoid: Chained queueable for void processing
  • AttemptRefund: Chained queueable for refund processing
  • ChargentOrders Package: Payment transaction management
  • Order/OrderItem Objects: Refund order data model
  • Subscription__c: Auto-renewal management

Business Owner

Primary Contact: Finance / Payment Operations Team Technical Owner: Salesforce Development Team Payment Gateway: Chargent / [Gateway Name] Last Reviewed: [Date]


Documentation Status: ✅ Complete Code Review Status: ⚠️ HIGH - Critical null checks missing, silent failures Test Coverage: Target 90%+ Callout Dependency: Requires Chargent managed package