feat: add local fixture pricing provider
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-09 14:02:03 -04:00
parent 0ffccf6617
commit a3f35c8cd1
9 changed files with 53 additions and 3 deletions
+1
View File
@@ -5,6 +5,7 @@ INITIAL_ADMIN_EMAIL=
INITIAL_ADMIN_PASSWORD= INITIAL_ADMIN_PASSWORD=
INITIAL_SELLER_NAME= INITIAL_SELLER_NAME=
MFA_ENCRYPTION_KEY= MFA_ENCRYPTION_KEY=
PRICING_FIXTURE_PROVIDER_ENABLED=false
PASSWORD_RESET_URL=http://localhost:5173/reset-password PASSWORD_RESET_URL=http://localhost:5173/reset-password
SMTP_FROM=no-reply@example.test SMTP_FROM=no-reply@example.test
SMTP_URL=smtp://localhost:1025 SMTP_URL=smtp://localhost:1025
+1
View File
@@ -37,6 +37,7 @@ jobs:
SMTP_URL=smtp://localhost:1025 SMTP_URL=smtp://localhost:1025
SMTP_FROM=no-reply@example.test SMTP_FROM=no-reply@example.test
MFA_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= MFA_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
PRICING_FIXTURE_PROVIDER_ENABLED=true
INITIAL_ADMIN_EMAIL=ci-admin@example.test INITIAL_ADMIN_EMAIL=ci-admin@example.test
INITIAL_ADMIN_PASSWORD=CiPassword123! INITIAL_ADMIN_PASSWORD=CiPassword123!
INITIAL_SELLER_NAME=CI Catalog Owner INITIAL_SELLER_NAME=CI Catalog Owner
+6 -1
View File
@@ -47,6 +47,8 @@ import { createPricingService } from '../modules/pricing/services/pricing-servic
import { createPricingRefreshJobRepository } from '../modules/pricing/repositories/pricing-refresh-job-repository.js'; import { createPricingRefreshJobRepository } from '../modules/pricing/repositories/pricing-refresh-job-repository.js';
import { createProviderMappingRepository } from '../modules/pricing/repositories/provider-mapping-repository.js'; import { createProviderMappingRepository } from '../modules/pricing/repositories/provider-mapping-repository.js';
import { createProviderMappingService } from '../modules/pricing/services/provider-mapping-service.js'; import { createProviderMappingService } from '../modules/pricing/services/provider-mapping-service.js';
import { createPricingProviderRegistry } from '../modules/pricing/providers/pricing-provider.js';
import { createFixturePricingProvider } from '../modules/pricing/providers/fixture-pricing-provider.js';
function parsePort(value) { function parsePort(value) {
const port = Number.parseInt(value ?? '3000', 10); const port = Number.parseInt(value ?? '3000', 10);
@@ -75,6 +77,9 @@ async function startServer() {
const pricingRepository = createPricingRepository(database.db); const pricingRepository = createPricingRepository(database.db);
const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db); const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db);
const providerMappingRepository = createProviderMappingRepository(database.db); const providerMappingRepository = createProviderMappingRepository(database.db);
const pricingProviders = process.env.PRICING_FIXTURE_PROVIDER_ENABLED === 'true'
? createPricingProviderRegistry([createFixturePricingProvider()])
: createPricingProviderRegistry();
const userRepository = createUserRepository(database.db); const userRepository = createUserRepository(database.db);
const sessionRepository = createSessionRepository(database.db); const sessionRepository = createSessionRepository(database.db);
const passwordResetRepository = createPasswordResetRepository(database.db); const passwordResetRepository = createPasswordResetRepository(database.db);
@@ -139,7 +144,7 @@ async function startServer() {
saleService: createSaleService({ saleRepository }), saleService: createSaleService({ saleRepository }),
acquisitionService: createAcquisitionService({ acquisitionRepository }), acquisitionService: createAcquisitionService({ acquisitionRepository }),
}), }),
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository }), pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository, pricingProviderRegistry: pricingProviders }),
providerMappingService: createProviderMappingService({ providerMappingRepository }), providerMappingService: createProviderMappingService({ providerMappingRepository }),
publicListingService: createPublicListingService({ listingRepository, publicImageService }), publicListingService: createPublicListingService({ listingRepository, publicImageService }),
productService: createProductService({ productRepository }), productService: createProductService({ productRepository }),
@@ -0,0 +1,18 @@
import { createPricingProvider } from './pricing-provider.js';
export function createFixturePricingProvider() {
return createPricingProvider({
id: 'fixture-provider',
async observe({ productId, inventoryItemId, conditionId }) {
return [{
productId,
inventoryItemId,
conditionId,
priceMinor: 2500,
currency: 'USD',
sourceIdentifier: `fixture-${inventoryItemId}`,
qualityScore: 50,
}];
},
});
}
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { createPricingProvider, createPricingProviderRegistry } from '../../src/modules/pricing/providers/pricing-provider.js'; import { createPricingProvider, createPricingProviderRegistry } from '../../src/modules/pricing/providers/pricing-provider.js';
import { createFixturePricingProvider } from '../../src/modules/pricing/providers/fixture-pricing-provider.js';
test('registers pricing providers behind a validated adapter contract', async () => { test('registers pricing providers behind a validated adapter contract', async () => {
const provider = createPricingProvider({ const provider = createPricingProvider({
@@ -19,4 +20,19 @@ test('rejects malformed provider adapters and responses', async () => {
assert.throws(() => createPricingProvider({ id: 'broken' }), /observe function is required/); assert.throws(() => createPricingProvider({ id: 'broken' }), /observe function is required/);
const provider = createPricingProvider({ id: 'broken', async observe() { return null; } }); const provider = createPricingProvider({ id: 'broken', async observe() { return null; } });
await assert.rejects(() => provider.observe({}), /must return an observation array/); await assert.rejects(() => provider.observe({}), /must return an observation array/);
});
test('fixture provider returns deterministic normalized observations', async () => {
const [observation] = await createFixturePricingProvider().observe({
productId: 'product-1', inventoryItemId: 'item-1', conditionId: 'condition-1',
});
assert.deepEqual(observation, {
productId: 'product-1',
inventoryItemId: 'item-1',
conditionId: 'condition-1',
priceMinor: 2500,
currency: 'USD',
sourceIdentifier: 'fixture-item-1',
qualityScore: 50,
});
}); });
+1 -1
View File
@@ -43,7 +43,7 @@ const acquisitionShipping = ref('');
const acquisitionTax = ref(''); const acquisitionTax = ref('');
const acquisitionDate = ref(new Date().toISOString().slice(0, 10)); const acquisitionDate = ref(new Date().toISOString().slice(0, 10));
const pricingInventoryId = ref(''); const pricingInventoryId = ref('');
const pricingProvider = ref('manual'); const pricingProvider = ref('fixture-provider');
const pricingSource = ref(''); const pricingSource = ref('');
const pricingPrice = ref(''); const pricingPrice = ref('');
const overridePrice = ref(''); const overridePrice = ref('');
+4 -1
View File
@@ -12,6 +12,7 @@ import { createPricingRepository } from '../../api/src/modules/pricing/repositor
import { createPricingRefreshJobRepository } from '../../api/src/modules/pricing/repositories/pricing-refresh-job-repository.js'; import { createPricingRefreshJobRepository } from '../../api/src/modules/pricing/repositories/pricing-refresh-job-repository.js';
import { createPricingService } from '../../api/src/modules/pricing/services/pricing-service.js'; import { createPricingService } from '../../api/src/modules/pricing/services/pricing-service.js';
import { createPricingProviderRegistry } from '../../api/src/modules/pricing/providers/pricing-provider.js'; import { createPricingProviderRegistry } from '../../api/src/modules/pricing/providers/pricing-provider.js';
import { createFixturePricingProvider } from '../../api/src/modules/pricing/providers/fixture-pricing-provider.js';
import { processNextPersistentImageDerivativeJob } from './image-derivative-worker.js'; import { processNextPersistentImageDerivativeJob } from './image-derivative-worker.js';
import { processNextPersistentPricingRefreshJob, runPricingRefreshJob } from './pricing-refresh-worker.js'; import { processNextPersistentPricingRefreshJob, runPricingRefreshJob } from './pricing-refresh-worker.js';
@@ -23,7 +24,9 @@ async function runWorker() {
const imageStorage = createLocalImageStorage(imageStorageRoot); const imageStorage = createLocalImageStorage(imageStorageRoot);
const jobRepository = createImageDerivativeJobRepository(database.db); const jobRepository = createImageDerivativeJobRepository(database.db);
const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db); const pricingRefreshJobRepository = createPricingRefreshJobRepository(database.db);
const pricingProviderRegistry = createPricingProviderRegistry(); const pricingProviderRegistry = process.env.PRICING_FIXTURE_PROVIDER_ENABLED === 'true'
? createPricingProviderRegistry([createFixturePricingProvider()])
: createPricingProviderRegistry();
const pricingService = createPricingService({ const pricingService = createPricingService({
pricingRepository: createPricingRepository(database.db), pricingRepository: createPricingRepository(database.db),
pricingProviderRegistry, pricingProviderRegistry,
+1
View File
@@ -153,6 +153,7 @@ Work through one bounded vertical slice at a time. The linked documentation rema
- Pricing refresh requests now enqueue seller-scoped jobs and expose a safe `GET /api/v1/admin/jobs/:id` polling contract instead of doing provider work inline. - Pricing refresh requests now enqueue seller-scoped jobs and expose a safe `GET /api/v1/admin/jobs/:id` polling contract instead of doing provider work inline.
- Product-level provider mappings now persist seller-scoped `PRODUCT`/`SEARCH` relationships and support active listing and controlled disabling. - Product-level provider mappings now persist seller-scoped `PRODUCT`/`SEARCH` relationships and support active listing and controlled disabling.
- Pricing refresh enqueue now resolves its provider from an active Product mapping and rejects unmapped Products explicitly. - Pricing refresh enqueue now resolves its provider from an active Product mapping and rejects unmapped Products explicitly.
- Local development and CI can enable a deterministic fixture provider for end-to-end refresh coverage; production provider adapters remain explicitly configured deployment boundaries.
- `npm install` currently reports dependency audit findings. Review and remediate them before production deployment; do not use `npm audit fix --force` without reviewing its changes. - `npm install` currently reports dependency audit findings. Review and remediate them before production deployment; do not use `npm audit fix --force` without reviewing its changes.
## Local Commands ## Local Commands
+5
View File
@@ -28,6 +28,11 @@ pricing service, and recalculates the item valuation. Missing providers return
an explicit unavailable response; the application never fabricates observations an explicit unavailable response; the application never fabricates observations
or values. or values.
Local development and CI may enable the deterministic `fixture-provider` with
`PRICING_FIXTURE_PROVIDER_ENABLED=true`. It exists only for repeatable
workflow/testing coverage and is disabled by default; production deployments
must register an approved external adapter instead.
Use officially supported APIs or permitted sources. Do not invent APIs or bypass restrictions. Use officially supported APIs or permitted sources. Do not invent APIs or bypass restrictions.
## Provider Product Mapping ## Provider Product Mapping