← SugerCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Suger
Snapshot Sep 30, 2026 · 23:00 UTC · version 1.0.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Generate the JavaScript script that builds a GCP Marketplace private offer (Standard, CPPO, or Replacement) from a Salesforce Opportunity / Quote / custom record. Covers all three GCP archetypes in one script — branch on $OfferType inside.",
"included_files": [],
"name": "offer-mapping-gcp",
"skill_md_contents": "---\nname: offer-mapping-gcp\ndescription: \"Generate the JavaScript script that builds a GCP Marketplace private offer (Standard, CPPO, or Replacement) from a Salesforce Opportunity / Quote / custom record. Covers all three GCP archetypes in one script — branch on $OfferType inside.\"\n---\n\n# Generate GCP Private Offer Mapping Script\n\nYou help the user write a JavaScript script that runs at offer-creation time and **builds the GCP Marketplace private offer body from a Salesforce source record**. The script handles all three GCP archetypes — Standard, CPPO, Replacement — by branching on `$OfferType` at runtime. (GCP uses \"Replacement\" as the renewal/amendment analog; AWS calls this \"ABO\".)\n\nThe script is the **primary mapping mechanism** for anything beyond simple scalar fields. Per-field \"fillers\" handle direct value mapping. The script is where the real work happens: querying Salesforce, building the `gcpPrivateOffer` body, computing duration / customer info / payment schedule, attaching contacts, and applying CPPO/Replacement-specific blocks.\n\n---\n\n## Runtime model — read this first\n\nSame as the AWS / Azure skills. The script is **NOT a function**, just top-level statements that mutate `$target`. The runtime wraps it as `(() => { <your script> })()`. NEVER write `function parseOfferInput(input) { return o; }` — this wrapper is from a non-existent older API.\n\n```js\n// ✅ Correct shape\nconst record = $query(\"SELECT ... FROM Quote WHERE Id = '\" + $source.Id + \"' LIMIT 1\");\n$target.info = $target.info || {};\n$target.info.gcpPrivateOffer = $target.info.gcpPrivateOffer || {};\n$target.info.gcpDuration = 12;\n```\n\n### Available globals (cloud-agnostic)\n\n| Global | What it is |\n|---|---|\n| `$source` | The SFDC record (`.Id` plus the fields the source-object schema returned) |\n| `$target` | The offer being built. Mutate `$target.*` |\n| `$OfferType` | `\"Standard\"`, `\"CPPO\"`, or `\"Replacement\"` for GCP. Branch on this for archetype-specific logic |\n| `$Product` | Suger Product picked in dialog (optional) |\n| `$Entitlement` | For Replacement offers — has `.id` of the prior entitlement being replaced |\n| `$query(soql)` | Runs SOQL via Salesforce API; returns one record (or null) |\n| `$createContact({name, emailAddress})` | Creates a Suger contact, returns `{ id, ... }` |\n| `$marketplaceApi.{getOAuth2Token, oauth2Request, getProduct, downloadFile}` | Same as other skills |\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/Azure — pick based on how the customer's pricing data lives in Salesforce. For Quote sources, always include the primary-quote filter (`IsSyncing = true`, `Status = \"Approved\"`, custom `Primary__c`) in the SOQL `WHERE` clause.\n\nIf the dialog's source object is `Opportunity` but the customer's data lives on `Quote`, **stop and ask the user to change the dialog's \"Source Object Type\" first** — don't switch silently.\n\n---\n\n## Output shape — GCP-specific\n\nGCP's offer body is split between a top-level `gcpPrivateOffer` (for the private offer record itself) and several flat fields under `info.*` for offer-creation parameters. Common paths:\n\n### Top-level `$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 pre-set by a filler |\n| `$target.expireTime` | Date / ISO string. Mirror to `info.gcpPrivateOffer.expireTime` |\n| `$target.contactIds` | Array of Suger contact IDs |\n| `$target.metaInfo.isRenewalOffer` | true for Replacement / native-renewal flows |\n| `$target.metaInfo.renewalOfferType` | `\"GcpMarketplace\"` when isRenewalOffer is true |\n\n### `$target.info.*` (offer-creation parameters)\n\n| Path | Meaning |\n|---|---|\n| `$target.info.eulaType` | `\"ISV\"` (default) or `\"CUSTOM\"` |\n| `$target.info.gcpDuration` | Term length in months (integer 1–60). REQUIRED for GCP private offers |\n| `$target.info.gcpCustomerInfo` | REQUIRED. Identifies the buyer. Use `{ billingAccountId: \"billingAccounts/<id>\" }` when the buyer's billing account ID is known and verified, OR `{ unverifiedBillingAccount: \"<id>\", organization, contact, email }` when the seller has only the customer's organization name and contact (Patterns C/D). Pattern B uses the contact-only form `{ organization, contact, email }` for non-CPPO. |\n| `$target.info.gcpProviderInfo` | `{ ... }` — provider/seller info |\n| `$target.info.gcpProviderInternalNote` | Seller-only note (not visible to buyer) |\n| `$target.info.gcpProviderPublicNote` | Buyer-visible note. Defaults to offer name if omitted |\n| `$target.info.gcpOfferDealType` | One of `OFFER_DEAL_TYPE_UNSPECIFIED` (new business), `CHANNEL_SHIFT`, `MIGRATION`, `NATIVE_RENEWAL` |\n| `$target.info.gcpFeatures` | Array of `GcpMarketplaceProductFeatureValue` — feature overrides |\n| `$target.info.gcpPlans` | Array — pricing plans referenced by this offer |\n| `$target.info.gcpUsagePlanPriceModel` | Usage-plan price model (only for Usage plan, not Subscription) |\n| `$target.info.gcpPaymentSchedule` | `\"PREPAY\"` or `\"POSTPAY\"` (deprecated soon; use `info.paymentSchedule`) |\n| `$target.info.gcpSkuDiscounts` | Array of `{ metricId, discountPercent }` for POSTPAY offers |\n| `$target.info.gcpSowAgreementDocument` | Optional — Statement of Work doc for professional services |\n| `$target.info.startTime` | Date — future start. Omit / null for acceptance-start |\n| `$target.info.paymentSchedule` | Cross-cloud `PREPAY`/`POSTPAY`. Prefer this over `gcpPaymentSchedule` |\n| `$target.info.paymentInstallments` | Array of `{ chargeOn: ISOString, amount, skuDiscounts? }` for PREPAY |\n\n### `$target.info.gcpPrivateOffer.*` (private offer body)\n\nThese mirror the GCP Cloud Billing API's privateOffer resource:\n\n| Path | Meaning |\n|---|---|\n| `$target.info.gcpPrivateOffer.offerTitle` | Customer-facing offer title |\n| `$target.info.gcpPrivateOffer.expireTime` | Date when the offer expires if not accepted |\n| `$target.info.gcpPrivateOffer.offerSource` | `\"OFFER\"` (Standard) or `\"RESOLD\"` (CPPO via channel partner) |\n| `$target.info.gcpPrivateOffer.providerPublicNote` | Buyer-visible note |\n| `$target.info.gcpPrivateOffer.providerInternalNote` | Seller-only note |\n| `$target.info.gcpPrivateOffer.policies` | `{ defaultRenewalPolicy, downgradePolicy, cancellationPolicy, purchaseApproval, offerDealType }` |\n| `$target.info.gcpPrivateOffer.useLegacyPartnerEula` | Boolean — true forces partner-EULA flow |\n| `$target.info.gcpPrivateOffer.replacementMetadata` | REQUIRED for Replacement offers — `{ replacedOfferId, replacedAgreement, ... }` |\n| `$target.info.gcpPrivateOffer.resellerInfo` | REQUIRED for CPPO (`offerSource = \"RESOLD\"`) — partner identity |\n\n### GCP-specific gotchas\n\n- **`gcpDuration` is REQUIRED**: term length in months (integer). Without it, GCP rejects the offer.\n- **`gcpCustomerInfo.billingAccountId` is REQUIRED**: format `billingAccounts/01ABCD-234567-EFGH89` (you'll see this exact prefix). NEVER invent — read from a CRM field.\n- **CPPO via `offerSource: \"RESOLD\"`**: GCP's CPPO model uses a separate `GcpResellerPrivateOfferPlan` template upstream. The script-time output for CPPO Standard private offers needs `offerSource = \"RESOLD\"` + `resellerInfo` + `partnerId` references.\n- **Replacement offers**: GCP's analog of AWS ABO. Set `gcpOfferDealType = \"NATIVE_RENEWAL\"` AND `gcpPrivateOffer.replacementMetadata = { replacedOfferId, ... }`. The replaced offer's id comes from `$Entitlement.id` or a related-entitlement query.\n- **PREPAY vs POSTPAY**: PREPAY uses `paymentInstallments`; POSTPAY uses `gcpSkuDiscounts` (per-metric discount %).\n\n---\n\n## Archetype branches\n\n### Standard (`$OfferType === \"Standard\"` or undefined)\n\nDirect customer offer, no channel partner.\n\n```js\n$target.info.gcpPrivateOffer.offerSource = \"OFFER\";\n// no resellerInfo, no replacementMetadata\n```\n\n### CPPO (`$OfferType === \"CPPO\"`)\n\nChannel partner / reseller offer.\n\n```js\n$target.info.gcpPrivateOffer.offerSource = \"RESOLD\";\n$target.info.gcpPrivateOffer.resellerInfo = {\n // TODO: shape varies — confirm with the user. Typical fields:\n // partnerName: \"<TODO partner display name>\",\n // partnerAccountId: \"<TODO partner account id>\",\n};\n// CPPO often uses a different EULA flow; confirm before setting useLegacyPartnerEula\n```\n\n### Replacement (`$OfferType === \"Replacement\"`) — GCP's renewal/amendment analog\n\nReplaces an existing GCP entitlement. Requires `$Entitlement.id` of the prior entitlement.\n\n```js\nif ($OfferType === \"Replacement\") {\n $target.metaInfo = $target.metaInfo || {};\n $target.metaInfo.isRenewalOffer = true;\n $target.metaInfo.renewalOfferType = \"GcpMarketplace\";\n\n $target.info.gcpOfferDealType = \"NATIVE_RENEWAL\";\n\n $target.info.gcpPrivateOffer.replacementMetadata = {\n // TODO: confirm exact shape with the user. Typical:\n // replacedOfferId: \"<prior offer id>\",\n // replacedAgreement: \"projects/<projectNumber>/agreements/<agreementId>\",\n };\n\n // If the customer's flow merges unbilled installments from the prior entitlement\n // (similar to the AWS ABO pattern), query the prior Suger Entitlement record:\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 // ... merge prior unbilled installments with the current schedule, sort by chargeOn ...\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, \" +\n \"Owner.Email, Owner.Name, \" +\n \"ExpirationDate, TotalPrice, Subscription_Term__c, \" +\n \"GCP_Billing_Account__c, \" +\n \"Type \" +\n \"FROM Quote WHERE Id = '\" + $source.Id + \"' AND IsSyncing = true LIMIT 1\"\n);\nif (!record) throw new Error(\"Primary syncing Quote not found for \" + $source.Id);\n```\n\n### 2. Customer info (REQUIRED — GCP rejects offers without this)\n\n```js\nconst billingAccount = String(record.GCP_Billing_Account__c || \"\").trim();\nif (!billingAccount) {\n // TODO: replace GCP_Billing_Account__c with the real CRM field path\n throw new Error(\"GCP billing account ID is required.\");\n}\n$target.info.gcpCustomerInfo = {\n billingAccountId: billingAccount, // Format: \"billingAccounts/01ABCD-234567-EFGH89\"\n};\n```\n\n### 3. Duration (REQUIRED)\n\n```js\nconst termMonths = Math.floor(Number(record.Subscription_Term__c || 0));\nif (!termMonths || termMonths < 1 || termMonths > 60) {\n throw new Error(\"GCP duration must be between 1 and 60 months. Got: \" + termMonths);\n}\n$target.info.gcpDuration = termMonths;\n```\n\n### 4. Notification contacts\n\n```js\nconst contactIds = [];\nconst opsContact = $createContact({\n name: \"Marketplace Operations\",\n emailAddress: \"<TODO ops alias email>\",\n});\ncontactIds.push(opsContact.id);\nif (record?.Owner?.Email) {\n const owner = $createContact({\n name: record.Owner.Name || \"Quote Owner\",\n emailAddress: record.Owner.Email,\n });\n contactIds.push(owner.id);\n}\n$target.contactIds = contactIds;\n```\n\n### 5. Renewal flag (always set, then specialise per archetype)\n\n```js\nif (record?.Type === \"Renewal\" || record?.Type === \"Amendment\") {\n $target.metaInfo = $target.metaInfo || {};\n $target.metaInfo.isRenewalOffer = true;\n $target.metaInfo.renewalOfferType = \"GcpMarketplace\";\n $target.info.gcpOfferDealType = \"NATIVE_RENEWAL\";\n}\n```\n\n### 6. Payment installments — PREPAY only\n\n```js\nfunction roundToTwo(n) { return Math.round(n * 100) / 100; }\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}\n\nconst totalAmount = roundToTwo(record.TotalPrice || 0);\nconst freq = String(record.Billing_Frequency__c || \"\").toLowerCase();\nlet numInstallments = 0;\nlet monthsPer = 1;\nif (freq === \"monthly\") { numInstallments = termMonths; monthsPer = 1; }\nelse if (freq === \"quarterly\") { numInstallments = Math.ceil(termMonths / 3); monthsPer = 3; }\nelse if (freq === \"annual\") { numInstallments = Math.ceil(termMonths / 12); monthsPer = 12; }\nelse if (freq === \"upfront\" || freq === \"all upfront\") {\n numInstallments = 1;\n monthsPer = termMonths;\n}\n\nconst installments = [];\nconst startDate = parseDate(record.Subscription_Start_Date__c) || new Date();\nif (totalAmount > 0 && numInstallments > 0) {\n const per = roundToTwo(totalAmount / numInstallments);\n let total = 0;\n for (let i = 0; i < numInstallments; i++) {\n const chargeOn = addMonths(startDate, i * monthsPer);\n installments.push({ amount: per, chargeOn: chargeOn.toISOString() });\n total += per;\n }\n // Round-drift adjustment on last installment\n const drift = roundToTwo(totalAmount - total);\n installments[installments.length - 1].amount = roundToTwo(installments[installments.length - 1].amount + drift);\n}\n$target.info.paymentInstallments = installments;\n$target.info.paymentSchedule = \"PREPAY\";\n$target.info.gcpPaymentSchedule = \"PREPAY\"; // legacy alias\n```\n\n### 7. EULA\n\n```js\n// Default\n$target.info.eulaType = \"ISV\";\n\n// Custom partner EULA\n// $target.info.eulaType = \"CUSTOM\";\n// $target.info.gcpPrivateOffer.useLegacyPartnerEula = true;\n```\n\n---\n\n## Hard constraints — never violate\n\n- **`gcpCustomerInfo.billingAccountId`**: required, format `billingAccounts/<id>`. NEVER invent.\n- **`gcpDuration`**: integer 1–60 (months).\n- **`gcpOfferDealType`**: must be one of `OFFER_DEAL_TYPE_UNSPECIFIED`, `CHANNEL_SHIFT`, `MIGRATION`, `NATIVE_RENEWAL`.\n- **`offerSource`**: must be `\"OFFER\"` or `\"RESOLD\"`.\n- **Replacement offers**: must include `replacementMetadata` referencing the prior agreement.\n- **PREPAY**: must include `paymentInstallments[]` with at least one entry; `chargeOn` ISO timestamps in the future.\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- ❌ Hard-code GCP billing account IDs, project IDs, or service names. Always use `<TODO ...>`.\n\n### ⚠️ Cross-cloud field contamination — the silent killer\n\nGCP has its own offer-body shape under `$target.info.gcpPrivateOffer.*` plus several flat `info.gcp*` fields. The GCP offer-creation API **silently ignores** any field it doesn't recognise — no error, no warning. So if your script writes AWS-only or Azure-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 a GCP script** (they belong to other clouds):\n\n| AWS-only — DO NOT use here | Azure-only — DO NOT use here |\n|---|---|\n| `$target.info.commits` | `$target.info.azurePrivateOffer.*` |\n| `$target.info.dimensions` | (everything under that nested object) |\n| `$target.info.awsCppoOpportunity` | |\n| `$target.info.buyerAwsAccountIds` | |\n| `$target.info.attachEulaType` | |\n\nFor GCP, the offer-specific fields are:\n- `$target.info.gcpPrivateOffer.*` (nested private-offer body)\n- `$target.info.gcpDuration` (REQUIRED — months)\n- `$target.info.gcpCustomerInfo` (REQUIRED — `billingAccountId`)\n- `$target.info.gcpProviderInfo` (sales contact)\n- `$target.info.gcpPlans[]` (plan references)\n- `$target.info.gcpOfferDealType` (deal-type enum)\n- `$target.info.gcpPaymentSchedule` / cross-cloud `info.paymentSchedule`\n- `$target.info.gcpUsagePlanPriceModel`\n- `$target.info.gcpSkuDiscounts[]` (POSTPAY)\n- `$target.info.gcpResellerPrivateOfferPlan` (CPPO)\n- `$target.info.paymentInstallments[]` (cross-cloud, used for PREPAY)\n\nSee the \"Output shape — GCP-specific\" tables above for the full list. Cross-cloud bleed is the single most common bug when adapting an AWS or Azure script to GCP; if you're translating from another cloud, **delete every line that touches `info.commits` / `info.dimensions` / `info.azurePrivateOffer.*` 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 AWS / Azure skills — `crmFields` may include virtual aliases like `_PrimaryContactEmail`, `_Contact_Decision_Maker` etc. These are framework-level placeholders resolved by 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 `gcpDuration`, `gcpCustomerInfo`, or `gcpOfferDealType` values\n\nThese three are non-optional for GCP offer creation. If the user hasn't given you real values, throw with a clear error message (better than silently passing a placeholder).\n\n### ⚠️ Don't treat `Replacement` like AWS `ABO` blindly\n\nThe two are conceptually similar (both replace a prior entitlement) but have different output shapes:\n- AWS ABO uses `paymentInstallments` merge from `Suger__Entitlement__c`\n- GCP Replacement requires `gcpPrivateOffer.replacementMetadata` with `replacedAgreement` resource name AND optionally the same installment-merge pattern\nConfirm with the user which fields their flow needs.\n\n---\n\n## Worked example patterns\n\nThe four anonymized patterns below are **distilled from real production GCP scripts** across multiple customers. Customer names, emails, OAuth client IDs, GCP plan names, and Suger product IDs have been replaced with placeholders — the SHAPES (control flow, SOQL, GCP customer/provider/plan structure, branching) are accurate. Always replace placeholders with the user's real values before saving.\n\n### Pattern A — SBQQ__Quote__c source, customer email from Opportunity.ContactId, frequency-based installments\n\nThe simplest GCP shape. Pulls the customer's contact name/email from the Opportunity's primary contact, builds payment installments from term + Payment_Frequency__c, and uses a fixed `gcpPlans` reference.\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 record = $query(\n \"SELECT SBQQ__Opportunity2__r.ContactId, SBQQ__NetAmount__c, \" +\n \"SBQQ__SubscriptionTerm__c, Payment_Frequency__c, SBQQ__StartDate__c \" +\n \"FROM SBQQ__Quote__c WHERE Id = '\" + $source.Id + \"' LIMIT 1\"\n);\nconst contact = $query(\n \"SELECT Name, Email FROM Contact WHERE Id = '\" + record.SBQQ__Opportunity2__r.ContactId + \"'\"\n);\n\n$target.info.gcpCustomerInfo.contact = contact.Name;\n$target.info.gcpCustomerInfo.email = contact.Email;\n$target.info.gcpPlans = [{ name: \"<TODO product gcp plan name>\" }];\n$target.info.gcpPaymentSchedule = \"PREPAY\";\n\n// Future-dated start only — leave null for acceptance-start\nconst startDate = parseDate(record.SBQQ__StartDate__c);\nif (startDate && startDate > new Date()) {\n $target.info.startTime = startDate.toISOString();\n}\n\n// Installments by frequency\nconst startTime = record.SBQQ__StartDate__c;\nconst termLength = record.SBQQ__SubscriptionTerm__c;\nconst amount = record.SBQQ__NetAmount__c;\nconst frequency = record.Payment_Frequency__c;\n$target.info.paymentInstallments = [];\n\nif (frequency === \"Upfront\") {\n // GCP upfront: a single installment charged at offer start. NEVER use\n // `info.commits` — that is an AWS-only field; GCP silently drops it.\n $target.info.paymentInstallments = [{\n chargeOn: startTime ? new Date(startTime).toISOString() : new Date().toISOString(),\n amount: parseFloat(amount.toFixed ? amount.toFixed(2) : Number(amount).toFixed(2)),\n }];\n} else if (termLength && startTime) {\n let interval = 0;\n if (frequency === \"Monthly\") interval = 1;\n if (frequency === \"Quarterly\") interval = 3;\n if (frequency === \"Semi Annual\") interval = 6;\n if (frequency === \"Annual\") interval = 12;\n\n if (interval) {\n const installments = [];\n const numberOfInstallments = termLength / interval;\n const installmentAmount = amount / numberOfInstallments;\n let curDate = new Date(startTime);\n for (let i = 0; i < numberOfInstallments; i++) {\n installments.push({\n chargeOn: curDate.toISOString(),\n chargeOnStr: curDate.toISOString().substring(0, 10),\n amount: parseFloat(installmentAmount.toFixed(2)),\n });\n curDate.setMonth(curDate.getMonth() + interval);\n }\n $target.info.paymentInstallments = installments;\n }\n}\n```\n\n### Pattern B — Standard Quote source, custom EULA, gcpResellerPrivateOfferPlan template, NATIVE_RENEWAL flow\n\nCustomer with a Master Customer Agreement attached as PDF, single bundled commit covering the offer, per-product `gcpPlans` lookup, and a CPPO-style reseller plan template (`gcpResellerPrivateOfferPlan`) for offer-term/payment-recurrence. CPPO branch differentiates by `$OfferType`.\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\n$target.info = {};\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 \"Opportunity.Contact__c \" +\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 accName = quoteData?.Account?.Name;\nconst totalPrice = quoteData?.TotalPrice || 0;\nconst amount = roundToTwoDecimalPlaces(totalPrice);\nconst termLength = quoteData?.Subscription_Term__c;\nconst oppLevelBuyerContactId = quoteData?.Opportunity?.Contact__c;\n\n// Buyer contact — try opportunity-level contact first, fall back to a TODO default\nlet buyerContactEmail, buyerContactName;\nif (oppLevelBuyerContactId) {\n const oppBuyerData = $query(\n \"SELECT Id, Name, Email FROM Contact WHERE Id = '\" + oppLevelBuyerContactId + \"'\"\n );\n buyerContactEmail = oppBuyerData?.Email;\n buyerContactName = oppBuyerData?.Name;\n}\nif (buyerContactEmail == null || buyerContactName == null) {\n buyerContactEmail = \"<TODO default buyer contact email>\";\n buyerContactName = \"<TODO default buyer contact name>\";\n}\n\n$target.info.gcpCustomerInfo = {};\nif ($OfferType !== \"CPPO\") {\n $target.info.gcpCustomerInfo.contact = buyerContactName;\n $target.info.gcpCustomerInfo.email = buyerContactEmail;\n}\n$target.info.gcpCustomerInfo.organization = accName;\n\nlet updatedAccName = (accName ?? \"\").toString().replace(/[^a-zA-Z0-9_-]/g, \"\");\n$target.name = quoteData?.QuoteNumber + \"_\" + updatedAccName;\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}\n$target.contactIds = contactIds;\n\n// Expiry / start / end\nconst today = new Date();\nconst expireDate = new Date();\nexpireDate.setDate(today.getDate() + 14);\n$target.expireTime = expireDate;\n\nconst startDate = new Date();\nstartDate.setDate(today.getDate() + 1);\nif ($OfferType === \"CPPO\") {\n $target.info.startTime = startDate;\n}\nconst endDate = new Date();\nendDate.setDate(startDate.getDate() + 14);\n$target.endTime = endDate;\n\n// Provider info\n$target.info.gcpProviderInfo = {};\n$target.info.gcpProviderInfo.salesContactName = \"<TODO sales contact name>\";\n$target.info.gcpProviderInfo.salesContactEmail = \"<TODO sales contact email>\";\n\n$target.info.gcpDuration = termLength;\n// GCP bundled-commit pricing lives on `info.gcpPlans` + the per-product\n// gcpResellerPrivateOfferPlan template defined later in this Pattern. Do\n// NOT use `info.commits` here — that is an AWS-only field and the GCP\n// offer creation API silently drops it, leaving the offer with no priced commit.\n\n// Custom EULA\n$target.info.eulaType = \"CUSTOM\";\n$target.info.eulaUrl = \"<TODO hosted EULA URL — supplied by user / customer config>\";\n\n$target.info.gcpUsagePlanPriceModel = \"CUD_LIST_PRICE\";\n$target.info.gcpPaymentSchedule = \"PREPAY\";\n$target.info.gcpOfferDealType = (recordType === \"Renewal\") ? \"CHANNEL_SHIFT\" : \"OFFER_DEAL_TYPE_UNSPECIFIED\";\n\n// Reseller offer-term template (used for both Standard and CPPO in this pattern)\n$target.info.gcpResellerPrivateOfferPlan = {\n offerTermTemplate: {\n paymentRecurrence: \"CUSTOM_PERIOD\",\n startPolicy: \"OFFER_START_POLICY_IMMEDIATE\",\n termDurationConstraint: { defaultDuration: { count: termLength } },\n },\n reusePolicy: \"REUSE_POLICY_SINGLE_USE\",\n startPolicy: \"OFFER_START_POLICY_IMMEDIATE\",\n};\n\n$target.info.paymentInstallments = [{ amount, discountPercentage: 0 }];\n\n// Per-product gcpPlans lookup\nlet productId = $target.productID;\nif (typeof $Product !== \"undefined\" && $Product !== null) productId = $Product.id;\n\nif (productId === \"<EXAMPLE_PRODUCT_ID_A>\") {\n $target.info.gcpPlans = [{\n name: \"<TODO gcp plan name for product A>\",\n priceInfo: { priceModel: \"SUBSCRIPTION\" },\n }];\n} else if (productId === \"<EXAMPLE_PRODUCT_ID_B>\") {\n $target.info.gcpPlans = [{\n name: \"<TODO gcp plan name for product B>\",\n priceInfo: { priceModel: \"SUBSCRIPTION\" },\n }];\n}\n```\n\n### Pattern C — SBQQ__Quote__c source, CPPO via gcpResellerPrivateOfferPlan, per-product gcpPlans for many products\n\nCPPO offer with reseller-plan template. Renewal flag derived from `Account.New_Customer_Status__c`. Includes installment schedule from explicit start/end dates and per-product `gcpPlans` lookup across many SKUs.\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 getMonthDifference(d1, d2) {\n const a = new Date(d1), b = new Date(d2);\n let total = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());\n if (b.getDate() < a.getDate()) total--;\n return total;\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}\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 = {};\n\nconst quoteData = $query(\n \"SELECT SBQQ__Opportunity2__r.Customer_AWS_Account_Number__c, \" +\n \"SBQQ__Opportunity2__r.Partner_AWS_Account_Number__c, \" +\n \"CPQ_Reseller_Total_Amount__c, 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__EndDate__c, SBQQ__StartDate__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 \"FROM SBQQ__Quote__c WHERE Id = '\" + $source.Id + \"' LIMIT 1\"\n);\n\nconst customerAwsAccNumber = quoteData.SBQQ__Opportunity2__r?.Customer_AWS_Account_Number__c || \"\";\nconst partnerName = quoteData.CPQ_PartnerAccountName__c;\nconst customerName = quoteData.SBQQ__Opportunity2__r?.Account.Name || \"\";\nconst customerStatus = quoteData.SBQQ__Opportunity2__r?.Account.New_Customer_Status__c || \"\";\nconst resellerTotalAmt = quoteData.CPQ_Reseller_Total_Amount__c;\nconst invoicingTerms = quoteData.Invoicing_Terms__c;\nconst nonStandardReq = quoteData.CPQ_Non_Standard_Request__c;\nconst quoteStartDate = parseDate(quoteData.SBQQ__StartDate__c);\nconst quoteEndDate = parseDate(quoteData.SBQQ__EndDate__c);\n\n// Offer name — different shape for CPPO\nlet offerName = customerName + \"-\" + partnerName + \"-<TODO ISV name>-\" + quoteData.Name + \"-\" + simpleCurrentDate();\nif ($OfferType === \"CPPO\") {\n offerName = customerName + \"-\" + partnerName + \"-GCP-<TODO ISV name>-\" + quoteData.Name + \"-\" + simpleCurrentDate();\n}\n$target.name = offerName.replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n$target.metaInfo.isRenewalOffer = customerStatus.includes(\"Current Customer\");\n\n// Notification contacts\nconst contactIds = [];\ncontactIds.push($createContact({\n name: \"Cloud Deal Desk\",\n emailAddress: \"<TODO ops alias email>\",\n}).id);\nconst oppOwnerEmail = quoteData.SBQQ__Opportunity2__r?.Owner?.Email;\nif (oppOwnerEmail) {\n contactIds.push($createContact({\n name: quoteData.SBQQ__Opportunity2__r.Owner.Name,\n emailAddress: oppOwnerEmail,\n }).id);\n}\n$target.contactIds = contactIds;\n\n$target.info.gcpCustomerInfo = {};\n$target.info.gcpCustomerInfo.unverifiedBillingAccount = customerAwsAccNumber;\n$target.info.gcpCustomerInfo.organization = customerName;\n\n$target.info.gcpUsagePlanPriceModel = \"CUD_LIST_PRICE\";\n$target.info.gcpPaymentSchedule = \"PREPAY\";\n$target.info.eulaType = \"CUSTOM\";\n\n// Installments — interval-based when start/end dates are explicit, otherwise single charge today\nif (\n nonStandardReq === \"Service Contract with Invoicing Schedule\" &&\n invoicingTerms !== \"Upfront\" &&\n quoteStartDate != null && quoteEndDate != null\n) {\n const numOfMonths = getMonthDifference(quoteStartDate, quoteEndDate);\n let interval = 0;\n let numOfInstalments = 0;\n if (invoicingTerms === \"Monthly\") { interval = 1; numOfInstalments = numOfMonths; }\n if (invoicingTerms === \"Annual\") { interval = 12; numOfInstalments = Math.ceil(numOfMonths / 12); }\n if (invoicingTerms === \"Quarterly\") { interval = 3; numOfInstalments = Math.ceil(numOfMonths / 3); }\n\n if (resellerTotalAmt > 0 && numOfInstalments > 0) {\n const amountPerMonth = roundToTwoDecimalPlaces(resellerTotalAmt / numOfInstalments);\n const instalments = [];\n let totalAmount = 0;\n for (let i = 0; i < numOfInstalments; i++) {\n const curChargeDate = addMonths(quoteStartDate, i * interval);\n instalments.push({ amount: amountPerMonth, chargeOn: curChargeDate.toISOString() });\n totalAmount += amountPerMonth;\n }\n const drift = roundToTwoDecimalPlaces(resellerTotalAmt - totalAmount);\n instalments[numOfInstalments - 1].amount = roundToTwoDecimalPlaces(\n instalments[numOfInstalments - 1].amount + drift\n );\n $target.info.paymentInstallments = instalments;\n }\n} else {\n $target.info.paymentInstallments = [{ amount: resellerTotalAmt, chargeOn: new Date().toISOString() }];\n}\n\n// CPPO-specific block\nif ($OfferType === \"CPPO\") {\n $target.info.startTime = new Date().toISOString();\n $target.info.gcpProviderInternalNote = \"All products related to <TODO order tag> \" + quoteData.Name;\n $target.info.gcpResellerPrivateOfferPlan = {\n offerTermTemplate: {\n paymentRecurrence: \"CUSTOM_PERIOD\",\n startPolicy: \"OFFER_START_POLICY_IMMEDIATE\",\n },\n reusePolicy: \"REUSE_POLICY_SINGLE_USE\",\n startPolicy: \"OFFER_START_POLICY_IMMEDIATE\",\n };\n}\n$target.info.gcpProviderPublicNote = \"All products related to <TODO order tag> \" + quoteData.Name;\n\n// Per-product gcpPlans lookup (many SKUs)\nlet productId = $target.productID;\nif (typeof $Product !== \"undefined\" && $Product !== null) productId = $Product.id;\n\nif (productId === \"<EXAMPLE_PRODUCT_ID_A>\") $target.info.gcpPlans = [{ name: \"<TODO gcp plan A>\" }];\nelse if (productId === \"<EXAMPLE_PRODUCT_ID_B>\") $target.info.gcpPlans = [{ name: \"<TODO gcp plan B>\" }];\nelse if (productId === \"<EXAMPLE_PRODUCT_ID_C>\") $target.info.gcpPlans = [{ name: \"<TODO gcp plan C>\" }];\n// ... etc, one branch per Suger Product\n\n$target.info.gcpDuration = -1; // -1 means use Quote-driven duration via offerTermTemplate\n```\n\n### Pattern D — External CPQ API + dynamic product lookup + skuDiscounts for POSTPAY + CPPO branch\n\nThe most complex GCP pattern. Calls an external CPQ via OAuth2 for full pricing, looks up Suger Product by ListKey, builds per-meter `gcpSkuDiscounts` for POSTPAY usage products, and includes both PREPAY (paymentInstallments) and POSTPAY (skuDiscounts on each installment) shapes.\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 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}\nfunction roundToTwoDecimalPlaces(n) { return Math.round(n * 100) / 100; }\n\nif (!$target.info) $target.info = {};\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, OpportunityId \" +\n \"FROM Quote WHERE Id = '\" + $source.Id + \"'\"\n);\n\n// Look up primary contact for sales-contact-name fallback\nconst contactData = $query(\n \"SELECT Contact.Name FROM OpportunityContactRole \" +\n \"WHERE IsPrimary = true AND OpportunityId = '\" + record.OpportunityId + \"'\"\n);\nconst primaryContactName = contactData?.Contact?.Name || \"Marketplace Operations\";\n\n// Call external CPQ via OAuth2\nconst token = $marketplaceApi.getOAuth2Token({\n clientId: \"<TODO oauth client id>\",\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\nconst amount = quoteData.TransactionTotal;\nlet listKey, planKey, numOfMonths, billFreq, discount, priceModel;\nif (Array.isArray(quoteData.QuoteLines)) {\n const firstLine = quoteData.QuoteLines[0];\n if (firstLine) {\n listKey = firstLine.ListKey;\n planKey = firstLine.PlanKey;\n billFreq = firstLine.BillingFrequency;\n numOfMonths = Number(firstLine.ServiceDuration);\n discount = Number(firstLine.Discount);\n priceModel = firstLine.PricingMode;\n }\n}\n\n// listKey is text from a CPQ quote line — validate before interpolating\n// into SOQL (see \"SOQL injection\" guidance above).\nif (!listKey || !/^[A-Za-z0-9_-]+$/.test(listKey)) {\n throw new Error('Invalid or missing ListKey: \"' + String(listKey) + '\"');\n}\nconst productInfo = $query(\n \"SELECT Suger__Product_ID__c, Name, Suger__Product_Type__c \" +\n \"FROM Suger__Product__c WHERE Suger__Product_External_ID__c = '\" + listKey + \"' LIMIT 1\"\n);\n$target.productID = productInfo.Suger__Product_ID__c;\n\n$target.metaInfo.isRenewalOffer = (quoteData.RenewalFlag === \"true\");\n\n// Offer name\nconst today = new Date();\nconst todaySimpleDate = today.toISOString().split(\"T\")[0];\n$target.name = (record.Account.Name + \"-\" + productInfo?.Name + \"-\" + todaySimpleDate)\n .replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n// Expiry — min(today + 28d, ExpirationDate)\nconst thirtyDaysFromToday = new Date(today);\nthirtyDaysFromToday.setDate(today.getDate() + 28);\nconst expirationDate = parseDate(record.ExpirationDate);\nconst expiryDate = expirationDate ? new Date(Math.min(thirtyDaysFromToday, expirationDate)) : thirtyDaysFromToday;\n$target.expireTime = expiryDate;\n\n$target.info.currency = quoteData.currency || \"USD\";\n\n// SKU discounts for POSTPAY usage dimensions\nconst skuDiscounts = quoteData.QuoteLines\n .filter((i) => i.DimensionType === \"BURST\" || i.DimensionType === \"PAYGO\")\n .map((i) => ({ metricId: i.DimensionKey, discount: Number(i.Discount) }));\n\n// Installments — frequency-based\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 firstInvoiceDate = new Date(expiryDate);\nconst instalments = [];\nif (amount > 0 && numOfInstallments > 0) {\n const amountPerMonth = roundToTwoDecimalPlaces(amount / numOfInstallments);\n let totalAmount = 0;\n for (let i = 0; i < numOfInstallments; i++) {\n const curChargeDate = addMonths(firstInvoiceDate, i * monthsPerInstallment);\n instalments.push({\n amount: amountPerMonth,\n chargeOn: curChargeDate,\n chargeOnStr: curChargeDate.toISOString(),\n skuDiscounts,\n discountPercentage: 0,\n });\n totalAmount += amountPerMonth;\n }\n const drift = roundToTwoDecimalPlaces(amount - totalAmount);\n instalments[numOfInstallments - 1].amount = roundToTwoDecimalPlaces(\n instalments[numOfInstallments - 1].amount + drift\n );\n $target.info.paymentInstallments = instalments;\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// Customer / provider info\n$target.info.gcpCustomerInfo = {};\n$target.info.gcpProviderInfo = {};\n$target.info.gcpCustomerInfo.unverifiedBillingAccount =\n $OfferType === \"CPPO\" ? quoteData.HyperScalarPartnerId : quoteData.HyperScalarCustomerId;\n$target.info.gcpCustomerInfo.organization = record.Account.Name;\n$target.info.gcpCustomerInfo.contact = \"Marketplace Operations\";\n$target.info.gcpCustomerInfo.email = \"<TODO ops alias email>\";\n$target.info.gcpProviderInfo.salesContactName = primaryContactName;\n$target.info.gcpProviderInfo.salesContactEmail = \"<TODO ops alias email>\";\n\n$target.info.gcpPlans = [{ name: planKey }];\n$target.info.eulaType = \"SCMP\";\n$target.info.gcpDuration = numOfMonths;\n\n// PREPAY vs POSTPAY\n$target.info.gcpPaymentSchedule = \"PREPAY\";\n$target.info.gcpUsagePlanPriceModel = priceModel;\n\n// POSTPAY-only: top-level skuDiscounts (PREPAY uses installment.skuDiscounts above)\n$target.info.gcpSkuDiscounts = skuDiscounts;\n$target.info.discountPercentage = discount;\n\n$target.info.gcpOfferDealType = quoteData.RenewalFlag === \"true\"\n ? \"NATIVE_RENEWAL\"\n : \"OFFER_DEAL_TYPE_UNSPECIFIED\";\n\n// CPPO branch — adds reseller plan, start/end dates, and per-installment discount\nif ($OfferType === \"CPPO\") {\n $target.info.gcpResellerPrivateOfferPlan = {\n offerTermTemplate: {\n paymentRecurrence: \"CUSTOM_PERIOD\",\n startPolicy: \"OFFER_START_POLICY_IMMEDIATE\",\n termDurationConstraint: { defaultDuration: { count: numOfMonths } },\n },\n reusePolicy: \"REUSE_POLICY_SINGLE_USE\",\n startPolicy: \"OFFER_START_POLICY_IMMEDIATE\",\n };\n $target.info.startTime = today;\n const endDate = new Date(today);\n endDate.setDate(today.getDate() + 28);\n $target.endTime = endDate;\n\n for (let i = 0; i < instalments.length; i++) {\n instalments[i].discountPercentage = discount;\n }\n}\n```\n\n---\n\n## CPQ Intake Form ingestion\n\nWhen the user pastes a numbered CPQ Intake Form, parse it and **skip the \"list unknowns\" step**. Use the AWS skill's intake mapping table — most rows apply. GCP-specific differences:\n\n- **`7a. SKU/Dimension strategy`** maps to `info.gcpPlans[]` shape. \"Single Generic Dimension\" → one plan reference; \"Granular\" → multiple plan entries.\n- **`5b. Default billing cadence`** → `info.paymentSchedule` (PREPAY) + per-row `gcpPaymentSchedule`.\n- **`4g. Expiration logic`** → `info.gcpPrivateOffer.expireTime`.\n- For Replacement: ask which Quote field identifies the prior GCP agreement / entitlement.\n- **`11a. Active AWS agreement warning`** is irrelevant for GCP; ignore.\n\n---\n\n## Workflow — one unified script, scaffold-first, never drip-feed questions\n\nSame workflow as AWS / Azure skills.\n\n### Step 1 — Read state once\n`get_form_values({ formId })`. Optionally `query_sfdc_object_schema({ objectName })`.\n\n### Step 2 — List unknowns + offer two paths in a SINGLE message, then STOP\nTypical GCP-specific unknowns:\n- Suger Product the offer is for (drives `info.gcpPlans[]`)\n- SFDC field for the GCP `billingAccounts/<id>` (REQUIRED)\n- SFDC field for term length in months (REQUIRED, drives `gcpDuration`)\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 (drives `offerSource = \"RESOLD\"`)\n- Whether the team uses Replacement (drives `gcpOfferDealType = \"NATIVE_RENEWAL\"` and `replacementMetadata`)\n- For Replacement: how to identify the prior agreement (Quote field path or `$Entitlement` lookup)\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 with all three branches present (standard body + CPPO branch + Replacement branch). Mark unknowns with `// TODO:`. Then `show_quick_choices([\"Save to editor\", \"Edit further\", \"Start over\"])`.\n\n### Step 3b — User picks \"I'll fill in the values\"\nSingle bulleted form. After user replies, produce the complete script.\n\n### Step 4 — On user approval\n`invoke_action({ action_id: \"set_offer_script\", script: \"<full script>\" })`. Handler pushes previous script to undo stack.\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 GCP offer archetype?\" — one script handles all three 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 of public snapshot: 6a7c6b1ea23c276d132523d113313ce1973f9ac2d879f8cf718e08c6e914664c