feat: add pricing refresh worker boundary
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-09 11:25:07 -04:00
parent 4effebd2f8
commit 39b4aab058
4 changed files with 108 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
export async function runPricingRefreshJob({ job, providerRegistry, pricingService }) {
if (!job || job.type !== 'PRICING_REFRESH') {
throw new Error('Unsupported pricing refresh job.');
}
const provider = providerRegistry?.get(job.providerId);
if (!provider) {
return {
...job,
status: 'FAILED',
error: 'Pricing provider is not configured.',
failedAt: new Date(),
};
}
try {
const observations = await provider.observe({
inventoryItemId: job.inventoryItemId,
productId: job.productId,
conditionId: job.conditionId,
});
const savedObservations = [];
for (const observation of observations) {
savedObservations.push(await pricingService.addObservation({
sellerId: job.sellerId,
...observation,
providerId: provider.id,
inventoryItemId: job.inventoryItemId,
productId: job.productId,
conditionId: job.conditionId,
}));
}
const valuation = await pricingService.calculate({
sellerId: job.sellerId,
inventoryItemId: job.inventoryItemId,
});
return {
...job,
status: 'COMPLETED',
observations: savedObservations,
valuation,
completedAt: new Date(),
};
} catch (error) {
return {
...job,
status: 'FAILED',
error: error instanceof Error ? error.message : 'Pricing refresh failed.',
failedAt: new Date(),
};
}
}
@@ -0,0 +1,48 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createPricingProvider, createPricingProviderRegistry } from '../../api/src/modules/pricing/providers/pricing-provider.js';
import { runPricingRefreshJob } from '../src/pricing-refresh-worker.js';
test('completes a pricing refresh through the provider and pricing service boundaries', async () => {
const providerRegistry = createPricingProviderRegistry([
createPricingProvider({
id: 'fixture-provider',
async observe() {
return [{ priceMinor: 2500, sourceIdentifier: 'fixture-1' }];
},
}),
]);
const saved = [];
const result = await runPricingRefreshJob({
job: {
id: 'job-1',
type: 'PRICING_REFRESH',
providerId: 'fixture-provider',
sellerId: 'seller-1',
productId: 'product-1',
inventoryItemId: 'inventory-1',
conditionId: 'condition-1',
},
providerRegistry,
pricingService: {
async addObservation(input) { saved.push(input); return input; },
async calculate() { return { valuation: { valueMinor: 2500 } }; },
},
});
assert.equal(result.status, 'COMPLETED');
assert.equal(result.valuation.valuation.valueMinor, 2500);
assert.equal(saved[0].providerId, 'fixture-provider');
});
test('fails a refresh clearly when a provider is unavailable', async () => {
const result = await runPricingRefreshJob({
job: { id: 'job-2', type: 'PRICING_REFRESH', providerId: 'missing' },
providerRegistry: createPricingProviderRegistry(),
pricingService: {},
});
assert.equal(result.status, 'FAILED');
assert.match(result.error, /not configured/);
});
+1
View File
@@ -149,6 +149,7 @@ Work through one bounded vertical slice at a time. The linked documentation rema
- No initial User/Seller is provisioned in the development database yet. Run `npm run provision:admin` after setting its required local `.env` variables. - No initial User/Seller is provisioned in the development database yet. Run `npm run provision:admin` after setting its required local `.env` variables.
- Listings are partially implemented: draft creation, seller-scoped reads, publication gating, lifecycle writes, archive behavior, sold/archived inventory checks, and item-owned image metadata are complete; storage-backed processing remains. - Listings are partially implemented: draft creation, seller-scoped reads, publication gating, lifecycle writes, archive behavior, sold/archived inventory checks, and item-owned image metadata are complete; storage-backed processing remains.
- Concrete provider adapters, asynchronous pricing refresh transport, richer exports/settings, full integration CI services, and deployment/backup automation remain incomplete to varying degrees. - Concrete provider adapters, asynchronous pricing refresh transport, richer exports/settings, full integration CI services, and deployment/backup automation remain incomplete to varying degrees.
- The pricing refresh worker boundary now has explicit completed/failed outcomes and routes provider observations through the pricing service; persistent refresh-job storage and a configured external provider remain deployment work.
- `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
+6
View File
@@ -104,6 +104,12 @@ Refresh:
Bulk refresh runs through BullMQ in the worker. Bulk refresh runs through BullMQ in the worker.
The worker-side refresh boundary is implemented with explicit `COMPLETED` and
`FAILED` outcomes. It resolves a registered provider, persists normalized
observations through the pricing service, and recalculates the valuation. A
provider that is missing or fails does not produce a fabricated value. Durable
refresh-job storage and external provider configuration remain deployment work.
Provider failure should not invalidate other valid observations. The failed provider is recorded for admin diagnostics. Provider failure should not invalidate other valid observations. The failed provider is recorded for admin diagnostics.
## Overrides ## Overrides