From Tamar Erlich: Powerful new OpportunityPartner Solutions using Flow with Apex-Defined Types
I was recently tasked with a requirement to display a list of opportunity partners on a custom object related to opportunity. I planned to use a flow to get the opportunity partner records and then display them using the Datatablev2 Flow Screen Component available on this site. When I tried to create the flow, I encountered some issues:
1. The Opportunity Partners object is a legacy object and cannot be customized. I could not create formulas for getting the account name or link to the account
2. I could not use dot notation to get the account name from the account ID in the get records component in the flow
3. I could not use the partner account ID as a filter criteria on a new get records element to get the account records.

I knew I had to create a custom way to get the data I needed to display. Eric Smith referred me to his blog post on how to use Apex defined data types and an invocable action to create data for the datatable component.
How to Use an Apex-Defined Object with my Datatable Flow Component
I create my Apex defined data type for opportunity partners.
/**
* @author : Tamar Erlich
* @date : October 07, 2020
* @description : Wrapper class for opportunity partners
* Note : This class is called by the GetOpportunityPartnersAction.
* *************************************************
* <runTest><runTest>
* *************************************************
* @History
* -------
* VERSION | AUTHOR | DATE | DESCRIPTION
* 1.0 | Tamar Erlich | October 07, 2020 | Created
**/
public with sharing class OpportunityPartnersWrapper {
// @AuraEnabled annotation exposes the methods to Lightning Components and Flows
@AuraEnabled
public String accountName;
@AuraEnabled
public String partnerRole;
@AuraEnabled
public Boolean isPrimary;
@AuraEnabled
public String accountLink;
// Define the structure of the Apex-Defined Variable
public OpportunityPartnersWrapper(
String accountName,
String partnerRole,
Boolean isPrimary,
String accountLink
) {
this.accountName = accountName;
this.partnerRole = partnerRole;
this.isPrimary = isPrimary;
this.accountLink = accountLink;
}
// Required no-argument constructor
public OpportunityPartnersWrapper() {}
}
I then created an invocable action that received the opportunity and account IDs and returns a JSON string that can be displayed in my flow table.
/**
* @author : Tamar Erlich
* @date : October 07, 2020
* @description : Invocable method that given opportunityId and accountId, returns a list of opportunity partners to display on opportunity plan record page
* Note : This class is called by the Flow.
* *************************************************
* <runTest>GetOpportunityPartnersActionTest<runTest>
* *************************************************
* @History
* -------
* VERSION | AUTHOR | DATE | DESCRIPTION
* 1.0 | Tamar Erlich | October 07, 2020 | Created
**/
global with sharing class GetOpportunityPartnersAction {
// Expose this Action to the Flow
@InvocableMethod
global static List<Results> get(List<Requests> requestList) {
// initialize variables
Results response = new Results();
List<Results> responseWrapper = new List<Results>();
String errors;
String success = 'true';
String stringOutput;
List<OpportunityPartner> opportunityPartners = new List<OpportunityPartner>();
List<OpportunityPartnersWrapper> opportunityPartnersSet = new List<OpportunityPartnersWrapper>();
Set<Id> setOppIds = new Set<Id>();
Set<Id> setAccIDs = new Set<Id>();
Map<Id, List<OpportunityPartner>> mapOppOppPartners = new Map<Id, List<OpportunityPartner>>();
// create sets of Ids to use as bind variables in the query
for (Requests rq : requestList) {
setOppIds.add(rq.opportunityId);
setAccIds.add(rq.accountId);
}
// query all request opportunities and their partners and create a map with opportunityId as the key and a list of parrtners as the values
for (Opportunity opp : [
SELECT
Id,
(
SELECT AccountTo.Name, IsPrimary, Role
FROM OpportunityPartnersFrom
WHERE OpportunityId = :setOppIds AND AccountToId != :setAccIds
)
FROM Opportunity
]) {
mapOppOppPartners.put(opp.Id, opp.OpportunityPartnersFrom);
}
for (Requests curRequest : requestList) {
String accountId = curRequest.accountId;
String opportunityId = curRequest.opportunityId;
try {
if (accountId == null || opportunityId == null) {
throw new InvocableActionException(
'When using the GetOpportunityPartners action, you need to provide BOTH an Account Id AND an Opportunity Id'
);
}
if (!accountId.startsWith('001') || !opportunityId.startsWith('006')) {
throw new InvocableActionException(
'Invalid Account or Opportunity ID'
);
}
// get the list of opportunity partners matching the current opportunityId from the map
if (accountId != null && opportunityId != null) {
opportunityPartners = mapOppOppPartners.get(opportunityId);
}
// populate the Apex defined wrapper type with opportunity partner information
for (opportunityPartner op : opportunityPartners) {
OpportunityPartnersWrapper opw = new OpportunityPartnersWrapper();
opw.accountName = op.AccountTo.Name;
opw.partnerRole = op.Role;
opw.isPrimary = op.IsPrimary;
opw.accountLink = '/' + op.AccountToId;
opportunityPartnersSet.add(opw);
}
// Convert Record Collection to Serialized String
stringOutput = JSON.serialize(opportunityPartnersSet);
} catch (InvocableActionException e) {
System.debug('exception occured: ' + e.getMessage());
errors = e.getMessage();
success = 'false - custom exception occured';
} catch (exception ex) {
System.debug('exception occured: ' + ex.getMessage());
errors = ex.getMessage();
success = 'false - exception occured';
}
// Prepare the response to send back to the Flow
// Set Output Values
response.errors = errors;
response.successful = success;
response.outputCollection = opportunityPartnersSet;
response.outputString = stringOutput;
responseWrapper.add(response);
}
// Return values back to the Flow
return responseWrapper;
}
// Attributes passed in from the Flow
global class Requests {
@InvocableVariable(label='Input the Related OpportunityId' required=true)
global String opportunityId;
@InvocableVariable(label='Input the Opportunity\'s AccountId' required=true)
global String accountId;
}
// Attributes passed back to the Flow
global class Results {
@InvocableVariable
global String errors;
@InvocableVariable
global String successful;
@InvocableVariable
public List<OpportunityPartnersWrapper> outputCollection;
@InvocableVariable
public String outputString;
}
// custom exception class
global class InvocableActionException extends Exception {
}
}
/**
* @author : Tamar Erlich
* @date : October 07, 2020
* @description : Test class for GetOpportunityPartnersAction Invocable method
* Note :
* *************************************************
* <runTest>GetOpportunityPartnersActionTest<runTest>
* *************************************************
* @History
* -------
* VERSION | AUTHOR | DATE | DESCRIPTION
* 1.0 | Tamar Erlich | October 07, 2020 | created
**/
@IsTest
public with sharing class GetOpportunityPartnersActionTest {
@isTest
public static void opportunityPartnersFound(){
// initialize variables
List<GetOpportunityPartnersAction.Requests> requestList = new List<GetOpportunityPartnersAction.Requests>();
String accountId;
String opportunityId;
// create test account
List<Account> accounts = new List<Account>();
for (Integer j = 0; j < 1; j++) {
Account a = new Account(
Name = 'email' + j + '.com'
);
accounts.add(a);
}
insert accounts;
// create test opportunity
Date closeDate = System.today();
Opportunity testOpp;
testOpp = new Opportunity(
Name = 'test opp',
StageName = 'Open In-Progress',
Amount = 100,
CloseDate = closeDate
);
insert testOpp;
accountId = accounts[0].Id;
opportunityId = testOpp.Id;
// create test opportunity partner
OpportunityPartner testPartner;
testPartner = new OpportunityPartner(
OpportunityId = opportunityId,
AccountToId = accountId,
Role = 'Dealer',
IsPrimary = false
);
insert testPartner;
// prepare request for GetOpportunityPartnersAction
GetOpportunityPartnersAction.Requests request = new GetOpportunityPartnersAction.Requests();
request.accountId = accountId;
request.opportunityId = opportunityId;
requestList.add(request);
// run test and assert results
List <GetOpportunityPartnersAction.Results> results = GetOpportunityPartnersAction.get(requestList);
System.assertEquals(results[0].errors, null,'Errors not expected');
System.assertNotEquals(results[0].successful, 'false', 'Success expected');
System.assertEquals(true, results[0].outputCollection.size()>0, 'Opportunity Partners expected');
}
}
I then used my new action in the flow to get the opportunity partner records.

I then configured my datatable on the flow screen




Some notes on the datatable configuration:
1. User Defined had to bet set to true
2. The Datatable Record String must be set to the variable returned from the action
3. The column field types must be set for all columns. I set the first column to the url type
4. My first column has special type attributes for displaying as a clickable link: 1:{label: { fieldName: accountName}, target: ‘_blank’}
5. Since this is for display only, I hid the checkbox columns
6. I used the new features available in v2.46 to apply an icon and title to the table
The View All link uses a formula to create the link

Here is the entire flow

And here is the end result

This use case can be easily adapted to display other opportunity related information like opportunity team members or opportunity contact roles.