# Revenue Accounting — KISS system memory

> **Audience: an LLM.** Deep, technical companion to the human "Revenue Accounting" playbook. Paste it into any assistant to give it an accurate model of how a customer hardware order moves across KISS's books from signing → collection → revenue recognition → commission/royalty payout — **including a walkthrough of every collection rail (card, ACH, wire, check, and invoiced/AR)** — plus what a person does by hand vs. what posts automatically, and where to verify each step.
>
> - **Surface:** Keep It Simple Storage (KISS), B2B self-storage access control — the **sales → deposits → revenue → commission/royalty** path. The revenue-side twin of the **Inventory Accounting** playbook (the COGS/inventory side).
> - **Last reviewed:** 2026-08-03
> - **Owning systems:** HubSpot (quote/deal fields), the billing app (`apps/billing`, customer signing + bill), QuickBooks Online (the GL), the collection rails (Ramp/ACH/Stripe), Gusto (commission payout). n8n workflows **F21** (booking), the collection engine + **F22** (paid-in-full), **F3** (revenue recognition), **F1/F2** (commission/royalty accrual + payout), **F15/F16/F19** (QBO invoice/payment sync + ingest), **F23/F28/F31** (receipt posters). Shared "kiss ops" Postgres (`invoice_lifecycle`, `order_collection_milestones`, `order_collection_payments`, `order_collection_charges`, `quotes`).
> - **Human version:** `/playbooks/revenue-accounting` in Hive. (Renamed from `hardware-revenue` on 2026-08-03 when the per-rail collection walkthroughs were folded in; the standalone `card-payments` playbook was absorbed at the same time so there is exactly one revenue-accounting document.)
> - **Governing principle:** *their money is in before ours goes out.* Customers fund their orders; KISS never finances a deal with working capital; **Accounts Receivable means only "shipped and owed."** As of 2026-07-30 this is enforced by exactly ONE gate — nothing ships until the order is paid in full (§1) — rather than by staged checkpoints counted back from the delivery date.
>
> **Orientation:** QuickBooks is the GL (actuals only). Hive/Supabase is the operational tracker (`invoice_lifecycle` = the rev-rec spine + the milestone schedule + the payments ledger + the JE log). Almost all the bookkeeping posts itself; §2A is the per-rail detail and §9 is the short list of what's still manual. This doc supersedes the retired Notion "Sales & Payment Accounting" + "Hardware Revenue System" pages.

---

## 0. One-paragraph model

A quote is signed (no invoice — the signed quote page **is** the bill). **F21** books the order onto the rev-rec spine (`status='booked'`), creates the Inflow SO + HubSpot Order, and generates the milestone schedule. Customers pay against the quote page; each receipt posts as a QBO **Sales Receipt (item 364 → Customer Deposits 2300)** — `Dr Cash / Cr 2300`, **no AR**. When every milestone is covered, **F22** flips the order paid-in-full and reclasses **`Dr 2300 / Cr 2200 Deferred Hardware Revenue`**. At **fulfillment**, **F3** creates the QBO **fulfillment invoice** (taxable SKU lines → 4100 Hardware Revenue + AST sales tax + a negative settlement line) and posts a **trimmed JE** (COGS + commission + royalty release). The settlement line is where the two variants diverge: a **paid-in-full** order draws down **2200** (item 372) so the invoice nets to ~$0 (no AR); an order **fulfilled before it's paid** (Variant B) draws down the collected **2300** (item 364) and leaves the remainder as **True AR (1200)**, which later payments clear. Commission/royalty run on a **three-leg model**: **accrue** at paid-in-full (F1: `Dr Deferred / Cr Accrued Payable`), **expense** at fulfillment (F3: `Dr COGS / Cr Deferred`), **pay out** via Gusto (`Dr Accrued Payable / Cr Cash`). The **Accountant** owns the ledger; the routine JEs auto-post; their day-to-day is the SO Checks tie-out + clearing exceptions.

---

## 1. The Payment & Delivery Policy (customer-facing contract)

**One rule: nothing ships until the order is paid in full.**

That is the entire policy. Sales records `payment_due_date` on each hardware line in HubSpot and those dates ARE the schedule; a blank one means due on signing. Delivery is a separate, later concern — onboarding sets `shipping_deadline` on the ticket after kickoff, and it moves fulfillment only, never money.

### Why it is only one rule (rewritten 2026-07-30)
The old policy staged the money against the delivery date: a deposit at signing, a check-in at ~T−90 that billed to 60% cumulative and "went hard", a final invoice at ~T−30, and a block on taking a deposit at all unless delivery was 90+ days out. **Every one of those dates was invented by the system**, not agreed with the customer, and the structure existed to keep customer cash ahead of our committed cost.

Paid-in-full-before-shipment achieves that on its own. The cash is in before a single component leaves the building, which is the outcome the staged checkpoints were approximating. So the checkpoints are gone and the restriction they stood in for is enforced once, at the point where it actually bites: fulfillment.

**This is deliberately the loosest workable version.** If it starts to create cash-flow pressure — the component pool running ahead of collections, or deposits sitting thin against committed POs — we add restrictions *then*, against evidence. We are not pre-building gates for a problem we do not have.

### Deposits
Sales sets `deposit_percent` (20–99%) and optionally `deposit_due_date` on the deal. A deposit is now purely a **schedule shape, not a gate**: it is the first milestone on the order, and the rest falls on the declared payment dates. There is no minimum-runway rule for taking one.

### Tranches (one signature, multiple payment dates)
Distinct `payment_due_date`s on the lines split the order into tranches — each its own shipment, schedule, and invoice tied to the same signed quote. A tranche is billed on its own declared payment date, not a computed offset from delivery. **The paid-in-full rule applies per tranche:** a tranche ships when that tranche is paid.

### Lines that still do not move
- **Paid in full before anything ships.** Rare exceptions need Michael's sign-off, logged + counted — this is the Variant B population (§3.3).
- Payment dates within 12 months. Rush (< 30 days) = 15% fee, only if confirmable.

### The Funding Rules (finance derivation behind the policy)
1. **Nothing ships unpaid** — per shipment/tranche. This is the load-bearing rule, and now the only gate.
2. **The pool stays customer-funded** (aggregate) — customer cash held pre-ship ≥ component pool (1350 + 1360); the "Pool funding coverage" card on the 13-Week page. This is a **monitor, not a gate**. It is the signal that would justify adding a restriction back, and the reason we can afford to run with only one.

*Retired:* the third rule, **their money leads ours** with ≥20% / ≥60% checkpoints at fixed offsets from delivery. Rule 1 subsumes it — with the order fully collected before it ships, cumulative customer cash necessarily leads committed cost at the only moment it can hurt us.

---

## 2. How an order runs — the machinery (all automated)

**No QBO invoice at signing, for either flow.** The signed quote page is the bill.

| Stage | What happens | Books impact |
|---|---|---|
| **Quote** | Rep fills line items + per-line `payment_due_date` (optional; blank = due on signing) + `deposit_percent` / `deposit_due_date` (deposit flow only). S1 validates + snapshots to the quote. | — |
| **Signing** | The billing app fires **F21: Order Booking** (which absorbed F9 and owns the webhook) — books the order (`status='booked'`, no invoice), creates Inflow SO + HubSpot Order, generates the milestone schedule, Slacks AR. | **Nothing — zero AR, zero revenue.** |
| **Schedule** | Built from the declared `payment_due_date`s (migration 161). No dates → one milestone, full, due on signing. Deposit → `deposit` on `deposit_due_date` + the balance on the declared date(s). The invented delivery−90 / delivery−30 offsets are retired. | — |
| **Collections** | Customer pays against the quote page. Each receipt → QBO **Sales Receipt, item 364, quote ULID in the memo**. **F19** ingests + FIFO-matches to milestones (2h). **F20** digests dues + exceptions each weekday. | `Dr Cash / Cr 2300 Customer Deposits`. **No AR.** |
| **Paid in full** | **F22** (hourly) detects all milestones covered. | `Dr 2300 / Cr 2200 Deferred Hardware Revenue`. Commission payout becomes eligible. |
| **Fulfillment** | Inflow ship → **F3** creates the fulfillment invoice + trimmed JE (see §3). | Invoice books revenue + draws down the deposit; trimmed JE books COGS + comm/royalty. |
| **Comp** | **F1** accrues monthly (paid-in-full basis); **F2** computes payouts; paid via Gusto coded to 2150/2160. | See §4. |

---

## 2A. Collection rails — one walkthrough each

**Every rail ends the same way: a QBO Sales Receipt using item 364 ("Hardware Deposit" → income account 2300) with the quote ULID in the memo.** That shared ending is what gives one reconciliation path instead of four. What differs is only the **deposit account** and how much is manual.

| Rail | Deposits to | Poster | Ledger `payment_method` | Manual step |
|---|---|---|---|---|
| **Card** | 1130 Stripe Clearing (AcctNum `1130`, Id `168`) | **F31** | `sales_receipt` | categorize the payout only |
| **ACH** | 1115 Ramp Operating (Id `186`) | **F25** → **F26** | `sales_receipt` | none (watch `WITHHELD`) |
| **Wire** (non-US) | 1110 Operating (Id `11`) | **F40** | `wire` | record it + resolve bank fees |
| **Check / other** | 1110 Operating (Id `11`) | **F40** | `check` / `other` | record it |
| **Invoiced order** | n/a — collects through **AR (1200)** | **F19** | QBO method name or id | receive against the invoice in QBO |

⚠️ **An order carrying a QBO invoice must never get an item-364 Sales Receipt** — that books the cash twice (once against the invoice, once to 2300). `orders_record_outside_payment` blocks this explicitly and its error message points to QuickBooks; F19 then ingests the QBO Payment on its 2h poll.

Accounts are resolved **by AcctNum at runtime**, never by hardcoded QBO Id, so a re-key in QuickBooks can't silently redirect cash. F26, F31 and F40 all do this.

### 2A.1 Card (Stripe)

**Customer-present only.** `quote-card-intent.js` mints a Stripe PaymentIntent with a **server-derived** amount from the open milestone; `quote-card-finalize.js` captures it and flips `order_collection_charges.status → CAPTURED`. Auto-pull off a stored card was **retired 2026-06-26** (off-session 3DS hard declines); the recurring premium subscription is the only kept-on-file instrument.

**F31** (`iaf84LXogY6o35NB`) then does the only GL entry that records the sale:
- atomically claims the post (`order_collection_charges.receipt_posted_at IS NULL → now()`, migrations 084 + 114) so its webhook and its 15-min backstop can both fire — one wins, the other no-ops;
- posts the **item-364 Sales Receipt into 1130** at the **gross** charge amount → `Dr 1130 / Cr 2300`;
- inserts `order_collection_payments` against **the charge's own `invoice_lifecycle_id`** (not a quote-wide join — tranche-safe), whose trigger runs `orders_match_collections` and stamps the milestone `actual_date`;
- stamps `qbo_salesreceipt_id` and fires **F28** (customer receipt).

The charge **stays at `CAPTURED`**: `apps/orders`, `apps/cash-flow` and the bill page all read `CAPTURED` as "collected, don't chase", so a settled-status flip would vanish the order from those surfaces. That's why idempotency rides a column, not a status. Authoritative "paid" is the milestone `actual_date`, never the raw Stripe capture.

**The payout is a separate, later, manual event.** Stripe batches charges and pays out **net of fees** to 1110. Categorize that bank deposit as a **Transfer from 1130 Stripe Clearing**, fee → **6390**. June 2026 is the reference implementation (transfers `11511, 11556, 11584, 11697, 11719, 11766, 11789, 12133, 12134`, one per `po_…`).

**Monthly tie-out.** 1130 takes charges in gross and pays out net, so fees accumulate as a residual — journal them out to 6390 monthly, then verify:

> **1130 balance = card charges collected but not yet paid out.**

A balance materially larger than a few days of charges means payouts aren't being cleared against it. That single check is what would have caught §2A.6 in week one.

### 2A.2 ACH (US bank)

Push/self-serve only — the customer re-enters bank details per payment on the bill page; no stored-bank auto-debit (**F30 retired 2026-06-26**, same reasoning as card auto-pull).

**F25** (`uB57qwHEVmoUhK5i`) submits a WEB eCheck and records `order_collection_charges` `PENDING`, then emails a "payment received, processing" note. **F26** (`XHpIVElLiNDfkLw8`) polls every 6h and, on `SUCCEEDED`, posts the item-364 receipt to **1115 Ramp Operating** → `Dr 1115 / Cr 2300`, flips the charge to `SETTLED`, inserts the payment row (matcher stamps the milestone) and fires F28.

- **Settlement takes 1–5 business days**, and that window is surfaced deliberately: Orders shows a **⏳ Processing** pill and the order is excluded from the Attention chase list (`inFlightCharge`), because the team was chasing customers who had already paid. Processing is **ACH-only** — a captured card is money already in hand.
- ⚠️ **`WITHHELD` never auto-clears.** An Intuit risk hold stays `WITHHELD` **even after the processor funds it** — confirmed on a funded $11,448.04 eCheck. F26 keeps it `PENDING` and Slack-alerts on every poll. Clearing one is a documented manual force-settle procedure; **ask before running it**.
- ⚠️ **QBO Payments API needs a different credential** from the Accounting API — `Quickbooks Production - Payment and Accounting` (`87nKk1v7updVBcDj`) as a generic oAuth2, not `QuickBooks PROD`. Getting this wrong silently stuck every eCheck at `PENDING` for five days and hid $37k.
- **F29** reminds only ACH-track milestones on their due date.

### 2A.3 Wire (international)

Gated to **non-US ship-to** in one place — `applyPaymentOptionsForCountry()` in `quote.js`. Domestic wire is a deliberate deferral, not an oversight. Wire orders get an **International Wire Instructions PDF** (`quote-wire-pdf`, rendered from the `WIRE_INSTRUCTIONS_TEXT` env var — banking details live only there, never in the repo), attached by F21 to the welcome email. **The quote ULID is the payment reference.**

Recording is the same **Record outside payment** action as a check (§2A.4, `kind='wire'`).

⚠️ **Expect the deposit to arrive short, and record the gross regardless.** Intermediary and receiving banks deduct charges in transit. Worked example (PS Taylors & Sons, 2026-07-22): $34,734.14 sent, **$34,685.81** received, **$48.33** withheld. The wire narrative itemised it — `BBI=/CHGS/USD0,00/CHGS/USD23,33/CHGS` (European decimal comma → $23.33) plus a $25.00 flat receiving fee. Resolution: receipt stays at $34,734.14, and the $48.33 goes to **6390** via the bank-match resolving difference (which QBO records as a Purchase — id `12797` in that case).

**Reading the narrative is the check that matters**, because a bank fee and a customer short-payment look identical at the bank and need opposite treatment. Compare: Practical Storage's $483.63 receipt against a $430.00 deposit was **not** a fee — the $53.63 was exactly `est_shipping` $23.53 + `est_tax` $30.10, i.e. the customer had paid the subtotal and the order total was wrong. Fee → expense; short-pay → chase or correct the order.

### 2A.4 Check and other outside payments

Money that lands straight in the bank with no rail behind it. Customers frequently select ACH at signing and then mail a check.

**Orders drawer → Record outside payment** (gated on the `orders-schedule-edit` capability; migration **171**). Fields: amount (defaults to the next open milestone), **bank date**, and kind (check / wire / other).

- `orders_record_outside_payment` validates: hardware, open balance, **no QBO invoice**, a linked `quotes.qbo_customer_id`, amount within the remaining balance, and no other post in flight (partial unique index `idx_oop_one_in_flight`). It writes the claim row + an `outside_payment_recorded` lifecycle event + an accounting note.
- **F40** (`jkZBzYF6zKsb83ZI`) posts the item-364 receipt to **1110**, dated the **bank date**, ULID in the memo → `Dr 1110 / Cr 2300`; then the payment row, `qbo_salesreceipt_id`, and F28. On failure it releases the claim, records the error and Slacks #accounting; `attempts < 5` caps retries.
- **Bank date has no default, deliberately.** It becomes the receipt `TxnDate`, which is what QBO matches the deposit against and which decides the cash month. The first three live wires were all keyed as the entry date rather than the arrival date (one nine days out); harmless only because they fell in the same month.
- A free-text reference field was **removed 2026-08-02** — unused across four live payments, and the memo already carries the ULID, customer and recorder. Column + RPC parameter retained, so re-adding is UI-only.

Full implementation detail: `apps/orders/README.md` → "Recording an outside payment".

### 2A.5 ⚠️ The invariant that governs receipt amounts

**The QBO Sales Receipt is the authority for `order_collection_payments.amount`, so the item-364 line must always carry the GROSS collected amount.**

F19's `Upsert deposit receipts` does `ON CONFLICT (source, qbo_payment_id, COALESCE(qbo_invoice_id,'')) DO UPDATE SET amount = EXCLUDED.amount`, where `EXCLUDED.amount` is the **sum of the receipt's item-364 lines** (`Explode deposit receipts` sums lines whose `ItemRef.value === '364'` **or** whose item name contains "hardware deposit"). Whatever a lane writes, F19 drags the ledger to the receipt's 364 total within 2h, and `orders_match_collections` re-derives the milestone from it.

Consequences, all observed in production:

1. A receipt posted **net of any fee** silently **un-collects** the milestone on F19's next pass, and F22's `collected = total` test then never fires — leaving a residual in 2300 that F3's `Dr 2200` over-draws at fulfillment.
2. Any fee must ride a **separate line with a different item**, never netted into the 364 line — and that item must **not** be named anything containing "hardware deposit".
3. The fee is an **expense** (6390), never negative revenue. Netting it against revenue corrupts `invoice_amount` → F3's `Cr 4100`, `gross_profit` and `commission_total` (20% of GP), and breaks the month-end invariant *4100 = Σ Hive fulfillment `invoice_amount`*.

⚠️ **No QBO item maps to 6390.** `Payment processing fee` (43) and `Processing Fee` (346) both map to **153 Other Revenue**; `Discount` (39) → **58 Returns & Refunds**. A negative line on any of them books a revenue contra, not a G&A expense. Automating a fee line needs a **new service item** mapped to 6390.

**Reading the method back.** `order_collection_payments.payment_method` has three shapes: F40 writes real words (`check`/`wire`/`other`); the engine lanes write the generic `sales_receipt` for card and ACH alike (so the rail comes from the latest `order_collection_charges.method`); and F19's QBO Payment ingest stores `PaymentMethodRef.name` when present but falls back to the raw QBO **id** — which is why legacy invoice-era rows read `"5"` or `"8"`. Resolved against the QBO PaymentMethod list: **2** Cash, **3** Check, **4** Credit Card, **5** QuickBooks Payments-Bank, **6** QuickBooks Payments-Credit Card, **7** PayPal, **8** Stripe, **9** ACH. The Orders drawer uses exactly this to label a collected milestone (*"collected Jul 23, 2026 · Check"*).

### 2A.6 The July 2026 Synder double-booking (resolved 2026-08-13)

Kept short, and kept only because the failure mode is instructive. Nothing here is outstanding except the residual noted at the end.

On **2026-07-29 09:07 PT** a second Stripe → QuickBooks sync (Synder) was switched on and back-filled 07-02 → 07-28. KISS already had a card lane, so every charge was booked twice on disjoint accounts: ours went item-364 → **1130** crediting **2300**, Synder's went to its own bank account **191** crediting **194 "Stripe sales"** as income at charge time, and the payout transferred the wrong direction. Proof they were the same money: Synder receipt **12554** carried our own metadata (`ch_3Tp8syIas0OnANRY1VVkVtnP`, `quote_id 01KWM7Q9HMP1AM7DC1CHHK64AJ`), the exact charge our SR **12055** had already receipted.

**Final footprint: 232 transactions, $63,806.60 of overstated revenue.**

⭐ **The lesson, and the whole reason this subsection survives.** The first inventory said **106 transactions / $47,234.69** and was wrong, because Synder models *subscription* charges as `Invoice` + `Payment` pairs and neither entity type had been queried. What exposed it was arithmetic, not suspicion: account 191's actual balance was **−$1,616.39** against **−$16,637.24** implied by the known transactions, a **$15,020.85** hole. Querying the two missing entity types closed it to the cent. So: **reconcile the bank account a sync deposits into, and keep querying entity types until it ties.** Arithmetic is what proves an inventory complete. Running out of ideas about what to query next proves nothing, and it is what produced a number 26% too low.

Two mechanical notes worth keeping for any future unwind of a sync: **delete Payments before their Invoices** (a payment locks its invoice, so the obvious ordering fails partway through), and reverse **home-currency** amounts on foreign-currency transactions, since FX uplift on two EUR invoices was itself $436.30 of the apparent discrepancy.

**Fully resolved 2026-08-13. Nothing outstanding.** All 232 transactions removed; the 12 residual Stripe-fee expenses on 191 (−$35.60) deleted; accounts **191–194** deactivated at zero balance; the duplicate customer **1166 `hrgc.net`** (shadowing **887 Ken Halcomb**) already inactive at zero; items **392/393** already gone. 1130 reconciled back down from $91,076.63. QBO auto-suffixes deactivated names with "(deleted)", which is how these read in the chart of accounts now.

⚠️ **If a sync ever creates duplicate customers again: deactivate, never merge.** QBO customer merges are irreversible.

---

## 3. Revenue recognition at fulfillment (F3 — the core)

**Principle: revenue is recognized at fulfillment (transfer of control), regardless of payment.** True AR (1200) exists precisely for a fulfilled-but-uncollected remainder.

F3 (`z4GoWv0MbiDnQZhF`, Inflow fulfillment webhook) atomically claims the lifecycle row, then for a post-cutover booked order (`qbo_invoice_id IS NULL` and `fulfillment_date >= 2026-06-23`) runs the **invoice-driven** path:

1. **Build the fulfillment invoice** (`quotes.line_items` + `est_*`): SKU revenue lines (one-time hardware SKUs 113–117 → QBO items 355–359 → 4100, `TaxCodeRef 'TAX'`), shipping/duty/fees lines, then a **negative settlement line**. QBO computes AST sales tax → **Sales Tax Payable**.
2. **Post the trimmed JE** (`Dr COGS / Cr Inventory`; commission + royalty release; a tax-variance plug on Variant A). No revenue leg — revenue lives on the invoice.
3. **Stamp** `fulfillment_date`, `f3_revrec_je_id`, `qbo_fulfillment_invoice_id`, `status` (`payment_date ? 'complete' : 'fulfilled'`).

### 3.1 Variant A — paid in full before fulfillment (the normal path)
Precondition: F22 already moved the cash `2300 → 2200`. Settlement line = negative **item 372 → 2200**, grossed up by AST tax (in "Build Invoice Update") so the invoice **nets to $0** — **no AR**.
```
Invoice:  Dr 2200 Deferred HW Revenue (T+tax) / Cr 4100 Revenue (T) / Cr Sales Tax Payable (tax)   → balance $0
Trimmed JE: Dr 5101 COGS/Cr 1360 Inventory · Dr 5500 COGS:Commission/Cr 1410 Deferred Commissions · Dr 5600 COGS:Royalties/Cr 1420 Deferred Royalties · (± Sales Tax Variance plug)
```
Tax-variance plug reconciles the AST tax vs the quote total F22 pre-funded into 2200 (Dr/Cr **Sales Tax Variance 1150040047** ↔ **Deferred HW Revenue**).

### 3.2 Variant B — fulfilled before paid in full (the exception; 0 today, Michael-signoff-gated)
Precondition: F22 has **not** run; `$C` of deposits sit in **2300**. Settlement line = negative **item 364 → 2300** for `$C` (the real collected cash, summed from `order_collection_payments`); the open balance is **True AR (1200)**. No net-to-$0, no tax variance.
```
Invoice:  Dr 2300 Customer Deposits ($C) + Dr 1200 AR (T+tax−C) / Cr 4100 Revenue (T) / Cr Sales Tax Payable (tax)
Trimmed JE: Dr 5101 COGS/Cr 1360 · Dr 5500 COGS:Commission/Cr **2150 Accrued Commissions Payable** · Dr 5600 COGS:Royalties/Cr **2160 Accrued Royalties Payable**
```
**Option 2 (commission/royalty):** Variant B credits the **payable directly** (not the Deferred bridge — there's no deferred asset yet, and crediting Deferred would run it transiently negative). F3 stamps `f1_accrual_je_id` with the trimmed-JE id so **F1 skips this order** at paid-in-full (F1's guard is `f1_accrual_je_id IS NULL`). Order lands `status='fulfilled'`. Later payments clear the AR (§5).

### 3.3 Guard outcomes (before the invoice-driven path)
`Match to Lifecycle` (atomic claim CTE) + `Guard Checks`: `duplicate` → silent skip · `quote_signed_date < 2026-04-01` → pre-cutover branch · zero amount → alert · **else `proceed`** (unpaid orders now proceed to Variant B — the old `booked_unpaid` skip was removed 2026-07-02). A legacy order with `qbo_invoice_id` set takes the old full-JE path (`Dr Deferred HW Revenue 1150040033 / Cr Revenue 31` + COGS/comm/roy), dying out.

---

## 4. Commission & royalty — the three-leg model

Two independent bridges (Deferred asset; Accrued Payable) between three events. **Only F1 + F3 post commission/royalty JEs** (verified across all workflows). F2 posts NO JE (calc + Gusto only); F21 only writes `commission_total`/`royalty_total` columns; F22 = 2300→2200 only.

| Leg | Trigger | JE |
|---|---|---|
| **Accrue** | order **paid in full** (`payment_date` set) — **F1**, monthly in arrears | `Dr 1410 Deferred Commissions / Cr 2150 Accrued Commissions Payable`; `Dr 1420 Deferred Royalties / Cr 2160 Accrued Royalties Payable` |
| **Expense** | **fulfillment** — **F3**, per order | `Dr 5500 COGS:Commission / Cr 1410`; `Dr 5600 COGS:Royalties / Cr 1420` **(Variant A)** — or `Dr 5500/5600 / Cr 2150/2160` **directly (Variant B)** |
| **Pay out** | commission run — **Gusto**, off-platform | `Dr 2150/2160 / Cr Cash`. Gusto's pay types must map to the **payables**, never to COGS: Sales & Marketing → **2150**, Research & Development → **2160**. Mapped to the payable it relieves the liability with no double-expense; mapped to COGS it expenses the comp a second time, because F3 already expensed it at fulfillment. ⚠️ The **R&D** type was mapped to **5600** until 2026-08-13 and did exactly that. Corrected in Gusto. |

- **Rule:** any order paid-in-full in month M → comped in M+1's run; "paid" = full balance only. Interim tranche deposits sit in 2300 and trigger nothing until the final payment.
- **F1** (`SSurt5KdsLODjHLW`, cron `0 6 1 * *`): sums `commission_total`/`royalty_total` for `invoice_lifecycle` rows with `payment_date` in the prior month and `f1_accrual_je_id IS NULL`; posts one pooled JE dated prior-month-end; stamps `f1_accrual_je_id`. **Activated 2026-07-03; the old signing/lump-basis F1 (`XpZgzpiu9kWmYx4B`) is archived.**
- **F2** (`OuMW5cF7HDsiaqH3`, ~6th): computes payouts for paid-in-full orders → Google Sheet + `projected_outflows` + Gusto; stamps `f2_commission_bill_id`/`f2_royalty_bill_id`. Royalties quarterly.
- **Deferred asset** = comp on orders **paid-in-full but not yet fulfilled**. **Accrued payable** = comp on orders **paid-in-full but not yet paid out**. Independent (one keyed on fulfillment, one on payout), so their balances differ.

---

## 5. Payment application after fulfillment (Variant B only)

Once a Variant B order is fulfilled with open AR, further collections must clear the **invoice AR**, not pile into 2300. **P1 design (shipped 2026-07-03):**

- **F23 (Collection Receipt Poster, `hHJLF0HgOtyjxUrx`)** + **F31 (Card Receipt Poster, `iaf84LXogY6o35NB`)**: their `Build SalesReceipt` node checks `fulfilled = qbo_fulfillment_invoice_id set && status==='fulfilled'`. If so, it builds a QBO **Payment** applied to the fulfillment invoice (`LinkedTxn` Invoice, deposited to the same clearing account — F23 = `1150040048`, F31 = Stripe clearing AcctNum 1130) → `Dr clearing / Cr 1200 AR`. The create node's URL is dynamic (`/payment` vs `/salesreceipt`). Pre-fulfillment collections are unchanged (item 364 → 2300). *(F28, `8nmoPpRPCIokZZEU`, only sends the receipt email — not a poster.)*
- **F15 (QBO Invoice CDC Sync, `rt0Px5bOnmfHY8HX`)**: a second UPDATE keyed on `qbo_fulfillment_invoice_id` stamps `payment_date` and flips `fulfilled → complete` when the fulfillment invoice hits $0 balance. (Deliberately narrow — it does NOT overwrite `total_invoice_amount`/`due_date`, which the first statement owns for the legacy invoice.)
- **F22** stands down for these (guard `f3_revrec_je_id IS NULL`) so it never re-moves 2300→2200 on an already-recognized order.
- **Known minor gap:** a post-fulfillment Payment logged to `order_collection_payments` carries `source='sales_receipt'` (a reporting-label quirk only; the GL — AR draw-down + the stamp — is correct).

---

## 6. The accounts (revenue side)

| Account | # | QBO internal id | Should contain |
|---|---|---|---|
| **Accounts Receivable — True AR** | 1200 | — | **Only** fulfilled orders with cash still outstanding (Variant B). Never bookings, never deposits. |
| **Customer Deposits** | 2300 | — | Customer cash on an order **not yet paid in full** (item 364 receipts). Refundable up to fulfillment — there is no earlier commitment point since the T−90 "goes hard" checkpoint was retired (§1). |
| **Deferred Hardware Revenue** | 2200 | 1150040033 | Paid-in-full orders awaiting fulfillment (funded from 2300 by F22). Item **372** ("Prepayment Applied") draws it down. |
| **Hardware Revenue** | 4100 | 31 · items 355–359 | Recognized at fulfillment (F3), via the invoice's SKU lines. |
| **COGS: Hardware Materials** | 5101 | 90 · Cr Inventory 10 (1360) | Landed cost of shipped goods (F3). |
| **COGS: Commission / Royalties** | 5500 / 5600 | 167 / 1150040028 | Comp expense at fulfillment (F3). |
| **Deferred Commissions / Royalties** | 1410 / 1420 | 1150040034 / 1150040035 | Comp on paid-in-full-but-not-fulfilled orders (the bridge F1 funds, F3 releases). |
| **Accrued Commissions / Royalties Payable** | 2150 / 2160 | 1150040031 / 1150040032 | Comp owed but not yet paid out; cleared by the Gusto payout. |
| **Sales Tax Payable / Sales Tax Variance** | — / — | — / 1150040047 | AST tax at fulfillment; the quote-vs-AST delta plug (Variant A). |
| **Deposit clearing** | — | 1150040048 (Ramp) · AcctNum 1130 (Stripe) | Where receipts/payments deposit before the bank sweep. |

Item **364** = "Hardware Deposit" → 2300. Item **372** = "Prepayment Applied" → 2200. QBO company id `9130356633288996`.

---

## 7. Workflows (n8n)

Canonical registry: `n8n/README.md`. Posting workflows are server-side; QBO writes only happen here, never in the Hive apps. Host: `keepitsimplestorage.app.n8n.cloud`.

| Workflow | ID | Trigger | Role |
|---|---|---|---|
| **F21 · Order Booking** | `TR2lWvjE4SwEDnnV` | Quote signed (via F9) | Books order (no invoice), Inflow SO + HubSpot Order, milestone schedule, writes `commission_total`/`royalty_total`. |
| **F22 · Paid-in-Full Poster** | `WYdj5467csH0KVgg` | hourly | All milestones covered → `Dr 2300 / Cr 2200`; sets `payment_date`. **Guard:** skips if `f3_revrec_je_id` set (Variant B stands down). |
| **F3 · Revenue Recognition** | `z4GoWv0MbiDnQZhF` | Inflow fulfillment webhook | Fulfillment invoice (revenue+tax, settlement line 372/2200 [A] or 364/2300+AR [B]) + trimmed JE (COGS/comm/royalty). §3. |
| **F1 · Commission/Royalty Accrual** | `SSurt5KdsLODjHLW` | cron `0 6 1 * *` | Paid-in-full accrual `Dr 1410/1420 / Cr 2150/2160`; skips `f1_accrual_je_id IS NOT NULL`. **Active** (old F1 `XpZgzpiu9kWmYx4B` archived). |
| **F2 · Comm/Royalty Payout Calc** | `OuMW5cF7HDsiaqH3` | ~6th | Payout calc → Sheet + `projected_outflows` + Gusto. **No JE.** |
| **F15 · QBO Invoice CDC Sync** | `rt0Px5bOnmfHY8HX` | schedule | Syncs invoice state (legacy `qbo_invoice_id`); 2nd UPDATE stamps `payment_date`/`complete` on the **fulfillment** invoice at $0 (§5). |
| **F16 · QBO Payment Sync** | `36eoHKagIIeVSBie` | schedule | Refines `payment_date` from QBO payments (legacy invoice). |
| **F19 · QBO Payment Ingest** | `z4DWimqjjKw2ltta` | 2h | Ingests QBO Payments + item-364 Sales Receipts → `order_collection_payments`; FIFO-matches milestones; fires receipt emails; mirrors to Inflow SO. |
| **F23 · Collection Receipt Poster** | `hHJLF0HgOtyjxUrx` | collection webhook | item-364 receipt → 2300, **or** (fulfilled) Payment → fulfillment invoice (§5). |
| **F31 · Card Receipt Poster** | `iaf84LXogY6o35NB` | Stripe settle webhook | Card receipt → 2300, **or** (fulfilled) Payment → fulfillment invoice (§5). |
| **F28 · Payment Receipt** | `8nmoPpRPCIokZZEU` | fired by posters | Sends the receipt / paid-in-full email. No GL. |
| **F4 · Invoice Paid in Full** | `7LpRr40dLy5OO5N7` | invoice-paid | Legacy full-invoice orders: sets paid + full-amount Inflow line. |
| **F20 · Collections Due Digest** | `w93iFwXSjZypfN8V` | weekday | Dues + exceptions digest. |

**Account ids vs numbers:** F-series JSON references QBO **internal ids** (2200=`1150040033`, 4100=`31`/items 355–359, COGS 5101=`90`/Inv `10`, 5500=`167`, 5600=`1150040028`, 1410=`1150040034`, 1420=`1150040035`, 2150=`1150040031`, 2160=`1150040032`, variance `1150040047`); the friendly numbers here are chart-of-accounts labels. Verify id↔number against the QBO chart if it matters.

**n8n edit gotchas (REST API):** the public-API `PUT /workflows/{id}` body must be exactly `{name,nodes,connections,settings}`, and `settings` must be **whitelisted** to `{executionOrder,errorWorkflow,save*,executionTimeout,timezone}` (cloud-only keys like `availableInMCP`/`callerPolicy` are rejected). An **active** workflow needs deactivate → PUT → activate. `queryReplacement` comma-lists **drop empty trailing params** — keep any nullable param out of the last position (or use `jsonb_to_record`). See `n8n/README.md`.

---

## 8. Known gaps & manual procedures (current state)

- **Sales tax** — the fulfillment invoice carries real **AST sales tax**; Kintsugi files off it. The quote-time estimate is the in-house `sales_tax_nexus` rate table. Any quote-vs-AST delta is absorbed to **Sales Tax Variance 1150040047** (Variant A) or flows into AR (Variant B).
- **Fulfilled before paid (Variant B)** — **now automated** (§3.2, §5). The first real one is Michael-signoff-gated and should be watched end-to-end (item-364-on-invoice draw + F15 stamp) since 0 have run through the new path.
- **Tranches** — run as linked quotes until the native spawner is built.
- **Ledger label** — the §5 `source='sales_receipt'` quirk on post-fulfillment Payments (reporting only).

---

## 8A. Traps (verified, each one cost real money)

Deliberately a short list, not a taxonomy. Every entry has the same shape: **two systems record the same money, and the second record is the one a person creates by hand because the first one is invisible to them.** Promote an entry into the body of the doc if it recurs.

**1. Gusto lines in the bank feed.** Gusto posts payroll as a **JournalEntry** (`DocNumber: "Gusto"`) that credits the bank account for net pay and tax and debits every expense account. That record is complete. Separately the bank feed downloads the real cash withdrawal to `GUSTO`. QuickBooks **filters journal entries out of the matcher's default view** (you must set Record type → Journal entries), so the line presents as unmatched and the natural response is to categorize it.
- **Rule:** never categorize a `GUSTO` feed line, never "Record as transfer." Match via Record type → Journal entries with a widened date range (the JE carries the period-end date, the cash moves days later), else **Exclude**.
- **Safety condition:** confirm the Gusto JE exists before excluding. If the sync failed, the feed line is the only record and must be booked. F1's Aug 1 2026 run died silently for six days, so this is not hypothetical.
- **Damage, June–July 2026: $37,675.69** of phantom wages in 6031, in three shapes, all the same missing match: JE **12488** (hand-written JE to 6031, $18,823.08, deleted), Purchase **12994** (categorized to 6031, $18,452.61, reclassed to 1115), Transfer **12733** (recorded as a wallet transfer to Reserve, $18,823.08, deleted). Clean 6031 run-rate is 2 × $10,650.17 = **$21,300.34/month**; anything above it is the suspect list.
- Volume makes this high-frequency: Ramp shows **10 separate Operating→GUSTO withdrawals in the six days 7/25–7/31 2026** (semi-monthly payroll plus contractor payouts).

**2. Ready Steady Store EUR subscription, monthly.** The Stripe path posts **both** an Invoice (revenue → 1201 AR-EUR, `PrivateNote: "Transaction ID: in_…"`, a Stripe *invoice* id) and a Sales Receipt (revenue → 1131 Stripe Clearing EUR, `py_…`, the actual *payment*) for the same charge. The receipt is real; the invoice is the duplicate and never gets paid, so it also parks phantom AR.
- Customer `810` "Ready Steady Store-EUR". Monthly pair is €2,025 + €450 = **€2,475**, roughly **$2,880**.
- **Detection:** `SELECT Id, DocNumber, TxnDate, TotalAmt, Balance, PrivateNote FROM Invoice WHERE CustomerRef = '810'` — any nonzero `Balance` with an `in_…` memo is unvoided. Docs 221/227 (Mar) and 325/340 (Apr) were caught and voided; **537/524 (Jun) voided 2026-08-12**; **440/433 (May) still open**.

**Cross-cutting rule for both:** matching and excluding both leave the GL correct, so the invariant is simply *do not categorize*. And bank-feed transactions (Purchase / Expense / Deposit) get **re-categorized, never deleted** — the cash moved, only the account is wrong. Hand-made JEs and integration duplicates get **deleted**.

---

## 9. History — the 2026 rev-rec + commission cutovers

- **2026-04-01 — signed-quote cutover.** Orders signed on/after this date run the two-track (deposit/2300 → 2200 → recognize) model; earlier ones are legacy full-invoice.
- **2026-06-24 — invoice-driven rev-rec live.** F3 began creating the fulfillment invoice (real AST tax + item-372 prepayment draw) instead of a revenue JE, for orders fulfilled on/after 2026-06-23.
- **2026-07-02 — commission re-point + Variant B.** F1 re-pointed from a signing/lump 20%-of-GP basis to **per-order paid-in-full** accrual (fixing over-accrual, deferred drift, and phantom `invoice_lifecycle` rows — 6 deleted). F3 gained **Variant B** (recognize at fulfillment regardless of payment) with **Option 2** commission handling.
- **2026-07-03 — payment-application + F1 cutover.** F23/F31/F15 gained the post-fulfillment Payment path; new F1 activated, old F1 archived.
- **The true-up.** Old F1's wrong basis left the four comp accounts adrift. The correcting entry (all four debits, offset = accountant's policy call) is in **`n8n/commission-royalty-trueup.md`** with a reproducible reconciliation query. Re-tie both sides at posting.

---

## 10. Where it's visible — Hive (read-only)

- **SO Ledger** (`apps/orders` / `apps/accounting`) — per-order money position + pipeline stage; stalls surface first.
- **SO Checks** — implied 2200/2300 balances, `ΣDr=ΣCr`, dues & exceptions, monthly recognition, QBO tie-out (variance flagged).
- **13-Week** — collections vs supplier vs comp + **Pool funding coverage**.
- **Orders — AR tab** — True AR (1200) vs backlog, team-readable.
- Every exposed figure ties to a QBO account by its own month — the tie-out discipline.

---

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

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

- **F3** (`z4GoWv0MbiDnQZhF`) — the invoice build (settlement line 372/2200 vs 364/2300), the trimmed JE (Variant A Deferred vs Variant B Accrued Payable), the guard, the `f1_accrual_je_id` stamp.
- **F1** (`SSurt5KdsLODjHLW`) + **F2** (`OuMW5cF7HDsiaqH3`) — the accrual basis + guard; the payout calc + Gusto pay-type coding (S&M → 2150, R&D → 2160; **never to COGS** — see §4 and §8A trap 1).
- **F22** (`WYdj5467csH0KVgg`) — the paid-in-full reclass + the `f3_revrec_je_id` stand-down guard.
- **F23/F31** (`hHJLF0HgOtyjxUrx`/`iaf84LXogY6o35NB`) — the fulfilled-order Payment redirect; **F15** (`rt0Px5bOnmfHY8HX`) — the fulfillment-invoice stamp UPDATE.
- **F19** (`z4DWimqjjKw2ltta`) — receipt/payment ingest + FIFO matching; **F21** (`TR2lWvjE4SwEDnnV`) — booking + the `commission_total`/`royalty_total` columns.
- Supabase `invoice_lifecycle` markers (`payment_date`, `f3_revrec_je_id`, `f1_accrual_je_id`, `f2_commission_bill_id`, `qbo_fulfillment_invoice_id`, `status`), `order_collection_milestones`, `order_collection_payments`.
- **The collection rails (§2A):** **F31** (`iaf84LXogY6o35NB`) card receipt poster · **F25** (`uB57qwHEVmoUhK5i`) + **F26** (`XHpIVElLiNDfkLw8`) ACH submit/settle · **F40** (`jkZBzYF6zKsb83ZI`) outside payments, with migration `171` (`order_outside_payments`, `orders_record_outside_payment`) · `apps/billing` (`quote-card-intent.js`, `quote-card-finalize.js`, `quote-wire-pdf.js`, `applyPaymentOptionsForCountry()` in `quote.js`) · `apps/orders/README.md` for the drawer action.
- **§2A.5 is load-bearing** — if F19's `Explode deposit receipts` / `Upsert deposit receipts` nodes change, re-check that the gross-not-net rule still holds, because every rail depends on it.
- Account-number ↔ QBO-internal-id map if the chart of accounts changes. The COGS/inventory side → the **Inventory Accounting** playbook. The true-up → `n8n/commission-royalty-trueup.md`.
