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
Empty file modified .forceignore
100755 → 100644
Empty file.
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ ehthumbs.db
$RECYCLE.BIN/

# Local environment variables
.env
.env

# Python Salesforce Functions
**/__pycache__/
**/.venv/
**/venv/
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm run precommit
10 changes: 0 additions & 10 deletions .prettierignore
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,10 +0,0 @@
# List files or directories below to ignore them when running prettier
# More information: https://prettier.io/docs/en/ignore.html
#

**/staticresources/**
.localdevserver
.sfdx
.vscode

coverage/
6 changes: 1 addition & 5 deletions .prettierrc
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
{
"plugins": ["@prettier/plugin-xml", "prettier-plugin-apex"],
"trailingComma": "all",
"arrowParens": "always",
"printWidth": 120,
"semi": true,
"trailingComma": "none",
"overrides": [
{
"files": "**/lwc/**/*.html",
Expand Down
44 changes: 11 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,18 @@
# Salesforce Senior Coding Challenge
# Salesforce DX Project: Next Steps

We appreciate you taking the time to participate and submit a coding challenge! 🥳
Now that you’ve created a Salesforce DX project, what’s next? Here are some documentation resources to get you started.

In the next step we would like you to implement a simple Invocable Apex Action to be used by your Admin colleagues for a Flow. They need to do HTTP callouts to a NPS Service, whenever an Order got fulfilled. Below you will find a list of tasks and optional bonus points required for completing the challenge.
## How Do You Plan to Deploy Your Changes?

**🚀 This is a template repo, just use the green button to create your own copy and get started!**
Do you want to deploy a set of changes, or create a self-contained application? Choose a [development model](https://developer.salesforce.com/tools/vscode/en/user-guide/development-models).

### Invocable:
## Configure Your Salesforce DX Project

* accepts the Order Record Ids as Input Parameter
* queries the required records to get the Bill To E-Mail Address (`Contact.Email`) and OrderNumber (`Order.OrderNumber`)
* sends the data to the NPS API
* add a basic Flow, that executes your Action whenever an Order Status is changed to `Fulfilled`
The `sfdx-project.json` file contains useful configuration information for your project. See [Salesforce DX Project Configuration](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm) in the _Salesforce DX Developer Guide_ for details about this file.

### The Mock NPS API:
## Read All About It

* Hosted at https://salesforce-coding-challenge.herokuapp.com
* ✨[API Documentation](https://thermondo.github.io/salesforce-coding-challenge/)
* 🔐 uses HTTP Basic Auth, username: `tmondo`, password: `Noy84LRpYvMZuETB`

### ⚠️ Must Haves:

* [ ] use `sfdx` and `git`, commit all code and metadata needed (so we can test with a scratch org)
* [ ] write good meaningful unit tests
* [ ] properly separate concerns
* [ ] make a list of limitations/possible problems

### ✨ Bonus Points:

* [ ] layer your Code (use [apex-common](https://github.com/apex-enterprise-patterns/fflib-apex-common) if you like)
* [ ] use Inversion of Control to write true unit tests and not integration tests
* [ ] make sure customers don't get duplicate emails
* [ ] think of error handling and return them to the Flow for the Admins to handle

### What if I don't finish?

Finishing these tasks should take about 2-3 hours, but we are all about **'quality > speed'**, so it's better to deliver a clean MVP and leave some TODOs open.

Try to produce something that is at least minimally functional. Part of the exercise is to see what you prioritize first when you have a limited amount of time. For any unfinished tasks, please do add `TODO` comments to your code with a short explanation. You will be given an opportunity later to go into more detail and explain how you would go about finishing those tasks.
- [Salesforce Extensions Documentation](https://developer.salesforce.com/tools/vscode/)
- [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm)
- [Salesforce DX Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_intro.htm)
- [Salesforce CLI Command Reference](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm)
19 changes: 3 additions & 16 deletions config/project-scratch-def.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
{
"orgName": "Thermondo Coding Challenge",
"edition": "Enterprise",
"features": ["EnableSetPasswordInApi", "PersonAccounts", "ServiceCloud", "FieldService:5"],
"orgName": "Demo company",
"edition": "Developer",
"features": ["EnableSetPasswordInApi"],
"settings": {
"lightningExperienceSettings": {
"enableS1DesktopEnabled": true
},
"mobileSettings": {
"enableS1EncryptedStoragePref2": false
},
"languageSettings": {
"enableTranslationWorkbench": true
},
"fieldServiceSettings": {
"enableWorkOrders": true
},
"omniChannelSettings": {
"enableOmniChannel": true,
"enableOmniSkillsRouting": true
},
"emailAdministrationSettings": {
"enableEnhancedEmailEnabled": true
}
}
}
8 changes: 8 additions & 0 deletions force-app/main/default/aura/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"plugins": ["@salesforce/eslint-plugin-aura"],
"extends": ["plugin:@salesforce/eslint-plugin-aura/recommended"],
"rules": {
"vars-on-top": "off",
"no-unused-expressions": "off"
}
}
19 changes: 19 additions & 0 deletions force-app/main/default/classes/NPSIntegration.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
public with sharing class NPSIntegration {

// Define a class to hold the result of the invocable action
public class InvocableResult {
@InvocableVariable(label='Result Message')
public String message;

@InvocableVariable(label='Success')
public Boolean success;

public InvocableResult(String message, Boolean success) {
this.message = message;
this.success = success;
}
public InvocableResult() {

}
}
}
5 changes: 5 additions & 0 deletions force-app/main/default/classes/NPSIntegration.cls-meta.xml
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>58.0</apiVersion>
<status>Active</status>
</ApexClass>
25 changes: 25 additions & 0 deletions force-app/main/default/classes/NPSIntegrationController.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
public class NPSIntegrationController {

private static List<NPSIntegration.InvocableResult> results = new List<NPSIntegration.InvocableResult>();

@InvocableMethod(label='Send Order Data to NPS API')
public static List<NPSIntegration.InvocableResult> sendOrderData(List<String> orderIds) {
try {
results = NPSIntegrationService.processOrders(orderIds);
} catch (Exception e) {
// Log the error
System.debug('Error: ' + e.getMessage());

// Create an InvocableResult for the error
results.add(new NPSIntegration.InvocableResult('Error: ' + e.getMessage(), false));
}
return results;
}

//TODO and Limitations
//Log errors for later analysis, and provide meaningful error messages to users when appropriate.
//Consider using custom exceptions to handle different error scenarios.
//Restrict the amount of data and resources code can consume.Common limits include SOQL queries (50,000 rows in a transaction),
// CPU time (6,000,000 milliseconds), and heap size (6 MB).
//Max 30 orders processed by NPS API
}
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>58.0</apiVersion>
<status>Active</status>
</ApexClass>
116 changes: 116 additions & 0 deletions force-app/main/default/classes/NPSIntegrationHandler.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
public with sharing class NPSIntegrationHandler {
public static List<NPSIntegration.InvocableResult> sendToNPSAPI(List<String> orderIds) {

Boolean success;
// Collect the Order Ids
Set<Id> orderIdsSet = new Set<Id>();

// Convert the List<String> to Set<Id>
for (String orderId : orderIds) {
orderIdsSet.add(orderId);
}

// Query Order and related Contact data
Map<Id, Order> ordersMap = new Map<Id, Order>([SELECT Id, OrderNumber, BillToContactId FROM Order WHERE Id IN :orderIdsSet]);
Set<Id> contactIds = new Set<Id>();
for (Order order : ordersMap.values()) {
contactIds.add(order.BillToContactId);
}
Map<Id, Contact> billToContactsMap = new Map<Id, Contact>([SELECT Id, Email FROM Contact WHERE Id IN :contactIds]);
List<NPSIntegration.InvocableResult> results = new List<NPSIntegration.InvocableResult>();
List<Id> markedAsSent = new List<ID>();

// Loop through the provided Order Ids
for (String orderId : orderIds) {
// Retrieve Order and related Contact data from the maps
Order orderRecord = ordersMap.get(orderId);
Contact billToContact = billToContactsMap.get(orderRecord.BillToContactId);

// Prepare data for the NPS API
String sfId = orderRecord.Id;
String orderNumber = orderRecord.OrderNumber;
String email = billToContact.Email;

// Check for duplicate emails
if (hasSentEmail(sfId)) {
// Log or handle duplicate email scenario
System.debug('Duplicate email detected for Order ' + orderNumber);
results.add(new NPSIntegration.InvocableResult('Email already processed for Order ' + orderNumber, false));
}
else{
success = sendToNPSAPInternal(orderNumber,email);
// Log the result
if (success) {
System.debug('Data sent to NPS API for Order ' );//+ orderNumber);
results.add(new NPSIntegration.InvocableResult('Data processed for Order '+orderNumber, success));
markedAsSent.add(sfId);
} else {
System.debug('Failed to send data to NPS API for Order ' + orderNumber);
results.add(new NPSIntegration.InvocableResult('Error: '+orderNumber , false));
}
}
}
if (markedAsSent.size()>0){
markEmailAsSent(markedAsSent);
}
return results;
}

private static Boolean sendToNPSAPInternal(String orderNumber, String email) {
// Define the NPS API endpoint from Named Credential
String npsApiEndpoint = 'callout:NPS_API';

// Construct request payload
String requestBody = '{"orderNumber":"' + orderNumber + '","email":"' + email + '"}';


// Set up the HTTP request
HttpRequest request = new HttpRequest();
request.setEndpoint(npsApiEndpoint);
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json');

// Set the request body
request.setBody(requestBody);

// Send the HTTP request
Http http = new Http();
HttpResponse response = http.send(request);

// Check the response status
if (response.getStatusCode() == 200) {
// The request was successful
return true;
} else {
// Log the error or handle it as needed
System.debug('Error sending data to NPS API. Status Code: ' + response.getStatusCode());
System.debug('Response Body: ' + response.getBody());
return false;
}
}

private static Boolean hasSentEmail(String orderId) {
//Query the custom object to check if there's a record with the given orderId
Integer count = 0;
count = [SELECT COUNT() FROM NPS_Order_Email__c WHERE Order__c = :orderId WITH SECURITY_ENFORCED];

// If count is greater than 0, it means an email has been sent
return count > 0;
}

@future
private static void markEmailAsSent(List<Id> orderId) {
//Create a new record in the custom object to mark the email as sent
List<NPS_Order_Email__c> emailRecords = new List<NPS_Order_Email__c>();
for(Id orderNo : orderId){
if (!Schema.sObjectType.NPS_Order_Email__c.fields.Order__c.isCreateable()){
System.debug('Insufficient Permissions');
}
else{
NPS_Order_Email__c emailSentRecord = new NPS_Order_Email__c(Order__c = orderNo,Name = orderNo);
emailRecords.add(emailSentRecord);
}
}
insert emailRecords;
}
}
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>58.0</apiVersion>
<status>Active</status>
</ApexClass>
7 changes: 7 additions & 0 deletions force-app/main/default/classes/NPSIntegrationService.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public class NPSIntegrationService {
public static List<NPSIntegration.InvocableResult> processOrders(List<String> orderIds) {
List<NPSIntegration.InvocableResult> results = new List<NPSIntegration.InvocableResult>();
results = NPSIntegrationHandler.sendToNPSAPI(orderIds);
return results;
}
}
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>58.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading