@salesforce/afv-skills 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -415
- package/package.json +5 -3
- package/skills/building-ui-bundle-app/SKILL.md +325 -0
- package/skills/building-ui-bundle-frontend/SKILL.md +122 -0
- package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/component.md +1 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +169 -0
- package/skills/creating-b2b-commerce-store/references/store-vs-storefront.md +169 -0
- package/skills/deploying-ui-bundle/SKILL.md +77 -0
- package/skills/generating-apex/CREDITS.md +30 -0
- package/skills/generating-apex/SKILL.md +399 -0
- package/skills/generating-apex/assets/abstract.cls +132 -0
- package/skills/generating-apex/assets/batch.cls +125 -0
- package/skills/generating-apex/assets/domain.cls +102 -0
- package/skills/generating-apex/assets/dto.cls +108 -0
- package/skills/generating-apex/assets/exception.cls +51 -0
- package/skills/generating-apex/assets/interface.cls +25 -0
- package/skills/generating-apex/assets/invocable.cls +115 -0
- package/skills/generating-apex/assets/queueable.cls +92 -0
- package/skills/generating-apex/assets/rest-resource.cls +300 -0
- package/skills/generating-apex/assets/schedulable.cls +75 -0
- package/skills/generating-apex/assets/selector.cls +92 -0
- package/skills/generating-apex/assets/service.cls +69 -0
- package/skills/generating-apex/assets/trigger.cls +45 -0
- package/skills/generating-apex/assets/utility.cls +97 -0
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
- package/skills/generating-apex/references/AccountSelector.cls +193 -0
- package/skills/generating-apex/references/AccountService.cls +201 -0
- package/skills/generating-apex-test/CREDITS.md +30 -0
- package/skills/generating-apex-test/SKILL.md +199 -0
- package/skills/generating-apex-test/assets/test-class-template.cls +93 -0
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +111 -0
- package/skills/generating-apex-test/references/assertion-patterns.md +108 -0
- package/skills/generating-apex-test/references/async-testing.md +193 -0
- package/skills/generating-apex-test/references/mocking-patterns.md +220 -0
- package/skills/generating-apex-test/references/test-data-factory.md +75 -0
- package/skills/generating-experience-react-site/SKILL.md +20 -9
- package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md +1 -1
- package/skills/generating-flexipage/SKILL.md +58 -60
- package/skills/generating-ui-bundle-features/SKILL.md +45 -0
- package/skills/generating-ui-bundle-metadata/SKILL.md +106 -0
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/SKILL.md +5 -5
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/constraints.md +2 -2
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/examples.md +1 -1
- package/skills/{implementing-webapp-file-upload → implementing-ui-bundle-file-upload}/SKILL.md +11 -11
- package/skills/searching-media/SKILL.md +342 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data}/SKILL.md +52 -25
- package/skills/using-ui-bundle-salesforce-data/references/mutation-query-generation.md +140 -0
- package/skills/using-ui-bundle-salesforce-data/references/query-testing.md +78 -0
- package/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md +307 -0
- package/skills/using-ui-bundle-salesforce-data/references/schema-introspection.md +53 -0
- package/skills/using-ui-bundle-salesforce-data/references/ui-bundle-integration.md +221 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data/scripts}/graphql-search.sh +75 -23
- package/skills/building-webapp-data-visualization/SKILL.md +0 -72
- package/skills/building-webapp-data-visualization/implementation/bar-line-chart.md +0 -316
- package/skills/building-webapp-data-visualization/implementation/dashboard-layout.md +0 -189
- package/skills/building-webapp-data-visualization/implementation/donut-chart.md +0 -181
- package/skills/building-webapp-data-visualization/implementation/stat-card.md +0 -150
- package/skills/building-webapp-react-components/SKILL.md +0 -96
- package/skills/configuring-webapp-csp-trusted-sites/SKILL.md +0 -90
- package/skills/configuring-webapp-metadata/SKILL.md +0 -158
- package/skills/creating-webapp/SKILL.md +0 -140
- package/skills/deploying-webapp-to-salesforce/SKILL.md +0 -226
- package/skills/installing-webapp-features/SKILL.md +0 -210
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/header-footer.md +0 -0
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/page.md +0 -0
- /package/skills/{configuring-webapp-csp-trusted-sites/implementation/metadata-format.md → generating-ui-bundle-metadata/implementation/csp-metadata-format.md} +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/style-tokens.md +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/troubleshooting.md +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility class for {describe the category of utilities: String, Date, Collection, etc.}.
|
|
3
|
+
* All methods are static and side-effect-free (no SOQL, no DML).
|
|
4
|
+
* Private constructor prevents instantiation.
|
|
5
|
+
* @author Generated by Apex Class Writer Skill
|
|
6
|
+
*/
|
|
7
|
+
public with sharing class {ClassName} {
|
|
8
|
+
|
|
9
|
+
// ─── Private Constructor ─────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Prevents instantiation — use static methods only
|
|
13
|
+
*/
|
|
14
|
+
@TestVisible
|
|
15
|
+
private {ClassName}() {
|
|
16
|
+
// Utility class — do not instantiate
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ─── Public Methods ──────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
// TODO: Add utility methods below. Examples:
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Safely converts a String to an Integer, returning a default if parsing fails
|
|
25
|
+
* @param value The String to parse
|
|
26
|
+
* @param defaultValue The fallback value if parsing fails
|
|
27
|
+
* @return The parsed Integer or the default value
|
|
28
|
+
* @example
|
|
29
|
+
* Integer result = {ClassName}.safeParseInteger('42', 0); // returns 42
|
|
30
|
+
* Integer result = {ClassName}.safeParseInteger('abc', 0); // returns 0
|
|
31
|
+
*/
|
|
32
|
+
public static Integer safeParseInteger(String value, Integer defaultValue) {
|
|
33
|
+
if (String.isBlank(value)) {
|
|
34
|
+
return defaultValue;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return Integer.valueOf(value.trim());
|
|
38
|
+
} catch (TypeException e) {
|
|
39
|
+
return defaultValue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Chunks a list into smaller sublists of the specified size.
|
|
45
|
+
* Useful for processing records in governor-limit-safe batches.
|
|
46
|
+
* @param items The list to chunk
|
|
47
|
+
* @param chunkSize The maximum size of each chunk
|
|
48
|
+
* @return A list of sublists, each containing up to chunkSize elements
|
|
49
|
+
* @example
|
|
50
|
+
* List<List<String>> chunks = {ClassName}.chunkList(myList, 200);
|
|
51
|
+
*/
|
|
52
|
+
public static List<List<Object>> chunkList(List<Object> items, Integer chunkSize) {
|
|
53
|
+
List<List<Object>> chunks = new List<List<Object>>();
|
|
54
|
+
if (items == null || items.isEmpty() || chunkSize <= 0) {
|
|
55
|
+
return chunks;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
List<Object> currentChunk = new List<Object>();
|
|
59
|
+
for (Object item : items) {
|
|
60
|
+
currentChunk.add(item);
|
|
61
|
+
if (currentChunk.size() == chunkSize) {
|
|
62
|
+
chunks.add(currentChunk);
|
|
63
|
+
currentChunk = new List<Object>();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!currentChunk.isEmpty()) {
|
|
68
|
+
chunks.add(currentChunk);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return chunks;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extracts a Set of non-null field values from a list of SObjects
|
|
76
|
+
* @param records The SObject records to extract from
|
|
77
|
+
* @param fieldName The API name of the field to extract
|
|
78
|
+
* @return A Set of non-null String values
|
|
79
|
+
* @example
|
|
80
|
+
* Set<String> emails = {ClassName}.pluckStrings(contacts, 'Email');
|
|
81
|
+
*/
|
|
82
|
+
public static Set<String> pluckStrings(List<SObject> records, String fieldName) {
|
|
83
|
+
Set<String> values = new Set<String>();
|
|
84
|
+
if (records == null || String.isBlank(fieldName)) {
|
|
85
|
+
return values;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (SObject record : records) {
|
|
89
|
+
Object val = record.get(fieldName);
|
|
90
|
+
if (val != null) {
|
|
91
|
+
values.add(String.valueOf(val));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return values;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch Apex class for identifying and flagging duplicate Account records.
|
|
3
|
+
* Compares Accounts by Name and BillingPostalCode to find potential duplicates.
|
|
4
|
+
* Flags duplicates by setting the Is_Potential_Duplicate__c checkbox.
|
|
5
|
+
* Implements Database.Stateful to track results across batch chunks.
|
|
6
|
+
* @author Generated by Apex Class Writer Skill
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Run with default batch size (200)
|
|
10
|
+
* Id jobId = AccountDeduplicationBatch.run();
|
|
11
|
+
*
|
|
12
|
+
* // Run with smaller batch size for complex processing
|
|
13
|
+
* Database.executeBatch(new AccountDeduplicationBatch(), 50);
|
|
14
|
+
*/
|
|
15
|
+
public with sharing class AccountDeduplicationBatch implements Database.Batchable<SObject>, Database.Stateful {
|
|
16
|
+
|
|
17
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
18
|
+
private static final Integer DEFAULT_BATCH_SIZE = 200;
|
|
19
|
+
|
|
20
|
+
// ─── Stateful Tracking ───────────────────────────────────────────────
|
|
21
|
+
private Integer totalScanned = 0;
|
|
22
|
+
private Integer duplicatesFound = 0;
|
|
23
|
+
private Integer totalErrors = 0;
|
|
24
|
+
private List<String> errorMessages = new List<String>();
|
|
25
|
+
|
|
26
|
+
// ─── Batchable Interface ─────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Queries all active Accounts that haven't already been flagged
|
|
30
|
+
* @param bc The batch context
|
|
31
|
+
* @return QueryLocator scoped to unflagged active Accounts
|
|
32
|
+
*/
|
|
33
|
+
public Database.QueryLocator start(Database.BatchableContext bc) {
|
|
34
|
+
return Database.getQueryLocator([
|
|
35
|
+
SELECT Id, Name, BillingPostalCode, Is_Potential_Duplicate__c
|
|
36
|
+
FROM Account
|
|
37
|
+
WHERE IsDeleted = FALSE
|
|
38
|
+
AND Is_Potential_Duplicate__c = FALSE
|
|
39
|
+
ORDER BY Name ASC
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Processes each batch by building a duplicate key and checking for matches.
|
|
45
|
+
* Uses a composite key of normalized Name + BillingPostalCode.
|
|
46
|
+
* @param bc The batch context
|
|
47
|
+
* @param scope List of Account records in the current batch
|
|
48
|
+
*/
|
|
49
|
+
public void execute(Database.BatchableContext bc, List<Account> scope) {
|
|
50
|
+
totalScanned += scope.size();
|
|
51
|
+
|
|
52
|
+
// Build duplicate keys for this batch
|
|
53
|
+
Map<String, List<Account>> dupeKeyMap = new Map<String, List<Account>>();
|
|
54
|
+
for (Account acct : scope) {
|
|
55
|
+
String key = buildDuplicateKey(acct);
|
|
56
|
+
if (String.isNotBlank(key)) {
|
|
57
|
+
if (!dupeKeyMap.containsKey(key)) {
|
|
58
|
+
dupeKeyMap.put(key, new List<Account>());
|
|
59
|
+
}
|
|
60
|
+
dupeKeyMap.get(key).add(acct);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Flag records that share a key with another record
|
|
65
|
+
List<Account> toUpdate = new List<Account>();
|
|
66
|
+
for (String key : dupeKeyMap.keySet()) {
|
|
67
|
+
List<Account> group = dupeKeyMap.get(key);
|
|
68
|
+
if (group.size() > 1) {
|
|
69
|
+
for (Account acct : group) {
|
|
70
|
+
toUpdate.add(new Account(
|
|
71
|
+
Id = acct.Id,
|
|
72
|
+
Is_Potential_Duplicate__c = true
|
|
73
|
+
));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!toUpdate.isEmpty()) {
|
|
79
|
+
List<Database.SaveResult> results = Database.update(toUpdate, false);
|
|
80
|
+
processResults(results);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Logs a summary of the deduplication batch run
|
|
86
|
+
* @param bc The batch context
|
|
87
|
+
*/
|
|
88
|
+
public void finish(Database.BatchableContext bc) {
|
|
89
|
+
String summary = String.format(
|
|
90
|
+
'AccountDeduplicationBatch completed. Scanned: {0}, Duplicates flagged: {1}, Errors: {2}',
|
|
91
|
+
new List<Object>{ totalScanned, duplicatesFound, totalErrors }
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
System.debug(LoggingLevel.INFO, summary);
|
|
95
|
+
|
|
96
|
+
if (!errorMessages.isEmpty()) {
|
|
97
|
+
System.debug(LoggingLevel.ERROR, 'Error details:\n' + String.join(errorMessages, '\n'));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Private Helpers ─────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Builds a normalized composite key for duplicate detection
|
|
105
|
+
* @param acct The Account record
|
|
106
|
+
* @return Normalized key string, or null if insufficient data
|
|
107
|
+
*/
|
|
108
|
+
private String buildDuplicateKey(Account acct) {
|
|
109
|
+
if (String.isBlank(acct.Name)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
String normalizedName = acct.Name.trim().toUpperCase().replaceAll('\\s+', ' ');
|
|
114
|
+
String postalCode = (acct.BillingPostalCode ?? '').trim().toUpperCase();
|
|
115
|
+
|
|
116
|
+
return normalizedName + '|' + postalCode;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Processes DML results, tracking successes and failures
|
|
121
|
+
* @param results List of Database.SaveResult from update operation
|
|
122
|
+
*/
|
|
123
|
+
private void processResults(List<Database.SaveResult> results) {
|
|
124
|
+
for (Database.SaveResult sr : results) {
|
|
125
|
+
if (sr.isSuccess()) {
|
|
126
|
+
duplicatesFound++;
|
|
127
|
+
} else {
|
|
128
|
+
totalErrors++;
|
|
129
|
+
for (Database.Error err : sr.getErrors()) {
|
|
130
|
+
errorMessages.add(
|
|
131
|
+
'Record ' + sr.getId() + ': ' +
|
|
132
|
+
err.getStatusCode() + ' - ' + err.getMessage()
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Static Helpers ──────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Convenience method to execute with default batch size
|
|
143
|
+
* @return The batch job Id
|
|
144
|
+
*/
|
|
145
|
+
public static Id run() {
|
|
146
|
+
return Database.executeBatch(new AccountDeduplicationBatch(), DEFAULT_BATCH_SIZE);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector class for Account queries.
|
|
3
|
+
* Encapsulates all SOQL for Account records.
|
|
4
|
+
* All methods return bulkified results (Lists or Maps).
|
|
5
|
+
* @author Generated by Apex Class Writer Skill
|
|
6
|
+
*/
|
|
7
|
+
public with sharing class AccountSelector {
|
|
8
|
+
|
|
9
|
+
// ─── Field Lists ─────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns the default set of fields to query for Account.
|
|
13
|
+
* Centralizes field references to keep queries DRY.
|
|
14
|
+
* @return Comma-separated field list as a String
|
|
15
|
+
*/
|
|
16
|
+
private static String getDefaultFields() {
|
|
17
|
+
return String.join(
|
|
18
|
+
new List<String>{
|
|
19
|
+
'Id',
|
|
20
|
+
'Name',
|
|
21
|
+
'AccountNumber',
|
|
22
|
+
'Type',
|
|
23
|
+
'Industry',
|
|
24
|
+
'AnnualRevenue',
|
|
25
|
+
'NumberOfEmployees',
|
|
26
|
+
'Phone',
|
|
27
|
+
'Website',
|
|
28
|
+
'OwnerId',
|
|
29
|
+
'CreatedDate',
|
|
30
|
+
'LastModifiedDate'
|
|
31
|
+
},
|
|
32
|
+
', '
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns fields needed for billing/territory operations
|
|
38
|
+
* @return Comma-separated field list as a String
|
|
39
|
+
*/
|
|
40
|
+
private static String getBillingFields() {
|
|
41
|
+
return String.join(
|
|
42
|
+
new List<String>{
|
|
43
|
+
'Id',
|
|
44
|
+
'Name',
|
|
45
|
+
'BillingStreet',
|
|
46
|
+
'BillingCity',
|
|
47
|
+
'BillingState',
|
|
48
|
+
'BillingPostalCode',
|
|
49
|
+
'BillingCountry',
|
|
50
|
+
'Territory__c'
|
|
51
|
+
},
|
|
52
|
+
', '
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Query Methods ───────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Selects Account records by their Ids
|
|
60
|
+
* @param recordIds Set of Account Ids to query
|
|
61
|
+
* @return List of Account records matching the provided Ids
|
|
62
|
+
* @example
|
|
63
|
+
* Set<Id> ids = new Set<Id>{ '001xx000003DGbY' };
|
|
64
|
+
* List<Account> results = AccountSelector.selectByIds(ids);
|
|
65
|
+
*/
|
|
66
|
+
public static List<Account> selectByIds(Set<Id> recordIds) {
|
|
67
|
+
if (recordIds == null || recordIds.isEmpty()) {
|
|
68
|
+
return new List<Account>();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Database.query(
|
|
72
|
+
'SELECT ' + getDefaultFields() +
|
|
73
|
+
' FROM Account' +
|
|
74
|
+
' WHERE Id IN :recordIds'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Selects Account records as a Map keyed by Id
|
|
80
|
+
* @param recordIds Set of Account Ids to query
|
|
81
|
+
* @return Map of Id to Account
|
|
82
|
+
*/
|
|
83
|
+
public static Map<Id, Account> selectMapByIds(Set<Id> recordIds) {
|
|
84
|
+
return new Map<Id, Account>(selectByIds(recordIds));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Selects Accounts with billing address fields for territory assignment
|
|
89
|
+
* @param recordIds Set of Account Ids to query
|
|
90
|
+
* @return List of Account records with billing address fields populated
|
|
91
|
+
*/
|
|
92
|
+
public static List<Account> selectWithBillingAddress(Set<Id> recordIds) {
|
|
93
|
+
if (recordIds == null || recordIds.isEmpty()) {
|
|
94
|
+
return new List<Account>();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return Database.query(
|
|
98
|
+
'SELECT ' + getBillingFields() +
|
|
99
|
+
' FROM Account' +
|
|
100
|
+
' WHERE Id IN :recordIds'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Selects Accounts by Account Type
|
|
106
|
+
* @param accountTypes Set of Account Type values to filter by
|
|
107
|
+
* @return List of matching Account records
|
|
108
|
+
* @example
|
|
109
|
+
* List<Account> prospects = AccountSelector.selectByType(
|
|
110
|
+
* new Set<String>{ 'Prospect', 'Customer - Direct' }
|
|
111
|
+
* );
|
|
112
|
+
*/
|
|
113
|
+
public static List<Account> selectByType(Set<String> accountTypes) {
|
|
114
|
+
if (accountTypes == null || accountTypes.isEmpty()) {
|
|
115
|
+
return new List<Account>();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Database.query(
|
|
119
|
+
'SELECT ' + getDefaultFields() +
|
|
120
|
+
' FROM Account' +
|
|
121
|
+
' WHERE Type IN :accountTypes' +
|
|
122
|
+
' ORDER BY Name ASC'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Selects Accounts by Industry with a minimum annual revenue
|
|
128
|
+
* @param industries Set of Industry values to filter by
|
|
129
|
+
* @param minRevenue Minimum AnnualRevenue threshold
|
|
130
|
+
* @return List of matching Account records ordered by revenue descending
|
|
131
|
+
* @example
|
|
132
|
+
* List<Account> techAccounts = AccountSelector.selectByIndustryAndRevenue(
|
|
133
|
+
* new Set<String>{ 'Technology' },
|
|
134
|
+
* 1000000
|
|
135
|
+
* );
|
|
136
|
+
*/
|
|
137
|
+
public static List<Account> selectByIndustryAndRevenue(Set<String> industries, Decimal minRevenue) {
|
|
138
|
+
if (industries == null || industries.isEmpty()) {
|
|
139
|
+
return new List<Account>();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Decimal revenueThreshold = minRevenue ?? 0;
|
|
143
|
+
|
|
144
|
+
return Database.query(
|
|
145
|
+
'SELECT ' + getDefaultFields() +
|
|
146
|
+
' FROM Account' +
|
|
147
|
+
' WHERE Industry IN :industries' +
|
|
148
|
+
' AND AnnualRevenue >= :revenueThreshold' +
|
|
149
|
+
' ORDER BY AnnualRevenue DESC'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Selects Accounts with their related Contacts (subquery)
|
|
155
|
+
* @param recordIds Set of Account Ids to query
|
|
156
|
+
* @return List of Account records with nested Contacts
|
|
157
|
+
*/
|
|
158
|
+
public static List<Account> selectWithContacts(Set<Id> recordIds) {
|
|
159
|
+
if (recordIds == null || recordIds.isEmpty()) {
|
|
160
|
+
return new List<Account>();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [
|
|
164
|
+
SELECT Id, Name, Type, Industry,
|
|
165
|
+
(SELECT Id, FirstName, LastName, Email, Title
|
|
166
|
+
FROM Contacts
|
|
167
|
+
ORDER BY LastName ASC)
|
|
168
|
+
FROM Account
|
|
169
|
+
WHERE Id IN :recordIds
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Aggregate Queries ───────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns a count of Accounts grouped by Industry
|
|
177
|
+
* @return List of AggregateResult with Industry and record count
|
|
178
|
+
* @example
|
|
179
|
+
* List<AggregateResult> results = AccountSelector.countByIndustry();
|
|
180
|
+
* for (AggregateResult ar : results) {
|
|
181
|
+
* System.debug(ar.get('Industry') + ': ' + ar.get('cnt'));
|
|
182
|
+
* }
|
|
183
|
+
*/
|
|
184
|
+
public static List<AggregateResult> countByIndustry() {
|
|
185
|
+
return [
|
|
186
|
+
SELECT Industry, COUNT(Id) cnt
|
|
187
|
+
FROM Account
|
|
188
|
+
WHERE Industry != NULL
|
|
189
|
+
GROUP BY Industry
|
|
190
|
+
ORDER BY COUNT(Id) DESC
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service class for Account business logic.
|
|
3
|
+
* Provides account deduplication, enrichment, and territory assignment.
|
|
4
|
+
* Delegates queries to AccountSelector and SObject manipulation to AccountDomain.
|
|
5
|
+
* @author Generated by Apex Class Writer Skill
|
|
6
|
+
*/
|
|
7
|
+
public with sharing class AccountService {
|
|
8
|
+
|
|
9
|
+
// ─── Constants ───────────────────────────────────────────────────────
|
|
10
|
+
private static final String ERROR_NULL_INPUT = 'Input cannot be null or empty.';
|
|
11
|
+
private static final String ERROR_MERGE_FAILED = 'Account merge failed for master Id: ';
|
|
12
|
+
private static final Integer MAX_MERGE_BATCH = 3;
|
|
13
|
+
|
|
14
|
+
// ─── Public API ──────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Merges duplicate Account records into a master record.
|
|
18
|
+
* The master record retains its field values; child records are reparented.
|
|
19
|
+
* @param masterIds Map of master Account Id to Set of duplicate Account Ids to merge
|
|
20
|
+
* @return List of master Account Ids that were successfully merged
|
|
21
|
+
* @throws AccountServiceException if merge processing fails
|
|
22
|
+
* @example
|
|
23
|
+
* Map<Id, Set<Id>> mergeMap = new Map<Id, Set<Id>>{
|
|
24
|
+
* masterAcctId => new Set<Id>{ dupeId1, dupeId2 }
|
|
25
|
+
* };
|
|
26
|
+
* List<Id> mergedIds = AccountService.mergeAccounts(mergeMap);
|
|
27
|
+
*/
|
|
28
|
+
public static List<Id> mergeAccounts(Map<Id, Set<Id>> mergeMap) {
|
|
29
|
+
if (mergeMap == null || mergeMap.isEmpty()) {
|
|
30
|
+
throw new AccountServiceException(ERROR_NULL_INPUT);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Collect all Ids for a single query
|
|
34
|
+
Set<Id> allIds = new Set<Id>();
|
|
35
|
+
allIds.addAll(mergeMap.keySet());
|
|
36
|
+
for (Set<Id> dupeIds : mergeMap.values()) {
|
|
37
|
+
allIds.addAll(dupeIds);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Query all records at once via Selector
|
|
41
|
+
Map<Id, Account> accountMap = AccountSelector.selectMapByIds(allIds);
|
|
42
|
+
|
|
43
|
+
List<Id> successfulMerges = new List<Id>();
|
|
44
|
+
List<String> errors = new List<String>();
|
|
45
|
+
|
|
46
|
+
for (Id masterId : mergeMap.keySet()) {
|
|
47
|
+
Account master = accountMap.get(masterId);
|
|
48
|
+
if (master == null) {
|
|
49
|
+
errors.add('Master account not found: ' + masterId);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
List<Account> duplicates = new List<Account>();
|
|
54
|
+
for (Id dupeId : mergeMap.get(masterId)) {
|
|
55
|
+
Account dupe = accountMap.get(dupeId);
|
|
56
|
+
if (dupe != null) {
|
|
57
|
+
duplicates.add(dupe);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Apex merge supports up to 3 records at a time
|
|
63
|
+
for (List<Account> chunk : chunkAccounts(duplicates, MAX_MERGE_BATCH)) {
|
|
64
|
+
Database.merge(master, chunk);
|
|
65
|
+
}
|
|
66
|
+
successfulMerges.add(masterId);
|
|
67
|
+
} catch (DmlException e) {
|
|
68
|
+
errors.add(ERROR_MERGE_FAILED + masterId + ' - ' + e.getMessage());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!errors.isEmpty()) {
|
|
73
|
+
System.debug(LoggingLevel.WARN, 'Merge errors: ' + String.join(errors, '\n'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return successfulMerges;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Assigns accounts to territories based on Billing State/Country.
|
|
81
|
+
* Uses Custom Metadata Type (Territory_Mapping__mdt) for mappings.
|
|
82
|
+
* @param accountIds Set of Account Ids to assign territories for
|
|
83
|
+
* @return Number of accounts successfully updated
|
|
84
|
+
* @throws AccountServiceException if territory assignment fails
|
|
85
|
+
*/
|
|
86
|
+
public static Integer assignTerritories(Set<Id> accountIds) {
|
|
87
|
+
if (accountIds == null || accountIds.isEmpty()) {
|
|
88
|
+
throw new AccountServiceException(ERROR_NULL_INPUT);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
List<Account> accounts = AccountSelector.selectWithBillingAddress(accountIds);
|
|
92
|
+
Map<String, String> territoryMap = loadTerritoryMappings();
|
|
93
|
+
|
|
94
|
+
List<Account> toUpdate = new List<Account>();
|
|
95
|
+
for (Account acct : accounts) {
|
|
96
|
+
String key = buildTerritoryKey(acct.BillingState, acct.BillingCountry);
|
|
97
|
+
String territory = territoryMap.get(key);
|
|
98
|
+
|
|
99
|
+
if (territory != null && territory != acct.Territory__c) {
|
|
100
|
+
toUpdate.add(new Account(
|
|
101
|
+
Id = acct.Id,
|
|
102
|
+
Territory__c = territory
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!toUpdate.isEmpty()) {
|
|
108
|
+
List<Database.SaveResult> results = Database.update(toUpdate, false);
|
|
109
|
+
return countSuccesses(results);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Convenience Overloads ───────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Single-account territory assignment convenience method
|
|
119
|
+
* @param accountId The Account Id to assign a territory for
|
|
120
|
+
* @return 1 if updated, 0 if no change needed
|
|
121
|
+
*/
|
|
122
|
+
public static Integer assignTerritory(Id accountId) {
|
|
123
|
+
return assignTerritories(new Set<Id>{ accountId });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Private Helpers ─────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Loads territory mappings from Custom Metadata
|
|
130
|
+
* @return Map of territory key (State:Country) to territory name
|
|
131
|
+
*/
|
|
132
|
+
private static Map<String, String> loadTerritoryMappings() {
|
|
133
|
+
Map<String, String> mappings = new Map<String, String>();
|
|
134
|
+
for (Territory_Mapping__mdt mapping : Territory_Mapping__mdt.getAll().values()) {
|
|
135
|
+
String key = buildTerritoryKey(mapping.State__c, mapping.Country__c);
|
|
136
|
+
mappings.put(key, mapping.Territory_Name__c);
|
|
137
|
+
}
|
|
138
|
+
return mappings;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Builds a consistent territory lookup key
|
|
143
|
+
* @param state The billing state
|
|
144
|
+
* @param country The billing country
|
|
145
|
+
* @return A normalized key string
|
|
146
|
+
*/
|
|
147
|
+
private static String buildTerritoryKey(String state, String country) {
|
|
148
|
+
return (state ?? '').toUpperCase() + ':' + (country ?? '').toUpperCase();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Chunks a list of Accounts into sublists of the given size
|
|
153
|
+
* @param accounts The accounts to chunk
|
|
154
|
+
* @param chunkSize Maximum chunk size
|
|
155
|
+
* @return List of account sublists
|
|
156
|
+
*/
|
|
157
|
+
private static List<List<Account>> chunkAccounts(List<Account> accounts, Integer chunkSize) {
|
|
158
|
+
List<List<Account>> chunks = new List<List<Account>>();
|
|
159
|
+
List<Account> current = new List<Account>();
|
|
160
|
+
|
|
161
|
+
for (Account acct : accounts) {
|
|
162
|
+
current.add(acct);
|
|
163
|
+
if (current.size() == chunkSize) {
|
|
164
|
+
chunks.add(current);
|
|
165
|
+
current = new List<Account>();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!current.isEmpty()) {
|
|
169
|
+
chunks.add(current);
|
|
170
|
+
}
|
|
171
|
+
return chunks;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Counts successful results from a DML operation
|
|
176
|
+
* @param results List of Database.SaveResult
|
|
177
|
+
* @return Count of successful operations
|
|
178
|
+
*/
|
|
179
|
+
private static Integer countSuccesses(List<Database.SaveResult> results) {
|
|
180
|
+
Integer count = 0;
|
|
181
|
+
for (Database.SaveResult sr : results) {
|
|
182
|
+
if (sr.isSuccess()) {
|
|
183
|
+
count++;
|
|
184
|
+
} else {
|
|
185
|
+
for (Database.Error err : sr.getErrors()) {
|
|
186
|
+
System.debug(LoggingLevel.WARN,
|
|
187
|
+
'Update failed for ' + sr.getId() + ': ' + err.getMessage()
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return count;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Exception ───────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Custom exception for AccountService errors
|
|
199
|
+
*/
|
|
200
|
+
public class AccountServiceException extends Exception {}
|
|
201
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Credits & Acknowledgments
|
|
2
|
+
|
|
3
|
+
This skill was influenced by the [sf-skills](https://github.com/Jaganpro/sf-skills) repository and built upon the collective wisdom of the Salesforce developer community. We gratefully acknowledge the following authors and resources whose ideas, patterns, and best practices have shaped this skill.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Authors & Contributors
|
|
8
|
+
|
|
9
|
+
### Jag Valaiyapathy (**[Jaganpro)](https://github.com/Jaganpro)**
|
|
10
|
+
|
|
11
|
+
**[sf-skills](https://github.com/Jaganpro/sf-skills)**
|
|
12
|
+
|
|
13
|
+
Key contributions influencing this skill:
|
|
14
|
+
|
|
15
|
+
- Pioneering open-source Salesforce skills for agentic coding tools
|
|
16
|
+
- Apex code generation and review patterns
|
|
17
|
+
- Best practices, anti-patterns, and design patterns reference material
|
|
18
|
+
- Template library for common Apex class types
|
|
19
|
+
|
|
20
|
+
This skill was influenced by the [sf-skills](https://github.com/Jaganpro/sf-skills) repository.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Special Thanks
|
|
25
|
+
|
|
26
|
+
To the entire Salesforce developer community for sharing knowledge, writing blogs, creating open-source tools, and helping each other build better solutions.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
*If we've missed anyone whose work influenced these skills, please let us know so we can add proper attribution.*
|