# CPQ — Configure Price Quote

_Finance_

CPQ is CRMown's structured quoting engine for orgs that sell configurable products: a product catalog with options + bundles, dynamic price rules (volume discounts, percent off, tier-based pricing), versioned quotes with line items, branded PDF rendering via Playwright, public share links, e-signature capture (type or draw), and one-click conversion to a Finance invoice on acceptance. Distinct from Proposals (which is freeform) — CPQ is for repeatable line-item-and-rule pricing where the catalog matters. Plan-gated to Max and Own.

## Steps
1. Tools → Quotes in the sidebar (/dashboard/cpq) — the sidebar entry reads Quotes, not CPQ. It opens on the Quotes tab, which lists quotes with status filters (All, Draft, Pending approval, Choose approver, Sent, Viewed, Accepted). The Products and Price rules tabs sit next to it for admins and owners only — on a non-admin login the Quotes tab is all you see. If the page instead reads “Quotes are part of the Enterprise suite”, activate Enterprise under Settings → Billing.
2. Products tab: build your catalog. Each CpqProduct has name, SKU, basePrice, currency, taxable, productType (standard / configurable / bundle / service), category, and configOptions (e.g. "color: red/blue/green", "tier: small/medium/large" with per-option price impact).
3. Price rules tab: define discount logic. Each CpqPriceRule has ruleType (volume_discount / percent_discount / fixed_discount / tier_based), conditions (e.g. {"quantity": {"gte": 10}}), effectConfig (e.g. {"discountPercent": 15}), and an optional productId (null = applies globally to all products). Set requiresApproval=true on a rule to gate matching quotes through the Approval & Review Process.
4. Quotes tab > "+ New quote": pick a contact (or enter ad-hoc customer details: name, email, company, address), then add line items. Each line item references a CpqProduct, has quantity, optional manual discount + reason, and any selected configOptions.
5. The pricing engine fires on every save: walks each line item, applies matching CpqPriceRules in order, then any manual line-item discount, then quote-level adjustments. Result is a structured pricing breakdown shown live in the builder UI — subtotal, discounts, tax, grand total — all rounded to 2 decimal places at the engine boundary.
6. When the quote's status moves out of "draft" (e.g. you click "Send"), the system checks for triggered approval rules. If a CpqPriceRule with requiresApproval=true matched, an ApprovalProcess gets auto-resolved and the quote transitions to pending_approval (1 matching process), pending_approval_choice (multiple processes — you pick one in a banner), or stays draft with a warning (no matching process configured).
7. Send the quote: enter the customer email and click Send. The PDF renders via Playwright, gets uploaded to R2, and the customer receives an email with a public share link (/share/quote/<token>). They view, sign (type their name OR draw the signature on a canvas), and accept right in the browser.
8. On accept: the quote transitions to "accepted", a Finance invoice is auto-created with the same line items pre-filled (you can convert manually before that via "Convert to Invoice"), and the customer / your team get acceptance notifications. The accepted signature blob (PNG for drawn, name string for typed) plus IP + user-agent are captured server-side for audit.
9. Use Revise to spin up a v2 of an existing quote — copies all line items + customer details into a new draft you can edit. Original quote stays unchanged for audit; the new one references it via parentQuoteId.

## Tips
- CPQ products and price rules are sandboxable — make changes in a sandbox first (Enterprise > Sandbox), test against cloned quote data, then promote selected products / rules back to production. This is the recommended flow when rolling out new pricing.
- Configurable threshold for approval: don't hardcode "anything over 25% off needs approval." Build a global CpqPriceRule with productId=null + requiresApproval=true + approvalThreshold to express that as actual configuration. The pricing engine reads it at quote time.
- Quote numbers are race-safe: an OrgQuoteCounter table holds the per-org per-year counter. Generation uses an atomic upsert-with-increment so two simultaneous "Create quote" requests can't collide on the same number.
- Public share token is a cuid stored on the Quote row at create time. Treat it as the auth credential for /share/quote/<token> — anyone with the link can view + accept. Disable a leaked link by revising the quote (which invalidates the parent's token).
- PDF rendering uses Playwright (already installed for the Lead Magnet feature). The HTML template renderQuoteHtml lives in cpq/pdf-renderer.ts. Customizing the template applies to all future PDF renders — re-render existing quotes via Revise.
- E-signature is E-SIGN Act compliant: the typed-name path uses the customer's typed full name as the signature; the draw path captures a base64 PNG of the canvas. Both are stored in Quote.acceptedSignature alongside IP + user-agent + timestamp.
- Quote line items support a parentLineItemId — used for bundle children. The parent line item references a "configurable" or "bundle" CpqProduct; the children reference the bundle's component products. Bundles can be priced as a single line or itemized.
- Discount on a line item supports two modes: discountPercent (e.g. 10 = 10% off) OR a fixed discount amount via discount + discountType="amount". The pricing engine resolves whichever is set; both being set is rejected at validation time.
- CPQ quotes show locked-pending-approval banners when an ApprovalRequest is open against them. The lock is enforced at the API layer (PATCH on a locked quote returns 423 Locked) — UI just renders the banner. Same lock pattern as PipelineItem under Approvals.

## Example
An MSP sells a tiered "infrastructure monitoring" service: Standard / Pro / Enterprise tiers, each with optional add-ons (24/7 ops, log retention upgrade, custom alerting). The RevOps lead builds a configurable CpqProduct with the three tiers as configOptions, plus 4 line-item add-ons as separate CpqProducts. They write 3 CpqPriceRules: 10% off for >$50k annual contract, 15% off for >$100k, and a global "any discount over 20% needs CFO approval" rule. A new prospect comes in for a $124k 3-year deal — the rep builds a quote in 4 minutes (pick tier, add 2 add-ons, enter quantity 36 months), the engine applies 15% volume discount automatically, the rep adds an extra 7% manual discount with reason "competitive deal — Datadog quote was 10% lower." Effective discount: 22%, which trips the global "needs approval" rule. The quote auto-routes to the CFO; she approves in 12 minutes. The rep clicks Send, customer e-signs that afternoon, invoice auto-creates. Net cycle: 6 hours from quote-build to signed contract.

## Troubleshooting
- **"CPQ access required" / "Plan-tier gated"** — CPQ is Max and Own only. Free / Starter / Pro orgs see the upsell. Fix: Settings > Billing > upgrade to Max plan (or buy an Own license). The CPQ tab appears in the sidebar as soon as the plan flips.
- **Quote total is wrong / off by a few cents** — Floating-point math in the pricing engine — the engine rounds to 2 decimal places only at the final boundary, which is correct, but if you're manually adding line-item totals you may not match the rounded grand total. Fix: Trust the engine's grand total — it's the authoritative value. If you need to verify, check the structured PricingResult returned by /cpq/quotes/<id>/pricing — it shows subtotal, per-line discount, total discount, tax, and grand total separately.
- **Quote stuck at "pending_approval_choice"** — Multiple ApprovalProcesses match the quote (pricing engine fired a discount rule with requiresApproval=true, but the auto-resolver couldn't pick a single applicable process). Fix: Open the quote — there's a banner that lists the matching processes. Pick one and click Submit. The backend creates the ApprovalRequest and transitions the quote to pending_approval. To prevent this in the future, narrow the entityType / triggerCriteria on your ApprovalProcesses so only one matches per quote.
- **"423 Locked" when trying to edit a quote** — The quote has an active ApprovalRequest open against it — the lock guard blocks PATCH/DELETE while approval is pending. Fix: Wait for the approval to be approved/rejected/withdrawn — the lock auto-releases. Or withdraw the ApprovalRequest first (if you submitted it and changed your mind).
- **Public share link returns 404** — Quote was deleted, or the publicShareToken was invalidated by a Revise (which generates a new token on the new version). Fix: If the quote was revised, find the latest version and share that link instead. If deleted, restore from a backup or rebuild.
- **Customer typed their name but accept button stays disabled** — The signature capture component (SignatureCapture.tsx) requires a non-empty typed name OR a non-empty drawn signature before allowing accept. If both are empty, the button is disabled by design. Fix: Tell the customer to either type their full name in the field or draw a signature on the canvas. The "Type" tab is the default; switch to "Draw" if they prefer.
- **Acceptance saved but no invoice created** — Auto-conversion triggers on acceptedAt being set; if the finance.service.createInvoiceFromQuote call failed silently (e.g. customer email malformed), the invoice never lands. Fix: Open the accepted quote, click "Convert to Invoice" manually. Check that the customer email is well-formed first — invoice creation requires it for sending.

## Error messages
- `cpq_access_required` — Your plan doesn't include CPQ. Upgrade to Max or buy an Own license.
- `quote_locked` — An ApprovalRequest is open against this quote — edits are blocked until the approval resolves. HTTP 423.
- `pricing_error: rule_match_failed` — A CpqPriceRule's conditions JSON couldn't be evaluated — likely malformed (missing operator, wrong field name). Fix the rule's conditions in the Price rules tab.
- `quote_status_invalid_for_action` — You tried an action that doesn't apply to the quote's current status (e.g. accept on a draft, send on an accepted quote). Check the quote's status before retrying.
- `public_token_not_found` — The /share/quote/<token> URL is invalid — quote may have been revised (which generates a new token) or deleted.
- `signature_required` — Customer tried to accept without providing a typed name or drawn signature. The frontend should disable the button before this fires server-side.

## Plans
Max plan: full CPQ access. Own license: full CPQ access. Free / Starter / Pro: not available — upgrade prompt shown. The /cpq/* API surface is gated server-side via checkCpqAccess. Permissions: cpq:read (broad), cpq:write (TEAM+), cpq:admin (OWNER/ADMIN/EXECUTIVE/DIRECTOR — for product + rule management).
