consumer-goods-promotion-bo-api-deploy

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Deploy the Promotion BO API

部署Promotion BO API

What this does

功能说明

Turns on the Consumer Goods Cloud TPM Promotion Business Object (BO) API in a Salesforce org, interviews the user for one custom Workflow Step to add to the promotion pipeline, generates and deploys that Apex class, registers it against a BO API entity, wires it into the customer's chosen subset of
{create, update, copy}
workflows, then proves the whole path end-to-end via three REST endpoints:
  • POST /services/apexrest/<prefix>/promotions/initialize
  • POST /services/apexrest/<prefix>/promotions/ingest
  • GET  /services/apexrest/<prefix>/promotions/status?importId=...
It is meant for headless delivery: point it at an org, answer the interview, and the skill installs the step and verifies it. The Promotion BO API ships in the CGCloud managed package (namespace
cgcloud
released,
cgcloud_dev
dev/beta). Every Apex identifier, object/field API name, and REST URL is derived from the detected package prefix at runtime — this skill never hard-codes it.
derive
is out of scope here (a follow-up skill covers it). Detailed runbooks for the write and smoke phases live under
references/
:
  • references/conventions-and-payload-rules.md
    — sales-org partitioning, schema-first contract derivation, and the eight cross-workflow payload rules R1–R8.
  • references/generate-and-wire.md
    — Phase 5a (generate + deploy the class) and Phase 5b (register + wire the step), with the full
    register-step.apex
    .
  • references/smoke-and-verify.md
    — Phase 6 (contract derivation, payload materialization, create/update/copy legs + verification).
  • references/reference-example-set-comment-value.md
    — the shipped
    SetCommentValue
    preset (assets at
    assets/set-comment-value/
    ).
在Salesforce组织中启用Consumer Goods Cloud TPM Promotion业务对象(BO)API,询问用户想要添加到推广流程中的一个自定义工作流步骤,生成并部署对应的Apex类,将其注册到BO API实体,集成到客户选择的
{create, update, copy}
工作流子集,然后通过三个REST端点完成全流程端到端验证:
  • POST /services/apexrest/<prefix>/promotions/initialize
  • POST /services/apexrest/<prefix>/promotions/ingest
  • GET  /services/apexrest/<prefix>/promotions/status?importId=...
本工具支持无界面交付:指定目标组织,回答访谈问题,即可完成步骤安装并验证。Promotion BO API包含在CGCloud托管包中(正式版命名空间为
cgcloud
,开发/测试版为
cgcloud_dev
)。所有Apex标识符、对象/字段API名称以及REST URL均在运行时根据检测到的包前缀生成——本工具绝不会硬编码前缀。
derive
工作流不在本次范围内(后续工具会覆盖该功能)。写入和冒烟测试阶段的详细运行手册位于
references/
目录下:
  • references/conventions-and-payload-rules.md
    ——销售组织分区、基于Schema的契约推导,以及跨工作流的8项负载规则R1–R8。
  • references/generate-and-wire.md
    ——第5a阶段(生成+部署类)和第5b阶段(注册+集成步骤),包含完整的
    register-step.apex
    代码。
  • references/smoke-and-verify.md
    ——第6阶段(契约推导、负载实例化、创建/更新/复制流程+验证)。
  • references/reference-example-set-comment-value.md
    ——附带的
    SetCommentValue
    预设示例(资源位于
    assets/set-comment-value/
    )。

Inputs to collect first

需预先收集的输入项

Ask before starting; do not guess.
  1. Org alias — the
    sf
    alias of the target org. Required.
  2. Sales org — required pre-parameter, default
    0001
    . Must be 4 chars, uppercase (mirrors
    TPMSetupData.validateSalesOrg
    ). Every downstream lookup (promotion template, tactic template, anchor account, product filter criteria, SKUs) is scoped to this sales org — see
    references/conventions-and-payload-rules.md
    ("Sales-org partitioning").
  3. Yes to seeding? — Phase 3a fetches the packaged BO API seed and asks for confirmation before inserting ~232 rows across 8 objects. Offer
    --yes
    for CI callers.
  4. Dry run?
    --dry-run
    runs Phases 1, 2, 3a-preview, the Phase 5 interview, and a validate-only deploy. Writes nothing.
What the customization is gets collected in Phase 5's interview, once the framework state is known.
开始前需询问用户,请勿猜测。
  1. 组织别名 ——目标组织的
    sf
    别名。必填项。
  2. 销售组织 ——必填前置参数,默认值为
    0001
    。必须为4位大写字符(与
    TPMSetupData.validateSalesOrg
    规则一致)。所有下游查询(推广模板、策略模板、锚定客户、产品筛选条件、SKU)均限定在该销售组织范围内——详见
    references/conventions-and-payload-rules.md
    中的“销售组织分区”部分。
  3. 是否允许初始化数据? ——第3a阶段会获取托管包中的BO API初始化数据,并在插入8个对象的约232条记录前征求用户确认。为CI调用者提供
    --yes
    选项。
  4. 试运行? ——
    --dry-run
    参数会运行第1、2、3a预览阶段、第5阶段的访谈,以及仅验证的部署操作,不会写入任何数据。
自定义内容的具体信息会在第5阶段的访谈中收集,需在了解框架状态后进行。

Find the namespace first

先查找命名空间

The prefix depends on the installed build — detect it, do not guess. Released package →
cgcloud
; dev/beta →
cgcloud_dev
; source-deployed → no prefix.
bash
sf data query --target-org <alias> \
  --query "SELECT NamespacePrefix FROM ApexClass WHERE Name = 'TPMSetupData'" --json
Read
records[0].NamespacePrefix
and bind three variables used everywhere below:
  • PREFIX_DOT
    — Apex references:
    cgcloud.
    /
    cgcloud_dev.
    / empty.
  • PREFIX_UNDER
    — object/field API names:
    cgcloud__
    /
    cgcloud_dev__
    / empty.
  • URL_NS
    — REST paths:
    /cgcloud
    /
    /cgcloud_dev
    / empty.
Every Apex call is
${PREFIX_DOT}ClassName.method(...)
; every SOQL object/field is
${PREFIX_UNDER}Object__c
; every REST URL is
/services/apexrest${URL_NS}/promotions/<endpoint>
. Substitute at run time.
前缀取决于已安装的构建版本——需自动检测,请勿猜测。正式版包→
cgcloud
;开发/测试版→
cgcloud_dev
;源码部署版→无前缀。
bash
sf data query --target-org <alias> \
  --query "SELECT NamespacePrefix FROM ApexClass WHERE Name = 'TPMSetupData'" --json
读取
records[0].NamespacePrefix
并绑定以下三个全局变量:
  • PREFIX_DOT
    ——Apex引用:
    cgcloud.
    /
    cgcloud_dev.
    / 空值。
  • PREFIX_UNDER
    ——对象/字段API名称:
    cgcloud__
    /
    cgcloud_dev__
    / 空值。
  • URL_NS
    ——REST路径:
    /cgcloud
    /
    /cgcloud_dev
    / 空值。
所有Apex调用格式为
${PREFIX_DOT}ClassName.method(...)
;所有SOQL对象/字段格式为
${PREFIX_UNDER}Object__c
;所有REST URL格式为
/services/apexrest${URL_NS}/promotions/<endpoint>
。需在运行时动态替换。

Phase 1 — Preflight

第1阶段——预检查

Run these and stop on the first failure.
  1. sf --version
    ;
    node --version
    (only if running shipped scripts).
  2. sf org display --target-org <alias> --json
    — confirm reachable, capture
    instanceUrl
    + org id (for display/logging only). Every REST call in later phases goes through
    sf api request rest
    , which uses the CLI's stored session — this skill never extracts the access token.
  3. Validate the sales org locally: length 4, uppercase, non-blank (mirrors
    TPMSetupData.validateSalesOrg
    ). Fail fast instead of surfacing an Apex stack trace.
  4. TPM entitlement (Permission Set License) — the TPM app is licensed through the
    CGCRetailAndTPMMgmtPsl
    Permission Set License ("CGC Retail and Trade Promotion Management"); there is no
    TPM App
    UserLicense. Check the running user's PSL assignment:
    sql
    SELECT Id FROM PermissionSetLicenseAssign
     WHERE Assignee.Username = '<running-user-name>'
       AND PermissionSetLicense.DeveloperName = 'CGCRetailAndTPMMgmtPsl'
    On scratch/dev-hub the PSL may be legitimately unassigned; detect env with
    SELECT OrganizationType, IsSandbox FROM Organization
    :
    • OrganizationType = 'Developer Edition'
      OR
      IsSandbox = true
      and empty → warn ("permset gate below is the real check"), continue.
    • Any other org shape and empty → stop ("TPM Permission Set License
      CGCRetailAndTPMMgmtPsl
      not assigned to the running user").
  5. Running-user permission set (TPM Admin persona) — the setup needs a permission set granting the TPM Master Data Admin persona. The shipped permset is
    TPM_Master_Data_Admin
    (packaged;
    PermissionSet.Name
    is the bare name regardless of namespace), but customers frequently clone it under their own name — so treat a miss as "ask", not "fail". Take
    <running-user-name>
    from
    sf org display ... .result.username
    :
    sql
    SELECT PermissionSet.Name, PermissionSet.Label FROM PermissionSetAssignment
     WHERE Assignee.Username = '<running-user-name>'
       AND (PermissionSet.Name = 'TPM_Master_Data_Admin'
            OR PermissionSet.Label LIKE '%TPM%Master Data Admin%')
    Match → proceed. No match → do not hard-fail on the name: show
    sf org assign permset --name TPM_Master_Data_Admin --target-org <alias>
    as the default option, but also ask the user to confirm whether they already hold an equivalent (possibly cloned/renamed) permission set for the TPM admin persona. Pause, wait for a cloned-permset confirmation or the assignment, then re-query. Proceed only once admin access is confirmed.
  6. Namespace discovery (above).
运行以下步骤,遇到第一个失败项即停止。
  1. sf --version
    node --version
    (仅在运行附带脚本时需要)。
  2. sf org display --target-org <alias> --json
    ——确认组织可访问,捕获
    instanceUrl
    和组织ID(仅用于显示/日志)。后续阶段的所有REST调用均通过
    sf api request rest
    执行,使用CLI存储的会话——本工具绝不会提取访问令牌。
  3. 本地验证销售组织:长度为4、大写、非空(与
    TPMSetupData.validateSalesOrg
    规则一致)。提前失败,避免出现Apex堆栈跟踪。
  4. TPM授权(权限集许可证) ——TPM应用通过**
    CGCRetailAndTPMMgmtPsl
    权限集许可证(“CGC Retail and Trade Promotion Management”)授权;不存在
    TPM App
    **用户许可证。检查当前用户的PSL分配情况:
    sql
    SELECT Id FROM PermissionSetLicenseAssign
     WHERE Assignee.Username = '<running-user-name>'
       AND PermissionSetLicense.DeveloperName = 'CGCRetailAndTPMMgmtPsl'
    在临时沙箱/开发中心组织中,PSL可能未被合法分配;通过
    SELECT OrganizationType, IsSandbox FROM Organization
    检测环境:
    • OrganizationType = 'Developer Edition'
      IsSandbox = true
      且查询结果为空 → 警告(“下方的权限集检查才是关键”),继续执行。
    • 其他组织类型且查询结果为空 → 停止(“当前用户未分配TPM权限集许可证
      CGCRetailAndTPMMgmtPsl
      ”)。
  5. 当前用户权限集(TPM管理员角色) ——设置操作需要授予TPM Master Data Admin角色的权限集。默认权限集为
    TPM_Master_Data_Admin
    (托管包中的权限集;
    PermissionSet.Name
    为无命名空间的裸名称),但客户经常会克隆该权限集并使用自定义名称——因此查询不到时应询问用户,而非直接失败。从
    sf org display ... .result.username
    获取
    <running-user-name>
    sql
    SELECT PermissionSet.Name, PermissionSet.Label FROM PermissionSetAssignment
     WHERE Assignee.Username = '<running-user-name>'
       AND (PermissionSet.Name = 'TPM_Master_Data_Admin'
            OR PermissionSet.Label LIKE '%TPM%Master Data Admin%')
    匹配成功 → 继续执行。无匹配结果 → 请勿因名称不匹配直接失败:将
    sf org assign permset --name TPM_Master_Data_Admin --target-org <alias>
    作为默认选项展示,同时询问用户是否已持有等效的(可能已克隆/重命名)TPM管理员角色权限集。暂停执行,等待用户确认已持有克隆权限集或完成权限集分配,然后重新查询。仅在确认管理员权限后继续执行。
  6. 命名空间检测(见前文)。

Phase 2 — Verify BO API framework state

第2阶段——验证BO API框架状态

Read-only; tells you whether Phase 3 needs to run. All three counts MUST be scoped to
<salesOrg>
— the framework rows are sales-org-partitioned.
bash
undefined
仅读取操作;用于判断是否需要运行第3阶段。所有三个查询结果必须限定在
<salesOrg>
范围内——框架数据按销售组织分区。
bash
undefined

1. Workflow rows for this sales org (expect 4: create/update/copy/derive)

1. 该销售组织的工作流记录(预期为4条:create/update/copy/derive)

sf data query --target-org <alias> --json --query " SELECT Name FROM ${PREFIX_UNDER}BO_API_Workflow__c WHERE ${PREFIX_UNDER}BO_API__r.Name = 'Promotion' AND ${PREFIX_UNDER}BO_API__r.${PREFIX_UNDER}Sales_Org__c = '<salesOrg>'"
sf data query --target-org <alias> --json --query " SELECT Name FROM ${PREFIX_UNDER}BO_API_Workflow__c WHERE ${PREFIX_UNDER}BO_API__r.Name = 'Promotion' AND ${PREFIX_UNDER}BO_API__r.${PREFIX_UNDER}Sales_Org__c = '<salesOrg>'"

2. Step rows for this sales org

2. 该销售组织的步骤记录

sf data query --target-org <alias> --json --query " SELECT COUNT(Id) FROM ${PREFIX_UNDER}BO_API_Workflow_Step__c WHERE ${PREFIX_UNDER}Sales_Org__c = '<salesOrg>'"
sf data query --target-org <alias> --json --query " SELECT COUNT(Id) FROM ${PREFIX_UNDER}BO_API_Workflow_Step__c WHERE ${PREFIX_UNDER}Sales_Org__c = '<salesOrg>'"

3. Junction rows whose parent workflow belongs to a Promotion BO API for this sales org

3. 父工作流属于该销售组织Promotion BO API的关联记录

sf data query --target-org <alias> --json --query " SELECT COUNT(Id) FROM ${PREFIX_UNDER}BO_API_Workflow_Workflow_Step__c WHERE ${PREFIX_UNDER}BO_API_Workflow__r.${PREFIX_UNDER}BO_API__r.Name = 'Promotion' AND ${PREFIX_UNDER}BO_API_Workflow__r.${PREFIX_UNDER}BO_API__r.${PREFIX_UNDER}Sales_Org__c = '<salesOrg>'"

- Zero workflow rows → fresh for this sales org, Phase 3 will seed.
- Full state (4 workflow rows `create/update/copy/derive`; >0 step + junction rows) → Phase 3 is a no-op for this sales org (still run for idempotency; 3a shows zero net-new).
- Partial state → **error**; stop, report which rows are missing. A human inspects first.
sf data query --target-org <alias> --json --query " SELECT COUNT(Id) FROM ${PREFIX_UNDER}BO_API_Workflow_Workflow_Step__c WHERE ${PREFIX_UNDER}BO_API_Workflow__r.${PREFIX_UNDER}BO_API__r.Name = 'Promotion' AND ${PREFIX_UNDER}BO_API_Workflow__r.${PREFIX_UNDER}BO_API__r.${PREFIX_UNDER}Sales_Org__c = '<salesOrg>'"

- 工作流记录数为0 → 该销售组织为全新状态,第3阶段将执行初始化。
- 完整状态(4条工作流记录`create/update/copy/derive`;步骤记录+关联记录数>0)→ 该销售组织无需执行第3阶段(仍会运行以保证幂等性;3a阶段会显示无新增记录)。
- 部分状态 → **错误**;停止执行,报告缺失的记录类型。需人工检查后再继续。

Phase 3a — Preview the default BO API seed (fetch + confirm)

第3a阶段——预览默认BO API初始化数据(获取+确认)

The workflows/steps/junctions/entities/input-structures ship as CSVs inside the packaged
TPMSetupData
static resource;
TPMSetupData.setupBOApi
reads and upserts them. Before running it, show the user what will land.
  1. Locate the resource:
    bash
    sf data query --target-org <alias> --json --query "
      SELECT Id, Name, NamespacePrefix, SystemModStamp, BodyLength
        FROM StaticResource WHERE Name = 'TPMSetupData'"
    Zero rows → stop ("TPMSetupData static resource not found — is the CGCloud package installed?"). Multiple rows → prefer the one whose
    NamespacePrefix
    matches Phase 1; if prefixes disagree, stop and ask.
  2. Retrieve + expand the resource cross-platform via SFDX — use
    sf project retrieve
    , which unpacks a zip StaticResource into a folder on every OS (no
    curl
    , no
    unzip
    unzip
    isn't present on Windows by default). Retrieve into a dedicated
    setup-data/
    subproject
    so the read-only managed resource never mixes into the Phase-5a deploy tree (
    ./.promotion-bo-api-deploy/force-app
    , which is what gets deployed back):
    bash
    mkdir -p ./.promotion-bo-api-deploy/setup-data/force-app/main/default
    printf '{ "packageDirectories": [{ "path": "force-app", "default": true }], "sourceApiVersion": "60.0" }' \
      > ./.promotion-bo-api-deploy/setup-data/sfdx-project.json
    ( cd ./.promotion-bo-api-deploy/setup-data && sf project retrieve start \
        --metadata "StaticResource:${PREFIX_UNDER}TPMSetupData" --target-org <alias> --json )
    Non-success → stop, print
    result
    errors.
  3. SFDX expands the zip to
    ./.promotion-bo-api-deploy/setup-data/force-app/main/default/staticresources/${PREFIX_UNDER}TPMSetupData/BOApi/
    .
    ls
    it. Expected:
    0_BO_API__c.csv
    ,
    0_BO_API_Entity__c.csv
    ,
    0_BO_API_Output_Entity__c.csv
    ,
    0_BO_API_Workflow__c.csv
    ,
    0_BO_API_Workflow_Entity__c.csv
    ,
    0_BO_API_Workflow_Step__c.csv
    ,
    0_BO_API_Workflow_Workflow_Step__c.csv
    ,
    0_BO_API_Step_Input_Structure__c.csv
    ,
    import.json
    . Any missing → stop, print the delta.
  4. Confirm each target sObject exists:
    sf sobject describe --sobject '${PREFIX_UNDER}BO_API_Workflow__c' --target-org <alias> > /dev/null
    (etc.). Any describe failure → stop (package partially installed).
  5. Show a summary (row counts per object; total ~232 rows across 8 objects; "upsert on Unique_Key__c — re-running is idempotent"), plus the first 3 rows of
    0_BO_API_Workflow__c.csv
    and
    0_BO_API_Workflow_Step__c.csv
    with
    {{NS}}
    /
    {{SALES_ORG}}
    substituted.
  6. Wait for confirmation unless
    --yes
    . "no"/blank → abort clean, point at
    ./.promotion-bo-api-deploy/setup-data/force-app/main/default/staticresources/${PREFIX_UNDER}TPMSetupData/BOApi/
    .
  7. On
    --dry-run
    → stop here; do not run 3b.
工作流/步骤/关联/实体/输入结构以CSV格式存储在托管包的
TPMSetupData
静态资源中;
TPMSetupData.setupBOApi
会读取并执行upsert操作。运行该方法前,需向用户展示即将插入的数据。
  1. 定位资源:
    bash
    sf data query --target-org <alias> --json --query "
      SELECT Id, Name, NamespacePrefix, SystemModStamp, BodyLength
        FROM StaticResource WHERE Name = 'TPMSetupData'"
    无记录 → 停止执行(“未找到TPMSetupData静态资源——是否已安装CGCloud包?”)。多条记录 → 优先选择与第1阶段检测到的命名空间匹配的资源;若命名空间不匹配,停止执行并询问用户。
  2. 通过SFDX跨平台获取并解压资源 ——使用
    sf project retrieve
    ,该命令可在所有操作系统上将压缩的StaticResource解压到文件夹(无需
    curl
    unzip
    ——Windows默认未安装
    unzip
    )。将资源获取到专用的
    setup-data/
    子项目
    中,避免只读托管资源混入第5a阶段的部署目录(
    ./.promotion-bo-api-deploy/force-app
    ,该目录内容会被部署回组织):
    bash
    mkdir -p ./.promotion-bo-api-deploy/setup-data/force-app/main/default
    printf '{ "packageDirectories": [{ "path": "force-app", "default": true }], "sourceApiVersion": "60.0" }' \
      > ./.promotion-bo-api-deploy/setup-data/sfdx-project.json
    ( cd ./.promotion-bo-api-deploy/setup-data && sf project retrieve start \
        --metadata "StaticResource:${PREFIX_UNDER}TPMSetupData" --target-org <alias> --json )
    执行失败 → 停止执行,打印
    result
    中的错误信息。
  3. SFDX会将压缩包解压到
    ./.promotion-bo-api-deploy/setup-data/force-app/main/default/staticresources/${PREFIX_UNDER}TPMSetupData/BOApi/
    目录。执行
    ls
    查看该目录。预期包含:
    0_BO_API__c.csv
    ,
    0_BO_API_Entity__c.csv
    ,
    0_BO_API_Output_Entity__c.csv
    ,
    0_BO_API_Workflow__c.csv
    ,
    0_BO_API_Workflow_Entity__c.csv
    ,
    0_BO_API_Workflow_Step__c.csv
    ,
    0_BO_API_Workflow_Workflow_Step__c.csv
    ,
    0_BO_API_Step_Input_Structure__c.csv
    ,
    import.json
    。若有缺失 → 停止执行,打印缺失的文件。
  4. 确认每个目标sObject存在:
    sf sobject describe --sobject '${PREFIX_UNDER}BO_API_Workflow__c' --target-org <alias> > /dev/null
    (以此类推)。若任何describe操作失败 → 停止执行(包未完全安装)。
  5. 展示摘要信息(每个对象的记录数;8个对象共约232条记录;“基于Unique_Key__c执行upsert——重复执行具有幂等性”),并展示
    0_BO_API_Workflow__c.csv
    0_BO_API_Workflow_Step__c.csv
    的前3行(替换
    {{NS}}
    /
    {{SALES_ORG}}
    )。
  6. 等待用户确认,除非使用
    --yes
    参数。输入“no”或空白 → 干净终止,指向
    ./.promotion-bo-api-deploy/setup-data/force-app/main/default/staticresources/${PREFIX_UNDER}TPMSetupData/BOApi/
    目录。
  7. 若使用
    --dry-run
    参数 → 在此处停止;不执行3b阶段。

Phase 3b — Apply the default seed

第3b阶段——应用默认初始化数据

Only after 3a confirmation.
  1. PHASE3_START=$(date -u +%FT%TZ)
    .
  2. Run metadata-wizard setup via anon Apex:
    ${PREFIX_DOT}TPMSetupData.setupMetadataWizard('<salesOrg>');
    sf apex run --target-org <alias> --file ./.promotion-bo-api-deploy/setup-metadata-wizard.apex
    .
  3. Wait for the whole batch chain.
    GenericDemoSetupDataBatch
    self-chains via
    finish()
    ; polling a single job id misses children. Poll by class:
    bash
    sf data query --target-org <alias> --json --query "
      SELECT COUNT() FROM AsyncApexJob
       WHERE ApexClass.Name = 'GenericDemoSetupDataBatch'
         AND CreatedDate >= ${PHASE3_START}
         AND Status NOT IN ('Completed','Failed','Aborted')"
    Sleep 5s between polls; break when count is zero for two consecutive polls (covers the gap between a parent's
    finish()
    and the child's
    AsyncApexJob
    row appearing).
  4. Final check — every child succeeded (
    SELECT Id, Status, NumberOfErrors, ExtendedStatus FROM AsyncApexJob WHERE ApexClass.Name = 'GenericDemoSetupDataBatch' AND CreatedDate >= ${PHASE3_START}
    ). Any
    Failed
    /
    Aborted
    /
    NumberOfErrors > 0
    → stop, print
    ExtendedStatus
    .
  5. Repeat 2–4 for
    ${PREFIX_DOT}TPMSetupData.setupBOApi('<salesOrg>');
    .
仅在3a阶段确认后执行。
  1. PHASE3_START=$(date -u +%FT%TZ)
  2. 通过匿名Apex运行元数据向导设置:
    ${PREFIX_DOT}TPMSetupData.setupMetadataWizard('<salesOrg>');
    sf apex run --target-org <alias> --file ./.promotion-bo-api-deploy/setup-metadata-wizard.apex
  3. 等待整个批处理链完成。
    GenericDemoSetupDataBatch
    通过
    finish()
    自动触发子批处理;仅轮询单个作业ID会遗漏子批处理。按类轮询:
    bash
    sf data query --target-org <alias> --json --query "
      SELECT COUNT() FROM AsyncApexJob
       WHERE ApexClass.Name = 'GenericDemoSetupDataBatch'
         AND CreatedDate >= ${PHASE3_START}
         AND Status NOT IN ('Completed','Failed','Aborted')"
    每5秒轮询一次;当连续两次轮询结果均为0时停止(覆盖父批处理
    finish()
    与子批处理
    AsyncApexJob
    记录出现之间的间隙)。
  4. 最终检查——所有子批处理均成功(
    SELECT Id, Status, NumberOfErrors, ExtendedStatus FROM AsyncApexJob WHERE ApexClass.Name = 'GenericDemoSetupDataBatch' AND CreatedDate >= ${PHASE3_START}
    )。若存在
    Failed
    /
    Aborted
    /
    NumberOfErrors > 0
    的记录 → 停止执行,打印
    ExtendedStatus
  5. ${PREFIX_DOT}TPMSetupData.setupBOApi('<salesOrg>');
    重复步骤2–4。

Phase 4 — Verify the BO API framework is on

第4阶段——验证BO API框架已启用

Re-run the Phase 2 queries. Expect 4
BO_API_Workflow__c
rows (
create
/
update
/
copy
/
derive
, uniqueness on
Unique_Key__c
), ≥46
BO_API_Workflow_Step__c
rows, ≥54
BO_API_Workflow_Workflow_Step__c
junction rows. Anything short → stop, print what's missing. Never claim success while a component failed.
重新运行第2阶段的查询。预期得到4条
BO_API_Workflow__c
记录(
create
/
update
/
copy
/
derive
,基于
Unique_Key__c
保证唯一性)、≥46条
BO_API_Workflow_Step__c
记录、≥54条
BO_API_Workflow_Workflow_Step__c
关联记录。若数量不足 → 停止执行,打印缺失的记录类型。绝不能在组件安装失败的情况下声称成功。

Phase 5 — Interview: what does the user want to customize?

第5阶段——访谈:用户需要什么样的自定义?

The framework is now on. The customization is user-supplied — do not skip the interview; do not invent an answer. Print:
text
The Promotion BO API framework is installed. To add a custom step, tell me:
  - Which BO API entity does it target? (e.g. Promotion or Tactic — I'll list the exact entities registered in your org)
  - Which workflows should it fire in? (create, update, copy)
  - What does it read from the ingest input?
  - What does it write on the SObject or elsewhere?
  - Any preconditions or side effects I should know about?
If you want a worked example, say "use the SetCommentValue reference" —
it copies the tactic input Comment onto Tactic.Comment__c.
框架已启用。自定义内容由用户提供——请勿跳过访谈;请勿自行编造答案。打印以下内容:
text
Promotion BO API框架已安装。如需添加自定义步骤,请告知:
  - 目标BO API实体是什么?(例如Promotion或Tactic——我会列出您组织中已注册的具体实体)
  - 需要在哪些工作流中触发?(create, update, copy)
  - 从导入输入中读取哪些内容?
  - 需要写入到SObject或其他位置的内容是什么?
  - 是否有需要注意的前置条件或副作用?
如果需要参考示例,请说“use the SetCommentValue reference”——
该示例会将策略输入中的Comment复制到Tactic.Comment__c字段。

5.1 — Answers to collect

5.1 ——需收集的答案

FieldRequiredNotes
stepName
yesPascalCase Apex class name + BO API Workflow Step
Name
. Unique across
ApexClass
.
entity
yesBO API entity
Name
— MUST match a row queried live from
${PREFIX_UNDER}BO_API_Entity__c
(shared, not sales-org-partitioned); do not assume a fixed list. In a current org these include
Promotion
,
Tactic
,
ProductFilter
,
ManualInputs
,
CustomState
(plus the
TPM_Promotion.*
structures). Query the org and offer the actual rows.
workflows
yesNon-empty subset of
{create, update, copy}
.
derive
is out of scope.
actionName
yesSymbolic action string the class receives in
call(String action, ...)
. Convention: lowerCamelCase of
stepName
.
inputPaths
yes (≥1)JSON paths to read from
currentInput
, e.g.
["Comment"]
.
inputPathType
yesJSON type of the input paths —
String
,
Array
,
Boolean
,
Number
,
Object
. Drives the
RecordType
on each SIS row; a wrong value causes
TransformationError: Expected <Type>
at ingest. Different types → run Phase 5b once per type group.
outputWrites
yes (≥1)
{field, source}
pairs;
field
is the target API name without the namespace prefix (skill adds
${PREFIX_UNDER}
),
source
references an
inputPaths
value / literal / computed expression.
description
yesOne-sentence step description →
${PREFIX_UNDER}Description__c
+ class docstring.
preconditions
optionalFree-text guards before writing.
sortAfter
optionalNamed packaged step to sort after; default
max(Sort__c) + 10
.
字段是否必填说明
stepName
PascalCase格式的Apex类名 + BO API工作流步骤
Name
。在
ApexClass
中需唯一。
entity
BO API实体
Name
——必须与从
${PREFIX_UNDER}BO_API_Entity__c
查询到的现有记录匹配(共享数据,不按销售组织分区);请勿假设固定列表。当前组织中的实体包括
Promotion
Tactic
ProductFilter
ManualInputs
CustomState
(以及
TPM_Promotion.*
结构)。需查询组织并提供实际存在的实体列表。
workflows
{create, update, copy}
的非空子集。
derive
不在本次范围内。
actionName
类在
call(String action, ...)
方法中接收的符号操作字符串。约定:
stepName
的小驼峰格式。
inputPaths
是(≥1)
currentInput
读取的JSON路径,例如
["Comment"]
inputPathType
输入路径的JSON类型——
String
Array
Boolean
Number
Object
。决定每个SIS记录的
RecordType
;类型错误会导致导入时出现
TransformationError: Expected <Type>
。若存在多种类型 → 按类型分组分别执行第5b阶段。
outputWrites
是(≥1)
{field, source}
键值对;
field
为目标API名称不含命名空间前缀(工具会自动添加
${PREFIX_UNDER}
),
source
引用
inputPaths
的值/常量/计算表达式。
description
步骤的一句话描述 → 写入
${PREFIX_UNDER}Description__c
字段 + 类文档字符串。
preconditions
可选写入操作前的自由文本前置条件。
sortAfter
可选要排在其后的托管包步骤名称;默认值为
max(Sort__c) + 10

5.2 — Interview flow

5.2 ——访谈流程

  1. Preset shortcut. If
    --preset set-comment-value
    or the user says "use the SetCommentValue reference", load the answers from
    references/reference-example-set-comment-value.md
    (
    assets/set-comment-value/interview-answers.json
    ) and skip the interactive interview.
    --interview-file <path>
    loads answers from a JSON file mirroring the 5.1 table (wins over
    --preset
    ).
  2. Ask the questions one at a time (or as a block); do not proceed until every required field has a concrete value.
  3. Show the entity's writeable fields after the user names the entity:
    bash
    sf sobject describe --target-org <alias> --sobject '${PREFIX_UNDER}<Entity>__c' --json \
      | jq -r '.fields[] | select(.updateable == true and .createable == true) | .name'
    Any
    outputWrites
    field must appear here; otherwise stop and ask for a different one (FLS is caller-scoped).
  4. Confirm the plan (recap Class/Entity/Workflows/Reads/Writes/Sort/Description → "Deploy + wire? [y/N]").
    n
    /blank → save answers to
    ./.promotion-bo-api-deploy/interview.json
    and exit.
    y
    → Phase 5a.
  1. 预设快捷方式。若使用
    --preset set-comment-value
    参数或用户说“use the SetCommentValue reference”,则从
    references/reference-example-set-comment-value.md
    assets/set-comment-value/interview-answers.json
    )加载答案并跳过交互式访谈。
    --interview-file <path>
    参数会从镜像5.1表格的JSON文件加载答案(优先级高于
    --preset
    )。
  2. 逐个提问(或批量提问);在所有必填字段获取具体值前请勿继续。
  3. 用户指定实体后,展示该实体的可写字段
    bash
    sf sobject describe --target-org <alias> --sobject '${PREFIX_UNDER}<Entity>__c' --json \
      | jq -r '.fields[] | select(.updateable == true and .createable == true) | .name'
    所有
    outputWrites
    中的字段必须出现在该列表中;否则停止执行并询问用户更换字段(字段级安全性由调用者决定)。
  4. 确认计划(回顾类名/实体/工作流/读取内容/写入内容/排序/描述 → “是否部署并集成?[y/N]”)。输入“n”或空白 → 将答案保存到
    ./.promotion-bo-api-deploy/interview.json
    并退出。输入“y” → 进入第5a阶段。

Phases 5a / 5b — Generate + deploy, register + wire

第5a/5b阶段——生成+部署,注册+集成

Runbook in
references/generate-and-wire.md
. In short: guard the class name for idempotency; confirm target-field writeability; generate a namespace-agnostic
Callable
from the interview (reads
currentInput
/
currentOutput
, derives the prefix from
getSObjectType()
at runtime); show + confirm;
sf project deploy start
. Then, in one anon-Apex transaction, upsert one
BO_API_Workflow_Step__c
(bare class name in
Classname__c
), one
BO_API_Workflow_Workflow_Step__c
junction per chosen workflow (never assume all three;
derive
dropped), and the
BO_API_Step_Input_Structure__c
rows (RecordType per
inputPathType
). Both upserts key on
Unique_Key__c
(idempotent).
运行手册位于
references/generate-and-wire.md
。简而言之:保证类名的幂等性;确认目标字段的可写性;根据访谈内容生成与命名空间无关的
Callable
类(在运行时从
getSObjectType()
推导前缀);展示并确认;执行
sf project deploy start
。然后,在一个匿名Apex事务中,upsert一条
BO_API_Workflow_Step__c
记录(
Classname__c
字段为裸类名)、为每个选定的工作流upsert一条
BO_API_Workflow_Workflow_Step__c
关联记录(绝不默认选择全部三个;排除
derive
),以及
BO_API_Step_Input_Structure__c
记录(按
inputPathType
设置RecordType)。所有upsert操作均基于
Unique_Key__c
保证幂等性。

Phase 6 — Smoke test through the REST endpoints

第6阶段——通过REST端点进行冒烟测试

Runbook in
references/smoke-and-verify.md
. Derive the payload contract per invoked workflow from
BO_API_Step_Input_Structure__c
(schema-first), apply rules R1–R8 from
references/conventions-and-payload-rules.md
, resolve every reference in-sales-org, materialize
./out/smoke/{create,update,copy}.json
, then run
initialize → ingest → poll status
for each chosen workflow. Verify via a direct
BO_API_Transaction_Log__c
query (any row
!= 'Calculated'
→ fail) plus an
outputWrites
field assertion against
./out/smoke/expected.json
. Update and copy legs run only if the interview included them.
运行手册位于
references/smoke-and-verify.md
。根据调用的工作流,从
BO_API_Step_Input_Structure__c
推导负载契约(基于Schema),应用
references/conventions-and-payload-rules.md
中的规则R1–R8,解析所有销售组织内的引用,生成
./out/smoke/{create,update,copy}.json
,然后为每个选定的工作流执行
initialize → ingest → 轮询状态
流程。通过直接查询
BO_API_Transaction_Log__c
验证(任何记录状态
!= 'Calculated'
→ 失败),并针对
./out/smoke/expected.json
断言
outputWrites
字段的值。仅当访谈中包含update和copy工作流时,才会执行对应的测试流程。

Phase 7 — Report

第7阶段——生成报告

Short status: org, sales org, dry-run flag; namespace prefix; BO API seed rows before/after (Phases 2 & 4); interview (preset name or resolved
stepName
,
entity
,
workflows
,
inputPaths
,
outputWrites
); class deploy result + workflow-step id + junction ids (1–3); smoke import ids per invoked workflow, all
Calculated
with matching write assertions — or exactly which failed (SOQL, expected, actual); manual follow-ups (
none
if clean).
简短状态报告:组织信息、销售组织、试运行标记;命名空间前缀;BO API初始化数据前后记录数(第2和第4阶段);访谈信息(预设名称或解析后的
stepName
entity
workflows
inputPaths
outputWrites
);类部署结果+工作流步骤ID+关联记录ID(1–3条);每个调用工作流的冒烟测试导入ID,所有状态均为
Calculated
且写入断言匹配——或明确指出失败项(SOQL、预期值、实际值);人工后续操作(若无问题则为
none
)。

Rules

规则

  • Never claim success while any
    BO_API_Transaction_Log__c
    row for the smoke import ids has
    Status__c != 'Calculated'
    .
    Calculated
    is the terminal success state (R2);
    Processed
    does NOT exist in the picklist.
  • Never claim success while any
    outputWrites
    assertion fails, or while any
    GenericDemoSetupDataBatch
    AsyncApexJob
    since the phase start is still running or failed.
  • Stop on the first failed preflight or verify check; report the exact failure.
  • Never hard-code the namespace prefix — compose every Apex/SOQL/REST reference from the Phase-1 detection.
  • Never invent Apex method signatures — use what the packaged
    TPMSetupData
    /
    BO_API_*
    objects and the shipping REST endpoints declare.
  • The seeded BO API metadata is packaged content — preview it and get user confirmation before invoking
    TPMSetupData.setupBOApi
    .
  • Never invent an interview answer. Absent a user and any
    --preset
    /
    --interview-file
    , stop and print the questions.
  • Never wire a workflow the interview did not include.
    create
    /
    update
    /
    copy
    are individually opt-in;
    derive
    is always excluded.
  • Never compose a smoke payload from a hard-coded template — derive accepted paths per workflow from
    BO_API_Step_Input_Structure__c
    and validate user input against that contract first.
  • Never resolve a reference value without the sales-org filter. A record under a different sales org is not valid for this invocation.
  • 若任何冒烟测试导入ID对应的
    BO_API_Transaction_Log__c
    记录状态
    Status__c != 'Calculated'
    ,绝不能声称成功。
    Calculated
    是最终成功状态(规则R2);选择列表中不存在
    Processed
    状态。
  • 若任何
    outputWrites
    断言失败,或自阶段启动以来存在仍在运行或失败的
    GenericDemoSetupDataBatch
    类型
    AsyncApexJob
    ,绝不能声称成功。
  • 遇到第一个失败的预检查或验证项时立即停止;报告具体失败原因。
  • 绝不硬编码命名空间前缀——所有Apex/SOQL/REST引用均从第1阶段的检测结果动态组合。
  • 绝不自行编造Apex方法签名——使用托管包
    TPMSetupData
    /
    BO_API_*
    对象和官方REST端点声明的签名。
  • 初始化的BO API元数据是托管包内容——在调用
    TPMSetupData.setupBOApi
    前需预览并获取用户确认。
  • 绝不自行编造访谈答案。若无用户输入且未使用
    --preset
    /
    --interview-file
    参数,停止执行并打印问题列表。
  • 绝不集成访谈中未包含的工作流。
    create
    /
    update
    /
    copy
    为单独可选;
    derive
    始终排除在外。
  • 绝不从硬编码模板生成冒烟测试负载——需从
    BO_API_Step_Input_Structure__c
    推导每个工作流的可接受路径,并先验证用户输入是否符合该契约。
  • 绝不未加销售组织筛选就解析引用值。属于其他销售组织的记录对本次调用无效。