# Working with Quotes — KISS system memory

> **Audience: an LLM.** Deep, technical companion to the human "Working with Quotes"
> playbook. Paste it into any assistant to give it an accurate model of how a KISS sales
> rep creates, edits, and voids a quote, what the customer experiences, and what the
> system does after signing.
>
> - **Surface:** Keep It Simple Storage (KISS), B2B self-storage access control — **hardware sales** path.
> - **Last reviewed:** 2026-06-19
> - **Owning systems:** HubSpot (the rep's entire surface), n8n workflows **S1** (generate/edit), **S4** (void), `apps/billing` (customer signing + bill), F21 (booking), the collection engine (F19/F23–F26) + customer-comms (F27/F28/F29), shared "kiss ops" Postgres (`quotes`, `quote_signatures`, `invoice_lifecycle`).
> - **Human version:** `/playbooks/working-with-quotes` in Hive.
>
> **Orientation:** the rep does everything in **HubSpot** by setting fields on the deal and its line items. The customer does everything on **billing.keepitsimplestorage.com**. No one uses HubSpot's native quote tool.

---

## 0. One-paragraph model

A rep sets fields on a HubSpot **deal** + its **line items**, then checks **`generate_quote`**. That fires n8n **S1**, which validates the deal, snapshots it into Supabase `quotes`, creates a **Billing Quote** custom object in HubSpot, and the customer gets a link to a self-hosted signing page. The rep changes a sent quote by setting the Billing Quote **`status = Edit`** and re-generating (S1 updates in place, same link). The rep cancels one with **`status = Void`** (S4 kills the public link). On signing, **F21** books the order(s); the collection engine drives payments and the F27/F28/F29 emails through paid-in-full → fulfillment.

---

## 1. The rep's three operations (all in HubSpot)

### 1.1 Generate — n8n **S1: Quote – Generate Link** (`8WMnuyKOUWc3cPHD`, webhook `POST /webhook/quote-generate`)
Fired by the deal checkbox **`generate_quote`** (a HubSpot workflow calls the webhook).

Rep inputs:
- **Per hardware (`1xx`) line item:** `requested_delivery_date` — **required** on every hardware line.
- **Deal `deposit_percent`** (optional) — populated = deposit flow, blank = pay-in-full. Number, percent-formatted, stored as a **fraction** (`0.25` = 25%), valid **20–99%**.
- **Deal `quote_expiration`** (optional) — custom expiry; null → defaults to 30 days (set by the Netlify function).
- **Deal↔Contact `Signer` association label** — resolves the signing contact.

What S1 does: validates the deal has a company + contact; resolves the signing contact (`Signer` label → `Primary` → first association); pulls deal/company/contact/line items; snapshots `terms.md` into `quotes.terms_text`; creates the Supabase `quotes` row; creates the HubSpot **Billing Quote** object (`2-60738190`); posts a Slack confirmation. Since Policy v1 it derives **payment type** from `deposit_percent` presence and snapshots `deposit_percent` + `requested_ship_date` onto the quote.

S1 **blocks** generation (→ Slack to the **fulfillment** channel) when:
- any hardware line is missing `requested_delivery_date`;
- any delivery date is **> 12 months** out;
- `deposit_percent` is set but **not 20–99%**, or the latest hardware delivery is **< today+90d** (inside 90 → pay-in-full only; a set deposit is rejected, not silently ignored at gen time).

### 1.2 Edit — S1 **Edit branch** (LIVE 2026-06-19, version `51c89117`)
Set the existing Billing Quote's **`status = Edit`** (case-insensitive), change deal/line-item fields, re-check `generate_quote`. S1 detects the Edit-flagged quote via the deal's `2-60738190` associations, passes that `quote_id` to `quote-generate` which **updates in place (same ULID/link)**, PATCHes the Billing Quote back to **`Pending`**, and posts a "Quote Updated" Slack.

Guardrails: **>1 Edit-flagged quote**, or a target that is **signed/expired** → 409 → fail Slack. Only **unsigned** quotes are editable.

### 1.3 Void — n8n **S4: Quote Void** (`tS3Huc3GZSfMoo7F`, webhook `POST /webhook/quote-void`)
Set the Billing Quote **`status = Void`** → HubSpot workflow → S4 stamps `quotes.voided_at`; the public link then returns **HTTP 410** (`quote-get`, `reason=void`, "no longer available") + a "Quote voided" Slack. **Un-void:** set `status = Pending` → clears `voided_at`. A **signed** quote is never voided (link protected) → "void blocked" Slack. (Webhook is `?key=`-guarded because HubSpot workflows can't send custom headers.)

---

## 2. The rules that shape a quote

- **`requested_delivery_date`** (line item, custom date, required on hardware) is the load-bearing input. The **latest** hardware delivery date becomes the quote's **`requested_ship_date`** — the anchor the payment schedule counts back from (T−90 check-in, T−30 balance).
- **90-day deposit gate:** deposits (`deposit_percent` 20–99%) are only allowed when the latest delivery is **≥ today+90d**. Inside 90 days = pay-in-full only.
- **All deliveries ≤ 12 months out.**
- **Tranches:** multiple distinct `requested_delivery_date`s on hardware lines → the order splits at booking (F21) into one order per date — own inFlow SO, EasyShip shipment, `invoice_lifecycle`, and collection schedule — all sharing the quote ULID. Booked rows upsert `ON CONFLICT (quote_id, requested_ship_date)` (NULLS NOT DISTINCT, migrations 091–093); a later tranche's full payment is due **ship−45**.

**Payment schedules** (counted back from `requested_ship_date`):
- *Pay in full* — one payment, due at signing.
- *Deposit* — (1) deposit % at signing, (2) check-in ~T−90 bringing cumulative to ~60%, (3) balance at T−30.

---

## 3. Data model

### HubSpot — Billing Quote object (`2-60738190`)
`quote_name`, `quote_id` (ULID), `quote_url`, **`status`** (`Pending`/`Signed`/`Expired`/`Edit`/`Void` — title-cased; consumers compare case-insensitively), `expires_at`, `payment_track` (`Payment in full`/`Deposit`, derived by S1), `deposit_percent` (fraction), `requested_ship_date`. Associated to Deals + Companies. F21 flips `status` → `Signed` after signing.

### HubSpot — Deal properties
`generate_quote` (checkbox; fires S1), `quote_expiration` (optional date; null → 30d), **`deposit_percent`** (the ONLY flow input since Policy v1 2026-06-12; the old `payment_track` / deal-level `requested_ship_date` staging fields are retired). S1 reads `dealname`, `amount`, `hubspot_owner_id`, `deal_comments`, `quote_expiration`, `deposit_percent` + the `companies`/`contacts`/`line_items` associations.

### HubSpot — association labels & line items
Deal↔Contact: **`Signer`** (S1 resolves the signing contact; F21 re-applies post-sign) and **`Billing Contact`**. Line-item fields: `name`, `hs_sku`, `quantity`, `price`, `amount`, `recurringbillingfrequency`, `requested_delivery_date`. **Gotcha:** `price` is list/MSRP; discounts live only in `amount` → always compute effective unit price as `amount / quantity`.

### Supabase (kiss ops Postgres)
`quotes` (canonical per-option snapshot incl. `terms_text`, `voided_at`, the two-track fields) and `quote_signatures` (signing data, audit trail, `hw_payment_method`, Stripe ids). A deal can spawn multiple quotes/options; the **signed one becomes the order**.

---

## 4. The customer side (`apps/billing`, billing.keepitsimplestorage.com/quote/{ULID})

Self-hosted 5-step wizard (no HubSpot native quote): **Review → Details → Order → Sign → Billing**. E-signature = type-name + consent (server-side audit trail, no third-party). After signing the URL becomes a **read-only signed view that is also the bill**: a Delivery & Payment Schedule card (milestones, due dates, amounts, paid ✓, progress, remittance text from `PAYMENT_REMITTANCE_TEXT`) + PDF download.

Pay lanes on the bill: **card** → Stripe (PM captured at signing for Credit-Card hardware; **F24** off-session charges per milestone) ; **ACH** → `pay-echeck` → **F25** WEB eCheck → **F26** settles. The signer can **route payment** to a billing contact ("Our billing department will handle payment") → `quote-route-payment` → **F27**. PDFs via `pdfkit` (`quote-pdf`, renders pending + signed from one template).

---

## 5. After signing (no rep action)

**F21: Order Booking (tranche)** (`TR2lWvjE4SwEDnnV`, `POST /webhook/billing-quote-signed`, Header-Auth `KISS Webhook Secret`) — the single post-sale booker since 2026-06-19 (absorbed F9). Groups hardware by `requested_delivery_date` into tranches; per tranche creates inFlow SO + EasyShip shipment + `invoice_lifecycle` row + collection schedule; derives `payment_track` (`deposit_percent`>0 → deposit, else full); resolves/creates QBO + inFlow customer; HubSpot ID writebacks; company→Customer; deal→Closed Won; Stripe-id writeback; opens onboarding ticket; applies `Signer`/`Billing Contact` labels. **No QBO invoice at signing** (two-track; F19/F22 reconcile). Software-only quotes (all `2xx`, no hardware) book nothing.

**Collection engine:** F19 (QBO payment/sales-receipt ingest, FIFO-matches `order_collection_milestones`), F24 (card), F25/F26 (ACH eCheck), F23 (push lane). **Two-track GL:** deposits → deferred `2300` (QBO item 364), pay-in-full → `2200` (F22); AR recognized at fulfillment (F3). **Customer emails:** welcome (01) on signing from **Marc / onboarding@**; **F27** payment-request (routed), **F28** receipt + paid-in-full, **F29** daily ACH due reminder (~10a ET) — emails 02–05 present to the customer as **KISS Accounting / accounting@**, **BCC michael@**, replies → accounting@.

> ⚠️ The exact mail transport + sender identity for emails 02–05 changed in the 2026-06-18 finance-email cutover (Mailgun "KISS Accounting"); some n8n notes still say Gmail `marc@`. Verify against F27/F28/F29 + the finance-email config before treating transport details as canonical.

---

## 6. Glossary / IDs

- **Billing Quote** — HubSpot object `2-60738190`; `status` drives generate/edit/void.
- **S1** `8WMnuyKOUWc3cPHD` — generate + edit. **S4** `tS3Huc3GZSfMoo7F` — void. (Named S4; an S3 already existed.)
- **F21** `TR2lWvjE4SwEDnnV` — booking. **F9** (retired 2026-06-19, kept for rollback).
- **ULID** quote id; **`ship − 45`** later-tranche due date; **item 364** = Hardware Deposit (QBO); **2300** deferred rev, **2200** pay-in-full booking.
- **`generate_quote`** deal checkbox = the one trigger for both create and edit.

---

## 7. When this file is wrong, fix these

Update this `.md` and the `.html` in the **same PR** as the change:

- `docs/hubspot.md` — the Billing Quote / deal / line-item properties, status enum, association labels.
- **S1** (`n8n/s1-quote-generate.json`) — generate validation, signer resolution, deposit gate, the Edit branch.
- **S4** (`n8n/s4-quote-void.json`) — void/un-void + signed-protection.
- `apps/billing` — the wizard steps, the signed-view-as-bill, the pay lanes.
- **F21** + the collection engine (F19/F23–F26) + **F27/F28/F29** — booking and the customer emails (esp. the mail-transport caveat above).
- The `(quote_id, requested_ship_date)` tranche uniqueness guard.
