From 430c67f244702e5784e3f25bc0b7f1f23ef6ac90 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 16 Apr 2026 09:16:44 +0700 Subject: [PATCH] feat(listings): add featured boost to search and expose isFeatured in API responses Featured listings now sort first in search results via featuredUntil desc ordering. All listing read DTOs (detail, search, seller) include isFeatured boolean and featuredUntil timestamp. Co-Authored-By: Paperclip --- .../domain/repositories/listing-read.dto.ts | 6 ++++++ .../repositories/listing-read.queries.ts | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/api/src/modules/listings/domain/repositories/listing-read.dto.ts b/apps/api/src/modules/listings/domain/repositories/listing-read.dto.ts index 0c0977c..74178a4 100644 --- a/apps/api/src/modules/listings/domain/repositories/listing-read.dto.ts +++ b/apps/api/src/modules/listings/domain/repositories/listing-read.dto.ts @@ -12,6 +12,8 @@ export interface ListingDetailData { viewCount: number; saveCount: number; inquiryCount: number; + isFeatured: boolean; + featuredUntil: string | null; publishedAt: string | null; createdAt: string; property: { @@ -63,6 +65,8 @@ export interface ListingSearchItem { transactionType: TransactionType; priceVND: string; pricePerM2: number | null; + isFeatured: boolean; + featuredUntil: string | null; viewCount: number; publishedAt: string | null; property: { @@ -92,6 +96,8 @@ export interface ListingSellerItem { status: ListingStatus; transactionType: TransactionType; priceVND: string; + isFeatured: boolean; + featuredUntil: string | null; property: { id: string; title: string; diff --git a/apps/api/src/modules/listings/infrastructure/repositories/listing-read.queries.ts b/apps/api/src/modules/listings/infrastructure/repositories/listing-read.queries.ts index e84bdfb..53f3880 100644 --- a/apps/api/src/modules/listings/infrastructure/repositories/listing-read.queries.ts +++ b/apps/api/src/modules/listings/infrastructure/repositories/listing-read.queries.ts @@ -34,6 +34,7 @@ export async function findByIdWithProperty( // location is NOT NULL in the database — geo extraction always succeeds for existing properties const geo = geoRows[0]!; + const now = new Date(); return { id: listing.id, status: listing.status, @@ -45,6 +46,8 @@ export async function findByIdWithProperty( viewCount: listing.viewCount, saveCount: listing.saveCount, inquiryCount: listing.inquiryCount, + isFeatured: listing.featuredUntil != null && listing.featuredUntil > now, + featuredUntil: listing.featuredUntil?.toISOString() ?? null, publishedAt: listing.publishedAt?.toISOString() ?? null, createdAt: listing.createdAt.toISOString(), property: { @@ -116,7 +119,10 @@ export async function searchListings( where, skip, take: limit, - orderBy: { createdAt: 'desc' }, + orderBy: [ + { featuredUntil: { sort: 'desc', nulls: 'last' } }, + { createdAt: 'desc' }, + ], include: { property: { include: { @@ -146,6 +152,7 @@ export async function searchListings( } } + const now = new Date(); return { data: data.map((listing) => { // location is NOT NULL — every property in the result set has geo data @@ -156,6 +163,8 @@ export async function searchListings( transactionType: listing.transactionType, priceVND: listing.priceVND.toString(), pricePerM2: listing.pricePerM2, + isFeatured: listing.featuredUntil != null && listing.featuredUntil > now, + featuredUntil: listing.featuredUntil?.toISOString() ?? null, viewCount: listing.viewCount, publishedAt: listing.publishedAt?.toISOString() ?? null, property: { @@ -213,12 +222,15 @@ export async function findBySellerIdQuery( prisma.listing.count({ where }), ]); + const sellerNow = new Date(); return { data: data.map((listing) => ({ id: listing.id, status: listing.status, transactionType: listing.transactionType, priceVND: listing.priceVND.toString(), + isFeatured: listing.featuredUntil != null && listing.featuredUntil > sellerNow, + featuredUntil: listing.featuredUntil?.toISOString() ?? null, property: { id: listing.property.id, title: listing.property.title,