Skip to content

Commit

Permalink
Do some minor refactorings.
Browse files Browse the repository at this point in the history
  • Loading branch information
chathurace committed Oct 17, 2023
1 parent 0ff67db commit c731306
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 136 deletions.
164 changes: 65 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,89 @@ 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);
ftp:Client fileServer = check new ftp:Client(ftpConfig);
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;
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;
}
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;
|};

0 comments on commit c731306

Please sign in to comment.