fix(valuation): send the report email only when there are numbers
Implements the single api item of recoupable/chat#1969 — the valuation email consumes the valuation the handler already computed and is only sent when totalStreams > 0. The zero-stream shell email ("Your catalog valuation is ready" with no valuation, no streams, no explanation — three real sends, one to a real funnel signup on 2026-08-16) becomes unrepresentable.
What changed
- Handler gates and passes data (
runValuationHandler): one deferred block, in order — the email fires only insideif (aggregate && aggregate.totalStreams > 0)with the handler's own{valuation, totalStreams, measuredSongCount, catalogAgeYears, ageFlooredToOneYear}; then the lead capture reports the email's actual fate. "Email after valuation" is now a data dependency, not anafter()timing rule. sendValuationReportEmailis presentation-only: deleted itsselectCatalogById/selectCatalogMeasurementsAggregate/getCatalogEarliestReleaseDate/computeValuationBandimports, the enrichmentPromise.all, and the silent-degradetry/catch(the same swallow class #1965 removed from the roster attach). Kept: theemail_send_loglong-window idempotency guard, theno_emailskip, and the best-effort release table (buildReleaseRowsself-degrades to no table, never to no numbers).- Types enforce the decision:
ValuationReportEmailParams.valuation(+totalStreams,measuredSongCount,catalogAgeYears,ageFlooredToOneYear) are required; the shell arm ofrenderValuationBlock, the conditional disclaimer, and the unclaimed-snapshot deep-link fallback are deleted.git grep computeValuationBand lib/emailsreturns nothing. - Outcome-aware alert line (decision refined on the issue, 2026-08-18): the Telegram lead alert renders the email's real fate from the send result that was previously discarded —
Email: sent/Email: skipped (0 streams)/Email: skipped (no measurements)/Email: skipped (already sent)/Email: SEND FAILED — <error>— via the newtoValuationEmailOutcomemapper. - Young-catalog honesty:
computeValuationBandnow also returnsageFlooredToOneYear; the email carries a one-line caveat when a sub-year catalog is priced on the model's one-year floor (Math.max(1, …)).
LOC note
Source diff is +127/−111 (+16): the approved outcome-aware alert (mapper file + plumbing, ~+45) is the entirety of the growth — the gated-email consolidation itself is net negative, per the issue's deletion-led intent. Test diff adds the suites the issue's Done-when demands.
Verification (local)
- TDD red→green per unit across
computeValuationBand, render/types,sendValuationReportEmail(including "does no data fetching or valuation math of its own" — data-layer mocks asserted never called),toValuationEmailOutcome,captureValuationLead, and the handler gate (zero streams → no send call + skip in the alert payload; thrown send → 200 +failedoutcome). - Full domain suites green;
tsc --noEmitand eslint clean in touched domains (baseline noise elsewhere unchanged).
Preview verification (zero-stream run → no new Resend row + Email: skipped (0 streams) in Telegram; real-catalog run → exactly one email with dollars + caveat correctness; in-app zero-state check per the issue) to follow as a PR comment.
Merge note: independent — no other PR in this train. No docs or database PR (no contract change).
🤖 Generated with Claude Code
Summary by cubic
Stops sending “valuation is ready” emails without numbers. The handler now emails only when it measured streams (>0), passes its computed valuation to the template, and the lead alert reports the actual send outcome.
- Handler gates on
aggregate.totalStreams > 0, then callssendValuationReportEmailwith{snapshot, catalogId, catalogName, valuation, totalStreams, measuredSongCount, catalogAgeYears, ageFlooredToOneYear, artist}; deferred withafter()but driven by the precomputed data. Lead capture renders “Report email: sent | skipped (0 streams | measurements unavailable | already sent | no email on account) | SEND FAILED — ” viatoValuationEmailOutcome. sendValuationReportEmailis presentation‑only. It no longer fetches catalog data or recomputes valuation; it keeps the long‑window idempotency guard, theno_emailskip, and a best‑effort release table.- Types make numbers required; the shell render path and unclaimed‑snapshot deep‑link fallback are removed, so an empty valuation email is unrepresentable.
computeValuationBandaddsageFlooredToOneYearand guards unparseable dates by falling back to the default age; the email adds a one‑line caveat for sub‑year catalogs.
Migration
- Callers must pass computed numbers to
sendValuationReportEmail:snapshot,catalogId,catalogName,valuation,totalStreams,measuredSongCount,catalogAgeYears,ageFlooredToOneYear(optionalartist).
Summary by CodeRabbit
-
New Features
- Valuation reports now explain when calculations use a minimum one-year age.
- Reports consistently display all key statistics, including unavailable values.
- Lead alerts now show whether valuation email delivery was sent, skipped, or failed.
-
Bug Fixes
- Improved valuation report generation and delivery handling.
- Valuation emails are skipped when required measurements or stream data are unavailable, with the outcome clearly recorded.