← Files SugerARCHIVED FILE

SKILL.md

43.3 KB · Oct 2, 2026 · 00:17 UTC

↓ Download file

---
name: azure-offer-diagnosis
description: "Diagnose Azure Marketplace private offer CREATE_FAILED errors using Azure private offer schema rules, pricing type validation, plan field requirements, billing account validation, date constraints, EULA rules, and overlap detection."
---

# Diagnose Azure Offer Creation Error

You are diagnosing why an Azure Marketplace private offer failed to create (CREATE_FAILED). Use the schema rules below as your primary reference. Do NOT guess — match the error against these rules.

## Quick Reference — Hard Constraints (use these exact values; never fabricate)

- **offerPricingType**: exactly one of `editExistingOfferPricingOnly`, `newCustomizedPlans`, `saasNewCustomizedPlans`, `vmSoftwareReservations`, `customerPromotion`, `cspPromotion`, `multipartyPromotionOriginator`, `multipartyPromotionChannelPartner`. Each maps to a different pricing-plan schema — wrong value = schema error.
- **Plan field shape** (must match `offerPricingType`): `editExistingOfferPricingOnly` → `plan` (no `basePlan`); `newCustomizedPlans`/`saasNewCustomizedPlans` → `basePlan` + `newPlanDetails.name`; `vmSoftwareReservations` → `plan` + `softwareReservation` (no `pricing`).
- **Discount type**: `editExistingOfferPricingOnly` allows percentage OR absolute; `newCustomizedPlans`/`saasNewCustomizedPlans`/`vmSoftwareReservations` require absolute ONLY.
- **Customer Billing Account ID**: customer-specific (GUID or compound GUID); ask the user, never invent.
- **Start Date**: first day of a month (`YYYY-MM-01`). **End Date**: last day of a month. `CPPO_OUT` dates are not auto-corrected by the backend.
- **ExpireTime / acceptance deadline**: future date (YYYY-MM-DD). Default to today + 15 days when auto-proposing. Never suggest literals like `2026-06-15` or `2026-12-31`.
- **Flexible billing chargeDates**: unique per plan; offer rejected if any date repeats.
- **Per-user pricing**: requires `userLimits.{min,max}` when `recurrentPriceMode === perUser`.

## Quick Diagnosis Checklist (Most Common to Least Common)

Work through this checklist in order. Stop as soon as you find the matching root cause.

### 0. VM offer UI state — is the plan/reservation even added? (CHECK FIRST on VM)
- On VM offers, before diagnosing payload shape, read these from `get_form_values`: `pricingPlansCount`, `hasPricingPlan`, and per-plan `reservationRowsCount` / `hasReservationRows`.
- If `hasPricingPlan === false` → user must click **"+ Add plan"**. Do NOT say "remove the plan".
- If `hasPricingPlan === true` and `hasReservationRows === false` → user must click **"+ Add reservation"** and add vCPU rows. Do NOT say "remove the plan".
- `catalogAvailableCoreSizes` is informational (catalog options); it is NOT the user's configuration. Only `configuredSoftwareReservation` is.
- See "V3 UI-state diagnosis" below for the full decision tree.

### 1. Schema Mismatch — `plan` vs `basePlan` (MOST COMMON)
- **Symptom**: `#/pricing/0: Expected 1 matching subschema but found 0`
- `editExistingOfferPricingOnly` MUST use `plan` field, MUST NOT have `basePlan` or `newPlanDetails`
- `newCustomizedPlans` / `saasNewCustomizedPlans` MUST use `basePlan` field, MUST have `newPlanDetails.name`
- `vmSoftwareReservations` MUST use `plan` field, MUST NOT have `basePlan`
- **Fix**: Change the plan reference field to match the pricing type. Fixable by editing.

### 2. Discount Type Mismatch
- **Symptom**: `#/pricing/0: Expected 1 matching subschema but found 0`
- `editExistingOfferPricingOnly` allows percentage OR absolute discount
- `newCustomizedPlans` / `saasNewCustomizedPlans` require absolute discount ONLY
- `vmSoftwareReservations` requires absolute discount ONLY
- **Fix**: Change discount type to absolute for newCustomized/vmReservation pricing. Fixable by editing.

### 3. V1/V2/V3 Field Mismatch
- **Symptom**: `#/pricing/0: Expected 1 matching subschema but found 0` or `DapiMissingBillingTermAndPaymentOption`
- V1 (`editExistingOfferPricingOnly`): uses `billingTerm` + `paymentOption` fields, has `pricing` object
- V2 (`newCustomizedPlans` / `saasNewCustomizedPlans`): uses `contractDuration` + `billingFrequency` fields, has `pricing` object
- V3 (`vmSoftwareReservations`): uses `reservationDuration` + `paymentSchedule` fields, has `softwareReservation` object (NOT `pricing`)
- Mixing fields across versions causes schema validation failure
- **Fix**: Use the correct field set for the schema version. Fixable by editing.

### 4. Billing Account ID Invalid
- **Symptom**: `billing account is invalid as there is no billing profile` or `Customer billing account ID has been updated`
- `customerPromotion`: numeric or compound GUID format, validated via Azure API
- `cspPromotion`: tenant GUID only, validation skipped
- `multipartyPromotionOriginator` / `multipartyPromotionChannelPartner`: compound GUID, validated via API
- **Fix (no billing profile)**: Customer needs to create a billing profile in Azure portal. External issue — contact customer.
- **Fix (ID updated)**: Get the new billing account ID from the customer. External issue — contact customer.

### 5. Product Status Not Valid
- **Symptom**: `product under review` or product status is RESTRICTED/PENDING/DRAFT
- Product must be in PUBLIC status to create private offers
- **Fix**: Wait for product to be approved, or contact Azure support. External issue.

### 6. EULA Missing or Invalid
- **Symptom**: EULA validation error
- If `eulaType` is `CUSTOM`, `eulaUrl` is required
- Standard EULA (SCMP) does not require a URL
- **TWO valid forms of `eulaUrl` — do NOT flag either as wrong:**
  - `https://...` — a public web URL the user typed in
  - `org/{orgId}/file/{hash}/{filename}` — a Suger internal file reference that is auto-generated when the user uploads a PDF via the "Attach" control. The backend resolves this to a signed URL before sending to Azure. **This is NOT user input and NOT a bad URL — treat it as a valid uploaded file.**
- Only flag `eulaUrl` as invalid when it is empty, or when it is a non-`https` / non-`org/` free-form string (e.g. a plain filename with no path, or `file:///...`).
- **Fix**: Provide a valid EULA URL or upload a PDF, or switch to standard EULA. Fixable by editing.

### 7. Date Format Issues
- **Symptom**: Date validation error
- Start date must be the first day of a month
- End date must be the last day of a month
- For PRIVATE offers, dates are auto-fixed by the backend
- For CPPO_OUT offers, dates are NOT auto-fixed and must be exact
- **Fix**: Correct the date to first/last of month. Fixable by editing.

### 8. Flexible Billing on Unsupported Type
- **Symptom**: `flexible billing is NOT supported for editExistingOfferPricingOnly`
- Only `newCustomizedPlans`, `saasNewCustomizedPlans`, and `vmSoftwareReservations` support flexible billing
- `editExistingOfferPricingOnly` does NOT support flexible billing
- `cspPromotion` does NOT support flexible billing
- **Fix**: Remove flexible billing or change pricing type. Fixable by editing.

### 9. Duplicate Flexible Billing Dates
- **Symptom**: `Charge dates cannot be repeated`
- Each flexible billing installment must have a unique charge date
- **Fix**: Remove or change duplicate dates. Fixable by editing.

### 10. Offer Name / Term Overlap (SELF-CONTAINED — stop scanning after matching this)
- **Symptom**: `conflicts with an existing private offer for same billing account`
- Offer name must be unique for the same billing account
- Term periods must not overlap with existing active offers for the same billing account + product
- **Fix — give the user all three options, in this order:**
  1. Change the offer name to something distinct from the conflicting offer
  2. Withdraw or modify the conflicting existing offer (linked in the Azure error message)
  3. Adjust Start/End Date so the term does not overlap with the existing offer
- **STOP after matching this.** Conflict errors are self-contained — do NOT also inspect EULA, pricing, dates, or any other field for "while you're at it" fixes. Those extra suggestions confuse the user and are usually wrong.
- **Detail page (no form registered): present the three options above and call `show_quick_choices(["Edit Draft"])`. Do NOT attempt Apply from the detail page.**
- **Edit page (form IS registered): propose a concrete new Offer Name and offer Apply.** Offer Name is NOT a customer-specific field — you MAY generate a new value. Do this:
  1. Call `get_form_values` and read the `name` field (the form field key is `name`; the user-visible label is "Offer Name"). If the form does not expose `name` as editable (e.g. amendment offers disable this field), fall back to suggesting option 2 or 3 from the list above.
  2. Build a new unique name by appending a short random disambiguator to the current `name` value — e.g. a 6-character alphanumeric suffix like `-a1b2c3`, or any other obviously-unique token. Keep the original base name so the user still recognizes it. Do NOT embed a date in the suffix (you have no reliable clock source inside this skill — the system time is passed in the frontend instruction for other fields, but not here). Do NOT copy any literal example verbatim — generate a fresh random token.
  3. Say (to the user): "Suggested new Offer Name: `<computed_new_name>`. Click Apply to fill it in."
  4. Call `show_quick_choices(["Apply", "Cancel"])`.
  5. ONLY when user clicks "Apply": call `set_form_values({ name: "<computed_new_name>" })` (the form field key is `name`, NOT `offerName`), then call `get_form_values` to verify the update landed, then tell the user to click the Create button, then `show_quick_choices(["Done"])`.
  6. If `set_form_values` returns `success: false` with `unknownFields: ["name"]` or similar — the form schema differs. Stop and ask the user to change the Offer Name manually.
  7. Do NOT say vague things like "If you want, I can help you pick a new offer name" — that is a failure mode. Always propose a specific concrete name up front.

### 11. CSP Restriction Violations
- **Symptom**: Various schema or validation errors on cspPromotion offers
- `cspPromotion` can ONLY use `editExistingOfferPricingOnly` pricing type
- `cspPromotion` CANNOT use `vmSoftwareReservations` or `newCustomizedPlans`
- `cspPromotion` CANNOT have flexible billing
- `cspPromotion` MUST NOT have `expireTime`
- **Fix**: Adjust the offer to comply with CSP restrictions. Fixable by editing.

### 12. Per-User Pricing Missing User Limits
- **Symptom**: Schema validation error on per-user plan
- If `recurrentPriceMode` is `perUser`, `userLimits` object with `min` and `max` is required
- **Fix**: Add userLimits with min and max values. Fixable by editing.

### 13. Buyer Population Warning
- **Symptom**: Warning about buyer population (non-blocking, offer may still submit)
- This is a non-blocking validation; the offer can still be created
- If it becomes blocking, verify the beneficiary recipients have `acceptBy` property set

### 14. Upgrade Offer Issues (IMPORTANT — check early if offer name starts with "upgradeFrom_")
- **Symptom**: `Upgrade is not currently supported for this account`, `Plan not found`, or any error on an offer whose name starts with `upgradeFrom_` or has an "Azure Original Offer ID" set
- Azure has deprecated/restricted upgrade offers. **Upgrade offers are no longer supported for most accounts.**
- If the offer name contains `upgradeFrom_` or the offer has a non-empty `Azure Original Offer ID` / `azureOriginalOfferId`, this is an upgrade offer
- **Classification**: External issue — NOT fixable by editing. Do NOT suggest "Edit Draft".
- **Fix**: Create a new standalone private offer instead of an upgrade. Contact Suger support if the upgrade entry point needs to be disabled.

### 15. Plan Not Available in Market/Region
- **Symptom**: `The plan XXX for the offer YYY is not available in the IL market associated with the billing account. Please add IL to XXX`
- The plan's market availability does not include the buyer's region/country
- **Fix**: Add the buyer's market/region to the plan in Azure Partner Center, or use a different plan that covers that market. External issue — contact ISV to update plan availability.

### 16. Seller ID Invalid (CPPO)
- **Symptom**: `The seller ID XXXXX is invalid`
- The CPPO seller/reseller ID is not recognized by Azure
- **Fix**: Verify the seller ID. External issue — contact Azure/Suger support.

### 17. Agreement-Based Offer on CPPO Entitlement
- **Symptom**: Backend validation error about CPPO agreement
- Agreement-based offers (upgrades/amendments) CANNOT be based on a CPPO entitlement
- **Fix**: Use a non-CPPO entitlement as the base, or create a new standalone offer. External issue.

### 18. Azure Marketplace Integration Failed
- **Symptom**: `Azure Marketplace integration failed` or product status issues
- The Azure integration may be misconfigured or the product is in a bad state
- Products with status "attention needed" sometimes can still create offers (Azure API bug)
- **Fix**: Publish the product to PUBLIC status, or try using V2 pricing type. May require external action.

### 19. External / Transient Errors (NOT fixable by editing — STOP, DO NOT ENTER DRAFT)
- `Unknown server error` / `internalServerError` — Azure-side generic failure. The error is NOT in the form fields.
- `Upgrade is not currently supported for this account` — see #14 above. Azure no longer supports upgrades.
- `Professional services not available for purchase outside US/UK/Canada` — region restriction. Contact Azure support.
- `publisherId field invalid` — known Azure API bug. Retry or contact Azure/Suger support.
- `StatusCode=403` parse error — Azure API transient error. Retry the operation.
- `StatusCode=404 Publisher not found` — Azure API transient error or publisher configuration issue. Retry or contact Suger support.
- `acceptBy property missing in beneficiaryRecipients` — Azure API data issue. Contact Suger support.
- `Plan not found` on an upgrade offer — Azure cannot resolve the original plan for upgrade. See #14.
- Expired offer conflict — Azure didn't update expiration date causing conflict with new offers. Contact Suger support.

**CRITICAL for #19 — STOP rules (violating these creates fake diagnoses):**
- **DO NOT** call `invoke_action("edit_draft_offer")`. DO NOT suggest `["Edit Draft"]`.
- **DO NOT** call `get_form_values` to "double-check" the draft. The form is not the problem.
- **DO NOT** invent a field-level issue (e.g. "missing billing term", "pricing incomplete", "EULA URL suspicious") to justify entering the draft. If `errorMessages` does not name a specific field, there is no field to fix.
- Response must be: (a) classify as transient/external, (b) tell user to retry once, (c) if it fails again, contact Suger support with offer ID + Azure job ID, (d) `show_quick_choices(["Done"])`. Stop.

---

## Offer Types

### PRIVATE Offer (`customerPromotion`)
- Private offer directly to an end customer
- Created via `ValidateAzurePrivateOffer`
- `privateOfferType` = `customerPromotion` (default if not specified)
- `expireTime` is REQUIRED
- Billing Account ID: numeric or compound GUID, validated via Azure API
- Supports all three pricing types: `editExistingOfferPricingOnly`, `newCustomizedPlans`/`saasNewCustomizedPlans`, `vmSoftwareReservations`

### CPPO_OUT Offer — CSP Promotion (`cspPromotion`)
- Channel partner offer via Cloud Solution Provider
- Created via `ValidateAzureCppoOut`
- `privateOfferType` = `cspPromotion`
- `expireTime` MUST NOT be set
- Billing Account ID: tenant GUID format, validation skipped
- Can ONLY use `editExistingOfferPricingOnly` pricing type
- CANNOT use flexible billing

### CPPO_OUT Offer — Multiparty Originator (`multipartyPromotionOriginator`)
- ISV-originated multiparty private offer
- Created via `ValidateAzureCppoOut`
- `privateOfferType` = `multipartyPromotionOriginator`
- `expireTime` is REQUIRED
- Billing Account ID: compound GUID, validated via Azure API
- Supports all pricing types

### CPPO_OUT Offer — Multiparty Channel Partner (`multipartyPromotionChannelPartner`)
- Channel-partner-originated multiparty private offer
- Created via `ValidateAzureCppoOut`
- `privateOfferType` = `multipartyPromotionChannelPartner`
- `expireTime` is REQUIRED
- Billing Account ID: compound GUID, validated via Azure API
- Supports all pricing types

---

## Pricing Types — Strict Field Requirements

| Offer Pricing Type | Version | Plan Field | Forbidden Fields | Discount Types | Data Object | Flexible Billing |
|---|---|---|---|---|---|---|
| `editExistingOfferPricingOnly` | **V1** | `plan` (NOT `basePlan`) | `basePlan`, `newPlanDetails` | percentage OR absolute | `pricing` | NOT supported |
| `newCustomizedPlans` / `saasNewCustomizedPlans` | **V2** | `basePlan` (NOT `plan`) | — | absolute ONLY | `pricing` | Supported |
| `saasNewCustomizedPlans` | `basePlan` (NOT `plan`) | — | absolute ONLY | V2 (`2025-05-01`) | Supported |
| `vmSoftwareReservations` | **V3** | `plan` (NOT `basePlan`) | `basePlan` | absolute ONLY | `softwareReservation` (NOT `pricing`) | Supported |

---

## `privateOfferPlan` — Complete Required Fields Reference

**CRITICAL**: Every `privateOfferPlan` object MUST always include these top-level fields:
- **`$schema`**: Schema URI (auto-set by backend, but if wrong causes errors)
- **`product`**: e.g. `"product/b68f2539-01c7-4af2-a27d-5f4e50f5ac18"` (ALWAYS REQUIRED)
- **`plan`** or **`basePlan`**: depends on pricing type (see below) (ALWAYS REQUIRED)
- **`offerPricingType`**: must match the offer's pricing type

If `product` or `plan`/`basePlan` is missing from `privateOfferPlan`, Azure returns: `Required properties ["product","plan"] were not present`. This is a **backend bug** (payload construction), NOT fixable by editing the form. Contact Suger support.

### V1: `editExistingOfferPricingOnly` (SaaS / Container discount)

Schema: `2022-07-01`

```
privateOfferPlan: {
  "$schema": "...price-and-availability-private-offer-plan/2022-07-01",
  "product": "product/...",              ← REQUIRED
  "plan": "plan/.../...",                ← REQUIRED (NOT basePlan)
  "offerPricingType": "editExistingOfferPricingOnly",
  "pricing": {                           ← REQUIRED (NOT softwareReservation)
    "recurrentPrice": {                  ← for SaaS subscription/flat-rate
      "priceInputOption": "usd",         ← REQUIRED: "usd" or "perMarket"
      "recurrentPriceMode": "flatRate",  ← optional: "flatRate" or "perUser"
      "prices": [                        ← REQUIRED, at least 1 item
        {
          "billingTerm": { "type": "year", "value": 1 },    ← REQUIRED (V1)
          "paymentOption": { "type": "year", "value": 1 },  ← REQUIRED (V1)
          "pricePerPaymentInUsd": 1000                       ← REQUIRED if priceInputOption="usd"
        }
      ]
    },
    "customMeters": {                    ← for usage-based metering (optional)
      "priceInputOption": "usd",
      "meters": { ... }                  ← can be empty {}
    },
    "systemMeterPricing": {              ← for AKS/Container metering (optional)
      "priceInputOption": "perCore",
      "price": 10
    }
  }
}
```

**Key V1 rules:**
- Each `(billingTerm, paymentOption)` pair MUST match one from the **original plan**
- Does NOT use `contractDuration` or `billingFrequency` (those are V2)
- Does NOT support flexible billing
- If `recurrentPriceMode` is `"perUser"`, `userLimits` with `min`/`max` is REQUIRED

### V2: `newCustomizedPlans` / `saasNewCustomizedPlans` (SaaS custom plan)

Schema: `2025-05-01`

```
privateOfferPlan: {
  "$schema": "...price-and-availability-private-offer-plan/2025-05-01",
  "product": "product/...",              ← REQUIRED
  "basePlan": "plan/.../...",            ← REQUIRED (NOT plan)
  "offerPricingType": "newCustomizedPlans",
  "newPlanDetails": {                    ← REQUIRED
    "name": "Custom Plan Name"           ← REQUIRED
  },
  "pricing": {                           ← REQUIRED (NOT softwareReservation)
    "recurrentPrice": {
      "priceInputOption": "usd",
      "prices": [
        {
          "contractDuration": { "type": "year", "value": 1 },   ← REQUIRED (V2)
          "billingFrequency": { "type": "month", "value": 1 },  ← optional (V2)
          "pricePerPaymentInUsd": 1000
        }
      ]
    },
    "customMeters": { ... }
  }
}
```

**Key V2 rules:**
- MUST have `newPlanDetails.name`
- Uses `contractDuration` + `billingFrequency` (NOT `billingTerm`/`paymentOption`)
- Discount MUST be absolute (percentage NOT allowed)
- Supports flexible billing: `billingFrequency.type = "flexible"` → requires `flexibleSchedule`
- Flexible billing requires `contractDuration.type = "year"`

### V3: `vmSoftwareReservations` (VM offers)

Schema: `2025-05-01`

```
privateOfferPlan: {
  "$schema": "...price-and-availability-private-offer-plan/2025-05-01",
  "product": "product/...",              ← REQUIRED
  "plan": "plan/.../...",                ← REQUIRED (NOT basePlan)
  "offerPricingType": "vmSoftwareReservations",
  "softwareReservation": {               ← REQUIRED (NOT pricing)
    "reservationDuration": { "type": "year", "value": 1 },  ← REQUIRED
    "paymentSchedule": { "type": "month", "value": 1 },     ← REQUIRED
    "vmPrices": {                        ← REQUIRED, must not be empty
      "1Core": { "quantity": 1, "unitPricePerPaymentPeriodInUsd": 100 },
      "2Core": { "quantity": 1, "unitPricePerPaymentPeriodInUsd": 200 }
    }
  }
}
```

**Key V3 rules:**
- Uses `softwareReservation`, MUST NOT have `pricing` object
- `vmPrices` keys: `^([0-9]+Core|sharedCore|allCores)$`
- If `allCores` used, it MUST be the only key
- Valid (reservationDuration, paymentSchedule) combos: 1yr/1mo, 1yr/1yr, 3yr/3yr, 3yr/1mo, 1yr/flexible, 3yr/flexible
- Flexible billing: `flexibleSchedule` in each vmPrice item
- **Each `vmPrices` entry needs its own `reservationDuration`.** Even after the user adds a core-size row (via the "+ Add reservation" button), the row itself must carry `reservationDuration` — otherwise Azure returns `VM core size 'NCore': reservationDuration is required for VM offers`.

### V3 UI-state diagnosis (ALWAYS check before recommending "remove and re-add")

VM offers have a two-level UI that maps onto the JSON shape. The form hands you these top-level signals in `get_form_values`:

- `pricingPlansCount` / `hasPricingPlan` — whether the user has clicked **"+ Add plan"** at all
- Per-plan `reservationRowsCount` / `hasReservationRows` — whether the user has clicked **"+ Add reservation"** to add vCPU rows
- Per-plan `configuredSoftwareReservation` — the user's actual reservation input (reservationDuration, paymentSchedule, vmPrices). **This is what Azure validates.**
- Per-plan `catalogAvailableCoreSizes` — catalog-provided options the user can pick from. **Informational only — NOT the user's configuration.** Do not read this as "the user added a reservation."

Diagnose in this order and pick the matching action:

1. **`hasPricingPlan === false`** (no plan added yet) → tell the user to click the **"+ Add plan"** button in the Pricing Information section, pick the plan, then add reservation rows. Do NOT say "remove the existing plan" — there is none.
2. **`hasPricingPlan === true` but `hasReservationRows === false`** → tell the user to click the **"+ Add reservation"** button under the plan and add one row per vCPU size they want to offer (e.g. 1Core, 2Core, 4Core), entering quantity and unit price for each. Do NOT say "remove the plan".
3. **Rows exist but `configuredSoftwareReservation.reservationDuration` / `paymentSchedule` is missing** → have the user set Contract Duration (1-year or 3-year) and Billing Frequency (Monthly / Upfront / Flexible) at the plan level.
4. **Rows exist but a per-row `reservationDuration` is missing** (error references a specific `VM core size 'NCore'`) → for that row, populate `reservationDuration`. If the UI does not expose a per-row duration field and the plan-level duration is already set, this is a payload-construction bug — contact Suger support.
5. **Only after confirming 1–4 don't apply**, consider "remove the plan and re-add it" as a last resort for corrupted catalog data.

### Common `pricing` sub-objects (shared by V1 and V2)

**`recurrentPrice`** (subscription/flat-rate):
- `priceInputOption`: REQUIRED — `"usd"` or `"perMarket"`
- `prices`: REQUIRED, at least 1 item
- `recurrentPriceMode`: optional — `"flatRate"` (default) or `"perUser"`
- If `perUser`: `userLimits` REQUIRED with `min` >= 0 and `max` >= 0, `min` <= `max`

**`customMeters`** (usage metering):
- `priceInputOption`: REQUIRED — `"usd"` or `"perMarket"` (default: `"usd"`)
- `meters`: REQUIRED (can be empty `{}`)
- Each meter: `pricePerPaymentInUsd` (for usd) or `prices` array (for perMarket)

**`systemMeterPricing`** (AKS/Container):
- `priceInputOption`: e.g. `"perCore"`
- `price`: number

### Diagnosing `Required properties [...] were not present`

When Azure says required properties are missing in `privateOfferPlan`:
1. **Missing `product` and/or `plan`**: These are ALWAYS required. If missing, this is a **backend payload construction bug** — the backend code didn't copy these from `originalPlan`. NOT fixable by editing. Contact Suger support.
2. **Missing `pricing`**: V1/V2 types require a `pricing` object. Check if the backend populated it.
3. **Missing `softwareReservation`**: V3 type requires this instead of `pricing`.
4. **Wrong schema version**: `editExistingOfferPricingOnly` should use V1 (2022-07-01), but backend may have set V2 (2025-05-01). This is a backend bug.

---

## Private Offer Types — Summary Table

| Private Offer Type | Suger Offer Type | `expireTime` | Billing Account ID Format | API Validation |
|---|---|---|---|---|
| `customerPromotion` | PRIVATE | Required | Numeric or compound GUID | Yes — validated via Azure Billing API |
| `cspPromotion` | CPPO_OUT | Must NOT have | Tenant GUID | No — validation skipped |
| `multipartyPromotionOriginator` | CPPO_OUT | Required | Compound GUID | Yes — validated via Azure Billing API |
| `multipartyPromotionChannelPartner` | CPPO_OUT | Required | Compound GUID | Yes — validated via Azure Billing API |

---

## Validation Layers (Processing Order)

The backend validates Azure private offers in this exact order. An error at any step halts processing.

### Layer 1: Basic Structure + Default `privateOfferType`
- If `privateOfferType` is empty, default to `customerPromotion`
- Validate basic required fields exist (name, product, etc.)

### Layer 2: Product Status
- Product status must NOT be `RESTRICTED`, `PENDING`, or `DRAFT`
- Product must be in `PUBLIC` status to create a private offer
- If product is under review, creation will fail

### Layer 3: CPPO-Specific `privateOfferType` Validation
- For CPPO_OUT offers, validate that `privateOfferType` is one of: `cspPromotion`, `multipartyPromotionOriginator`, `multipartyPromotionChannelPartner`
- Validate CSP restrictions apply if `cspPromotion`

### Layer 4: EULA Validation
- `eulaType` of `CUSTOM` requires a non-empty `eulaUrl`
- `eulaUrl` may be either a public `https://...` URL OR a Suger internal file reference (`org/{orgId}/file/{hash}/{filename}`) produced by uploading a PDF. Both are valid — the backend resolves the file reference to a signed URL before submission.
- Standard (SCMP) EULA does not require a URL

### Layer 5: Date Validation
- Start date must be the first day of a month
- End date must be the last day of a month
- For PRIVATE offers (`customerPromotion`), dates are auto-corrected to first/last of month
- For CPPO_OUT offers, dates are NOT auto-corrected — must be exact
- `expireTime` rules depend on `privateOfferType` (see table above)

### Layer 6: Pricing Plan Validation
- Schema version is auto-set based on pricing type:
  - `editExistingOfferPricingOnly` -> V1 (`2022-07-01`)
  - All others -> V2 (`2025-05-01`)
- `plan` vs `basePlan` field must match the pricing type (see Pricing Types table)
- Discount type must be compatible with pricing type
- Flexible billing only allowed on V2 pricing types
- Per-user pricing requires `userLimits` (min, max)

### Layer 7: Billing Account ID Validation
- Format validation based on `privateOfferType`
- For `customerPromotion`: numeric or compound GUID format
- For `cspPromotion`: tenant GUID format, API validation SKIPPED
- For multiparty types: compound GUID format
- API validation calls Azure Billing API to verify the account exists and has a billing profile

### Layer 8: Buyer Population (Non-blocking Warning)
- Validates beneficiary recipients
- Missing `acceptBy` property generates a warning but does not block creation
- This is a non-blocking validation step

### Layer 9: Overlap Validation
- Offer name must be unique for the same billing account + product
- Term periods must not overlap with existing active private offers for the same billing account + product
- If overlap is detected, creation fails with a conflict error

---

## Real Production Error Examples

### Error: `#/pricing/0: Expected 1 matching subschema but found 0`

This is the single most common Azure private offer error. It means the pricing payload does not match ANY of the allowed schemas. Check these causes IN ORDER:

1. **plan vs basePlan mismatch**: The pricing type expects one field but the payload contains the other. Example: `newCustomizedPlans` with `plan` instead of `basePlan`.
2. **Discount type mismatch**: Using percentage discount with `newCustomizedPlans` (which requires absolute only).
3. **V1/V2 field mismatch**: Using `billingTerm`/`paymentOption` (V1) in a `newCustomizedPlans` payload (V2), or using `contractDuration`/`billingFrequency` (V2) in an `editExistingOfferPricingOnly` payload (V1).
4. **Missing required fields**: `newCustomizedPlans` missing `newPlanDetails.name`, or V1 pricing missing a valid `(billingTerm, paymentOption)` pair.

**Classification**: Fixable by editing. Identify which sub-cause applies and fix the specific field.

### Error: `DapiMissingBillingTermAndPaymentOption`

- **Root cause**: V1 pricing (`editExistingOfferPricingOnly`) is missing a required `billingTerm` + `paymentOption` pair. Each pricing entry must have a valid combination.
- **Fix**: Add the missing `billingTerm` and `paymentOption` fields.
- **Classification**: Fixable by editing.

### Error (VM): `ValidateAzurePrivateOffer pricing[0].SoftwareReservation: VM core size 'NCore': reservationDuration is required for VM offers`

- **Root cause**: A VM reservation row was added (the user clicked "+ Add reservation" and entered vCPU size / quantity / unit price), but the row is missing its `reservationDuration`. Each `vmPrices` entry needs its own duration.
- **Fix**: In Pricing Information, for the offending VM size row (e.g. "2Core"), confirm Contract Duration is set (1-year / 3-year). If the row-level duration is missing even though the plan duration is set, this is a payload bug — contact Suger support.
- **Classification**: Usually fixable by editing. Do NOT jump to "remove and re-add the plan" — first verify Contract Duration is selected.

### Error (VM, AI false positive): "Remove and re-add the plan" when user hasn't added one

- **Symptom**: Offer is `CREATE_FAILED` or the Create button is disabled, and the form shows `pricingPlansCount: 0` (or a plan with `hasReservationRows: false`).
- **Wrong response**: "Remove the current plan and re-add it." There is no plan to remove.
- **Correct response**: Guide the user to click **"+ Add plan"** (if `pricingPlansCount === 0`) or **"+ Add reservation"** (if the plan exists but has zero rows), then fill in vCPU size, quantity, and unit price. See "V3 UI-state diagnosis" above.

### Error (AI false positive): "Custom EULA URL is not a public web URL"

- **Symptom**: AI tells the user their Custom EULA URL is invalid because it looks like an internal file path (`org/{orgId}/file/{hash}/filename.pdf`).
- **Wrong response**: "Replace Custom EULA URL with a public https://... link."
- **Why it's wrong**: `eulaUrl` values starting with `org/` are Suger's internal file storage references produced when the user uploads a PDF. The backend resolves them to signed URLs before sending to Azure. These are NOT invalid URLs and NOT user-typed strings — the user literally clicked "Attach" and uploaded a file.
- **Correct response**: Ignore the `eulaUrl` — it's a valid uploaded file. Only flag it if `eulaType === "CUSTOM"` AND `eulaUrl` is empty, or if `eulaUrl` is a free-form non-URL non-`org/` string.

### Error: `Upgrade is not currently supported for this account`

- **Root cause**: The customer's Azure account does not support the upgrade path. This is an Azure platform limitation.
- **Fix**: Customer needs to contact Azure support to resolve the account limitation.
- **Classification**: External issue. Do NOT suggest "Edit Draft". Suggest contacting Azure support.

### Error: `billing account is invalid as there is no billing profile`

- **Root cause**: The billing account ID is valid, but the customer has not set up a billing profile in the Azure portal.
- **Fix**: Customer needs to create a billing profile in their Azure portal.
- **Classification**: External issue. Do NOT suggest "Edit Draft". Suggest the customer create a billing profile.

### Error: `Customer billing account ID has been updated`

- **Root cause**: The customer's billing account ID has changed (e.g., migration to a new billing account). The ID in the offer is stale.
- **Fix**: Get the new billing account ID from the customer and update the offer.
- **Classification**: Fixable by editing (once the new ID is obtained from the customer).

### Error: `Professional services not available for purchase outside US/UK/Canada`

- **Root cause**: Azure restricts professional services private offers to customers in the US, UK, and Canada.
- **Fix**: Cannot be fixed by editing. The customer is in an unsupported region.
- **Classification**: External issue. Do NOT suggest "Edit Draft". Suggest contacting Azure support.

### Error: `conflicts with an existing private offer for same billing account`

- **Root cause**: An active private offer already exists for the same billing account + product with an overlapping term period, or the offer name conflicts. Azure identifies the specific conflicting offer in the error message (look for the name after "conflicts with an existing private offer").
- **Fix — present all three options to the user in this order:**
  1. **Rename** the new offer to something distinct from the conflicting one (quickest)
  2. **Withdraw or modify** the existing offer via the Azure Partner Center
  3. **Shift the term** (Start Date / End Date) so it does not overlap with the existing offer
- **SELF-CONTAINED ERROR**: Do NOT also diagnose EULA, pricing, discount type, or any other field — the conflict message tells you exactly what's wrong. Extra "while you're at it" suggestions are almost always false positives on a form that was working before the conflict was introduced.
- **Classification**: Fixable by editing (name/term change) or requires withdrawing the existing offer.

### Error: `Charge dates cannot be repeated`

- **Root cause**: Flexible billing payment schedule contains duplicate charge dates.
- **Fix**: Remove or change the duplicate dates in the flexible billing schedule.
- **Classification**: Fixable by editing.

### Error: `publisherId field invalid`

- **Root cause**: Known Azure API bug. The publisher ID in the payload is rejected by Azure even though it is correct.
- **Fix**: Retry the operation. If persistent, contact Azure support or Suger support.
- **Classification**: External issue. Do NOT suggest "Edit Draft". Suggest retrying or contacting support.

### Error: `StatusCode=403` parse error

- **Root cause**: Azure API returned a transient 403 error. This is typically a temporary Azure-side issue.
- **Fix**: Retry the operation after a few minutes.
- **Classification**: External issue. Do NOT suggest "Edit Draft". Suggest retrying.

### Error: `acceptBy` property missing in `beneficiaryRecipients`

- **Root cause**: Azure API data structure issue where the `acceptBy` field is missing from the buyer population data.
- **Fix**: Contact Suger support to investigate the data issue.
- **Classification**: External issue. Do NOT suggest "Edit Draft". Suggest contacting Suger support.

### Error: Product under review

- **Root cause**: The product is not yet in PUBLIC status. It may be in DRAFT, PENDING, or RESTRICTED status.
- **Fix**: Wait for the product review to complete, or contact Azure support to check the product status.
- **Classification**: External issue. Do NOT suggest "Edit Draft".

### Error: Offer name doesn't match naming conventions

- **Root cause**: The offer name contains characters or patterns that violate Azure's naming rules.
- **Fix**: Update the offer name to comply with Azure naming conventions.
- **Classification**: Fixable by editing.

---

## CSP Promotion (`cspPromotion`) — Complete Restrictions

CSP promotion offers have the strictest limitations of all Azure private offer types:

1. **Pricing type**: ONLY `editExistingOfferPricingOnly` is allowed
2. **Forbidden pricing types**: `vmSoftwareReservations`, `newCustomizedPlans`, `saasNewCustomizedPlans`
3. **Flexible billing**: NOT supported
4. **expireTime**: MUST NOT be set (will cause validation failure)
5. **Billing account**: Tenant GUID format only, API validation is skipped
6. **Schema version**: V1 only (`2022-07-01`)

---

## Communication Rules (IMPORTANT)

**The user is a business person, NOT a developer.** Follow these rules when communicating:

1. **NEVER show raw JSON, schema URIs, or technical field paths** to the user. Instead, use the UI field labels they see on screen (e.g., "Pricing Plan", "Expiry Date", "Customer Billing Account ID").
2. **Give simple action steps**, not technical explanations. For example:
   - GOOD: "Please remove the current pricing plan and re-add it. This will refresh the pricing data."
   - BAD: "The privateOfferPlan is missing the `product` and `plan` fields. Please provide the Azure product ID..."
3. **When pricing data is incomplete or corrupted**: Tell the user to delete the plan and add it back, not to manually construct JSON.
4. **When a field value is wrong**: Tell them what to change it to in plain language (e.g., "Change the Expiry Date to a future date roughly 15 days from today, in YYYY-MM-DD format"). Do NOT suggest hardcoded future dates like `2026-06-15` — always compute relative to today.
5. **When it's a backend/system bug**: Say "This appears to be a system issue that cannot be fixed by editing the form. Please contact Suger support." Do NOT ask the user for technical data.
6. **When it's an external issue**: Explain what happened in simple terms and what the user should do (e.g., "Azure says your customer's billing account doesn't have a billing profile. Please ask your customer to create one in the Azure portal.").
7. **Customer-specific fields — NEVER invent a value.** For `billingAccountId`, `beneficiaries[*].id`, `azureOriginalOfferId`, tenant GUIDs, customer email, or any buyer-supplied identifier: ASK the user to provide it in chat, then call `show_quick_choices(["Apply", "Cancel"])` so the fix can be applied after they respond. Do NOT fabricate GUIDs or compound billing account IDs.
8. **Date fields — NEVER use a hardcoded far-future date.** For `expireTime`, propose a date roughly 15 days from today (YYYY-MM-DD). For `startTime` / `endTime`, respect Azure's first-day-of-month / last-day-of-month rules when applicable. Do NOT suggest literals like `2026-06-15` or `2026-12-31`.

## Workflow

1. Call `get_ui_context` immediately to understand the current page context.
2. **Read `metaInfo.errorMessages` — this is the authoritative source.** It's Azure's raw response and carries all the specific facts (exact conflicting offer names, billing account IDs, core sizes, timestamps, etc.). Every recommendation you make must trace back to something in here.
   - **IGNORE `metaInfo.prettifiedErrorMessages` for diagnosis.** That field is a generic, one-time LLM paraphrase the backend generates for email/Slack notifications. It drops specifics (e.g. the exact name of the conflicting offer), introduces generic suggestions ("refer to the documentation"), and sometimes misses valid fix options the raw error implies. Using it as your basis will give the user a watered-down, less accurate answer than reading the raw error yourself. You may re-read it if the raw error is completely unintelligible, but NEVER copy its language or its suggestion list wholesale.
3. **FIRST — classify the error before looking at the form.** Scan `errorMessages` for transient/external patterns (see checklist #19): `Unknown server error`, `internalServerError`, `StatusCode=403`, `StatusCode=404 Publisher not found`, `publisherId field invalid`, `Upgrade is not currently supported`, region restrictions. If matched → this is an EXTERNAL issue. Do NOT call `get_form_values`. Do NOT call `invoke_action("edit_draft_offer")`. Do NOT hunt for field-level problems. Output: retry guidance + support contact + `show_quick_choices(["Done"])`. Stop.
4. If a form is available AND the error is NOT transient/external: call `get_form_values`, compare actual field values against the rules above, propose a specific fix using **plain language UI instructions**. Your proposed fix MUST trace back to `errorMessages` — either (a) to a concrete substring/phrase in the raw error (schema path like `#/pricing/0`, exact offer name, exact billing account), or (b) to a documented Azure validation signal (`DapiMissingBillingTermAndPaymentOption`, `Expected 1 matching subschema`, `conflicts with an existing private offer`, etc.) whose fix is listed in the checklist above. If neither applies, you are hallucinating — stop and reclassify as external.
5. If no form AND the error is fixable: diagnose from the error message + offer data in non-technical terms, then offer `["Edit Draft"]` so the user can correct the field values.
6. For external issues (Azure platform errors, account issues, transient errors): do NOT show "Edit Draft". Instead, suggest the appropriate external action (contact Azure support, contact customer, retry, contact Suger support).
7. **Use UI field labels, not technical field names.** For example, say "Pricing Plan" not "pricingPlans", say "Customer Billing Account ID" not "beneficiaries[0].id".
8. **ONLY fix errors described in errorMessages — do NOT report other issues that are not causing the failure.** This rule is strict. If the error is "offer name conflicts with existing offer", your response must be about the conflict only. Do not also scan EULA, pricing, dates, billing account, or any other field for "while you're at it" improvements — those extra suggestions are almost always false positives and erode user trust.
9. **Self-contained errors — stop after matching.** These errors tell you exactly what's wrong; once matched, output the fix and stop diagnosing:
   - Offer name / term conflict (`conflicts with an existing private offer`) — see checklist #10
   - Upgrade-not-supported (`Upgrade is not currently supported for this account`) — see checklist #14
   - Account-level issues (`no billing profile`, `Customer billing account ID has been updated`) — see checklist #4
   - Region restrictions (`Professional services not available for purchase outside US/UK/Canada`)
   - Azure transient / server errors (`Unknown server error`, `internalServerError`, `StatusCode=403`, `publisherId field invalid`, etc.) — see checklist #19. For these, DO NOT enter the draft and DO NOT `get_form_values`.
10. When multiple errors exist in `errorMessages`, address them in the order of the diagnosis checklist above. But multiple errors are rare — Azure usually returns one at a time.
11. **If the fix involves removing and re-adding a plan or section**: Guide the user step by step (e.g., "1. Click the remove button next to the plan. 2. Click 'Add Plan' to add it back. 3. Select the same plan. 4. Try creating again.").

SHA-256: b63a2e63033281da5f06e4e2428b2db554b952203abc80b2ce1234a770a54200