# Inventory Accounting — KISS system memory

> **Audience: an LLM.** Deep, technical companion to the human "Inventory Accounting"
> playbook. Paste it into any assistant to give it an accurate model of how a hardware
> purchase moves across KISS's books from PO to payment to COGS, **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 — **hardware purchasing → inventory → COGS** path.
> - **Last reviewed:** 2026-06-22
> - **Owning systems:** Inflow (warehouse truth), Ramp (AP system of record), QuickBooks Online (general ledger). n8n workflows **F10** (Inflow PO sync + conductor), **F11/F12** (Ramp bill received/paid), **F17** (receipt recognition JE), **F18** (true-up JE), **F3** (fulfillment → rev-rec + COGS). Hive **`apps/accounting`** (audit trail + checks) and **`apps/cash-flow`** (forward outflow planning). Shared "kiss ops" Postgres (`inventory_*`, `invoice_lifecycle`, `products`).
> - **Human version:** `/playbooks/inventory-accounting` in Hive (structured: Part 1 manual steps, Part 2 what happens underneath).
> - **System-of-record doc:** `apps/accounting/README.md` (the five-rule model + views). When this `.md` and that README disagree, the README wins — reconcile.
>
> **Orientation:** Inflow holds physical truth (on-hand, receipts, fulfillments, MOs). Ramp is the AP system of record for vendor bills/payments. QuickBooks is the GL and, for inventory, **sees only journal entries — never bills**. The governing rule is **no payable before receipt**. Almost all the bookkeeping posts itself; §1 is the complete list of manual steps.

---

## 0. One-paragraph model

A PO is placed in **Inflow** (no books). Supplier deposit/interim payments flow through **Ramp** and post as prepayments — **`Dr 1350 Prepaid / Cr Cash`**, no AP. When Fulfillment marks the PO **received** in Inflow, **F10** fires **F17**, which posts the single Event-4 recognition JE at **full PO value** — **`Dr 1360 Inventory / Cr 1350 Prepaid (prepaid-to-date) / Cr 2100 AP (PO value − prepaid)`**. The balance bill, once paid post-receipt, pays down AP — **`Dr 2100 AP / Cr Cash`**. If the Inflow total changes after receipt, **F18** trues up 1360 + AP. At **fulfillment** (shipment), **F3** posts COGS out of inventory — **`Dr 5101 COGS / Cr 1360`** at `products.cogs` — alongside the revenue-recognition pairs. Non-fulfillment moves (cycle counts, scrap, returns) are still a manual monthly batch JE. The **Accountant** owns the ledger but the routine JEs auto-post; their day-to-day is reviewing the audit trail and clearing exceptions in **`apps/accounting`**.

---

## 1. Manual steps — what a person does by hand

Everything else auto-posts off these actions.

**Fulfillment Team (Inflow):**
1. **Place the PO** — lines, qty, unit costs, vendor PO#, terms (deposit %, balance %, expected delivery). Inflow assigns the KISS PO number (`PO-######`).
2. **Mark the receipt** against the PO when goods arrive (inspect, count, flag shortfall/damage/partial). Receipt in Inflow is the recognition trigger (Event 4) — do not wait for the balance invoice.

**AP Team (Ramp):**
3. Ensure the vendor is in the **Inventory Vendor Group** in Ramp (any bill citing a KISS PO# is inventory even if untagged — flag to add).
4. **★ Put the KISS PO number in the Ramp bill's `Memo` as `PO-######`** (exactly 6 digits, e.g. `PO-001234`). This is the load-bearing manual step — it's how F11 auto-links the bill to its PO so every downstream JE posts itself. See §1.1.
5. **Pay the bill** normally — the PO match carries from bill to payment; nothing extra at pay time.
6. Route a **separately-arriving freight/customs/duty bill** (Event 6, rare) to Fulfillment then the Accountant; don't code it.

**Accountant (Accounting app + QBO):**
7. **One-time per vendor (Vendors view):** set `qbo_vendor_id` + `ramp_vendor_id`. Missing → JEs post untagged and bills land unmatched.
8. **Ramp Inbox:** match bills that arrived without a clean `PO-######` to their PO. The match is a DB link only (`matched_po_id` + `match_source='manual'`); to post the queued JEs, open the PO's audit trail and click **Catch up JEs** (fires **F11c**). See §8.
9. **PO Checks:** post recognitions F17 skipped (overpaid / invoice > PO) and resolve invoice-vs-PO discrepancies (adjust the Inflow PO up to actual → F18 trues up).
10. **Landed-cost add-on** bills (Event 6): book by hand to 1360 (on hand) or 5101 (shipped).
11. **Monthly at close:** post the adjustment batch (cycle counts/scrap/returns, §3.8) and run the tie-out (§9).

### 1.1 The PO-on-the-Ramp-bill detail (most common failure point)

- **F11's `Normalize Ramp Bill` node parses the PO from `bill.reference || bill.memo` with the regex `/PO-\d{6}/`** — `PO-` plus **exactly six digits**. First match wins; otherwise `inflow_po_number = null`.
- In the **Ramp UI, use the bill's `Memo` field** (F11 reads `memo`; `reference` is the API alias). Ramp's separate "Invoice number" field maps to `vendor_invoice_number` (used in the JE memo), **not** the PO match.
- **The number comes from Inflow but is entered in Ramp.** Inflow only *assigns* the `PO-######` (shown on the PO); there is **no field to edit in Inflow** for this. Type it onto the Ramp bill.
- **If absent/malformed:** F11 leaves `matched_po_id = NULL`, the bill is logged as `match_source = 'unmatched'` and surfaces in the **Ramp Inbox** for manual matching. Nothing is lost; it just isn't automatic.
- **Caveat:** the regex is hard-coded to 6 digits. If KISS PO numbering ever changes length/format, F11's parse **and** this playbook's step must change together.
- F11 then matches by **vendor + parsed PO number**; a unique hit auto-attaches `matched_po_id`, ambiguous/zero → unmatched.

### 1.2 Automated — don't duplicate (and the one manual exception)

The flip side of §1: behaviors the team should know about so no one redoes them by hand.

- **Payment → Inflow back-write (GET → merge → PUT).** Both **F12** (on a matched bill's payment) and **F11c** (on catch-up) write the payment to the Inflow PO so no one re-keys it. The mechanism: **fetch the PO's current `paymentLines`** (`GET …/purchase-orders/{id}?include=paymentLines`), **append** the new line (dedup by Ramp id, or by amount+date so a manually-entered Inflow line isn't duplicated), then **`PUT …/purchase-orders` with the full array**. **This GET-merge is mandatory because Inflow's PUT *replaces* the paymentLines collection** — sending only the new line wipes the PO's other payments (learned the hard way 2026-06-23, after a single-line PUT erased a deposit line; note there is **no** `POST …/payment-lines` sub-route — it 404s). F11c aggregates all of a catch-up's payments into **one GET + one PUT** (no intra-run race). Wipe-proof: if the GET fails, the back-write skips (no PUT); the PUT node is `onError: continueRegularOutput`, so it never hard-fails after the JEs have posted. A bill paid while *unmatched* no-ops in F12 (no JE, no line); matching it and clicking **Catch up JEs** (F11c) lands both the QBO JEs and the Inflow line. **(Sales-order side — don't generalize this:** F4, which marks an SO paid-in-full, deliberately *replaces* the SO's `paymentLines` with one **full-amount** line. That's correct and idempotent for its single-full-payment model — converting F4 to GET-merge would double-count on a duplicate webhook. GET-merge is only right where payments are *incremental* and accrue multiple lines, i.e. the PO side here.)
- **PO-total true-up.** Change a received PO's `total_amount` in Inflow → **F18** posts the 1360/AP delta on the next F10 sync. No manual adjustment JE.
- **Supplier auto-create.** **F10** inserts the supplier row (`inflow_vendor_id` + name) on first PO sync; only `qbo_vendor_id`/`ramp_vendor_id` are manual (§1 step 7).
- **Forecast schedule.** A new PO arrives with **no plan at all** and a *Not scheduled* tag — payment-terms templates and every seeding path were removed on 2026-08-20 (migrations `240`/`241`). Someone sets the schedule by hand in the Cash Flow drawer, in dollars or as a percent of the PO, and revisits it weekly. Nothing moves a date for you: the Friday roll-forward (n8n F41) is off too. The one automatic write left is a payment landing drawing the plan down against the balance (`inventory_sync_plan_to_balance`), which only ever *reduces* it. **Forecast only — not the GL.**
- **⚠️ The one thing NOT auto-booked:** a two-track **deposit order fulfilled before it's paid in full**. **F3** skips COGS/rev-rec (recognition must draw 2300 + an AR remainder) and **alerts Michael's Slack** (`C090PKJRKV4`); booked by hand with the accountant. Don't assume COGS posted.

---

## 2. The governing principle — no payable before receipt

KISS recognizes **neither 1360 Inventory nor 2100 AP** until goods are physically received.

- Pre-receipt money (deposits, interim payments) is a **prepayment → 1350 Prepaid Inventory** (asset), never AP.
- At receipt, a single JE recognizes inventory at **full landed PO value**, clears prepaid, and books the unpaid remainder as AP.
- Therefore inventory invoices are booked as **journal entries, never QBO bills** — a QBO bill posts AP at entry (before receipt) and collapses the two states (prepaid vs. received-and-owed) we keep separate.
- **Any invoice citing a KISS PO number is inventory-related by definition.** It flows through Ramp like any vendor bill; in QBO it lands as a JE.
- "What do we owe a supplier?" is answered in **QuickBooks**, not Ramp.

---

## 3. The accounting model — five rules (system of record: `apps/accounting/README.md`)

Every purchasing event reduces to five JE rules plus a vendor-tagged line. **No QBO bills.**

| Event | Goods received yet? | JE |
|---|---|---|
| Ramp bill received | No | (none — log only) |
| Ramp bill received | Yes | `Dr 1360 / Cr 2100 AP` |
| Ramp bill paid | No | `Dr 1350 Prepaid / Cr Cash` |
| Ramp bill paid | Yes | `Dr 2100 AP / Cr Cash` |
| Inflow goods received | — | recognize at receipt (Event 4, see §3.4) |

- **Memo** on every JE: `KISS PO {po_number} / Vendor PO {vendor_po_number} / Vendor invoice {vendor_invoice_number} / {source event ID}` — built by the workflow, not hand-typed.
- The 1350 line (pre-receipt payments) and the 2100 AP line (post-receipt bills/payments) are **tagged with the supplier's `qbo_vendor_id`** (`JournalEntry.Line.JournalEntryLineDetail.Entity.EntityRef`), so QBO vendor-activity / AP-aging reports still work without bills.
- **Refunds (v1):** a negative-amount Ramp payment fires F12 with the reverse JE (`Dr Cash / Cr 1350` pre-receipt; `Dr Cash / Cr 2100 AP` post-receipt).
- **Robust to PO-total changes:** every JE is built from the amount on its triggering event, not a cached `inventory_purchase_orders.total`. A revised total never moves a posted JE — F18 trues up instead.

> ⚠️ **Interplay to verify before treating as canonical:** the "Ramp bill received + goods already received → `Dr 1360 / Cr 2100 AP`" rule and F17's full-PO-value recognition can both touch 1360/AP. In steady state F17 recognizes the whole PO at receipt and F12 pays down AP; the post-receipt *bill-received* JE is for catch-up/backfill (F11c) or landed-cost add-ons, not a second recognition of the same PO. PO Checks ("recognized 1360 ≠ current PO total", "received + open balance, no recognition", "prepaid stuck on a received PO") is the backstop that catches double-counts or gaps. Confirm against the live F11/F17 guards when precision matters.

---

## 4. Purchase lifecycle — eight events

### 4.1 Event 1 — PO placed (Inflow, **manual**). Books: none. Fulfillment creates the PO (lines, qty, unit cost), records vendor PO# + terms, sends it. KISS PO# assigned.

### 4.2 Event 2 — Deposit paid (Ramp → QBO). Typically 40–50% of PO value for Shanghai Lijin. `Dr 1350 Prepaid / Cr Cash` — **no AP**. Auto-posted by **F12** (Ramp bill paid, pre-receipt).

### 4.3 Event 3 — Interim payment (rare; usually waived). Same treatment as Event 2 → 1350.

### 4.4 Event 4 — Goods received (Inflow → QBO) — **the recognition moment**
Fulfillment marks receipt against the KISS PO. **Receipt alone triggers recognition; the supplier balance invoice is NOT required.** Single JE at **full PO value**:

```
Dr 1360 Inventory             — full PO value
   Cr 1350 Prepaid Inventory  — prepaid-to-date on this PO (Σ Ramp payments dated ≤ receipt)
   Cr 2100 Accounts Payable   — PO value − prepaid-to-date (unpaid remainder; first point AP exists)
```

Handles any payment timing: fully prepaid → AP line 0; nothing prepaid → AP = full PO value; partial → AP = unpaid balance.

- **Auto-posted by F17**, fired by **F10** the moment `actual_delivery_date` is set (Inflow `isCompleted` / `inventoryStatus → fulfilled`, regardless of paid status).
- **F17 guards / skips** (no post): PO not received, already has a `receipt_recognition`/`goods_received_sweep` JE, zero total, or prepaid > PO (overpayment). Skipped cases surface in **PO Checks** for hand-posting — no manual queue.
- **Scope:** the full-PO-value model applies to POs with an open balance or not-yet-fulfilled. POs *both paid in full and received* under the old method are deliberately excluded.
- **Discrepancy (V1):** if the supplier's later balance invoice ≠ (PO value − prepaid) — usually invoice > PO — flag for the Accountant, don't auto-adjust. Resolution: adjust the Inflow PO up to actual, then F18 posts the true-up.

### 4.5 Event 5 — Final supplier invoice paid (Ramp → QBO). Post-receipt → pays down AP: `Dr 2100 AP / Cr Cash`. Auto-posted by **F12** (post-receipt).

### 4.6 Event 6 — Landed-cost add-on (rare, **manual**). A freight/customs/duty/broker/insurance bill arriving separately from the primary supplier. AP escalates to Fulfillment then the Accountant, who books by hand: on hand → `Dr 1360 / Cr 2100 AP`; shipped → `Dr 5101 Cost of Hardware / Cr 2100 AP`. See §6.

### 4.7 Event 7 — COGS recognition (Inflow fulfillment → QBO). On shipment, **F3** posts the up-to-8-line rev-rec JE. Inventory-side pair:

```
Dr 5101 COGS: Hardware Materials / Cr 1360 Inventory   (cogs_amount, from products.cogs)
```

Plus the revenue side (deferred revenue → 4100 Hardware Revenue) and the commission/royalty pairs. **Full mechanics + the revenue/commission/royalty accounts: the Hardware Revenue playbook.** F3 details: §7.

### 4.8 Event 8 — Adjustments, scrap, returns (**manual**, monthly batch). Off the SO fulfillment path; **not automated yet** (planned F14). Logged in a tracker, posted as one batch JE at close, memo citing Inflow event IDs:

```
Cycle count down:   Dr Inventory Variance / Cr 1360
Cycle count up:     Dr 1360 / Cr Inventory Variance
Scrap / damage:     Dr Scrap Expense / Cr 1360
Customer return (restockable):  Dr 1360 / Cr 5101   (+ revenue side: credit memo + accrual reversal)
Return to supplier (credit):    Dr 2100 AP / Cr 1360
```

**Inventory Variance** and **Scrap Expense** accounts may not exist in QBO — confirm/create (sub-accounts of COGS or Other Expense) before the first batch.

---

## 5. The accounts (inventory side)

| Account | Should contain |
|---|---|
| **1350 Prepaid Inventory** | Deposits/interim payments on POs **not yet received**. Stale, growing 1350 = delayed shipments. |
| **1360 Inventory** | On-hand goods at **landed cost**; ties to Inflow physical valuation. |
| **2100 Accounts Payable** | Owed for goods **received but unpaid**. Never holds a deposit. |
| **5101 Cost of Hardware** | Landed cost of goods **shipped to customers**. |

Anything else in these accounts = process exception → investigate. Revenue-side accounts touched at shipment (2200 Deferred HW Revenue, 2300 Customer Deposits, 4100 Hardware Revenue, deferred commission/royalty) belong to the Hardware Revenue playbook.

---

## 6. Landed cost

Capitalize every dollar to get goods to the warehouse ready to sell:

- **Include:** supplier invoice price · inbound freight · customs duties & tariffs · import broker fees · transit insurance · required pre-sale inspection/handling.
- **Exclude:** outbound freight to customer (selling expense) · own-warehouse storage (period expense) · sales commissions (separate deferred-commission flow).
- **Allocation across SKUs:** by value (proportional to supplier cost) is the default; by quantity is simpler/less accurate; per-line at receipt (Inflow landed-cost feature) is most precise but unconfirmed. Until locked: post add-on bills to 1360 with a memo naming the receipt; true up quarterly.

**Manufacturing Orders:** piece-part assembly (components → finished locks) is **GL-neutral** — both are inventory, cost shifts within Inflow, total 1360 unchanged. Risk: if finished-good unit cost ≠ Σ component costs, 1360 drifts. Sample one MO at close; the monthly tie-out catches aggregate drift.

---

## 7. Workflows (n8n)

All canonical JSONs live in `n8n/`; registry rows in `n8n/README.md`. Posting workflows are server-side; QBO writes only happen here, never in the Hive apps.

| Workflow | ID | Trigger | Posts |
|---|---|---|---|
| **F10 · Inflow PO Sync** | `G68tK0PMljA6tt87` | Inflow `purchaseOrder.created/updated` (`POST /webhook/f10-inflow-purchase-order`) | Upserts PO/lines/payments to Postgres (powers Cash Flow). **Conductor:** fires **F17** `{po_id}` on receipt (`actual_delivery_date` set) and **F18** `{po_id}` on every sync. Skips POs with `total ≤ 0`. |
| **F11 · Ramp Bill Received** | — | Ramp bill creation webhook | `Normalize Ramp Bill` parses `PO-\d{6}` from `bill.reference \|\| bill.memo`; matches by vendor + PO; upserts `inventory_ramp_bills`; posts `Dr 1360 / Cr 2100 AP` only if post-receipt; unmatched → Ramp Inbox. |
| **F12 · Ramp Bill Paid** | — | Ramp payment webhook (signed amount) | Pre-receipt → `Dr 1350 / Cr Cash`; post-receipt → `Dr 2100 AP / Cr Cash`; negative → reverse. Back-writes the Inflow paymentLine. |
| **F17 · Post Receipt JE** | `XGW9PBCeLQg9ej67` | `POST /webhook/f17-post-receipt-je` `{po_id}` (auto, from F10) | Event-4 recognition at full PO value: `Dr 1360 / Cr 1350 / Cr 2100 AP`. `je_purpose='receipt_recognition'` (mig 063). Self-guards; idempotent on `('qbo','receipt-'+inflow_po_id)`. **Active.** |
| **F18 · Post True-Up JE** | `oJaENslwrnmRPaQA` | `POST /webhook/f18-post-true-up-je` `{po_id}` (auto, from F10 every sync) | Trues 1360+AP to a revised total: up → `Dr 1360 Δ / Cr 2100 AP Δ`; down → reverse. `je_purpose='true_up_recognition'` (mig 064; signed delta). No-ops unless a delta is owed. **Active.** |
| **F3 · Revenue Recognition** | `z4GoWv0MbiDnQZhF` | `POST /webhook/f3-inflow-fulfillment` | On fulfillment (post-cutover ship date), atomic-claims the lifecycle row, recomputes COGS from `products`, posts the up-to-8-line JE incl. `Dr 5101 / Cr 1360`. Skips (no-match / zero / booked-unpaid) alert Michael's Slack. |
| **F13 · Inflow Receipt Sweep** | `9q2VERzEnPamkiZn` | *(retired 2026-06-05)* | Old `Dr 1360 / Cr 1350` partial sweep on receipt. **Superseded by F17**; unpublished. JSON kept for history. |
| **F11b / F11c** | — | one-shot / per-PO | Backfill: F11b seeds Ramp bills+payments (no QBO); F11c replays a matched PO's events chronologically and posts missing JEs (idempotent on `source_event_id`) **and back-writes each caught-up payment onto the Inflow PO** via GET-merge (fetch existing `paymentLines`, append, PUT the full array — Inflow's PUT replaces the collection; same as F12). **F17-aware:** if the PO already has an F17 `receipt_recognition` JE, F11c skips the `bill_received`/`sweep` recognition JEs (posts payments only) so it doesn't double-count 1360. |

**Account ids vs numbers:** F3 references QBO **internal account ids** in its JSON (e.g. revenue `1150040033→31`, COGS `90→10`, commission `167→1150040034`, royalty `1150040028→1150040035`); the friendly numbers in this doc (1350/1360/2100/5101/4100/2200/2300/5500/1410/5600/1420) are the chart-of-accounts labels. F11/F12/F17/F18 take the 1350/1360/2100/cash account ids as configured placeholders. Verify the id↔number mapping against the QBO chart of accounts if it matters.

---

## 8. Where it's visible — `apps/accounting` (Hive, read-only)

The audit-trail + reconciliation surface, and where the Accountant does the manual review steps (§1, steps 7–9). Reads Supabase under RLS (`inventory_staff` allowlist + `has_app_access('accounting')` for sales-side views). The one browser write is admin-only (matching a Ramp bill to a PO).

- **PO Audit Trail** (default) — POs with rolled-up **1350/2100/1360** balances from posted-JE lines; drawer = per-PO timeline (`inventory_po_events` joined to JEs + Ramp bills/payments), every external ID as chips.
- **PO Checks** — confidence layer: implied 1350/2100/1360, `ΣDr=ΣCr` badge, paste-in **QBO tie-out** (variance > $1 flagged), and pass/fail checks (Ramp payment with no JE · received + open balance, no recognition · prepaid stuck on received PO · recognized 1360 ≠ current PO total). **The monthly tie-out, in-app.**
- **Ramp Inbox** — unmatched Ramp bills (`matched_po_id IS NULL` — missing/malformed `PO-######`); admin picks the PO (with a parsed hint), which writes `matched_po_id` + `match_source='manual'` (a DB link only — fires no workflow). The **Catch up JEs** button on the PO's audit trail then POSTs `{po_id}` to **F11c** (`/webhook/f11c-post-pending-jes`), which replays the PO's matched bills/payments/receipt and posts the queued JEs (idempotent). The match alone posts nothing.
- **Vendors** — supplier directory; flags vendors missing `qbo_vendor_id` (JE tagging) / `ramp_vendor_id` (bill auto-match).
- **SO Ledger / SO Checks** — sales-side twin (2200 Deferred HW Revenue / 2300 Customer Deposits / recognized-this-month).

**Data model** (`inventory_*`, migrations 045–048 + 063/064): `inventory_ramp_bills`, `inventory_ramp_bill_payments`, `inventory_po_qbo_entries` (one row/JE, PK `qbo_entity_id`, `je_purpose` enum incl. `receipt_recognition`/`true_up_recognition`, `accounting_lines` jsonb), `inventory_po_events` (append-only, unique `(source, source_event_id)` → idempotent).

---

## 9. Monthly tie-out

Goal: **QBO 1360 = Inflow physical on-hand value at cost.** Do it in **PO Checks** first (implied balances + paste-in QBO tie-out + checks); use SQL only to dig into a flag.

1. Paste QBO month-end 1360/2100/1350 into PO Checks → variances flagged.
2. Cross-check Inflow on-hand × unit cost (`products.cogs` today) vs implied 1360.
3. Variance > threshold → investigate before close; genuinely unattributable → **3990 Cleanup Suspense** with a memo.

**Variance causes (by likelihood):** missed F3 JE (fulfilled, no COGS posted — most common) · missed adjustment (not in the monthly batch) · MO costing drift · stale `products.cogs` · off-flow manual JE on 1360.

**Threshold:** TBD — set after 2–3 months of normal noise (suggested start: $1K or 0.5% of 1360, whichever is greater).

> The SQL below is illustrative — **verify column names against the live `invoice_lifecycle` schema** (the `f3_revrec_je_id` / `f3_revrec_date` / `cogs_amount` markers are F3-owned; F15 keeps `payment_date`/`due_date` fresh). Prefer PO/SO Checks, which already encode this logic.

```sql
-- Fulfillments missing their F3 COGS JE (a prime variance source)
SELECT qbo_invoice_number, customer_name, fulfillment_date, cogs_amount
FROM invoice_lifecycle
WHERE fulfillment_date >= '[first_of_prior_month]'
  AND fulfillment_date <  '[first_of_current_month]'
  AND f3_revrec_je_id IS NULL
  AND status NOT IN ('voided', 'written_off');
```

---

## 10. History — the April 2026 cutover

- **Apr 1, 2026 — recognition-model change.** Switched from QBO inventory items (auto-COGS at invoice creation) to non-inventory items + F3 (COGS via JE at fulfillment), so revenue and COGS recognize together under the deferred-revenue model.
- **Apr 9–10, 2026 — the phantom-COGS incident.** Inactivating the old inventory items (which still carried on-hand qty) triggered QBO's auto inventory adjustment → **$743,535** of credits to 5101 (ref `ITEM_INACTIVATED`), distorting net income to ~$768K vs. ~$25K reality. Reversed 04/30 with `Dr 5101 / Cr 1360 $743,535`.
- **Pending — cutover bridge JE.** A one-time JE to align 1360 to Inflow's physical value at the cutover moment, offset to 3990 Cleanup Suspense. Until posted, every monthly tie-out carries an inherited gap indistinguishable from current drift.
- **Lesson:** before inactivating a QBO inventory-type item with on-hand qty, zero on-hand manually first (control the offset) or delete if it has no transaction history. Never inactivate without checking on-hand.

---

## 11. Open items

1. **Cost basis (high).** Receipts hit 1360 at actual bill amount; fulfillments leave at static `products.cogs` → drift. Leaning toward Inflow weighted-average canonical at fulfillment (depends on Inflow API exposing cost cleanly).
2. **Adjustment automation (med).** Planned **F14** poller for Inflow cycle-count/scrap/return history → queued JEs (with a review queue), replacing the manual monthly batch.
3. **Variance threshold** for tie-out — set after a few months.
4. **Inventory Variance / Scrap Expense accounts** — confirm/create in QBO before first batch.
5. **Cutover bridge JE** — post to close out the April cutover.
6. **Landed-cost method** — Inflow per-line allocation vs. a freight-in clearing account; document once chosen.

---

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

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

- `apps/accounting/README.md` — the five-rule JE model, the views, the data model, the F-series wiring. **The system of record; it wins on conflict.**
- **F11** `Normalize Ramp Bill` — the `PO-\d{6}` parse over `bill.reference || bill.memo`. If KISS PO numbering changes, update the regex **and** the §1 "put `PO-######` in the Memo" step.
- **F17** (`n8n/f17-post-receipt-je.json`) + **F18** (`n8n/f18-post-true-up-je.json`) + **F10** (the receipt/true-up branches that fire them) — `n8n/README.md`.
- **F12** — Ramp bill paid JE rules + refunds; **F3** (`n8n/f3-revenue-recognition.json`) — the COGS pair + cost source (revenue/commission/royalty side → Hardware Revenue playbook).
- The **Inventory Vendor Group** in Ramp + per-vendor `qbo_vendor_id` / `ramp_vendor_id` mapping (Vendors view).
- Supabase `products.cogs` (static cost basis) and the `invoice_lifecycle` markers used by the tie-out SQL.
- Account-number ↔ QBO-internal-id mapping if the chart of accounts changes.
