feat: add pricing quality controls
Quality / test (push) Canceled after 0s

Add robust outlier detection, stale valuation metadata, and audited seller-scoped market-value overrides with migration and tests.
This commit is contained in:
2026-09-09 01:19:51 -04:00
parent ba88814af0
commit 75c0e89511
12 changed files with 141 additions and 3 deletions
@@ -289,6 +289,16 @@ export const marketValuations = pgTable('market_valuations', {
calculatedAt: timestamp('calculated_at', { withTimezone: true }).defaultNow().notNull(), calculatedAt: timestamp('calculated_at', { withTimezone: true }).defaultNow().notNull(),
}); });
export const marketValueOverrides = pgTable('market_value_overrides', {
id: uuid('id').defaultRandom().primaryKey(),
inventoryItemId: uuid('inventory_item_id').notNull().references(() => inventoryItems.id),
valueMinor: integer('value_minor').notNull(),
currency: text('currency').notNull(),
reason: text('reason').notNull(),
createdBy: uuid('created_by').notNull().references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
export const inventoryImages = pgTable( export const inventoryImages = pgTable(
'inventory_images', 'inventory_images',
{ {
@@ -52,3 +52,15 @@ export function calculateMedianPriceMinor(prices) {
return sorted[middle]; return sorted[middle];
} }
export function identifyOutlierPrices(prices) {
if (!Array.isArray(prices) || prices.length < 4) return new Set();
const sorted = [...prices].sort((left, right) => left - right);
const middle = Math.floor(sorted.length / 2);
const median = sorted.length % 2
? sorted[middle]
: (sorted[middle - 1] + sorted[middle]) / 2;
const lowerFence = median / 3;
const upperFence = median * 3;
return new Set(prices.filter((price) => price < lowerFence || price > upperFence));
}
@@ -1,6 +1,7 @@
import { and, desc, eq } from 'drizzle-orm'; import { and, desc, eq, inArray } from 'drizzle-orm';
import { inventoryItems, marketValuations, pricingObservations, products } from '../../../infrastructure/database/schema.js'; import { inventoryItems, marketValueOverrides, marketValuations, pricingObservations, products, securityAuditEvents } from '../../../infrastructure/database/schema.js';
import { identifyOutlierPrices } from '../domain/pricing-observation.js';
export function createPricingRepository(db) { export function createPricingRepository(db) {
return { return {
@@ -39,5 +40,33 @@ export function createPricingRepository(db) {
}).returning(); }).returning();
return { status: 'CALCULATED', valuation, observations }; return { status: 'CALCULATED', valuation, observations };
}, },
async markOutliersForSeller({ inventoryItemId, sellerId }) {
const [ownedItem] = await db.select({ id: inventoryItems.id }).from(inventoryItems)
.innerJoin(products, eq(products.id, inventoryItems.productId))
.where(and(eq(inventoryItems.id, inventoryItemId), eq(products.sellerId, sellerId)));
if (!ownedItem) return { status: 'NOT_FOUND' };
const observations = await db.select().from(pricingObservations).where(eq(pricingObservations.inventoryItemId, inventoryItemId));
const prices = observations.map(({ priceMinor }) => priceMinor);
const outlierPrices = identifyOutlierPrices(prices);
await db.update(pricingObservations).set({ isOutlier: false, outlierReason: null }).where(eq(pricingObservations.inventoryItemId, inventoryItemId));
if (outlierPrices.size) {
await db.update(pricingObservations).set({ isOutlier: true, outlierReason: 'Outside the 1.5 IQR range.' })
.where(and(eq(pricingObservations.inventoryItemId, inventoryItemId), inArray(pricingObservations.priceMinor, [...outlierPrices])));
}
return { status: 'UPDATED' };
},
async createOverride({ inventoryItemId, sellerId, actorUserId, valueMinor, currency, reason }) {
return db.transaction(async (transaction) => {
const [ownedItem] = await transaction.select({ id: inventoryItems.id }).from(inventoryItems)
.innerJoin(products, eq(products.id, inventoryItems.productId))
.where(and(eq(inventoryItems.id, inventoryItemId), eq(products.sellerId, sellerId)));
if (!ownedItem) return { status: 'NOT_FOUND' };
const [override] = await transaction.insert(marketValueOverrides).values({ inventoryItemId, valueMinor, currency, reason, createdBy: actorUserId }).returning();
await transaction.insert(securityAuditEvents).values({ userId: actorUserId, event: 'MARKET_VALUE_OVERRIDDEN', metadata: { inventoryItemId, overrideId: override.id, reason } });
return { status: 'CREATED', override };
});
},
}; };
} }
@@ -1,5 +1,5 @@
import { toAdminMarketValuationDto, toAdminPricingObservationDto } from '@pokemon-inventory/contracts/pricing'; import { toAdminMarketValuationDto, toAdminPricingObservationDto } from '@pokemon-inventory/contracts/pricing';
import { createPricingObservationSchema } from '@pokemon-inventory/validation/pricing'; import { createMarketValueOverrideSchema, createPricingObservationSchema } from '@pokemon-inventory/validation/pricing';
import { createApplicationError } from '../../../shared/errors/application-error.js'; import { createApplicationError } from '../../../shared/errors/application-error.js';
@@ -40,4 +40,17 @@ export async function registerAdminPricingRoutes(app, options) {
const valuation = await pricingService.calculate({ sellerId: request.admin.sellerId, inventoryItemId: request.params.id }); const valuation = await pricingService.calculate({ sellerId: request.admin.sellerId, inventoryItemId: request.params.id });
return reply.code(200).send({ data: toAdminMarketValuationDto(valuation) }); return reply.code(200).send({ data: toAdminMarketValuationDto(valuation) });
}); });
app.post('/inventory/:id/valuation/override', async (request, reply) => {
await authorizeUnsafeRequest?.(request.cookies[sessionCookieName], request.headers['x-csrf-token']);
const result = createMarketValueOverrideSchema.safeParse(request.body);
if (!result.success) return validationError(reply, result);
const override = await pricingService.override({
...result.data,
inventoryItemId: request.params.id,
sellerId: request.admin.sellerId,
actorUserId: request.admin.userId,
});
return reply.code(201).send({ data: override });
});
} }
@@ -10,7 +10,22 @@ export function createPricingService({ pricingRepository }) {
return result.observation; return result.observation;
}, },
async markOutliers({ sellerId, inventoryItemId }) {
const result = await pricingRepository.markOutliersForSeller({ sellerId, inventoryItemId });
if (result.status === 'NOT_FOUND') throw createApplicationError('INVENTORY_NOT_FOUND', 'Inventory item was not found.', 404);
return result;
},
async override({ sellerId, actorUserId, inventoryItemId, valueMinor, currency = 'USD', reason }) {
if (!Number.isInteger(valueMinor) || valueMinor < 0) throw createApplicationError('INVALID_OVERRIDE_VALUE', 'Override value must be a non-negative integer minor unit.', 400);
if (typeof reason !== 'string' || reason.trim().length < 3) throw createApplicationError('OVERRIDE_REASON_REQUIRED', 'An override reason is required.', 400);
const result = await pricingRepository.createOverride({ sellerId, actorUserId, inventoryItemId, valueMinor, currency: currency.toUpperCase(), reason: reason.trim() });
if (result.status === 'NOT_FOUND') throw createApplicationError('INVENTORY_NOT_FOUND', 'Inventory item was not found.', 404);
return result.override;
},
async calculate({ sellerId, inventoryItemId }) { async calculate({ sellerId, inventoryItemId }) {
await pricingRepository.markOutliersForSeller({ sellerId, inventoryItemId });
const result = await pricingRepository.calculateForSeller({ sellerId, inventoryItemId }); const result = await pricingRepository.calculateForSeller({ sellerId, inventoryItemId });
if (result.status === 'NOT_FOUND') throw createApplicationError('INVENTORY_NOT_FOUND', 'Inventory item was not found.', 404); if (result.status === 'NOT_FOUND') throw createApplicationError('INVENTORY_NOT_FOUND', 'Inventory item was not found.', 404);
if (result.status === 'NO_DATA') throw createApplicationError('PRICING_DATA_UNAVAILABLE', 'No pricing observations are available.', 404); if (result.status === 'NO_DATA') throw createApplicationError('PRICING_DATA_UNAVAILABLE', 'No pricing observations are available.', 404);
@@ -10,6 +10,9 @@ test('records an owned pricing observation and calculates a valuation', async ()
assert.equal(sellerId, 'seller-1'); assert.equal(sellerId, 'seller-1');
return { status: 'CREATED', observation }; return { status: 'CREATED', observation };
}, },
async markOutliersForSeller() {
return { status: 'UPDATED' };
},
async calculateForSeller() { async calculateForSeller() {
return { status: 'CALCULATED', valuation: { valueMinor: 2500 }, observations: [] }; return { status: 'CALCULATED', valuation: { valueMinor: 2500 }, observations: [] };
}, },
@@ -23,4 +26,12 @@ test('records an owned pricing observation and calculates a valuation', async ()
assert.equal(observation.priceMinor, 2500); assert.equal(observation.priceMinor, 2500);
const valuation = await service.calculate({ sellerId: 'seller-1', inventoryItemId: 'inventory-1' }); const valuation = await service.calculate({ sellerId: 'seller-1', inventoryItemId: 'inventory-1' });
assert.equal(valuation.valuation.valueMinor, 2500); assert.equal(valuation.valuation.valueMinor, 2500);
});
test('requires a reason for an audited market-value override', async () => {
const service = createPricingService({ pricingRepository: { async createOverride() { throw new Error('should not run'); } } });
await assert.rejects(
() => service.override({ sellerId: 'seller-1', actorUserId: 'user-1', inventoryItemId: 'inventory-1', valueMinor: 2000, reason: 'x' }),
(error) => error.code === 'OVERRIDE_REASON_REQUIRED' && error.statusCode === 400,
);
}); });
@@ -8,6 +8,7 @@ import {
import { import {
calculateMedianPriceMinor, calculateMedianPriceMinor,
createPricingObservation, createPricingObservation,
identifyOutlierPrices,
} from '../../src/modules/pricing/domain/pricing-observation.js'; } from '../../src/modules/pricing/domain/pricing-observation.js';
test('calculates acquisition cost basis from all cost components', () => { test('calculates acquisition cost basis from all cost components', () => {
@@ -57,3 +58,8 @@ test('creates pricing observations and calculates a median observed price', () =
assert.equal(observation.priceMinor, 2499); assert.equal(observation.priceMinor, 2499);
assert.equal(calculateMedianPriceMinor([1199, 2499, 2000]), 2000); assert.equal(calculateMedianPriceMinor([1199, 2499, 2000]), 2000);
}); });
test('identifies extreme pricing observations without discarding normal values', () => {
assert.deepEqual([...identifyOutlierPrices([1000, 1050, 1100, 5000])], [5000]);
assert.equal(identifyOutlierPrices([1000, 1050, 1100]).size, 0);
});
@@ -25,4 +25,23 @@ test('accepts a protected pricing observation and valuation request', async (tes
assert.equal(valuationResponse.statusCode, 200); assert.equal(valuationResponse.statusCode, 200);
assert.equal(valuationResponse.json().data.valueMinor, 2500); assert.equal(valuationResponse.json().data.valueMinor, 2500);
assert.equal(csrfChecks, 2); assert.equal(csrfChecks, 2);
});
test('accepts a CSRF-protected market-value override', async (testContext) => {
let received;
const app = buildApp({
authenticateAdmin: async () => ({ userId: 'user-1', sellerId: 'seller-1' }),
authorizeUnsafeRequest: async () => {},
pricingService: { async override(input) { received = input; return { id: 'override-1', ...input }; } },
});
testContext.after(() => app.close());
const response = await app.inject({
method: 'POST',
url: '/api/v1/admin/inventory/21eb218b-7a95-4f80-a9e3-e306f7ca1f2e/valuation/override',
headers: { 'x-csrf-token': 'csrf' },
payload: { valueMinor: 2600, reason: 'Verified recent sale' },
});
assert.equal(response.statusCode, 201);
assert.equal(received.actorUserId, 'user-1');
assert.equal(response.json().data.valueMinor, 2600);
}); });
@@ -0,0 +1,9 @@
CREATE TABLE "market_value_overrides" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"inventory_item_id" uuid NOT NULL REFERENCES "inventory_items"("id"),
"value_minor" integer NOT NULL,
"currency" text NOT NULL,
"reason" text NOT NULL,
"created_by" uuid NOT NULL REFERENCES "users"("id"),
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
+7
View File
@@ -113,6 +113,13 @@
"when": 1789891200000, "when": 1789891200000,
"tag": "0015_pricing", "tag": "0015_pricing",
"breakpoints": true "breakpoints": true
},
{
"idx": 16,
"version": "7",
"when": 1789977600000,
"tag": "0016_pricing_quality",
"breakpoints": true
} }
] ]
} }
+1
View File
@@ -26,6 +26,7 @@ export function toAdminMarketValuationDto({ valuation, observations }) {
confidence: valuation.confidence, confidence: valuation.confidence,
sourceCount: valuation.sourceCount, sourceCount: valuation.sourceCount,
calculatedAt: toIsoString(valuation.calculatedAt), calculatedAt: toIsoString(valuation.calculatedAt),
stale: Date.now() - new Date(valuation.calculatedAt).getTime() > 7 * 24 * 60 * 60 * 1000,
observations: observations.map(toAdminPricingObservationDto), observations: observations.map(toAdminPricingObservationDto),
}; };
} }
+6
View File
@@ -10,4 +10,10 @@ export const createPricingObservationSchema = z.object({
conditionId: z.uuid(), conditionId: z.uuid(),
sourceUrl: z.string().trim().url().optional(), sourceUrl: z.string().trim().url().optional(),
qualityScore: z.int().min(0).max(100).optional(), qualityScore: z.int().min(0).max(100).optional(),
}).strict();
export const createMarketValueOverrideSchema = z.object({
valueMinor: z.int().nonnegative(),
currency: z.string().trim().regex(/^[A-Za-z]{3}$/).optional(),
reason: z.string().trim().min(3).max(1000),
}).strict(); }).strict();