From 94227c477a054ce7cdbaa421eae09bce8044022e Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Wed, 9 Sep 2026 16:39:57 -0400 Subject: [PATCH] feat: add dashboard pricing metrics --- apps/api/src/app/server.js | 1 + .../dashboard/services/dashboard-service.js | 10 ++++++++-- .../pricing/repositories/pricing-repository.js | 14 ++++++++++++++ .../tests/application/dashboard-service.test.js | 6 +++++- apps/web/src/pages/AdminHomePage.vue | 3 +++ 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/api/src/app/server.js b/apps/api/src/app/server.js index efaa546..e6c8742 100644 --- a/apps/api/src/app/server.js +++ b/apps/api/src/app/server.js @@ -143,6 +143,7 @@ async function startServer() { listingService: createListingService({ listingRepository, inventoryImageRepository }), saleService: createSaleService({ saleRepository }), acquisitionService: createAcquisitionService({ acquisitionRepository }), + pricingRepository, }), 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 c31ff3a..272ae4b 100644 --- a/apps/api/src/modules/dashboard/services/dashboard-service.js +++ b/apps/api/src/modules/dashboard/services/dashboard-service.js @@ -1,16 +1,19 @@ -export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService }) { +export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService, pricingRepository }) { return { async get({ sellerId }) { - const [products, inventory, listings, sales, acquisitions] = await Promise.all([ + const [products, inventory, listings, sales, acquisitions, valuations] = await Promise.all([ productService.listBySellerId(sellerId), inventoryItemService.listBySellerId(sellerId), listingService.list({ sellerId }), saleService.list({ sellerId }), acquisitionService.list({ sellerId }), + pricingRepository?.listLatestValuationsForSeller(sellerId) ?? [], ]); const activeListings = listings.filter(({ status }) => ['AVAILABLE', 'PENDING'].includes(status)); const completedSales = sales.filter(({ status }) => status !== 'VOIDED'); + const valuationByItem = new Map(valuations.map((valuation) => [valuation.inventoryItemId, valuation])); + const staleThreshold = 7 * 24 * 60 * 60 * 1000; const total = (values, field) => values.reduce((sum, value) => sum + (value[field] ?? 0), 0); return { @@ -23,12 +26,15 @@ export function createDashboardService({ productService, inventoryItemService, l sales: completedSales.length, acquisitions: acquisitions.length, 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, }, financials: { totalAcquisitionCostMinor: total(acquisitions, 'costBasisMinor'), totalAskingValueMinor: total(activeListings, 'askingPriceMinor'), totalNetSaleProceedsMinor: total(completedSales, 'netProceedsMinor'), totalProfitMinor: total(completedSales, 'profitMinor'), + totalMarketValueMinor: total([...valuationByItem.values()], 'valueMinor'), }, recentSales: completedSales.slice(0, 5), recentAcquisitions: acquisitions.slice(0, 5), diff --git a/apps/api/src/modules/pricing/repositories/pricing-repository.js b/apps/api/src/modules/pricing/repositories/pricing-repository.js index 0c40f2d..c55f6c9 100644 --- a/apps/api/src/modules/pricing/repositories/pricing-repository.js +++ b/apps/api/src/modules/pricing/repositories/pricing-repository.js @@ -41,6 +41,20 @@ export function createPricingRepository(db) { return { status: 'CALCULATED', valuation, observations }; }, + async listLatestValuationsForSeller(sellerId) { + const rows = await db.select({ valuation: marketValuations, inventoryItemId: inventoryItems.id }) + .from(marketValuations) + .innerJoin(inventoryItems, eq(inventoryItems.id, marketValuations.inventoryItemId)) + .innerJoin(products, eq(products.id, inventoryItems.productId)) + .where(eq(products.sellerId, sellerId)) + .orderBy(desc(marketValuations.calculatedAt)); + const latest = new Map(); + for (const row of rows) { + if (!latest.has(row.inventoryItemId)) latest.set(row.inventoryItemId, row.valuation); + } + return [...latest.values()]; + }, + async markOutliersForSeller({ inventoryItemId, sellerId }) { const [ownedItem] = await db.select({ id: inventoryItems.id }).from(inventoryItems) .innerJoin(products, eq(products.id, inventoryItems.productId)) diff --git a/apps/api/tests/application/dashboard-service.test.js b/apps/api/tests/application/dashboard-service.test.js index c83e391..1620ba9 100644 --- a/apps/api/tests/application/dashboard-service.test.js +++ b/apps/api/tests/application/dashboard-service.test.js @@ -6,10 +6,11 @@ import { createDashboardService } from '../../src/modules/dashboard/services/das test('aggregates seller dashboard counts and financial totals', async () => { const service = createDashboardService({ productService: { async listBySellerId() { return [{ id: 'product-1' }]; } }, - inventoryItemService: { async listBySellerId() { return [{ status: 'ACTIVE' }, { status: 'SOLD' }]; } }, + inventoryItemService: { async listBySellerId() { return [{ id: 'item-1', status: 'ACTIVE' }, { id: 'item-2', status: 'SOLD' }]; } }, listingService: { async list() { return [{ status: 'AVAILABLE', askingPriceMinor: 2500, imageCount: 1 }]; } }, 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() }]; } }, }); const dashboard = await service.get({ sellerId: 'seller-1' }); @@ -22,11 +23,14 @@ test('aggregates seller dashboard counts and financial totals', async () => { sales: 1, acquisitions: 1, missingImages: 0, + missingPricing: 1, + stalePricing: 0, }); assert.deepEqual(dashboard.financials, { totalAcquisitionCostMinor: 1500, totalAskingValueMinor: 2500, totalNetSaleProceedsMinor: 2200, totalProfitMinor: 700, + totalMarketValueMinor: 2800, }); }); \ No newline at end of file diff --git a/apps/web/src/pages/AdminHomePage.vue b/apps/web/src/pages/AdminHomePage.vue index c76c141..7672f2a 100644 --- a/apps/web/src/pages/AdminHomePage.vue +++ b/apps/web/src/pages/AdminHomePage.vue @@ -252,8 +252,11 @@ onMounted(load);
Available listings
{{ dashboard.counts.availableListings }}
Sold inventory
{{ dashboard.counts.soldInventory }}
Missing images
{{ dashboard.counts.missingImages }}
+
Missing pricing
{{ dashboard.counts.missingPricing }}
+
Stale pricing
{{ dashboard.counts.stalePricing }}
Acquisition cost
${{ (dashboard.financials.totalAcquisitionCostMinor / 100).toFixed(2) }}
Asking value
${{ (dashboard.financials.totalAskingValueMinor / 100).toFixed(2) }}
+
Market value
${{ (dashboard.financials.totalMarketValueMinor / 100).toFixed(2) }}
Net proceeds
${{ (dashboard.financials.totalNetSaleProceedsMinor / 100).toFixed(2) }}
Profit
${{ (dashboard.financials.totalProfitMinor / 100).toFixed(2) }}