From 7522826bd087dbff807abb5a97ef68dbd0a3dfa9 Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Tue, 1 Sep 2026 14:07:35 -0500 Subject: [PATCH] feat(intake): record who chose an item's price (#225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure and separately tested because the failure it guards is silent. Items are priced on arrival, so the schema no longer stops a number nobody chose reaching the storefront — the review queue does, by showing that nobody chose it, and an item selling at a default price looks exactly like one selling at a chosen price. Editing the number is the only thing that confirms it. Publishing an untouched field deliberately does not, because that would record "I did not look at this" as "I approved this". Co-Authored-By: Claude Opus 5 --- backend/src/intake/priceSource.ts | 34 ++++++++++++++++++++++++++ backend/tests/unit/priceSource.test.ts | 32 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 backend/src/intake/priceSource.ts create mode 100644 backend/tests/unit/priceSource.test.ts diff --git a/backend/src/intake/priceSource.ts b/backend/src/intake/priceSource.ts new file mode 100644 index 0000000..ff87238 --- /dev/null +++ b/backend/src/intake/priceSource.ts @@ -0,0 +1,34 @@ +/** + * Where an item's price came from, and when that changes. + * + * This is the protection that used to live in the schema. Items are priced on + * arrival — the model's suggestion, or the 80.00 default — so nothing stops a + * number nobody chose from reaching the storefront except the review queue + * showing that it was never chosen. + * + * Pure and separately tested because the failure is silent. An item that sells + * at a default price looks exactly like one that sells at a chosen price; + * 80.00 was picked precisely because it reads as a decision rather than as an + * obvious sentinel the way 0.00 would. + */ +export type PriceSource = 'default' | 'ai' | 'admin'; + +/** + * Editing the number is the admin taking responsibility for it, and it is the + * only thing that can. Publishing without touching the field deliberately does + * NOT confirm it — that would turn "I did not look at this" into "I approved + * this", which is the exact misrecording the review queue exists to prevent. + */ +export function nextPriceSource( + current: PriceSource, + submittedCents: number, + storedCents: number +): PriceSource { + if (current === 'admin') return 'admin'; + return submittedCents === storedCents ? current : 'admin'; +} + +/** Anything a person did not choose, which the screen marks visibly. */ +export function isUnconfirmed(source: PriceSource): boolean { + return source !== 'admin'; +} diff --git a/backend/tests/unit/priceSource.test.ts b/backend/tests/unit/priceSource.test.ts new file mode 100644 index 0000000..d967d35 --- /dev/null +++ b/backend/tests/unit/priceSource.test.ts @@ -0,0 +1,32 @@ +import { nextPriceSource, isUnconfirmed } from '../../src/intake/priceSource'; + +describe('nextPriceSource', () => { + // Touching the number is the admin taking responsibility for it. That is the + // only event that can confirm a price. + it('becomes admin when the number changes', () => { + expect(nextPriceSource('default', 9500, 8000)).toBe('admin'); + expect(nextPriceSource('ai', 4000, 4500)).toBe('admin'); + }); + + // Publishing without touching the field must NOT silently confirm it. That + // is the whole failure this screen exists to prevent: an item selling at a + // number nobody chose, with nothing recording that. + it('leaves an untouched price unconfirmed', () => { + expect(nextPriceSource('default', 8000, 8000)).toBe('default'); + expect(nextPriceSource('ai', 4500, 4500)).toBe('ai'); + }); + + // Already confirmed stays confirmed, including when re-submitted unchanged. + it('keeps admin once set', () => { + expect(nextPriceSource('admin', 9500, 9500)).toBe('admin'); + expect(nextPriceSource('admin', 7000, 9500)).toBe('admin'); + }); +}); + +describe('isUnconfirmed', () => { + it('treats anything but admin as unconfirmed', () => { + expect(isUnconfirmed('default')).toBe(true); + expect(isUnconfirmed('ai')).toBe(true); + expect(isUnconfirmed('admin')).toBe(false); + }); +});