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 executionDatabase.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¶
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¶
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¶
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()¶
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)¶
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 Order → Refund OrderItems → RelatedOrderItemId → Original 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/AttemptRefundcallouts - Chargent Payment Processor: Managed package integration
Design Patterns¶
- Queueable Chaining: Sequences asynchronous jobs for multi-step processing
- Chain of Responsibility: Delegates to specialized handlers (Void vs Refund)
- Strategy Pattern: Selects processing strategy based on business rules
- Dependency Injection: Passes Order and Transaction objects to chained jobs
- 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_QueueableRefund→AttemptVoid/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¶
-
Add Query Limits: Prevent runaway queries for complex orders
-
Monitor Queue Depth: Check for queue exhaustion
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 foundDmlException: 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
Recommended Improvements¶
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 amountTransaction.Amount: Payment amountTransaction.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¶
- Run as System: Use
without sharingfor financial processing classes - Audit Trail: Log refund initiation user and timestamp
- 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
RelatedOrderIdlink - OrderItem: Original and refund items with
RelatedOrderItemIdlink - 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
Idtype - Confusing Parameter Name:
dontAttemptVoidis double-negative - Rename to
forceRefundorskipVoidAttempt - No Queueable Limit Check: Doesn't verify queue depth before enqueuing
- Unused Variable:
lastSaleTransactionIdassigned 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
AsyncApexJobdaily 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¶
- Persistent Error Logging: Add Flow_Error_Log__c inserts
- Configurable Void Window: Move 24-hour limit to custom metadata
- Partial Void Support: Handle partial voids if gateway supports
- Audit Trail: Log refund decision logic and user context
- 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/AttemptRefundclasses breaks queueable chain
🔗 Related Components¶
- 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