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(),
});
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(
'inventory_images',
{
@@ -52,3 +52,15 @@ export function calculateMedianPriceMinor(prices) {
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) {
return {
@@ -39,5 +40,33 @@ export function createPricingRepository(db) {
}).returning();
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 { createPricingObservationSchema } from '@pokemon-inventory/validation/pricing';
import { createMarketValueOverrideSchema, createPricingObservationSchema } from '@pokemon-inventory/validation/pricing';
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 });
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;
},
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 }) {
await pricingRepository.markOutliersForSeller({ 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 === 'NO_DATA') throw createApplicationError('PRICING_DATA_UNAVAILABLE', 'No pricing observations are available.', 404);