@salesforce/afv-skills 1.45.0 → 1.47.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/package.json +1 -1
- package/skills/agentforce-observe/SKILL.md +32 -4
- package/skills/agentforce-observe/references/ahm-alerts.md +719 -0
- package/skills/automation-flow-generate/SKILL.md +11 -5
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +275 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/README.md +32 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls +75 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls-meta.xml +5 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/interview-answers.json +13 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +10 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/create.json +20 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/update.json +16 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +273 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +236 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/reference-example-set-comment-value.md +132 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +211 -0
- package/skills/dx-code-analyzer-configure/scripts/validate-config.sh +14 -10
- package/skills/dx-code-analyzer-run/scripts/apply-fixes.js +45 -4
- package/skills/dx-code-analyzer-run/scripts/describe-rule.js +52 -32
- package/skills/dx-devops-project-manage/SKILL.md +197 -0
- package/skills/dx-devops-project-manage/examples/common-workflows.md +197 -0
- package/skills/dx-devops-project-manage/references/cli-commands.md +295 -0
- package/skills/dx-devops-project-manage/scripts/create-project.sh +48 -0
- package/skills/dx-devops-project-manage/scripts/list-projects.sh +51 -0
- package/skills/dx-devops-project-manage/scripts/update-project.sh +96 -0
- package/skills/education-cloud-academic-calendar-generate/SKILL.md +225 -0
- package/skills/education-cloud-academic-calendar-generate/examples/quarter-calendar.json +47 -0
- package/skills/education-cloud-academic-calendar-generate/examples/sample-output.md +57 -0
- package/skills/education-cloud-academic-calendar-generate/examples/semester-calendar.json +54 -0
- package/skills/education-cloud-academic-calendar-generate/references/calendar-systems.md +127 -0
- package/skills/education-cloud-academic-calendar-generate/references/date-validation.md +222 -0
- package/skills/education-cloud-academic-calendar-generate/references/foundation_prerequisites.md +40 -0
- package/skills/education-cloud-academic-calendar-generate/scripts/validate_calendar_dates.py +143 -0
- package/skills/education-cloud-course-catalog-migrate/SKILL.md +321 -0
- package/skills/education-cloud-course-catalog-migrate/references/gotchas-detail.md +16 -0
- package/skills/education-cloud-course-catalog-migrate/references/gotchas.md +16 -0
- package/skills/education-cloud-course-catalog-migrate/references/large-catalog-handling.md +42 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/batch_courses.py +36 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/detect_linked_courses.py +51 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/detect_modality_variants.py +48 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/resolve_api_version.py +43 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/split_course_code.py +39 -0
- package/skills/education-cloud-course-catalog-migrate/scripts/validate_completeness.py +54 -0
- package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +3 -5
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +177 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/agent-and-subagents.md +151 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/customer-narration.md +34 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/execution-model.md +54 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/flows.md +82 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/grounding.md +199 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/permissions.md +183 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/platform-enablement.md +82 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +158 -0
- package/skills/education-cloud-student-recruitment-agent-configure/references/routing.md +141 -0
- package/skills/experience-cms-brand-apply/SKILL.md +5 -5
- package/skills/experience-cms-brand-create/SKILL.md +2 -2
- package/skills/experience-cms-content-generate/SKILL.md +1 -0
- package/skills/experience-cms-content-render/SKILL.md +173 -0
- package/skills/experience-cms-content-render/assets/angular/DetailPage.component.ts +25 -0
- package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +133 -0
- package/skills/experience-cms-content-render/assets/angular/TypeList.component.ts +38 -0
- package/skills/experience-cms-content-render/assets/angular/TypeRenderer.component.ts +90 -0
- package/skills/experience-cms-content-render/assets/angular/cms-content.component.ts +248 -0
- package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +100 -0
- package/skills/experience-cms-content-render/assets/react/DetailPage.tsx +20 -0
- package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +129 -0
- package/skills/experience-cms-content-render/assets/react/TypeList.tsx +40 -0
- package/skills/experience-cms-content-render/assets/react/TypeRenderer.tsx +64 -0
- package/skills/experience-cms-content-render/assets/react/heuristicRenderer.tsx +310 -0
- package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +129 -0
- package/skills/experience-cms-content-render/assets/shared/cmsContentType.ts +49 -0
- package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +96 -0
- package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +55 -0
- package/skills/experience-cms-content-render/references/bulk-loading.md +60 -0
- package/skills/experience-cms-content-render/references/codegen-guardrails.md +111 -0
- package/skills/experience-cms-content-render/references/detail-pages.md +87 -0
- package/skills/experience-cms-content-render/references/embed-recipes.md +127 -0
- package/skills/experience-cms-content-render/references/failure-modes.md +96 -0
- package/skills/experience-cms-content-render/references/heuristic-render-rules.md +131 -0
- package/skills/experience-cms-content-render/references/init-scaffold.md +122 -0
- package/skills/experience-cms-content-render/references/interaction-model.md +173 -0
- package/skills/experience-cms-content-render/references/package-api.md +106 -0
- package/skills/experience-cms-content-render/references/schema-sync.md +114 -0
- package/skills/experience-cms-content-render/references/styling-scopes.md +65 -0
- package/skills/experience-cms-content-render/references/verify.md +49 -0
- package/skills/experience-cms-content-type-generate/SKILL.md +2 -2
- package/skills/experience-content-media-stock-image-search/SKILL.md +5 -4
- package/skills/experience-search-coordinate/SKILL.md +198 -0
- package/skills/experience-search-coordinate/assets/search-payload-template.json +25 -0
- package/skills/experience-search-coordinate/references/content-route.md +313 -0
- package/skills/experience-search-coordinate/references/content-type-discovery.md +57 -0
- package/skills/experience-search-coordinate/references/media-route.md +172 -0
- package/skills/experience-search-coordinate/references/scope-resolution.md +14 -0
- package/skills/experience-ui-bundle-localize/SKILL.md +1 -1
- package/skills/experience-ui-bundle-localize/references/i18n-setup.md +5 -3
- package/skills/experience-ui-bundle-project-generate/SKILL.md +18 -14
- package/skills/experience-ui-bundle-project-generate/references/angular-project-generate.md +22 -0
- package/skills/experience-ui-bundle-project-generate/references/react-project-generate.md +20 -0
- package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +58 -54
- package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +6 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +2 -2
- package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +6 -0
- package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +2 -1
- package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +6 -0
- package/skills/experience-ui-bundle-site-generate/SKILL.md +59 -8
- package/skills/experience-ui-bundle-site-generate/references/configure-metadata-digital-experience.md +8 -3
- package/skills/experience-ui-bundle-site-generate/references/configure-metadata-language-settings.md +120 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +336 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +143 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +127 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-deploy-commands.md +116 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +111 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-overview.md +312 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +171 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-state-tracking.md +64 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-trigger-handlers.md +122 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-overview.md +335 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-user-provisioning-details.md +140 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +196 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-metadata-cache-generation.md +155 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +307 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +211 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/references/state-machine-and-changes.md +108 -0
- package/skills/life-sciences-kam-coordinate/SKILL.md +241 -0
- package/skills/life-sciences-kam-coordinate/references/orchestration-flow.md +152 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +79 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-deploy-commands.md +131 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-kam-config-records.md +85 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +112 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-overview.md +202 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +67 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-state-tracking.md +65 -0
- package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-trigger-handlers.md +123 -0
- package/skills/life-sciences-kam-coordinate/references/stage-4-participant-role-and-sprint.md +89 -0
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +337 -0
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +248 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-ipad-validation-script.md +35 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-metadata-cache-generation.md +155 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-details.md +146 -0
- package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-overview.md +89 -0
- package/skills/life-sciences-kam-coordinate/references/state-machine-and-changes.md +114 -0
- package/skills/life-sciences-prerequisites-validate/SKILL.md +138 -0
- package/skills/life-sciences-prerequisites-validate/references/checks-org-settings.md +190 -0
- package/skills/life-sciences-prerequisites-validate/references/checks-user-and-package.md +211 -0
- package/skills/life-sciences-territory-configure/SKILL.md +217 -0
- package/skills/life-sciences-territory-configure/references/territory-metadata.md +262 -0
- package/skills/platform-apex-logs-debug/SKILL.md +7 -7
- package/skills/platform-custom-application-generate/SKILL.md +4 -4
- package/skills/platform-custom-object-generate/SKILL.md +7 -7
- package/skills/platform-custom-tab-generate/SKILL.md +1 -1
- package/skills/platform-dsar-policy-manage/SKILL.md +272 -0
- package/skills/platform-dsar-policy-manage/references/configure.md +106 -0
- package/skills/platform-dsar-policy-manage/references/export-and-history.md +123 -0
- package/skills/platform-dsar-policy-manage/references/gap-analysis-guide.md +150 -0
- package/skills/platform-dsar-policy-manage/references/gap-scan.md +129 -0
- package/skills/platform-dsar-policy-manage/references/headless-sor.md +59 -0
- package/skills/platform-dsar-policy-manage/references/report-format.md +59 -0
- package/skills/platform-dsar-policy-manage/scripts/tests/__init__.py +0 -0
- package/skills/platform-dsar-policy-manage/scripts/tests/test_validate_policy_tree.py +76 -0
- package/skills/platform-dsar-policy-manage/scripts/validate-policy-tree.py +130 -0
- package/skills/platform-flexipage-generate/SKILL.md +4 -0
- package/skills/platform-list-view-generate/SKILL.md +1 -0
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +359 -0
- package/skills/platform-salesforce-connect-adapter-generate/references/official-examples.md +69 -0
- package/skills/platform-salesforce-connect-adapter-generate/references/scenarios.md +187 -0
- package/skills/platform-soql-query/SKILL.md +8 -8
- package/skills/platform-value-set-generate/SKILL.md +2 -2
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +20 -27
- package/skills/service-native-voice-recording-transcription-configure/SKILL.md +47 -27
- package/skills/service-native-voice-recording-transcription-configure/references/thunderbird-voice-settings.md +13 -9
- package/skills/service-native-voice-recording-transcription-configure/scripts/enable-recording-transcription.sh +104 -45
package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Conventions & payload rules (Promotion BO API)
|
|
2
|
+
|
|
3
|
+
Load-bearing conventions and the eight cross-workflow payload rules the
|
|
4
|
+
`BO_API_Step_Input_Structure__c` schema alone cannot express. The main
|
|
5
|
+
`SKILL.md` references this file from Phase 5b (registering input paths) and
|
|
6
|
+
Phase 6 (composing/validating smoke payloads).
|
|
7
|
+
|
|
8
|
+
## CLI shape
|
|
9
|
+
|
|
10
|
+
- `<alias>` — target org `sf` alias. Required.
|
|
11
|
+
- `--sales-org <4-char>` — default `0001`. Must match `^[A-Z0-9]{4}$` (mirrors `TPMSetupData.validateSalesOrg`).
|
|
12
|
+
- `--dry-run` — emit artifacts to `./out/`, skip destructive operations.
|
|
13
|
+
- `--yes` — skip confirmation gates (for CI callers).
|
|
14
|
+
- `--preset set-comment-value` / `--interview-file <path>` — bypass the interactive Phase 5 interview.
|
|
15
|
+
|
|
16
|
+
## Runtime artifacts
|
|
17
|
+
|
|
18
|
+
- The skill writes to `./out/` inside its own directory.
|
|
19
|
+
- Rollback artifacts follow `./out/rollback-<subject>-<UTC-timestamp>.apex`.
|
|
20
|
+
- Before-snapshots follow `./out/<subject>-before.<ext>`.
|
|
21
|
+
|
|
22
|
+
## Sales-org partitioning (pre-parameter for every lookup)
|
|
23
|
+
|
|
24
|
+
`--sales-org` is not just a filter — it is the tenancy boundary for **every
|
|
25
|
+
reference lookup** the skill performs. Before any Promotion/Tactic/Product-filter
|
|
26
|
+
payload is composed, every referenced entity MUST be resolved against records
|
|
27
|
+
that carry the same `<prefix>__Sales_Org__c` (namespace resolved at runtime per
|
|
28
|
+
`PREFIX_UNDER` from Phase 1; e.g. `cgcloud__` / `cgcloud_dev__` in installed
|
|
29
|
+
subscriber orgs, or the dev-package prefix).
|
|
30
|
+
|
|
31
|
+
| Reference in payload | SObject to query | Filter |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `PromotionTemplate` | `<prefix>__Promotion_Template__c` | `Name = <val> AND <prefix>__Sales_Org__c = <sales-org>` |
|
|
34
|
+
| `Tactics[*].TacticTemplate` | `<prefix>__Tactic_Template__c` | `Name = <val> AND <prefix>__Sales_Org__c = <sales-org>` (and linked to the chosen PromotionTemplate via `<prefix>__Promotion_Template_Tactic_Template__c`) |
|
|
35
|
+
| `AnchorAccount` | `Account` | `<prefix>__ExternalId__c = <val> AND <prefix>__Sales_Org__c = <sales-org>` |
|
|
36
|
+
| `ProductFilter.Criteria.Brand[*]`, `.Category[*]`, `.Subcategory[*]`, `.Flavor[*]`, `.Package[*]` | `<prefix>__Product_Attribute__c` (or Category equivalents) | `Name = <val> AND <prefix>__Sales_Org__c = <sales-org>` |
|
|
37
|
+
| `ProductFilter.{Included,Excluded}Products[*]` | `Product2` | matched by external key AND `<prefix>__Sales_Org__c = <sales-org>` |
|
|
38
|
+
| `PromotionAccountSet` / `AnchorAccountSet` | `<prefix>__Account_Set__c` | `Name = <val> AND <prefix>__Sales_Org__c = <sales-org>` |
|
|
39
|
+
|
|
40
|
+
Fail-fast — before any REST call — if any user-supplied reference does not
|
|
41
|
+
resolve inside the sales org. "Belongs to another sales org" and "does not
|
|
42
|
+
exist" are the same error class for the user; report the sales org in the message.
|
|
43
|
+
|
|
44
|
+
## Payload contract derivation (schema-first, before user input)
|
|
45
|
+
|
|
46
|
+
Never hard-code create/update/copy payload shape. The BO API workflow steps
|
|
47
|
+
themselves declare the accepted paths. Before interviewing the user or accepting
|
|
48
|
+
an intent JSON, retrieve the contract from the org:
|
|
49
|
+
|
|
50
|
+
```sql
|
|
51
|
+
SELECT sis.<prefix>__Path__c,
|
|
52
|
+
sis.<prefix>__Required__c,
|
|
53
|
+
sis.<prefix>__Maps_To__c,
|
|
54
|
+
sis.<prefix>__Maximum_Items__c, sis.<prefix>__Minimum_Items__c,
|
|
55
|
+
sis.<prefix>__Maximum_Length__c, sis.<prefix>__Minimum_Length__c,
|
|
56
|
+
sis.<prefix>__Pattern__c,
|
|
57
|
+
sis.<prefix>__BO_API_Workflow_Step__r.Name step
|
|
58
|
+
FROM <prefix>__BO_API_Step_Input_Structure__c sis
|
|
59
|
+
WHERE sis.<prefix>__BO_API_Workflow_Step__c IN (
|
|
60
|
+
SELECT <prefix>__BO_API_Workflow_Step__c
|
|
61
|
+
FROM <prefix>__BO_API_Workflow_Workflow_Step__c
|
|
62
|
+
WHERE <prefix>__BO_API_Workflow__r.Name = '<workflow>'
|
|
63
|
+
AND <prefix>__BO_API_Workflow__r.<prefix>__BO_API__r.Name = '<entity>'
|
|
64
|
+
AND <prefix>__BO_API_Workflow__r.<prefix>__BO_API__r.<prefix>__Sales_Org__c = '<sales-org>'
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Merge rows across steps → the required set is the union of `Required__c = true`
|
|
69
|
+
paths; the optional set is everything else. The skill's interview and
|
|
70
|
+
mass-upload template are BOTH generated from this contract, per invocation. Two
|
|
71
|
+
additional constraints the schema alone cannot express and that the skill must
|
|
72
|
+
layer on:
|
|
73
|
+
|
|
74
|
+
- **Reference-data existence and sales-org membership.** Every non-primitive value (template name, external id, category name) must resolve per the table in "Sales-org partitioning" above.
|
|
75
|
+
- **Template-conditional required fields.** Some fields are required only for specific `PromotionTemplate` values (e.g. `AnchorAccount` is required for `Customer Promotion` even though the schema declares it optional). Detect these from `mapPromotionValues*` variants, `setPromotionAnchor*` variants, and by pre-flighting a validate call against the target template — do not hard-code the list; template membership can change.
|
|
76
|
+
|
|
77
|
+
Where mass upload is offered, provide a CSV/JSONL template whose columns are
|
|
78
|
+
exactly the union of paths from the derived contract (with `[R]` markers on
|
|
79
|
+
required columns).
|
|
80
|
+
|
|
81
|
+
## Promotion payload — cross-workflow rules the schema cannot express
|
|
82
|
+
|
|
83
|
+
The `BO_API_Step_Input_Structure__c` contract declares WHICH keys the workflow
|
|
84
|
+
accepts and which are `Required__c=true`, but eight additional constraints govern
|
|
85
|
+
whether a create/update/copy/derive call will actually succeed. Enforce them
|
|
86
|
+
BEFORE the REST call — otherwise the request 202s, then dies asynchronously
|
|
87
|
+
inside a workflow step with a `BO_API_Transaction_Log__c.Status__c = 'Error'`.
|
|
88
|
+
|
|
89
|
+
### R1. Always-required create fields (`DateFrom`, `DateThru`, `Slogan`)
|
|
90
|
+
|
|
91
|
+
Beyond the schema-declared requireds, `Promotion2BoApiCoreWorkflowSteps.setPromotionDates`
|
|
92
|
+
throws `PBACWS0008` when a new promotion is missing `DateFrom` or `DateThru`.
|
|
93
|
+
`Slogan` is required by the CSV row `SIS-068` on `mapPromotionValues2`. All three
|
|
94
|
+
are create-workflow only — update omits them.
|
|
95
|
+
|
|
96
|
+
`DateThru` is NOT derivable by the framework. When user intent is "N weeks
|
|
97
|
+
starting D" (e.g. "6 weeks starting 2026-08-01"), the skill MUST compute it
|
|
98
|
+
client-side:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
DateThru = DateFrom + (duration_weeks * 7) - 1
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Never send a `durationWeeks` — the workflow does not accept it.
|
|
105
|
+
`PlacementDateFrom/OrderDateFrom/DeliveryDateFrom` (and their `-Thru` siblings)
|
|
106
|
+
default to the top-level dates on new promotions; do not need to be sent.
|
|
107
|
+
|
|
108
|
+
### R2. Terminal status is `Calculated`, not `Processed`
|
|
109
|
+
|
|
110
|
+
`BO_API_Transaction_Log__c.Status__c` is a restricted picklist: `Initial,
|
|
111
|
+
Written, ToBeCalculated, Calculated, Error`. `Processed` is NOT in the picklist
|
|
112
|
+
and is never set anywhere in the codebase. The success predicate for every
|
|
113
|
+
workflow (`create`/`update`/`copy`/`derive`) is `Status__c = 'Calculated'`; the
|
|
114
|
+
pending predicate is `Status__c != 'Calculated' AND Status__c != 'Error'`. Any
|
|
115
|
+
poll of `/status` or the transaction log MUST match on `Calculated` — matching
|
|
116
|
+
on `Processed` hangs forever.
|
|
117
|
+
|
|
118
|
+
### R3. ProductFilter criteria keys are level NAMES, not field API names
|
|
119
|
+
|
|
120
|
+
The payload sends level NAMES verbatim as the `ProductFilter.Criteria` keys —
|
|
121
|
+
`Category`, `SubCategory`, `Brand`, `Flavor`, `Package` — NOT Product2 field API
|
|
122
|
+
names. The framework consults `Product_Levels_Mapping__mdt` internally to
|
|
123
|
+
translate. Sending a Product2 field API name (e.g. `<prefix>__Criterion_1_Product__c`)
|
|
124
|
+
as a key fails schema validation with `"Properties other than those defined were
|
|
125
|
+
not expected at #.ProductFilter.Criteria.<key>"`.
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
"ProductFilter": {
|
|
129
|
+
"Criteria": {
|
|
130
|
+
"Category": ["Snacks"],
|
|
131
|
+
"Brand": ["YetiBar"]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`Product_Levels_Mapping__mdt` still matters for two skill tasks:
|
|
137
|
+
1. **Enumerate levels available in this sales org** — a level not present in the mapping for the sales org, and not in the fallback (`Category`, `SubCategory`, `Brand`, `Flavor`, `Package`), cannot be sent.
|
|
138
|
+
2. **SOQL verification after ingest** — translate the level back to its Product2 field to build the verify SOQL.
|
|
139
|
+
|
|
140
|
+
Default mapping (fallback and every seeded metadata row):
|
|
141
|
+
|
|
142
|
+
| Level name (payload key) | Product2 field (verify SOQL target) |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `Category` | `<prefix>__Criterion_1_Product__c` (invariant across every sales org) |
|
|
145
|
+
| `SubCategory` | `<prefix>__Criterion_2_Product__c` |
|
|
146
|
+
| `Brand` | `<prefix>__Criterion_3_Product__c` |
|
|
147
|
+
| `Flavor` | `<prefix>__Criterion_4_Product__c` |
|
|
148
|
+
| `Package` | `<prefix>__Criterion_5_Product__c` |
|
|
149
|
+
|
|
150
|
+
Discovery SOQL:
|
|
151
|
+
```sql
|
|
152
|
+
SELECT <prefix>__Product_Level_Name__c, <prefix>__Product_SObject_Field__c, <prefix>__Hierarchy_Level__c
|
|
153
|
+
FROM <prefix>__Product_Levels_Mapping__mdt
|
|
154
|
+
WHERE <prefix>__Sales_Org__c = :salesOrg
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### R4. Anchor account must have a `Plan` Customer Extension valid for the promotion period
|
|
158
|
+
|
|
159
|
+
Promotions can only be created for accounts with `Account_Plan_Type__c = 'Plan'`
|
|
160
|
+
on `Account_Extension__c` (label "Customer Extension") for the target sales org,
|
|
161
|
+
with `Promotion_Valid_From__c <= DateFrom` AND `Promotion_Valid_Thru__c >=
|
|
162
|
+
DateThru`. The create path validates the anchor exists in the sales org but does
|
|
163
|
+
NOT enforce Plan+dates — the skill MUST pre-flight. The object API name is
|
|
164
|
+
`Account_Extension__c` even though the label reads "Customer Extension"; there is
|
|
165
|
+
no `Customer_Extension__c` object.
|
|
166
|
+
|
|
167
|
+
```sql
|
|
168
|
+
SELECT Id, <prefix>__Account__c
|
|
169
|
+
FROM <prefix>__Account_Extension__c
|
|
170
|
+
WHERE <prefix>__Account__c = :anchorId
|
|
171
|
+
AND <prefix>__Account_Plan_Type__c = 'Plan'
|
|
172
|
+
AND <prefix>__Sales_Org__c = :salesOrg
|
|
173
|
+
AND <prefix>__Promotion_Valid_From__c <= :dateFrom
|
|
174
|
+
AND <prefix>__Promotion_Valid_Thru__c >= :dateThru
|
|
175
|
+
LIMIT 1
|
|
176
|
+
```
|
|
177
|
+
Zero rows → stop with `"Account <anchor> is not enrolled as a Plan account in sales org <salesOrg> for the period <dateFrom>..<dateThru>"`.
|
|
178
|
+
|
|
179
|
+
### R5. ManualInputs KPIs are `editable` / `editable_calculated` on the template's KPI Set
|
|
180
|
+
|
|
181
|
+
`ManualInputs[]` accepts only KPIs the template's `KPI_Set__c` includes, and only
|
|
182
|
+
those whose `RecordType.DeveloperName` is `editable` or `editable_calculated`.
|
|
183
|
+
Any other KPI is rejected by `Promotion2BoApiCoreWorkflowSteps.validateManualInputKPI`.
|
|
184
|
+
Read the allowed set from the org — do not hard-code. If the template's
|
|
185
|
+
`KPI_Set__c` is null, ManualInputs are silently skipped (framework logs
|
|
186
|
+
`PBACWS0018`) — ask the user before proceeding.
|
|
187
|
+
|
|
188
|
+
The base object is `<prefix>__KPI_Definition__c` (NOT `KPI__c`).
|
|
189
|
+
`KPI_Definition__c` and `KPI_Set__c` have no `Sales_Org__c` field — filter via
|
|
190
|
+
`Promotion_Template__c.Sales_Org__c`.
|
|
191
|
+
|
|
192
|
+
```sql
|
|
193
|
+
SELECT kd.Id, kd.Name, kd.<prefix>__Editable_Measure_Code__c,
|
|
194
|
+
kd.<prefix>__Compound_Main__c, kd.<prefix>__Editable_Fixed_Totals__c,
|
|
195
|
+
kd.<prefix>__BoM_Scope__c, kd.RecordType.DeveloperName
|
|
196
|
+
FROM <prefix>__KPI_Definition__c kd
|
|
197
|
+
WHERE Id IN (
|
|
198
|
+
SELECT <prefix>__KPI_Definition__c FROM <prefix>__KPI_Set_KPI_Definition__c
|
|
199
|
+
WHERE <prefix>__KPI_Set__c IN (
|
|
200
|
+
SELECT <prefix>__KPI_Set__c FROM <prefix>__Promotion_Template__c
|
|
201
|
+
WHERE Id = :promotionTemplateId AND <prefix>__Sales_Org__c = :salesOrg))
|
|
202
|
+
AND RecordType.DeveloperName IN ('editable','editable_calculated')
|
|
203
|
+
```
|
|
204
|
+
Framework-enforced extras (fail on the REST call, not pre-flight): `editable`
|
|
205
|
+
KPIs with `Editable_Fixed_Totals__c=true` are rejected; `editable_calculated`
|
|
206
|
+
KPIs must have `Compound_Main__c=true` unless BoM components. The payload's
|
|
207
|
+
`ManualInputs[].KPI` value is `KPI_Definition__c.Name`; the downstream measure
|
|
208
|
+
code is `Editable_Measure_Code__c`.
|
|
209
|
+
|
|
210
|
+
### R6. Derive / Copy required fields — read them from the workflow metadata
|
|
211
|
+
|
|
212
|
+
Do not hard-code the required set for `copy` and `derive`; derive them from
|
|
213
|
+
`BO_API_Step_Input_Structure__c` (`Required__c=true`) the same way as `create`,
|
|
214
|
+
then layer the Apex-only preconditions:
|
|
215
|
+
|
|
216
|
+
**Copy:**
|
|
217
|
+
- Schema-required: `.Id` (source promotion).
|
|
218
|
+
- Apex-only: source promotion's template must have `Is_Copyable__c = true`; template `Promotion_Type__c` must equal `'Promotion'`; if template `Anchor_Type__c = 'Customer'`, `.AnchorAccount` must resolve in the sales org; if `'CustomerSet'`, `.AnchorAccountSet` must resolve.
|
|
219
|
+
|
|
220
|
+
**Derive:**
|
|
221
|
+
- Schema-required: `.Id`, `.PromotionTemplate`, `.AnchorAccount` — all at the derive input root (`currentInput`), not nested (`PBACWS_REQUIRED_PROP_MISSING` fires against `Path__c` verbatim).
|
|
222
|
+
- Apex-only: source template `Is_Derivable__c = true`; a `Promotion_Template_Hierarchy__c` row with `Usage__c='Derive'` and `Child_Promotion_Template__c` = target; target template `Promotion_Type__c = 'Promotion'`; `.AnchorAccount`/`.AnchorAccountSet` valid within the promotion date range (recurses to R4).
|
|
223
|
+
|
|
224
|
+
> Note: **this skill wires only `{create, update, copy}`; `derive` is out of scope.** R6's derive detail is here for completeness / a follow-up skill.
|
|
225
|
+
|
|
226
|
+
If any Apex-only precondition is not derivable from the org, treat the field as mandatory in the interview.
|
|
227
|
+
|
|
228
|
+
### R7. Tactic templates must be linked to the promotion template AND share its sales org
|
|
229
|
+
|
|
230
|
+
`Tactics[*].TacticTemplate` must resolve to a `Tactic_Template__c` in the target
|
|
231
|
+
sales org AND be linked to the chosen `PromotionTemplate` via the
|
|
232
|
+
`Promotion_Template_Tactic_Template__c` junction (no `Sales_Org__c` of its own;
|
|
233
|
+
Name is auto-number `PTTT-{0000}`) — filter through both parents:
|
|
234
|
+
|
|
235
|
+
```sql
|
|
236
|
+
SELECT Id
|
|
237
|
+
FROM <prefix>__Promotion_Template_Tactic_Template__c
|
|
238
|
+
WHERE <prefix>__Promotion_Template__r.Name = :promotionTemplateName
|
|
239
|
+
AND <prefix>__Promotion_Template__r.<prefix>__Sales_Org__c = :salesOrg
|
|
240
|
+
AND <prefix>__Tactic_Template__r.Name = :tacticTemplateName
|
|
241
|
+
AND <prefix>__Tactic_Template__r.<prefix>__Sales_Org__c = :salesOrg
|
|
242
|
+
LIMIT 1
|
|
243
|
+
```
|
|
244
|
+
Zero rows → stop with `"Tactic template <tt> is not linked to promotion template <pt> in sales org <salesOrg>"`.
|
|
245
|
+
|
|
246
|
+
### R8. PromotionTemplate must be `Promotion_Type__c = 'Promotion'` — reject Sellable/Retail templates
|
|
247
|
+
|
|
248
|
+
`Promotion2BoApiCoreCommon.cls:867-875` throws `CPC0009` when the resolved
|
|
249
|
+
`PromotionTemplate.Promotion_Type__c != 'Promotion'`. Sellable/Retail templates
|
|
250
|
+
(`New Item Introduction`, `Selectable Promotion`, `Distributor Promotion`,
|
|
251
|
+
`Regional Promotion`, `Consumer Promotion`, `Campaign Promotion`) carry a
|
|
252
|
+
different type and belong to the retail sellable-promotion path.
|
|
253
|
+
|
|
254
|
+
On create the failure surfaces as an off-platform `WriteError` with a misleading
|
|
255
|
+
message (`Invalid JSON! /productfilter must be object`, HTTP 400 from the
|
|
256
|
+
Hyperforce CGCPS validator) — it blames `/productfilter` but the actual defect is
|
|
257
|
+
the template type. The user-facing error does NOT name the template type, so the
|
|
258
|
+
skill MUST fail-fast on template type before ingest.
|
|
259
|
+
|
|
260
|
+
```sql
|
|
261
|
+
SELECT Id, Name, <prefix>__Promotion_Type__c
|
|
262
|
+
FROM <prefix>__Promotion_Template__c
|
|
263
|
+
WHERE Name IN :templateNames
|
|
264
|
+
AND <prefix>__Sales_Org__c = :salesOrg
|
|
265
|
+
```
|
|
266
|
+
Reject any row where `Promotion_Type__c != 'Promotion'` with:
|
|
267
|
+
`"Promotion template <name> has Promotion_Type__c='<type>'; this workflow only supports templates with Promotion_Type__c='Promotion'. Sellable Promotions belong to the retail path."`
|
|
268
|
+
|
|
269
|
+
## Wire-format quirks
|
|
270
|
+
|
|
271
|
+
- `POST /promotions/initialize` deserializes lowercase `salesorg` / `nrofitems`. Always emit lowercase.
|
|
272
|
+
- `BO_API_Workflow_Workflow_Step__c.Enabled__c` is not honored at runtime. To disable a step, DELETE the junction; do not flip the boolean.
|
|
273
|
+
- Success matches `Status__c = 'Calculated'` (R2). `Processed` is not a valid status; polling for it will hang.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Phase 5a / 5b — Generate + deploy the Apex class, register + wire the step
|
|
2
|
+
|
|
3
|
+
Detailed runbook for the two write phases. `SKILL.md` Phase 5a/5b link here.
|
|
4
|
+
Both phases run only on a green Phase 4 and a confirmed Phase 5 interview.
|
|
5
|
+
`PREFIX_DOT` / `PREFIX_UNDER` come from the Phase-1 namespace detection.
|
|
6
|
+
|
|
7
|
+
## Phase 5a — Generate + deploy the customer's Apex class
|
|
8
|
+
|
|
9
|
+
1. **Idempotency guard** — is the class name already taken?
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
sf data query --target-org <alias> --json --query "
|
|
13
|
+
SELECT NamespacePrefix FROM ApexClass WHERE Name = '<stepName>'
|
|
14
|
+
"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Non-empty and non-namespaced → an ApexClass by that name already exists in
|
|
18
|
+
the customer's namespace. Ask: overwrite (redeploy same name), rename the
|
|
19
|
+
interview answer, or abort. Namespaced → a *packaged* class with that name
|
|
20
|
+
(unexpected); stop and ask before overwriting anything.
|
|
21
|
+
|
|
22
|
+
2. **Confirm target-field writeability.** For every entry in `outputWrites`,
|
|
23
|
+
describe the resolved field `${PREFIX_UNDER}<field>` on
|
|
24
|
+
`${PREFIX_UNDER}<entity>__c` and verify `createable=true, updateable=true`
|
|
25
|
+
for the running user. Any failure → stop, print the failing field + FLS
|
|
26
|
+
remediation.
|
|
27
|
+
|
|
28
|
+
3. **Generate the Apex class from the interview.** The generator takes the
|
|
29
|
+
schema captured in the interview (Phase 5.1) and emits a namespace-agnostic
|
|
30
|
+
Callable in the same shape as the reference example
|
|
31
|
+
(`references/reference-example-set-comment-value.md`). Rules (do not deviate):
|
|
32
|
+
- Declare the class **`global`** and the `call` method **`global`** — `global with sharing class <stepName> implements Callable { global Object call(...) }`. This is REQUIRED, not stylistic: on a managed-INSTALLED org the packaged `CallableManager` resolves/invokes this subscriber class across the namespace boundary via `Type.forName`, which cannot see a `public` class — the step then fails at *runtime* with `Type "<stepName>" does not exist` and the transaction-log row lands in `Error` (the class still shows `Active`, so this is invisible until an ingest runs). `public` only works on a same-namespace source/dev org (e.g. a `cgcloud_dev` scratch org); `global` works on both. Inner exception types may stay `public`.
|
|
33
|
+
- `implements Callable`; single `Object call(String action, Map<String, Object> context)` method.
|
|
34
|
+
- Guard `action != <actionName>` with a class-specific exception.
|
|
35
|
+
- Read `context.get('currentInput')` (Map) and `context.get('currentOutput')` (SObject) — these are the keys `BoTransformationEngine` seeds (verified at `BoTransformationEngine.cls:825-864`).
|
|
36
|
+
- Compute the namespace prefix from `String.valueOf(output.getSObjectType())` at runtime (see the reference example for the exact substring logic). Never emit a compile-time `<ns>__` prefix.
|
|
37
|
+
- For each `outputWrite`, resolve the source value from `currentInput` (via the `inputPaths` map), apply any preconditions, then call `output.put(prefix + '<field>', ...)`.
|
|
38
|
+
- Return `null`.
|
|
39
|
+
|
|
40
|
+
Materialize the generated source into a **project-relative** working tree —
|
|
41
|
+
never `/tmp`. Generated deployment artifacts must stay inside the project so
|
|
42
|
+
the workflow is portable and auditable (add `./.promotion-bo-api-deploy/` to
|
|
43
|
+
`.gitignore`; it is ephemeral scratch):
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
./.promotion-bo-api-deploy/
|
|
47
|
+
sfdx-project.json
|
|
48
|
+
force-app/main/default/classes/
|
|
49
|
+
<stepName>.cls
|
|
50
|
+
<stepName>.cls-meta.xml
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`sfdx-project.json`:
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"packageDirectories": [{ "path": "force-app", "default": true }],
|
|
57
|
+
"sourceApiVersion": "60.0"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`<stepName>.cls-meta.xml`:
|
|
62
|
+
```xml
|
|
63
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
64
|
+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
65
|
+
<apiVersion>60.0</apiVersion>
|
|
66
|
+
<status>Active</status>
|
|
67
|
+
</ApexClass>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
4. **Show the user the generated class + confirm.** Print the full
|
|
71
|
+
`<stepName>.cls` body and ask "Deploy this? [y/N]". `n` → save to
|
|
72
|
+
`./.promotion-bo-api-deploy/` and exit; the user can hand-edit and re-run
|
|
73
|
+
with `--interview-file` pointing at their tweaked answers, or drop their own
|
|
74
|
+
`.cls` into the temp tree before re-invoking. `y` → deploy.
|
|
75
|
+
|
|
76
|
+
5. **Deploy:**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
sf project deploy start \
|
|
80
|
+
--target-org <alias> \
|
|
81
|
+
--source-dir ./.promotion-bo-api-deploy/force-app \
|
|
82
|
+
--wait 10 --json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
On `--dry-run`, add `--dry-run` (fall back to `--check-only` on older CLIs).
|
|
86
|
+
Non-success → stop, print `result.details.componentFailures`.
|
|
87
|
+
|
|
88
|
+
## Phase 5b — Register + wire the step (into the user-chosen workflows)
|
|
89
|
+
|
|
90
|
+
Insert one `${PREFIX_UNDER}BO_API_Workflow_Step__c` record for the generated
|
|
91
|
+
class, then one `${PREFIX_UNDER}BO_API_Workflow_Workflow_Step__c` junction per
|
|
92
|
+
**`workflows`** entry from the interview (subset of `{create, update, copy}` —
|
|
93
|
+
never assume all three).
|
|
94
|
+
|
|
95
|
+
Rationale (do not skip):
|
|
96
|
+
- Store the **bare** class name (`<stepName>`) in `Classname__c`. The packaged `CallableManager` resolves it against the caller's namespace, so `Classname__c` never carries the `cgcloud`/`cgcloud_dev` prefix. (Every shipped seed row uses a bare name.)
|
|
97
|
+
- Compute `Sort__c` per workflow. If the interview supplied a `sortAfter` step name, resolve its junction row for the target workflow and use `sortAfter.Sort__c + 5`. Otherwise use `max(existing sort in that workflow) + 10`.
|
|
98
|
+
- `derive` is never wired by this skill. If the user selected `derive`, print "the `derive` workflow is out of scope for this skill — a follow-up skill will handle it" and drop it.
|
|
99
|
+
|
|
100
|
+
Do the whole thing in a single anonymous-Apex transaction to avoid partial
|
|
101
|
+
state. Placeholders in `<...>` come from the Phase 5 interview:
|
|
102
|
+
|
|
103
|
+
```apex
|
|
104
|
+
// ./.promotion-bo-api-deploy/register-step.apex
|
|
105
|
+
String salesOrg = '<salesOrg>';
|
|
106
|
+
String stepName = '<stepName>'; // e.g. 'SetCommentValue'
|
|
107
|
+
String actionName = '<actionName>'; // e.g. 'setCommentValue'
|
|
108
|
+
String entityName = '<entity>'; // e.g. 'Tactic'
|
|
109
|
+
String description = '<description>';
|
|
110
|
+
List<String> workflows = new List<String>{ <workflows-as-quoted-csv> }; // e.g. 'create','update','copy'
|
|
111
|
+
List<String> inputPaths = new List<String>{ <inputPaths-as-quoted-csv> }; // e.g. '.Comment' from the interview inputPaths
|
|
112
|
+
String inputPathType = '<inputPathType>'; // one of: String, Array, Boolean, Number, Object (SIS RecordType)
|
|
113
|
+
|
|
114
|
+
// Query the entity id (packaged)
|
|
115
|
+
List<${PREFIX_DOT}BO_API_Entity__c> entity = [
|
|
116
|
+
SELECT Id FROM ${PREFIX_DOT}BO_API_Entity__c
|
|
117
|
+
WHERE Name = :entityName
|
|
118
|
+
LIMIT 1
|
|
119
|
+
];
|
|
120
|
+
if (entity.isEmpty()) {
|
|
121
|
+
throw new CGCloudException(entityName + ' BO API entity not found for sales org ' + salesOrg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Upsert the step (Unique_Key__c is a writable external id — caller must set it)
|
|
125
|
+
${PREFIX_DOT}BO_API_Workflow_Step__c step = new ${PREFIX_DOT}BO_API_Workflow_Step__c(
|
|
126
|
+
Name = stepName,
|
|
127
|
+
${PREFIX_DOT}Classname__c = stepName,
|
|
128
|
+
${PREFIX_DOT}Method__c = actionName,
|
|
129
|
+
${PREFIX_DOT}Sales_Org__c = salesOrg,
|
|
130
|
+
${PREFIX_DOT}Entity__c = entity[0].Id,
|
|
131
|
+
${PREFIX_DOT}Description__c = description,
|
|
132
|
+
${PREFIX_DOT}Unique_Key__c = stepName + salesOrg
|
|
133
|
+
);
|
|
134
|
+
upsert step ${PREFIX_DOT}Unique_Key__c;
|
|
135
|
+
|
|
136
|
+
// Ensure entity is in each workflow's WE list before inserting junctions
|
|
137
|
+
// (validation rule requires it — copy fails otherwise)
|
|
138
|
+
Map<String, Id> wfByName = new Map<String, Id>();
|
|
139
|
+
for (${PREFIX_DOT}BO_API_Workflow__c wf : [
|
|
140
|
+
SELECT Id, Name FROM ${PREFIX_DOT}BO_API_Workflow__c
|
|
141
|
+
WHERE Name IN :workflows
|
|
142
|
+
AND ${PREFIX_DOT}BO_API__r.Name = 'Promotion'
|
|
143
|
+
AND ${PREFIX_DOT}BO_API__r.${PREFIX_DOT}Sales_Org__c = :salesOrg
|
|
144
|
+
]) {
|
|
145
|
+
wfByName.put(wf.Name, wf.Id);
|
|
146
|
+
}
|
|
147
|
+
for (String wf : workflows) {
|
|
148
|
+
if (!wfByName.containsKey(wf)) {
|
|
149
|
+
throw new CGCloudException('BO API workflow "' + wf + '" not found for sales org ' + salesOrg);
|
|
150
|
+
}
|
|
151
|
+
Integer existing = [
|
|
152
|
+
SELECT COUNT() FROM ${PREFIX_DOT}BO_API_Workflow_Entity__c
|
|
153
|
+
WHERE ${PREFIX_DOT}BO_API_Workflow__c = :wfByName.get(wf)
|
|
154
|
+
AND ${PREFIX_DOT}BO_API_Entity__c = :entity[0].Id
|
|
155
|
+
];
|
|
156
|
+
if (existing == 0) {
|
|
157
|
+
insert new ${PREFIX_DOT}BO_API_Workflow_Entity__c(
|
|
158
|
+
${PREFIX_DOT}BO_API_Workflow__c = wfByName.get(wf),
|
|
159
|
+
${PREFIX_DOT}BO_API_Entity__c = entity[0].Id
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Wire junctions into the interview's chosen workflows only
|
|
165
|
+
for (String wf : workflows) {
|
|
166
|
+
AggregateResult ar = [
|
|
167
|
+
SELECT MAX(${PREFIX_DOT}Sort__c) maxSort
|
|
168
|
+
FROM ${PREFIX_DOT}BO_API_Workflow_Workflow_Step__c
|
|
169
|
+
WHERE ${PREFIX_DOT}BO_API_Workflow__c = :wfByName.get(wf)
|
|
170
|
+
];
|
|
171
|
+
Decimal maxSort = (Decimal) ar.get('maxSort');
|
|
172
|
+
Decimal nextSort = (maxSort == null ? 10 : maxSort + 10);
|
|
173
|
+
// If interview supplied sortAfter, resolve that here and use sortAfter.Sort__c + 5 instead.
|
|
174
|
+
|
|
175
|
+
${PREFIX_DOT}BO_API_Workflow_Workflow_Step__c junction = new ${PREFIX_DOT}BO_API_Workflow_Workflow_Step__c(
|
|
176
|
+
${PREFIX_DOT}BO_API_Workflow__c = wfByName.get(wf),
|
|
177
|
+
${PREFIX_DOT}BO_API_Workflow_Step__c = step.Id,
|
|
178
|
+
${PREFIX_DOT}Enabled__c = true,
|
|
179
|
+
${PREFIX_DOT}Sort__c = nextSort,
|
|
180
|
+
${PREFIX_DOT}Unique_Key__c = String.valueOf(wfByName.get(wf)) + stepName + salesOrg
|
|
181
|
+
);
|
|
182
|
+
upsert junction ${PREFIX_DOT}Unique_Key__c;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Register accepted input paths (SIS rows). Required — without these,
|
|
186
|
+
// the payload validator rejects with "Properties other than those defined
|
|
187
|
+
// were not expected at #.<path>". SIS has no external id → insert with
|
|
188
|
+
// pre-check. SIS uses RecordType to declare the JSON type (String, Array,
|
|
189
|
+
// Boolean, Number, Object) — a missing RecordTypeId defaults to Array,
|
|
190
|
+
// which produces "Expected List<Object>" at runtime.
|
|
191
|
+
Id sisRecordTypeId = [
|
|
192
|
+
SELECT Id FROM RecordType
|
|
193
|
+
WHERE SobjectType = '${PREFIX_UNDER}BO_API_Step_Input_Structure__c'
|
|
194
|
+
AND DeveloperName = :inputPathType
|
|
195
|
+
LIMIT 1
|
|
196
|
+
].Id;
|
|
197
|
+
Set<String> existingPaths = new Set<String>();
|
|
198
|
+
for (${PREFIX_DOT}BO_API_Step_Input_Structure__c s : [
|
|
199
|
+
SELECT ${PREFIX_DOT}Path__c FROM ${PREFIX_DOT}BO_API_Step_Input_Structure__c
|
|
200
|
+
WHERE ${PREFIX_DOT}BO_API_Workflow_Step__c = :step.Id
|
|
201
|
+
]) {
|
|
202
|
+
existingPaths.add(String.valueOf(s.get('${PREFIX_DOT}Path__c')));
|
|
203
|
+
}
|
|
204
|
+
List<${PREFIX_DOT}BO_API_Step_Input_Structure__c> sisRows =
|
|
205
|
+
new List<${PREFIX_DOT}BO_API_Step_Input_Structure__c>();
|
|
206
|
+
for (String inputPath : inputPaths) {
|
|
207
|
+
if (existingPaths.contains(inputPath)) continue;
|
|
208
|
+
sisRows.add(new ${PREFIX_DOT}BO_API_Step_Input_Structure__c(
|
|
209
|
+
${PREFIX_DOT}BO_API_Workflow_Step__c = step.Id,
|
|
210
|
+
${PREFIX_DOT}Path__c = inputPath,
|
|
211
|
+
RecordTypeId = sisRecordTypeId
|
|
212
|
+
));
|
|
213
|
+
}
|
|
214
|
+
if (!sisRows.isEmpty()) insert sisRows;
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`PREFIX_DOT` above is the placeholder the skill textually replaces before
|
|
218
|
+
writing the file to `./.promotion-bo-api-deploy/`. In the final on-disk file every `${PREFIX_DOT}`
|
|
219
|
+
becomes either `cgcloud.` / `cgcloud_dev.` / empty, and every
|
|
220
|
+
`${PREFIX_DOT}Sales_Org__c`-style token becomes `cgcloud__Sales_Org__c` /
|
|
221
|
+
`cgcloud_dev__Sales_Org__c` / bare.
|
|
222
|
+
|
|
223
|
+
Run:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
sf apex run --target-org <alias> --file ./.promotion-bo-api-deploy/register-step.apex
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Non-zero exit or non-`Compiled: true, Success: true` → stop, print output.
|
|
230
|
+
|
|
231
|
+
Re-run behaviour: both upserts are keyed on `Unique_Key__c` (writable external
|
|
232
|
+
id set from stable interview inputs), so a second run of Phase 5b is a no-op if
|
|
233
|
+
the answers are unchanged. Reducing the `workflows` set across runs does **not**
|
|
234
|
+
retire junctions the previous run created — the skill only adds. Print a warning
|
|
235
|
+
if Phase 2 shows a junction for `<stepName>` on a workflow no longer in the
|
|
236
|
+
interview list; a human should decide whether to disable it.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Reference example — `SetCommentValue`
|
|
2
|
+
|
|
3
|
+
One worked customization the skill ships as a preset. It exists so a reviewer can
|
|
4
|
+
see a complete, verified answer to the Phase 5 interview. The skill does not
|
|
5
|
+
deploy it by default — the customer picks it explicitly via
|
|
6
|
+
`--preset set-comment-value` or by saying "use the SetCommentValue reference"
|
|
7
|
+
during the interview. All files below ship under
|
|
8
|
+
`assets/set-comment-value/`.
|
|
9
|
+
|
|
10
|
+
## Interview answers
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"stepName": "SetCommentValue",
|
|
15
|
+
"entity": "Tactic",
|
|
16
|
+
"workflows": ["create", "update", "copy"],
|
|
17
|
+
"actionName": "setCommentValue",
|
|
18
|
+
"inputPaths": ["Comment"],
|
|
19
|
+
"outputWrites": [
|
|
20
|
+
{ "field": "Comment__c", "source": "Comment" }
|
|
21
|
+
],
|
|
22
|
+
"description": "Copies the tactic input Comment to Tactic.Comment__c.",
|
|
23
|
+
"preconditions": null,
|
|
24
|
+
"sortAfter": null
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Ships at `assets/set-comment-value/interview-answers.json`.
|
|
29
|
+
|
|
30
|
+
## Generated class
|
|
31
|
+
|
|
32
|
+
The class is namespace-agnostic — it works whether the target org has `cgcloud`,
|
|
33
|
+
`cgcloud_dev`, or no prefix, because it derives the Tactic namespace from the
|
|
34
|
+
passed SObject at runtime and never types `<ns>__Tactic__c` or `<ns>__Comment__c`
|
|
35
|
+
at compile time. Context-key names (`currentInput`, `currentOutput`) are the ones
|
|
36
|
+
the packaged `BoTransformationEngine` seeds — verified at lines 825–864 of
|
|
37
|
+
`BoTransformationEngine.cls`. Ships at
|
|
38
|
+
`assets/set-comment-value/SetCommentValue.cls`.
|
|
39
|
+
|
|
40
|
+
```apex
|
|
41
|
+
/**
|
|
42
|
+
* Custom BO API Workflow Step: lifts a top-level `Comment` value out of
|
|
43
|
+
* the ingest tactic input onto Tactic.Comment__c.
|
|
44
|
+
*
|
|
45
|
+
* Registered against the `Tactic` BO API entity and wired into the
|
|
46
|
+
* `create`, `update`, and `copy` workflows.
|
|
47
|
+
*/
|
|
48
|
+
global with sharing class SetCommentValue implements Callable {
|
|
49
|
+
|
|
50
|
+
public class SetCommentValueException extends Exception {}
|
|
51
|
+
|
|
52
|
+
// global (not public): the packaged CallableManager must resolve/invoke this
|
|
53
|
+
// subscriber Callable across the namespace boundary on a managed-installed org
|
|
54
|
+
// (a public class fails at runtime with `Type "SetCommentValue" does not exist`).
|
|
55
|
+
global Object call(String action, Map<String, Object> context) {
|
|
56
|
+
if (action != 'setCommentValue') {
|
|
57
|
+
throw new SetCommentValueException('Unsupported action: ' + action);
|
|
58
|
+
}
|
|
59
|
+
if (context == null) {
|
|
60
|
+
throw new SetCommentValueException('null context');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
Object rawInput = context.get('currentInput');
|
|
64
|
+
Object rawOutput = context.get('currentOutput');
|
|
65
|
+
|
|
66
|
+
if (!(rawInput instanceof Map<String, Object>)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
Map<String, Object> tacticInput = (Map<String, Object>) rawInput;
|
|
70
|
+
Object commentValue = tacticInput.get('Comment');
|
|
71
|
+
if (commentValue == null) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The engine wraps Tactic__c in TPM_Promotion.TacticRecord (see
|
|
76
|
+
// BoTransformationEngineEntityTree.generateObject:696 —
|
|
77
|
+
// promo.addTactic(new Tactic__c()) returns a TacticRecord wrapper).
|
|
78
|
+
// Unwrap to the underlying SObject via getRecord() before writing;
|
|
79
|
+
// a raw SObject cast fails silently and the write never persists.
|
|
80
|
+
SObject tactic;
|
|
81
|
+
if (rawOutput instanceof SObject) {
|
|
82
|
+
tactic = (SObject) rawOutput;
|
|
83
|
+
} else if (rawOutput instanceof TPM_Promotion.Record) {
|
|
84
|
+
tactic = ((TPM_Promotion.Record) rawOutput).getRecord();
|
|
85
|
+
} else {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
String tacticType = String.valueOf(tactic.getSObjectType());
|
|
90
|
+
String prefix = '';
|
|
91
|
+
if (tacticType.contains('__') && tacticType.endsWith('__c')) {
|
|
92
|
+
Integer firstUnderscoreUnderscore = tacticType.indexOf('__');
|
|
93
|
+
Integer lastUnderscoreUnderscore = tacticType.lastIndexOf('__');
|
|
94
|
+
if (firstUnderscoreUnderscore < lastUnderscoreUnderscore) {
|
|
95
|
+
prefix = tacticType.substring(0, firstUnderscoreUnderscore) + '__';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
tactic.put(prefix + 'Comment__c', String.valueOf(commentValue));
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Class metadata
|
|
106
|
+
|
|
107
|
+
Ships at `assets/set-comment-value/SetCommentValue.cls-meta.xml`.
|
|
108
|
+
|
|
109
|
+
```xml
|
|
110
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
111
|
+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
112
|
+
<apiVersion>60.0</apiVersion>
|
|
113
|
+
<status>Active</status>
|
|
114
|
+
</ApexClass>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Reference payloads
|
|
118
|
+
|
|
119
|
+
Live at `assets/set-comment-value/payloads/`:
|
|
120
|
+
- `create.json` — sets `Comment` on a new promotion's tactic.
|
|
121
|
+
- `update.json` — updates the tactic's `Comment`.
|
|
122
|
+
- `copy.json` — copies the source promotion; expects the tactic's `Comment` to be carried across.
|
|
123
|
+
|
|
124
|
+
Each carries the `REPLACED-AT-RUNTIME` marker for `importId` (and, for
|
|
125
|
+
`update`/`copy`, marker ids for the promotion + tactic captured from the
|
|
126
|
+
preceding response).
|
|
127
|
+
|
|
128
|
+
## Expected write
|
|
129
|
+
|
|
130
|
+
`Tactic__c.Comment__c` on the created / updated / copied promotion's tactic must
|
|
131
|
+
equal the `Comment` value from the corresponding payload — this is the
|
|
132
|
+
interview's single `outputWrites` assertion.
|