diff --git a/apps/api/src/app/server.js b/apps/api/src/app/server.js index e6c8742..b60bb98 100644 --- a/apps/api/src/app/server.js +++ b/apps/api/src/app/server.js @@ -144,6 +144,7 @@ async function startServer() { saleService: createSaleService({ saleRepository }), acquisitionService: createAcquisitionService({ acquisitionRepository }), pricingRepository, + pricingRefreshJobRepository, }), pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository, pricingProviderRegistry: pricingProviders }), providerMappingService: createProviderMappingService({ providerMappingRepository }), diff --git a/apps/api/src/modules/dashboard/services/dashboard-service.js b/apps/api/src/modules/dashboard/services/dashboard-service.js index 272ae4b..8c657b1 100644 --- a/apps/api/src/modules/dashboard/services/dashboard-service.js +++ b/apps/api/src/modules/dashboard/services/dashboard-service.js @@ -1,13 +1,14 @@ -export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService, pricingRepository }) { +export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService, pricingRepository, pricingRefreshJobRepository }) { return { async get({ sellerId }) { - const [products, inventory, listings, sales, acquisitions, valuations] = await Promise.all([ + const [products, inventory, listings, sales, acquisitions, valuations, failedPricingJobs] = await Promise.all([ productService.listBySellerId(sellerId), inventoryItemService.listBySellerId(sellerId), listingService.list({ sellerId }), saleService.list({ sellerId }), acquisitionService.list({ sellerId }), pricingRepository?.listLatestValuationsForSeller(sellerId) ?? [], + pricingRefreshJobRepository?.countFailedForSeller(sellerId) ?? 0, ]); const activeListings = listings.filter(({ status }) => ['AVAILABLE', 'PENDING'].includes(status)); @@ -28,6 +29,7 @@ export function createDashboardService({ productService, inventoryItemService, l missingImages: listings.filter(({ imageCount }) => imageCount < 1).length, missingPricing: inventory.filter(({ id }) => !valuationByItem.has(id)).length, stalePricing: valuations.filter(({ calculatedAt }) => Date.now() - new Date(calculatedAt).getTime() > staleThreshold).length, + failedPricingJobs, }, financials: { totalAcquisitionCostMinor: total(acquisitions, 'costBasisMinor'), diff --git a/apps/api/src/modules/pricing/repositories/pricing-refresh-job-repository.js b/apps/api/src/modules/pricing/repositories/pricing-refresh-job-repository.js index d8e7628..f3cb607 100644 --- a/apps/api/src/modules/pricing/repositories/pricing-refresh-job-repository.js +++ b/apps/api/src/modules/pricing/repositories/pricing-refresh-job-repository.js @@ -31,6 +31,13 @@ export function createPricingRefreshJobRepository(db) { return job ?? null; }, + async countFailedForSeller(sellerId) { + const rows = await db.select({ id: pricingRefreshJobs.id }) + .from(pricingRefreshJobs) + .where(and(eq(pricingRefreshJobs.sellerId, sellerId), eq(pricingRefreshJobs.status, 'FAILED'))); + return rows.length; + }, + async complete(id, observations, valuation) { const [completed] = await db.update(pricingRefreshJobs) .set({ status: 'COMPLETED', observations, valuation, error: null, updatedAt: new Date() }) diff --git a/apps/api/tests/application/dashboard-service.test.js b/apps/api/tests/application/dashboard-service.test.js index 1620ba9..6c239aa 100644 --- a/apps/api/tests/application/dashboard-service.test.js +++ b/apps/api/tests/application/dashboard-service.test.js @@ -11,6 +11,7 @@ test('aggregates seller dashboard counts and financial totals', async () => { saleService: { async list() { return [{ status: 'COMPLETED', netProceedsMinor: 2200, profitMinor: 700 }]; } }, acquisitionService: { async list() { return [{ costBasisMinor: 1500 }]; } }, pricingRepository: { async listLatestValuationsForSeller() { return [{ inventoryItemId: 'item-1', valueMinor: 2800, calculatedAt: new Date() }]; } }, + pricingRefreshJobRepository: { async countFailedForSeller() { return 2; } }, }); const dashboard = await service.get({ sellerId: 'seller-1' }); @@ -25,6 +26,7 @@ test('aggregates seller dashboard counts and financial totals', async () => { missingImages: 0, missingPricing: 1, stalePricing: 0, + failedPricingJobs: 2, }); assert.deepEqual(dashboard.financials, { totalAcquisitionCostMinor: 1500, diff --git a/apps/web/src/pages/AdminHomePage.vue b/apps/web/src/pages/AdminHomePage.vue index d3b812c..3e91e16 100644 --- a/apps/web/src/pages/AdminHomePage.vue +++ b/apps/web/src/pages/AdminHomePage.vue @@ -254,6 +254,7 @@ onMounted(load);
Missing images
{{ dashboard.counts.missingImages }}
Missing pricing
{{ dashboard.counts.missingPricing }}
Stale pricing
{{ dashboard.counts.stalePricing }}
+
Provider failures
{{ dashboard.counts.failedPricingJobs }}
Acquisition cost
${{ (dashboard.financials.totalAcquisitionCostMinor / 100).toFixed(2) }}
Asking value
${{ (dashboard.financials.totalAskingValueMinor / 100).toFixed(2) }}
Market value
${{ (dashboard.financials.totalMarketValueMinor / 100).toFixed(2) }}
diff --git a/apps/web/tests/e2e/admin-catalog-workflow.spec.js b/apps/web/tests/e2e/admin-catalog-workflow.spec.js index 65c11ff..886f6dd 100644 --- a/apps/web/tests/e2e/admin-catalog-workflow.spec.js +++ b/apps/web/tests/e2e/admin-catalog-workflow.spec.js @@ -18,6 +18,7 @@ test('signs in and publishes a listing after adding an inventory image', async ( await expect(page.getByRole('heading', { name: 'At a glance' })).toBeVisible(); await expect(page.getByText('Active inventory')).toBeVisible(); await expect(page.locator('.dashboard-summary__grid div').filter({ hasText: 'Missing images' }).getByRole('link')).toHaveAttribute('href', '#inventory-title'); + await expect(page.locator('.dashboard-summary__grid div').filter({ hasText: 'Provider failures' }).getByRole('link')).toHaveAttribute('href', '#pricing-title'); const category = page.locator('#product-category option').nth(1); await expect(category).toBeAttached();