diff --git a/apps/api/src/modules/listings/repositories/listing-repository.js b/apps/api/src/modules/listings/repositories/listing-repository.js index 5299018..5356ee6 100644 --- a/apps/api/src/modules/listings/repositories/listing-repository.js +++ b/apps/api/src/modules/listings/repositories/listing-repository.js @@ -17,6 +17,8 @@ import { marketValueOverrides, marketValuations, products, + sellers, + sellerSettings, } from "../../../infrastructure/database/schema.js"; export function createListingRepository(db) { @@ -220,7 +222,12 @@ export function createListingRepository(db) { async findPublicById(listingId) { const [result] = await db - .select({ listing: listings, condition: conditions }) + .select({ + listing: listings, + condition: conditions, + sellerDisplayName: sellers.displayName, + sellerSettings: sellerSettings.settings, + }) .from(listings) .innerJoin( inventoryItems, @@ -228,6 +235,8 @@ export function createListingRepository(db) { ) .innerJoin(products, eq(products.id, inventoryItems.productId)) .innerJoin(conditions, eq(conditions.id, inventoryItems.conditionId)) + .innerJoin(sellers, eq(sellers.id, products.sellerId)) + .leftJoin(sellerSettings, eq(sellerSettings.sellerId, sellers.id)) .where( and( eq(listings.id, listingId), @@ -263,6 +272,9 @@ export function createListingRepository(db) { marketValueMinor: override?.valueMinor ?? valuation?.valueMinor ?? null, marketValueCurrency: override?.currency ?? valuation?.currency ?? null, marketValueUpdatedAt: override?.createdAt ?? valuation?.calculatedAt ?? null, + sellerDisplayName: result.sellerDisplayName, + sellerContactEmail: result.sellerSettings?.general?.publicContactEmail ?? null, + sellerContactPhone: result.sellerSettings?.general?.publicContactPhone ?? null, }; }, diff --git a/apps/api/src/modules/products/repositories/product-repository.js b/apps/api/src/modules/products/repositories/product-repository.js index e0d2cb7..9bd3729 100644 --- a/apps/api/src/modules/products/repositories/product-repository.js +++ b/apps/api/src/modules/products/repositories/product-repository.js @@ -4,6 +4,8 @@ import { inventoryItems, listings, products, + sellers, + sellerSettings, } from '../../../infrastructure/database/schema.js'; export function createProductRepository(db) { @@ -24,10 +26,16 @@ export function createProductRepository(db) { async findPublicBySlug(slug) { const [result] = await db - .select({ product: products }) + .select({ + product: products, + sellerDisplayName: sellers.displayName, + sellerSettings: sellerSettings.settings, + }) .from(products) .innerJoin(inventoryItems, eq(inventoryItems.productId, products.id)) .innerJoin(listings, eq(listings.inventoryItemId, inventoryItems.id)) + .innerJoin(sellers, eq(sellers.id, products.sellerId)) + .leftJoin(sellerSettings, eq(sellerSettings.sellerId, sellers.id)) .where(and( eq(products.slug, slug), eq(inventoryItems.status, 'ACTIVE'), @@ -35,7 +43,13 @@ export function createProductRepository(db) { )) .limit(1); - return result?.product ?? null; + if (!result) return null; + return { + ...result.product, + sellerDisplayName: result.sellerDisplayName, + sellerContactEmail: result.sellerSettings?.general?.publicContactEmail ?? null, + sellerContactPhone: result.sellerSettings?.general?.publicContactPhone ?? null, + }; }, async findBySellerIdAndSlug(sellerId, slug) { diff --git a/apps/api/tests/http/public-listing-routes.test.js b/apps/api/tests/http/public-listing-routes.test.js index c4f9153..d5492b6 100644 --- a/apps/api/tests/http/public-listing-routes.test.js +++ b/apps/api/tests/http/public-listing-routes.test.js @@ -36,6 +36,9 @@ function createTestApp() { conditionId: 'condition-1', askingPriceMinor: 2499, status: 'PENDING', + sellerDisplayName: 'Pika Collectibles', + sellerContactEmail: 'hello@pika.example', + sellerContactPhone: null, }; }, }, @@ -72,6 +75,8 @@ test('returns a public listing detail or a not-found response', async (testConte assert.equal(detailResponse.statusCode, 200); assert.equal('inventoryItemId' in detailResponse.json().data, false); + assert.equal(detailResponse.json().data.sellerDisplayName, 'Pika Collectibles'); + assert.equal(detailResponse.json().data.sellerContactEmail, 'hello@pika.example'); assert.equal(missingResponse.statusCode, 404); assert.equal(missingResponse.json().error.code, 'LISTING_NOT_FOUND'); }); \ No newline at end of file diff --git a/apps/api/tests/http/public-product-routes.test.js b/apps/api/tests/http/public-product-routes.test.js index ad681a8..5fd8a0e 100644 --- a/apps/api/tests/http/public-product-routes.test.js +++ b/apps/api/tests/http/public-product-routes.test.js @@ -21,6 +21,9 @@ function createTestApp() { name: 'Pikachu Plush', slug, description: 'A friendly plush.', + sellerDisplayName: 'Pika Collectibles', + sellerContactEmail: 'hello@pika.example', + sellerContactPhone: null, createdAt: new Date('2026-09-07T00:00:00.000Z'), updatedAt: new Date('2026-09-07T00:00:00.000Z'), }; @@ -51,6 +54,9 @@ test('returns a public product without private seller identity', async (testCont eventName: null, releaseYear: null, tags: [], + sellerDisplayName: 'Pika Collectibles', + sellerContactEmail: 'hello@pika.example', + sellerContactPhone: null, createdAt: '2026-09-07T00:00:00.000Z', updatedAt: '2026-09-07T00:00:00.000Z', }); diff --git a/apps/web/src/components/organisms/CollectibleDetail.stories.js b/apps/web/src/components/organisms/CollectibleDetail.stories.js index a8586bd..7948a3d 100644 --- a/apps/web/src/components/organisms/CollectibleDetail.stories.js +++ b/apps/web/src/components/organisms/CollectibleDetail.stories.js @@ -24,6 +24,9 @@ export const ListingDetail = { { id: "front", label: "FRONT", alt: "Front of Charizard ex" }, { id: "back", label: "BACK", alt: "Back of Charizard ex" }, ], + sellerDisplayName: "Pika Collectibles", + sellerContactEmail: "hello@pika.example", + sellerContactPhone: "555-0100", }, }; diff --git a/apps/web/src/components/organisms/CollectibleDetail.vue b/apps/web/src/components/organisms/CollectibleDetail.vue index 250b30d..c3c5448 100644 --- a/apps/web/src/components/organisms/CollectibleDetail.vue +++ b/apps/web/src/components/organisms/CollectibleDetail.vue @@ -15,6 +15,9 @@ defineProps({ description: { type: String, default: "" }, facts: { type: Array, default: () => [] }, images: { type: Array, default: () => [] }, + sellerDisplayName: { type: String, default: "" }, + sellerContactEmail: { type: String, default: "" }, + sellerContactPhone: { type: String, default: "" }, }); @@ -47,6 +50,14 @@ defineProps({
Sold by
+{{ sellerDisplayName }}
+ +