# Inventory, cost, seats and shelfware

Rules behind INV01 to INV08 and USE01 to USE05 in stack_inventory.py, and how to turn spend into savings figures a finance lead will sign.

## Register columns

The register is [../assets/tool-register-template.csv](../assets/tool-register-template.csv). Multi-value cells use semicolons. Dates are YYYY-MM-DD; pass `--date-format` when the register uses another format, and a date the script cannot read stops the run with its row and column. Blank or "unknown" stays visible as unknown; it never counts as a pass.

- Identity: `tool_id` (stable, one per tool and environment; split production and sandbox when owners or connections differ), `tool_name`, `vendor`, `aliases` (billing descriptors and app names, for example `HUBSPOT;HubSpot Inc`), `environment`, `status`.
- Job: `capabilities` (IDs from [../assets/capability-vocabulary.csv](../assets/capability-vocabulary.csv)), `customer_actions` (channel:purpose, for example `email:trial_onboarding`, `task:lead_follow_up`, `audience:retargeting_trials`), `system_of_record_for` (object or object.field, for example `contact.lifecycle_stage`, `subscription.status`), `critical` (yes when the tool is on a traced workflow or is a system of record).
- Ownership: `business_owner_role`, `technical_owner_role`, `account_owned_by` (company, agency or unknown), `personal_data`, `dpa_on_file` (yes, no, unknown; n/a allowed for the DPA).
- Money: `spend_type` (subscription, usage, media, services, free, netted), `plan_or_edition`, `billing_unit`, `billing_period` (monthly, annual, multi-year), `contract_value_annual`, `currency`.
- Contract: `term_end`, `notice_days`, `notice_mechanism` (account_toggle, written_notice, unknown), `auto_renew`, `pricing_basis` (list, discounted), `discount_end`, `reduction_rule` (any_time, at_renewal, no_reduction_in_term, unknown). Monthly tools need no notice fields.
- Capacity: `paid_seats`, `committed_units`, `measured_peak_units`, `next_lower_tier_units`.
- Decision: `change_lead_days` (overrides the default lead), `disposition`, `disposition_destination`, `evidence_ref`, `evidence_date`.
- Status values: active, trial, migrating, retiring, retired, out_of_scope. A row counts unless it is retired or out_of_scope. Any other word, such as unknown or in use, stays in scope and a Note lists the row.
- Retired and out_of_scope rows still match charges and app names, so they stop INV01 and INV02 findings. Channel coverage lists them while any file still shows them, and their spend is reported apart from register tool spend. INV08 runs on retired rows; no other check runs on either. Give every out_of_scope row a reason in `evidence_ref`.

## Checks that print NOT CHECKED

A check runs only when its input columns hold data. Otherwise it prints NOT CHECKED with the column to fill, never PASS. Early registers are sparse, so expect these until the columns are filled, and re-run after steps 4 and 5:

- INV01, INV03, INV06 and CON02: at least one charge inside the window. INV03 also needs a payment source column, INV06 a row with `spend_type` subscription or usage, CON02 `contract_value_annual`.
- INV02: an SSO or OAuth file with rows. INV04: a tool in scope.
- INV05: `account_owned_by` on some row, or `critical` set to yes.
- INV07: `personal_data` on some row.
- CON01: a spend file or `contract_value_annual`, so the top share can be ranked.
- CON03, CON04 and CON06: an annual row with both `term_end` and `notice_days`. CON05: `discount_end` or `pricing_basis`.
- USE01 and USE03: an enabled seat row or an SSO sign-in on a register tool. USE02: `paid_seats` or `paid_seat`. USE05: `paid_seats`. USE04: `measured_peak_units` and `next_lower_tier_units` on one row.
- OVL01: `capabilities` on two rows. OVL02: `customer_actions`. OVL03: `system_of_record_for`.
- ACC02: a seat row with an enabled admin. ACC03: an `email` column. ACC04: the `mfa` column and an enabled admin on a critical tool. ACC05: the `idp_status` column. ACC06: an OAuth file with rows.
- INV08: prints only when the register has a retired row, and then needs a spend, SSO, OAuth or seat file.

On a partly filled register the PASS line names the population, for example "2 of 20 tools record system_of_record_for", so the reader sees how much was checked.

## Owners

- An owner is a role with authority and time, never "marketing". Business owner decides need and budget; technical owner runs access, connections and changes.
- Unknown owner: assign a temporary accountable owner to trace invoices, admin access and downstream consumers, and record the notice deadline while that happens. Silence is not permission to cancel.

## How the script matches vendors

- Names are lowercased; payment processor prefixes before a `*` are dropped (PAYPAL, SQ, PADDLE.NET, FS); any other `*` becomes a space, so `GOOGLE*ADS` stays "google ads"; punctuation, trailing legal suffixes (inc, llc, ltd, gmbh, corp, co and similar) and trailing digit groups are removed.
- An exact match on a tool name or alias wins when only one row holds it. Then a tool name or alias of 4 or more characters may match inside a longer descriptor, longest first.
- A vendor name never matches on its own, because one vendor often sells several products (Twilio, Google, Microsoft, Adobe, Salesforce). A descriptor that equals a register row's vendor stays unmatched, and its INV01 line names the rows with that vendor. If it is the same product, add the descriptor to that row's `aliases`.
- Two rows matching equally well leave the charge unmatched. Add an alias that matches each row's own billing descriptor.
- The channel coverage section lists the spend descriptors matched to each tool, and `--json` carries them per tool. Read it once: a second product from the same vendor shows up there as a descriptor nobody expected.
- Review the unmatched list by hand before raising INV01: a renamed product looks like a new vendor. Add any non-martech vendor that still appears (rent, payroll, cloud hosting, legal) as an out_of_scope row with a reason.

## Spend rules

- Only subscription and usage spend enters tool spend and the top-80% contract scope. Media and services are reported apart. Unregistered charges count as tool spend until classified.
- Use 12-month actuals. A monthly equivalent (annual divided by 12) is for comparison only; keep the payment date and the non-cancellable commitment.
- For usage tools, report a recent average and a plausible high month, never one convenient month.
- An annual payment that falls twice or not at all inside the window distorts CON02. Read the payment dates before reporting variance.
- INV06 (no evidence) means one of four things: a free tier, paid by another entity or department, prepaid before the window, or a dead row. Ask finance before calling it dead.
- INV08 (WARN, retired-still-active): a retired row with a charge or SSO sign-in in the last 45 days (`--dormant-days`) or after its `evidence_date`, an OAuth grant with users, or an enabled seat. The monthly re-run uses it to catch a retirement that did not finish or a renewal that fired after notice.

## Cost of ownership

Keep these apart, and keep one unallocated row so nothing is forced into a tool:

- Licences and commitments.
- Metered usage.
- Integration build and change work.
- Integration maintenance: internal hours at an agreed loaded rate. This is capacity, never cash.
- Shared operations and exit: admin, security review, parallel running, migration.

Never count the same hours twice: a contractor invoice that already includes review time replaces those internal hours.

Meters that surprise people (verify the current rules at run time):

- Zapier counts successful action steps, including successful error-handler steps and steps repeated by a full replay; trigger polls do not count.
- HubSpot private-app daily API limits differ by subscription tier and app type.
- A contact count and a monthly active user count bill the same audience very differently.

## Seats and dormancy

- Dormant: no successful sign-in for 45 days or more. The threshold follows CIS Controls v8 safeguard 5.3, which asks for dormant accounts to be disabled or deleted after 45 days of inactivity where supported. Its metric is enabled dormant accounts over all dormant accounts.
- `--dormant-days` changes the threshold; some teams use 90 days for licence reclaim. The report states the value used.
- A source that keeps fewer days than the threshold proves nothing (USE03). Entra Free keeps 7 days and P1 or P2 keep 30. A generic sign-in file needs `--sso-retention-days`; the script stops without it, and a tool whose only login source is shorter than the threshold gets USE01 NOT CHECKED, never PASS.
- An unknown `last_login` or `source_retention_days` counts as unknown activity (USE03), never as dormant. A blank `last_login` means no login. A filled date the script cannot read stops the run; pass `--date-format` for another format.
- The seat columns `mfa` and `idp_status` feed ACC04 and ACC05; their rules are in [governance-access-ai.md](governance-access-ai.md).
- Reclaimable seats = paid seats - active paid holders - dormant holders whose role needs periodic access - holders whose activity is unknown. Unknown is never reclaimable.
- Cash timing comes from the reduction rule: any_time means the next billing date; at_renewal means the term end; no_reduction_in_term means the term end, and a smaller quantity may reprice the unit.
- Removing a user changes access; it does not change the purchased quantity. Keep the access clean-up and the commercial change as two tasks with evidence for both.
- Check reassignment rules and paid-feature needs before treating seats as equal. A viewer, an admin, a seller and a service account can need different licences.

## Capacity tiers

- USE04 flags a tier as oversized when the 12-month peak multiplied by (1 + growth) fits the next lower tier. The default growth is 0%, printed as an assumption; re-run with `--growth` set to the owner's written forecast.
- Know what consumes the unit and when the count resets before proposing a change.

## Shelfware evidence ladder

Logins only screen. A tool is shelfware only with one of:

- No jobs, syncs, API calls or workflow references in 90 days. Use proxy signals: records written, API call usage, workflows that end in the tool, connected-app last activity, warehouse jobs.
- Feature-level telemetry from a SaaS management platform, when one exists.
- The owner's written confirmation that the work ended.

A one-seat dedupe, enrichment or sync tool can look idle while every lead passes through it.

## Savings in three figures

Report these separately and never add them together:

- Cash through a stated date: avoided invoices less transition costs, on real payment dates.
- Run-rate after commitments end: the retired commitment less the replacement's ongoing cost.
- Released capacity: verified hours and the work they move to. Salary does not disappear because a task got shorter.

Fictional example: an old tool costs 1,200 a month, billed monthly, with six non-cancellable months left. The replacement costs 500 a month from month one, and migration costs 6,000 upfront.

```text
Run-rate after retirement   = 1,200 - 500                      = 700 a month
First-year keep cost        = 12 x 1,200                       = 14,400
First-year change cost      = (6 x 1,200) + (12 x 500) + 6,000 = 19,200
First-year cash benefit     = 14,400 - 19,200                  = -4,800
Cumulative cash turns positive at the end of month 19
```

A cancelled login is not an avoided renewal, and a future run-rate is not cash already saved.

## Loss register for bad data

One row per distinct consequence, linked by a shared incident ID. Columns: process, defect, affected period, evidence location, occurrences, cost type, calculation, owner, confidence, and whether the amount is observed, estimated or conditional.

- Keep cash, staff capacity, uncertain commercial exposure and future incident exposure apart.
- Give every saving a realisation condition (for example "only if the manual report is retired").
- Never multiply missed leads or customers by lifetime value.
- Fictional month: a reconciliation invoice of 600 plus 120 of unneeded billable requests is 720 cash; 12 hours of report repair at 60 and 8 hours of lead corrections at 50 is 1,120 of capacity. A repair costing 4,000 upfront and 150 a month is judged against those two figures separately.
- Keep customer personal data out of the register; link to a restricted evidence location instead.
