Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add servicenow-salesforce and shopify-salesforce samples. #39

Merged
merged 14 commits into from
Jan 4, 2024
Merged
165 changes: 66 additions & 99 deletions ftp-edi-message-to-salesforce-opportunity/main.bal
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ballerina/file;
import ballerina/ftp;
import ballerina/io;
import ballerina/log;

import ballerinax/edifact.d03a.retail.mREQOTE;
import ballerinax/salesforce as sf;
Expand All @@ -11,124 +12,90 @@ configurable string ftpProcessedQuotesPath = ?;
configurable sf:ConnectionConfig salesforceConfig = ?;
configurable string salesforcePriceBookId = ?;

public function main() returns error? {
sf:Client salesforce = check new (salesforceConfig);
ftp:Client fileServer = check new ftp:Client(ftpConfig);
final ftp:Client fileServer = check new (ftpConfig);
final sf:Client salesforce = check new (salesforceConfig);

// Get new quotes from the FTP new quotes directory, and iterate through them.
public function main() returns error? {
ftp:FileInfo[] quoteList = check fileServer->list(ftpNewQuotesPath);
foreach ftp:FileInfo quoteFile in quoteList {
if !quoteFile.name.endsWith(".edi") {
string|error quoteText = getFileText(quoteFile);
if quoteText is error {
log:printError(quoteText.message());
continue;
}

// Fetch the EDI file containing the quote from the FTP server.
stream<byte[] & readonly, io:Error?> fileStream = check fileServer->get(quoteFile.path);
string quoteText = check streamToString(fileStream);

// Parse the EDI file and transform in to Ballerina record containing only the required data.
mREQOTE:EDI_REQOTE_Request_for_quote_message quote = check mREQOTE:fromEdiString(quoteText);
QuoteRequest quoteRequest = check transformQuoteRequest(quote);

// Get the corresponding account Id and oppurtunity Id from Salesforce.
// Create a new opportunity if an opportunity with the given name does not exist.
stream<Id, error?> accQuery = check salesforce->query(
string `SELECT Id FROM Account WHERE Name = '${quoteRequest.accountName}'`);
record {|Id value;|}? account = check accQuery.next();
check accQuery.close();
if account is () {
return error("Account not found. Account name: " + quoteRequest.accountName);
}
Opportunity opp = {
Name: quoteRequest.oppName,
AccountId: account.value.Id,
Pricebook2Id: salesforcePriceBookId
};
string oppId = "";
stream<Id, error?> oppQuery = check salesforce->query(
string `SELECT Id FROM Opportunity WHERE Name = '${quoteRequest.oppName}'`);
record {|Id value;|}? existingOpp = check oppQuery.next();
check oppQuery.close();
if existingOpp is () {
sf:CreationResponse oppResult = check salesforce->create("Opportunity", opp);
oppId = oppResult.id;
} else {
oppId = existingOpp.value.Id;
}

// Create opportunity line items for each item in the quote.
foreach ItemData item in quoteRequest.itemData {
stream<PriceBookEntry, error?> query = check salesforce->query(string `SELECT UnitPrice FROM PricebookEntry WHERE Pricebook2Id = '01s6C000000UN4PQAW' AND Product2Id = '${item.itemId}'`);
record {|PriceBookEntry value;|}? unionResult = check query.next();
check query.close();
if unionResult is () {
return error(string `Pricebook entry not found. Opportunity name: ${quoteRequest.oppName}, Item ID: ${item.itemId}`);
}
OpportunityProduct oppProduct = {
OpportunityId: oppId,
Product2Id: item.itemId,
Quantity: item.quantity,
UnitPrice: unionResult.value.UnitPrice
};
_ = check salesforce->create("OpportunityLineItem", oppProduct);
string|error accountId = getSalesforceAccountId(quoteRequest.accountName);
if accountId is error {
log:printError(accountId.message());
continue;
}

// Move the processed quote to the processed quotes FTP directory.
check fileServer->put(check file:joinPath(ftpProcessedQuotesPath, quoteFile.name), quoteText.toBytes());
check fileServer->delete(quoteFile.path);
string oppId = check getSalesforceOpportunityId(accountId, quoteRequest.oppName);
check createLineItems(quoteRequest.itemData, oppId);
check moveProcessedFile(quoteFile, quoteText);
}
}

function transformQuoteRequest(mREQOTE:EDI_REQOTE_Request_for_quote_message quote) returns QuoteRequest|error {
QuoteRequest quoteRequest = {accountName: "", oppName: ""};
mREQOTE:Segment_group_1_GType[] segmentGroup1 = quote.Segment_group_1;
foreach mREQOTE:Segment_group_1_GType ref in segmentGroup1 {
if ref.REFERENCE.REFERENCE.Reference_code_qualifier == "AES" {
string? oppId = ref.REFERENCE.REFERENCE.Reference_identifier;
if oppId is () {
return error("Opportunity ID is not given");
}
quoteRequest.oppName = oppId;
}
function getSalesforceAccountId(string accountName) returns string|error {
stream<Id, error?> accQuery = check salesforce->query(
string `SELECT Id FROM Account WHERE Name = '${accountName}'`);
record {|Id value;|}? account = check accQuery.next();
check accQuery.close();
if account is () {
return error("Account not found. Account name: " + accountName);
}
mREQOTE:Segment_group_11_GType[] segmentGroup11 = quote.Segment_group_11;
foreach mREQOTE:Segment_group_11_GType party in segmentGroup11 {
if party.NAME_AND_ADDRESS.Party_function_code_qualifier == "BY" {
string? prospectId = party.NAME_AND_ADDRESS?.PARTY_IDENTIFICATION_DETAILS?.Party_identifier;
if prospectId is () {
return error("Prospect identifier not available in quote.");
}
quoteRequest.accountName = prospectId;
}
return account.value.Id;
}

function getSalesforceOpportunityId(string accountId, string oppName) returns string|error {
stream<Id, error?> oppQuery = check salesforce->query(
string `SELECT Id FROM Opportunity WHERE Name = '${oppName}'`);
record {|Id value;|}? existingOpp = check oppQuery.next();
check oppQuery.close();
if existingOpp !is () {
return existingOpp.value.Id;
}
mREQOTE:Segment_group_27_GType[] items = quote.Segment_group_27;
foreach mREQOTE:Segment_group_27_GType item in items {
string? itemId = item.LINE_ITEM.Line_item_identifier;
if itemId is () {
return error("Item ID is not given");
}
ItemData itemData = {itemId};
mREQOTE:QUANTITY_Type[] quantities = item.QUANTITY;
foreach mREQOTE:QUANTITY_Type quantity in quantities {
if quantity.QUANTITY_DETAILS.Quantity_type_code_qualifier == "21" {
int|error amount = int:fromString(quantity.QUANTITY_DETAILS.Quantity);
if amount is error {
return error("Quantity must be a valid number.");
}
itemData.quantity = amount;
break;
}
Opportunity opp = {
Name: oppName,
AccountId: accountId,
Pricebook2Id: salesforcePriceBookId
};
sf:CreationResponse oppResult = check salesforce->create("Opportunity", opp);
return oppResult.id;
}

function createLineItems(ItemData[] items, string oppId) returns error? {
foreach ItemData item in items {
stream<PriceBookEntry, error?> query = check salesforce->query(
string `SELECT UnitPrice FROM PricebookEntry WHERE Pricebook2Id = '01s6C000000UN4PQAW' AND Product2Id = '${item.itemId}'`);
record {|PriceBookEntry value;|}? unionResult = check query.next();
check query.close();
if unionResult is () {
return error(string `Pricebook entry not found. Opportunity: ${oppId}, Item ID: ${item.itemId}`);
}
quoteRequest.itemData.push(itemData);
OpportunityProduct oppProduct = {
OpportunityId: oppId,
Product2Id: item.itemId,
Quantity: item.quantity,
UnitPrice: unionResult.value.UnitPrice
};
_ = check salesforce->create("OpportunityLineItem", oppProduct);
}
return quoteRequest;
}

function streamToString(stream<byte[] & readonly, io:Error?> inStream) returns string|error {
function moveProcessedFile(ftp:FileInfo quoteFile, string quoteText) returns error? {
check fileServer->put(check file:joinPath(ftpProcessedQuotesPath, quoteFile.name), quoteText.toBytes());
check fileServer->delete(quoteFile.path);
}

function getFileText(ftp:FileInfo quoteFile) returns string|error {
if !quoteFile.name.endsWith(".edi") {
return error("Invalid file type. File name: " + quoteFile.name);
}
stream<byte[] & readonly, io:Error?> fileStream = check fileServer->get(quoteFile.path);
byte[] content = [];
check inStream.forEach(function (byte[] & readonly chunk) {
check fileStream.forEach(function(byte[] & readonly chunk) {
content.push(...chunk);
});
return string:fromBytes(content);
}

45 changes: 45 additions & 0 deletions ftp-edi-message-to-salesforce-opportunity/transformer.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ballerinax/edifact.d03a.retail.mREQOTE;
chathurace marked this conversation as resolved.
Show resolved Hide resolved
function transformQuoteRequest(mREQOTE:EDI_REQOTE_Request_for_quote_message quote) returns QuoteRequest|error {
QuoteRequest quoteRequest = {accountName: "", oppName: ""};
mREQOTE:Segment_group_1_GType[] segmentGroup1 = quote.Segment_group_1;
foreach mREQOTE:Segment_group_1_GType ref in segmentGroup1 {
if ref.REFERENCE.REFERENCE.Reference_code_qualifier == "AES" {
string? oppId = ref.REFERENCE.REFERENCE.Reference_identifier;
if oppId is () {
return error("Opportunity ID is not given");
}
quoteRequest.oppName = oppId;
}
}
mREQOTE:Segment_group_11_GType[] segmentGroup11 = quote.Segment_group_11;
foreach mREQOTE:Segment_group_11_GType party in segmentGroup11 {
if party.NAME_AND_ADDRESS.Party_function_code_qualifier == "BY" {
string? prospectId = party.NAME_AND_ADDRESS?.PARTY_IDENTIFICATION_DETAILS?.Party_identifier;
if prospectId is () {
return error("Prospect identifier not available in quote.");
}
quoteRequest.accountName = prospectId;
}
}
mREQOTE:Segment_group_27_GType[] items = quote.Segment_group_27;
foreach mREQOTE:Segment_group_27_GType item in items {
string? itemId = item.LINE_ITEM.Line_item_identifier;
if itemId is () {
return error("Item ID is not given");
}
ItemData itemData = {itemId};
mREQOTE:QUANTITY_Type[] quantities = item.QUANTITY;
foreach mREQOTE:QUANTITY_Type quantity in quantities {
if quantity.QUANTITY_DETAILS.Quantity_type_code_qualifier == "21" {
int|error amount = int:fromString(quantity.QUANTITY_DETAILS.Quantity);
if amount is error {
return error("Quantity must be a valid number.");
}
itemData.quantity = amount;
break;
}
}
quoteRequest.itemData.push(itemData);
}
return quoteRequest;
}
chathurace marked this conversation as resolved.
Show resolved Hide resolved
51 changes: 14 additions & 37 deletions gmail-to-salesforce-lead/main.bal
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,23 @@ import ballerinax/googleapis.gmail;
import ballerinax/openai.chat;
import ballerinax/salesforce as sf;

type Email record {|
string 'from;
string subject;
string body;
|};

type Name record {|
string firstName__c;
string lastName__c;
|};

type Lead record {|
*Name;
string email__c;
string phoneNumber__c;
string company__c;
string designation__c;
|};

configurable string gmailAccessToken = ?;
configurable string openAIKey = ?;
configurable string salesforceBaseUrl = ?;
configurable string salesforceAccessToken = ?;

const LABEL = "Lead";

final gmail:Client gmail = check new ({auth: {token: gmailAccessToken}});
final chat:Client openAiChat = check new ({auth: {token: openAIKey}});
final sf:Client salesforce = check new ({baseUrl: salesforceBaseUrl, auth: {token: salesforceAccessToken}});

public function main() returns error? {
while true {
Email[] emails = check getEmails(LABEL);
Lead[] leads = from Email email in emails
let Lead? lead = generateLead(email)
where lead is Lead
select lead;
addLeadsToSalesforce(leads);
runtime:sleep(600);
}
Email[] emails = check getEmails(LABEL);
Lead[] leads = from Email email in emails
let Lead? lead = generateLead(email)
where lead is Lead
select lead;
addLeadsToSalesforce(leads);
}

function getEmails(string label) returns Email[]|error {
Expand All @@ -58,16 +35,16 @@ function getEmails(string label) returns Email[]|error {
gmail:Message[] matchingEmails = getMatchingEmails(gmail, matchingMailThreads);

return from gmail:Message message in matchingEmails
let Email|error email = parseEmail(message)
where email is Email
select email;
let Email|error email = parseEmail(message)
where email is Email
select email;
}

function getLabelIds(gmail:Client gmail, string[] labelsToMatch) returns string[]|error {
gmail:LabelList labelList = check gmail->listLabels("me");
return from gmail:Label {name, id} in labelList.labels
where labelsToMatch.indexOf(name) != ()
select id;
where labelsToMatch.indexOf(name) != ()
select id;
}

function getMatchingMailThreads(gmail:Client gmail, string[] labelIdsToMatch) returns gmail:MailThread[]|error {
Expand All @@ -77,7 +54,7 @@ function getMatchingMailThreads(gmail:Client gmail, string[] labelIdsToMatch) re
};

return from gmail:MailThread mailThread in check gmail->listThreads(filter = searchFilter)
select mailThread;
select mailThread;
}

function removeLabels(gmail:Client gmail, gmail:MailThread[] mailThreads, string[] labelIds) {
Expand All @@ -96,7 +73,7 @@ function getMatchingEmails(gmail:Client gmail, gmail:MailThread[] mailThreads) r
foreach gmail:MailThread mailThread in mailThreads {
gmail:MailThread|error response = gmail->readThread(mailThread.id);
if response is error {
log:printError("An error occured while reading the email.",
log:printError("An error occured while reading the email.",
response, response.stackTrace(), threadId = mailThread.id);
continue;
}
Expand Down Expand Up @@ -175,7 +152,7 @@ function addLeadsToSalesforce(Lead[] leads) {
do {
sf:CreationResponse|error createResponse = salesforce->create("EmailLead__c", lead);
if createResponse is error {
log:printError("An error occured while creating a Lead object on salesforce.",
log:printError("An error occured while creating a Lead object on salesforce.",
createResponse, createResponse.stackTrace(), lead = lead);
} else {
log:printInfo("Lead successfully created.", lead = lead);
Expand Down
18 changes: 18 additions & 0 deletions gmail-to-salesforce-lead/types.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
type Email record {|
string 'from;
string subject;
string body;
|};

type Name record {|
string firstName__c;
string lastName__c;
|};

type Lead record {|
*Name;
string email__c;
string phoneNumber__c;
string company__c;
string designation__c;
|};
4 changes: 4 additions & 0 deletions servicenow-case-to-salesforce-case/.devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"image": "ballerina/ballerina-devcontainer:2201.7.2",
"extensions": ["WSO2.ballerina"],
}
3 changes: 3 additions & 0 deletions servicenow-case-to-salesforce-case/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target
generated
Config.toml
8 changes: 8 additions & 0 deletions servicenow-case-to-salesforce-case/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
org = "integration_samples"
name = "servicenow_case_to_salesforce_case"
version = "0.1.0"
distribution = "2201.7.2"

[build-options]
observabilityIncluded = true
Loading