diff --git a/.env.example b/.env.example index 6ec3be6..cc0ccd5 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ INITIAL_ADMIN_EMAIL= INITIAL_ADMIN_PASSWORD= INITIAL_SELLER_NAME= MFA_ENCRYPTION_KEY= +PRICING_FIXTURE_PROVIDER_ENABLED=false PASSWORD_RESET_URL=http://localhost:5173/reset-password SMTP_FROM=no-reply@example.test SMTP_URL=smtp://localhost:1025 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index e3e7df7..3ea71c6 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -37,6 +37,7 @@ jobs: SMTP_URL=smtp://localhost:1025 SMTP_FROM=no-reply@example.test MFA_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + PRICING_FIXTURE_PROVIDER_ENABLED=true INITIAL_ADMIN_EMAIL=ci-admin@example.test INITIAL_ADMIN_PASSWORD=CiPassword123! INITIAL_SELLER_NAME=CI Catalog Owner diff --git a/apps/api/src/app/server.js b/apps/api/src/app/server.js index 4e63cef..efaa546 100644 --- a/apps/api/src/app/server.js +++ b/apps/api/src/app/server.js @@ -47,6 +47,8 @@ import { createPricingService } from '../modules/pricing/services/pricing-servic import { createPricingRefreshJobRepository } from '../modules/pricing/repositories/pricing-refresh-job-repository.js'; import { createProviderMappingRepository } from '../modules/pricing/repositories/provider-mapping-repository.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) { const port = Number.parseInt(value ?? '3000', 10); @@ -75,6 +77,9 @@ async function startServer() { const pricingRepository = createPricingRepository(database.db); const pricingRefreshJobRepository = createPricingRefreshJobRepository(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 sessionRepository = createSessionRepository(database.db); const passwordResetRepository = createPasswordResetRepository(database.db); @@ -139,7 +144,7 @@ async function startServer() { saleService: createSaleService({ saleRepository }), acquisitionService: createAcquisitionService({ acquisitionRepository }), }), - pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository }), + pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository, pricingProviderRegistry: pricingProviders }), providerMappingService: createProviderMappingService({ providerMappingRepository }), publicListingService: createPublicListingService({ listingRepository, publicImageService }), productService: createProductService({ productRepository }), diff --git a/apps/api/src/modules/pricing/providers/fixture-pricing-provider.js b/apps/api/src/modules/pricing/providers/fixture-pricing-provider.js new file mode 100644 index 0000000..0298ce5 --- /dev/null +++ b/apps/api/src/modules/pricing/providers/fixture-pricing-provider.js @@ -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, + }]; + }, + }); +} diff --git a/apps/api/tests/application/pricing-provider.test.js b/apps/api/tests/application/pricing-provider.test.js index c984947..4cccc14 100644 --- a/apps/api/tests/application/pricing-provider.test.js +++ b/apps/api/tests/application/pricing-provider.test.js @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; 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 () => { const provider = createPricingProvider({ @@ -19,4 +20,19 @@ test('rejects malformed provider adapters and responses', async () => { assert.throws(() => createPricingProvider({ id: 'broken' }), /observe function is required/); const provider = createPricingProvider({ id: 'broken', async observe() { return null; } }); 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, + }); }); \ No newline at end of file diff --git a/apps/web/src/pages/AdminHomePage.vue b/apps/web/src/pages/AdminHomePage.vue index 9431961..24599a3 100644 --- a/apps/web/src/pages/AdminHomePage.vue +++ b/apps/web/src/pages/AdminHomePage.vue @@ -43,7 +43,7 @@ const acquisitionShipping = ref(''); const acquisitionTax = ref(''); const acquisitionDate = ref(new Date().toISOString().slice(0, 10)); const pricingInventoryId = ref(''); -const pricingProvider = ref('manual'); +const pricingProvider = ref('fixture-provider'); const pricingSource = ref(''); const pricingPrice = ref(''); const overridePrice = ref(''); diff --git a/apps/worker/src/worker-runtime.js b/apps/worker/src/worker-runtime.js index 6dd22ef..d71d729 100644 --- a/apps/worker/src/worker-runtime.js +++ b/apps/worker/src/worker-runtime.js @@ -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 { createPricingService } from '../../api/src/modules/pricing/services/pricing-service.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 { processNextPersistentPricingRefreshJob, runPricingRefreshJob } from './pricing-refresh-worker.js'; @@ -23,7 +24,9 @@ async function runWorker() { const imageStorage = createLocalImageStorage(imageStorageRoot); const jobRepository = createImageDerivativeJobRepository(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({ pricingRepository: createPricingRepository(database.db), pricingProviderRegistry, diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index 95b9127..fdd81aa 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -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. - 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. +- 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. ## Local Commands diff --git a/docs/PRICING.md b/docs/PRICING.md index ac3ed65..1b11006 100644 --- a/docs/PRICING.md +++ b/docs/PRICING.md @@ -28,6 +28,11 @@ pricing service, and recalculates the item valuation. Missing providers return an explicit unavailable response; the application never fabricates observations 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. ## Provider Product Mapping