diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/SubscriptionData_MockHTTP.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/SubscriptionData_MockHTTP.cls
index baeae2c2b..1816c6202 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/SubscriptionData_MockHTTP.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/SubscriptionData_MockHTTP.cls
@@ -1,15 +1,32 @@
+/**
+ * Mock HTTP Callout class for testing mc_SubscriptionData
+ *
+ * Implements HttpCalloutMock to provide mock responses for HTTP callouts
+ * during test execution. This allows testing of the subscription data
+ * retrieval without making actual API calls.
+ *
+ * @author Andy Haas
+ */
@isTest
-global with sharing class SubscriptionData_MockHTTP implements HttpCalloutMock {
- // Implement this interface method
+global with sharing class SubscriptionData_MockHTTP implements HttpCalloutMock {
+ // API version constant for REST API calls (matches main class)
+ private static final String API_VERSION = 'v65.0';
+
+ /**
+ * Implement this interface method to provide mock HTTP responses
+ *
+ * @param req HTTPRequest object from the callout
+ * @return HTTPResponse Mock response with subscription data
+ */
global HTTPResponse respond(HTTPRequest req) {
- // Optionally, only send a mock response for a specific endpoint
- // and method.
+ // Optionally, only send a mock response for a specific endpoint and method.
+ String expectedEndpoint = 'https://salesforce.com/services/data/' + API_VERSION + '/analytics/notifications?source=lightningReportSubscribe';
req.setEndpoint('https://salesforce.com/' +
- 'services/data/v55.0/analytics/' +
+ 'services/data/' + API_VERSION + '/analytics/' +
'notifications?source=lightningReportSubscribe');
req.setMethod('GET');
- System.assertEquals('https://salesforce.com/services/data/v55.0/analytics/notifications?source=lightningReportSubscribe', req.getEndpoint());
- System.assertEquals('GET', req.getMethod());
+ System.assertEquals(expectedEndpoint, req.getEndpoint(), 'Endpoint should match expected value');
+ System.assertEquals('GET', req.getMethod(), 'HTTP method should be GET');
// Create a fake response
HttpResponse res = new HttpResponse();
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/SubscriptionData_MockHTTP.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/SubscriptionData_MockHTTP.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/SubscriptionData_MockHTTP.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/SubscriptionData_MockHTTP.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipients.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipients.cls
index af75b4c5a..7a93b22f8 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipients.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipients.cls
@@ -1,152 +1,276 @@
+/**
+ * Flow Action: Delete Selected Recipients
+ *
+ * Deletes selected recipients from an analytics notification subscription.
+ * If all recipients are deleted, the entire notification is removed.
+ * Otherwise, the notification is updated with the remaining recipients.
+ *
+ * @author Andy Haas
+ * @see https://unofficialsf.com/from-andy-haas-use-analytics-management-actions-to-show-report-subscribers-and-assignments/
+ */
public with sharing class mc_DeleteRecipients {
- @InvocableMethod(label='Delete Selected Recipients' description='Get Subscription Data' category='Analytic Subscription')
-public static List mc_DeleteRecipients(List requests) {
+ // API version constant for REST API calls
+ private static final String API_VERSION = 'v65.0';
+
+ /**
+ * Invocable method to delete selected recipients from a notification subscription
+ *
+ * @param requests List of Request objects containing notificationId, recipients, definition, and deletedRecipients
+ * @return List List of Results containing HTTP status code
+ */
+ @InvocableMethod(label='Delete Selected Recipients' description='Delete selected recipients from a notification subscription' category='Analytic Subscription')
+ public static List mc_DeleteRecipients(List requests) {
// Set Response Wrapper
List responseWrapper = new List();
- Results response = new Results();
// Instantiate a new http object
Http h = new Http();
// Get Domain URL
- string domainURL = URL.getSalesforceBaseUrl().toExternalForm();
+ string domainURL = System.Url.getOrgDomainUrl().toExternalForm();
System.debug('domainURL: ' + domainURL);
-
// Set Session ID
string sessionId = 'some_text\nwritten_here';
- if(!test.isRunningTest()){
- sessionId = Page.usf3__GenerateSessionIdForLWC.getContent().toString();
- }
- // Fix Session Id
- sessionId = sessionId.substring(sessionId.indexOf('\n')+1);
-
+ if(!test.isRunningTest()){
+ sessionId = Page.usf3__GenerateSessionIdForLWC.getContent().toString();
+ }
+ // Fix Session Id - extract the actual session ID after the newline
+ if (sessionId.indexOf('\n') >= 0) {
+ sessionId = sessionId.substring(sessionId.indexOf('\n')+1);
+ }
for (Request request : requests) {
- // Set Base URL
- string baseURL = '/services/data/v55.0/analytics/notifications/' + request.notificationId;
- // Set URL from domain and base url
- string url = domainURL + baseURL;
-
- // Set Lists
- List recipients = request.recipients;
- List recipientsDifferent = new List();
- List recipientsSame = new List();
-
- // Deserialize the request with SubscriptionListDefinition
- List recipientsToDelete =
- new List();
- if(!test.isRunningTest()){
- recipientsToDelete = (List)
- JSON.deserialize(request.deletedRecipients,
- List.class);
- }
-
- // Deseralize the request with SubscriptionListDefinition
- mc_SubscriptionListDefinition definitionData = request.definition;
-
- // If recipients size equals recipientsToDelete size if they are the same then delete the entire notification else create a new recipient list
- if (recipients.size() == recipientsToDelete.size()) {
- // Delete the entire notification
- System.debug('Delete the entire notification' + request.notificationId);
+ try {
+ Results response = new Results();
+
+ // Validate required inputs
+ if (String.isBlank(request.notificationId)) {
+ System.debug('Error: notificationId is required');
+ response.status_code = 400;
+ responseWrapper.add(response);
+ continue;
+ }
+
+ if (request.recipients == null) {
+ System.debug('Error: recipients list is required');
+ response.status_code = 400;
+ responseWrapper.add(response);
+ continue;
+ }
+
+ // Set Base URL
+ string baseURL = '/services/data/' + API_VERSION + '/analytics/notifications/' + request.notificationId;
+ // Set URL from domain and base url
+ string url = domainURL + baseURL;
+
+ // Get current recipients list
+ List recipients = request.recipients;
- // Instantiate a new HTTP request, specify the method (DELETE) as well as the endpoint
- HttpRequest req = new HttpRequest();
- req.setEndpoint(url);
- req.setMethod('DELETE');
- req.setHeader('Authorization', 'OAuth ' + sessionId);
+ // Deserialize the recipients to delete
+ List recipientsToDelete =
+ new List();
+ if(!test.isRunningTest() && !String.isBlank(request.deletedRecipients)){
+ try {
+ recipientsToDelete = (List)
+ JSON.deserialize(request.deletedRecipients,
+ List.class);
+ } catch (JSONException e) {
+ System.debug('Error deserializing deletedRecipients: ' + e.getMessage());
+ response.status_code = 400;
+ responseWrapper.add(response);
+ continue;
+ }
+ }
+
+ // Validate recipientsToDelete is not null
+ if (recipientsToDelete == null) {
+ recipientsToDelete = new List();
+ }
+
+ // If all recipients are being deleted, delete the entire notification
+ if (recipients.size() == recipientsToDelete.size() && recipients.size() > 0) {
+ // Delete the entire notification
+ System.debug('Delete the entire notification: ' + request.notificationId);
- // Send the request, and return a response
- HttpResponse res = h.send(req);
+ // Instantiate a new HTTP request, specify the method (DELETE) as well as the endpoint
+ HttpRequest req = new HttpRequest();
+ req.setEndpoint(url);
+ req.setMethod('DELETE');
+ req.setHeader('Authorization', 'OAuth ' + sessionId);
- response.status_code = res.getStatusCode();
- responseWrapper.add(response);
+ // Send the request, and return a response
+ HttpResponse res = h.send(req);
+ // Validate response
+ response.status_code = res.getStatusCode();
+ if (res.getStatusCode() != 200 && res.getStatusCode() != 204) {
+ System.debug('Error deleting notification: HTTP ' + res.getStatusCode() + ' - ' + res.getStatus());
+ System.debug('Response Body: ' + res.getBody());
+ }
+ responseWrapper.add(response);
- } else {
- // Create a new recipient list with the recipients that are not in the recipientsToDelete list
- System.debug('recipients' + recipients);
- System.debug('recipientsToDelete' + recipientsToDelete);
-
- // Remove recipents from multidimensional list based on id and add to recipientsDifferent
- Set myset = new Set();
- List result = new List();
- myset.addAll(recipientsToDelete);
- System.debug('myset' + myset);
- result.addAll(recipients);
- System.debug('result' + result);
- myset.removeAll(result);
- System.debug('removeAll' + myset);
- recipientsDifferent.addAll(myset);
-
- // Update the recipients list with the new list
- System.debug('recipientsDifferent: ' + recipientsDifferent);
-
- // Instantiate a new HTTP request, specify the method (GET)
- HttpRequest req = new HttpRequest();
- req.setEndpoint(url);
- req.setMethod('GET');
- req.setHeader('Authorization', 'OAuth ' + sessionId);
- req.setHeader('Content-Type', 'application/json');
-
- // Send the request, and return a response
- HttpResponse res = h.send(req);
-
- // replace time: with timeOfDay: in the response body
- String responseBody = res.getBody().replace('"time":', '"timeOfDay":');
-
- System.debug('responseBody: ' + responseBody);
-
- // Deserialize the response body
- mc_SubscriptionListDefinition definition = (mc_SubscriptionListDefinition)JSON.deserialize(responseBody, mc_SubscriptionListDefinition.class);
-
- system.debug('definition: before; ' + definition);
-
- // Update definitionData with the new recipients list
- definition.thresholds[0].actions[0].configuration.recipients = recipientsDifferent;
-
- System.debug('definition: after; ' + definition);
-
- // Seralize Body
- String body = JSON.serialize(definition, true);
-
- // put timeOfDay back to time otherwise it will fail
- body = body.replace('"timeOfDay":', '"time":');
- System.debug('body: ' + body);
-
-
- // Instantiate a new HTTP request, specify the method (PUT) as well as the endpoint
- HttpRequest req2 = new HttpRequest();
- req2.setEndpoint(url);
- req2.setMethod('PUT');
- req2.setHeader('Authorization', 'OAuth ' + sessionId);
- req2.setHeader('Content-Type', 'application/json');
- req2.setBody(body);
-
- // Send the request, and return a response
- HttpResponse res2 = h.send(req2);
- System.debug('Response: ' + res2.getBody());
- response.status_code = res2.getStatusCode();
- responseWrapper.add(response);
+ } else {
+ // Create a new recipient list with the recipients that are NOT in the recipientsToDelete list
+ System.debug('Current recipients count: ' + recipients.size());
+ System.debug('Recipients to delete count: ' + recipientsToDelete.size());
+
+ // Create a set of recipient IDs to delete for efficient lookup
+ Set recipientIdsToDelete = new Set();
+ for (mc_SubscriptionListDefinition_Recipients recipientToDelete : recipientsToDelete) {
+ if (recipientToDelete != null && recipientToDelete.id != null) {
+ recipientIdsToDelete.add(recipientToDelete.id);
+ }
+ }
+
+ // Filter recipients: keep only those NOT in the delete list
+ List recipientsToKeep = new List();
+ for (mc_SubscriptionListDefinition_Recipients recipient : recipients) {
+ if (recipient != null && recipient.id != null && !recipientIdsToDelete.contains(recipient.id)) {
+ recipientsToKeep.add(recipient);
+ }
+ }
+
+ System.debug('Recipients to keep count: ' + recipientsToKeep.size());
+
+ // Validate we have recipients to keep
+ if (recipientsToKeep.isEmpty()) {
+ System.debug('Warning: No recipients remaining, but size check failed. Deleting notification instead.');
+ // Delete the entire notification if no recipients remain
+ HttpRequest req = new HttpRequest();
+ req.setEndpoint(url);
+ req.setMethod('DELETE');
+ req.setHeader('Authorization', 'OAuth ' + sessionId);
+ HttpResponse res = h.send(req);
+ response.status_code = res.getStatusCode();
+ responseWrapper.add(response);
+ continue;
+ }
+
+ // Get current notification definition to update
+ HttpRequest req = new HttpRequest();
+ req.setEndpoint(url);
+ req.setMethod('GET');
+ req.setHeader('Authorization', 'OAuth ' + sessionId);
+ req.setHeader('Content-Type', 'application/json');
+
+ // Send the request, and return a response
+ HttpResponse res = h.send(req);
+
+ // Validate GET response
+ if (res.getStatusCode() != 200) {
+ System.debug('Error retrieving notification: HTTP ' + res.getStatusCode() + ' - ' + res.getStatus());
+ System.debug('Response Body: ' + res.getBody());
+ response.status_code = res.getStatusCode();
+ responseWrapper.add(response);
+ continue;
+ }
+
+ // Replace time: with timeOfDay: in the response body (for Apex class compatibility)
+ String responseBody = res.getBody();
+ if (String.isBlank(responseBody)) {
+ System.debug('Error: Empty response body received');
+ response.status_code = 500;
+ responseWrapper.add(response);
+ continue;
+ }
+
+ responseBody = responseBody.replace('"time":', '"timeOfDay":');
+
+ System.debug('responseBody length: ' + responseBody.length());
+
+ // Deserialize the response body
+ mc_SubscriptionListDefinition definition;
+ try {
+ definition = (mc_SubscriptionListDefinition)JSON.deserialize(responseBody, mc_SubscriptionListDefinition.class);
+ } catch (JSONException e) {
+ System.debug('Error deserializing notification definition: ' + e.getMessage());
+ response.status_code = 500;
+ responseWrapper.add(response);
+ continue;
+ }
+
+ System.debug('definition retrieved successfully');
+
+ // Validate definition structure before updating
+ if (definition == null ||
+ definition.thresholds == null ||
+ definition.thresholds.isEmpty() ||
+ definition.thresholds[0].actions == null ||
+ definition.thresholds[0].actions.isEmpty() ||
+ definition.thresholds[0].actions[0].configuration == null) {
+ System.debug('Error: Invalid notification definition structure');
+ response.status_code = 500;
+ responseWrapper.add(response);
+ continue;
+ }
+
+ // Update definition with the new recipients list
+ definition.thresholds[0].actions[0].configuration.recipients = recipientsToKeep;
+
+ System.debug('definition updated with ' + recipientsToKeep.size() + ' recipients');
+
+ // Serialize Body
+ String body = JSON.serialize(definition, true);
+
+ // Put timeOfDay back to time otherwise API will fail
+ body = body.replace('"timeOfDay":', '"time":');
+ System.debug('body length: ' + body.length());
+
+ // Instantiate a new HTTP request, specify the method (PUT) as well as the endpoint
+ HttpRequest req2 = new HttpRequest();
+ req2.setEndpoint(url);
+ req2.setMethod('PUT');
+ req2.setHeader('Authorization', 'OAuth ' + sessionId);
+ req2.setHeader('Content-Type', 'application/json');
+ req2.setBody(body);
+
+ // Send the request, and return a response
+ HttpResponse res2 = h.send(req2);
+
+ // Validate PUT response
+ response.status_code = res2.getStatusCode();
+ if (res2.getStatusCode() != 200) {
+ System.debug('Error updating notification: HTTP ' + res2.getStatusCode() + ' - ' + res2.getStatus());
+ System.debug('Response Body: ' + res2.getBody());
+ } else {
+ System.debug('Notification updated successfully');
+ }
+ responseWrapper.add(response);
+ }
+ } catch (Exception e) {
+ System.debug('Error processing request: ' + e.getMessage());
+ System.debug('Stack trace: ' + e.getStackTraceString());
+ Results errorResponse = new Results();
+ errorResponse.status_code = 500;
+ responseWrapper.add(errorResponse);
}
}
return responseWrapper;
}
+ /**
+ * Request wrapper class for invocable method input
+ */
public class Request {
@InvocableVariable(required=true)
public string notificationId;
+
@InvocableVariable(required=true)
public List recipients;
+
@InvocableVariable(required=true)
public mc_SubscriptionListDefinition definition;
+
@InvocableVariable(required=true)
public string deletedRecipients;
}
+ /**
+ * Results wrapper class for invocable method output
+ */
public class Results {
@InvocableVariable
public Integer status_code;
}
-
}
\ No newline at end of file
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipients.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipients.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipients.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipients.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipientsTest.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipientsTest.cls
index 24cf7a04b..a9272f268 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipientsTest.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipientsTest.cls
@@ -1,62 +1,557 @@
+/**
+ * Test class for mc_DeleteRecipients
+ *
+ * Provides test coverage for the Delete Selected Recipients invocable action.
+ * Tests both scenarios: deleting all recipients (which deletes the notification)
+ * and deleting some recipients (which updates the notification).
+ *
+ * @author Andy Haas
+ */
@isTest
public class mc_DeleteRecipientsTest {
+ /**
+ * Test method for mc_DeleteRecipients
+ *
+ * Tests the recipient deletion functionality with mock HTTP callouts.
+ * Verifies that the method processes requests correctly for both
+ * full deletion and partial deletion scenarios.
+ */
@isTest
private static void mc_DeleteRecipients_test(){
- list rqLst = new list();
- list rqLst1 = new list();
- for(integer i = 0; i < 5; i++){
- mc_DeleteRecipients.request rq = new mc_DeleteRecipients.request();
- mc_DeleteRecipients.request rq1 = new mc_DeleteRecipients.request();
- List rec =
- new List{
- new mc_SubscriptionListDefinition_Recipients(),
- new mc_SubscriptionListDefinition_Recipients()
- };
- List rec1 =
- new List();
- rq1.recipients = rec;
- rq.recipients = rec1;
- rqLst.add(rq1);
- rqlst.add(rq);
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ // Test 1: Full deletion scenario (all recipients deleted)
+ List fullDeleteRequests = new List();
+ mc_DeleteRecipients.Request fullDeleteReq = new mc_DeleteRecipients.Request();
+ fullDeleteReq.notificationId = '0Au3C00000000kdSAA';
+
+ // Create recipients list
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip1 = new mc_SubscriptionListDefinition_Recipients();
+ recip1.id = '0055e000001mKpC';
+ recip1.displayName = 'Test User 1';
+ recip1.type = 'user';
+ recipients.add(recip1);
+
+ mc_SubscriptionListDefinition_Recipients recip2 = new mc_SubscriptionListDefinition_Recipients();
+ recip2.id = '0055e000001mKpD';
+ recip2.displayName = 'Test User 2';
+ recip2.type = 'user';
+ recipients.add(recip2);
+
+ fullDeleteReq.recipients = recipients;
+ fullDeleteReq.definition = createTestDefinition();
+ fullDeleteReq.deletedRecipients = JSON.serialize(recipients); // Delete all
+ fullDeleteRequests.add(fullDeleteReq);
+
+ Test.startTest();
+ List fullDeleteResults = mc_DeleteRecipients.mc_DeleteRecipients(fullDeleteRequests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, fullDeleteResults, 'Full delete results should not be null');
+ System.assertEquals(1, fullDeleteResults.size(), 'Should return one result');
+ }
+
+ /**
+ * Test partial deletion scenario (some recipients deleted)
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testPartialDeletion(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ // Create recipients list with 3 recipients
+ List allRecipients = new List();
+ for (Integer i = 0; i < 3; i++) {
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKp' + String.valueOf(i);
+ recip.displayName = 'Test User ' + i;
+ recip.type = 'user';
+ allRecipients.add(recip);
}
- test.startTest();
- test.setMock(httpCalloutMock.class, new mock());
- mc_DeleteRecipients.mc_DeleteRecipients(rqLst);
- mc_DeleteRecipients.mc_DeleteRecipients(rqlst1);
- test.stopTest();
- }
- public class mock implements httpCalloutMock {
- public system.HttpResponse respond(system.HttpRequest req){
- httpResponse resp = new system.HttpResponse();
+
+ req.recipients = allRecipients;
+ req.definition = createTestDefinition();
+
+ // Delete only first recipient
+ List recipientsToDelete = new List();
+ recipientsToDelete.add(allRecipients[0]);
+ req.deletedRecipients = JSON.serialize(recipientsToDelete);
+
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test validation errors - blank notificationId
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testValidationErrors(){
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = ''; // Blank notificationId
+ req.recipients = new List();
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]';
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ System.assertEquals(400, results[0].status_code, 'Should return 400 for validation error');
+ }
+
+ /**
+ * Test validation errors - null recipients
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testNullRecipients(){
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+ req.recipients = null; // Null recipients
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]';
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ System.assertEquals(400, results[0].status_code, 'Should return 400 for validation error');
+ }
+
+ /**
+ * Test error handling - HTTP error response
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testHttpError(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseError());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recipients.add(recip);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]'; // Partial deletion to trigger GET request
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ }
+
+ /**
+ * Test error handling - invalid JSON in deletedRecipients
+ * Note: In test context, JSON deserialization is skipped, so this tests the null handling path
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testInvalidJSON(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recipients.add(recip);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '{invalid json}'; // Invalid JSON - in test context this is handled as null
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test scenario where all recipients are deleted (empty recipientsToKeep)
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testEmptyRecipientsToKeep(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ // Create recipients list
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recipients.add(recip);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ // Delete all recipients by providing the same list
+ req.deletedRecipients = JSON.serialize(recipients);
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test scenario with empty response body from GET request
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testEmptyResponseBody(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseEmpty());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recipients.add(recip);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]'; // Partial deletion to trigger GET request
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test scenario with invalid JSON in GET response
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testInvalidJSONResponse(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseInvalidJSON());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recipients.add(recip);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]'; // Partial deletion to trigger GET request
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test scenario with invalid definition structure
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testInvalidDefinitionStructure(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseInvalidStructure());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recipients.add(recip);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]'; // Partial deletion to trigger GET request
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test scenario with empty recipients list (edge case for size check)
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testEmptyRecipientsList(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ // Empty recipients list
+ req.recipients = new List();
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]';
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test scenario with recipients that have null IDs
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testRecipientsWithNullIds(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ // Recipients with null IDs
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip1 = new mc_SubscriptionListDefinition_Recipients();
+ recip1.id = null; // Null ID
+ recip1.displayName = 'Test User';
+ recipients.add(recip1);
+
+ mc_SubscriptionListDefinition_Recipients recip2 = new mc_SubscriptionListDefinition_Recipients();
+ recip2.id = '0055e000001mKpC';
+ recipients.add(recip2);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]'; // No deletion, triggers partial update path
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test scenario with PUT request returning non-200 response
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testPutRequestError(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponsePutError());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recipients.add(recip);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ req.deletedRecipients = '[]'; // Partial deletion to trigger PUT request
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Test scenario with null deletedRecipients (to cover null check path)
+ */
+ @isTest
+ private static void mc_DeleteRecipients_testNullDeletedRecipients(){
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ List requests = new List();
+ mc_DeleteRecipients.Request req = new mc_DeleteRecipients.Request();
+ req.notificationId = '0Au3C00000000kdSAA';
+
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recipients.add(recip);
+
+ req.recipients = recipients;
+ req.definition = createTestDefinition();
+ req.deletedRecipients = null; // Null deletedRecipients
+ requests.add(req);
+
+ Test.startTest();
+ List results = mc_DeleteRecipients.mc_DeleteRecipients(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ }
+
+ /**
+ * Helper method to create test subscription definition
+ */
+ private static mc_SubscriptionListDefinition createTestDefinition() {
+ mc_SubscriptionListDefinition def = new mc_SubscriptionListDefinition();
+ def.id = '0Au3C00000000kdSAA';
+ def.active = true;
+ def.source = 'lightningReportSubscribe';
+ def.recordId = '00O5e000008VZezEAG';
+ return def;
+ }
+ /**
+ * Mock HTTP Callout class for successful responses
+ */
+ public class MockHttpResponse implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse resp = new HttpResponse();
resp.setStatusCode(200);
resp.setStatus('OK');
resp.setHeader('Content-Type', 'application/json');
- string jsonInp = '{"active": true,' + '\n';
- jsonInp += '"thresholds":';
- list thresholds =
- new list();
- mc_SubscriptionListDefinition_Thresholds threshold =
- new mc_SubscriptionListDefinition_Thresholds();
- list actions =
- new list();
- mc_SubscriptionListDefinition_Actions action =
- new mc_SubscriptionListDefinition_Actions();
- mc_SubscriptionListDef_Config conf = new mc_SubscriptionListDef_Config();
- List recips =
- new List();
- mc_SubscriptionListDefinition_Recipients recip =
- new mc_SubscriptionListDefinition_Recipients();
- recip.displayName = 'Andy';
- recip.type = 'user';
- recips.add(recip);
- conf.recipients = recips;
- action.configuration = conf;
- actions.add(action);
- threshold.actions = actions;
- thresholds.add(threshold);
- jsonInp += json.serialize(thresholds);
- jsonInp += '}';
- resp.setBody(jsonInp);
+
+ // Handle DELETE requests
+ if (req.getMethod() == 'DELETE') {
+ resp.setStatusCode(204);
+ resp.setStatus('No Content');
+ resp.setBody('');
+ return resp;
+ }
+
+ // Handle GET requests - return full subscription definition
+ String jsonBody = '{"active":true,"createdDate":"2022-10-12T01:10:52Z","deactivateOnTrigger":false,"id":"0Au3C00000000kdSAA","lastModifiedDate":"2022-10-12T01:10:52Z","name":"Notification","owner":{"id":"0055e000001mKpC","name":"Andy Haas"},"recordId":"00O5e000008VZezEAG","runAs":null,"schedule":{"details":{"daysOfWeek":["tue"],"timeOfDay":20},"frequency":"weekly"},"source":"lightningReportSubscribe","thresholds":[{"type":"always","conditions":null,"actions":[{"type":"sendEmail","configuration":{"excludeSnapshot":false,"recipients":[{"id":"0055e000001mKpC","displayName":"Andy Haas","type":"user"}]}}]}]}';
+ resp.setBody(jsonBody);
+ return resp;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for error responses
+ */
+ public class MockHttpResponseError implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse resp = new HttpResponse();
+ resp.setStatusCode(404);
+ resp.setStatus('Not Found');
+ resp.setHeader('Content-Type', 'application/json');
+ resp.setBody('{"error":"Not found"}');
+ return resp;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for empty responses
+ */
+ public class MockHttpResponseEmpty implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse resp = new HttpResponse();
+ if (req.getMethod() == 'GET') {
+ resp.setStatusCode(200);
+ resp.setBody(''); // Empty body
+ } else {
+ resp.setStatusCode(200);
+ resp.setBody('{"active":true}');
+ }
+ resp.setHeader('Content-Type', 'application/json');
+ return resp;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for invalid JSON responses
+ */
+ public class MockHttpResponseInvalidJSON implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse resp = new HttpResponse();
+ if (req.getMethod() == 'GET') {
+ resp.setStatusCode(200);
+ resp.setBody('{invalid json}');
+ } else {
+ resp.setStatusCode(200);
+ resp.setBody('{"active":true}');
+ }
+ resp.setHeader('Content-Type', 'application/json');
+ return resp;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for invalid structure responses
+ */
+ public class MockHttpResponseInvalidStructure implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse resp = new HttpResponse();
+ if (req.getMethod() == 'GET') {
+ resp.setStatusCode(200);
+ // Response without thresholds/actions structure
+ resp.setBody('{"active":true,"id":"0Au3C00000000kdSAA","thresholds":null}');
+ } else {
+ resp.setStatusCode(200);
+ resp.setBody('{"active":true}');
+ }
+ resp.setHeader('Content-Type', 'application/json');
+ return resp;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for PUT request error responses
+ */
+ public class MockHttpResponsePutError implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse resp = new HttpResponse();
+ if (req.getMethod() == 'GET') {
+ resp.setStatusCode(200);
+ String jsonBody = '{"active":true,"createdDate":"2022-10-12T01:10:52Z","deactivateOnTrigger":false,"id":"0Au3C00000000kdSAA","lastModifiedDate":"2022-10-12T01:10:52Z","name":"Notification","owner":{"id":"0055e000001mKpC","name":"Andy Haas"},"recordId":"00O5e000008VZezEAG","runAs":null,"schedule":{"details":{"daysOfWeek":["tue"],"timeOfDay":20},"frequency":"weekly"},"source":"lightningReportSubscribe","thresholds":[{"type":"always","conditions":null,"actions":[{"type":"sendEmail","configuration":{"excludeSnapshot":false,"recipients":[{"id":"0055e000001mKpC","displayName":"Andy Haas","type":"user"}]}}]}]}';
+ resp.setBody(jsonBody);
+ } else if (req.getMethod() == 'PUT') {
+ resp.setStatusCode(500); // Error response
+ resp.setBody('{"error":"Internal Server Error"}');
+ } else {
+ resp.setStatusCode(200);
+ resp.setBody('{"active":true}');
+ }
+ resp.setHeader('Content-Type', 'application/json');
return resp;
}
}
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipientsTest.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipientsTest.cls-meta.xml
index fbbad0af5..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipientsTest.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_DeleteRecipientsTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 56.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipients.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipients.cls
index 5562fc72a..845d02649 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipients.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipients.cls
@@ -1,50 +1,114 @@
+/**
+ * Flow Action: Parse Recipient Data
+ *
+ * Parses subscription definition JSON string to extract recipient information.
+ * This invocable action takes a serialized subscription definition and extracts
+ * the recipients list for use in Flow screens or other actions.
+ *
+ * @author Andy Haas
+ * @see https://unofficialsf.com/from-andy-haas-use-analytics-management-actions-to-show-report-subscribers-and-assignments/
+ */
public with sharing class mc_GetRecipients {
- @InvocableMethod(label='Parse Recipient Data' description='Parses the Recipient Data into usuable class for the flow' category='Analytic Subscription')
+ /**
+ * Invocable method to parse recipient data from subscription definition string
+ *
+ * @param requests List of Request objects containing definition_string (JSON)
+ * @return List List of Results containing recipients and notification details
+ */
+ @InvocableMethod(label='Parse Recipient Data' description='Parses the Recipient Data into usable class for the flow' category='Analytic Subscription')
public static List mc_GetRecipients(List requests) {
- System.debug('mc_GetRecipients: ' + requests);
+ System.debug('mc_GetRecipients: Processing ' + requests.size() + ' request(s)');
+
// Set Response Wrapper
List responseWrapper = new List();
- // From the list of requests, get the recipients
- List recipients = new List();
-
// Loop through the requests
for (Request request : requests) {
- // Fix definition_string to remove [ and ]]
- // Remove the first character
- String definition_string = request.definition_string.substring(1);
- // Remove the last character
- definition_string = definition_string.substring(0, definition_string.length() - 1);
-
- // Deserialize the request with SubscriptionListDefinition
- mc_SubscriptionListDefinition subscriptionListDefinition = (mc_SubscriptionListDefinition)JSON.deserialize(definition_string, mc_SubscriptionListDefinition.class);
-
- System.debug('mc_GetRecipients: subscriptionListDefinition: ' + subscriptionListDefinition);
-
- Results response = new Results();
-
- // Check if subscriptionListDefinition is not null and then if not then add to the response wrapper
- if (subscriptionListDefinition != null) {
- response.recipients_string = JSON.serialize(subscriptionListDefinition.thresholds[0].actions[0].configuration.recipients != null ? subscriptionListDefinition.thresholds[0].actions[0].configuration.recipients : null);
- response.recipients = subscriptionListDefinition.thresholds[0].actions[0].configuration.recipients != null ? subscriptionListDefinition.thresholds[0].actions[0].configuration.recipients : null;
- response.notificationId = subscriptionListDefinition.id != null ? subscriptionListDefinition.id : null;
- response.definition = subscriptionListDefinition != null ? subscriptionListDefinition : null;
- responseWrapper.add(response);
- System.debug('recipients_string: ' + response.recipients_string);
- System.debug('recipients: ' + response.recipients);
- }
+ try {
+ // Validate input
+ if (String.isBlank(request.definition_string)) {
+ System.debug('Warning: Empty definition_string provided');
+ continue;
+ }
+
+ // Fix definition_string to remove [ and ] brackets if present
+ String definition_string = request.definition_string.trim();
+
+ // Remove the first character if it's a bracket
+ if (definition_string.startsWith('[')) {
+ definition_string = definition_string.substring(1);
+ }
+
+ // Remove the last character if it's a bracket
+ if (definition_string.endsWith(']')) {
+ definition_string = definition_string.substring(0, definition_string.length() - 1);
+ }
+ // Deserialize the request with SubscriptionListDefinition
+ mc_SubscriptionListDefinition subscriptionListDefinition;
+ try {
+ subscriptionListDefinition = (mc_SubscriptionListDefinition)JSON.deserialize(definition_string, mc_SubscriptionListDefinition.class);
+ } catch (JSONException e) {
+ System.debug('Error deserializing definition_string: ' + e.getMessage());
+ continue;
+ }
+
+ System.debug('mc_GetRecipients: subscriptionListDefinition: ' + subscriptionListDefinition);
+
+ Results response = new Results();
+
+ // Check if subscriptionListDefinition is not null and has required data
+ if (subscriptionListDefinition != null) {
+ // Validate thresholds and actions exist before accessing
+ if (subscriptionListDefinition.thresholds != null &&
+ !subscriptionListDefinition.thresholds.isEmpty() &&
+ subscriptionListDefinition.thresholds[0].actions != null &&
+ !subscriptionListDefinition.thresholds[0].actions.isEmpty() &&
+ subscriptionListDefinition.thresholds[0].actions[0].configuration != null) {
+
+ // Extract recipients safely
+ List recipients =
+ subscriptionListDefinition.thresholds[0].actions[0].configuration.recipients;
+
+ response.recipients_string = JSON.serialize(recipients != null ? recipients : new List());
+ response.recipients = recipients != null ? recipients : new List();
+ } else {
+ // Set empty recipients if structure is invalid
+ response.recipients_string = JSON.serialize(new List());
+ response.recipients = new List();
+ System.debug('Warning: Subscription definition missing thresholds or actions structure');
+ }
+
+ response.notificationId = subscriptionListDefinition.id;
+ response.definition = subscriptionListDefinition;
+ responseWrapper.add(response);
+
+ System.debug('recipients_string: ' + response.recipients_string);
+ System.debug('recipients count: ' + (response.recipients != null ? response.recipients.size() : 0));
+ } else {
+ System.debug('Warning: subscriptionListDefinition is null after deserialization');
+ }
+ } catch (Exception e) {
+ System.debug('Error processing request: ' + e.getMessage());
+ System.debug('Stack trace: ' + e.getStackTraceString());
+ // Continue to next request instead of failing entire batch
+ }
}
return responseWrapper;
-
}
+ /**
+ * Request wrapper class for invocable method input
+ */
public class Request {
@InvocableVariable(required=true)
public string definition_string;
}
+ /**
+ * Results wrapper class for invocable method output
+ */
public class Results {
@InvocableVariable
public List recipients;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipients.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipients.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipients.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipients.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipientsTest.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipientsTest.cls
index f50ddb518..29a6d82f1 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipientsTest.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipientsTest.cls
@@ -1,28 +1,170 @@
+/**
+ * Test class for mc_GetRecipients
+ *
+ * Provides test coverage for the Parse Recipient Data invocable action.
+ * Tests parsing of subscription definition JSON strings to extract recipient information.
+ *
+ * @author Andy Haas
+ */
@isTest
public with sharing class mc_GetRecipientsTest {
- public mc_GetRecipientsTest() {
-
- }
+ /**
+ * Test method for mc_GetRecipients - successful parsing
+ *
+ * Tests the recipient parsing functionality by deserializing
+ * a subscription definition string and extracting recipients.
+ */
@isTest
public static void mc_GetRecipients_test(){
- list reqs = new list();
- mc_GetRecipients.request req = new mc_GetRecipients.request();
- req.definition_string = '${"active": true,';
- req.definition_string += '"thresholds":';
- List thresholds =
- new list();
- mc_SubscriptionListDefinition_Thresholds threshold =
- new mc_SubscriptionListDefinition_Thresholds();
- List actions =
- new List();
+ List reqs = new List();
+ mc_GetRecipients.Request req = new mc_GetRecipients.Request();
+
+ // Create a valid subscription definition with recipients
+ mc_SubscriptionListDefinition def = new mc_SubscriptionListDefinition();
+ def.active = true;
+ def.id = '0Au3C00000000kdSAA';
+
+ List thresholds = new List();
+ mc_SubscriptionListDefinition_Thresholds threshold = new mc_SubscriptionListDefinition_Thresholds();
+ threshold.type = 'always';
+
+ List actions = new List();
+ mc_SubscriptionListDefinition_Actions action = new mc_SubscriptionListDefinition_Actions();
+ action.type = 'sendEmail';
+
+ mc_SubscriptionListDef_Config config = new mc_SubscriptionListDef_Config();
+ config.excludeSnapshot = false;
+
+ List recipients = new List();
+ mc_SubscriptionListDefinition_Recipients recip = new mc_SubscriptionListDefinition_Recipients();
+ recip.id = '0055e000001mKpC';
+ recip.displayName = 'Test User';
+ recip.type = 'user';
+ recipients.add(recip);
+ config.recipients = recipients;
+
+ action.configuration = config;
+ actions.add(action);
+ threshold.actions = actions;
+ thresholds.add(threshold);
+ def.thresholds = thresholds;
+
+ // Serialize with brackets to test bracket removal
+ req.definition_string = '[' + JSON.serialize(def) + ']';
+ reqs.add(req);
+
+ Test.startTest();
+ List results = mc_GetRecipients.mc_GetRecipients(reqs);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ System.assertNotEquals(null, results[0].recipients, 'Recipients should not be null');
+ System.assertEquals(1, results[0].recipients.size(), 'Should have one recipient');
+ }
+
+ /**
+ * Test empty definition_string
+ */
+ @isTest
+ public static void mc_GetRecipients_testEmptyString(){
+ List reqs = new List();
+ mc_GetRecipients.Request req = new mc_GetRecipients.Request();
+ req.definition_string = ''; // Empty string
+ reqs.add(req);
+
+ Test.startTest();
+ List results = mc_GetRecipients.mc_GetRecipients(reqs);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(0, results.size(), 'Should return empty list for invalid input');
+ }
+
+ /**
+ * Test invalid JSON
+ */
+ @isTest
+ public static void mc_GetRecipients_testInvalidJSON(){
+ List reqs = new List();
+ mc_GetRecipients.Request req = new mc_GetRecipients.Request();
+ req.definition_string = '{invalid json}';
+ reqs.add(req);
+
+ Test.startTest();
+ List results = mc_GetRecipients.mc_GetRecipients(reqs);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(0, results.size(), 'Should return empty list for invalid JSON');
+ }
+
+ /**
+ * Test missing thresholds/actions structure
+ */
+ @isTest
+ public static void mc_GetRecipients_testMissingStructure(){
+ List reqs = new List();
+ mc_GetRecipients.Request req = new mc_GetRecipients.Request();
+
+ // Create definition without thresholds
+ mc_SubscriptionListDefinition def = new mc_SubscriptionListDefinition();
+ def.active = true;
+ def.id = '0Au3C00000000kdSAA';
+ def.thresholds = null; // No thresholds
+
+ req.definition_string = JSON.serialize(def);
+ reqs.add(req);
+
+ Test.startTest();
+ List results = mc_GetRecipients.mc_GetRecipients(reqs);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ System.assertNotEquals(null, results[0].recipients, 'Recipients should not be null');
+ System.assertEquals(0, results[0].recipients.size(), 'Should have empty recipients list');
+ }
+
+ /**
+ * Test with null recipients
+ */
+ @isTest
+ public static void mc_GetRecipients_testNullRecipients(){
+ List reqs = new List();
+ mc_GetRecipients.Request req = new mc_GetRecipients.Request();
+
+ mc_SubscriptionListDefinition def = new mc_SubscriptionListDefinition();
+ def.active = true;
+ def.id = '0Au3C00000000kdSAA';
+
+ List thresholds = new List();
+ mc_SubscriptionListDefinition_Thresholds threshold = new mc_SubscriptionListDefinition_Thresholds();
+ threshold.type = 'always';
+
+ List actions = new List();
mc_SubscriptionListDefinition_Actions action = new mc_SubscriptionListDefinition_Actions();
+ action.type = 'sendEmail';
+
mc_SubscriptionListDef_Config config = new mc_SubscriptionListDef_Config();
+ config.excludeSnapshot = false;
+ config.recipients = null; // Null recipients
action.configuration = config;
actions.add(action);
threshold.actions = actions;
thresholds.add(threshold);
- req.definition_string += json.serialize(thresholds) + '}$';
+ def.thresholds = thresholds;
+
+ req.definition_string = JSON.serialize(def);
reqs.add(req);
- mc_GetRecipients.mc_GetRecipients(reqs);
+
+ Test.startTest();
+ List results = mc_GetRecipients.mc_GetRecipients(reqs);
+ Test.stopTest();
+
+ System.assertNotEquals(null, results, 'Results should not be null');
+ System.assertEquals(1, results.size(), 'Should return one result');
+ System.assertNotEquals(null, results[0].recipients, 'Recipients should not be null');
+ System.assertEquals(0, results[0].recipients.size(), 'Should have empty recipients list');
}
}
\ No newline at end of file
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipientsTest.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipientsTest.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipientsTest.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetRecipientsTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimit.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimit.cls
index e5d012d4e..558dee1ee 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimit.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimit.cls
@@ -1,65 +1,111 @@
+/**
+ * Flow Action: Get User Subscription Limit
+ *
+ * Retrieves the subscription limit for a user using the Analytics Notification Limits API.
+ * This invocable action queries the maximum number of subscriptions allowed for a user
+ * based on the source type (e.g., lightningReportSubscribe).
+ *
+ * @author Andy Haas
+ * @see https://unofficialsf.com/from-andy-haas-use-analytics-management-actions-to-show-report-subscribers-and-assignments/
+ */
public with sharing class mc_GetUserSubscriptionLimit {
+ // API version constant for REST API calls
+ private static final String API_VERSION = 'v65.0';
+
+ /**
+ * Invocable method to get user subscription limit from Analytics Notification Limits API
+ *
+ * @param requestList List of Request objects containing source and optional recordId
+ * @return List List of Results containing subscription limit definition
+ */
@InvocableMethod(label='Get User Subscription Limit' description='Get the subscription limit for a user' category='Analytic Subscription')
public static List mc_GetUserSubscriptionLimitList(List requestList) {
- // Set list of results
- List userLimit = new List();
-
// Set Response wrapper
List responseList = new List();
for( Request currRequest : requestList ) {
- System.debug('source: ' + currRequest.source);
- System.debug('recordId: ' + currRequest.recordId);
-
- // Instantiate a new http object
- Http http = new Http();
+ try {
+ System.debug('source: ' + currRequest.source);
+ System.debug('recordId: ' + currRequest.recordId);
+
+ // Instantiate a new http object
+ Http http = new Http();
- // Get Domain URL
- string domainURL = URL.getSalesforceBaseUrl().toExternalForm();
- string baseURL = '/services/data/v55.0/analytics/notifications/limits?source=' + currRequest.source;
+ // Get Domain URL
+ string domainURL = System.Url.getOrgDomainUrl().toExternalForm();
+ string baseURL = '/services/data/' + API_VERSION + '/analytics/notifications/limits?source=' + currRequest.source;
- // Go throuhg inputs and set parameters
- if ( currRequest.recordId != null ) {
- baseURL += '&recordId=' + currRequest.recordId;
- }
+ // Go through inputs and set parameters
+ if ( currRequest.recordId != null && currRequest.recordId != '' ) {
+ baseURL += '&recordId=' + currRequest.recordId;
+ }
- // Set URL from domain and base URL
- string url = domainURL + baseURL;
+ // Set URL from domain and base URL
+ string url = domainURL + baseURL;
- // Set Session Id
- string sessionId = 'session \n id';
- if(!test.isRunningTest()){
- sessionId = Page.usf3__GenerateSessionIdForLWC.getContent().toString();
- }
- // Fix Session Id
- sessionId = sessionId.substring(sessionId.indexOf('\n')+1);
+ // Set Session Id
+ string sessionId = 'session \n id';
+ if(!test.isRunningTest()){
+ sessionId = Page.usf3__GenerateSessionIdForLWC.getContent().toString();
+ }
+ // Fix Session Id - extract the actual session ID after the newline
+ if (sessionId.indexOf('\n') >= 0) {
+ sessionId = sessionId.substring(sessionId.indexOf('\n')+1);
+ }
- // Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
- HttpRequest request = new HttpRequest();
- request.setEndpoint(url);
- request.setMethod('GET');
- request.setHeader('Accept', 'application/json');
- request.setHeader('Accept-Encoding', 'gzip, deflate, br');
- request.setHeader('Authorization', 'OAuth ' + sessionId);
+ // Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
+ HttpRequest request = new HttpRequest();
+ request.setEndpoint(url);
+ request.setMethod('GET');
+ request.setHeader('Accept', 'application/json');
+ request.setHeader('Accept-Encoding', 'gzip, deflate, br');
+ request.setHeader('Authorization', 'OAuth ' + sessionId);
- // Send the request and store the response
- HttpResponse response = http.send(request);
+ // Send the request and store the response
+ HttpResponse response = http.send(request);
- // Set Response Body
- String responseBody = response.getBody();
- System.debug('responseBody: ' + responseBody);
+ // Validate HTTP response status
+ if (response.getStatusCode() != 200) {
+ System.debug('Error: HTTP Status Code ' + response.getStatusCode() + ' - ' + response.getStatus());
+ System.debug('Response Body: ' + response.getBody());
+ continue;
+ }
- // Parse JSON Response
- mc_SubscriptionLimitDefinition parsedResponse = (mc_SubscriptionLimitDefinition)JSON.deserialize(responseBody, mc_SubscriptionLimitDefinition.class);
- Results results = new Results();
- results.definition = parsedResponse;
+ // Set Response Body
+ String responseBody = response.getBody();
+ System.debug('responseBody length: ' + responseBody.length());
- responseList.add(results);
+ // Validate response body is not empty
+ if (String.isBlank(responseBody)) {
+ System.debug('Warning: Empty response body received');
+ continue;
+ }
+
+ // Parse JSON Response
+ mc_SubscriptionLimitDefinition parsedResponse;
+ try {
+ parsedResponse = (mc_SubscriptionLimitDefinition)JSON.deserialize(responseBody, mc_SubscriptionLimitDefinition.class);
+ } catch (JSONException e) {
+ System.debug('Error parsing JSON response: ' + e.getMessage());
+ continue;
+ }
+
+ Results results = new Results();
+ results.definition = parsedResponse;
+ responseList.add(results);
+ } catch (Exception e) {
+ System.debug('Error processing request: ' + e.getMessage());
+ System.debug('Stack trace: ' + e.getStackTraceString());
+ // Continue to next request instead of failing entire batch
+ }
}
return responseList;
}
+ /**
+ * Request wrapper class for invocable method input
+ */
public class Request {
@InvocableVariable(required=true)
public string source;
@@ -68,6 +114,9 @@ public with sharing class mc_GetUserSubscriptionLimit {
public string recordId;
}
+ /**
+ * Results wrapper class for invocable method output
+ */
public class Results {
@InvocableVariable
public mc_SubscriptionLimitDefinition definition;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimit.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimit.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimit.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimit.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimitTest.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimitTest.cls
new file mode 100644
index 000000000..3199976e2
--- /dev/null
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimitTest.cls
@@ -0,0 +1,203 @@
+/**
+ * Test class for mc_GetUserSubscriptionLimit
+ *
+ * Provides test coverage for the Get User Subscription Limit invocable action.
+ * Tests subscription limit retrieval with mock HTTP callouts.
+ *
+ * @author Andy Haas
+ */
+@isTest
+public with sharing class mc_GetUserSubscriptionLimitTest {
+ /**
+ * Test method for mc_GetUserSubscriptionLimitList
+ *
+ * Tests the subscription limit retrieval with mock HTTP callout.
+ * Verifies that the method processes requests and returns results.
+ */
+ @isTest
+ public static void mc_GetUserSubscriptionLimitTest() {
+ // Set mock callout class
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ // Create Request
+ mc_GetUserSubscriptionLimit.Request request = new mc_GetUserSubscriptionLimit.Request();
+ List requests = new List();
+
+ // Set request parameters
+ request.source = 'lightningReportSubscribe';
+ request.recordId = '';
+
+ // Add request to list
+ requests.add(request);
+
+ // Call method to test
+ Test.startTest();
+ List res = mc_GetUserSubscriptionLimit.mc_GetUserSubscriptionLimitList(requests);
+ Test.stopTest();
+
+ // Verify response received contains fake values
+ System.assertNotEquals(null, res, 'Response should not be null');
+ System.assertEquals(1, res.size(), 'Should return one result');
+ System.assertNotEquals(null, res[0].definition, 'Definition should not be null');
+ }
+
+ /**
+ * Test method with recordId parameter
+ */
+ @isTest
+ public static void mc_GetUserSubscriptionLimitTestWithRecordId() {
+ // Set mock callout class
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
+
+ // Create Request
+ mc_GetUserSubscriptionLimit.Request request = new mc_GetUserSubscriptionLimit.Request();
+ List requests = new List();
+
+ // Set request parameters
+ request.source = 'lightningReportSubscribe';
+ request.recordId = '00O5e000008VZezEAG';
+
+ // Add request to list
+ requests.add(request);
+
+ // Call method to test
+ Test.startTest();
+ List res = mc_GetUserSubscriptionLimit.mc_GetUserSubscriptionLimitList(requests);
+ Test.stopTest();
+
+ // Verify response
+ System.assertNotEquals(null, res, 'Response should not be null');
+ System.assertEquals(1, res.size(), 'Should return one result');
+ }
+
+ /**
+ * Test error handling for non-200 HTTP response
+ */
+ @isTest
+ public static void mc_GetUserSubscriptionLimitTestErrorResponse() {
+ // Set mock callout class with error response
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseError());
+
+ // Create Request
+ mc_GetUserSubscriptionLimit.Request request = new mc_GetUserSubscriptionLimit.Request();
+ List requests = new List();
+
+ request.source = 'lightningReportSubscribe';
+ request.recordId = '';
+
+ requests.add(request);
+
+ // Call method to test
+ Test.startTest();
+ List res = mc_GetUserSubscriptionLimit.mc_GetUserSubscriptionLimitList(requests);
+ Test.stopTest();
+
+ // Verify error handling - should return empty list or continue gracefully
+ System.assertNotEquals(null, res, 'Response should not be null');
+ }
+
+ /**
+ * Test error handling for empty response body
+ */
+ @isTest
+ public static void mc_GetUserSubscriptionLimitTestEmptyResponse() {
+ // Set mock callout class with empty response
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseEmpty());
+
+ // Create Request
+ mc_GetUserSubscriptionLimit.Request request = new mc_GetUserSubscriptionLimit.Request();
+ List requests = new List();
+
+ request.source = 'lightningReportSubscribe';
+ request.recordId = '';
+
+ requests.add(request);
+
+ // Call method to test
+ Test.startTest();
+ List res = mc_GetUserSubscriptionLimit.mc_GetUserSubscriptionLimitList(requests);
+ Test.stopTest();
+
+ // Verify error handling
+ System.assertNotEquals(null, res, 'Response should not be null');
+ }
+
+ /**
+ * Test error handling for invalid JSON
+ */
+ @isTest
+ public static void mc_GetUserSubscriptionLimitTestInvalidJSON() {
+ // Set mock callout class with invalid JSON
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseInvalidJSON());
+
+ // Create Request
+ mc_GetUserSubscriptionLimit.Request request = new mc_GetUserSubscriptionLimit.Request();
+ List requests = new List();
+
+ request.source = 'lightningReportSubscribe';
+ request.recordId = '';
+
+ requests.add(request);
+
+ // Call method to test
+ Test.startTest();
+ List res = mc_GetUserSubscriptionLimit.mc_GetUserSubscriptionLimitList(requests);
+ Test.stopTest();
+
+ // Verify error handling
+ System.assertNotEquals(null, res, 'Response should not be null');
+ }
+
+ /**
+ * Mock HTTP Callout class for successful responses
+ */
+ public class MockHttpResponse implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{"userLimit":{"max":10,"remaining":5}}');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for error responses
+ */
+ public class MockHttpResponseError implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{"error":"Not found"}');
+ res.setStatusCode(404);
+ return res;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for empty responses
+ */
+ public class MockHttpResponseEmpty implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for invalid JSON responses
+ */
+ public class MockHttpResponseInvalidJSON implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{invalid json}');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+}
+
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimitTest.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimitTest.cls-meta.xml
new file mode 100644
index 000000000..82775b98b
--- /dev/null
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_GetUserSubscriptionLimitTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 65.0
+ Active
+
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_NotificationDefinition.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_NotificationDefinition.cls
index 3eda06b81..7b9e082df 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_NotificationDefinition.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_NotificationDefinition.cls
@@ -1,3 +1,12 @@
+/**
+ * Notification Definition wrapper class
+ *
+ * Custom definition class for use in DataTable and Flow screens.
+ * Contains simplified subscription notification data including report name,
+ * action type, recipients, schedule, and threshold information.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_NotificationDefinition {
// Custom Definitions for use in DataTable
@AuraEnabled
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_NotificationDefinition.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_NotificationDefinition.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_NotificationDefinition.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_NotificationDefinition.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionData.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionData.cls
index d84935d9c..0aa1388d2 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionData.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionData.cls
@@ -1,159 +1,240 @@
+/**
+ * Flow Action: Get Subscription Data
+ *
+ * Retrieves analytics notification subscription data using the Analytics Notification API.
+ * This invocable action queries subscriptions for reports and returns detailed information
+ * including thresholds, actions, schedules, and recipients.
+ *
+ * @author Andy Haas
+ * @see https://unofficialsf.com/from-andy-haas-use-analytics-management-actions-to-show-report-subscribers-and-assignments/
+ */
public with sharing class mc_SubscriptionData {
+ // API version constant for REST API calls
+ private static final String API_VERSION = 'v65.0';
+
+ /**
+ * Invocable method to get subscription data from Analytics Notification API
+ *
+ * @param requestList List of Request objects containing source, ownerId, and recordId
+ * @return List List of Results containing subscription definitions and details
+ */
@InvocableMethod(label='Get Subscription Data' description='Get Subscription Data' category='Analytic Subscription')
public static List mc_GetSubscriptionDataList(List requestList) {
- System.Debug('mc_SubscriptionDataList');
-
- // Set list of subscriptions
- List subscriptionList = new List();
+ System.debug('mc_SubscriptionDataList: Processing ' + requestList.size() + ' request(s)');
// Set Response Wrapper
List responseWrapper = new List();
for( Request currRequest : requestList ) {
- System.Debug('source: ' + currRequest.source);
- System.Debug('ownerId: ' + currRequest.ownerId);
- System.Debug('recordId: ' + currRequest.recordId);
-
- // Instantiate a new http object
- Http h = new Http();
-
- // Get Domain URL
- string domainURL = URL.getSalesforceBaseUrl().toExternalForm();
- System.debug('domainURL: ' + domainURL);
- string baseURL = '/services/data/v55.0/analytics/notifications?source=' + currRequest.source;
-
- // Go through the inputs and set the URL Parameters
- if (currRequest.ownerId != null) {
- baseURL = baseURL + '&ownerId=' + currRequest.ownerId;
- }
-
- if (currRequest.recordId != null) {
- baseURL = baseURL + '&recordId=' + currRequest.recordId;
- }
-
- // Set URL from domain and base url
- string url = domainURL + baseURL;
-
- // Set Session ID
- string sessionId = 'session \n id';
- if(!test.isRunningTest()){
- sessionId = Page.usf3__GenerateSessionIdForLWC.getContent().toString();
- }
- // Fix Session Id
- sessionId = sessionId.substring(sessionId.indexOf('\n')+1);
-
- // Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
- HttpRequest req = new HttpRequest();
- req.setEndpoint(url);
- req.setMethod('GET');
- req.setHeader('Accept', 'application/json');
- req.setHeader('Accept-Encoding', 'gzip, deflate, br');
- req.setHeader('Authorization', 'OAuth ' + sessionId);
-
- System.Debug('userSessionId: ' + sessionId);
+ try {
+ System.debug('source: ' + currRequest.source);
+ System.debug('ownerId: ' + currRequest.ownerId);
+ System.debug('recordId: ' + currRequest.recordId);
- // Send the request, and return a response
- HttpResponse res = h.send(req);
-
- // Set Response Body
- string responseBody = res.getBody();
- System.Debug('responseBody: ' + responseBody);
-
- // replace time: with timeOfDay: in the response body
- responseBody = responseBody.replace('"time":', '"timeOfDay":');
-
- // Parse JSON Response
- List parsedResponse = (List)JSON.deserialize(responseBody, List.class);
- System.Debug('paredResponse: ' + parsedResponse);
- Results response = new Results();
- response.definition = parsedResponse;
-
- // Create a temp list for notificationDefinitions
- List notificationDefinitions = new List();
-
- // Iterate through the list of subscriptions and set report name and other details to another list
- for( mc_SubscriptionListDefinition currSubscription : response.definition ) {
- System.Debug('currSubscription: ' + currSubscription.recordId);
-
- // Set notificationDefinition to null
- mc_NotificationDefinition notificationDefinition = new mc_NotificationDefinition();
+ // Instantiate a new http object
+ Http h = new Http();
+
+ // Get Domain URL
+ string domainURL = System.Url.getOrgDomainUrl().toExternalForm();
+ System.debug('domainURL: ' + domainURL);
+ string baseURL = '/services/data/' + API_VERSION + '/analytics/notifications?source=' + currRequest.source;
+
+ // Go through the inputs and set the URL Parameters
+ if (currRequest.ownerId != null && currRequest.ownerId != '') {
+ baseURL = baseURL + '&ownerId=' + currRequest.ownerId;
+ }
- // Set Report Name
+ if (currRequest.recordId != null && currRequest.recordId != '') {
+ baseURL = baseURL + '&recordId=' + currRequest.recordId;
+ }
+
+ // Set URL from domain and base url
+ string url = domainURL + baseURL;
+
+ // Set Session ID
+ string sessionId = 'session \n id';
if(!test.isRunningTest()){
- notificationDefinition.reportName = [SELECT Name FROM Report WHERE Id = :currSubscription.recordId].Name;
+ sessionId = Page.usf3__GenerateSessionIdForLWC.getContent().toString();
+ }
+ // Fix Session Id - extract the actual session ID after the newline
+ if (sessionId.indexOf('\n') >= 0) {
+ sessionId = sessionId.substring(sessionId.indexOf('\n')+1);
}
- // Set Action Type
- notificationDefinition.actionType = currSubscription.thresholds[0].type;
-
- // Set Exclude Snapshot
- notificationDefinition.excludeSnapshot = currSubscription.thresholds[0].actions[0].configuration.excludeSnapshot;
-
- // Set Recipent Count
- notificationDefinition.recipientCount = currSubscription.thresholds[0].actions[0].configuration.recipients.size();
-
- // Set Thresholds
- notificationDefinition.thresholds = currSubscription.thresholds;
-
- // Set Time of Day (if applicable) with elvis operator
- notificationDefinition.timeOfDay = currSubscription.schedule.details.timeOfDay != null ? currSubscription.schedule.details.timeOfDay : null;
-
- // Set Days of Week (if applicable) with elvis operator
- notificationDefinition.daysOfWeek = currSubscription.schedule.details.daysOfWeek != null ? currSubscription.schedule.details.daysOfWeek.toString() : null;
-
- // Set Days of Month (if applicable) with elvis operator
- notificationDefinition.daysOfMonth = currSubscription.schedule.details.daysOfMonth != null ? currSubscription.schedule.details.daysOfMonth.toString() : null;
- // Set Week In Month (if applicable) with elvis operator
- notificationDefinition.weekInMonth = currSubscription.schedule.details.weekInMonth != null ? currSubscription.schedule.details.weekInMonth : null;
-
- // Set Frequency
- notificationDefinition.frequency = currSubscription.schedule.frequency;
-
- // Set Active
- notificationDefinition.active = currSubscription.active;
-
- // Set Id
- notificationDefinition.id = currSubscription.id;
-
- // Set createdDate
- notificationDefinition.createdDate = currSubscription.createdDate;
-
- // Set lastModifiedDate
- notificationDefinition.lastModifiedDate = currSubscription.lastModifiedDate;
-
- // Set name
- notificationDefinition.name = currSubscription.name;
+ // Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
+ HttpRequest req = new HttpRequest();
+ req.setEndpoint(url);
+ req.setMethod('GET');
+ req.setHeader('Accept', 'application/json');
+ req.setHeader('Accept-Encoding', 'gzip, deflate, br');
+ req.setHeader('Authorization', 'OAuth ' + sessionId);
+
+ System.debug('userSessionId: ' + (sessionId.length() > 10 ? sessionId.substring(0, 10) + '...' : sessionId));
+
+ // Send the request, and return a response
+ HttpResponse res = h.send(req);
+
+ // Validate HTTP response status
+ if (res.getStatusCode() != 200) {
+ System.debug('Error: HTTP Status Code ' + res.getStatusCode() + ' - ' + res.getStatus());
+ System.debug('Response Body: ' + res.getBody());
+ // Continue to next request instead of throwing exception
+ continue;
+ }
+
+ // Set Response Body
+ string responseBody = res.getBody();
+ System.debug('responseBody length: ' + responseBody.length());
+
+ // Validate response body is not empty
+ if (String.isBlank(responseBody)) {
+ System.debug('Warning: Empty response body received');
+ continue;
+ }
+
+ // replace time: with timeOfDay: in the response body (for Apex class compatibility)
+ responseBody = responseBody.replace('"time":', '"timeOfDay":');
+
+ // Parse JSON Response
+ List parsedResponse;
+ try {
+ parsedResponse = (List)JSON.deserialize(responseBody, List.class);
+ } catch (JSONException e) {
+ System.debug('Error parsing JSON response: ' + e.getMessage());
+ continue;
+ }
+
+ System.debug('parsedResponse size: ' + (parsedResponse != null ? parsedResponse.size() : 0));
+
+ Results response = new Results();
+ response.definition = parsedResponse;
+
+ // Create a temp list for notificationDefinitions
+ List notificationDefinitions = new List();
+
+ // Iterate through the list of subscriptions and set report name and other details to another list
+ if (parsedResponse != null) {
+ for( mc_SubscriptionListDefinition currSubscription : parsedResponse ) {
+ System.debug('currSubscription: ' + currSubscription.recordId);
+
+ // Validate subscription has required data before processing
+ if (currSubscription == null ||
+ currSubscription.thresholds == null ||
+ currSubscription.thresholds.isEmpty() ||
+ currSubscription.thresholds[0].actions == null ||
+ currSubscription.thresholds[0].actions.isEmpty()) {
+ System.debug('Warning: Skipping subscription with missing threshold or action data: ' + currSubscription?.recordId);
+ continue;
+ }
+
+ mc_NotificationDefinition notificationDefinition = new mc_NotificationDefinition();
+
+ // Set Report Name
+ if(!test.isRunningTest() && currSubscription.recordId != null){
+ try {
+ List reports = [SELECT Name FROM Report WHERE Id = :currSubscription.recordId LIMIT 1];
+ if (!reports.isEmpty()) {
+ notificationDefinition.reportName = reports[0].Name;
+ }
+ } catch (Exception e) {
+ System.debug('Error retrieving report name: ' + e.getMessage());
+ }
+ }
+
+ // Set Action Type (with null check)
+ if (currSubscription.thresholds[0].type != null) {
+ notificationDefinition.actionType = currSubscription.thresholds[0].type;
+ }
+
+ // Set Exclude Snapshot (with null check)
+ if (currSubscription.thresholds[0].actions[0].configuration != null) {
+ notificationDefinition.excludeSnapshot = currSubscription.thresholds[0].actions[0].configuration.excludeSnapshot;
+ }
+
+ // Set Recipient Count (with null check)
+ if (currSubscription.thresholds[0].actions[0].configuration != null &&
+ currSubscription.thresholds[0].actions[0].configuration.recipients != null) {
+ notificationDefinition.recipientCount = currSubscription.thresholds[0].actions[0].configuration.recipients.size();
+ } else {
+ notificationDefinition.recipientCount = 0;
+ }
+
+ // Set Thresholds
+ notificationDefinition.thresholds = currSubscription.thresholds;
+
+ // Set Time of Day (if applicable) with null check
+ if (currSubscription.schedule != null &&
+ currSubscription.schedule.details != null) {
+ notificationDefinition.timeOfDay = currSubscription.schedule.details.timeOfDay;
+
+ // Set Days of Week (if applicable)
+ if (currSubscription.schedule.details.daysOfWeek != null) {
+ notificationDefinition.daysOfWeek = currSubscription.schedule.details.daysOfWeek.toString();
+ }
+
+ // Set Days of Month (if applicable)
+ if (currSubscription.schedule.details.daysOfMonth != null) {
+ notificationDefinition.daysOfMonth = currSubscription.schedule.details.daysOfMonth.toString();
+ }
+
+ // Set Week In Month (if applicable)
+ notificationDefinition.weekInMonth = currSubscription.schedule.details.weekInMonth;
+ }
+
+ // Set Frequency (with null check)
+ if (currSubscription.schedule != null) {
+ notificationDefinition.frequency = currSubscription.schedule.frequency;
+ }
+
+ // Set Active
+ notificationDefinition.active = currSubscription.active;
+
+ // Set Id
+ notificationDefinition.id = currSubscription.id;
+
+ // Set createdDate
+ notificationDefinition.createdDate = currSubscription.createdDate;
+
+ // Set lastModifiedDate
+ notificationDefinition.lastModifiedDate = currSubscription.lastModifiedDate;
+
+ // Set name
+ notificationDefinition.name = currSubscription.name;
+
+ // Set recordId
+ notificationDefinition.recordId = currSubscription.recordId;
+
+ // Set source
+ notificationDefinition.source = currSubscription.source;
+
+ // Push notification object to list
+ System.debug('notificationDefinition: ' + notificationDefinition);
+ notificationDefinitions.add(notificationDefinition);
+ }
+ }
- // Set recordId
- notificationDefinition.recordId = currSubscription.recordId;
+ // Add notificationDefinitions to response
+ response.definitionDetails = notificationDefinitions;
- // Set source
- notificationDefinition.source = currSubscription.source;
+ // Stringify the response for use in DataTable
+ response.definition_string = JSON.serialize(response.definition);
+ response.definitionDetails_string = JSON.serialize(response.definitionDetails);
- // Push notification object to list
- System.debug('notificationDefinition: ' + notificationDefinition);
- notificationDefinitions.add(notificationDefinition);
- System.debug('response.definitionDetails: ' + response.definitionDetails);
+ responseWrapper.add(response);
+ } catch (Exception e) {
+ System.debug('Error processing request: ' + e.getMessage());
+ System.debug('Stack trace: ' + e.getStackTraceString());
+ // Continue to next request instead of failing entire batch
}
-
- // Add notificationDefinitions to response
- response.definitionDetails = notificationDefinitions;
-
- // Stringify the response for use in DataTable
- response.definition_string = JSON.serialize(response.definition);
- response.definitionDetails_string = JSON.serialize(response.definitionDetails);
-
- responseWrapper.add(response);
-
}
-
-
// Return the list of records
return responseWrapper;
-
}
+ /**
+ * Request wrapper class for invocable method input
+ */
public class Request {
@InvocableVariable(required=true)
public string source;
@@ -165,6 +246,9 @@ public with sharing class mc_SubscriptionData {
public string recordId;
}
+ /**
+ * Results wrapper class for invocable method output
+ */
public class Results {
@InvocableVariable
public List definition;
@@ -179,16 +263,13 @@ public with sharing class mc_SubscriptionData {
public string definitionDetails_string;
}
+ /**
+ * Helper method to parse JSON string into SubscriptionListDefinition
+ *
+ * @param json JSON string to parse
+ * @return mc_SubscriptionListDefinition Parsed subscription definition
+ */
public static mc_SubscriptionListDefinition parse(String json){
return (mc_SubscriptionListDefinition) System.JSON.deserialize(json, mc_SubscriptionListDefinition.class);
}
-
- // static testMethod void testParse() {
- // String json='[{"active":true,"createdDate":"2022-10-12T01:10:52Z","deactivateOnTrigger":false,"id":"0Au3C00000000kdSAA","lastModifiedDate":"2022-10-12T01:10:52Z","name":"Notification","owner":{"id":"0055e000001mKpC","name":"Andy Haas"},"recordId":"00O5e000008VZezEAG","runAs":null,"schedule":{"details":{"daysOfWeek":["tue"],"time":20},"frequency":"weekly"},"source":"lightningReportSubscribe","thresholds":[{"actions":[{"configuration":{"excludeSnapshot":false,"recipients":[{"id":"0055e000001mKpC","displayName":"Andy Haas","type":"user"}]},"type":"sendEmail"}],"conditions":null,"type":"always"}]}]'+
- // '';
- // ResultSubscriptionList obj = parse(json);
- // System.assert(obj != null);
- // }
-
-
}
\ No newline at end of file
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionData.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionData.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionData.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionData.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionDataTest.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionDataTest.cls
index ca7a6277f..0fb39df7e 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionDataTest.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionDataTest.cls
@@ -1,10 +1,25 @@
+/**
+ * Test class for mc_SubscriptionData
+ *
+ * Provides test coverage for the Get Subscription Data invocable action.
+ * Uses HttpCalloutMock to simulate API responses without making actual callouts.
+ *
+ * @author Andy Haas
+ */
@isTest
public with sharing class mc_SubscriptionDataTest {
+ /**
+ * Test method for mc_GetSubscriptionDataList - basic test
+ *
+ * Tests the subscription data retrieval with mock HTTP callout.
+ * Verifies that the method processes requests and returns results.
+ */
@isTest
public static void mc_SubscriptionDataTest() {
// Set mock callout class
Test.setMock(HttpCalloutMock.class, new SubscriptionData_MockHTTP());
- system.debug([select id from report].size());
+ System.debug('Available reports: ' + [SELECT Id FROM Report].size());
+
// Create Request
mc_SubscriptionData.Request request = new mc_SubscriptionData.Request();
List requests = new List();
@@ -18,13 +33,184 @@ public with sharing class mc_SubscriptionDataTest {
requests.add(request);
// Call method to test.
- // This causes a fake response to be sent
- // from the class that implements HttpCalloutMock.
Test.startTest();
List res = mc_SubscriptionData.mc_GetSubscriptionDataList(requests);
Test.stopTest();
// Verify response received contains fake values
-
+ System.assertNotEquals(null, res, 'Response should not be null');
+ System.assertEquals(1, res.size(), 'Should return one result');
+ System.assertNotEquals(null, res[0].definition, 'Definition should not be null');
+ }
+
+ /**
+ * Test with ownerId and recordId parameters
+ */
+ @isTest
+ public static void mc_SubscriptionDataTestWithParameters() {
+ Test.setMock(HttpCalloutMock.class, new SubscriptionData_MockHTTP());
+
+ mc_SubscriptionData.Request request = new mc_SubscriptionData.Request();
+ List requests = new List();
+
+ request.source = 'lightningReportSubscribe';
+ request.ownerId = '0055e000001mKpC';
+ request.recordId = '00O5e000008VZezEAG';
+
+ requests.add(request);
+
+ Test.startTest();
+ List res = mc_SubscriptionData.mc_GetSubscriptionDataList(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, res, 'Response should not be null');
+ System.assertEquals(1, res.size(), 'Should return one result');
+ }
+
+ /**
+ * Test error handling - HTTP error response
+ */
+ @isTest
+ public static void mc_SubscriptionDataTestErrorResponse() {
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseError());
+
+ mc_SubscriptionData.Request request = new mc_SubscriptionData.Request();
+ List requests = new List();
+
+ request.source = 'lightningReportSubscribe';
+ request.ownerId = '';
+ request.recordId = '';
+
+ requests.add(request);
+
+ Test.startTest();
+ List res = mc_SubscriptionData.mc_GetSubscriptionDataList(requests);
+ Test.stopTest();
+
+ // Should handle error gracefully and continue
+ System.assertNotEquals(null, res, 'Response should not be null');
+ }
+
+ /**
+ * Test error handling - empty response body
+ */
+ @isTest
+ public static void mc_SubscriptionDataTestEmptyResponse() {
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseEmpty());
+
+ mc_SubscriptionData.Request request = new mc_SubscriptionData.Request();
+ List requests = new List();
+
+ request.source = 'lightningReportSubscribe';
+ request.ownerId = '';
+ request.recordId = '';
+
+ requests.add(request);
+
+ Test.startTest();
+ List res = mc_SubscriptionData.mc_GetSubscriptionDataList(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, res, 'Response should not be null');
+ }
+
+ /**
+ * Test error handling - invalid JSON
+ */
+ @isTest
+ public static void mc_SubscriptionDataTestInvalidJSON() {
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseInvalidJSON());
+
+ mc_SubscriptionData.Request request = new mc_SubscriptionData.Request();
+ List requests = new List();
+
+ request.source = 'lightningReportSubscribe';
+ request.ownerId = '';
+ request.recordId = '';
+
+ requests.add(request);
+
+ Test.startTest();
+ List res = mc_SubscriptionData.mc_GetSubscriptionDataList(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, res, 'Response should not be null');
+ }
+
+ /**
+ * Test with subscription missing thresholds/actions
+ */
+ @isTest
+ public static void mc_SubscriptionDataTestMissingStructure() {
+ Test.setMock(HttpCalloutMock.class, new MockHttpResponseMissingStructure());
+
+ mc_SubscriptionData.Request request = new mc_SubscriptionData.Request();
+ List requests = new List();
+
+ request.source = 'lightningReportSubscribe';
+ request.ownerId = '';
+ request.recordId = '';
+
+ requests.add(request);
+
+ Test.startTest();
+ List res = mc_SubscriptionData.mc_GetSubscriptionDataList(requests);
+ Test.stopTest();
+
+ System.assertNotEquals(null, res, 'Response should not be null');
+ System.assertEquals(1, res.size(), 'Should return one result');
+ }
+
+ /**
+ * Mock HTTP Callout class for error responses
+ */
+ public class MockHttpResponseError implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{"error":"Not found"}');
+ res.setStatusCode(404);
+ return res;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for empty responses
+ */
+ public class MockHttpResponseEmpty implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for invalid JSON responses
+ */
+ public class MockHttpResponseInvalidJSON implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ res.setBody('{invalid json}');
+ res.setStatusCode(200);
+ return res;
+ }
+ }
+
+ /**
+ * Mock HTTP Callout class for missing structure responses
+ */
+ public class MockHttpResponseMissingStructure implements HttpCalloutMock {
+ public HTTPResponse respond(HTTPRequest req) {
+ HttpResponse res = new HttpResponse();
+ res.setHeader('Content-Type', 'application/json');
+ // Response without thresholds
+ res.setBody('[{"active":true,"id":"0Au3C00000000kdSAA","recordId":"00O5e000008VZezEAG","source":"lightningReportSubscribe","thresholds":null}]');
+ res.setStatusCode(200);
+ return res;
+ }
}
}
\ No newline at end of file
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionDataTest.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionDataTest.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionDataTest.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionDataTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinition.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinition.cls
index 106f75d57..50962eea2 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinition.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinition.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription Limit Definition wrapper class
+ *
+ * Represents the subscription limit information for a user as returned
+ * by the Analytics Notification Limits API.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionLimitDefinition {
@AuraEnabled
public mc_SubscriptionLimitDefinitionDetails userLimit;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinition.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinition.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinition.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinition.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinitionDetails.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinitionDetails.cls
index 8aac80a1b..424fe0418 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinitionDetails.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinitionDetails.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription Limit Definition Details wrapper class
+ *
+ * Represents detailed subscription limit information for a user.
+ * Contains maximum allowed subscriptions and remaining count.
+ *
+ * @author Andy Haas
+ */
public class mc_SubscriptionLimitDefinitionDetails {
@AuraEnabled
public Integer max;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinitionDetails.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinitionDetails.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinitionDetails.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionLimitDefinitionDetails.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDef_Config.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDef_Config.cls
index cd04fda1d..2934a926a 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDef_Config.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDef_Config.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition Configuration wrapper class
+ *
+ * Represents configuration settings for notification actions.
+ * Contains exclude snapshot flag and list of recipients.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDef_Config {
@AuraEnabled
public boolean excludeSnapshot;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDef_Config.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDef_Config.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDef_Config.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDef_Config.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition.cls
index ab0b77c44..84a90fe7d 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition.cls
@@ -1,3 +1,12 @@
+/**
+ * Subscription List Definition wrapper class
+ *
+ * Represents the full structure of an analytics notification subscription
+ * as returned by the Analytics Notification API. This class maps directly
+ * to the JSON response from the API.
+ *
+ * @author Andy Haas
+ */
global with sharing class mc_SubscriptionListDefinition {
@AuraEnabled
public boolean active;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Actions.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Actions.cls
index 9af583a40..e79913b87 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Actions.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Actions.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition Actions wrapper class
+ *
+ * Represents actions to be taken when notification thresholds are met.
+ * Contains action type (e.g., "sendEmail") and configuration details.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDefinition_Actions {
@AuraEnabled
public mc_SubscriptionListDef_Config configuration;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Actions.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Actions.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Actions.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Actions.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Conditions.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Conditions.cls
index aa79e747b..c23a7ad25 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Conditions.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Conditions.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition Conditions wrapper class
+ *
+ * Represents threshold conditions for analytics notification subscriptions.
+ * Contains operator (e.g., "greaterThan"), value, and column name.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDefinition_Conditions {
@AuraEnabled
public String operator; //greaterThan
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Conditions.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Conditions.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Conditions.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Conditions.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Details.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Details.cls
index 0d404039f..d48304265 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Details.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Details.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition Details wrapper class
+ *
+ * Represents detailed schedule information for analytics notification subscriptions.
+ * Contains days of week, time of day, week in month, and days of month.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDefinition_Details {
@AuraEnabled
public string[] daysOfWeek;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Details.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Details.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Details.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Details.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Owner.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Owner.cls
index 0a7cda717..a3752b441 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Owner.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Owner.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition Owner wrapper class
+ *
+ * Represents the owner of an analytics notification subscription.
+ * Contains the owner's user ID and name.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDefinition_Owner {
@AuraEnabled
public Id id; //0055e000001mKpC
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Owner.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Owner.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Owner.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Owner.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Recipients.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Recipients.cls
index 3c1cae3da..ef1c4cd85 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Recipients.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Recipients.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition Recipients wrapper class
+ *
+ * Represents a recipient in an analytics notification subscription.
+ * Contains the recipient's ID, display name, and type (user, role, etc.).
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDefinition_Recipients {
@AuraEnabled
public Id id; //0055e000001mKpC
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Recipients.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Recipients.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Recipients.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Recipients.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_RunAs.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_RunAs.cls
index d8ebdd85b..480f9491c 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_RunAs.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_RunAs.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition RunAs wrapper class
+ *
+ * Represents the user context under which a notification subscription runs.
+ * Contains the user ID and name.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDefinition_RunAs {
@AuraEnabled
public Id id; //0055e000001mKpC
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_RunAs.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_RunAs.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_RunAs.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_RunAs.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Schedule.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Schedule.cls
index f758a7338..d8765ec85 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Schedule.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Schedule.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition Schedule wrapper class
+ *
+ * Represents the schedule configuration for analytics notification subscriptions.
+ * Contains frequency (e.g., "weekly", "daily") and schedule details.
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDefinition_Schedule {
@AuraEnabled
public mc_SubscriptionListdefinition_Details details;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Schedule.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Schedule.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Schedule.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Schedule.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Thresholds.cls b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Thresholds.cls
index a0fc090ce..719a59a03 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Thresholds.cls
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Thresholds.cls
@@ -1,3 +1,11 @@
+/**
+ * Subscription List Definition Thresholds wrapper class
+ *
+ * Represents threshold configuration for analytics notification subscriptions.
+ * Contains actions, conditions, and threshold type (e.g., "always").
+ *
+ * @author Andy Haas
+ */
public with sharing class mc_SubscriptionListDefinition_Thresholds {
@AuraEnabled
public List actions;
diff --git a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Thresholds.cls-meta.xml b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Thresholds.cls-meta.xml
index 4b0bc9f38..82775b98b 100644
--- a/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Thresholds.cls-meta.xml
+++ b/flow_action_components/AnalyticManagementAPI/force-app/main/default/classes/mc_SubscriptionListDefinition_Thresholds.cls-meta.xml
@@ -1,5 +1,5 @@
- 55.0
+ 65.0
Active
diff --git a/flow_action_components/AnalyticManagementAPI/sfdx-project.json b/flow_action_components/AnalyticManagementAPI/sfdx-project.json
index 295b68f31..883379992 100644
--- a/flow_action_components/AnalyticManagementAPI/sfdx-project.json
+++ b/flow_action_components/AnalyticManagementAPI/sfdx-project.json
@@ -1,25 +1,25 @@
{
- "packageDirectories": [
+ "packageDirectories": [
+ {
+ "path": "force-app",
+ "default": true,
+ "package": "AnalyticsManagementActions",
+ "versionName": "ver 0.3",
+ "versionNumber": "0.3.0.NEXT",
+ "dependencies": [
{
- "path": "force-app",
- "default": true,
- "package": "AnalyticsManagementActions",
- "versionName": "ver 0.3",
- "versionNumber": "0.3.0.NEXT",
- "dependencies": [
- {
- "subscriberPackageVersionId": "04t8b000000mKnoAAE"
- }
- ]
+ "subscriberPackageVersionId": "04t8b000000mKnoAAE"
}
- ],
- "name": "AnalyticsManagementActions",
- "namespace": "",
- "sfdcLoginUrl": "https://login.salesforce.com",
- "sourceApiVersion": "55.0",
- "packageAliases": {
- "FlowActionsBasePack@3.6.0-0": "04t8b000000mKnoAAE",
- "AnalyticsManagementActions": "0Ho5G0000008OceSAE",
- "AnalyticsManagementActions@1.0.0-0": "04t5G0000043xrnQAA"
+ ]
}
-}
\ No newline at end of file
+ ],
+ "name": "AnalyticsManagementActions",
+ "namespace": "",
+ "sfdcLoginUrl": "https://login.salesforce.com",
+ "sourceApiVersion": "65.0",
+ "packageAliases": {
+ "FlowActionsBasePack@3.6.0-0": "04t8b000000mKnoAAE",
+ "AnalyticsManagementActions": "0Ho5G0000008OceSAE",
+ "AnalyticsManagementActions@1.0.0-0": "04t5G0000043xrnQAA"
+ }
+}