diff --git a/apps/api/src/modules/listings/repositories/listing-repository.js b/apps/api/src/modules/listings/repositories/listing-repository.js index d317348..2ffcb48 100644 --- a/apps/api/src/modules/listings/repositories/listing-repository.js +++ b/apps/api/src/modules/listings/repositories/listing-repository.js @@ -161,6 +161,16 @@ export function createListingRepository(db) { } const offset = (page - 1) * limit; + const marketValue = sql`COALESCE( + (SELECT ${marketValueOverrides.valueMinor} + FROM ${marketValueOverrides} + WHERE ${marketValueOverrides.inventoryItemId} = ${inventoryItems.id} + ORDER BY ${marketValueOverrides.createdAt} DESC LIMIT 1), + (SELECT ${marketValuations.valueMinor} + FROM ${marketValuations} + WHERE ${marketValuations.inventoryItemId} = ${inventoryItems.id} + ORDER BY ${marketValuations.calculatedAt} DESC LIMIT 1) + )`; const sortOrder = sort === "updated" ? desc(listings.updatedAt) @@ -168,6 +178,10 @@ export function createListingRepository(db) { ? products.name : sort === "name_desc" ? desc(products.name) + : sort === "market_low" + ? sql`${marketValue} ASC NULLS LAST` + : sort === "market_high" + ? sql`${marketValue} DESC NULLS LAST` : sort === "price_low" ? listings.askingPriceMinor : sort === "price_high" diff --git a/apps/api/src/modules/search/domain/search-query.js b/apps/api/src/modules/search/domain/search-query.js index 4c679e4..1075610 100644 --- a/apps/api/src/modules/search/domain/search-query.js +++ b/apps/api/src/modules/search/domain/search-query.js @@ -67,8 +67,8 @@ export function createSearchQuery({ throw new Error("minPriceMinor must be <= maxPriceMinor"); } - if (!["recent", "updated", "name_asc", "name_desc", "price_low", "price_high"].includes(sort)) { - throw new Error("sort must be recent, updated, name_asc, name_desc, price_low, or price_high"); + if (!["recent", "updated", "name_asc", "name_desc", "price_low", "price_high", "market_low", "market_high"].includes(sort)) { + throw new Error("sort must be recent, updated, name_asc, name_desc, price_low, price_high, market_low, or market_high"); } return { diff --git a/apps/api/tests/domain/search-query.test.js b/apps/api/tests/domain/search-query.test.js index 5e1afc2..8ed1f49 100644 --- a/apps/api/tests/domain/search-query.test.js +++ b/apps/api/tests/domain/search-query.test.js @@ -66,6 +66,8 @@ test("accepts only the supported public search sort values", () => { assert.equal(createSearchQuery({ sort: "updated" }).sort, "updated"); assert.equal(createSearchQuery({ sort: "name_asc" }).sort, "name_asc"); assert.equal(createSearchQuery({ sort: "name_desc" }).sort, "name_desc"); + assert.equal(createSearchQuery({ sort: "market_low" }).sort, "market_low"); + assert.equal(createSearchQuery({ sort: "market_high" }).sort, "market_high"); assert.throws( () => createSearchQuery({ sort: "random" }), /sort must be recent/, diff --git a/apps/web/src/components/atoms/CatalogSortSelect.stories.js b/apps/web/src/components/atoms/CatalogSortSelect.stories.js index 3569430..915def9 100644 --- a/apps/web/src/components/atoms/CatalogSortSelect.stories.js +++ b/apps/web/src/components/atoms/CatalogSortSelect.stories.js @@ -4,3 +4,4 @@ export default { title: 'Atoms/CatalogSortSelect', component: CatalogSortSelect export const RecentlyAdded = { args: { modelValue: 'recent' } }; export const NameAscending = { args: { modelValue: 'name_asc' } }; +export const MarketValueDescending = { args: { modelValue: 'market_high' } }; diff --git a/apps/web/src/components/atoms/CatalogSortSelect.vue b/apps/web/src/components/atoms/CatalogSortSelect.vue index abe5fb6..a0cb4cb 100644 --- a/apps/web/src/components/atoms/CatalogSortSelect.vue +++ b/apps/web/src/components/atoms/CatalogSortSelect.vue @@ -19,6 +19,8 @@ defineEmits(['update:modelValue']); + + diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index 0dcd745..a6c41bb 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -166,6 +166,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro - Public catalog sorting now supports recently added, recently updated, name A-Z, name Z-A, price low-to-high, and price high-to-low through a URL-synchronized atomic sort control. Broader inventory filter parity and market-value sorting remain. - Public catalog filtering now supports URL-synchronized set, event, and tag text filters through a reusable atomic text-filter control and the shared search API. - Public catalog filtering now supports explicit Available, Pending, Sold, and All status modes plus a 30-day Newly added toggle through the shared search API and atomic controls. +- Public catalog sorting now supports latest manual/calculated market value low-to-high and high-to-low, with null values placed last. ## Local Commands