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 <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 09:16:44 +07:00
parent deb04989de
commit 430c67f244
2 changed files with 19 additions and 1 deletions

View File

@@ -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;

View File

@@ -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,