scc-universal 1.1.0
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/.claude-plugin/plugin.json +44 -0
- package/.cursor/agents/deep-researcher.md +142 -0
- package/.cursor/agents/doc-updater.md +219 -0
- package/.cursor/agents/eval-runner.md +335 -0
- package/.cursor/agents/learning-engine.md +210 -0
- package/.cursor/agents/loop-operator.md +245 -0
- package/.cursor/agents/refactor-cleaner.md +119 -0
- package/.cursor/agents/sf-admin-agent.md +127 -0
- package/.cursor/agents/sf-agentforce-agent.md +126 -0
- package/.cursor/agents/sf-apex-agent.md +117 -0
- package/.cursor/agents/sf-architect.md +426 -0
- package/.cursor/agents/sf-aura-reviewer.md +369 -0
- package/.cursor/agents/sf-bugfix-agent.md +101 -0
- package/.cursor/agents/sf-flow-agent.md +155 -0
- package/.cursor/agents/sf-integration-agent.md +141 -0
- package/.cursor/agents/sf-lwc-agent.md +123 -0
- package/.cursor/agents/sf-review-agent.md +357 -0
- package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
- package/.cursor/hooks/adapter.js +81 -0
- package/.cursor/hooks/after-file-edit.js +26 -0
- package/.cursor/hooks/after-mcp-execution.js +12 -0
- package/.cursor/hooks/after-shell-execution.js +30 -0
- package/.cursor/hooks/after-tab-file-edit.js +12 -0
- package/.cursor/hooks/before-mcp-execution.js +11 -0
- package/.cursor/hooks/before-read-file.js +13 -0
- package/.cursor/hooks/before-shell-execution.js +29 -0
- package/.cursor/hooks/before-submit-prompt.js +23 -0
- package/.cursor/hooks/pre-compact.js +7 -0
- package/.cursor/hooks/session-end.js +10 -0
- package/.cursor/hooks/session-start.js +10 -0
- package/.cursor/hooks/stop.js +18 -0
- package/.cursor/hooks/subagent-start.js +10 -0
- package/.cursor/hooks/subagent-stop.js +10 -0
- package/.cursor/hooks.json +107 -0
- package/.cursor/skills/aside/SKILL.md +115 -0
- package/.cursor/skills/checkpoint/SKILL.md +50 -0
- package/.cursor/skills/configure-scc/SKILL.md +160 -0
- package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
- package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
- package/.cursor/skills/model-route/SKILL.md +81 -0
- package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
- package/.cursor/skills/refactor-clean/SKILL.md +133 -0
- package/.cursor/skills/resume-session/SKILL.md +111 -0
- package/.cursor/skills/save-session/SKILL.md +183 -0
- package/.cursor/skills/search-first/SKILL.md +140 -0
- package/.cursor/skills/security-scan/SKILL.md +142 -0
- package/.cursor/skills/sessions/SKILL.md +124 -0
- package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
- package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
- package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
- package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
- package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
- package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
- package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
- package/.cursor/skills/sf-api-design/SKILL.md +237 -0
- package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
- package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
- package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
- package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
- package/.cursor/skills/sf-debugging/SKILL.md +362 -0
- package/.cursor/skills/sf-deployment/SKILL.md +291 -0
- package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
- package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
- package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
- package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
- package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
- package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
- package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
- package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
- package/.cursor/skills/sf-help/SKILL.md +156 -0
- package/.cursor/skills/sf-integration/SKILL.md +479 -0
- package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
- package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
- package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
- package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
- package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
- package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
- package/.cursor/skills/sf-security/SKILL.md +330 -0
- package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
- package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
- package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
- package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
- package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
- package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
- package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
- package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
- package/.cursor/skills/strategic-compact/SKILL.md +205 -0
- package/.cursor/skills/update-docs/SKILL.md +162 -0
- package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/agents/deep-researcher.md +145 -0
- package/agents/doc-updater.md +222 -0
- package/agents/eval-runner.md +340 -0
- package/agents/learning-engine.md +211 -0
- package/agents/loop-operator.md +247 -0
- package/agents/refactor-cleaner.md +122 -0
- package/agents/sf-admin-agent.md +131 -0
- package/agents/sf-agentforce-agent.md +132 -0
- package/agents/sf-apex-agent.md +124 -0
- package/agents/sf-architect.md +435 -0
- package/agents/sf-aura-reviewer.md +372 -0
- package/agents/sf-bugfix-agent.md +105 -0
- package/agents/sf-flow-agent.md +159 -0
- package/agents/sf-integration-agent.md +146 -0
- package/agents/sf-lwc-agent.md +127 -0
- package/agents/sf-review-agent.md +366 -0
- package/agents/sf-visualforce-reviewer.md +468 -0
- package/assets/logo.svg +18 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/authoring-guide.md +373 -0
- package/docs/hook-development.md +578 -0
- package/docs/token-optimization.md +139 -0
- package/docs/workflow-examples.md +645 -0
- package/examples/agentforce-action/README.md +227 -0
- package/examples/apex-trigger-handler/README.md +114 -0
- package/examples/devops-pipeline/README.md +325 -0
- package/examples/flow-automation/README.md +188 -0
- package/examples/integration-pattern/README.md +416 -0
- package/examples/lwc-component/README.md +180 -0
- package/examples/platform-events/README.md +492 -0
- package/examples/scratch-org-setup/README.md +138 -0
- package/examples/security-audit/README.md +244 -0
- package/examples/visualforce-migration/README.md +314 -0
- package/hooks/hooks.json +338 -0
- package/hooks/memory-persistence/README.md +73 -0
- package/manifests/install-modules.json +217 -0
- package/manifests/install-profiles.json +17 -0
- package/mcp-configs/mcp-servers.json +19 -0
- package/package.json +89 -0
- package/schemas/hooks.schema.json +123 -0
- package/schemas/install-modules.schema.json +76 -0
- package/schemas/install-profiles.schema.json +28 -0
- package/schemas/install-state.schema.json +73 -0
- package/schemas/package-manager.schema.json +18 -0
- package/schemas/plugin.schema.json +112 -0
- package/schemas/scc-install-config.schema.json +29 -0
- package/schemas/state-store.schema.json +111 -0
- package/scripts/cli/install-apply.js +170 -0
- package/scripts/cli/uninstall.js +193 -0
- package/scripts/hooks/check-console-log.js +101 -0
- package/scripts/hooks/check-hook-enabled.js +17 -0
- package/scripts/hooks/check-platform-docs-age.js +48 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +98 -0
- package/scripts/hooks/governor-check.js +220 -0
- package/scripts/hooks/learning-observe.sh +206 -0
- package/scripts/hooks/mcp-health-check.js +588 -0
- package/scripts/hooks/post-bash-build-complete.js +34 -0
- package/scripts/hooks/post-bash-pr-created.js +43 -0
- package/scripts/hooks/post-edit-console-warn.js +61 -0
- package/scripts/hooks/post-edit-format.js +79 -0
- package/scripts/hooks/post-edit-typecheck.js +98 -0
- package/scripts/hooks/post-write.js +168 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
- package/scripts/hooks/pre-compact.js +51 -0
- package/scripts/hooks/pre-tool-use.js +163 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +251 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +135 -0
- package/scripts/hooks/session-end-marker.js +29 -0
- package/scripts/hooks/session-end.js +311 -0
- package/scripts/hooks/session-start.js +202 -0
- package/scripts/hooks/sfdx-scanner-check.js +142 -0
- package/scripts/hooks/sfdx-validate.js +119 -0
- package/scripts/hooks/stop-hook.js +170 -0
- package/scripts/hooks/suggest-compact.js +67 -0
- package/scripts/lib/agent-adapter.js +82 -0
- package/scripts/lib/apex-analysis.js +194 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install-config.js +73 -0
- package/scripts/lib/install-executor.js +363 -0
- package/scripts/lib/install-state.js +121 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.js +124 -0
- package/scripts/lib/project-detect.js +228 -0
- package/scripts/lib/schema-validator.js +190 -0
- package/scripts/lib/skill-adapter.js +100 -0
- package/scripts/lib/state-store.js +376 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
- package/scripts/lib/utils.js +313 -0
- package/scripts/scc.js +164 -0
- package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
- package/skills/_reference/APEX_CURSOR.md +159 -0
- package/skills/_reference/API_VERSIONS.md +78 -0
- package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
- package/skills/_reference/ASYNC_PATTERNS.md +163 -0
- package/skills/_reference/AURA_COMPONENTS.md +146 -0
- package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
- package/skills/_reference/DATA_MODELING.md +124 -0
- package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
- package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
- package/skills/_reference/DEPRECATIONS.md +79 -0
- package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
- package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
- package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
- package/skills/_reference/FLOW_PATTERNS.md +113 -0
- package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
- package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
- package/skills/_reference/LWC_PATTERNS.md +79 -0
- package/skills/_reference/METADATA_TYPES.md +115 -0
- package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
- package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
- package/skills/_reference/PLATFORM_EVENTS.md +121 -0
- package/skills/_reference/REPORTING_API.md +143 -0
- package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
- package/skills/_reference/SECURITY_PATTERNS.md +127 -0
- package/skills/_reference/SHARING_MODEL.md +120 -0
- package/skills/_reference/SOQL_PATTERNS.md +119 -0
- package/skills/_reference/TESTING_STANDARDS.md +96 -0
- package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
- package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
- package/skills/aside/SKILL.md +118 -0
- package/skills/checkpoint/SKILL.md +53 -0
- package/skills/configure-scc/SKILL.md +163 -0
- package/skills/continuous-agent-loop/SKILL.md +264 -0
- package/skills/mcp-server-patterns/SKILL.md +146 -0
- package/skills/model-route/SKILL.md +84 -0
- package/skills/prompt-optimizer/SKILL.md +369 -0
- package/skills/refactor-clean/SKILL.md +136 -0
- package/skills/resume-session/SKILL.md +114 -0
- package/skills/save-session/SKILL.md +186 -0
- package/skills/search-first/SKILL.md +144 -0
- package/skills/security-scan/SKILL.md +146 -0
- package/skills/sessions/SKILL.md +127 -0
- package/skills/sf-agentforce-development/SKILL.md +450 -0
- package/skills/sf-apex-async-patterns/SKILL.md +326 -0
- package/skills/sf-apex-best-practices/SKILL.md +425 -0
- package/skills/sf-apex-constraints/SKILL.md +81 -0
- package/skills/sf-apex-cursor/SKILL.md +338 -0
- package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
- package/skills/sf-apex-testing/SKILL.md +409 -0
- package/skills/sf-api-design/SKILL.md +238 -0
- package/skills/sf-approval-processes/SKILL.md +315 -0
- package/skills/sf-aura-development/SKILL.md +263 -0
- package/skills/sf-build-fix/SKILL.md +121 -0
- package/skills/sf-data-modeling/SKILL.md +278 -0
- package/skills/sf-debugging/SKILL.md +363 -0
- package/skills/sf-deployment/SKILL.md +295 -0
- package/skills/sf-deployment-constraints/SKILL.md +155 -0
- package/skills/sf-devops-ci-cd/SKILL.md +325 -0
- package/skills/sf-docs-lookup/SKILL.md +103 -0
- package/skills/sf-e2e-testing/SKILL.md +324 -0
- package/skills/sf-experience-cloud/SKILL.md +249 -0
- package/skills/sf-flow-development/SKILL.md +377 -0
- package/skills/sf-governor-limits/SKILL.md +323 -0
- package/skills/sf-harness-audit/SKILL.md +142 -0
- package/skills/sf-help/SKILL.md +159 -0
- package/skills/sf-integration/SKILL.md +483 -0
- package/skills/sf-lwc-constraints/SKILL.md +130 -0
- package/skills/sf-lwc-development/SKILL.md +303 -0
- package/skills/sf-lwc-testing/SKILL.md +388 -0
- package/skills/sf-metadata-management/SKILL.md +288 -0
- package/skills/sf-platform-events-cdc/SKILL.md +375 -0
- package/skills/sf-quickstart/SKILL.md +173 -0
- package/skills/sf-security/SKILL.md +334 -0
- package/skills/sf-security-constraints/SKILL.md +127 -0
- package/skills/sf-soql-constraints/SKILL.md +131 -0
- package/skills/sf-soql-optimization/SKILL.md +354 -0
- package/skills/sf-tdd-workflow/SKILL.md +336 -0
- package/skills/sf-testing-constraints/SKILL.md +200 -0
- package/skills/sf-trigger-constraints/SKILL.md +90 -0
- package/skills/sf-trigger-frameworks/SKILL.md +347 -0
- package/skills/sf-visualforce-development/SKILL.md +260 -0
- package/skills/strategic-compact/SKILL.md +208 -0
- package/skills/update-docs/SKILL.md +165 -0
- package/skills/update-platform-docs/SKILL.md +90 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Flow Automation Example
|
|
2
|
+
|
|
3
|
+
Sample Record-Triggered Flow with best practices for review. API version 66.0 (Spring '26).
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
force-app/main/default/flows/
|
|
9
|
+
Account_Set_Rating.flow-meta.xml
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Flow Design: Account Rating Auto-Set
|
|
13
|
+
|
|
14
|
+
**Type:** Record-Triggered Flow (Before Save)
|
|
15
|
+
**Object:** Account
|
|
16
|
+
**When:** A record is created or updated
|
|
17
|
+
**Why Before Save:** Same-record field updates in a Before Save flow require no extra DML statement, making it more efficient than After Save for this use case.
|
|
18
|
+
|
|
19
|
+
### Logic
|
|
20
|
+
|
|
21
|
+
1. **Entry Criteria:** Annual Revenue is not null AND has changed (prevents recursion)
|
|
22
|
+
2. **Decision:** Evaluate revenue ranges (ordered most-specific first)
|
|
23
|
+
- Revenue > 1,000,000 → Set Rating = "Hot"
|
|
24
|
+
- Revenue > 100,000 → Set Rating = "Warm"
|
|
25
|
+
- Revenue <= 100,000 → Set Rating = "Cold"
|
|
26
|
+
3. **Assignment:** Set the Rating field on `$Record` (no Update element needed in Before Save)
|
|
27
|
+
|
|
28
|
+
## Flow Metadata
|
|
29
|
+
|
|
30
|
+
```xml
|
|
31
|
+
<!-- Account_Set_Rating.flow-meta.xml -->
|
|
32
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
+
<Flow xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
34
|
+
<apiVersion>66.0</apiVersion>
|
|
35
|
+
<description>Sets Account Rating based on Annual Revenue ranges.
|
|
36
|
+
Before-save flow — no DML needed for same-record updates.</description>
|
|
37
|
+
<interviewLabel>Account Set Rating {!$Flow.CurrentDateTime}</interviewLabel>
|
|
38
|
+
<label>Account Set Rating</label>
|
|
39
|
+
<processType>AutoLaunchedFlow</processType>
|
|
40
|
+
<status>Active</status>
|
|
41
|
+
|
|
42
|
+
<!-- Entry point: triggers on Account create/update -->
|
|
43
|
+
<start>
|
|
44
|
+
<locationX>50</locationX>
|
|
45
|
+
<locationY>0</locationY>
|
|
46
|
+
<connector>
|
|
47
|
+
<targetReference>Check_Revenue</targetReference>
|
|
48
|
+
</connector>
|
|
49
|
+
<filterFormula>
|
|
50
|
+
NOT(ISNULL({!$Record.AnnualRevenue}))
|
|
51
|
+
&& ISCHANGED({!$Record.AnnualRevenue})
|
|
52
|
+
</filterFormula>
|
|
53
|
+
<object>Account</object>
|
|
54
|
+
<recordTriggerType>CreateAndUpdate</recordTriggerType>
|
|
55
|
+
<triggerType>RecordBeforeSave</triggerType>
|
|
56
|
+
</start>
|
|
57
|
+
|
|
58
|
+
<!-- Decision: evaluate revenue ranges -->
|
|
59
|
+
<decisions>
|
|
60
|
+
<name>Check_Revenue</name>
|
|
61
|
+
<label>Check Revenue</label>
|
|
62
|
+
<locationX>182</locationX>
|
|
63
|
+
<locationY>158</locationY>
|
|
64
|
+
<defaultConnector>
|
|
65
|
+
<targetReference>Set_Rating_Cold</targetReference>
|
|
66
|
+
</defaultConnector>
|
|
67
|
+
<defaultConnectorLabel>Cold</defaultConnectorLabel>
|
|
68
|
+
<rules>
|
|
69
|
+
<name>Is_Hot</name>
|
|
70
|
+
<conditionLogic>and</conditionLogic>
|
|
71
|
+
<conditions>
|
|
72
|
+
<leftValueReference>$Record.AnnualRevenue</leftValueReference>
|
|
73
|
+
<operator>GreaterThan</operator>
|
|
74
|
+
<rightValue>
|
|
75
|
+
<numberValue>1000000</numberValue>
|
|
76
|
+
</rightValue>
|
|
77
|
+
</conditions>
|
|
78
|
+
<connector>
|
|
79
|
+
<targetReference>Set_Rating_Hot</targetReference>
|
|
80
|
+
</connector>
|
|
81
|
+
<label>Hot</label>
|
|
82
|
+
</rules>
|
|
83
|
+
<rules>
|
|
84
|
+
<name>Is_Warm</name>
|
|
85
|
+
<conditionLogic>and</conditionLogic>
|
|
86
|
+
<conditions>
|
|
87
|
+
<leftValueReference>$Record.AnnualRevenue</leftValueReference>
|
|
88
|
+
<operator>GreaterThan</operator>
|
|
89
|
+
<rightValue>
|
|
90
|
+
<numberValue>100000</numberValue>
|
|
91
|
+
</rightValue>
|
|
92
|
+
</conditions>
|
|
93
|
+
<connector>
|
|
94
|
+
<targetReference>Set_Rating_Warm</targetReference>
|
|
95
|
+
</connector>
|
|
96
|
+
<label>Warm</label>
|
|
97
|
+
</rules>
|
|
98
|
+
</decisions>
|
|
99
|
+
|
|
100
|
+
<!-- Assignment elements: set Rating on $Record (Before Save = no DML) -->
|
|
101
|
+
<assignments>
|
|
102
|
+
<name>Set_Rating_Hot</name>
|
|
103
|
+
<label>Set Rating Hot</label>
|
|
104
|
+
<locationX>50</locationX>
|
|
105
|
+
<locationY>334</locationY>
|
|
106
|
+
<assignmentItems>
|
|
107
|
+
<assignToReference>$Record.Rating</assignToReference>
|
|
108
|
+
<operator>Assign</operator>
|
|
109
|
+
<value>
|
|
110
|
+
<stringValue>Hot</stringValue>
|
|
111
|
+
</value>
|
|
112
|
+
</assignmentItems>
|
|
113
|
+
</assignments>
|
|
114
|
+
<assignments>
|
|
115
|
+
<name>Set_Rating_Warm</name>
|
|
116
|
+
<label>Set Rating Warm</label>
|
|
117
|
+
<locationX>182</locationX>
|
|
118
|
+
<locationY>334</locationY>
|
|
119
|
+
<assignmentItems>
|
|
120
|
+
<assignToReference>$Record.Rating</assignToReference>
|
|
121
|
+
<operator>Assign</operator>
|
|
122
|
+
<value>
|
|
123
|
+
<stringValue>Warm</stringValue>
|
|
124
|
+
</value>
|
|
125
|
+
</assignmentItems>
|
|
126
|
+
</assignments>
|
|
127
|
+
<assignments>
|
|
128
|
+
<name>Set_Rating_Cold</name>
|
|
129
|
+
<label>Set Rating Cold</label>
|
|
130
|
+
<locationX>314</locationX>
|
|
131
|
+
<locationY>334</locationY>
|
|
132
|
+
<assignmentItems>
|
|
133
|
+
<assignToReference>$Record.Rating</assignToReference>
|
|
134
|
+
<operator>Assign</operator>
|
|
135
|
+
<value>
|
|
136
|
+
<stringValue>Cold</stringValue>
|
|
137
|
+
</value>
|
|
138
|
+
</assignmentItems>
|
|
139
|
+
</assignments>
|
|
140
|
+
</Flow>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Review Checklist
|
|
144
|
+
|
|
145
|
+
- [ ] Uses Record-Triggered Flow (not Process Builder or Workflow)
|
|
146
|
+
- [ ] Entry criteria prevents unnecessary executions
|
|
147
|
+
- [ ] Decision elements are ordered most-specific to least-specific
|
|
148
|
+
- [ ] No DML operations inside loops
|
|
149
|
+
- [ ] Bulkification-safe (handles 200+ records)
|
|
150
|
+
- [ ] Error handling with fault paths
|
|
151
|
+
- [ ] Description filled in for all elements
|
|
152
|
+
- [ ] Flow is versioned (not overwriting active version)
|
|
153
|
+
|
|
154
|
+
## Anti-Patterns to Avoid
|
|
155
|
+
|
|
156
|
+
1. **Loops with DML** — Never put Create/Update/Delete inside a Loop element
|
|
157
|
+
2. **Missing fault paths** — Always add fault connectors to DML elements
|
|
158
|
+
3. **Recursive triggers** — Use `$Record__Prior` to check if values actually changed
|
|
159
|
+
4. **Too many flows per object** — Consolidate into fewer flows with decision elements
|
|
160
|
+
5. **Hardcoded values** — Use Custom Metadata or Custom Labels instead
|
|
161
|
+
|
|
162
|
+
## Testing
|
|
163
|
+
|
|
164
|
+
```apex
|
|
165
|
+
@IsTest
|
|
166
|
+
static void shouldSetRatingToHotForHighRevenue() {
|
|
167
|
+
Account acc = new Account(Name = 'Test', AnnualRevenue = 2000000);
|
|
168
|
+
insert acc;
|
|
169
|
+
|
|
170
|
+
Account result = [SELECT Rating FROM Account WHERE Id = :acc.Id];
|
|
171
|
+
System.assertEquals('Hot', result.Rating, 'High revenue should set rating to Hot');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@IsTest
|
|
175
|
+
static void shouldSetRatingToColdForLowRevenue() {
|
|
176
|
+
Account acc = new Account(Name = 'Small Co', AnnualRevenue = 50000);
|
|
177
|
+
insert acc;
|
|
178
|
+
|
|
179
|
+
Account result = [SELECT Rating FROM Account WHERE Id = :acc.Id];
|
|
180
|
+
System.assertEquals('Cold', result.Rating, 'Low revenue should set rating to Cold');
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## SCC Skills
|
|
185
|
+
|
|
186
|
+
- `sf-flow-development` -- Flow best practices and anti-patterns
|
|
187
|
+
- `sf-governor-limits` -- verify flow doesn't accumulate DML in loops
|
|
188
|
+
- `sf-apex-testing` -- write Apex tests that fire record-triggered flows
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# External Integration with Named Credentials
|
|
2
|
+
|
|
3
|
+
Secure external API integration using Named Credentials, HTTP callout service, retry handling, async processing, and test mocking. Compatible with API version 66.0 (Spring '26).
|
|
4
|
+
|
|
5
|
+
## When to Use This Pattern
|
|
6
|
+
|
|
7
|
+
- Calling external REST APIs from Salesforce (payment gateways, ERPs, shipping providers)
|
|
8
|
+
- Building callouts that need authentication managed by the platform
|
|
9
|
+
- Implementing retry logic for transient network failures
|
|
10
|
+
- Processing callouts asynchronously to avoid governor limits in synchronous contexts
|
|
11
|
+
- Writing testable integration code with `HttpCalloutMock`
|
|
12
|
+
|
|
13
|
+
## Structure
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
force-app/main/default/
|
|
17
|
+
namedCredentials/
|
|
18
|
+
Payment_Gateway.namedCredential-meta.xml
|
|
19
|
+
externalCredentials/
|
|
20
|
+
Payment_Gateway_Credential.externalCredential-meta.xml
|
|
21
|
+
classes/
|
|
22
|
+
PaymentGatewayService.cls # HTTP callout service
|
|
23
|
+
PaymentGatewayService_Test.cls # Test with mock
|
|
24
|
+
PaymentGatewayQueueable.cls # Async callout wrapper
|
|
25
|
+
PaymentGatewayMock.cls # HttpCalloutMock implementation
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Named Credential Setup
|
|
29
|
+
|
|
30
|
+
```xml
|
|
31
|
+
<!-- Payment_Gateway.namedCredential-meta.xml -->
|
|
32
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
+
<NamedCredential xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
34
|
+
<fullName>Payment_Gateway</fullName>
|
|
35
|
+
<label>Payment Gateway</label>
|
|
36
|
+
<type>SecuredEndpoint</type>
|
|
37
|
+
<url>https://api.paymentgateway.example.com/v2</url>
|
|
38
|
+
<externalCredential>Payment_Gateway_Credential</externalCredential>
|
|
39
|
+
<allowMergeFieldsInBody>false</allowMergeFieldsInBody>
|
|
40
|
+
<allowMergeFieldsInHeader>false</allowMergeFieldsInHeader>
|
|
41
|
+
<generateAuthorizationHeader>true</generateAuthorizationHeader>
|
|
42
|
+
</NamedCredential>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```xml
|
|
46
|
+
<!-- Payment_Gateway_Credential.externalCredential-meta.xml -->
|
|
47
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
48
|
+
<ExternalCredential xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
49
|
+
<fullName>Payment_Gateway_Credential</fullName>
|
|
50
|
+
<label>Payment Gateway Credential</label>
|
|
51
|
+
<authenticationProtocol>Custom</authenticationProtocol>
|
|
52
|
+
<externalCredentialParameters>
|
|
53
|
+
<parameterName>Authorization</parameterName>
|
|
54
|
+
<parameterType>AuthHeader</parameterType>
|
|
55
|
+
</externalCredentialParameters>
|
|
56
|
+
</ExternalCredential>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## HTTP Callout Service Class
|
|
60
|
+
|
|
61
|
+
```apex
|
|
62
|
+
public with sharing class PaymentGatewayService {
|
|
63
|
+
|
|
64
|
+
private static final String NAMED_CREDENTIAL = 'callout:Payment_Gateway';
|
|
65
|
+
private static final Integer DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Charges a payment method. Returns a PaymentResult with success/failure details.
|
|
69
|
+
*/
|
|
70
|
+
public static PaymentResult chargePayment(String paymentMethodId, Decimal amount, String currency_x) {
|
|
71
|
+
Map<String, Object> requestBody = new Map<String, Object>{
|
|
72
|
+
'payment_method' => paymentMethodId,
|
|
73
|
+
'amount' => (amount * 100).intValue(), // Convert to cents
|
|
74
|
+
'currency' => currency_x,
|
|
75
|
+
'capture' => true
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
HttpResponse response = sendRequest('POST', '/charges', JSON.serialize(requestBody));
|
|
79
|
+
return parsePaymentResponse(response);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Retrieves the status of an existing charge.
|
|
84
|
+
*/
|
|
85
|
+
public static PaymentResult getChargeStatus(String chargeId) {
|
|
86
|
+
HttpResponse response = sendRequest('GET', '/charges/' + chargeId, null);
|
|
87
|
+
return parsePaymentResponse(response);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Core HTTP request method. All callouts route through here.
|
|
92
|
+
*/
|
|
93
|
+
private static HttpResponse sendRequest(String method, String path, String body) {
|
|
94
|
+
HttpRequest req = new HttpRequest();
|
|
95
|
+
req.setEndpoint(NAMED_CREDENTIAL + path);
|
|
96
|
+
req.setMethod(method);
|
|
97
|
+
req.setTimeout(DEFAULT_TIMEOUT);
|
|
98
|
+
req.setHeader('Content-Type', 'application/json');
|
|
99
|
+
req.setHeader('Accept', 'application/json');
|
|
100
|
+
req.setHeader('Idempotency-Key', generateIdempotencyKey());
|
|
101
|
+
|
|
102
|
+
if (body != null) {
|
|
103
|
+
req.setBody(body);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Http http = new Http();
|
|
107
|
+
return http.send(req);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static PaymentResult parsePaymentResponse(HttpResponse response) {
|
|
111
|
+
PaymentResult result = new PaymentResult();
|
|
112
|
+
result.statusCode = response.getStatusCode();
|
|
113
|
+
|
|
114
|
+
if (response.getStatusCode() == 200 || response.getStatusCode() == 201) {
|
|
115
|
+
Map<String, Object> responseBody = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
|
|
116
|
+
result.success = true;
|
|
117
|
+
result.chargeId = (String) responseBody.get('id');
|
|
118
|
+
result.status = (String) responseBody.get('status');
|
|
119
|
+
} else {
|
|
120
|
+
result.success = false;
|
|
121
|
+
result.errorMessage = 'HTTP ' + response.getStatusCode() + ': ' + response.getBody();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private static String generateIdempotencyKey() {
|
|
128
|
+
Blob randomBytes = Crypto.generateAesKey(128);
|
|
129
|
+
return EncodingUtil.convertToHex(randomBytes);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public class PaymentResult {
|
|
133
|
+
@AuraEnabled public Boolean success;
|
|
134
|
+
@AuraEnabled public String chargeId;
|
|
135
|
+
@AuraEnabled public String status;
|
|
136
|
+
@AuraEnabled public String errorMessage;
|
|
137
|
+
@AuraEnabled public Integer statusCode;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Retry and Error Handling
|
|
143
|
+
|
|
144
|
+
```apex
|
|
145
|
+
public with sharing class CalloutRetryHelper {
|
|
146
|
+
|
|
147
|
+
private static final Integer MAX_RETRIES = 3;
|
|
148
|
+
private static final Set<Integer> RETRYABLE_STATUS_CODES = new Set<Integer>{
|
|
149
|
+
408, 429, 500, 502, 503, 504
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Executes an HTTP request with exponential backoff retry.
|
|
154
|
+
* Only retries on transient errors (5xx, 408, 429).
|
|
155
|
+
*/
|
|
156
|
+
public static HttpResponse sendWithRetry(HttpRequest request) {
|
|
157
|
+
Integer attempts = 0;
|
|
158
|
+
HttpResponse response;
|
|
159
|
+
Http http = new Http();
|
|
160
|
+
|
|
161
|
+
while (attempts < MAX_RETRIES) {
|
|
162
|
+
attempts++;
|
|
163
|
+
try {
|
|
164
|
+
response = http.send(request);
|
|
165
|
+
|
|
166
|
+
if (!RETRYABLE_STATUS_CODES.contains(response.getStatusCode())) {
|
|
167
|
+
return response;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
System.debug(LoggingLevel.WARN,
|
|
171
|
+
'Retryable status ' + response.getStatusCode()
|
|
172
|
+
+ ' on attempt ' + attempts + ' of ' + MAX_RETRIES
|
|
173
|
+
);
|
|
174
|
+
} catch (CalloutException e) {
|
|
175
|
+
System.debug(LoggingLevel.ERROR,
|
|
176
|
+
'Callout exception on attempt ' + attempts + ': ' + e.getMessage()
|
|
177
|
+
);
|
|
178
|
+
if (attempts >= MAX_RETRIES) {
|
|
179
|
+
throw e;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Note: Apex does not support Thread.sleep(). In synchronous context,
|
|
184
|
+
// retries happen immediately. For true backoff, use Queueable chaining.
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return response;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Logs a failed callout to a custom object for monitoring and replay.
|
|
192
|
+
*/
|
|
193
|
+
public static void logFailedCallout(String endpoint, String method, String body,
|
|
194
|
+
Integer statusCode, String errorMessage) {
|
|
195
|
+
Integration_Log__c log = new Integration_Log__c(
|
|
196
|
+
Endpoint__c = endpoint,
|
|
197
|
+
Method__c = method,
|
|
198
|
+
Request_Body__c = body != null ? body.left(131072) : null, // Long text area limit
|
|
199
|
+
Status_Code__c = statusCode,
|
|
200
|
+
Error_Message__c = errorMessage,
|
|
201
|
+
Timestamp__c = Datetime.now(),
|
|
202
|
+
Status__c = 'Failed'
|
|
203
|
+
);
|
|
204
|
+
insert log;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Queueable for Async Callouts
|
|
210
|
+
|
|
211
|
+
```apex
|
|
212
|
+
public class PaymentGatewayQueueable implements Queueable, Database.AllowsCallouts {
|
|
213
|
+
|
|
214
|
+
private final Id opportunityId;
|
|
215
|
+
private final String paymentMethodId;
|
|
216
|
+
private final Decimal amount;
|
|
217
|
+
private final String currency_x;
|
|
218
|
+
|
|
219
|
+
public PaymentGatewayQueueable(Id opportunityId, String paymentMethodId,
|
|
220
|
+
Decimal amount, String currency_x) {
|
|
221
|
+
this.opportunityId = opportunityId;
|
|
222
|
+
this.paymentMethodId = paymentMethodId;
|
|
223
|
+
this.amount = amount;
|
|
224
|
+
this.currency_x = currency_x;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
public void execute(QueueableContext context) {
|
|
228
|
+
try {
|
|
229
|
+
PaymentGatewayService.PaymentResult result =
|
|
230
|
+
PaymentGatewayService.chargePayment(paymentMethodId, amount, currency_x);
|
|
231
|
+
|
|
232
|
+
Opportunity opp = new Opportunity(Id = opportunityId);
|
|
233
|
+
if (result.success) {
|
|
234
|
+
opp.Payment_Status__c = 'Charged';
|
|
235
|
+
opp.Payment_Reference__c = result.chargeId;
|
|
236
|
+
} else {
|
|
237
|
+
opp.Payment_Status__c = 'Failed';
|
|
238
|
+
opp.Payment_Error__c = result.errorMessage;
|
|
239
|
+
|
|
240
|
+
CalloutRetryHelper.logFailedCallout(
|
|
241
|
+
'Payment_Gateway/charges', 'POST',
|
|
242
|
+
JSON.serialize(new Map<String, Object>{
|
|
243
|
+
'payment_method' => paymentMethodId,
|
|
244
|
+
'amount' => amount
|
|
245
|
+
}),
|
|
246
|
+
result.statusCode, result.errorMessage
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
update opp;
|
|
250
|
+
} catch (Exception e) {
|
|
251
|
+
CalloutRetryHelper.logFailedCallout(
|
|
252
|
+
'Payment_Gateway/charges', 'POST', null, null, e.getMessage()
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Mock for Testing (HttpCalloutMock)
|
|
260
|
+
|
|
261
|
+
```apex
|
|
262
|
+
@IsTest
|
|
263
|
+
public class PaymentGatewayMock implements HttpCalloutMock {
|
|
264
|
+
|
|
265
|
+
private final Integer statusCode;
|
|
266
|
+
private final String responseBody;
|
|
267
|
+
|
|
268
|
+
public PaymentGatewayMock(Integer statusCode, String responseBody) {
|
|
269
|
+
this.statusCode = statusCode;
|
|
270
|
+
this.responseBody = responseBody;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public HttpResponse respond(HttpRequest request) {
|
|
274
|
+
HttpResponse response = new HttpResponse();
|
|
275
|
+
response.setStatusCode(statusCode);
|
|
276
|
+
response.setBody(responseBody);
|
|
277
|
+
response.setHeader('Content-Type', 'application/json');
|
|
278
|
+
return response;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Convenience factory methods for common scenarios
|
|
282
|
+
public static PaymentGatewayMock success() {
|
|
283
|
+
return new PaymentGatewayMock(200, JSON.serialize(new Map<String, Object>{
|
|
284
|
+
'id' => 'ch_test_123456',
|
|
285
|
+
'status' => 'succeeded',
|
|
286
|
+
'amount' => 5000,
|
|
287
|
+
'currency' => 'usd'
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
public static PaymentGatewayMock failure() {
|
|
292
|
+
return new PaymentGatewayMock(402, JSON.serialize(new Map<String, Object>{
|
|
293
|
+
'error' => new Map<String, Object>{
|
|
294
|
+
'type' => 'card_error',
|
|
295
|
+
'message' => 'Your card was declined'
|
|
296
|
+
}
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
public static PaymentGatewayMock serverError() {
|
|
301
|
+
return new PaymentGatewayMock(500, '{"error": "Internal server error"}');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Test Class
|
|
307
|
+
|
|
308
|
+
```apex
|
|
309
|
+
@IsTest
|
|
310
|
+
private class PaymentGatewayService_Test {
|
|
311
|
+
|
|
312
|
+
@IsTest
|
|
313
|
+
static void testChargePayment_Success() {
|
|
314
|
+
Test.setMock(HttpCalloutMock.class, PaymentGatewayMock.success());
|
|
315
|
+
|
|
316
|
+
Test.startTest();
|
|
317
|
+
PaymentGatewayService.PaymentResult result =
|
|
318
|
+
PaymentGatewayService.chargePayment('pm_test_123', 50.00, 'usd');
|
|
319
|
+
Test.stopTest();
|
|
320
|
+
|
|
321
|
+
System.assertEquals(true, result.success);
|
|
322
|
+
System.assertEquals('ch_test_123456', result.chargeId);
|
|
323
|
+
System.assertEquals('succeeded', result.status);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@IsTest
|
|
327
|
+
static void testChargePayment_Failure() {
|
|
328
|
+
Test.setMock(HttpCalloutMock.class, PaymentGatewayMock.failure());
|
|
329
|
+
|
|
330
|
+
Test.startTest();
|
|
331
|
+
PaymentGatewayService.PaymentResult result =
|
|
332
|
+
PaymentGatewayService.chargePayment('pm_test_123', 50.00, 'usd');
|
|
333
|
+
Test.stopTest();
|
|
334
|
+
|
|
335
|
+
System.assertEquals(false, result.success);
|
|
336
|
+
System.assertEquals(402, result.statusCode);
|
|
337
|
+
System.assert(result.errorMessage.contains('402'));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@IsTest
|
|
341
|
+
static void testGetChargeStatus() {
|
|
342
|
+
Test.setMock(HttpCalloutMock.class, PaymentGatewayMock.success());
|
|
343
|
+
|
|
344
|
+
Test.startTest();
|
|
345
|
+
PaymentGatewayService.PaymentResult result =
|
|
346
|
+
PaymentGatewayService.getChargeStatus('ch_test_123456');
|
|
347
|
+
Test.stopTest();
|
|
348
|
+
|
|
349
|
+
System.assertEquals(true, result.success);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
@IsTest
|
|
353
|
+
static void testQueueableCallout_Success() {
|
|
354
|
+
Opportunity opp = new Opportunity(
|
|
355
|
+
Name = 'Test Opp',
|
|
356
|
+
StageName = 'Closed Won',
|
|
357
|
+
CloseDate = Date.today()
|
|
358
|
+
);
|
|
359
|
+
insert opp;
|
|
360
|
+
|
|
361
|
+
Test.setMock(HttpCalloutMock.class, PaymentGatewayMock.success());
|
|
362
|
+
|
|
363
|
+
Test.startTest();
|
|
364
|
+
System.enqueueJob(
|
|
365
|
+
new PaymentGatewayQueueable(opp.Id, 'pm_test_123', 100.00, 'usd')
|
|
366
|
+
);
|
|
367
|
+
Test.stopTest();
|
|
368
|
+
|
|
369
|
+
Opportunity updated = [SELECT Payment_Status__c, Payment_Reference__c FROM Opportunity WHERE Id = :opp.Id];
|
|
370
|
+
System.assertEquals('Charged', updated.Payment_Status__c);
|
|
371
|
+
System.assertEquals('ch_test_123456', updated.Payment_Reference__c);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
@IsTest
|
|
375
|
+
static void testRetryHelper_RetryableStatusCode() {
|
|
376
|
+
Test.setMock(HttpCalloutMock.class, PaymentGatewayMock.serverError());
|
|
377
|
+
|
|
378
|
+
HttpRequest req = new HttpRequest();
|
|
379
|
+
req.setEndpoint('callout:Payment_Gateway/charges');
|
|
380
|
+
req.setMethod('POST');
|
|
381
|
+
req.setBody('{}');
|
|
382
|
+
|
|
383
|
+
Test.startTest();
|
|
384
|
+
HttpResponse response = CalloutRetryHelper.sendWithRetry(req);
|
|
385
|
+
Test.stopTest();
|
|
386
|
+
|
|
387
|
+
// In test context, all retries return the same mock
|
|
388
|
+
System.assertEquals(500, response.getStatusCode());
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Key Principles
|
|
394
|
+
|
|
395
|
+
- Always use Named Credentials for external endpoints; never hardcode URLs or credentials in Apex
|
|
396
|
+
- Route all HTTP requests through a single method for consistent headers, timeouts, and logging
|
|
397
|
+
- Use idempotency keys for POST/PUT requests to prevent duplicate charges on retries
|
|
398
|
+
- Implement callouts in `Queueable` (with `Database.AllowsCallouts`) to avoid governor limits in triggers
|
|
399
|
+
- Create reusable `HttpCalloutMock` implementations with factory methods for common scenarios
|
|
400
|
+
- Log failed callouts to a custom object for monitoring, alerting, and manual replay
|
|
401
|
+
|
|
402
|
+
## Common Pitfalls
|
|
403
|
+
|
|
404
|
+
- Hardcoding API keys or endpoints instead of using Named Credentials (security risk and deployment headache)
|
|
405
|
+
- Making callouts in trigger context without wrapping in a `Queueable` or `@future` method
|
|
406
|
+
- Not setting `Test.setMock` before making callouts in tests, which causes "uncommitted work pending" errors
|
|
407
|
+
- Forgetting `Database.AllowsCallouts` on the `Queueable` class, which throws a runtime exception
|
|
408
|
+
- Not handling non-JSON error responses (some APIs return HTML on 500 errors)
|
|
409
|
+
- Exceeding the 100-callout-per-transaction limit in batch jobs without tracking callout count
|
|
410
|
+
|
|
411
|
+
## SCC Skills
|
|
412
|
+
|
|
413
|
+
- `/sf-security` -- verify Named Credential usage and no hardcoded secrets
|
|
414
|
+
- `/sf-apex-best-practices` -- review callout service code for best practices
|
|
415
|
+
- `/sf-tdd-workflow` -- write tests first using mock classes
|
|
416
|
+
- `/sf-governor-limits` -- check callout limits in async and batch contexts
|