This commit is contained in:
@@ -144,6 +144,7 @@ async function startServer() {
|
|||||||
saleService: createSaleService({ saleRepository }),
|
saleService: createSaleService({ saleRepository }),
|
||||||
acquisitionService: createAcquisitionService({ acquisitionRepository }),
|
acquisitionService: createAcquisitionService({ acquisitionRepository }),
|
||||||
pricingRepository,
|
pricingRepository,
|
||||||
|
pricingRefreshJobRepository,
|
||||||
}),
|
}),
|
||||||
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository, pricingProviderRegistry: pricingProviders }),
|
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository, pricingProviderRegistry: pricingProviders }),
|
||||||
providerMappingService: createProviderMappingService({ providerMappingRepository }),
|
providerMappingService: createProviderMappingService({ providerMappingRepository }),
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService, pricingRepository }) {
|
export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService, pricingRepository, pricingRefreshJobRepository }) {
|
||||||
return {
|
return {
|
||||||
async get({ sellerId }) {
|
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),
|
productService.listBySellerId(sellerId),
|
||||||
inventoryItemService.listBySellerId(sellerId),
|
inventoryItemService.listBySellerId(sellerId),
|
||||||
listingService.list({ sellerId }),
|
listingService.list({ sellerId }),
|
||||||
saleService.list({ sellerId }),
|
saleService.list({ sellerId }),
|
||||||
acquisitionService.list({ sellerId }),
|
acquisitionService.list({ sellerId }),
|
||||||
pricingRepository?.listLatestValuationsForSeller(sellerId) ?? [],
|
pricingRepository?.listLatestValuationsForSeller(sellerId) ?? [],
|
||||||
|
pricingRefreshJobRepository?.countFailedForSeller(sellerId) ?? 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const activeListings = listings.filter(({ status }) => ['AVAILABLE', 'PENDING'].includes(status));
|
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,
|
missingImages: listings.filter(({ imageCount }) => imageCount < 1).length,
|
||||||
missingPricing: inventory.filter(({ id }) => !valuationByItem.has(id)).length,
|
missingPricing: inventory.filter(({ id }) => !valuationByItem.has(id)).length,
|
||||||
stalePricing: valuations.filter(({ calculatedAt }) => Date.now() - new Date(calculatedAt).getTime() > staleThreshold).length,
|
stalePricing: valuations.filter(({ calculatedAt }) => Date.now() - new Date(calculatedAt).getTime() > staleThreshold).length,
|
||||||
|
failedPricingJobs,
|
||||||
},
|
},
|
||||||
financials: {
|
financials: {
|
||||||
totalAcquisitionCostMinor: total(acquisitions, 'costBasisMinor'),
|
totalAcquisitionCostMinor: total(acquisitions, 'costBasisMinor'),
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ export function createPricingRefreshJobRepository(db) {
|
|||||||
return job ?? null;
|
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) {
|
async complete(id, observations, valuation) {
|
||||||
const [completed] = await db.update(pricingRefreshJobs)
|
const [completed] = await db.update(pricingRefreshJobs)
|
||||||
.set({ status: 'COMPLETED', observations, valuation, error: null, updatedAt: new Date() })
|
.set({ status: 'COMPLETED', observations, valuation, error: null, updatedAt: new Date() })
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ test('aggregates seller dashboard counts and financial totals', async () => {
|
|||||||
saleService: { async list() { return [{ status: 'COMPLETED', netProceedsMinor: 2200, profitMinor: 700 }]; } },
|
saleService: { async list() { return [{ status: 'COMPLETED', netProceedsMinor: 2200, profitMinor: 700 }]; } },
|
||||||
acquisitionService: { async list() { return [{ costBasisMinor: 1500 }]; } },
|
acquisitionService: { async list() { return [{ costBasisMinor: 1500 }]; } },
|
||||||
pricingRepository: { async listLatestValuationsForSeller() { return [{ inventoryItemId: 'item-1', valueMinor: 2800, calculatedAt: new Date() }]; } },
|
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' });
|
const dashboard = await service.get({ sellerId: 'seller-1' });
|
||||||
@@ -25,6 +26,7 @@ test('aggregates seller dashboard counts and financial totals', async () => {
|
|||||||
missingImages: 0,
|
missingImages: 0,
|
||||||
missingPricing: 1,
|
missingPricing: 1,
|
||||||
stalePricing: 0,
|
stalePricing: 0,
|
||||||
|
failedPricingJobs: 2,
|
||||||
});
|
});
|
||||||
assert.deepEqual(dashboard.financials, {
|
assert.deepEqual(dashboard.financials, {
|
||||||
totalAcquisitionCostMinor: 1500,
|
totalAcquisitionCostMinor: 1500,
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ onMounted(load);
|
|||||||
<div><dt>Missing images</dt><dd><a href="#inventory-title">{{ dashboard.counts.missingImages }}</a></dd></div>
|
<div><dt>Missing images</dt><dd><a href="#inventory-title">{{ dashboard.counts.missingImages }}</a></dd></div>
|
||||||
<div><dt>Missing pricing</dt><dd><a href="#pricing-title">{{ dashboard.counts.missingPricing }}</a></dd></div>
|
<div><dt>Missing pricing</dt><dd><a href="#pricing-title">{{ dashboard.counts.missingPricing }}</a></dd></div>
|
||||||
<div><dt>Stale pricing</dt><dd><a href="#pricing-title">{{ dashboard.counts.stalePricing }}</a></dd></div>
|
<div><dt>Stale pricing</dt><dd><a href="#pricing-title">{{ dashboard.counts.stalePricing }}</a></dd></div>
|
||||||
|
<div><dt>Provider failures</dt><dd><a href="#pricing-title">{{ dashboard.counts.failedPricingJobs }}</a></dd></div>
|
||||||
<div><dt>Acquisition cost</dt><dd>${{ (dashboard.financials.totalAcquisitionCostMinor / 100).toFixed(2) }}</dd></div>
|
<div><dt>Acquisition cost</dt><dd>${{ (dashboard.financials.totalAcquisitionCostMinor / 100).toFixed(2) }}</dd></div>
|
||||||
<div><dt>Asking value</dt><dd>${{ (dashboard.financials.totalAskingValueMinor / 100).toFixed(2) }}</dd></div>
|
<div><dt>Asking value</dt><dd>${{ (dashboard.financials.totalAskingValueMinor / 100).toFixed(2) }}</dd></div>
|
||||||
<div><dt>Market value</dt><dd>${{ (dashboard.financials.totalMarketValueMinor / 100).toFixed(2) }}</dd></div>
|
<div><dt>Market value</dt><dd>${{ (dashboard.financials.totalMarketValueMinor / 100).toFixed(2) }}</dd></div>
|
||||||
|
|||||||
@@ -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.getByRole('heading', { name: 'At a glance' })).toBeVisible();
|
||||||
await expect(page.getByText('Active inventory')).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: '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);
|
const category = page.locator('#product-category option').nth(1);
|
||||||
await expect(category).toBeAttached();
|
await expect(category).toBeAttached();
|
||||||
|
|||||||
Reference in New Issue
Block a user