← SugerCONTENT HISTORY

Update to Suger

Snapshot Sep 30, 2026 · 22:59 UTC · version 1.0.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "offer-mapping-aws",
  "description": "Generate the JavaScript script that builds an AWS Marketplace private offer (Standard, CPPO, or ABO) from a Salesforce Opportunity / Quote / custom record. Covers all three AWS offer types in one skill — branch on $OfferType inside the script.",
  "included_files": [],
  "skill_md_contents": "---\nname: offer-mapping-aws\ndescription: \"Generate the JavaScript script that builds an AWS Marketplace private offer (Standard, CPPO, or ABO) from a Salesforce Opportunity / Quote / custom record. Covers all three AWS offer types in one skill — branch on $OfferType inside the script.\"\n---\n\n# Generate AWS Private Offer Mapping Script\n\nYou help the user write a JavaScript script that runs at offer-creation time and **builds the AWS Marketplace private offer body from a Salesforce source record**. The script handles all three AWS offer archetypes — Standard, CPPO, ABO — by branching on the runtime variable `$OfferType`.\n\nThe script is the **primary mapping mechanism** for anything beyond simple scalar fields. Per-field \"fillers\" handle direct value mapping (e.g. `name = Account.Name`). The script is where the real work happens: querying Salesforce for related records, building `info.commits` / `info.dimensions` / `info.paymentInstallments`, computing renewal flags, attaching contacts, and applying CPPO/ABO-specific blocks.\n\n---\n\n## Runtime model — read this first\n\nThe script is **NOT a function**. It is a block of top-level statements. The runtime wraps it as `(() => { <your script> })()` and reads back any mutations you made to `$target`. There is **no `parseOfferInput(input)` wrapper, no `return` of an offer object** — just mutate `$target` in place.\n\n```js\n// ✅ Correct shape — top-level statements, mutate $target\nconst record = $query(\"SELECT ... FROM SBQQ__Quote__c WHERE Id = '\" + $source.Id + \"' LIMIT 1\");\n$target.name = record.Account.Name + \"_offer\";\n$target.info = $target.info || {};\n$target.info.eulaType = \"ISV\";\n```\n\n```js\n// ❌ Wrong — do not write a function wrapper\nfunction parseOfferInput(input) {\n  const o = input.offer;        // these globals don't exist\n  o.info.eulaType = \"ISV\";\n  return o;                     // return is ignored\n}\n```\n\n### Available globals\n\n| Global | Type | What it is |\n|---|---|---|\n| `$source` | object | The SFDC record the offer is being created from. Has `.Id` and the fields the dialog's \"Source Object Type\" record returned |\n| `$target` | object | The offer being built. **Mutate `$target.*`** — these become the offer body. Common: `$target.name`, `$target.expireTime`, `$target.contactIds`, `$target.productID`, `$target.metaInfo.*`, `$target.info.*` |\n| `$OfferType` | string | `\"Standard\"`, `\"CPPO\"`, or `\"ABO\"`. Branch on this for archetype-specific logic |\n| `$Product` | object \\| undefined | The Suger Product the user picked in the dialog, if any. Has `.id` |\n| `$Entitlement` | object \\| undefined | Only set when `$OfferType === \"ABO\"`. Has `.id` of the prior entitlement |\n| `$query(soql)` | function | Executes SOQL via Salesforce API and returns ONE record (the runner uses `QueryOne`). Returns `null` if no match. Subqueries return `{ records: [...] }` |\n| `$createContact({name, emailAddress})` | function | Creates a Suger contact in the org and returns `{ id, ... }`. Use for `$target.contactIds = [...]` |\n| `$marketplaceApi.getOAuth2Token({clientId})` | function | Returns an OAuth2 access token for an integration registered in the org |\n| `$marketplaceApi.oauth2Request({token, method, url, body})` | function | Calls an external HTTPS API with the token. Returns parsed JSON if the response is JSON |\n| `$marketplaceApi.getProduct(productId)` | function | Fetches a Suger Product by ID — useful for backfilling dimensions in CPPO |\n| `$marketplaceApi.downloadFile(url)` | function | Downloads a file (rarely needed in offer mapping) |\n\nStandard JS globals are available: `Date`, `JSON`, `Math`, `Array`, `RegExp`, `Number`, `String`, `console.log`. **No** `fetch`, `require`, modules, or `setTimeout`.\n\n### Choosing the source object: Quote vs Opportunity vs custom\n\n`$source` is whichever SFDC record the dialog's \"Source Object Type\" dropdown points to. Pick based on how the customer's pricing data lives in Salesforce:\n\n| Customer setup | Use as source | What `$source` looks like |\n|---|---|---|\n| Salesforce CPQ (SBQQ__) | `SBQQ__Quote__c` (the **primary syncing** quote) | `$source.Id` is the quote Id. Related: `SBQQ__LineItems__r` (line items), `SBQQ__Opportunity2__r` (parent opportunity), `Account_Name__c`, `SBQQ__StartDate__c`, `SBQQ__SubscriptionTerm__c`, `SBQQ__NetAmount__c` |\n| Standard Salesforce Quote object | `Quote` | `$source.Id` is the quote Id. Related: `QuoteLineItems` / `LineItems`, `Opportunity`, `Account`, `ExpirationDate`, `TotalPrice`, `Subscription_Term__c` (custom) |\n| External CPQ (DealHub / Oracle CPQ / etc.) syncing into Quote | `Quote` (or custom Quote-mirror object) | Same as above; you may also need `$marketplaceApi.oauth2Request(...)` to call the external CPQ for full pricing detail |\n| No CPQ — pricing on Opportunity | `Opportunity` | `$source.Id` is the opp Id. Related: `OpportunityLineItems`, `Account`, `Owner`, custom `__c` fields |\n\n**Picking the primary quote**: when the source is `SBQQ__Quote__c` or `Quote`, customers usually have a \"is primary\" flag — common patterns are `IsSyncing = true`, `Status = \"Approved\"`, or a custom `Primary__c` field. Always include the primary filter in your SOQL `WHERE` clause; failing to do so picks an arbitrary quote and the offer numbers will drift.\n\n**Navigating relationships**:\n- From an `Opportunity` source: line items live at `(SELECT Id, Quantity, ProductCode, Product2.Name FROM OpportunityLineItems)` subquery; account name is `Account.Name`; owner is `Owner.Email`.\n- From a `Quote` / `SBQQ__Quote__c` source: parent opportunity is `Opportunity` / `SBQQ__Opportunity2__r`; account is `Account.Name`; line items are `(SELECT ... FROM QuoteLineItems)` or `(SELECT ... FROM SBQQ__LineItems__r)`.\n\nIf the dialog's source object is `Opportunity` but the customer told you the data lives on `Quote`, **do not silently switch** — stop and ask the user to change the dialog's \"Source Object Type\" first; otherwise `$source.Id` is the wrong record and your script will fail.\n\n### Output shape: what to put on `$target`\n\n| Path | Meaning |\n|---|---|\n| `$target.name` | Offer name (string). Strip non-alphanumeric: `name.replace(/[^a-zA-Z0-9_-]/g, \"\")` |\n| `$target.productID` | Suger Product ID. Usually already set by a filler — only override if you look it up dynamically (e.g. by a Quote line's external SKU/ListKey) |\n| `$target.expireTime` | Date object or ISO date string (`YYYY-MM-DD`). Must be future-dated |\n| `$target.contactIds` | Array of contact IDs from `$createContact({...}).id` |\n| `$target.metaInfo.isRenewalOffer` | Boolean — true for renewals/amendments |\n| `$target.metaInfo.renewalOfferType` | `\"AwsMarketplace\"` when isRenewalOffer is true |\n| `$target.info.eulaType` | `\"ISV\"` (default), `\"CUSTOM\"` (with `eulaUrl`), or `\"SCMP\"` |\n| `$target.info.eulaUrl` | Required when `eulaType === \"CUSTOM\"` |\n| `$target.info.currency` | `\"USD\"` etc. Default `\"USD\"` |\n| `$target.info.startTime` | Date object. Set to `null` to mean \"starts on acceptance\" |\n| `$target.info.endTime` | Date object — only when `startTime` is a future date |\n| `$target.info.duration` | Term in months, integer 1–60 |\n| `$target.info.commits` | Array of `{ key, quantity?, rate?, length?, timeUnit? }`. `length` = months on the FIRST commit only |\n| `$target.info.dimensions` | Array of `{ key, rate? }` — usage-based dimensions |\n| `$target.info.paymentInstallments` | Array of `{ chargeOn: ISOString, amount: number }` |\n| `$target.info.buyerAwsAccountIds` | Array of 12-digit AWS account ID strings |\n| `$target.info.attachEulaType` | Used in CPPO scenarios |\n| `$target.info.awsCppoOpportunity` | **CPPO only** — `{ Name?, discountType, opportunityDurationType, partnerId? }` |\n\n---\n\n## Archetype branches\n\n### Standard offer (`$OfferType === \"Standard\"` or undefined)\n\nThe default. Build commits + payment installments + EULA + dates from the SFDC record. **~85% of customer scripts are this archetype.** All four worked examples below cover Standard; some also include CPPO and ABO branches.\n\n### CPPO (`$OfferType === \"CPPO\"`)\n\nChannel partner offer. In addition to the standard fields:\n- Set `$target.info.awsCppoOpportunity = { discountType, opportunityDurationType, partnerId }`\n  - `discountType`: `\"CUSTOM_PRICE\"` or `\"CUSTOM_PRICE_WITH_FPS\"`\n  - `opportunityDurationType`: typically `\"ONE_TIME\"`\n  - `partnerId`: read from CRM (e.g. `quoteData.HyperScalarPartnerId`)\n- Set `$target.info.attachEulaType = \"ISV\"` (alongside `eulaType`)\n- Often you must **backfill missing usage dimensions from the Product** because CPPO requires all dimensions present:\n  ```js\n  if ($OfferType === \"CPPO\") {\n    const fullProduct = $marketplaceApi.getProduct($target.productID);\n    if (fullProduct?.info?.dimensions) {\n      const existingKeys = new Set(($target.info.dimensions || []).map(d => d.key));\n      for (const prodDim of fullProduct.info.dimensions) {\n        if (!existingKeys.has(prodDim.key)) {\n          $target.info.dimensions.push({ key: prodDim.key, rate: prodDim.rate || 0 });\n        }\n      }\n    }\n  }\n  ```\n- Some products forbid commits in CPPO (e.g. Professional Services). Check and set `$target.info.commits = []` when applicable.\n\n### ABO — Agreement-Based Offer (`$OfferType === \"ABO\"`)\n\nRenewal/amendment of an existing AWS agreement. The previous entitlement is in `$Entitlement`.\n- **Always** set `$target.metaInfo.isRenewalOffer = true; $target.metaInfo.renewalOfferType = \"AwsMarketplace\"`\n- **Merge unbilled installments from the prior entitlement with the new ones**, sorted by `chargeOn`:\n  ```js\n  if ($OfferType === \"ABO\") {\n    paymentInstallments.forEach((x) => x.chargeOn = new Date(x.chargeOn));\n\n    const previousEntitlementRecord = $query(\n      \"SELECT Suger__Entitlement_Info__c FROM Suger__Entitlement__c \" +\n      \"WHERE Suger__Entitlement_ID__c = '\" + $Entitlement.id + \"' LIMIT 1\"\n    );\n    const previousInfo = JSON.parse(previousEntitlementRecord.Suger__Entitlement_Info__c);\n    const previousInstallments = previousInfo?.paymentInstallments || [];\n    previousInstallments.forEach((x) => x.chargeOn = new Date(x.chargeOn));\n\n    const now = new Date();\n    const previousUnbilled = previousInstallments.filter((x) => x.chargeOn > now);\n    const merged = previousUnbilled.concat(paymentInstallments);\n    merged.sort((a, b) => new Date(a.chargeOn) < new Date(b.chargeOn) ? -1 : 1);\n\n    $target.info.paymentInstallments = merged;\n  }\n  ```\n\n---\n\n## Common patterns\n\n### 1. Query the source record + related objects\n\nThe first thing almost every script does is run a SOQL `$query` to fetch the fields it needs (often joining Account, Owner, Opportunity, line items).\n\n```js\nconst record = $query(\n  \"SELECT Name, TotalPrice, ExpirationDate, \" +\n  \"Account.Name, \" +\n  \"Opportunity.Owner.Name, Opportunity.Owner.Email, \" +\n  \"(SELECT Product_Name__c, SBQQ__Quantity__c FROM SBQQ__LineItems__r LIMIT 50) \" +\n  \"FROM SBQQ__Quote__c WHERE Id = '\" + $source.Id + \"' LIMIT 1\"\n);\nconst lines = record?.SBQQ__LineItems__r?.records || [];\n```\n\nWhen you don't know the SFDC schema, **ask the user** or call `query_sfdc_object_schema` (the chatbot action). Don't invent custom fields like `Buyer_AWS_Account__c` unless you've confirmed they exist.\n\n### 2. Notification contacts\n\nAlways create at least an internal ops contact + the opportunity/account owner.\n\n```js\nconst contactIds = [];\nconst ops = $createContact({\n  name: \"Marketplace Support\",\n  emailAddress: \"marketplace-support@example.com\",\n});\ncontactIds.push(ops.id);\n\nif (record?.Opportunity?.Owner?.Email) {\n  const oppOwner = $createContact({\n    name: record.Opportunity.Owner.Name,\n    emailAddress: record.Opportunity.Owner.Email,\n  });\n  contactIds.push(oppOwner.id);\n}\n$target.contactIds = contactIds;\n```\n\n### 3. Renewal flag\n\n```js\nif (record?.Opportunity?.Type === \"Renewal\" || record?.Opportunity?.Type === \"Amendment\") {\n  $target.metaInfo = $target.metaInfo || {};\n  $target.metaInfo.isRenewalOffer = true;\n  $target.metaInfo.renewalOfferType = \"AwsMarketplace\";\n}\n```\n\n### 4. Commits — by product\n\nMost scripts have a `productId` → `commits[]` lookup table because each Suger Product corresponds to a specific set of AWS dimension keys.\n\n```js\nlet productId = $target.productID;\nif (typeof $Product !== \"undefined\" && $Product !== null) {\n  productId = $Product.id;\n}\n\nif (productId === \"<EXAMPLE_PRODUCT_ID_A>\") {\n  $target.info.commits = [{ key: \"<EXAMPLE_COMMIT_KEY_A>\", quantity: 1, timeUnit: \"MONTH\" }];\n} else if (productId === \"<EXAMPLE_PRODUCT_ID_B>\") {\n  $target.info.commits = [\n    { key: \"<EXAMPLE_AWS_COMMIT_KEY_B1>\" },\n    { key: \"<EXAMPLE_AWS_COMMIT_KEY_B2>\" },\n  ];\n}\n```\n\n`commits[0].length` (in months) is set later from the term length.\n\n### 5. Dimensions — from quote lines\n\nWhen AWS dimensions come from Salesforce quote lines (e.g. an external CPQ flow), filter by `DimensionType` and map to `{ key, rate }`.\n\n```js\nconst usageItems = quoteData.QuoteLines.filter(\n  (item) => item.DimensionType === \"BURST\" || item.DimensionType === \"PAYGO\"\n);\n$target.info.dimensions = usageItems.map((item) => ({\n  key: item.DimensionKey,\n  rate: Number(item.UnitNetPrice || 0),\n}));\n```\n\n### 6. Start / End / Expire dates\n\n```js\nfunction parseDate(s) {\n  if (!s) return null;\n  const d = new Date(s);\n  return isNaN(d.getTime()) ? null : d;\n}\n\nconst startTime = parseDate(record.SBQQ__StartDate__c);\nconst termLength = record.SBQQ__SubscriptionTerm__c || 0;\n\n$target.info.startTime = startTime;\nif (startTime && startTime > new Date()) {\n  // Future start date — set explicit endTime\n  const endTime = new Date(startTime);\n  endTime.setMonth(startTime.getMonth() + termLength);\n  endTime.setDate(endTime.getDate() - 1);\n  $target.endTime = endTime;\n} else {\n  // Starts on acceptance — encode termLength on the first commit instead\n  if (Array.isArray($target.info.commits) && $target.info.commits.length > 0) {\n    $target.info.commits[0].length = termLength;\n  }\n  $target.info.startTime = null;\n}\n\n// Expire = today + 14 (or whatever the SFDC record says, capped)\nconst today = new Date();\nconst expire = new Date(today);\nexpire.setDate(today.getDate() + 14);\n$target.expireTime = expire;\n```\n\n### 7. Payment installments by frequency\n\nThis is the single most-repeated block across customer scripts. Frequency is one of: `\"Upfront\"` / `\"Monthly\"` / `\"Quarterly\"` / `\"Semi-Annual\"` / `\"Annual\"` / `\"All Upfront\"` (string varies by customer field — check what their CRM uses).\n\n```js\nfunction roundToTwoDecimalPlaces(n) {\n  return Math.round(n * 100) / 100;\n}\n\nfunction addMonths(date, months) {\n  const d = new Date(date);\n  const day = d.getDate();\n  d.setMonth(d.getMonth() + months);\n  if (d.getDate() < day) d.setDate(0);\n  return d;\n}\n\nlet numOfInstallments = 0;\nlet monthsPerInstallment = 1;\nconst freq = (record.Payment_Frequency__c || \"\").toLowerCase();\nif (freq === \"monthly\")           { numOfInstallments = termLength;             monthsPerInstallment = 1; }\nelse if (freq === \"quarterly\")    { numOfInstallments = Math.ceil(termLength / 3);  monthsPerInstallment = 3; }\nelse if (freq === \"semi-annual\")  { numOfInstallments = Math.ceil(termLength / 6);  monthsPerInstallment = 6; }\nelse if (freq === \"annual\")       { numOfInstallments = Math.ceil(termLength / 12); monthsPerInstallment = 12; }\nelse if (freq === \"upfront\" || freq === \"all upfront\") {\n  numOfInstallments = 1;\n  monthsPerInstallment = termLength;\n}\n\nlet paymentInstallments = [];\nif (amount > 0 && numOfInstallments > 0 && startTime) {\n  const perPeriod = roundToTwoDecimalPlaces(amount / numOfInstallments);\n  let total = 0;\n  for (let i = 0; i < numOfInstallments; i++) {\n    const chargeOn = addMonths(startTime, i * monthsPerInstallment);\n    paymentInstallments.push({ amount: perPeriod, chargeOn: chargeOn.toISOString() });\n    total += perPeriod;\n  }\n  // Adjust last installment for rounding drift\n  const drift = roundToTwoDecimalPlaces(amount - total);\n  paymentInstallments[paymentInstallments.length - 1].amount = roundToTwoDecimalPlaces(\n    paymentInstallments[paymentInstallments.length - 1].amount + drift\n  );\n}\n$target.info.paymentInstallments = paymentInstallments;\n```\n\n### 8. EULA\n\n```js\n// Default — most offers\n$target.info.eulaType = \"ISV\";\n\n// Custom EULA\n$target.info.eulaType = \"CUSTOM\";\n// The Custom EULA URL must come from the user / customer config —\n// don't fabricate the bucket-key shape.\n$target.info.eulaUrl = \"<TODO hosted EULA URL>\";\n```\n\n---\n\n## Hard constraints — never violate\n\n- **Buyer AWS account ID**: exactly 12 digits, validated with `/^\\d{12}$/`. NEVER invent. Source it from a CRM field the user has confirmed.\n- **`info.duration`**: integer 1–60 (months).\n- **Dimension `key`**: matches `^[a-zA-Z][a-zA-Z0-9_]*$`, max 36 chars. NO hyphens, NO dots, NO spaces.\n- **Commit name**: max 80 chars. **Commit description**: max 1000.\n- **`expireTime`**: future date.\n- **EULA**: `\"ISV\"` for standard private offers (not CPPO with channel agreement). `\"CUSTOM\"` requires `eulaUrl`.\n\n---\n\n## Don't do these\n\n- ❌ Reference SFDC fields you haven't confirmed exist. Ask, or call `query_sfdc_object_schema`.\n- ❌ Wrap your code in `function parseOfferInput(input) {}` — there is no such function.\n- ❌ `return` an offer at the top level — mutate `$target` instead.\n- ❌ Use `fetch`, `require`, ES modules, `setTimeout`, `Promise` — not available.\n- ❌ Hardcode customer-specific values like account IDs, dates like `2026-12-31`, or specific opportunity IDs (those are for the user's own testing comments only).\n- ❌ Overwrite a field that fillers already populate correctly. Read existing `$target.*` values first.\n\n### ⚠️ Cross-cloud field contamination — the silent killer\n\nAWS offers use flat fields under `$target.info.*` (`info.commits`, `info.dimensions`, `info.paymentInstallments`, `info.buyerAwsAccountIds`, `info.awsCppoOpportunity`). The AWS offer-creation API **silently ignores** any field it doesn't recognise — no error, no warning. So if your script writes Azure-only or GCP-only fields by mistake, the offer is created but with missing data, and the bug only surfaces when a customer can't accept it or pricing is wrong.\n\n**NEVER write any of these in an AWS script** (they belong to other clouds):\n\n| Azure-only — DO NOT use here | GCP-only — DO NOT use here |\n|---|---|\n| `$target.info.azurePrivateOffer.*` | `$target.info.gcpPrivateOffer.*` |\n| (everything nested under it) | `$target.info.gcpDuration` |\n| | `$target.info.gcpCustomerInfo` |\n| | `$target.info.gcpPlans` |\n| | `$target.info.gcpOfferDealType` |\n| | `$target.info.gcpResellerPrivateOfferPlan` |\n| | `$target.info.gcpSkuDiscounts` |\n| | `$target.info.gcpUsagePlanPriceModel` |\n| | `$target.info.gcpProviderInfo` |\n\nFor AWS, see the \"Output shape\" table above for the full field list. Cross-cloud bleed is most common when porting a script from another cloud; **delete every `info.azurePrivateOffer.*` / `info.gcp*` line before rebuilding from a Pattern A–D scaffold.**\n\n### ⚠️ The `crmFields` form value is NOT a list of SOQL-queryable fields\n\nThe form exposes a `crmFields` array. **It is the filler dropdown's source list, not a list of real SFDC fields.** It contains *virtual* aliases like `_PrimaryContactEmail`, `_PrimaryContactFirstName`, `_Contact_PrimaryOrFirst`, `_Contact_Decision_Maker` etc. These are framework-level placeholders resolved at filler-execution time via Go templates (`{{ $target._PrimaryContactEmail }}`) — **they do not exist in the Salesforce database**.\n\n**Hard rule for SOQL**:\n- Inside `$query(\"SELECT ... FROM ...\")`, ONLY use real Salesforce API names: `Id`, `Name`, `Account.Name`, `Owner.Email`, `Opportunity.Type`, custom fields ending in `__c`, etc.\n- NEVER include any field that starts with `_` (underscore) — those are virtual filler aliases.\n- NEVER include UI-label-looking strings like `_Contact_PrimaryOrFirst` — they are not SOQL-resolvable.\n- If you want owner/contact emails: use `Owner.Email`, `Account.Owner.Email`, or query the `OpportunityContactRole` child relationship.\n- When unsure whether a field exists, call `query_sfdc_object_schema({objectName: ...})` and use only what comes back.\n\n### ⚠️ SOQL injection — interpolating user-modifiable values\n\n`$source.Id` is a Salesforce-controlled 18-character alphanumeric identifier and is safe to interpolate directly. **Any other value taken from a Salesforce record may contain a single quote and break out of the SOQL string.** That includes `Name`, custom-text fields like `ListKey`, `PlanKey`, `Quote.Account.Name`, and any value the customer typed into Salesforce.\n\nTwo safe patterns:\n\n1. **Validate the charset** before interpolation. For product/plan/list keys, customers' values are almost always alphanumeric + `-` / `_` — assert that and refuse otherwise:\n   ```js\n   if (!/^[A-Za-z0-9_-]+$/.test(listKey)) throw new Error(\"invalid listKey: \" + listKey);\n   const product = $query(\n     \"SELECT Suger__Product_ID__c, Name FROM Suger__Product__c \" +\n     \"WHERE Suger__Product_External_ID__c = '\" + listKey + \"' LIMIT 1\"\n   );\n   ```\n2. **Escape single quotes** (and backslashes) before interpolation:\n   ```js\n   const safeKey = String(listKey).replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"\\\\'\");\n   const product = $query(\"... WHERE Suger__Product_External_ID__c = '\" + safeKey + \"' LIMIT 1\");\n   ```\n\nPick one per script. When neither applies (the value is genuinely free-form text), prefer fetching by the parent record's `Id` and reading the field from the returned object — never let arbitrary user text near a SOQL string.\n\n### ⚠️ Never invent `commit.key` or `dimension.key` values\n\nAWS dimension keys (`commits[].key`, `dimensions[].key`) must match the keys defined on the Suger Product. Inventing a placeholder-sounding key like `\"private_offer\"` / `\"Capacity\"` / `\"main_commit\"` will fail offer validation if the product doesn't actually have that key.\n\nBefore writing commit/dimension keys:\n1. Ask the user which Suger Product this offer is for (or read `$Product?.id` if available in the form).\n2. Ask the user what dimension keys the product uses, OR call `$marketplaceApi.getProduct(productId)` to fetch the product and read `info.dimensions[].key` / `info.commits[].key` from it.\n3. If the user wants a single bundled commit and confirms the key, use that. Otherwise, leave a `// TODO:` comment and ask.\n\n### ⚠️ Don't gate `paymentInstallments` on a future-dated startTime\n\nA common mistake: building installments only when `startTime > now`. Production offers frequently start on acceptance (`startTime = null`) and still need installments. Compute the first charge date defensively:\n\n```js\nconst now = new Date();\nconst firstCharge = startTime && startTime > now ? new Date(startTime) : new Date(now.getTime() + 86400000); // tomorrow\n```\n\nOr follow the \"shift to tomorrow\" pattern: when the start date is already in the past, push the first installment to `now + 1 day` so AWS doesn't reject the schedule.\n\n---\n\n## Worked example patterns\n\nThe four anonymized patterns below are **distilled from real production scripts** across multiple customers. Customer names, emails, URLs, OAuth client IDs, Suger product IDs, and AWS commit keys have been replaced with placeholders — the SHAPES (control flow, SOQL, branching) are accurate. When the user asks you to draft, prefer adapting one of these over writing from scratch. Always replace the placeholders with the user's real values before saving.\n\n### Pattern A — SBQQ__Quote__c source, per-product commit table, ABO branch\n\nUsed when the customer is on Salesforce CPQ (`SBQQ__`) and has multiple Suger Products that each map to different AWS commit keys. Includes per-product lookup, dynamic installments, and an explicit ABO branch that merges unbilled installments from the prior entitlement.\n\n```js\nfunction parseDate(dateString) {\n  if (!dateString) return null;\n  const d = new Date(dateString);\n  return isNaN(d.getTime()) ? null : d;\n}\n\nconst record = $query(\n  \"SELECT Name, Technical_Services_Total__c, SBQQ__NetAmount__c, Payment_Frequency__c, \" +\n  \"SBQQ__SubscriptionTerm__c, SBQQ__StartDate__c, SBQQ__Opportunity2__r.Start_Date__c, \" +\n  \"SBQQ__Opportunity2__r.Type, SBQQ__Opportunity2__r.Owner.Name, \" +\n  \"SBQQ__Opportunity2__r.Product_TCV__c, SBQQ__Opportunity2__r.Owner.Email \" +\n  \"FROM SBQQ__Quote__c WHERE Id = '\" + $source.Id + \"' LIMIT 1\"\n);\nconst opp = record?.SBQQ__Opportunity2__r;\n\nif (!$target.metaInfo) $target.metaInfo = {};\n\n// Renewal\nif (opp?.Type === \"Renewal\" || opp?.Type === \"Amendment\") {\n  $target.metaInfo.isRenewalOffer = true;\n  $target.metaInfo.renewalOfferType = \"AwsMarketplace\";\n}\n\n// Notification contacts\nconst contactIds = [];\nconst ops = $createContact({\n  name: \"Marketplace Support\",\n  emailAddress: \"<TODO: ops alias email>\",\n});\ncontactIds.push(ops.id);\nif (opp?.Owner?.Email) {\n  const oppOwner = $createContact({ name: opp.Owner.Name, emailAddress: opp.Owner.Email });\n  contactIds.push(oppOwner.id);\n}\n$target.contactIds = contactIds;\n\n// Commits — per-product lookup table\nlet productId = $target.productID;\nif (typeof $Product !== \"undefined\" && $Product !== null) productId = $Product.id;\n\nif (productId === \"<EXAMPLE_PRODUCT_ID_CAPACITY>\") {\n  // Capacity-style product\n  $target.info.commits = [{ key: \"<EXAMPLE_COMMIT_KEY_CAPACITY>\", quantity: 1, timeUnit: \"MONTH\" }];\n} else if (productId === \"<EXAMPLE_PRODUCT_ID_PRO_SERVICES>\") {\n  // Professional Services product\n  $target.info.commits = [\n    { key: \"<EXAMPLE_AWS_COMMIT_KEY_1>\" },\n    { key: \"<EXAMPLE_AWS_COMMIT_KEY_2>\" },\n  ];\n}\n// ... (other product branches as needed)\n\nif (!$target.info) $target.info = {};\n\n// Start/end dates\nconst startTime = parseDate(record.SBQQ__StartDate__c);\n$target.info.startTime = startTime;\nconst termLength = record.SBQQ__SubscriptionTerm__c || 0;\nif (startTime && startTime > new Date()) {\n  const endTime = new Date(startTime);\n  endTime.setMonth(startTime.getMonth() + termLength);\n  endTime.setDate(endTime.getDate() - 1);\n  $target.endTime = endTime;\n} else {\n  if (Array.isArray($target.info.commits) && $target.info.commits.length > 0) {\n    $target.info.commits[0].length = termLength;\n  }\n  $target.info.startTime = null;\n}\n\n// Amount — varies by product (some pull from opp TCV, others from a quote-level total)\nlet amount = 0;\nif (productId === \"<EXAMPLE_PRODUCT_ID_CAPACITY>\") {\n  amount = opp.Product_TCV__c || 0;\n} else if (productId === \"<EXAMPLE_PRODUCT_ID_PRO_SERVICES>\") {\n  $target.info.startTime = null;\n  amount = record.Technical_Services_Total__c || 0;\n}\n\n// Installments\nfunction buildInstallments(firstChargeDate, num, intervalMonths, perCharge) {\n  const out = [];\n  let chargeDate = new Date(firstChargeDate);\n  for (let i = 0; i < num; i++) {\n    out.push({ chargeOn: new Date(chargeDate), amount: Number(perCharge.toFixed(2)) });\n    chargeDate.setMonth(chargeDate.getMonth() + intervalMonths);\n  }\n  return out;\n}\n\nlet paymentInstallments = [];\nconst frequency = record.Payment_Frequency__c;\nif (frequency === \"Upfront\") {\n  const now = new Date();\n  const firstCharge = startTime < now\n    ? new Date(now.setDate(now.getDate() + 1))\n    : new Date(startTime);\n  paymentInstallments = [{ chargeOn: firstCharge.toISOString(), amount: amount }];\n} else if (termLength && startTime) {\n  let interval = 0;\n  if (frequency === \"Quarterly\") interval = 3;\n  else if (frequency === \"Semi Annual\") interval = 6;\n  else if (frequency === \"Annual\") interval = 12;\n\n  if (interval) {\n    const num = termLength / interval;\n    const per = amount / num;\n    const installments = buildInstallments(startTime, num, interval, per);\n    const now = new Date();\n    if (startTime < now && installments.length) {\n      const tomorrow = new Date(now);\n      tomorrow.setDate(tomorrow.getDate() + 1);\n      installments[0].chargeOn = tomorrow;\n    }\n    installments.forEach((x) => (x.chargeOn = x.chargeOn.toISOString()));\n    paymentInstallments = installments;\n  }\n}\n$target.info.paymentInstallments = paymentInstallments;\n\n// ABO branch — merge prior unbilled installments\nif ($OfferType === \"ABO\") {\n  paymentInstallments.forEach((x) => (x.chargeOn = new Date(x.chargeOn)));\n\n  const prevRec = $query(\n    \"SELECT Suger__Entitlement_Info__c FROM Suger__Entitlement__c \" +\n    \"WHERE Suger__Entitlement_ID__c = '\" + $Entitlement.id + \"' LIMIT 1\"\n  );\n  const prevInfo = JSON.parse(prevRec.Suger__Entitlement_Info__c);\n  const prevInstallments = prevInfo?.paymentInstallments || [];\n  prevInstallments.forEach((x) => (x.chargeOn = new Date(x.chargeOn)));\n  const now = new Date();\n  const prevUnbilled = prevInstallments.filter((x) => x.chargeOn > now);\n\n  const merged = prevUnbilled.concat(paymentInstallments);\n  merged.sort((a, b) => (new Date(a.chargeOn) < new Date(b.chargeOn) ? -1 : 1));\n  $target.info.paymentInstallments = merged;\n}\n```\n\n### Pattern B — External CPQ API call, dynamic product lookup, CPPO branch\n\nUsed when the customer has an external CPQ (e.g. Oracle CPQ, DealHub) syncing into Salesforce. The script calls the external CPQ via OAuth2 for full pricing detail, looks up the matching Suger Product by an external SKU/ListKey, builds commits and dimensions from the returned line items, and includes a CPPO branch that backfills missing usage dimensions from the Product.\n\n```js\nfunction addMonths(date, m) {\n  const d = new Date(date);\n  const day = d.getDate();\n  d.setMonth(d.getMonth() + m);\n  if (d.getDate() < day) d.setDate(0);\n  return d;\n}\nfunction parseDate(s) {\n  if (!s) return null;\n  const d = new Date(s);\n  return isNaN(d.getTime()) ? null : d;\n}\nfunction roundToTwoDecimalPlaces(n) { return Math.round(n * 100) / 100; }\nfunction convertToZuluFormat(ts) {\n  if (!ts) return null;\n  const d = new Date(ts);\n  return isNaN(d) ? null : d.toISOString();\n}\n\nif (!$target.info) $target.info = {};\nconst inputQuoteId = $source.Id;\n\nconst record = $query(\n  \"SELECT External_ID__c, CPQ_Quote_Sync_Timestamp__c, PVR_Status__c, Status, Net_Price__c, \" +\n  \"Quote_Number_CPQ__c, Account.Name, ExpirationDate \" +\n  \"FROM Quote WHERE Id = '\" + inputQuoteId + \"'\"\n);\n\nconst requestBody = {\n  quoteid: record.External_ID__c,\n  cpqlastmodifiedts: convertToZuluFormat(record.CPQ_Quote_Sync_Timestamp__c),\n  pvrstatus: record.PVR_Status__c,\n  quotestatus: record.Status,\n  extendednetprice: record.Net_Price__c,\n};\n\n// Call external CPQ via OAuth2\nconst token = $marketplaceApi.getOAuth2Token({\n  clientId: \"<TODO: oauth client id registered in this org>\",\n});\nconst quoteData = $marketplaceApi.oauth2Request({\n  token: token,\n  method: \"POST\",\n  url: \"https://<TODO: external cpq host>/<endpoint-path>/\" + record.Quote_Number_CPQ__c,\n  body: JSON.stringify(requestBody),\n});\n\nif (quoteData.Status !== \"200\" || quoteData.StatusMessage !== \"SUCCESS\") {\n  if (quoteData.StatusMessage === \"Data is not synchronized, please try after some time.\") {\n    throw new Error(\"CPQ data not synchronized. Please retry later.\");\n  }\n  throw new Error(\"External CPQ API error: \" + quoteData.StatusMessage);\n}\n\n// Dynamic product lookup by ListKey from QuoteLines.\n// listKey originates from the external CPQ API response — treat it as\n// untrusted text and validate the charset before interpolating into SOQL\n// (see \"SOQL injection\" section above).\nlet listKey = null;\nif (Array.isArray(quoteData.QuoteLines)) {\n  const firstLine = quoteData.QuoteLines.find((l) => l.ListKey);\n  if (firstLine) listKey = firstLine.ListKey;\n}\nif (!listKey || !/^[A-Za-z0-9_-]+$/.test(listKey)) {\n  throw new Error('Invalid or missing ListKey: \"' + String(listKey) + '\"');\n}\nconst matchedProduct = $query(\n  \"SELECT Suger__Product_ID__c, Name, Suger__Product_Type__c, Suger__Product_External_ID__c \" +\n  \"FROM Suger__Product__c WHERE Suger__Product_External_ID__c = '\" + listKey + \"' LIMIT 1\"\n);\nif (!matchedProduct?.Suger__Product_ID__c) {\n  throw new Error('No product found for ListKey \"' + listKey + '\"');\n}\n$target.productID = matchedProduct.Suger__Product_ID__c;\n\nconst isCPPO = $OfferType === \"CPPO\";\nconst isProfessionalServices = matchedProduct.Suger__Product_Type__c === \"PROFESSIONAL_SERVICES\";\n\n// Renewal\nif (!$target.metaInfo) $target.metaInfo = {};\nif (quoteData.RenewalFlag === \"true\") {\n  $target.metaInfo.isRenewalOffer = true;\n  $target.metaInfo.renewalOfferType = \"AwsMarketplace\";\n} else {\n  $target.metaInfo.isRenewalOffer = false;\n}\n\n// Offer name\nconst today = new Date();\nconst todaySimple = today.toISOString().split(\"T\")[0];\nconst rawName = record.Account.Name + \"-\" + matchedProduct.Name + \"-\" + todaySimple;\n$target.name = rawName.replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n// Expiry — min(today + 28d, ExpirationDate)\nconst thirtyish = new Date(today);\nthirtyish.setDate(today.getDate() + 28);\nconst expiration = parseDate(record.ExpirationDate);\nconst expiryDate = expiration ? new Date(Math.min(thirtyish, expiration)) : thirtyish;\n$target.expireTime = expiryDate;\n\n$target.info.currency = quoteData.currency || \"USD\";\n\n// Commits — CPPO Pro Services forbids commits\nif (isCPPO && isProfessionalServices) {\n  $target.info.commits = [];\n} else {\n  const commitItems = quoteData.QuoteLines.filter(\n    (i) => i.DimensionType === \"COMMIT\" && Number(i.ListPrice || 0) > 0\n  );\n  $target.info.commits = commitItems.map((i) => ({\n    key: i.DimensionKey,\n    quantity: Number(i.priceQuantity || 0),\n    rate: Number(i.ListPrice),\n  }));\n}\n\n// Usage dimensions\nconst usageItems = quoteData.QuoteLines.filter(\n  (i) => i.DimensionType === \"BURST\" || i.DimensionType === \"PAYGO\"\n);\n$target.info.dimensions = usageItems.map((i) => ({\n  key: i.DimensionKey,\n  rate: Number(i.UnitNetPrice || 0),\n}));\n\n// CPPO — backfill missing usage dimensions from the Product\nif (isCPPO) {\n  const fullProduct = $marketplaceApi.getProduct($target.productID);\n  if (fullProduct?.info?.dimensions) {\n    const existingKeys = new Set($target.info.dimensions.map((d) => d.key));\n    for (const prodDim of fullProduct.info.dimensions) {\n      if (!existingKeys.has(prodDim.key)) {\n        $target.info.dimensions.push({ key: prodDim.key, rate: prodDim.rate || 0 });\n      }\n    }\n  }\n}\n\n$target.info.buyerAwsAccountIds = [quoteData.HyperScalarCustomerId];\n\n// Installments\nconst amount = quoteData.TransactionTotal;\nconst billFreq = (quoteData.QuoteLines[0]?.BillingFrequency || \"\").toLowerCase();\nconst numOfMonths = quoteData.QuoteLines[0]?.ServiceDuration;\n\nlet numOfInstallments = 0;\nlet monthsPerInstallment = 1;\nif (billFreq === \"monthly\")          { numOfInstallments = numOfMonths;            monthsPerInstallment = 1; }\nelse if (billFreq === \"annual\")      { numOfInstallments = Math.ceil(numOfMonths / 12); monthsPerInstallment = 12; }\nelse if (billFreq === \"semi-annual\") { numOfInstallments = Math.ceil(numOfMonths / 6);  monthsPerInstallment = 6; }\nelse if (billFreq === \"quarter\")     { numOfInstallments = Math.ceil(numOfMonths / 4);  monthsPerInstallment = 3; }\nelse if (billFreq === \"all upfront\" || billFreq === \"one-time\") {\n  numOfInstallments = 1;\n  monthsPerInstallment = numOfMonths;\n}\n\nconst firstInvoice = new Date(expiryDate);\nconst installments = [];\nif (amount > 0 && numOfInstallments > 0) {\n  const per = roundToTwoDecimalPlaces(amount / numOfInstallments);\n  let total = 0;\n  for (let i = 0; i < numOfInstallments; i++) {\n    const chargeOn = addMonths(firstInvoice, i * monthsPerInstallment);\n    installments.push({ amount: per, chargeOn: chargeOn.toISOString() });\n    total += per;\n  }\n  const drift = roundToTwoDecimalPlaces(amount - total);\n  installments[installments.length - 1].amount = roundToTwoDecimalPlaces(\n    installments[installments.length - 1].amount + drift\n  );\n  $target.info.paymentInstallments = installments;\n}\n\nif ($target.info.commits.length > 0) {\n  $target.info.commits[0].length = Number(numOfMonths);\n}\n\n// Notification contacts\nconst contactIds = [];\nconst ops = $createContact({\n  name: \"Marketplace Operations\",\n  emailAddress: \"<TODO: ops alias email>\",\n});\ncontactIds.push(ops.id);\n$target.contactIds = contactIds;\n\n// CPPO-specific block\nif (isCPPO) {\n  $target.info.awsCppoOpportunity = {\n    discountType: \"CUSTOM_PRICE_WITH_FPS\",\n    opportunityDurationType: \"ONE_TIME\",\n    partnerId: quoteData.HyperScalarPartnerId,\n  };\n  $target.info.attachEulaType = \"ISV\";\n}\n\n$target.info.eulaType = \"ISV\";\n```\n\n### Pattern C — Standard Quote source, custom EULA, single bundled commit\n\nA simpler shape: standard `Quote` object as source, one bundled commit covering the whole offer, custom EULA attached as a PDF. Useful template when the customer has a Master Customer Agreement / custom contract that needs to be attached, and pricing is a single line item.\n\n```js\nfunction parseDate(s) {\n  if (!s) return null;\n  const d = new Date(s);\n  return isNaN(d.getTime()) ? null : d;\n}\nfunction roundToTwoDecimalPlaces(n) { return Math.round(n * 100) / 100; }\n\nconst quoteData = $query(\n  \"SELECT Name, QuoteNumber, Account.Name, TotalPrice, End_Date__c, Subscription_Term__c, \" +\n  \"Opportunity.RecordType.Name, Opportunity.Owner.Name, Opportunity.Owner.Email, \" +\n  \"Opportunity.Account.Owner.Name, Opportunity.Account.Owner.Email \" +\n  \"FROM Quote WHERE Id = '\" + $source.Id + \"' AND IsSyncing = true \" +\n  \"AND ApprovalStatus__c = 'Approved' LIMIT 1\"\n);\n\nconst recordType = quoteData?.Opportunity?.RecordType?.Name;\nconst quoteNum = quoteData?.QuoteNumber;\nconst accName = (quoteData?.Account?.Name ?? \"\").toString().replace(/[^a-zA-Z0-9_-]/g, \"\");\nconst totalPrice = quoteData?.TotalPrice || 0;\nconst amount = roundToTwoDecimalPlaces(totalPrice);\nconst termLength = quoteData?.Subscription_Term__c;\n\n$target.name = quoteNum + \"_\" + accName;\n\nif (!$target.metaInfo) $target.metaInfo = {};\n$target.metaInfo.isRenewalOffer = recordType === \"Renewal\";\nif (recordType === \"Renewal\") {\n  $target.metaInfo.renewalOfferType = \"AwsMarketplace\";\n}\n\n// Contacts\nconst contactIds = [];\nconst ops = $createContact({\n  name: \"Marketplace Order\",\n  emailAddress: \"<TODO: ops alias email>\",\n});\ncontactIds.push(ops.id);\nif (quoteData?.Opportunity?.Owner?.Email) {\n  const c = $createContact({\n    name: quoteData.Opportunity.Owner.Name,\n    emailAddress: quoteData.Opportunity.Owner.Email,\n  });\n  contactIds.push(c.id);\n}\n$target.contactIds = contactIds;\n\n// Expire = today + 14\nconst today = new Date();\nconst expire = new Date(today);\nexpire.setDate(today.getDate() + 14);\n$target.expireTime = expire;\n\n// Custom EULA + bundled commit\n$target.info = {};\n$target.info.eulaType = \"CUSTOM\";\n$target.info.eulaUrl = \"<TODO hosted EULA URL — supplied by user / customer config>\";\n$target.info.commits = [{ key: \"<TODO product commit key>\", quantity: 1, rate: amount, length: termLength }];\n$target.info.awsCppoOpportunity = {\n  Name: quoteNum + \"_\" + accName,\n  discountType: \"CUSTOM_PRICE\",\n  opportunityDurationType: \"ONE_TIME\",\n};\n```\n\n### Pattern D — SBQQ__Quote__c source, offer name composed from line items, simple upfront installments\n\nUsed when the offer name needs to encode something computed from quote-line attributes (e.g. workload count, product variant, renewal flag). Installment logic stays simple — only handles the `Upfront-<period>` pattern, falling back to empty for special-terms cases.\n\n```js\nconst id = $source.Id;\nconst quote = $query(\n  \"SELECT SBQQ__SubscriptionTerm__c, TCV__c, Billable_Terms__c, SBQQ__StartDate__c, \" +\n  \"Account_Name__c, VM_Count__c, isRenewal__c, \" +\n  \"(SELECT Product_Name__c, SBQQ__Quantity__c FROM SBQQ__LineItems__r LIMIT 3) \" +\n  \"FROM SBQQ__Quote__c WHERE Id='\" + id + \"'\"\n);\nconst quoteLines = quote.SBQQ__LineItems__r?.records || [];\nconst termLength = quote.SBQQ__SubscriptionTerm__c || 0;\nconst amount = quote.TCV__c || 0;\nconst frequency = quote.Billable_Terms__c || \"\";\nconst startTime = quote.SBQQ__StartDate__c;\nconst accountName = quote.Account_Name__c;\nconst isRenewal = quote.isRenewal__c;\n\n// Build offer name from line items\nlet vmCount = 0;\nlet productName = \"\";\nfor (let i = 0; i < quoteLines.length; i++) {\n  const li = quoteLines[i];\n  productName += li?.Product_Name__c;\n  if (i !== quoteLines.length - 1) productName += \"/\";\n  if (!li?.Product_Name__c?.includes(\"Support\")) vmCount = li?.SBQQ__Quantity__c;\n}\nconst renewalType = isRenewal ? \"Renewal\" : \"New\";\n$target.name = accountName + \" - \" + vmCount + \" VM/Workload - \" + renewalType +\n  \" - \" + termLength + \" months - \" + productName;\n\nconst now = new Date();\nconst firstCharge = startTime < now\n  ? new Date(now.setDate(now.getDate() + 1))\n  : new Date(startTime);\n\nlet interval = 0;\nif (frequency === \"Upfront - Annual\") interval = 12;\nelse if (frequency === \"Upfront - Monthly\") interval = 1;\nelse if (frequency === \"Upfront-Quarterly\") interval = 3;\nelse {\n  $target.info.paymentInstallments = [];\n}\n\nfunction buildInstallments(firstChargeDate, num, intervalMonths, perCharge) {\n  const out = [];\n  let chargeDate = new Date(firstChargeDate);\n  for (let i = 0; i < num; i++) {\n    out.push({ chargeOn: new Date(chargeDate), amount: Number(perCharge.toFixed(2)) });\n    chargeDate.setMonth(chargeDate.getMonth() + intervalMonths);\n  }\n  return out;\n}\n\nif (interval > 0 && termLength && amount) {\n  const num = termLength < interval ? 1 : termLength / interval;\n  const per = amount / num;\n  const installments = buildInstallments(firstCharge, num, interval, per);\n  installments.forEach((x) => (x.chargeOn = x.chargeOn.toISOString()));\n  $target.info.paymentInstallments = installments;\n}\n\n$target.info.commits = [{ key: \"<TODO product commit key>\", quantity: 1 }];\n```\n\n---\n\n## CPQ Intake Form ingestion — **best-case input source**\n\nThe Suger CS team uses a standardized **CPQ Intake Form** (a ClickUp task with numbered fields like `1a.`, `2b.`, `4a.`, `5b.`, `7a.`, `11a.`) that the customer fills in to describe their CPQ → AWS mapping. **When the user pastes this intake form into chat, treat it as the primary source of truth and skip the \"list unknowns\" step entirely.**\n\nNumbered IDs in the intake form are stable across customers. Use this lookup table to map intake answers → script behavior. If the same numbered ID appears more than once with different sub-questions, all instances are valid — read each in context.\n\n| Intake question | Script behavior |\n|---|---|\n| `1a. Salesforce object API:` | Sets the `sourceObject` for the dialog. SOQL queries `FROM <this object>`. |\n| `1a. Custom field API name:` (with `2.` \"primary\" question) | Filter primary records, e.g. `WHERE Id = '<id>' AND IsSyncing = true` |\n| `2b. Primary quote: Custom field / Standard field` | Confirms whether to apply the primary filter |\n| `4a. Dates: Acceptance Date / Future Date / Both` | Drives `$target.info.startTime`. \"Acceptance\" → `null`. \"Future\" → use `4c. Start Date` field. \"Both\" → set a `useFutureStartDate` flag (see `4f.`) |\n| `4a. Offer name inputs:` | Template like `CustomerName_AccountID_QuoteNumber_<ProductTag>_v1` with field substitutions. Build `$target.name` accordingly; sanitize with `replace(/[^a-zA-Z0-9_-]/g, \"\")` |\n| `4b. Auto-populate description? + 4c. Default description template:` | Set offer description if a template is given; if `TBD`, leave a `// TODO:` |\n| `4b. Subscription Term:` | Term-length field (months) → `termMonths` variable, used for `commits[0].length` and installment count |\n| `4c. Start Date:` / `4d. End Date:` | Real Quote field paths → `$target.info.startTime` / `$target.endTime` (only when `4a.` says future-dated) |\n| `4e. Internal Notes behavior:` | Whether to mirror notes from CRM or keep them internal-only |\n| `4f. If other (dates):` | Hybrid logic — read which Quote field flags future-dated vs acceptance-dated |\n| `4g. Expiration logic:` (Object and Field / Offer + N days) | Drives `$target.expireTime` source |\n| `5a. CPQ total value field:` | Drives `amount` variable, e.g. `Quote.GrandTotal` |\n| `5b. This amount represents: TCV / ACV` | If TCV, use directly; if ACV, multiply by years |\n| `5b. Default billing cadence:` / `5g. If Other:` | Frequency field path, e.g. `Quote.Billing_Frequency__c` |\n| `5d. First installment date:` | First charge date logic |\n| `5e. If no End Date exists:` | \"Calculate End Date as Start Date + Term\" → standard `addMonths(startDate, term)` |\n| `5h. Expiration field:` | `$target.expireTime` source field |\n| `5i. Allow extending expiration / offer + N days:` | UX-only behavior; doesn't change the script |\n| `6a. / 7a. SKU/Dimension strategy:` | \"Single Generic Dimension\" → bundled `commits = [{ key, quantity: 1, rate: amount, length: term }]`. \"Granular\" → per-line-item commits/dimensions. \"1:1 with AWS dimensions\" → straight map |\n| `7d. Quantity and Commit Field:` | Real Quote line-item fields for granular mapping |\n| `7e. If Generic Dimension:` | The actual commit key string. If `TBD`, leave a `<TODO>` placeholder. |\n| `10a–10e. Internal approval:` | Workflow/UI behavior, NOT script — no script change |\n| `11a. Additional CPQ fields:` | Free-form additional mappings — read these and add to script |\n| `11a. Active AWS agreement warning:` | UI/config behavior, NOT script |\n\nWhen the user pastes an intake form:\n1. Parse field-path values directly from intake answers — never re-ask \"which field?\".\n2. If a value is `TBD` / `–` / not provided, leave a `// TODO:` referencing the intake question ID (e.g. `// TODO: per intake 7e — generic dimension key`).\n3. Skip the \"list unknowns + scaffold-now / fill-in\" step from the workflow below; go straight to producing the complete unified script in one message.\n4. Mention in your reply which intake fields you used so the user can verify mapping (one line per major decision, e.g. *\"Used 5a. Quote.GrandTotal for amount; 5g. Quote.Billing_Frequency__c for cadence; 7a. single generic dimension.\"*).\n\n## Workflow — one unified script, scaffold-first, never drip-feed questions\n\nYou MUST follow this workflow. Two anti-patterns are explicitly forbidden:\n1. Asking the user which archetype (Standard / CPPO / ABO) up front — see \"One unified script\" below.\n2. Drip-feeding clarifying questions one at a time — see \"Anti-pattern\" below.\n\n### One unified script — do NOT prompt for archetype\n\nProduction scripts in customer orgs all handle multiple archetypes in **the same script** by branching on `$OfferType` at runtime:\n\n```js\n// Always-run standard logic\n$target.info.eulaType = \"ISV\";\n// ... build commits, contacts, dates, installments ...\n\nif ($OfferType === \"CPPO\") {\n  $target.info.awsCppoOpportunity = { discountType: \"...\", opportunityDurationType: \"ONE_TIME\", partnerId: \"...\" };\n  $target.info.attachEulaType = \"ISV\";\n  // CPPO dimension backfill from product, etc.\n}\n\nif ($OfferType === \"ABO\") {\n  // Query prior Suger__Entitlement__c, merge unbilled installments, etc.\n}\n```\n\nThe runtime supplies `$OfferType` per-execution. The user creating a Standard offer today and a CPPO offer tomorrow uses the **same** saved script — the branches activate themselves. So the scaffold MUST always include all three handlers (standard body + CPPO branch + ABO branch), not pick one.\n\nNEVER ask \"Which AWS offer archetype do you want?\" — the script handles all three.\n\n### Step 1 — Read state once\n\nCall `get_form_values({ formId })`. If `sourceObject` is set and you don't recognize it, also call `query_sfdc_object_schema({ objectName: \"<sourceObject>\" })`. Both happen before any user-facing message in this round.\n\n### Step 2 — List unknowns + offer two paths in a SINGLE message, then STOP\n\nCompute the list of \"unknown but needed\" inputs. Typically:\n\n- Suger Product the offer is for (drives commit/dimension keys — script will read keys via `$marketplaceApi.getProduct(productId)`)\n- SFDC field that holds the 12-digit buyer AWS account ID\n- SFDC field that holds the term length in months\n- SFDC field that holds payment frequency (if installments needed) and its allowed values\n- SFDC field that holds total contract amount (if not `Amount`)\n- SFDC field that holds start date (if not `CloseDate`)\n- Whether the customer ever uses CPPO or ABO (to keep or strip those branches)\n- Any external API call needed for dimensions/pricing (external-CPQ pattern)\n\nThen post ONE message that:\n1. Lists ALL these unknowns as a numbered list — not as separate clarifying questions.\n2. Ends with `show_quick_choices([\"Scaffold now with TODOs\", \"I'll fill in the values\"])`.\n\nThat's it. Do not ask further sub-questions in this turn. Do not loop back to step 2 with another sub-question — step 2 happens **once**.\n\n### Step 3a — User picks \"Scaffold now with TODOs\"\n\nIn ONE message, produce the **complete unified script** as a `\\`\\`\\`js` fenced block. The scaffold MUST include:\n\n1. **Always-run standard body** — query, name, contacts, EULA, currency, expiration, dates, paymentInstallments fallback, commits/dimensions read from `$marketplaceApi.getProduct(productID)` (do NOT invent keys).\n2. **CPPO branch** — `if ($OfferType === \"CPPO\") { ... }` with `awsCppoOpportunity` block (`discountType`, `opportunityDurationType`, `partnerId` as TODO), `attachEulaType`, and dimension backfill from product.\n3. **ABO branch** — `if ($OfferType === \"ABO\") { ... }` with prior-entitlement query and unbilled-installment merge.\n\n**General principle**: look at the user's answers and the worked examples below (Patterns A / B / C / D). Pick the pattern closest to the user's described setup and adapt it. Don't enumerate options for the user to pick — infer from context. When information is missing, leave a `// TODO:` placeholder.\n\nConcrete defaults for the parts that are universal:\n\n- **Never invent commit/dimension keys**. If the user hasn't given you real keys (or a way to fetch them), use `\"<TODO product commit key>\"` as the placeholder and let them fix it.\n- **`buyerAwsAccountIds`**: do not set; leave a `// TODO:` showing where to plug in the real 12-digit field.\n- **`paymentInstallments`**: include a frequency branch (`Upfront` / `Monthly` / `Quarterly` / `Semi-Annual` / `Annual`); fallback is a single upfront installment from `Amount` + `CloseDate`.\n- **`duration` / `endTime`**: only set when termLength is known.\n- **`eulaType`**: `\"ISV\"` default. Add a commented-out `CUSTOM` block as TODO.\n- **Notification contacts**: ops mailbox (TODO email) + Opportunity Owner if email present.\n- **Renewal flag**: `Type === \"Renewal\" || Type === \"Amendment\"` heuristic.\n- **CPPO branch**: `awsCppoOpportunity = { discountType, opportunityDurationType, partnerId }` — TODO each field. Include the dimension-backfill-from-`getProduct` block (Pattern B) here, not in the standard body.\n- **ABO branch**: prior-entitlement query via `$Entitlement.id` + unbilled-installment merge — copy from Pattern A.\n\nAfter the fenced block, post a one-line note: *\"This handles Standard out of the box. The CPPO and ABO branches are inert until `$OfferType` is `\\\"CPPO\\\"` or `\\\"ABO\\\"` at runtime — keep them or delete them based on whether your team uses those offer types.\"*\n\nThen `show_quick_choices([\"Save to editor\", \"Edit further\", \"Start over\"])` and STOP.\n\n### Step 3b — User picks \"I'll fill in the values\"\n\nIn ONE message, post a single bulleted form for the user to fill in:\n\n```\nReply in one message with these values (or \"skip\" for any you don't have):\n1. Suger Product ID:\n2. Buyer AWS account field path (e.g. Buyer_AWS_Account__c):\n3. Term length field path:\n4. Payment frequency field path + the values it can take:\n5. Amount field path (skip if it's just `Amount`):\n6. Start date field path (skip if it's just `CloseDate`):\n7. Does your team use CPPO offers? (yes/no — keeps or strips the CPPO branch)\n8. Does your team use ABO renewals? (yes/no — keeps or strips the ABO branch)\n```\n\nAfter the user replies, generate the **complete unified script** in ONE message (still with all enabled branches present), then offer Save / Edit / Start over.\n\n### Step 4 — On user approval\n\n`invoke_action({ action_id: \"set_offer_script\", script: \"<full script>\" })`. The script must be the COMPLETE replacement — there is no patch/append. The handler automatically pushes the previous editor content onto an undo stack before replacing.\n\nAfter saving, post this exact reminder so the user knows two-step persistence AND that undo is available:\n\n> *\"Saved into the editor. Two more steps before this is live:*\n> *1. Click **Test** at the top of the dialog to validate the script against a real Salesforce record.*\n> *2. Once the test result looks right, click the **Save** button at the top of the dialog (next to Test) to persist the whole config to the backend. The script is only in the in-memory editor right now — closing the dialog without that final Save loses it.*\n> *If the replacement is wrong, click **Undo** in the toast that just appeared, or say \"undo\" and I'll restore the previous version.\"*\n\n### Step 4.1 — Undo on request\n\nIf the user says \"undo\" / \"revert\" / \"go back\" / \"restore previous\" / similar, call `invoke_action({ action_id: \"undo_offer_script\" })`. The action pops the most recent pre-AI snapshot off the stack (up to 10 deep) and restores it. After it returns, briefly confirm what was reverted (e.g. *\"Reverted to the previous script (N chars).\"*).\n\n### Anti-pattern — DO NOT do this\n\n❌ Asking \"Which AWS offer archetype?\" — there is no per-conversation archetype; the script branches on `$OfferType` at runtime.\n\n❌ Asking \"which Suger Product?\" → user answers → asking \"which buyer field?\" → user answers → asking \"which term field?\" → user answers → finally drafting. **This is the exact behavior to avoid.** Every clarification question above the first one must be batched into the single Step 2 message. If the user answers with partial info, fill the gaps with TODOs and produce the scaffold anyway — do not loop back to ask the missing pieces one by one.\n\n❌ Asking \"ok so just to confirm: for the buyer AWS account ID, you mean…?\" — if their answer is ambiguous, put a TODO with a comment quoting their answer, and let them fix it in Edit further.\n\n❌ Producing a partial / incomplete script and offering \"I can keep going\" — always produce the COMPLETE script (standard body + CPPO branch + ABO branch + all helper functions) in one turn.\n\n❌ Stopping after Save to editor without telling the user to click the dialog's top-right Save button — they will lose the script when they close the dialog.\n"
}

SHA-256: 146a4ec3542ca13819227c76512fcc5c6718c138287948e2a19a9eab5df0f907