Class Name: DailyMembershipScheduler¶
Last Updated: 2025-10-22 Source Code: https://github.com/AANP-IT/I2C.Salesforce.Metadata/blob/STAGING/force-app/main/default/classes/DailyMembershipScheduler.cls
API Name: DailyMembershipScheduler Type: Schedulable Test Coverage: Target 100%
Business Purpose¶
The DailyMembershipScheduler class is a schedulable Apex job that initiates the daily membership processing workflow. It serves as the entry point for the automated membership lifecycle management system, executing daily to:
1. Retrieve grace period configuration
2. Launch DailyMembershipStatusBatch to update membership statuses
3. Indirectly trigger DailyMembershipAccountsBatch (chained from status batch)
This scheduler ensures memberships transition through their lifecycle stages (Active → Within Grace → Expired) and Account fields stay synchronized with current membership data.
Class Overview¶
- Scope/Sharing:
with sharing- Respects record-level security - Access Modifier:
global- Accessible from managed packages and external systems - Implements:
Schedulable- Standard Salesforce scheduler interface - Schedule Frequency: Daily (typically 00:05 AM based on inline comment)
- Key Responsibilities:
- Retrieve grace period configuration from custom metadata
- Execute daily membership status batch job
- Handle scheduling errors gracefully
Why Global?¶
The global access modifier allows this scheduler to be invoked from:
- Anonymous Apex scripts
- Managed package installations
- Remote site callbacks
- External schedulers
This is standard practice for schedulable classes that may be deployed across orgs.
Public Methods¶
execute()¶
Purpose: Executes the scheduled job, launching the membership status batch with configured grace period.
Parameters:
- sc (SchedulableContext) - Standard scheduler context (unused in implementation)
Returns: void
Business Logic:
- Grace Period Retrieval (line 10):
- Queries
Environment_Settings__mdtcustom metadata - Returns configured value or defaults to 30 days
-
Stored in local variable for batch initialization
-
Batch Execution (line 11):
- Creates new instance of
DailyMembershipStatusBatch - Passes grace period to batch constructor
- Batch size: 200 memberships per chunk
-
Returns
AsyncApexJobID (discarded) -
Exception Handling (lines 12-14):
- Catches all exceptions during batch launch
- Logs error message to debug logs
- Prevents scheduler from failing (continues to next scheduled run)
Issues/Concerns:
- ⚠️ Silent Failure: Only logs to System.debug - no notifications sent on failure
- ⚠️ No Job ID Capture: Doesn't store AsyncApexJob ID for monitoring
- ⚠️ Generic Exception Catch: Catches all exceptions including system errors
- ⚠️ No Retry Logic: Failed batch launch not retried until next scheduled run
- ⚠️ Unused SchedulableContext: Parameter sc never used (could inspect job details)
- ✅ Resilient: Exception handling prevents scheduler from aborting
- ✅ Configurable Batch Size: Hardcoded 200 but easy to modify
Scheduling Example:
// Schedule daily at 12:05 AM
String jobName = 'Daily Membership Update';
String cronExp = '0 5 0 * * ?'; // Minutes Hours DayOfMonth Month DayOfWeek Year
Id jobId = System.schedule(jobName, cronExp, new DailyMembershipScheduler());
// Verify scheduled
CronTrigger ct = [
SELECT Id, CronExpression, TimesTriggered, NextFireTime
FROM CronTrigger
WHERE Id = :jobId
];
System.debug('Next run: ' + ct.NextFireTime);
Cron Expression Breakdown:
'0 5 0 * * ?'
│ │ │ │ │ │
│ │ │ │ │ └── Year (optional, omitted)
│ │ │ │ └──── Day of Week (? = no specific value)
│ │ │ └────── Month (* = every month)
│ │ └──────── Day of Month (* = every day)
│ └────────── Hour (0 = midnight)
└──────────── Minute (5 = 5 minutes past hour)
Result: Runs at 00:05 AM daily
Dependencies¶
Salesforce Objects¶
- None directly (batch jobs query Membership__c and Account)
Custom Settings/Metadata¶
- Environment_Settings__mdt: Grace period configuration (via
DailyMembershipUtil.getGraceDays())
Other Classes¶
- DailyMembershipUtil:
getGraceDays()- Retrieves grace period from custom metadata- DailyMembershipStatusBatch:
- Membership status transition batch (Active → Grace → Expired)
- DailyMembershipAccountsBatch:
- Chained from status batch to sync Account fields (indirect dependency)
External Services¶
- None
Design Patterns¶
- Scheduler Pattern: Implements
Schedulablefor time-based execution - Batch Chain Initiator: Starts multi-step batch workflow
- Configuration Externalization: Grace period retrieved from metadata (not hardcoded)
- Fail-Safe Exception Handling: Prevents single failure from breaking schedule
- Dependency Injection: Passes configuration (graceDays) to batch constructor
Governor Limits Considerations¶
Current Impact (Per Execution)¶
- SOQL Queries: 1 query (via
getGraceDays()) - SOQL Rows: 1 row maximum (custom metadata)
- DML Statements: 0 (batch launch is asynchronous)
- Async Apex Jobs: 1 (status batch launch)
- Heap Size: Minimal (two integers + one batch instance)
Scalability Analysis¶
- ✅ Minimal Resource Usage: Scheduler itself uses almost no limits
- ✅ Asynchronous Execution: Batch runs separately from scheduler context
- ⚠️ No Duplicate Check: Could launch duplicate batch if scheduled multiple times
- ✅ Independent Job Limits: Batch has its own governor limit context
Batch Chain Impact¶
When scheduler executes, it triggers a chain of batch jobs: 1. DailyMembershipStatusBatch (launched by scheduler) - Processes memberships needing status transitions - Updates Membership__c and Subscription__c records 2. DailyMembershipAccountsBatch (chained from status batch) - Syncs Account membership fields - Updates Account records
Total Async Jobs: 2 per scheduler execution
Scheduling Recommendations¶
// Production: Run daily at low-traffic time
String cronExp = '0 5 0 * * ?'; // 12:05 AM daily
// Development: Run every 6 hours for testing
String cronExp = '0 0 0/6 * * ?'; // 12:00 AM, 6:00 AM, 12:00 PM, 6:00 PM
// Avoid: Midnight exactly (high system load)
String cronExp = '0 0 0 * * ?'; // BAD - many orgs run jobs at midnight
Error Handling¶
Exception Types Thrown¶
- None - All exceptions caught internally
Exception Types Caught¶
- Exception (line 12): Generic catch-all for any errors during:
- Grace period retrieval
- Batch instantiation
Database.executeBatch()call
Error Handling Strategy¶
- Try-Catch Wrapper: Wraps entire execution in try-catch
- Debug Logging: Logs error message to System.debug
- Graceful Degradation: Allows scheduler to continue (doesn't re-throw)
- Next-Run Recovery: Failed execution retried at next scheduled time
Error Handling Gaps¶
- No Persistent Error Logging: Only logs to debug (lost after 24 hours)
- No Notifications: No email/platform event alerts on failure
- No Retry Logic: Waits 24 hours until next scheduled run
- Silent Failure: Operations team unaware of failures without monitoring
- No Job Status Check: Doesn't verify batch actually started
Monitoring Recommendations¶
// Query recent scheduler executions
SELECT Id, Status, StartTime, EndTime, CronJobDetail.Name
FROM CronJobDetail
WHERE Name LIKE 'Daily Membership%'
ORDER BY StartTime DESC
LIMIT 10
// Query launched batch jobs
SELECT Id, Status, CompletedDate, NumberOfErrors, TotalJobItems,
ApexClass.Name, ExtendedStatus
FROM AsyncApexJob
WHERE ApexClass.Name = 'DailyMembershipStatusBatch'
AND CreatedDate = TODAY
ORDER BY CreatedDate DESC
// Check for errors in last 7 days
SELECT COUNT(Id) ErrorCount
FROM AsyncApexJob
WHERE ApexClass.Name IN ('DailyMembershipStatusBatch', 'DailyMembershipAccountsBatch')
AND CreatedDate = LAST_N_DAYS:7
AND Status IN ('Failed', 'Aborted')
Recommended Improvements¶
global void execute(SchedulableContext sc) {
try {
Integer graceDays = DailyMembershipUtil.getGraceDays();
Id jobId = Database.executeBatch(new DailyMembershipStatusBatch(graceDays), 200);
// Log successful launch
System.debug('DailyMembershipScheduler launched batch: ' + jobId);
} catch (Exception e) {
// Log error with context
System.debug(LoggingLevel.ERROR, 'DailyMembershipScheduler failed: ' + e.getMessage());
System.debug(LoggingLevel.ERROR, 'Stack trace: ' + e.getStackTraceString());
// Create error log record
Flow_Error_Log__c errorLog = new Flow_Error_Log__c(
FlowName__c = 'DailyMembershipScheduler',
ErrorMessage__c = e.getMessage(),
FlowRunDateTime__c = System.now(),
ObjectName__c = 'Scheduled Job'
);
try {
insert errorLog;
} catch (Exception insertEx) {
System.debug(LoggingLevel.FATAL, 'Cannot insert error log: ' + insertEx.getMessage());
}
// Optional: Send platform event for real-time monitoring
// EventBus.publish(new Batch_Failure__e(
// Job_Name__c = 'DailyMembershipScheduler',
// Error_Message__c = e.getMessage()
// ));
}
}
Security Considerations¶
Sharing Model¶
- WITH SHARING: Respects record-level security
- Implication: Scheduler runs in context of user who scheduled it
- Recommendation: Schedule as System Administrator or Integration User with "View All" permissions
User Context¶
When a scheduled job executes: - Automated Process: Shows as "Automated Process" in CreatedBy fields - Permission Sets: Uses permissions of scheduling user at schedule time - Sharing Rules: Respects org-wide defaults and sharing rules - FLS: Subject to field-level security of scheduling user
Best Practices¶
// Schedule as system user with full permissions
System.runAs(new User(Id = UserInfo.getUserId())) {
System.schedule(
'Daily Membership Update',
'0 5 0 * * ?',
new DailyMembershipScheduler()
);
}
Data Access Patterns¶
- Read Access: Custom metadata (Environment_Settings__mdt)
- Async Job Launch: Creates AsyncApexJob record
- No Direct DML: All data changes in chained batches
Test Class Requirements¶
Required Test Coverage¶
@IsTest
public class DailyMembershipSchedulerTest {
@TestSetup
static void setup() {
// Create test account and memberships for batch processing
Account acc = new Account(
FirstName = 'Test',
LastName = 'Scheduler',
RecordTypeId = [SELECT Id FROM RecordType WHERE DeveloperName = 'PersonAccount' LIMIT 1].Id
);
insert acc;
// Active membership ending yesterday (will trigger batch processing)
Membership__c mem = new Membership__c(
Account_Name__c = acc.Id,
Status__c = 'Active',
Start_Date__c = Date.today().addDays(-365),
End_Date__c = Date.today().addDays(-1),
Member_Type__c = 'Individual Member',
Product_Family__c = 'Membership'
);
insert mem;
}
@IsTest
static void testSchedulerExecution() {
Test.startTest();
// Schedule the job
String cronExp = '0 5 0 * * ?';
String jobId = System.schedule(
'Test Daily Membership Scheduler',
cronExp,
new DailyMembershipScheduler()
);
Test.stopTest();
// Verify job was scheduled
CronTrigger ct = [
SELECT Id, CronExpression, TimesTriggered, NextFireTime, State
FROM CronTrigger
WHERE Id = :jobId
];
Assert.isNotNull(ct, 'CronTrigger should be created');
Assert.areEqual('0 5 0 * * ?', ct.CronExpression, 'Cron expression should match');
Assert.areEqual('WAITING', ct.State, 'Job should be waiting for next execution');
}
@IsTest
static void testSchedulerLaunchesBatch() {
Test.startTest();
// Manually execute scheduler (simulates scheduled run)
DailyMembershipScheduler scheduler = new DailyMembershipScheduler();
scheduler.execute(null);
Test.stopTest();
// Verify batch was launched
List<AsyncApexJob> batchJobs = [
SELECT Id, ApexClass.Name, Status, JobType
FROM AsyncApexJob
WHERE ApexClass.Name = 'DailyMembershipStatusBatch'
AND JobType = 'BatchApex'
];
Assert.isFalse(batchJobs.isEmpty(), 'Should launch DailyMembershipStatusBatch');
Assert.areEqual(1, batchJobs.size(), 'Should launch exactly one batch job');
}
@IsTest
static void testSchedulerUsesConfiguredGraceDays() {
// Test that grace days are retrieved and passed to batch
// Note: Actual grace days value depends on Environment_Settings__mdt
Test.startTest();
DailyMembershipScheduler scheduler = new DailyMembershipScheduler();
scheduler.execute(null);
Test.stopTest();
// Batch should execute successfully regardless of grace period value
List<AsyncApexJob> jobs = [
SELECT Id, Status
FROM AsyncApexJob
WHERE ApexClass.Name = 'DailyMembershipStatusBatch'
];
Assert.areEqual(1, jobs.size(), 'Should launch one batch');
}
@IsTest
static void testSchedulerHandlesException() {
// Test exception handling by simulating error condition
// Note: Difficult to force exception without modifying DailyMembershipUtil
Test.startTest();
// Execute scheduler - should not throw exception even if batch fails
try {
DailyMembershipScheduler scheduler = new DailyMembershipScheduler();
scheduler.execute(null);
} catch (Exception e) {
Assert.fail('Scheduler should not throw exceptions: ' + e.getMessage());
}
Test.stopTest();
// If exception occurred, it should be caught and logged
// (No assertion possible without custom error logging)
}
@IsTest
static void testMultipleScheduledJobs() {
Test.startTest();
// Schedule multiple instances with different names
String jobId1 = System.schedule(
'Daily Membership Scheduler 1',
'0 5 0 * * ?',
new DailyMembershipScheduler()
);
String jobId2 = System.schedule(
'Daily Membership Scheduler 2',
'0 10 0 * * ?', // Different time
new DailyMembershipScheduler()
);
Test.stopTest();
// Verify both jobs scheduled
List<CronTrigger> jobs = [
SELECT Id, CronJobDetail.Name
FROM CronTrigger
WHERE Id IN (:jobId1, :jobId2)
];
Assert.areEqual(2, jobs.size(), 'Should create two scheduled jobs');
}
@IsTest
static void testAbortScheduledJob() {
Test.startTest();
// Schedule job
String jobId = System.schedule(
'Test Abort Job',
'0 5 0 * * ?',
new DailyMembershipScheduler()
);
// Verify scheduled
CronTrigger ct1 = [
SELECT State
FROM CronTrigger
WHERE Id = :jobId
];
Assert.areEqual('WAITING', ct1.State, 'Should be waiting');
// Abort job
System.abortJob(jobId);
Test.stopTest();
// Verify deleted
List<CronTrigger> ct2 = [
SELECT Id
FROM CronTrigger
WHERE Id = :jobId
];
Assert.isTrue(ct2.isEmpty(), 'Job should be aborted/deleted');
}
@IsTest
static void testSchedulerWithNullContext() {
// Test that null SchedulableContext doesn't cause issues
Test.startTest();
try {
DailyMembershipScheduler scheduler = new DailyMembershipScheduler();
scheduler.execute(null); // Explicitly pass null
} catch (NullPointerException e) {
Assert.fail('Should handle null SchedulableContext: ' + e.getMessage());
}
Test.stopTest();
}
}
Test Data Requirements¶
- Account: Person Account for batch processing
- Membership__c: Test membership records to verify batch execution
- RecordType: Person Account record type
Code Coverage Notes¶
- Target: 100% (only 16 lines, very simple class)
- Key Scenarios: Scheduling, execution, exception handling
- Actual Batch Processing: Not testable within scheduler test (use batch test classes)
Changes & History¶
| Date | Author | Description |
|---|---|---|
| Unknown | Original Developer | Initial implementation with inline documentation |
| (Current) | - | Full documentation added |
Pre-Go-Live Concerns¶
CRITICAL¶
- None - Class is straightforward with minimal risk
HIGH¶
- No Error Notifications: Scheduler failures only logged to debug
- Add persistent error logging to
Flow_Error_Log__c - Consider platform event for real-time monitoring
- Send email alerts to operations team on failure
MEDIUM¶
- No Job ID Capture: Batch job ID not stored for monitoring
- Store job ID in custom object for tracking
- Query job status in subsequent runs to detect failures
- Hardcoded Batch Size: 200 hardcoded on line 11
- Extract to custom metadata for tuning flexibility
LOW¶
- Unused SchedulableContext: Parameter not utilized
- Could use
sc.getTriggerId()for logging - Generic Exception Catch: Could differentiate between error types
- Separate handling for metadata errors vs batch launch errors
Maintenance Notes¶
📋 Monitoring Recommendations¶
- Daily Verification: Query
AsyncApexJobtable each morning to verify execution - Alert on Failures: Set up Process Builder/Flow to monitor batch job failures
- Scheduling Conflicts: Ensure no overlapping batch jobs scheduled at same time
- Grace Period Config: Periodically review
Environment_Settings__mdtvalue
🔧 Scheduling Management¶
// List all scheduled jobs
SELECT Id, CronJobDetail.Name, State, NextFireTime, PreviousFireTime
FROM CronTrigger
WHERE CronJobDetail.Name LIKE '%Membership%'
// Abort existing scheduled job
System.abortJob(jobId);
// Reschedule with new cron expression
String newCronExp = '0 10 0 * * ?'; // Change to 12:10 AM
System.schedule(
'Daily Membership Update - v2',
newCronExp,
new DailyMembershipScheduler()
);
⚠️ Breaking Change Risks¶
- Changing
DailyMembershipUtil.getGraceDays()signature will break compilation - Modifying
DailyMembershipStatusBatchconstructor will require scheduler update - Changing batch size impacts processing time and resource usage
🔗 Related Components¶
- DailyMembershipStatusBatch: Batch launched by this scheduler
- DailyMembershipAccountsBatch: Chained from status batch (indirect dependency)
- DailyMembershipUtil: Provides grace period configuration
- Environment_Settings__mdt: Stores grace period value
- Flow_Error_Log__c: Potential error logging target (not currently used)
Business Owner¶
Primary Contact: Membership Operations Team Technical Owner: Salesforce Development Team Scheduled By: System Administrator Last Reviewed: [Date]
Documentation Status: ✅ Complete Code Review Status: ✅ Approved (simple, well-documented class) Test Coverage: Target 100% Deployment Notes: Schedule after deployment using provided cron expression