Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
"**/node_modules": true,
"**/bower_components": true,
"**/.sfdx": true
},
"workbench.colorCustomizations": {
"statusBar.background": null,
"activityBar.background": null
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[![Open in Visual Studio Code](https://classroom.github.com/assets/open-in-vscode-718a45dd9cf7e7f842a935f5ebbe5719a5e09af4491e668f4dbf3b35d5cca122.svg)](https://classroom.github.com/online_ide?assignment_repo_id=14992435&assignment_repo_type=AssignmentRepo)
# Developer Kickstart: Integrations
This repository is a pivotal element of the Developer Kickstart curriculum at Cloud Code Academy. Crafted for aspiring Salesforce developers, this module delves into the intricate domain of integrations, spotlighting the principles, tools, and best practices necessary to effectively connect Salesforce with external systems and platforms.

Expand Down
57 changes: 57 additions & 0 deletions force-app/main/default/classes/ContactTriggerHandler.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Key Behaviors:
* 1. When a new Contact is inserted and doesn't have a value for the DummyJSON_Id__c field, the trigger generates a random number between 0 and 100 for it.
* 2. Upon insertion, if the generated or provided DummyJSON_Id__c value is less than or equal to 100, the trigger initiates the getDummyJSONUserFromId API call.
* 3. If a Contact record is updated and the DummyJSON_Id__c value is greater than 100, the trigger initiates the postCreateDummyJSONUser API call.
*
* Best Practices for Callouts in Triggers:
*
* 1. Avoid Direct Callouts: Triggers do not support direct HTTP callouts. Instead, use asynchronous methods like @future or Queueable to make the callout.
* 2. Bulkify Logic: Ensure that the trigger logic is bulkified so that it can handle multiple records efficiently without hitting governor limits.
* 3. Avoid Recursive Triggers: Ensure that the callout logic doesn't result in changes that re-invoke the same trigger, causing a recursive loop.
*/

public with sharing class ContactTriggerHandler extends TriggerHandler {

private List<Contact> newConts;

/**
* Constructor. Set instance variables.
*/
public ContactTriggerHandler() {
this.newConts = (List<Contact>) Trigger.new;
}

/**
* Before Insert method.
*
* When a contact is inserted
* if DummyJSON_Id__c is null, generate a random number between 0 and 100 and set this as the contact's DummyJSON_Id__c value
* if DummyJSON_Id__c is less than or equal to 100, call the getDummyJSONUserFromId API
*/
public override void beforeInsert() {
List<String> dummyJSONIdList = new List<String>();
for (Contact cont : newConts) {
if (cont.DummyJSON_Id__c == null) {
cont.DummyJSON_Id__c = String.valueOf(Math.round(Math.random() * 100));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
if (Integer.valueOf(cont.DummyJSON_Id__c) <= 100 && !System.isFuture()) {
DummyJSONCallout.getDummyJSONUserFromId(cont.DummyJSON_Id__c);
}
}
}

/**
* After Update method.
*
* When a contact is updated
* if DummyJSON_Id__c is greater than 100, call the postCreateDummyJSONUser API
*/
public override void afterUpdate() {
for (Contact cont : newConts) {
if (Integer.valueOf(cont.DummyJSON_Id__c) > 100 && !System.isFuture()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also consider adding an additional validation on the DummyJSON_Last_Updated__c field.

DummyJSONCallout.postCreateDummyJSONUser(cont.Id);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>59.0</apiVersion>
<status>Active</status>
</ApexClass>
86 changes: 64 additions & 22 deletions force-app/main/default/classes/DummyJSONCallout.cls
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*
* For more detailed information on HTTP callouts in Apex, refer to the official Salesforce documentation:
* https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_classes_restful_http_httprequest.htm
*
* Implemented by Oxana Suvorova
*/

public with sharing class DummyJSONCallout {
Expand All @@ -33,19 +35,26 @@ public with sharing class DummyJSONCallout {
*
* @param dummyUserId The ID of the user in the external system to fetch data for.
*/

@future(callout = true)
public static void getDummyJSONUserFromId(String dummyUserId) {
// Create HTTP request to send.

HttpRequest request = new HttpRequest();
// Set the endpoint URL. Use direct URL or for best practices use Named Credential.

request.setEndpoint('callout:DummyJsonUser' + '/' + dummyUserId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

// Set the HTTP method to GET.

request.setMethod('GET');
// Send the HTTP request and get the response.

// If the HTTP response code is successful, parse the JSON response and update the contact.
Contact cont = null;
upsert cont DummyJSON_Id__c; //insert/update from the JSON response using the external id (dummyUserId)
try {
Http http = new Http();
HttpResponse response = http.send(request);
// If the HTTP response code is successful, parse the JSON response and update the contact.
if (response.getStatusCode() == 200) {
Contact cont = createContactFromJson(response.getBody());
upsert cont DummyJSON_Id__c; // insert/update from the JSON response using the external id (dummyUserId)
}
} catch (Exception ex) {
System.debug('Error: ' + ex.getMessage());
}
}

/*
Expand All @@ -66,16 +75,27 @@ public with sharing class DummyJSONCallout {
@TestVisible // Allows test class to see this method. Since it is private, it would not be visible otherwise.
private static Contact createContactFromJson(String jsonResponse) {
// Deserialize the JSON string into map of primitive data types.

Map<String, Object> userDataMap = (Map<String, Object>) JSON.deserializeUntyped(jsonResponse);
// Create a new contact from the JSON response.

Contact cont = new Contact();
// Set the contact fields from the JSON response.

cont.FirstName = (String) userDataMap.get('firstName');
cont.LastName = (String) userDataMap.get('lastName');
cont.Email = (String) userDataMap.get('email');
cont.Phone = (String) userDataMap.get('phone');
cont.Birthdate = Date.valueOf((String) userDataMap.get('birthDate'));
cont.DummyJSON_Id__c = String.valueOf(userDataMap.get('id'));

// Deserialize the address from the JSON response.

Map<String, Object> addressMap = (Map<String, Object>) userDataMap.get('address');
// Set the address fields on the contact.
cont.MailingStreet = (String) addressMap.get('address');
cont.MailingCity = (String) addressMap.get('city');
cont.MailingPostalCode = (String) addressMap.get('postalCode');
cont.MailingState = (String) addressMap.get('state');
cont.MailingCountry = (String) addressMap.get('country');

return null;
return cont;
}

/*
Expand All @@ -91,19 +111,31 @@ public with sharing class DummyJSONCallout {
*
* @param contactId The Salesforce Contact ID used to generate the JSON payload for the external system.
*/

@future(callout = true)
public static void postCreateDummyJSONUser(String contactId) {
// Create HTTP request to send.

HttpRequest request = new HttpRequest();
// Set the endpoint URL. Use direct URL or for best practices use Named Credential.

request.setEndpoint('callout:DummyJsonUser/add');
// Set the HTTP method to POST.

request.setMethod('POST');
// Set the body using generateDummyJsonUserPayload method.

// String jsonBody = generateDummyJsonUserPayload();
// if (jsonBody.length() > 0)
request.setBody(generateDummyJsonUserPayload(contactId));

// Send the HTTP request and get the response.

Http http = new Http();
HttpResponse response = http.send(request);
// If the HTTP response code is successful, update the contact.
if (response.getStatusCode() >= 200 || response.getStatusCode() <= 299) {
Contact cont = [
SELECT Id, DummyJSON_Last_Updated__c
FROM Contact
WHERE Id = :contactId];
cont.DummyJSON_Last_Updated__c = Datetime.now();
update cont;
}
}

/*
Expand All @@ -124,13 +156,23 @@ public with sharing class DummyJSONCallout {
@TestVisible // Allows test class to see this method. Since it is private, it would not be visible otherwise.
private static String generateDummyJsonUserPayload(String contactId) {
// Query the contact to get the field values to generate the JSON payload.

Contact cont = [
SELECT Id, FirstName, LastName, Email, Phone, DummyJSON_Id__c
FROM Contact
WHERE Id = :contactId
];
// Create a map of the field values.

Map<String, Object> valuesMap = new Map<String, Object>();
valuesMap.put('salesforceId', contactId);
valuesMap.put('firstName', String.isNotBlank(cont.FirstName) ? cont.FirstName : 'unknown');
valuesMap.put('lastName', String.isNotBlank(cont.LastName) ? cont.LastName : 'unknown');
valuesMap.put('email', String.isNotBlank(cont.Email) ? cont.Email : 'unknown');
valuesMap.put('phone', String.isNotBlank(cont.Phone) ? cont.Phone : 'unknown');
Comment on lines +167 to +170

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work catching these values if they are empty.
Putting unknown for the email may cause an error.

// Serialize the map into a JSON string.
String json = JSON.serialize(valuesMap);
// Make sure to check that required contacts fields have a value. Default the value to unknown if it does not exists.
// Integration data can change over time. It is a best practice to add safeguards/validation to ensure the integration does not break.

return null;
return json;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>57.0</apiVersion>
<apiVersion>60.0</apiVersion>
<status>Active</status>
</ApexClass>
35 changes: 23 additions & 12 deletions force-app/main/default/classes/DummyJSONCalloutTest.cls
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*
* Update the ContactTrigger.trigger to do a callout to the DummyJSONCallout class to retrieve/send user data from the Dummy JSON API.
* Implement the DummyJSONCallout class to handle the callouts to the Dummy JSON API.
*/
@IsTest
public with sharing class DummyJSONCalloutTest {
// This test method will test the getDummyJSONUserFromId future method with a mock HTTP response.
Expand Down Expand Up @@ -129,28 +133,35 @@ public with sharing class DummyJSONCalloutTest {

@IsTest
static void testContactInsert() {
Contact cont = new Contact(FirstName = 'Test', LastName = 'User');
//Create 100 contact
List<Contact> contacts = new List<Contact>();
for (Integer i = 0; i < 1; i++) {
contacts.add(new Contact(FirstName = 'Test', LastName = 'User' + i));
}

// Register the mock callout class
Test.setMock(HttpCalloutMock.class, new DummyJSONCalloutMockGenerator());

// As this is a future method, we need to enclose it in Test.startTest() and Test.stopTest() to ensure it's executed in the test context.
Test.startTest();
insert cont;
insert contacts;
Test.stopTest();

// After the stopTest, the future method will have run. Now we can check if the contact was created correctly.
cont = [
SELECT Email, Phone, Birthdate, MailingStreet, MailingCity, MailingPostalCode, MailingState, MailingCountry
contacts = [
SELECT DummyJSON_Id__c, Email, Phone, Birthdate, MailingStreet, MailingCity, MailingPostalCode, MailingState, MailingCountry
FROM Contact
WHERE Id = :cont.Id
WHERE Id IN :contacts AND DummyJSON_Id__c != null
];
System.assertEquals('[email protected]', cont.Email, 'Email does not match your value: ' + cont.Email);
System.assertEquals('+123456789', cont.Phone, 'Phone does not match your value: ' + cont.Phone);
System.assertEquals(
cont.Birthdate,
Date.valueOf('1990-01-01'),
'Birthdate does not match your value: ' + cont.Birthdate
);
Assert.isTrue(!contacts.isEmpty(), 'No contacts were created');

for (Contact cont : contacts) {
Assert.isTrue(cont.DummyJSON_Id__c.isNumeric(), 'DummyJSON_Id is not numeric string');
Integer dummyJSONId = Integer.valueOf(cont.DummyJSON_Id__c);
Assert.isTrue(
dummyJSONId >=0 && dummyJSONId <= 100,
'Expected range of DummyJSON_Id is [0-100]'
);
}
}
}
Loading