# Phase 4 — Margin & COGS data contract

## Purpose
Standardize how **cost at time of sale/refund** and **margin analytics** read from:
- `clients/{clientId}/purchases/*` (canonical buy price per line)
- `clients/{clientId}/inventoryLedger/*` (movement audit, including `purchase` rows with `unitCost`)
- `clients/{clientId}/orders/*` (revenue, channel, optional `refundedQty` on lines)
- `clients/{clientId}/products/*` (fallback pricing for heuristic cost when no purchase history)

## Ledger event fields (Phase 4 optional enrichment)

For `eventType` in `sale` | `refund`, writers SHOULD set when known:

| Field | Type | Meaning |
|-------|------|--------|
| `unitCost` | number \| null | **Unit cost basis at event time** (per unit or per kg matching `uom`). Used as COGS snapshot for analytics. |
| `costMethod` | string | e.g. `moving_avg_v1`, `ledger_unitCost`, `heuristic_sale_065`, `missing` |
| `costSourceRef` | object \| null | Optional pointer (e.g. `{ kind: 'purchase_batch', purchaseId }`) |

`schemaVersion` on the row is **2** when `costMethod` or explicit `unitCostAtSale` is present; otherwise remains **1** (Phase 3 rows).

> Note: `buildLedgerEvent` already had `unitCost`; Phase 4 uses it as the **sale-time COGS unit** for sale/refund rows. `unitCostAtSale` is an alias only in docs if both exist; prefer single field `unitCost` + `costMethod`.

## Analytics engine (moving average v1)

1. **Aggregate buys** from all `purchases[].items` and from `inventoryLedger` rows with `eventType === 'purchase'` where `unitCost` is set and `qtyDelta > 0`.
2. Per SKU key `productId|variantIdOrEmpty`: `avgUnitCost = sum(netLineCost) / sum(qty)`.
3. **Sale COGS** for an order line with net sold qty `q`: `q * avgUnitCost` (same `uom` as stock: `unit` or `kg`).
4. **Fallback** when no purchases: `costMethod = heuristic_sale_065` using `salePrice * 0.65` (variant or product), flagged in data-quality output.
5. **Refunds (POS lines)**: when `refundedQty` exists on `items[]`, reduce revenue and COGS by `refundedQty * price` and `refundedQty * unitCost` using the **same** avg map (v1 approximation).

## Shrinkage / wastage cost (reporting)

- `eventType === 'wastage'` or `adjustment` with negative `qtyDelta`: estimated cost `abs(qtyDelta) * unitCost` where `unitCost` is taken from the ledger row if present, else from avg map.

## Rounding

- Money: 2 decimals (`roundMoney2` from `pos-finance.js`).
- Weight kg: 3 decimals where applicable.

## Future (not in v1)

- Time-phased true moving average simulation (consume purchases in strict chronological order vs sales).

## Related

- Phase 5 finance contract v2: [PHASE5_MARGIN.md](./PHASE5_MARGIN.md)
- Deterministic checks: `margin-analytics-fixtures.js` (`runMarginAnalyticsFixtureChecks`).
- `siteSettings.marginAnalytics` (`lowMarginPct`, `minRevenueForLowMargin`, `shrinkageCostAlert`) is read/written from **Analytics** (Phase 4 UI).
