diff --git a/ftp-edi-message-to-salesforce-opportunity/main.bal b/ftp-edi-message-to-salesforce-opportunity/main.bal index 88bb6cc..aa491f7 100644 --- a/ftp-edi-message-to-salesforce-opportunity/main.bal +++ b/ftp-edi-message-to-salesforce-opportunity/main.bal @@ -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; @@ -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 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 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 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 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 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 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 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 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 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); } - diff --git a/ftp-edi-message-to-salesforce-opportunity/transformer.bal b/ftp-edi-message-to-salesforce-opportunity/transformer.bal new file mode 100644 index 0000000..204fcae --- /dev/null +++ b/ftp-edi-message-to-salesforce-opportunity/transformer.bal @@ -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; +} \ No newline at end of file diff --git a/gmail-to-salesforce-lead/main.bal b/gmail-to-salesforce-lead/main.bal index ece6646..4f6c2fc 100644 --- a/gmail-to-salesforce-lead/main.bal +++ b/gmail-to-salesforce-lead/main.bal @@ -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 { @@ -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 { @@ -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) { @@ -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; } @@ -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); diff --git a/gmail-to-salesforce-lead/types.bal b/gmail-to-salesforce-lead/types.bal new file mode 100644 index 0000000..2e10e69 --- /dev/null +++ b/gmail-to-salesforce-lead/types.bal @@ -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; +|}; \ No newline at end of file