This commit is contained in:
@@ -143,6 +143,7 @@ async function startServer() {
|
|||||||
listingService: createListingService({ listingRepository, inventoryImageRepository }),
|
listingService: createListingService({ listingRepository, inventoryImageRepository }),
|
||||||
saleService: createSaleService({ saleRepository }),
|
saleService: createSaleService({ saleRepository }),
|
||||||
acquisitionService: createAcquisitionService({ acquisitionRepository }),
|
acquisitionService: createAcquisitionService({ acquisitionRepository }),
|
||||||
|
pricingRepository,
|
||||||
}),
|
}),
|
||||||
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository, pricingProviderRegistry: pricingProviders }),
|
pricingService: createPricingService({ pricingRepository, pricingRefreshJobRepository, providerMappingRepository, pricingProviderRegistry: pricingProviders }),
|
||||||
providerMappingService: createProviderMappingService({ providerMappingRepository }),
|
providerMappingService: createProviderMappingService({ providerMappingRepository }),
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService }) {
|
export function createDashboardService({ productService, inventoryItemService, listingService, saleService, acquisitionService, pricingRepository }) {
|
||||||
return {
|
return {
|
||||||
async get({ sellerId }) {
|
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),
|
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) ?? [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const activeListings = listings.filter(({ status }) => ['AVAILABLE', 'PENDING'].includes(status));
|
const activeListings = listings.filter(({ status }) => ['AVAILABLE', 'PENDING'].includes(status));
|
||||||
const completedSales = sales.filter(({ status }) => status !== 'VOIDED');
|
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);
|
const total = (values, field) => values.reduce((sum, value) => sum + (value[field] ?? 0), 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -23,12 +26,15 @@ export function createDashboardService({ productService, inventoryItemService, l
|
|||||||
sales: completedSales.length,
|
sales: completedSales.length,
|
||||||
acquisitions: acquisitions.length,
|
acquisitions: acquisitions.length,
|
||||||
missingImages: listings.filter(({ imageCount }) => imageCount < 1).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: {
|
financials: {
|
||||||
totalAcquisitionCostMinor: total(acquisitions, 'costBasisMinor'),
|
totalAcquisitionCostMinor: total(acquisitions, 'costBasisMinor'),
|
||||||
totalAskingValueMinor: total(activeListings, 'askingPriceMinor'),
|
totalAskingValueMinor: total(activeListings, 'askingPriceMinor'),
|
||||||
totalNetSaleProceedsMinor: total(completedSales, 'netProceedsMinor'),
|
totalNetSaleProceedsMinor: total(completedSales, 'netProceedsMinor'),
|
||||||
totalProfitMinor: total(completedSales, 'profitMinor'),
|
totalProfitMinor: total(completedSales, 'profitMinor'),
|
||||||
|
totalMarketValueMinor: total([...valuationByItem.values()], 'valueMinor'),
|
||||||
},
|
},
|
||||||
recentSales: completedSales.slice(0, 5),
|
recentSales: completedSales.slice(0, 5),
|
||||||
recentAcquisitions: acquisitions.slice(0, 5),
|
recentAcquisitions: acquisitions.slice(0, 5),
|
||||||
|
|||||||
@@ -41,6 +41,20 @@ export function createPricingRepository(db) {
|
|||||||
return { status: 'CALCULATED', valuation, observations };
|
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 }) {
|
async markOutliersForSeller({ inventoryItemId, sellerId }) {
|
||||||
const [ownedItem] = await db.select({ id: inventoryItems.id }).from(inventoryItems)
|
const [ownedItem] = await db.select({ id: inventoryItems.id }).from(inventoryItems)
|
||||||
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { createDashboardService } from '../../src/modules/dashboard/services/das
|
|||||||
test('aggregates seller dashboard counts and financial totals', async () => {
|
test('aggregates seller dashboard counts and financial totals', async () => {
|
||||||
const service = createDashboardService({
|
const service = createDashboardService({
|
||||||
productService: { async listBySellerId() { return [{ id: 'product-1' }]; } },
|
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 }]; } },
|
listingService: { async list() { return [{ status: 'AVAILABLE', askingPriceMinor: 2500, imageCount: 1 }]; } },
|
||||||
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() }]; } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const dashboard = await service.get({ sellerId: 'seller-1' });
|
const dashboard = await service.get({ sellerId: 'seller-1' });
|
||||||
@@ -22,11 +23,14 @@ test('aggregates seller dashboard counts and financial totals', async () => {
|
|||||||
sales: 1,
|
sales: 1,
|
||||||
acquisitions: 1,
|
acquisitions: 1,
|
||||||
missingImages: 0,
|
missingImages: 0,
|
||||||
|
missingPricing: 1,
|
||||||
|
stalePricing: 0,
|
||||||
});
|
});
|
||||||
assert.deepEqual(dashboard.financials, {
|
assert.deepEqual(dashboard.financials, {
|
||||||
totalAcquisitionCostMinor: 1500,
|
totalAcquisitionCostMinor: 1500,
|
||||||
totalAskingValueMinor: 2500,
|
totalAskingValueMinor: 2500,
|
||||||
totalNetSaleProceedsMinor: 2200,
|
totalNetSaleProceedsMinor: 2200,
|
||||||
totalProfitMinor: 700,
|
totalProfitMinor: 700,
|
||||||
|
totalMarketValueMinor: 2800,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -252,8 +252,11 @@ onMounted(load);
|
|||||||
<div><dt>Available listings</dt><dd>{{ dashboard.counts.availableListings }}</dd></div>
|
<div><dt>Available listings</dt><dd>{{ dashboard.counts.availableListings }}</dd></div>
|
||||||
<div><dt>Sold inventory</dt><dd>{{ dashboard.counts.soldInventory }}</dd></div>
|
<div><dt>Sold inventory</dt><dd>{{ dashboard.counts.soldInventory }}</dd></div>
|
||||||
<div><dt>Missing images</dt><dd>{{ dashboard.counts.missingImages }}</dd></div>
|
<div><dt>Missing images</dt><dd>{{ dashboard.counts.missingImages }}</dd></div>
|
||||||
|
<div><dt>Missing pricing</dt><dd>{{ dashboard.counts.missingPricing }}</dd></div>
|
||||||
|
<div><dt>Stale pricing</dt><dd>{{ dashboard.counts.stalePricing }}</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>Net proceeds</dt><dd>${{ (dashboard.financials.totalNetSaleProceedsMinor / 100).toFixed(2) }}</dd></div>
|
<div><dt>Net proceeds</dt><dd>${{ (dashboard.financials.totalNetSaleProceedsMinor / 100).toFixed(2) }}</dd></div>
|
||||||
<div><dt>Profit</dt><dd>${{ (dashboard.financials.totalProfitMinor / 100).toFixed(2) }}</dd></div>
|
<div><dt>Profit</dt><dd>${{ (dashboard.financials.totalProfitMinor / 100).toFixed(2) }}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
Reference in New Issue
Block a user