← SugerCONTENT HISTORY

Update to Suger

Snapshot Sep 30, 2026 · 23:00 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-azure",
  "description": "Generate the JavaScript script that builds an Azure Marketplace private offer (Standard or CPPO) from a Salesforce Opportunity / Quote / custom record. Covers Standard and CPPO archetypes in one script — branch on $OfferType inside.",
  "included_files": [],
  "skill_md_contents": "---\nname: offer-mapping-azure\ndescription: \"Generate the JavaScript script that builds an Azure Marketplace private offer (Standard or CPPO) from a Salesforce Opportunity / Quote / custom record. Covers Standard and CPPO archetypes in one script — branch on $OfferType inside.\"\n---\n\n# Generate Azure Private Offer Mapping Script\n\nYou help the user write a JavaScript script that runs at offer-creation time and **builds the Azure Marketplace private offer body from a Salesforce source record**. The script handles both Azure archetypes — Standard and CPPO — by branching on `$OfferType`. (Azure does not have a separate \"ABO / amendment\" offer type — renewals are signalled with `customerContractRenewal: true` on a Standard-shaped offer body.)\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, building the `azurePrivateOffer` body, computing dates / pricing / payment schedule, attaching notification contacts, and applying CPPO-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 — top-level statements, mutate $target\nconst record = $query(\"SELECT ... FROM Quote WHERE Id = '\" + $source.Id + \"' LIMIT 1\");\n$target.name = record.Account.Name + \"_offer\";\n$target.info = $target.info || {};\n$target.info.azurePrivateOffer = $target.info.azurePrivateOffer || {};\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  return o;                     // return is ignored\n}\n```\n\n### Available globals (cloud-agnostic)\n\n| Global | What it is |\n|---|---|\n| `$source` | The SFDC record. Has `.Id` plus the fields the dialog's Source Object Type returned |\n| `$target` | The offer being built. Mutate `$target.*` in place |\n| `$OfferType` | `\"Standard\"` or `\"CPPO\"` for Azure. Branch on this for archetype-specific logic |\n| `$Product` | The Suger Product picked in the dialog (optional) |\n| `$query(soql)` | Runs SOQL via the Salesforce API; returns one record (or null) |\n| `$createContact({name, emailAddress})` | Creates a Suger contact, returns `{ id, ... }` |\n| `$marketplaceApi.getOAuth2Token({clientId})` | OAuth2 token for an integration registered in this org |\n| `$marketplaceApi.oauth2Request({token, method, url, body})` | External HTTPS API call |\n| `$marketplaceApi.getProduct(productId)` | Fetches a Suger Product by ID |\n| `$marketplaceApi.downloadFile(url)` | Downloads a file (rare in offer mapping) |\n\nStandard JS globals: `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\nSame as AWS — pick based on how the customer's pricing data lives in Salesforce. Quote-based source is preferred when the customer is on Salesforce CPQ or a CPQ-integrated platform; Opportunity-based when there's no CPQ. For Quote sources, **always include the primary-quote filter** (`IsSyncing = true`, `Status = \"Approved\"`, or a custom `Primary__c`) in the SOQL `WHERE` clause — picking a non-primary quote silently produces wrong offer numbers.\n\nIf the dialog's source is `Opportunity` but the customer's data lives on `Quote`, **do not silently switch** — stop and ask the user to change the dialog's \"Source Object Type\" first.\n\n---\n\n## Output shape — Azure-specific\n\nThe most important field on `$target` for Azure is `$target.info.azurePrivateOffer`, which mirrors the Microsoft Marketplace Private Offer schema. Common paths:\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 pre-set by a filler |\n| `$target.expireTime` | Date / ISO string / `YYYY-MM-DD`. Future-dated. Mirror to `acceptBy` below |\n| `$target.contactIds` | Array of Suger contact IDs from `$createContact({...}).id` |\n| `$target.metaInfo.isRenewalOffer` | Boolean (true for renewals/amendments) |\n| `$target.metaInfo.renewalOfferType` | `\"AzureMarketplace\"` when isRenewalOffer is true |\n| `$target.info.eulaType` | `\"ISV\"` (default) or `\"CUSTOM\"` |\n| `$target.info.azurePrivateOffer.name` | Customer-facing Azure offer name |\n| `$target.info.azurePrivateOffer.preparedBy` | Email of the user creating the offer |\n| `$target.info.azurePrivateOffer.privateOfferType` | One of: `\"customerPromotion\"` (Standard), `\"multipartyPromotionOriginator\"` (CPPO out), `\"multipartyPromotionChannelPartner\"` (CPPO in), `\"cspPromotion\"` (CSP). Drives the CPPO/Standard distinction at the Azure API level |\n| `$target.info.azurePrivateOffer.start` | `YYYY-MM-DD`, **must be the first day of a month**. Empty if `variableStartDate: true` |\n| `$target.info.azurePrivateOffer.end` | `YYYY-MM-DD`, **must be the last day of a month** |\n| `$target.info.azurePrivateOffer.acceptBy` | `YYYY-MM-DD`, future-dated. Same as `$target.expireTime` |\n| `$target.info.azurePrivateOffer.variableStartDate` | Boolean — `true` means start on acceptance, `start` field empty |\n| `$target.info.azurePrivateOffer.notificationContacts` | Array of plain email strings (NOT contactId — Azure uses raw emails) |\n| `$target.info.azurePrivateOffer.beneficiaries` | Array of `{ id, type, recipients }` describing who can accept the offer (e.g. `{ id: \"<tenantId>\", type: \"tenant\" }` for an Azure AD tenant) |\n| `$target.info.azurePrivateOffer.partners` | Array of channel partner records — REQUIRED for CPPO archetypes |\n| `$target.info.azurePrivateOffer.pricing` | Array (max 10) of pricing entries. Each references a product/plan + customized prices |\n| `$target.info.azurePrivateOffer.termsAndConditionsDocs` | Array of `{ fileName, customerFacingDocumentName, sasUrl }` — used in multiparty CPPO |\n| `$target.info.azurePrivateOffer.termsAndConditionsDocSasUrl` | Single SAS URL — used in `customerPromotion` and `cspPromotion` |\n| `$target.info.azurePrivateOffer.customerContractRenewal` | Boolean — true when the offer is a renewal of an existing contract (Azure has no separate ABO / amendment offer type) |\n| `$target.info.paymentSchedule` | `\"PREPAY\"` / `\"POSTPAY\"` |\n\n### Azure-specific gotchas\n\n- **Date format**: Azure uses **date-only `YYYY-MM-DD`**, NOT date-time. `start` must be a month's first day (`2026-06-01`); `end` must be a month's last day (`2027-05-31`). Do NOT pass ISO date-time strings — Azure's backend will reject them.\n- **Pricing array cap**: `pricing[]` accepts at most 10 entries. If the customer's quote has more line items, you must aggregate — ask the user how.\n- **Notification contacts**: Azure takes plain email strings on `azurePrivateOffer.notificationContacts`. The cross-cloud `$target.contactIds` (Suger contact IDs) is separate and used for Suger's own UX.\n- **Renewals**: Azure has NO separate \"ABO\" offer type. To mark a renewal, set `azurePrivateOffer.customerContractRenewal = true` AND `metaInfo.isRenewalOffer = true`.\n\n---\n\n## Archetype branches\n\n### Standard (`$OfferType === \"Standard\"` or undefined)\n\nSingle-customer or single-tenant offer.\n\n```js\n$target.info.azurePrivateOffer.privateOfferType = \"customerPromotion\";\n// beneficiaries: tenant ID(s) of the buyer\n$target.info.azurePrivateOffer.beneficiaries = [\n  { id: \"<TODO buyer tenant id>\", type: \"tenant\" }\n];\n// no partners[] for Standard\n```\n\n### CPPO (`$OfferType === \"CPPO\"`)\n\nChannel partner offer — Microsoft's \"multiparty promotion\" or \"CSP\" model.\n\n```js\n$target.info.azurePrivateOffer.privateOfferType = \"multipartyPromotionOriginator\";\n// or \"multipartyPromotionChannelPartner\" / \"cspPromotion\" depending on perspective\n$target.info.azurePrivateOffer.partners = [\n  {\n    name: \"<TODO partner display name>\",\n    accountId: \"<TODO partner account id>\",\n    role: \"<TODO role: ChannelPartner / CSP / etc.>\"\n  }\n];\n$target.info.azurePrivateOffer.beneficiaries = [\n  { id: \"<TODO end-customer tenant id>\", type: \"tenant\" }\n];\n// CPPO often uses termsAndConditionsDocs[] (multiple) instead of single termsAndConditionsDocSasUrl\n$target.info.azurePrivateOffer.termsAndConditionsDocs = [\n  // { fileName, customerFacingDocumentName, sasUrl }\n];\n```\n\n### No ABO branch — Azure renewals are flagged, not branched\n\n```js\n// In your standard body, when the SFDC record indicates a renewal:\nif (record?.Type === \"Renewal\" || record?.Type === \"Amendment\") {\n  $target.metaInfo = $target.metaInfo || {};\n  $target.metaInfo.isRenewalOffer = true;\n  $target.metaInfo.renewalOfferType = \"AzureMarketplace\";\n  $target.info.azurePrivateOffer.customerContractRenewal = true;\n}\n```\n\n---\n\n## Common patterns\n\n### 1. Query the source record\n\n```js\nconst record = $query(\n  \"SELECT Id, Name, Account.Name, Account.Tenant_Id__c, \" +\n  \"Owner.Email, ExpirationDate, TotalPrice, Subscription_Term__c, \" +\n  \"Type \" +\n  \"FROM Quote WHERE Id = '\" + $source.Id + \"' AND IsSyncing = true LIMIT 1\"\n);\nif (!record) {\n  throw new Error(\"Primary syncing Quote not found for \" + $source.Id);\n}\n```\n\n### 2. Notification contacts (Azure-style — plain email strings)\n\n```js\nconst emails = [];\nif (record?.Owner?.Email) emails.push(record.Owner.Email);\nemails.push(\"<TODO ops alias email>\");\n$target.info.azurePrivateOffer.notificationContacts = emails;\n\n// Cross-cloud Suger contact IDs (separate from Azure's notificationContacts):\nconst contactIds = [];\nconst opsContact = $createContact({\n  name: \"Marketplace Operations\",\n  emailAddress: \"<TODO ops alias email>\",\n});\ncontactIds.push(opsContact.id);\n$target.contactIds = contactIds;\n```\n\n### 3. Date helpers (Azure month-boundary requirement)\n\n```js\nfunction firstOfMonth(date) {\n  const d = new Date(date);\n  d.setDate(1);\n  return d.toISOString().slice(0, 10);  // YYYY-MM-DD\n}\nfunction lastOfMonth(date) {\n  const d = new Date(date);\n  d.setMonth(d.getMonth() + 1, 0);  // day 0 of next month = last day of this month\n  return d.toISOString().slice(0, 10);\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```\n\n### 4. Start / End / AcceptBy\n\n```js\nconst startDate = parseDate(record.Subscription_Start_Date__c);\nconst termMonths = Number(record.Subscription_Term__c || 0);\n\nif (startDate && startDate > new Date()) {\n  // Future start — explicit start/end\n  $target.info.azurePrivateOffer.start = firstOfMonth(startDate);\n  if (termMonths) {\n    const endDate = addMonths(startDate, termMonths);\n    endDate.setDate(endDate.getDate() - 1); // exclusive end\n    $target.info.azurePrivateOffer.end = lastOfMonth(endDate);\n  }\n  $target.info.azurePrivateOffer.variableStartDate = false;\n} else {\n  // Acceptance start\n  $target.info.azurePrivateOffer.start = \"\";\n  $target.info.azurePrivateOffer.variableStartDate = true;\n  // end still required — calculate from termMonths starting from \"now + safe lead time\"\n  if (termMonths) {\n    const projectedStart = new Date();\n    projectedStart.setMonth(projectedStart.getMonth() + 1, 1); // start of next month\n    const projectedEnd = addMonths(projectedStart, termMonths);\n    projectedEnd.setDate(projectedEnd.getDate() - 1);\n    $target.info.azurePrivateOffer.end = lastOfMonth(projectedEnd);\n  }\n}\n\n// AcceptBy = today + 14 days, or use a CRM-driven expiration if present\nconst today = new Date();\nconst acceptBy = new Date(today);\nacceptBy.setDate(today.getDate() + 14);\nconst acceptByYmd = acceptBy.toISOString().slice(0, 10);\n$target.info.azurePrivateOffer.acceptBy = acceptByYmd;\n$target.expireTime = acceptByYmd;\n```\n\n### 5. Pricing entries (NEVER invent the productId / planId)\n\n```js\n// Pricing array: each entry references a real Azure product + plan from the Suger Product config.\n// NEVER hard-code productId / planId — they must come from the Suger Product or the user.\n$target.info.azurePrivateOffer.pricing = [];\n// Example shape (placeholder — customise once user confirms):\n// $target.info.azurePrivateOffer.pricing = [\n//   {\n//     product: \"<TODO Azure productId from Suger Product>\",\n//     plan: \"<TODO Azure planId>\",\n//     pricingPolicies: [\n//       { policyType: \"absoluteDiscount\", value: \"10\" /* % off list */ }\n//     ]\n//   }\n// ];\n```\n\n### 6. EULA / terms-and-conditions\n\n```js\n// Default — ISV terms\n$target.info.eulaType = \"ISV\";\n\n// CUSTOM EULA — single doc (used in customerPromotion / cspPromotion)\n// $target.info.eulaType = \"CUSTOM\";\n// $target.info.azurePrivateOffer.termsAndConditionsDocSasUrl = \"<TODO SAS URL>\";\n\n// Multiparty CPPO — array of docs\n// $target.info.azurePrivateOffer.termsAndConditionsDocs = [\n//   { fileName: \"...\", customerFacingDocumentName: \"...\", sasUrl: \"...\" }\n// ];\n```\n\n---\n\n## Hard constraints — never violate\n\n- **Dates**: `start`, `end`, `acceptBy` MUST be `YYYY-MM-DD` strings. `start` MUST be the first day of a month; `end` MUST be the last day of a month.\n- **`pricing[]`**: max 10 entries. Aggregate when the source has more line items.\n- **`privateOfferType`**: must be one of `customerPromotion`, `cspPromotion`, `multipartyPromotionOriginator`, `multipartyPromotionChannelPartner`. NEVER make up a new value.\n- **`beneficiaries[]`**: required. At least one entry with a real Azure tenant ID for the buyer.\n- **CPPO**: `partners[]` REQUIRED, must include at least one partner record.\n- **Renewals**: set both `metaInfo.isRenewalOffer = true` AND `azurePrivateOffer.customerContractRenewal = true`.\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- ❌ Pass ISO date-time strings to Azure date fields — Azure requires plain `YYYY-MM-DD`.\n- ❌ Set `start` to a non-first-day-of-month date or `end` to a non-last-day-of-month date.\n- ❌ Hard-code Azure tenant IDs, product IDs, plan IDs, or partner account IDs as placeholders. Always use `<TODO ...>` until the user confirms.\n\n### ⚠️ Cross-cloud field contamination — the silent killer\n\nAzure has its own offer-body shape under `$target.info.azurePrivateOffer.*`. The Azure offer-creation API **silently ignores** any field it doesn't recognise — no error, no warning. So if your script writes AWS-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 Azure script** (they belong to other clouds):\n\n| AWS-only — DO NOT use here | GCP-only — DO NOT use here |\n|---|---|\n| `$target.info.commits` | `$target.info.gcpPrivateOffer.*` |\n| `$target.info.dimensions` | `$target.info.gcpDuration` |\n| `$target.info.awsCppoOpportunity` | `$target.info.gcpCustomerInfo` |\n| `$target.info.buyerAwsAccountIds` | `$target.info.gcpPlans` |\n| `$target.info.attachEulaType` | `$target.info.gcpOfferDealType` |\n| | `$target.info.gcpResellerPrivateOfferPlan` |\n| | `$target.info.gcpSkuDiscounts` |\n| | `$target.info.gcpUsagePlanPriceModel` |\n| | `$target.info.gcpProviderInfo` |\n\nFor Azure, **everything offer-specific lives under `$target.info.azurePrivateOffer.*`** — see the \"Output shape — Azure-specific\" table above for the full list. Cross-cloud bleed is the single most common bug when adapting an AWS script to Azure; if you're translating a customer's existing AWS script, **delete every line that touches `info.commits` / `info.dimensions` / `info.awsCppoOpportunity` and rebuild from a Pattern A–D scaffold.**\n\n### ⚠️ The `crmFields` form value is NOT a list of SOQL-queryable fields\n\nSame warning as the AWS skill — `crmFields` may include virtual filler aliases like `_PrimaryContactEmail`, `_Contact_PrimaryOrFirst`, `_Contact_Decision_Maker`. These are framework-level placeholders resolved at filler-execution time via Go templates and **do not exist in the Salesforce database**. NEVER include `_`-prefixed names in SOQL.\n\n### ⚠️ SOQL injection — interpolating user-modifiable values\n\n`$source.Id` is the safe Salesforce Id format. Any other value taken from a Salesforce record — `Name`, custom-text fields like `ListKey`, `PlanKey`, customer-typed fields — may contain a single quote and break the SOQL string. Validate the charset (`/^[A-Za-z0-9_-]+$/`) before interpolation, or escape single quotes with `replace(/\\\\\\\\/g, \"\\\\\\\\\\\\\\\\\").replace(/'/g, \"\\\\\\\\'\")`. Never interpolate free-form text directly into a SOQL `WHERE` clause.\n\n### ⚠️ Never invent Azure productId / planId / tenant id values\n\nAzure offer validation rejects unknown product IDs / plan IDs immediately. If the user hasn't confirmed them, leave a `// TODO: per intake / per user — Azure productId for the X plan` comment.\n\n### ⚠️ Don't gate `paymentSchedule` on a future-dated startDate\n\nA common mistake from AWS porting: setting `paymentSchedule = \"PREPAY\"` only when `start > now`. Azure offers can be PREPAY/POSTPAY independent of date. Read it from a CRM field or ask the user.\n\n---\n\n## Worked example patterns\n\nThe four anonymized patterns below are **distilled from real production Azure scripts** across multiple customers. Customer names, emails, OAuth client IDs, Azure product/plan UUIDs, and Suger product IDs have been replaced with placeholders — the SHAPES (control flow, SOQL, Azure pricing structure, 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, multi-year flexible billing schedule, renewal/amendment branches\n\nUsed when the customer is on Salesforce CPQ with a multi-year contract and per-month flexible installments (`flexibleSchedule.billingSchedule[]`). Handles renewal flag (`customerContractRenewal`) and amendment end-date override.\n\n```js\nfunction addMonths(date, monthsToAdd) {\n  const newDate = new Date(date);\n  const originalDay = newDate.getDate();\n  newDate.setMonth(newDate.getMonth() + monthsToAdd);\n  if (newDate.getDate() < originalDay) newDate.setDate(0);\n  return newDate;\n}\nfunction roundToTwoDecimalPlaces(num) { return Math.round(num * 100) / 100; }\nfunction parseDate(s) {\n  if (!s) return null;\n  const d = new Date(s);\n  return isNaN(d.getTime()) ? null : d;\n}\n\nconst record = $query(\n  \"SELECT SBQQ__NetAmount__c, SBQQ__StartDate__c, Payment_Frequency__c, \" +\n  \"SBQQ__SubscriptionTerm__c, Name, Amendment_End_Date__c, \" +\n  \"SBQQ__Opportunity2__r.Term__c, SBQQ__Opportunity2__r.Start_Date__c, \" +\n  \"SBQQ__Opportunity2__r.Type, SBQQ__Opportunity2__r.Owner.Name, \" +\n  \"SBQQ__Opportunity2__r.Owner.Email \" +\n  \"FROM SBQQ__Quote__c WHERE Id = '\" + $source.Id + \"' LIMIT 1\"\n);\nconst opp = record?.SBQQ__Opportunity2__r;\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  contactIds.push($createContact({ name: opp.Owner.Name, emailAddress: opp.Owner.Email }).id);\n}\n$target.contactIds = contactIds;\n\n// Start time — null out and switch to variableStartDate when the SFDC start is in the past\nconst oppStartDate = parseDate(opp.Start_Date__c);\nif ($target.info.startTime) {\n  if (oppStartDate < new Date()) {\n    $target.info.azurePrivateOffer.start = null;\n    $target.info.azurePrivateOffer.variableStartDate = true;\n  } else {\n    $target.info.azurePrivateOffer.start = oppStartDate.toISOString().slice(0, 10);\n  }\n}\n\n// End time — last day of (start + termMonths). Amendment uses an explicit override field.\nlet oppTermMonths = 12;\nif (opp.Term__c) oppTermMonths = Number(opp.Term__c);\nif (oppStartDate) {\n  const lastDayOfMonth = new Date(Date.UTC(\n    oppStartDate.getUTCFullYear(),\n    oppStartDate.getUTCMonth() + oppTermMonths,\n    0\n  ));\n  $target.info.azurePrivateOffer.end = lastDayOfMonth.toISOString().slice(0, 10);\n  $target.endTime = lastDayOfMonth.toISOString();\n}\nif (opp.Type === \"Amendment\") {\n  const amendmentEndDate = parseDate(record.Amendment_End_Date__c);\n  $target.info.azurePrivateOffer.end = amendmentEndDate?.toISOString()?.slice(0, 10);\n}\n\n$target.info.eulaType = \"SCMP\";\n\n// Renewal flag (Azure has no separate ABO archetype — flag it on the body)\nif (opp.Type === \"Renewal\") {\n  $target.info.azurePrivateOffer.customerContractRenewal = true;\n} else {\n  $target.info.azurePrivateOffer.customerContractRenewal = false;\n}\n\n$target.info.azurePrivateOffer.offerPricingType = \"newCustomizedPlans\";\n\n// Build flexible billing schedule from term + payment frequency\nconst term = record.SBQQ__SubscriptionTerm__c;\nconst paymentFreq = record.Payment_Frequency__c;\nconst amount = record.SBQQ__NetAmount__c;\nconst quoteStartDate = parseDate(record.SBQQ__StartDate__c);\n\nlet contractDuration = 1;\nif (term) {\n  const ContractYears = term / 12;\n  if (ContractYears > 3) contractDuration = 3;\n  else {\n    const roundedDown = Math.floor(ContractYears);\n    if (roundedDown) contractDuration = roundedDown;\n  }\n}\n\nconst instalments = [];\nif (quoteStartDate && term && paymentFreq) {\n  let numOfMonthlyInstalments = term + 1;\n  let numOfInstalments = 0;\n  if (paymentFreq === \"Monthly Fixed Amount\") numOfInstalments = numOfMonthlyInstalments;\n  if (paymentFreq === \"Quarterly\")            numOfInstalments = Math.ceil(numOfMonthlyInstalments / 4);\n  if (paymentFreq === \"Semi Annual\")          numOfInstalments = Math.ceil(numOfMonthlyInstalments / 6);\n\n  let chargeStartDate = quoteStartDate;\n  if (quoteStartDate <= new Date()) chargeStartDate = new Date();\n\n  if (amount > 0 && numOfInstalments > 0) {\n    const amountPerMonth = roundToTwoDecimalPlaces(amount / numOfInstalments);\n    let totalAmount = 0;\n    let curChargeDate = chargeStartDate;\n    for (let i = 0; i < numOfInstalments; i++) {\n      instalments.push({ pricePerPaymentInUsd: amountPerMonth, chargeDate: curChargeDate.toISOString() });\n      curChargeDate = addMonths(chargeStartDate, i + 1);\n      totalAmount += amountPerMonth;\n    }\n    // Round-drift adjustment on last installment\n    const diffInAmt = roundToTwoDecimalPlaces(amount - totalAmount);\n    instalments[numOfInstalments - 1].amount = roundToTwoDecimalPlaces(\n      instalments[numOfInstalments - 1].amount + diffInAmt\n    );\n  }\n}\n\n$target.info.azurePrivateOffer.pricing = [{\n  plan: \"<TODO Azure planId — plan/<productGuid>/<planGuid>>\",\n  product: \"<TODO Azure productId — product/<productGuid>>\",\n  planName: \"<TODO product+plan display name>\",\n  discountType: \"absolute\",\n  privateOfferPlan: {\n    $schema: \"https://schema.mp.microsoft.com/schema/price-and-availability-private-offer-plan/2025-05-01\",\n    pricing: {\n      recurrentPrice: {\n        priceInputOption: \"usd\",\n        prices: [{\n          billingFrequency: { type: \"flexible\", value: 1 },\n          contractDuration: { type: \"year\", value: contractDuration },\n          flexibleSchedule: {\n            initialCharge: { pricePerPaymentInUsd: 0 },\n            billingSchedule: instalments,\n          },\n        }],\n      },\n    },\n    plan: \"<TODO Azure planId>\",\n    product: \"<TODO Azure productId>\",\n  },\n}];\n```\n\n### Pattern B — Standard Quote source, custom EULA, per-product Azure plan lookup table\n\nSimpler shape: `Quote` object as source, single bundled commit covering the whole offer, custom EULA attached as PDF, per-product plan lookup. Useful template for customers with a Master Customer Agreement.\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, Subscription_Term__c, \" +\n  \"Opportunity.RecordType.Name, \" +\n  \"Opportunity.Owner.Name, Opportunity.Owner.Email, \" +\n  \"Opportunity.Account.Owner.Name, Opportunity.Account.Owner.Email \" +\n  \"FROM Quote WHERE Id = '\" + $source.Id + \"' \" +\n  \"AND IsSyncing = true 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\n// Notification contacts\nconst contactIds = [];\ncontactIds.push($createContact({\n  name: \"Marketplace Order\",\n  emailAddress: \"<TODO ops alias email>\",\n}).id);\nif (quoteData?.Opportunity?.Owner?.Email) {\n  contactIds.push($createContact({\n    name: quoteData.Opportunity.Owner.Name,\n    emailAddress: quoteData.Opportunity.Owner.Email,\n  }).id);\n}\nif (quoteData?.Opportunity?.Account?.Owner?.Email) {\n  contactIds.push($createContact({\n    name: quoteData.Opportunity.Account.Owner.Name,\n    emailAddress: quoteData.Opportunity.Account.Owner.Email,\n  }).id);\n}\n$target.contactIds = contactIds;\n\n$target.info = {};\n\n// Expiration + end date\nconst today = new Date();\nconst expireDate = new Date();\nexpireDate.setDate(today.getDate() + 14);\n$target.expireTime = expireDate;\n\n$target.info.azurePrivateOffer = {};\n$target.info.azurePrivateOffer.variableStartDate = true;\n$target.info.azurePrivateOffer.preparedBy = \"<TODO ops alias email>\";\nif (termLength) {\n  const endDate = new Date();\n  endDate.setMonth(expireDate.getMonth() + termLength);\n  $target.info.azurePrivateOffer.end = endDate.toISOString().split(\"T\")[0];\n}\n\n// Per-product plan lookup\nlet planField = \"\";\nlet productField = \"\";\nlet productName = \"\";\nlet planName = \"\";\nlet productId = $target.productID;\nif (typeof $Product !== \"undefined\" && $Product !== null) productId = $Product.id;\n\nif (productId === \"<EXAMPLE_PRODUCT_ID_A>\") {\n  planField    = \"<TODO Azure planId for product A>\";\n  productField = \"<TODO Azure productId for product A>\";\n  productName  = \"<TODO product A display name>\";\n  planName     = \"<TODO product A plan name>\";\n} else if (productId === \"<EXAMPLE_PRODUCT_ID_B>\") {\n  planField    = \"<TODO Azure planId for product B>\";\n  productField = \"<TODO Azure productId for product B>\";\n  productName  = \"<TODO product B display name>\";\n  planName     = \"<TODO product B plan name>\";\n} else {\n  // Default fallback when product is unknown\n  planField    = \"<TODO Azure planId — default fallback>\";\n  productField = \"<TODO Azure productId — default fallback>\";\n  productName  = \"<TODO default product display name>\";\n  planName     = \"<TODO default plan name>\";\n}\n\nlet billingTermLength = 1;\nif (termLength) billingTermLength = Math.round(termLength / 12);\n\n$target.info.azurePrivateOffer.pricing = [{\n  plan: planField,\n  planName,\n  discountType: \"absolute\",\n  privateOfferPlan: {\n    $schema: \"https://schema.mp.microsoft.com/schema/price-and-availability-private-offer-plan/2022-07-01\",\n    pricing: {\n      recurrentPrice: {\n        priceInputOption: \"usd\",\n        recurrentPriceMode: \"flatRate\",\n        prices: [{\n          pricePerPaymentInUsd: amount,\n          billingTerm: { type: \"year\", value: billingTermLength },\n          paymentOption: { type: \"year\", value: billingTermLength },\n        }],\n      },\n    },\n    plan: planField,\n    product: productField,\n  },\n  product: productField,\n  productName,\n}];\n\nif (recordType === \"Renewal\") {\n  $target.info.azurePrivateOffer.customerContractRenewal = true;\n}\n\n// Custom EULA\n$target.info.eulaUrl = \"<TODO hosted EULA URL — supplied by user / customer config>\";\n$target.info.eulaType = \"CUSTOM\";\n```\n\n### Pattern C — SBQQ__Quote__c source, CPPO with partner accountId, VM software-reservation OR newCustomizedPlans pricing\n\nThe full multi-archetype Azure script. Branches on `$OfferType === \"CPPO\"` for partner accountId; reads product type from `Suger__Product_Type__c` to switch between `vmSoftwareReservations` (per-core sizing) and `newCustomizedPlans` (flat-rate). Also reads CPQ line items for per-core VM pricing.\n\n```js\nfunction simpleCurrentDate() {\n  const date = new Date();\n  const months = [\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\"];\n  return months[date.getMonth()] + date.getFullYear();\n}\nfunction roundToTwoDecimalPlaces(n) { return Math.round(n * 100) / 100; }\nfunction addMonths(d, months) {\n  const date = new Date(d);\n  const day = date.getDate();\n  date.setMonth(date.getMonth() + months);\n  if (date.getDate() < day) date.setDate(0);\n  return date;\n}\n\nif (!$target.info) $target.info = {};\nif (!$target.metaInfo) $target.metaInfo = {};\n\nconst quoteData = $query(\n  \"SELECT SBQQ__Opportunity2__r.Customer_AWS_Account_Number__c, \" +\n  \"SBQQ__Opportunity2__r.Partner_AWS_Account_Number__c, CPQ_Reseller_Total_Amount__c, \" +\n  \"Invoicing_Terms__c, CPQ_Non_Standard_Request__c, \" +\n  \"SBQQ__Opportunity2__r.Account.Name, Name, CPQ_PartnerAccountName__c, \" +\n  \"SBQQ__Opportunity2__r.Account.New_Customer_Status__c, \" +\n  \"SBQQ__Opportunity2__r.Owner.Name, SBQQ__Opportunity2__r.Owner.Email, \" +\n  \"SBQQ__Opportunity2__r.Account.Owner.Name, SBQQ__Opportunity2__r.Account.Owner.Email, \" +\n  \"(SELECT CPQ_Product_Code__c, SBQQ__Quantity__c, CPQ_ResellerPrice__c FROM SBQQ__LineItems__r LIMIT 10) \" +\n  \"FROM SBQQ__Quote__c WHERE Id = '\" + $source.Id + \"' LIMIT 1\"\n);\n\nconst customerAwsAccNumber = quoteData.SBQQ__Opportunity2__r?.Customer_AWS_Account_Number__c || \"\";\nconst partnerAwsAccNumber  = quoteData.SBQQ__Opportunity2__r?.Partner_AWS_Account_Number__c || \"\";\nconst resellerTotalAmt     = quoteData.CPQ_Reseller_Total_Amount__c;\nconst customerName         = quoteData.SBQQ__Opportunity2__r?.Account.Name || \"\";\nconst partnerName          = quoteData.CPQ_PartnerAccountName__c;\nconst customerStatus       = quoteData.SBQQ__Opportunity2__r?.Account.New_Customer_Status__c || \"\";\nconst quoteLines           = quoteData.SBQQ__LineItems__r?.records || [];\nconst oppOwnerName         = quoteData.SBQQ__Opportunity2__r?.Owner?.Name;\nconst oppOwnerEmail        = quoteData.SBQQ__Opportunity2__r?.Owner?.Email;\n\n// Offer name — different shape for CPPO vs Standard\nlet offerName = customerName + \"-Azure-<TODO ISV name>-\" + quoteData.Name + \"-\" + simpleCurrentDate();\nif ($OfferType === \"CPPO\") {\n  offerName = customerName + \"-\" + partnerName + \"-Azure-<TODO ISV name>-\" + quoteData.Name + \"-\" + simpleCurrentDate();\n}\n$target.name = offerName.replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n// Notification contacts\nconst contactIds = [];\ncontactIds.push($createContact({\n  name: \"Cloud Deal Desk\",\n  emailAddress: \"<TODO ops alias email>\",\n}).id);\nif (oppOwnerEmail) contactIds.push($createContact({ name: oppOwnerName, emailAddress: oppOwnerEmail }).id);\n$target.contactIds = contactIds;\n\n$target.info.eulaType = \"CUSTOM\";\n\n$target.info.azurePrivateOffer = {};\n$target.info.azurePrivateOffer.beneficiaries = [{\n  id: customerAwsAccNumber,\n  description: \"Azure customer account number\",\n}];\n$target.info.azurePrivateOffer.variableStartDate = true;\n$target.info.azurePrivateOffer.partners = [{ id: partnerAwsAccNumber }];\n\n// Renewal — derived from Account.New_Customer_Status__c\nif (customerStatus.includes(\"Current Customer\")) {\n  $target.info.azurePrivateOffer.customerContractRenewal = true;\n} else {\n  $target.info.azurePrivateOffer.customerContractRenewal = false;\n}\n\n// Per-product plan + pricing-input-option lookup. Determines whether we build\n// vmSoftwareReservations (per-core) or newCustomizedPlans (flat-rate).\nlet productId = $target.productID;\nif (typeof $Product !== \"undefined\" && $Product !== null) productId = $Product.id;\n\nconst productInfo = $query(\n  \"SELECT Suger__Product_ID__c, Suger__Product_Info__c, Suger__Product_Type__c \" +\n  \"FROM Suger__Product__c WHERE Suger__Product_ID__c = '\" + productId + \"' LIMIT 1\"\n);\n\nlet planField = \"\";\nlet productField = \"\";\nlet productName = \"\";\nlet planName = \"\";\nlet priceInputOption = \"flat\";\n\nif (productId === \"<EXAMPLE_PRODUCT_ID_A>\") {\n  planField    = \"<TODO Azure planId for VM product A>\";\n  productField = \"<TODO Azure productId for VM product A>\";\n  productName  = \"<TODO VM product A display name>\";\n  planName     = \"<TODO VM product A plan name>\";\n  priceInputOption = \"perCoreSize\"; // VM with per-core pricing\n} else if (productId === \"<EXAMPLE_PRODUCT_ID_B>\") {\n  planField    = \"<TODO Azure planId for SaaS product B>\";\n  productField = \"<TODO Azure productId for SaaS product B>\";\n  productName  = \"<TODO SaaS product B display name>\";\n  planName     = \"<TODO SaaS product B plan name>\";\n  // priceInputOption stays \"flat\"\n}\n\n// Build the right pricing structure based on priceInputOption + product type\nconst recurrentPrice = {\n  priceInputOption: \"usd\",\n  recurrentPriceMode: \"flatRate\",\n  prices: [{\n    pricePerPaymentInUsd: resellerTotalAmt,\n    billingFrequency: { type: \"year\", value: 1 },\n    contractDuration: { type: \"year\", value: 1 },\n  }],\n};\n\nconst softwareReservation = {\n  paymentSchedule: { type: \"year\", value: 1 },\n  reservationDuration: { type: \"year\", value: 1 },\n  vmPrices: {},\n};\nif (priceInputOption === \"perCoreSize\") {\n  // Each line item corresponds to a VM core size — extract from CPQ_Product_Code__c suffix\n  for (let i = 0; i < quoteLines.length; i++) {\n    const productCode = quoteLines[i].CPQ_Product_Code__c;\n    let coreNum;\n    if (productCode != null) {\n      const secondItem = productCode.split(\"-\")[1];\n      coreNum = Number(secondItem.slice(2));\n    }\n    const quantity = quoteLines[i].SBQQ__Quantity__c;\n    const unitPricePerPaymentPeriodInUsd = quoteLines[i].CPQ_ResellerPrice__c;\n    if (coreNum === 0) {\n      softwareReservation.vmPrices.sharedcore = { quantity, unitPricePerPaymentPeriodInUsd };\n    } else {\n      softwareReservation.vmPrices[coreNum + \"Core\"] = { quantity, unitPricePerPaymentPeriodInUsd };\n    }\n  }\n} else {\n  softwareReservation.vmPrices.allCores = { quantity: 1, unitPricePerPaymentPeriodInUsd: resellerTotalAmt };\n}\n\n$target.info.azurePrivateOffer.pricing = [{\n  plan: planField,\n  planName,\n  product: productField,\n  productName,\n  discountType: \"absolute\",\n  privateOfferPlan: {\n    $schema: \"https://schema.mp.microsoft.com/schema/price-and-availability-private-offer-plan/2025-05-01\",\n    plan: planField,\n    planName,\n    product: productField,\n  },\n}];\n\nif (productInfo && productInfo.Suger__Product_Type__c === \"VM\") {\n  $target.info.azurePrivateOffer.offerPricingType = \"vmSoftwareReservations\";\n  $target.info.azurePrivateOffer.pricing[0].privateOfferPlan.softwareReservation = softwareReservation;\n  $target.info.azurePrivateOffer.pricing[0].privateOfferPlan.offerPricingType = \"vmSoftwareReservations\";\n} else {\n  $target.info.azurePrivateOffer.offerPricingType = \"newCustomizedPlans\";\n  $target.info.azurePrivateOffer.pricing[0].basePlan = planField;\n  $target.info.azurePrivateOffer.pricing[0].newPlanDetails = {\n    name: planName + \" newCustomizedPlans\",\n    description: \"newCustomizedPlans pricing for \" + planName,\n  };\n  $target.info.azurePrivateOffer.pricing[0].privateOfferPlan.pricing = { recurrentPrice };\n  $target.info.azurePrivateOffer.pricing[0].privateOfferPlan.offerPricingType = \"newCustomizedPlans\";\n}\n```\n\n### Pattern D — External CPQ API + dynamic product/plan lookup by ListKey/PlanKey + custom meters backfill\n\nThe most complex pattern. Calls an external CPQ system (e.g. Oracle CPQ) via OAuth2 for full pricing detail, looks up the matching Suger Product by ListKey and the matching plan within it by PlanKey, builds custom usage meters from BURST/PAYGO/COMMIT line items, and backfills missing meters from the product definition. Includes CPPO partners[] block.\n\n```js\nfunction convertToZuluFormat(ts) {\n  if (!ts) return null;\n  const d = new Date(ts);\n  return isNaN(d) ? null : d.toISOString();\n}\nfunction parseDate(s) {\n  if (!s) return null;\n  const d = new Date(s);\n  return isNaN(d.getTime()) ? null : d;\n}\n\nif (!$target.info) $target.info = {};\nif (!$target.metaInfo) $target.metaInfo = {};\nif (!$target.info.azurePrivateOffer) $target.info.azurePrivateOffer = {};\n\nconst record = $query(\n  \"SELECT External_ID__c, CPQ_Quote_Sync_Timestamp__c, PVR_Status__c, Status, \" +\n  \"Net_Price__c, Quote_Number_CPQ__c, Account.Name, ExpirationDate \" +\n  \"FROM Quote WHERE Id = '\" + $source.Id + \"'\"\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({\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\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// Step 1: ListKey + PlanKey from first QuoteLine\nlet planKey, listKey, serviceDuration, billingFreq;\nif (Array.isArray(quoteData.QuoteLines)) {\n  const firstLine = quoteData.QuoteLines[0];\n  if (firstLine) {\n    listKey         = firstLine.ListKey;\n    planKey         = firstLine.PlanKey;\n    serviceDuration = Number(firstLine.ServiceDuration);\n    billingFreq     = firstLine.BillingFrequency;\n  }\n}\nif (!listKey || !planKey) throw new Error(\"Missing ListKey or PlanKey in CPQ quote lines\");\n// listKey/planKey come from CPQ quote line text fields — validate the\n// charset before interpolating into SOQL (see \"SOQL injection\" guidance\n// near the top of this skill).\nif (!/^[A-Za-z0-9_-]+$/.test(listKey)) {\n  throw new Error('Invalid ListKey: \"' + listKey + '\"');\n}\n\n// Step 2: Resolve Suger Product by external ID\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 + \"'\"\n);\n$target.productID = matchedProduct.Suger__Product_ID__c;\n\n// Build usage meters from BURST/PAYGO/COMMIT line items\nconst transactionTotal = Number(quoteData.TransactionTotal);\nlet contractDurationYears = 1;\nif (serviceDuration === 12)      contractDurationYears = 1;\nelse if (serviceDuration === 24) contractDurationYears = 2;\nelse if (serviceDuration === 36) contractDurationYears = 3;\nelse                              contractDurationYears = Math.ceil(serviceDuration / 12);\nconst contractDuration = { type: \"year\", value: contractDurationYears };\n\nlet billingFrequency = { type: \"year\", value: 1 };\nlet pricePerPayment = 0;\nif (billingFreq.toLowerCase() === \"annual\") {\n  billingFrequency = { type: \"year\", value: 1 };\n  pricePerPayment  = parseFloat((transactionTotal / contractDurationYears).toFixed(2));\n} else if (billingFreq.toLowerCase() === \"monthly\") {\n  billingFrequency = { type: \"month\", value: 1 };\n  pricePerPayment  = parseFloat((transactionTotal / serviceDuration).toFixed(2));\n} else if (billingFreq.toLowerCase() === \"upfront\" || billingFreq.toLowerCase() === \"one-time\") {\n  billingFrequency = { type: \"year\", value: contractDurationYears };\n  pricePerPayment  = parseFloat(transactionTotal.toFixed(2));\n} else if (billingFreq.toLowerCase() === \"paygo\") {\n  billingFrequency = { type: \"year\", value: contractDurationYears };\n  pricePerPayment  = 0;\n}\n\nconst meters = {};\nconst burstDimensions  = quoteData.QuoteLines.filter((i) => i.DimensionType === \"BURST\" || i.DimensionType === \"PAYGO\");\nconst commitDimensions = quoteData.QuoteLines.filter((i) => i.DimensionType === \"COMMIT\");\n\nfor (const ql of burstDimensions) {\n  meters[ql.DimensionKey] = {\n    includedQuantities: [{ contractDuration, isInfinite: false, quantity: 0 }],\n    pricePerPaymentInUsd: parseFloat(ql.UnitNetPrice),\n  };\n}\nfor (const ql of commitDimensions) {\n  const listPrice = parseFloat(ql.ListPrice);\n  const quantity = listPrice > 0 ? parseInt(ql.priceQuantity) : 0;\n  const isInfinite = quantity > 10000;\n  if (!meters[ql.DimensionKey]) {\n    meters[ql.DimensionKey] = {\n      includedQuantities: [{ contractDuration, isInfinite, quantity }],\n      pricePerPaymentInUsd: parseFloat(ql.UnitNetPrice),\n    };\n  } else {\n    meters[ql.DimensionKey].includedQuantities = [{ contractDuration, isInfinite, quantity }];\n  }\n}\n\n// Step 3: Resolve plan within product, backfill missing meters from plan definition\nlet planField, productField, planName, productName, planType;\nconst fullProduct = $marketplaceApi.getProduct(matchedProduct.Suger__Product_ID__c);\nif (!fullProduct?.info) {\n  throw new Error(\"Failed to get full product info for product ID \" + matchedProduct.Suger__Product_ID__c);\n}\n\nconst plans = fullProduct.info.azureProductResource?.plans || [];\nconst selectedPlan = plans.find((p) => p.plan?.identity?.externalId === planKey);\nif (!selectedPlan) {\n  const availableKeys = plans.map((p) => p.plan?.identity?.externalId).filter(Boolean);\n  throw new Error('No plan matching PlanKey \"' + planKey + '\". Available: [' + availableKeys.join(\", \") + \"]\");\n}\nplanField    = selectedPlan.plan.id;\nproductField = selectedPlan.plan.product;\nplanName     = selectedPlan.planListing?.name || selectedPlan.plan?.alias || planKey;\nproductName  = fullProduct.info.azureProductResource?.listing?.title || matchedProduct.Name;\nplanType     = fullProduct.productType;\n\n// Backfill meters that exist in plan but weren't in CPQ QuoteLines\nconst existingMeters = selectedPlan.priceAndAvailabilityPlan?.pricing?.customMeters?.meters || {};\nfor (const k of Object.keys(existingMeters)) {\n  if (!meters[k]) {\n    const orig = existingMeters[k];\n    meters[k] = {\n      includedQuantities: [{ contractDuration, isInfinite: false, quantity: 0 }],\n      pricePerPaymentInUsd: orig?.pricePerPaymentInUsd ?? orig?.price ?? 0,\n    };\n  }\n}\n\n// Fall back to flexible billing if the plan doesn't support the requested frequency\nconst recurrentPrices = selectedPlan.priceAndAvailabilityPlan?.pricing?.recurrentPrice?.prices || [];\nconst billingTypes = recurrentPrices.map((p) => p?.paymentOption?.type);\nif (!billingTypes.includes(billingFrequency.type)) {\n  billingFrequency = { type: \"flexible\", value: 1 };\n}\n\n// Build the v2 newCustomizedPlans pricing body\n$target.info.azurePrivateOffer.pricing = [{\n  basePlan: planField,\n  planName,\n  product: productField,\n  productName,\n  planType,\n  discountType: \"absolute\",\n  newPlanDetails: {\n    name: planName + \" newCustomizedPlans\",\n    description: \"newCustomizedPlans pricing for \" + planName,\n  },\n  privateOfferPlan: {\n    $schema: \"https://schema.mp.microsoft.com/schema/price-and-availability-private-offer-plan/2025-05-01\",\n    plan: planField,\n    planName,\n    product: productField,\n    pricing: {\n      customMeters: { priceInputOption: \"usd\", meters },\n      recurrentPrice: {\n        priceInputOption: \"usd\",\n        recurrentPriceMode: \"flatRate\",\n        prices: [{ pricePerPaymentInUsd: pricePerPayment, billingFrequency, contractDuration }],\n      },\n    },\n  },\n}];\n$target.info.azurePrivateOffer.offerPricingType = \"newCustomizedPlans\";\n$target.metaInfo.isRenewalOffer = (quoteData.RenewalFlag === \"true\");\n$target.info.eulaType = \"SCMP\";\n\n// Customer billing account ID\n$target.info.azurePrivateOffer.beneficiaries = [{\n  id: quoteData.HyperScalarCustomerId,\n  description: \"Azure customer account number\",\n}];\n\n// Notification contacts\nconst contactIds = [];\ncontactIds.push($createContact({\n  name: \"Marketplace Operations\",\n  emailAddress: \"<TODO ops alias email>\",\n}).id);\n$target.contactIds = contactIds;\n\n// CPPO branch — add partners[]\nif ($OfferType === \"CPPO\") {\n  $target.info.azurePrivateOffer.partners = [{ id: quoteData.HyperScalarPartnerId }];\n}\n```\n\n---\n\n## CPQ Intake Form ingestion\n\nWhen the user pastes a CPQ Intake Form (numbered fields like `1a.`, `4a.`, `5a.`, `7a.`, `11a.`), parse the field paths directly from it and **skip the \"list unknowns\" step**. Use the AWS skill's intake mapping table — most rows apply unchanged. Azure-specific differences:\n\n- **`7a. SKU/Dimension strategy`** maps to `azurePrivateOffer.pricing[]` shape — single bundled = one pricing entry; granular = multiple entries (cap 10).\n- **`5b. Default billing cadence`** maps to `paymentSchedule` (`PREPAY` for upfront, `POSTPAY` for invoice-based).\n- **`4g. Expiration logic`** maps to `azurePrivateOffer.acceptBy` — same `YYYY-MM-DD` format.\n- **`11a. Active AWS agreement warning`** is irrelevant for Azure; ignore.\n\n---\n\n## Workflow — one unified script, scaffold-first, never drip-feed questions\n\nSame workflow as the AWS skill. Two anti-patterns are explicitly forbidden:\n1. Asking which archetype (Standard / CPPO) up front — write ONE script that branches on `$OfferType`.\n2. Drip-feeding clarifying questions one at a time — batch all unknowns into a single message.\n\n### Step 1 — Read state once\nCall `get_form_values({ formId })`. If `sourceObject` is unfamiliar, also call `query_sfdc_object_schema({ objectName })`.\n\n### Step 2 — List unknowns + offer two paths in a SINGLE message, then STOP\nTypical Azure-specific unknowns:\n- Suger Product the offer is for (drives `pricing[].product` / `plan`)\n- Azure tenant ID for the buyer (`beneficiaries[].id`)\n- Channel partner ID(s) if CPPO is used\n- SFDC field for term length in months\n- SFDC field for billing frequency / paymentSchedule\n- SFDC field for total contract amount (if not `Amount` / `TotalPrice`)\n- SFDC field for start date (if not `CloseDate`)\n- Whether the team uses CPPO\n\nEnd with `show_quick_choices([\"Scaffold now with TODOs\", \"I'll fill in the values\"])`.\n\n### Step 3a — User picks \"Scaffold now with TODOs\"\nProduce the COMPLETE unified script in one `\\`\\`\\`js` fenced block with both branches present (standard body + `if ($OfferType === \"CPPO\") { ... }`). Mark every unknown with `// TODO:`. Then `show_quick_choices([\"Save to editor\", \"Edit further\", \"Start over\"])` and STOP.\n\n### Step 3b — User picks \"I'll fill in the values\"\nPost a single bulleted form. After the user replies, produce the complete script in one message.\n\n### Step 4 — On user approval\n`invoke_action({ action_id: \"set_offer_script\", script: \"<full script>\" })`. The handler pushes the previous editor content onto an undo stack before replacing.\n\nAfter saving, post the standard two-step Save reminder:\n> *\"Saved into the editor. Two more steps before this is live: 1) click Test at the top of the dialog; 2) click Save at the top of the dialog to persist. If wrong, click Undo in the toast or say 'undo'.\"*\n\n### Step 4.1 — Undo on request\nIf user says \"undo\" / \"revert\" / \"go back\", call `invoke_action({ action_id: \"undo_offer_script\" })`.\n\n### Anti-patterns — DO NOT do this\n- ❌ Asking \"Which Azure offer archetype?\" — one script handles both via `$OfferType`.\n- ❌ Asking sequential clarifying questions — batch them.\n- ❌ Producing a partial script with \"I can keep going\".\n- ❌ Stopping after Save to editor without telling the user about the dialog's top Save button.\n"
}

SHA-256: 38b7645ce70a756e6dd44fca290cf2de216617581ca700c36baf5c5697b5e6b7