promotion_handoff
This implementation is a dependency of the documentation branch.
Return an exact human console destination for a campaign, benefit or product change. The tool observes context and targets using GET; it never performs the requested offer or financial action.
Access and input
Requires OWNER or DELEGATEE with MANAGE_PROMOTION. Hosted source scope is the exact selected channel, not account scope. channelCode and opaque pcid/pbid follow the reader's identity rules. Strict inputs do not accept arbitrary URLs, financial payloads, requestId, expectedRevision or dryRun.
| Input | Contract |
|---|---|
| channelCode | Required selected channel code. |
| action | Required discriminated object. |
| action.kind | One of the eight destinations below. |
| action.pcid | Required except for create-campaign. |
| action.pbid | Required for benefit-edit, benefit-activation, products and product-add only. |
{"channelCode":"selected-channel-code","action":{"kind":"create-campaign"}}
{"channelCode":"selected-channel-code","action":{"kind":"campaign-edit","pcid":"observed-campaign"}}
{"channelCode":"selected-channel-code","action":{"kind":"product-add","pcid":"observed-campaign","pbid":"observed-benefit"}}
Creation policy
create-campaign reads an unfiltered page 1 with limit 15 and the selected channel's current plan. It uses pagination.totalEntries, not the current displayed/filtered row count. NORMAL_ESSENTIAL permits creation only when the existing count is zero. NORMAL_BUSINESS, NORMAL_FROM_ENTERPRISE, NORMAL_PRO, ENTERPRISE_TRIAL, ENTERPRISE_ENTERPRISE, PARTNER_TRIAL, PARTNER_PARTNER and INTERNAL_INTERNAL are eligible under the shared higher-plan rule. FREE_TRIAL, NORMAL_STARTER, NORMAL_STARTER_2023_04 and NORMAL_BASIC are denied. Unknown or missing plans remain unverified and cannot create.
creationPolicy contains currentPlan, canCreate, policyVerified, existingCampaignCount, sourceRule, sourceCountObserved:true and reserved:false. sourceRule describes Essential-or-higher with one Essential campaign slot. Using the unfiltered total is a conservative interpretation of the source button's displayed-list check. A count/plan observation is not a slot reservation, subscription change or proof an eligible creation will succeed.
Blocked or unknown creation policy returns PROMOTION_PLAN_ACTION and the exact channel-plan destination. An eligible creation or verified existing target returns PROMOTION_HUMAN_ACTION. No plan is purchased or upgraded.
Destinations and result
All routes below are relative to /channels/{channelCode}/promotion-campaign and encode dynamic identities. Existing campaign/benefit targets receive repeated parent/content, selected-channel, operator and credential checks before navigation.
| action.kind | Required identity | Console suffix |
|---|---|---|
| create-campaign | None | /campaigns/create; blocked policy uses channel-plan instead. |
| campaign-edit | pcid | /campaigns/{pcid}/profile/edit |
| campaign-activation | pcid | /campaigns/{pcid}/profile |
| add-benefit | pcid | /campaigns/{pcid}/benefits/add |
| benefit-edit | pcid, pbid | /campaigns/{pcid}/benefits/{pbid}/edit |
| benefit-activation | pcid, pbid | /campaigns/{pcid}/benefits/{pbid} |
| products | pcid, pbid | /campaigns/{pcid}/benefits/{pbid}/target-product |
| product-add | pcid, pbid | /campaigns/{pcid}/benefits/{pbid}/target-product/add |
The result is always action_required on a valid handoff, with writesPerformed:false, action, requiredAction.kind open_console and verification {confirmed:false,targetObserved:true,financialActionPerformed:false}. Creation alone includes creationPolicy. Demo omits live consoleUrl. An observed target or returned link is not completed work. When choosing the human console path, product price/disconnection dialogs are opened by the person from the products screen; no modal URL or generic console-home fallback is invented.
Errors use the reader's identity, pagination, stability and permission boundaries. A missing or foreign target does not fall back to a broader route. No task journal is created; task status/resume and mutation retry do not apply. This handoff performs none of the creation, activation, schedule, discount, grant, product attachment or pricing actions; use the separate implemented writers below where their exact scope applies. The separate notification writer supports only the two documented coupon push settings.
Current existing-campaign editing
The earlier mandatory handoff boundary for campaign metadata/schedule/activation is superseded. promotions_get view profile now returns campaignRevision for promotion_campaign_apply, which saves the selected existing campaign profile or activation while preserving benefits and product prices. Existing benefit profile/activation uses promotion_benefit_apply. A handoff remains available for people. promotion_campaign_create supports a new active campaign with one initial ORDER_DISCOUNT benefit. promotion_benefit_create separately adds one ORDER_DISCOUNT or DISCOUNT to an existing campaign with Business-or-higher. Money movement stays human-operated.
For an existing connected relationship, use promotion_benefit_apply product-disconnect with the exact products-page revision and connected row ID. This removes only the relationship, verifies complete bounded absence and preserves the underlying product. Eligible new connections now use the bounded product-connect flow below; restorationSupported remains false, with no automatic relinking or rollback. A handoff is still GET-only/action_required and never claims the change happened. Financial execution remains a human-console action.
To connect eligible existing products to an existing DISCOUNT benefit, read product-options and use product-connect with connectionRevision and explicit values. The active-campaign prerequisite and five supported families apply; category model ambiguity, fixed-price CP conversion and deleted-product reconnection remain gaps. This creates relationships only. Additional DISCOUNT creation uses promotion_benefit_create with its own options/creation revisions and Business-or-higher gate, permitting inactive parents. Neither flow creates underlying products, restores previous identities or executes finance.
Inputs
| Field | Required here | Contract |
|---|---|---|
action |
Yes | one of: create-campaign, campaign-edit, campaign-activation, add-benefit, benefit-edit, benefit-activation, products, product-add (by kind) |
action.pcid |
Yes | When action.kind is campaign-edit or campaign-activation or add-benefit: string; pattern ^[A-Za-z0-9_-]{1,128}$ |
action.pcid |
Yes | When action.kind is benefit-edit or benefit-activation or products or product-add: string; pattern ^[A-Za-z0-9_-]{1,128}$ |
action.pbid |
Yes | When action.kind is benefit-edit or benefit-activation or products or product-add: string; pattern ^[A-Za-z0-9_-]{1,128}$ |