diff --git a/lib/valuation/__tests__/fixtures/valuation-golden.json b/lib/valuation/__tests__/fixtures/valuation-golden.json new file mode 100644 index 0000000..672228d --- /dev/null +++ b/lib/valuation/__tests__/fixtures/valuation-golden.json @@ -0,0 +1,61 @@ +{ + "description": "Golden valuation-band cases shared verbatim between recoupable/api (lib/catalog/computeValuationBand.ts) and recoupable/marketing (lib/valuation/computeCatalogValuation.ts). The two copies of this file must stay byte-identical, and each repo has a test asserting its implementation reproduces every case to the cent. If a test fails, the implementations have diverged: fix the code or update this fixture in BOTH repos in the same change. Formula: annualGross = (totalStreams / catalogAgeYears) * 0.0035; band = annualGross * grossUp{1.25,1.4,1.6} * (1 - 0.15) * (1 - 0.25) * multiple{10,13,16}; catalogAgeYears = round(msSince(earliestReleaseDate) / 365.25d) clamped to >= 1, defaulting to 5 when earliestReleaseDate is null. Band keys here are low/mid/high; marketing names the middle value 'central'.", + "cases": [ + { + "name": "large catalog with unknown release date falls back to the 5y default age", + "input": { + "totalStreams": 16308837441, + "earliestReleaseDate": null, + "now": "2026-07-06T00:00:00.000Z" + }, + "expected": { + "catalogAgeYears": 5, + "low": 90972733.85, + "mid": 132456300.49, + "high": 186312158.93 + } + }, + { + "name": "zero streams yields a zero band", + "input": { + "totalStreams": 0, + "earliestReleaseDate": "2020-01-01T00:00:00.000Z", + "now": "2026-07-06T00:00:00.000Z" + }, + "expected": { + "catalogAgeYears": 7, + "low": 0, + "mid": 0, + "high": 0 + } + }, + { + "name": "ten-year catalog ages from its earliest release date", + "input": { + "totalStreams": 100000000, + "earliestReleaseDate": "2016-07-01T00:00:00.000Z", + "now": "2026-07-01T00:00:00.000Z" + }, + "expected": { + "catalogAgeYears": 10, + "low": 278906.25, + "mid": 406087.5, + "high": 571200 + } + }, + { + "name": "months-old catalog clamps age to one year", + "input": { + "totalStreams": 1000000, + "earliestReleaseDate": "2026-05-01T00:00:00.000Z", + "now": "2026-07-06T00:00:00.000Z" + }, + "expected": { + "catalogAgeYears": 1, + "low": 27890.63, + "mid": 40608.75, + "high": 57120 + } + } + ] +} diff --git a/lib/valuation/__tests__/valuationGoldenFixture.test.ts b/lib/valuation/__tests__/valuationGoldenFixture.test.ts new file mode 100644 index 0000000..10fd679 --- /dev/null +++ b/lib/valuation/__tests__/valuationGoldenFixture.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { computeCatalogValuation } from "../computeCatalogValuation"; +import fixture from "./fixtures/valuation-golden.json"; + +/** + * Cross-repo divergence guard (recoupable/chat#1850): the valuation model is + * implemented twice — here and in recoupable/api + * (lib/catalog/computeValuationBand.ts). This fixture's twin lives at + * api/lib/catalog/__tests__/fixtures/valuation-golden.json and the two JSON + * files must stay byte-identical. If this test fails, the marketing model has + * drifted from the shared formula: fix the code, or change the fixture in + * BOTH repos in the same coordinated change. The fixture's `mid` maps to this + * repo's `central` band key. + */ +describe("computeCatalogValuation golden fixture", () => { + it.each(fixture.cases)("$name", ({ input, expected }) => { + const { valueBand, catalogAgeYears } = computeCatalogValuation({ + totalStreams: input.totalStreams, + earliestReleaseDate: input.earliestReleaseDate, + now: new Date(input.now), + }); + + expect(catalogAgeYears).toBe(expected.catalogAgeYears); + // Fixture values are rounded to the cent; assert to within one cent. + expect(Math.abs(valueBand.low - expected.low)).toBeLessThanOrEqual(0.01); + expect(Math.abs(valueBand.central - expected.mid)).toBeLessThanOrEqual(0.01); + expect(Math.abs(valueBand.high - expected.high)).toBeLessThanOrEqual(0.01); + }); +});