Docs.

Replacement Prices — Operations Runbook

operations/replacement-prices.md

items.replacement_value_usd_cents holds each item's real replacement (re-purchase) value — what it would cost to buy the item new today. It drives rental deposits and the insurance/COI threshold, so it needs to reflect current market price, not what we paid.

It is an admin-maintained field. The inventory importer (scripts/import-inventory.ts) does not write it (it is in the same not-touched bucket as cover_image, name_es, and the rate columns). The sheet's "Approximate Value" column is the purchase price the owner paid and feeds only assets.acquired_cost_usd_cents — that is often far below replacement cost (e.g. a lens bought used at $1,300 that is ~$3,500 new), which is exactly why the two must be tracked separately.

Each time a real value is set, items.replacement_value_updated_at is stamped with now(). A NULL value means "never researched"; an old stamp means "possibly stale, re-check."

See the schema reference in ../archive/inventory-schema-sheet-era.md.

When to run this

  • After a fresh inventory import that added new items (they arrive with replacement_value_usd_cents = NULL).
  • Periodically, to refresh stale values (street prices drift; new models supersede discontinued ones).

Step 1 — Get the worklist

List the items that need a value (NULL, or set longer ago than the freshness window):

# Human-readable worklist (default freshness window: 365 days)
pnpm tsx scripts/list-replacement-prices.ts

# Tighter window — anything set more than 180 days ago is "stale"
pnpm tsx scripts/list-replacement-prices.ts --stale-days 180

# Seed scripts/replacement-prices.csv with current rows (CSV to stdout)
pnpm tsx scripts/list-replacement-prices.ts --csv > scripts/replacement-prices.csv

# List EVERY item regardless of state
pnpm tsx scripts/list-replacement-prices.ts --all

Each line shows sku, manufacturer + name, category, and mpn — enough to look the item up.

Step 2 — Research current replacement cost

For each item, find what it costs to buy new today and record where you got the number:

  • Prefer, in order: the manufacturer's own store/MSRP → B&H PhotoFull Compass → another reputable US retailer. These are the same retailers used to source the product images, so the mpn usually resolves to an exact product page.
  • Use USD; the DB stores USD cents and converts to COP for display.
  • If a model is discontinued with no current listing, use the closest current equivalent (and note it in the source).
  • Record a source_url per item — the exact product page. This is your audit trail and survives in items.notes_internal as a replacement-source:<url> token.
  • If you can't price something confidently, leave its replacement_value_usd blank. The apply step will skip it (it stays NULL) and report it so a human can finish it. Do not guess wildly.

Step 3 — Fill in scripts/replacement-prices.csv

The reviewable artifact is a CSV keyed by sku (or id):

sku,name,replacement_value_usd,source_url
sony-fx6,FX6,5999.00,https://www.bhphotovideo.com/c/product/...
sony-e-pz-18-110mm-f4-g-oss,E PZ 18–110mm F4 G OSS,3499.00,https://www.bhphotovideo.com/c/product/...
  • name is for human review only; the importer ignores it.
  • Matching by sku updates all items sharing that sku (the sheet reuses a sku across multiple units of the same model). To target one specific row, use an id column with the item UUID instead.
  • A blank replacement_value_usd is skipped (left NULL).

JSON is also accepted (--file path/to/prices.json): an array of objects, or an object keyed by sku/id, each with replacement_value_usd + source_url.

Step 4 — Apply

# Preview without writing
pnpm tsx scripts/apply-replacement-prices.ts --file scripts/replacement-prices.csv --dry-run

# Apply for real (sets the value + stamps replacement_value_updated_at = now())
pnpm tsx scripts/apply-replacement-prices.ts --file scripts/replacement-prices.csv

The apply step is idempotent — re-running with an unchanged file is a no-op (it only writes rows whose value or source actually changed, and only those rows get a fresh replacement_value_updated_at). It reports how many rows were updated, how many were already current, how many were skipped (blank value), and any sku/id it couldn't find.

Verify

# Should print 0 once everything researchable has been applied
pnpm tsx scripts/list-replacement-prices.ts | head -1

Anything still listed either has a blank value in the CSV (needs a human) or is genuinely new since the last run.