diff --git a/app/marketplace/listings/[id]/_components/AuctionRoomClient.tsx b/app/marketplace/listings/[id]/_components/AuctionRoomClient.tsx
index f65a568..c8cc79a 100644
--- a/app/marketplace/listings/[id]/_components/AuctionRoomClient.tsx
+++ b/app/marketplace/listings/[id]/_components/AuctionRoomClient.tsx
@@ -7,7 +7,7 @@ import { Card } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
-import { Timer, Users, TrendingUp, Award, Zap, Clock, Gavel, AlertCircle } from 'lucide-react'
+import { Timer, Zap, Gavel, AlertCircle } from 'lucide-react'
import { formatCurrency, formatTimeLeft, getInitials } from '@/lib/utils'
import { useToast } from '@/hooks/use-toast'
import type { BidDTO, AuctionDTO } from '@/lib/marketplace/types'
@@ -31,16 +31,25 @@ interface AuctionData {
}
function getAuctionData(data: AuctionData | AuctionDTO | null): AuctionData {
- if (!data) return null as unknown as AuctionData
+ if (!data) {
+ return { id: '', title: '', basePrice: 0, endDate: '', auctionStatus: '', _count: { bids: 0 }, bids: [] }
+ }
if ('listing' in data) {
const a = data as AuctionDTO
return {
- id: a.listing.id, title: a.listing.title, description: a.listing.description,
- basePrice: a.listing.basePrice, bidIncrement: a.listing.bidIncrement,
- endDate: a.listing.endDate, auctionStatus: a.listing.auctionStatus,
- autoExtendMinutes: a.listing.autoExtendMinutes, currentLeaderId: a.listing.currentLeaderId,
- highestBid: a.listing.highestBid, winningBid: a.listing.winningBid,
- _count: { bids: a.stats.totalBids }, bids: a.listing.bids,
+ id: a.listing?.id || '',
+ title: a.listing?.title || '',
+ description: a.listing?.description,
+ basePrice: a.listing?.basePrice || 0,
+ bidIncrement: a.listing?.bidIncrement,
+ endDate: a.listing?.endDate || '',
+ auctionStatus: a.listing?.auctionStatus || '',
+ autoExtendMinutes: a.listing?.autoExtendMinutes,
+ currentLeaderId: a.listing?.currentLeaderId,
+ highestBid: a.listing?.highestBid,
+ winningBid: a.listing?.winningBid,
+ _count: { bids: a.stats?.totalBids ?? 0 },
+ bids: a.listing?.bids || [],
}
}
return data as AuctionData
@@ -53,7 +62,6 @@ interface AuctionRoomClientProps {
export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientProps) {
const [bidAmount, setBidAmount] = useState('')
- const [isAutoBid, setIsAutoBid] = useState(false)
const { toast } = useToast()
const { auction, bids, loading, placeBid, timeRemaining, isLive, error } = useAuction(listingId)
@@ -66,11 +74,11 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
if (!bidAmount) return
const amount = parseFloat(bidAmount)
if (amount < minNextBid) {
- toast({ title: 'Bid too low', description: "Minimum bid is " + formatCurrency(minNextBid), variant: 'destructive' })
+ toast({ title: 'Bid too low', description: 'Minimum bid is ' + formatCurrency(minNextBid), variant: 'destructive' })
return
}
try {
- await placeBid(amount, isAutoBid)
+ await placeBid(amount, false)
setBidAmount('')
toast({ title: 'Bid placed!', description: 'Your bid has been registered.' })
} catch (err) {
@@ -78,7 +86,7 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
}
}
- if (!currentAuction) {
+ if (!currentAuction || !currentAuction.id) {
return
}
@@ -101,7 +109,7 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
@@ -114,7 +122,7 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
{index === 0 &&
Leader}
{getInitials(bid.farmer?.name || '?')}
-
{bid.farmer?.name || 'Anonymous'}
{new Date(bid.createdAt).toLocaleString()}
+
{bid.farmer?.name || 'Anonymous'}
{new Date(bid.createdAt).toISOString().replace("T", " ").substring(0, 19)}
{formatCurrency(bid.amount)}
@@ -129,7 +137,7 @@ export function AuctionRoomClient({ listingId, initialData }: AuctionRoomClientP
- Starting Price
- {formatCurrency(currentAuction.basePrice)}
- Bid Increment
- {formatCurrency(currentAuction.bidIncrement || 1000)}
- - Ends At
- {new Date(currentAuction.endDate).toLocaleString()}
+ - Ends At
- {new Date(currentAuction.endDate).toISOString().replace("T", " ").substring(0, 19)}
diff --git a/hooks/useAuction.ts b/hooks/useAuction.ts
index ff3acec..a2cb31a 100644
--- a/hooks/useAuction.ts
+++ b/hooks/useAuction.ts
@@ -59,13 +59,14 @@ export function useAuction(listingId: string): UseAuctionReturn {
const fetchAuctionData = useCallback(async () => {
try {
- const response = await fetch("/api/marketplace/" + listingId);
+ const response = await fetch("/api/marketplace/" + listingId + "/auction");
if (!response.ok) throw new Error("Failed to fetch auction");
const result = await response.json();
const data = result.data || result;
+ const auctionData = data.listing || data;
setAuction(data);
- setBids(data.bids || []);
- const endTime = new Date(data.endDate).getTime();
+ setBids(auctionData.bids || []);
+ const endTime = new Date(auctionData.endDate).getTime();
setTimeRemaining(Math.max(0, endTime - Date.now()));
setError(null);
} catch (err) {
@@ -137,8 +138,8 @@ export function useAuction(listingId: string): UseAuctionReturn {
if (amount < minBid) throw new Error("Bid must be at least ?" + minBid.toLocaleString());
try {
- const response = await fetch("/api/marketplace/" + listingId, {
- method: "PUT",
+ const response = await fetch("/api/marketplace/" + listingId + "/bids", {
+ method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ farmerId, amount, isAutoBid }),
});
diff --git a/hooks/usePusher.ts b/hooks/usePusher.ts
index 7cec433..021e5e8 100644
--- a/hooks/usePusher.ts
+++ b/hooks/usePusher.ts
@@ -1,6 +1,6 @@
// hooks/usePusher.ts
import { useEffect } from 'react'
-import { pusherClient } from '@/lib/pusher/client'
+import { getPusherClient } from '@/lib/pusher/client'
// Define types that match your API response
export interface NewBidEvent {
@@ -18,43 +18,33 @@ export interface AuctionExtendedEvent {
newEndTime?: string
}
-export interface AuctionEndedEvent {
- winningBidId: string
- winningAmount: number
- winnerId: string
- message: string
-}
-
-// Type-safe callback types
-type NewBidCallback = (data: NewBidEvent) => void
-type AuctionExtendedCallback = (data: AuctionExtendedEvent) => void
-type AuctionEndedCallback = (data: AuctionEndedEvent) => void
-
interface PusherCallbacks {
- onNewBid?: NewBidCallback
- onAuctionExtended?: AuctionExtendedCallback
- onAuctionEnded?: AuctionEndedCallback
+ onNewBid?: (data: NewBidEvent) => void
+ onAuctionExtended?: (data: AuctionExtendedEvent) => void
}
-export function usePusher(channelName: string, callbacks: PusherCallbacks) {
+export function usePusher(channel: string, callbacks: PusherCallbacks) {
useEffect(() => {
- const channel = pusherClient.subscribe(channelName)
+ const client = getPusherClient()
+ if (!client) return
+
+ const ch = client.subscribe(channel)
if (callbacks.onNewBid) {
- channel.bind('new-bid', callbacks.onNewBid)
+ ch.bind('new-bid', callbacks.onNewBid)
}
-
if (callbacks.onAuctionExtended) {
- channel.bind('auction-extended', callbacks.onAuctionExtended)
- }
-
- if (callbacks.onAuctionEnded) {
- channel.bind('auction-ended', callbacks.onAuctionEnded)
+ ch.bind('auction-extended', callbacks.onAuctionExtended)
}
return () => {
- channel.unbind_all()
- pusherClient.unsubscribe(channelName)
+ if (callbacks.onNewBid) {
+ ch.unbind('new-bid', callbacks.onNewBid)
+ }
+ if (callbacks.onAuctionExtended) {
+ ch.unbind('auction-extended', callbacks.onAuctionExtended)
+ }
+ client.unsubscribe(channel)
}
- }, [channelName, callbacks.onNewBid, callbacks.onAuctionExtended, callbacks.onAuctionEnded])
+ }, [channel, callbacks.onNewBid, callbacks.onAuctionExtended])
}
\ No newline at end of file
diff --git a/lib/marketplace/queries.ts b/lib/marketplace/queries.ts
index 82fb94c..579d649 100644
--- a/lib/marketplace/queries.ts
+++ b/lib/marketplace/queries.ts
@@ -11,16 +11,8 @@ export const queries = {
*/
buildFeedQuery(filters: FeedFilters, pagination: PaginationParams): Prisma.LandListingFindManyArgs {
const {
- search,
- minPrice,
- maxPrice,
- landType,
- state,
- district,
- minSize,
- maxSize,
- irrigation,
- verifiedOnly,
+ search, minPrice, maxPrice, landType, state, district,
+ minSize, maxSize, irrigation, verifiedOnly,
sortBy = 'hotnessScore',
} = filters;
@@ -71,20 +63,13 @@ export const queries = {
const orderBy: Prisma.LandListingOrderByWithRelationInput[] = (() => {
switch (sortBy) {
- case 'hotnessScore':
- return [{ hotnessScore: 'desc' }, { engagementScore: 'desc' }, { createdAt: 'desc' }];
- case 'newest':
- return [{ createdAt: 'desc' }];
- case 'endingSoon':
- return [{ endDate: 'asc' }];
- case 'priceLowToHigh':
- return [{ basePrice: 'asc' }];
- case 'priceHighToLow':
- return [{ basePrice: 'desc' }];
- case 'mostBids':
- return [{ totalBids: 'desc' }];
- default:
- return [{ hotnessScore: 'desc' }];
+ case 'hotnessScore': return [{ hotnessScore: 'desc' }, { engagementScore: 'desc' }, { createdAt: 'desc' }];
+ case 'newest': return [{ createdAt: 'desc' }];
+ case 'endingSoon': return [{ endDate: 'asc' }];
+ case 'priceLowToHigh': return [{ basePrice: 'asc' }];
+ case 'priceHighToLow': return [{ basePrice: 'desc' }];
+ case 'mostBids': return [{ totalBids: 'desc' }];
+ default: return [{ hotnessScore: 'desc' }];
}
})();
@@ -94,167 +79,130 @@ export const queries = {
skip,
take: Math.min(limit, MARKETPLACE_CONSTANTS.PAGINATION.MAX_LIMIT),
select: {
- id: true,
- title: true,
- description: true,
- basePrice: true,
- highestBid: true,
- endDate: true,
- startDate: true,
- auctionStatus: true,
- minimumLeaseDuration: true,
- maximumLeaseDuration: true,
- createdAt: true,
- updatedAt: true,
- publishedAt: true,
- lastBidAt: true,
- viewCount: true,
- totalBids: true,
- hotnessScore: true,
- engagementScore: true,
+ id: true, title: true, description: true,
+ basePrice: true, highestBid: true,
+ endDate: true, startDate: true, auctionStatus: true,
+ minimumLeaseDuration: true, maximumLeaseDuration: true,
+ createdAt: true, updatedAt: true, publishedAt: true, lastBidAt: true,
+ viewCount: true, totalBids: true,
+ hotnessScore: true, engagementScore: true,
land: {
select: {
- id: true,
- size: true,
- landType: true,
- district: true,
- state: true,
- village: true,
- latitude: true,
- longitude: true,
- soilType: true,
- irrigationAvailable: true,
- electricityAvailable: true,
- roadAccess: true,
- fencingAvailable: true,
- waterSource: true,
+ id: true, size: true, landType: true,
+ district: true, state: true, village: true,
+ latitude: true, longitude: true,
+ soilType: true, irrigationAvailable: true,
+ electricityAvailable: true, roadAccess: true,
+ fencingAvailable: true, waterSource: true,
images: {
take: MARKETPLACE_CONSTANTS.LISTING.FEED_IMAGES,
- select: {
- id: true,
- url: true,
- caption: true,
- isPrimary: true,
- },
+ select: { id: true, url: true, caption: true, isPrimary: true },
},
},
},
owner: {
select: {
- id: true,
- name: true,
- imageUrl: true,
- landownerProfile: {
- select: {
- isVerified: true,
- verificationLevel: true,
- },
- },
+ id: true, name: true, imageUrl: true,
+ landownerProfile: { select: { isVerified: true, verificationLevel: true } },
},
},
images: {
take: MARKETPLACE_CONSTANTS.LISTING.FEED_IMAGES,
orderBy: { sortOrder: 'asc' },
- select: {
- id: true,
- url: true,
- caption: true,
- isPrimary: true,
- },
- },
- _count: {
- select: {
- bids: true,
- savedBy: true,
- },
+ select: { id: true, url: true, caption: true, isPrimary: true },
},
+ _count: { select: { bids: true, savedBy: true } },
},
};
},
- /**
- * Listing detail query builder
- */
+ // ============================================================
+ // PHASE 2: OPTIMIZED Listing detail - include → select
+ // ============================================================
buildListingDetailQuery(listingId: string, userId?: string): Prisma.LandListingFindUniqueArgs {
return {
where: { id: listingId },
- include: {
+ select: {
+ // Core listing fields
+ id: true, title: true, description: true,
+ basePrice: true, highestBid: true,
+ endDate: true, startDate: true, auctionStatus: true,
+ minimumLeaseDuration: true, maximumLeaseDuration: true,
+ createdAt: true, updatedAt: true, publishedAt: true, lastBidAt: true,
+ viewCount: true, totalBids: true,
+ hotnessScore: true, engagementScore: true,
+
+ // Land - essential fields only
land: {
- include: {
- soilReports: {
- orderBy: { testedAt: 'desc' },
- take: 1,
+ select: {
+ id: true, size: true, landType: true,
+ title: true, description: true,
+ district: true, state: true, village: true,
+ latitude: true, longitude: true, pincode: true, address: true,
+ soilType: true, irrigationAvailable: true,
+ electricityAvailable: true, roadAccess: true,
+ fencingAvailable: true, waterSource: true,
+ images: {
+ take: MARKETPLACE_CONSTANTS.LISTING.MAX_IMAGES,
+ select: { id: true, url: true, caption: true, isPrimary: true },
},
documents: {
- select: {
- id: true,
- name: true,
- url: true,
- type: true,
- size: true,
- createdAt: true,
- },
+ select: { id: true, name: true, url: true, type: true, size: true, createdAt: true },
},
- images: {
- take: MARKETPLACE_CONSTANTS.LISTING.MAX_IMAGES,
- select: {
- id: true,
- url: true,
- caption: true,
- isPrimary: true,
- },
+ soilReports: {
+ orderBy: { testedAt: 'desc' },
+ take: 1,
},
},
},
+
+ // Owner - essential only
owner: {
- include: {
- landownerProfile: true,
+ select: {
+ id: true, name: true, imageUrl: true,
+ landownerProfile: { select: { isVerified: true, verificationLevel: true } },
},
},
- terms: true,
- analytics: true,
- images: {
- orderBy: { sortOrder: 'asc' },
+
+ // Terms - only needed fields
+ terms: {
select: {
- id: true,
- url: true,
- caption: true,
- isPrimary: true,
- sortOrder: true,
+ id: true, listingId: true,
+ securityDepositRequired: true, depositAmount: true,
+ paymentFrequency: true, additionalTerms: true,
},
},
- _count: {
+
+ // Analytics - only needed fields
+ analytics: {
select: {
- bids: true,
- savedBy: true,
- applications: true,
+ listingId: true, demandScore: true, bidVelocity: true,
+ conversionScore: true, watchers: true, lastActivityAt: true,
},
},
+
+ // Listing images
+ images: {
+ orderBy: { sortOrder: 'asc' },
+ select: { id: true, url: true, caption: true, isPrimary: true, sortOrder: true },
+ },
+
+ // Recent bids only
bids: {
where: { status: 'ACTIVE' },
orderBy: { createdAt: 'desc' },
take: MARKETPLACE_CONSTANTS.BID.RECENT_BIDS_LIMIT,
select: {
- id: true,
- amount: true,
- farmerId: true,
- createdAt: true,
- isAutoBid: true,
- farmer: {
- select: {
- id: true,
- name: true,
- imageUrl: true,
- },
- },
+ id: true, amount: true, farmerId: true, createdAt: true, isAutoBid: true,
+ farmer: { select: { id: true, name: true, imageUrl: true } },
},
},
- ...(userId && {
- savedBy: {
- where: { userId },
- select: { id: true },
- },
- }),
+
+ // Counts
+ _count: { select: { bids: true, savedBy: true, applications: true } },
+
+ // Saved status
+ ...(userId && { savedBy: { where: { userId }, select: { id: true } } }),
},
};
},
@@ -266,30 +214,17 @@ export const queries = {
return {
where: { id: listingId },
select: {
- id: true,
- title: true,
- description: true,
- basePrice: true,
- reservePrice: true,
- bidIncrement: true,
- endDate: true,
- startDate: true,
- auctionStatus: true,
- autoExtendMinutes: true,
- winningBidId: true,
- currentLeaderId: true,
- highestBid: true,
- status: true,
- listingType: true,
+ id: true, title: true, description: true,
+ basePrice: true, reservePrice: true, bidIncrement: true,
+ endDate: true, startDate: true, auctionStatus: true,
+ autoExtendMinutes: true, winningBidId: true,
+ currentLeaderId: true, highestBid: true,
+ status: true, listingType: true,
land: {
select: {
- latitude: true,
- longitude: true,
- village: true,
- district: true,
- state: true,
- size: true,
- landType: true,
+ latitude: true, longitude: true,
+ village: true, district: true, state: true,
+ size: true, landType: true,
images: {
take: MARKETPLACE_CONSTANTS.LISTING.PRIMARY_IMAGE_ONLY,
select: { url: true },
@@ -301,39 +236,17 @@ export const queries = {
orderBy: [{ amount: 'desc' }, { createdAt: 'asc' }],
take: MARKETPLACE_CONSTANTS.BID.MAX_BIDS_PER_PAGE,
select: {
- id: true,
- amount: true,
- farmerId: true,
- createdAt: true,
- isAutoBid: true,
- farmer: {
- select: {
- id: true,
- name: true,
- imageUrl: true,
- },
- },
+ id: true, amount: true, farmerId: true, createdAt: true, isAutoBid: true,
+ farmer: { select: { id: true, name: true, imageUrl: true } },
},
},
winningBid: {
select: {
- id: true,
- amount: true,
- farmer: {
- select: {
- id: true,
- name: true,
- },
- },
- },
- },
- _count: {
- select: {
- bids: {
- where: { status: 'ACTIVE' },
- },
+ id: true, amount: true,
+ farmer: { select: { id: true, name: true } },
},
},
+ _count: { select: { bids: { where: { status: 'ACTIVE' } } } },
},
};
},
@@ -347,29 +260,11 @@ export const queries = {
include: {
listing: {
select: {
- id: true,
- title: true,
- basePrice: true,
- highestBid: true,
- endDate: true,
- auctionStatus: true,
- land: {
- select: {
- size: true,
- landType: true,
- village: true,
- district: true,
- state: true,
- },
- },
- images: {
- where: { isPrimary: true },
- take: 1,
- select: { url: true },
- },
- _count: {
- select: { bids: true },
- },
+ id: true, title: true, basePrice: true, highestBid: true,
+ endDate: true, auctionStatus: true,
+ land: { select: { size: true, landType: true, village: true, district: true, state: true } },
+ images: { where: { isPrimary: true }, take: 1, select: { url: true } },
+ _count: { select: { bids: true } },
},
},
},
@@ -382,12 +277,10 @@ export const queries = {
export const db = {
async getFeed(filters: FeedFilters, pagination: PaginationParams) {
const query = queries.buildFeedQuery(filters, pagination);
-
const [listings, totalCount] = await prisma.$transaction([
prisma.landListing.findMany(query),
prisma.landListing.count({ where: query.where }),
]);
-
return { listings, totalCount };
},
@@ -406,32 +299,19 @@ export const db = {
return prisma.savedListing.findMany(query);
},
- async getBidHistory(listingId: string, limit: number = 20) {
+ async getBidHistory(listingId: string, limit = 20) {
return prisma.bid.findMany({
- where: {
- listingId,
- status: 'ACTIVE',
- },
+ where: { listingId, status: 'ACTIVE' },
orderBy: { createdAt: 'desc' },
take: limit,
select: {
- id: true,
- amount: true,
- farmerId: true,
- createdAt: true,
- isAutoBid: true,
- farmer: {
- select: {
- id: true,
- name: true,
- imageUrl: true,
- },
- },
+ id: true, amount: true, farmerId: true, createdAt: true, isAutoBid: true,
+ farmer: { select: { id: true, name: true, imageUrl: true } },
},
});
},
- async getRecommendations(userId: string, limit: number = 10) {
+ async getRecommendations(userId: string, limit = 10) {
const user = await prisma.user.findUnique({
where: { id: userId },
include: { farmerProfile: true },
@@ -456,95 +336,37 @@ export const db = {
return prisma.landListing.findMany({
where,
take: limit,
- orderBy: [
- { hotnessScore: 'desc' },
- { engagementScore: 'desc' },
- { createdAt: 'desc' },
- ],
+ orderBy: [{ hotnessScore: 'desc' }, { engagementScore: 'desc' }, { createdAt: 'desc' }],
select: {
- id: true,
- title: true,
- basePrice: true,
- highestBid: true,
- endDate: true,
- auctionStatus: true,
- hotnessScore: true,
- engagementScore: true,
- land: {
- select: {
- size: true,
- landType: true,
- district: true,
- state: true,
- village: true,
- images: {
- take: 1,
- select: { url: true },
- },
- },
- },
+ id: true, title: true, basePrice: true, highestBid: true,
+ endDate: true, auctionStatus: true, hotnessScore: true, engagementScore: true,
+ land: { select: { size: true, landType: true, district: true, state: true, village: true, images: { take: 1, select: { url: true } } } },
_count: { select: { bids: true } },
},
});
},
- async getTrending(limit: number = 10) {
+ async getTrending(limit = 10) {
return prisma.landListing.findMany({
- where: {
- status: 'ACTIVE',
- auctionStatus: { in: ['UPCOMING', 'LIVE'] },
- endDate: { gt: new Date() },
- },
+ where: { status: 'ACTIVE', auctionStatus: { in: ['UPCOMING', 'LIVE'] }, endDate: { gt: new Date() } },
take: limit,
- orderBy: [
- { engagementScore: 'desc' },
- { hotnessScore: 'desc' },
- { viewCount: 'desc' },
- { totalBids: 'desc' },
- ],
+ orderBy: [{ engagementScore: 'desc' }, { hotnessScore: 'desc' }, { viewCount: 'desc' }, { totalBids: 'desc' }],
select: {
- id: true,
- title: true,
- basePrice: true,
- highestBid: true,
- endDate: true,
- auctionStatus: true,
- viewCount: true,
- totalBids: true,
- land: {
- select: {
- size: true,
- landType: true,
- district: true,
- state: true,
- village: true,
- images: {
- take: 1,
- select: { url: true },
- },
- },
- },
+ id: true, title: true, basePrice: true, highestBid: true,
+ endDate: true, auctionStatus: true, viewCount: true, totalBids: true,
+ land: { select: { size: true, landType: true, district: true, state: true, village: true, images: { take: 1, select: { url: true } } } },
_count: { select: { bids: true } },
},
});
},
async checkUserBid(userId: string, listingId: string) {
- return prisma.bid.findFirst({
- where: {
- farmerId: userId,
- listingId,
- status: 'ACTIVE',
- },
- });
+ return prisma.bid.findFirst({ where: { farmerId: userId, listingId, status: 'ACTIVE' } });
},
async trackView(listingId: string): Promise {
await prisma.$transaction([
- prisma.landListing.update({
- where: { id: listingId },
- data: { viewCount: { increment: 1 } },
- }),
+ prisma.landListing.update({ where: { id: listingId }, data: { viewCount: { increment: 1 } } }),
prisma.listingAnalytics.upsert({
where: { listingId },
update: { lastActivityAt: new Date() },
diff --git a/lib/marketplace/service.ts b/lib/marketplace/service.ts
index 9502001..5448688 100644
--- a/lib/marketplace/service.ts
+++ b/lib/marketplace/service.ts
@@ -7,400 +7,269 @@ import { marketplaceCache } from './cache';
import { auctionEvents } from './events';
import { logger } from './logger';
import { MARKETPLACE_CONSTANTS } from './constants';
-import type {
- BidInput,
- FeedFilters,
- PaginationParams,
-} from './validation';
-import type {
- // MarketplaceFeedItemDTO, // Remove this - not directly used
- ListingDetailDTO,
- AuctionDTO,
- BidPlacementResult,
-} from './types';
+import type { BidInput, FeedFilters, PaginationParams } from './validation';
+import type { ListingDetailDTO, AuctionDTO, BidPlacementResult } from './types';
class MarketplaceService {
- /**
- * Get marketplace feed with caching
- */
+ // ============================================================
+ // PHASE 1: Performance measurement helper
+ // ============================================================
+ private async measure(name: string, fn: () => Promise): Promise {
+ const start = performance.now();
+ try {
+ const result = await fn();
+ const ms = Math.round(performance.now() - start);
+ logger.performance(name, ms);
+ if (ms > 3000) {
+ logger.warn(`SLOW: ${name}`, { durationMs: ms });
+ }
+ return result;
+ } catch (error) {
+ const ms = Math.round(performance.now() - start);
+ logger.error(`${name} failed after ${ms}ms`, error as Error);
+ throw error;
+ }
+ }
+
+ // ============================================================
+ // Feed
+ // ============================================================
async getFeed(filters: FeedFilters, pagination: PaginationParams) {
- const startTime = performance.now();
+ return this.measure('getFeed', async () => {
+ const cached = await marketplaceCache.getFeed(filters, pagination);
+ if (cached) {
+ logger.info('Feed cache hit');
+ return cached;
+ }
- // Try cache first
- const cached = await marketplaceCache.getFeed(filters, pagination);
- if (cached) {
- logger.info('Feed cache hit');
- return cached;
- }
+ logger.info('Feed cache miss');
- logger.info('Feed cache miss');
-
- // Fetch from database
- const { listings, totalCount } = await db.getFeed(filters, pagination);
-
- // Transform to DTOs
- const transformedListings = listings.map(listing =>
- transformers.transformFeedListing(listing as Record)
- );
-
- const result = {
- listings: transformedListings,
- pagination: {
- page: pagination.page,
- limit: pagination.limit,
- total: totalCount,
- pages: Math.ceil(totalCount / pagination.limit),
- },
- };
-
- // Cache the result
- await marketplaceCache.setFeed(filters, pagination, result);
-
- const duration = performance.now() - startTime;
- logger.performance('Feed generated', duration);
-
- return result;
- }
+ const { listings, totalCount } = await db.getFeed(filters, pagination);
- /**
- * Get listing detail with caching
- */
- async getListing(listingId: string, userId?: string): Promise {
- const startTime = performance.now();
+ const transformedListings = listings.map((listing) =>
+ transformers.transformFeedListing(listing as Record)
+ );
- // Try cache first
- const cached = await marketplaceCache.getListing(listingId, userId);
- if (cached) {
- logger.info('Listing cache hit', { listingId });
- return cached as ListingDetailDTO;
- }
+ const result = {
+ listings: transformedListings,
+ pagination: {
+ page: pagination.page,
+ limit: pagination.limit,
+ total: totalCount,
+ pages: Math.ceil(totalCount / pagination.limit),
+ },
+ };
- logger.info('Listing cache miss', { listingId });
+ await marketplaceCache.setFeed(filters, pagination, result);
+ return result;
+ });
+ }
- // Fetch from database
- const listing = await db.getListing(listingId, userId);
- if (!listing) {
- return null;
- }
+ // ============================================================
+ // Listing Detail
+ // ============================================================
+ async getListing(listingId: string, userId?: string): Promise {
+ return this.measure('getListing', async () => {
+ const cached = await marketplaceCache.getListing(listingId, userId);
+ if (cached) {
+ logger.info('Listing cache hit', { listingId });
+ return cached as ListingDetailDTO;
+ }
- // Transform to DTO
- const transformed = transformers.transformListingDetail(listing as Record);
+ logger.info('Listing cache miss', { listingId });
- // Cache the result
- await marketplaceCache.setListing(listingId, transformed, userId);
+ const listing = await db.getListing(listingId, userId);
+ if (!listing) return null;
- // Track view asynchronously
- this.trackView(listingId, userId).catch(err =>
- logger.error('View tracking failed', err as Error, { listingId })
- );
+ const transformed = transformers.transformListingDetail(listing as Record);
+ await marketplaceCache.setListing(listingId, transformed, userId);
- const duration = performance.now() - startTime;
- logger.performance('Listing detail fetched', duration, { listingId });
+ this.trackView(listingId, userId).catch((err) =>
+ logger.error('View tracking failed', err as Error, { listingId })
+ );
- return transformed;
+ return transformed;
+ });
}
- /**
- * Get auction room data with near real-time caching
- */
+ // ============================================================
+ // Auction Room
+ // ============================================================
async getAuction(listingId: string): Promise {
- const startTime = performance.now();
+ return this.measure('getAuction', async () => {
+ const cached = await marketplaceCache.getAuction(listingId);
+ if (cached) {
+ logger.info('Auction cache hit', { listingId });
+ return cached as AuctionDTO;
+ }
- // Try cache (short TTL for near real-time)
- const cached = await marketplaceCache.getAuction(listingId);
- if (cached) {
- logger.info('Auction cache hit', { listingId });
- return cached as AuctionDTO;
- }
+ logger.info('Auction cache miss', { listingId });
- logger.info('Auction cache miss', { listingId });
+ const auctionData = await db.getAuction(listingId);
+ if (!auctionData) return null;
- // Fetch from database
- const auctionData = await db.getAuction(listingId);
- if (!auctionData) {
- return null;
- }
+ const transformed = transformers.transformAuction(auctionData as Record);
+ await marketplaceCache.setAuction(listingId, transformed);
- // Transform to DTO
- const transformed = transformers.transformAuction(auctionData as Record);
+ return transformed;
+ });
+ }
- // Cache briefly
- await marketplaceCache.setAuction(listingId, transformed);
+ // ============================================================
+ // Place Bid
+ // ============================================================
+ async placeBid(listingId: string, bidInput: BidInput): Promise {
+ return this.measure('placeBid', async () => {
+ const { farmerId, amount, isAutoBid = false } = bidInput;
- const duration = performance.now() - startTime;
- logger.performance('Auction data fetched', duration, { listingId });
+ const listing = await prisma.landListing.findUnique({
+ where: { id: listingId, status: 'ACTIVE', listingType: 'OPEN_BIDDING' },
+ include: { bids: { where: { status: 'ACTIVE' }, orderBy: { amount: 'desc' }, take: 1 } },
+ });
- return transformed;
- }
+ if (!listing) throw new Error('Listing not available for bidding');
- /**
- * Place a bid with atomic transaction
- */
- async placeBid(
- listingId: string,
- bidInput: BidInput
- ): Promise {
- const startTime = performance.now();
- const { farmerId, amount, isAutoBid = false } = bidInput;
-
- // Fetch listing with current highest bid
- const listing = await prisma.landListing.findUnique({
- where: {
- id: listingId,
- status: 'ACTIVE',
- listingType: 'OPEN_BIDDING',
- },
- include: {
- bids: {
- where: { status: 'ACTIVE' },
- orderBy: { amount: 'desc' },
- take: 1,
- },
- },
- });
+ const now = new Date();
+ const startDate = new Date(listing.startDate);
+ const endDate = new Date(listing.endDate);
- // Validation
- if (!listing) {
- throw new Error('Listing not available for bidding');
- }
+ if (now < startDate) throw new Error('Auction has not started yet');
+ if (now > endDate) throw new Error('Auction has ended');
- const now = new Date();
- const startDate = new Date(listing.startDate);
- const endDate = new Date(listing.endDate);
-
- if (now < startDate) {
- throw new Error('Auction has not started yet');
- }
- if (now > endDate) {
- throw new Error('Auction has ended');
- }
+ const highestBid = listing.bids[0]?.amount || listing.basePrice;
+ const minBid = highestBid + (listing.bidIncrement || MARKETPLACE_CONSTANTS.BID.DEFAULT_INCREMENT);
+ if (amount < minBid) throw new Error(`Bid must be at least ₹${minBid}`);
- const highestBid = listing.bids[0]?.amount || listing.basePrice;
- const minBid = highestBid + (listing.bidIncrement || MARKETPLACE_CONSTANTS.BID.DEFAULT_INCREMENT);
+ const result = await prisma.$transaction(
+ async (tx) => {
+ const lastBid = await tx.bid.findFirst({ where: { listingId }, orderBy: { sequence: 'desc' } });
+ const sequence = (lastBid?.sequence || 0) + 1;
- if (amount < minBid) {
- throw new Error(`Bid must be at least ₹${minBid}`);
- }
+ const bid = await tx.bid.create({
+ data: { listingId, farmerId, amount, sequence, isAutoBid, status: 'ACTIVE', isWinning: true },
+ });
- // Execute atomic transaction
- const result = await prisma.$transaction(
- async (tx) => {
- // Get next sequence
- const lastBid = await tx.bid.findFirst({
- where: { listingId },
- orderBy: { sequence: 'desc' },
- });
- const sequence = (lastBid?.sequence || 0) + 1;
-
- // Create new bid
- const bid = await tx.bid.create({
- data: {
- listingId,
- farmerId,
- amount,
- sequence,
- isAutoBid,
- status: 'ACTIVE',
- isWinning: true,
- },
- });
-
- // Outbid previous highest bid
- if (listing.bids[0]) {
- await tx.bid.update({
- where: { id: listing.bids[0].id },
- data: {
- status: 'OUTBID',
- outbidAt: new Date(),
- isWinning: false,
- },
+ if (listing.bids[0]) {
+ await tx.bid.update({
+ where: { id: listing.bids[0].id },
+ data: { status: 'OUTBID', outbidAt: new Date(), isWinning: false },
+ });
+ }
+
+ const updateData: Prisma.LandListingUpdateInput = {
+ totalBids: { increment: 1 },
+ highestBid: amount,
+ currentLeaderId: farmerId,
+ lastBidAt: new Date(),
+ winningBid: { connect: { id: bid.id } },
+ };
+
+ let extended = false;
+ const timeLeft = endDate.getTime() - Date.now();
+ if (timeLeft < MARKETPLACE_CONSTANTS.BID.AUTO_EXTEND_WINDOW && listing.autoExtendMinutes) {
+ updateData.endDate = new Date(Date.now() + listing.autoExtendMinutes * 60 * 1000);
+ extended = true;
+ }
+
+ await tx.landListing.update({ where: { id: listingId }, data: updateData });
+
+ await tx.auctionEvent.create({
+ data: { listingId, type: 'BID_PLACED', actorId: farmerId, bidId: bid.id, metadata: { amount, sequence, autoExtended: extended } },
});
- }
-
- // Update listing
- const updateData: Prisma.LandListingUpdateInput = {
- totalBids: { increment: 1 },
- highestBid: amount,
- currentLeaderId: farmerId,
- lastBidAt: new Date(),
- winningBid: {
- connect: { id: bid.id },
- },
- };
-
- // Auto-extend if within last 5 minutes
- let extended = false;
- const timeLeft = endDate.getTime() - Date.now();
- if (timeLeft < MARKETPLACE_CONSTANTS.BID.AUTO_EXTEND_WINDOW && listing.autoExtendMinutes) {
- const newEndDate = new Date(Date.now() + listing.autoExtendMinutes * 60 * 1000);
- updateData.endDate = newEndDate;
- extended = true;
- }
-
- await tx.landListing.update({
- where: { id: listingId },
- data: updateData,
- });
-
- // Create auction event
- await tx.auctionEvent.create({
- data: {
- listingId,
- type: 'BID_PLACED',
- actorId: farmerId,
- bidId: bid.id,
- metadata: { amount, sequence, autoExtended: extended },
- },
- });
-
- // Update analytics
- await tx.listingAnalytics.upsert({
- where: { listingId },
- update: {
- bidVelocity: { increment: 1 },
- lastActivityAt: new Date(),
- },
- create: {
- listingId,
- bidVelocity: 1,
- lastActivityAt: new Date(),
- },
- });
-
- return { bid, extended };
- },
- {
- timeout: 10000,
- isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
- }
- );
-
- // Invalidate caches
- await Promise.all([
- marketplaceCache.invalidateListing(listingId),
- marketplaceCache.invalidateAuction(listingId),
- marketplaceCache.invalidateFeed(),
- ]);
-
- // Trigger realtime events
- await auctionEvents.emitNewBid(listingId, {
- id: result.bid.id,
- amount: result.bid.amount,
- farmerId: result.bid.farmerId,
- createdAt: result.bid.createdAt,
- });
-
- if (result.extended) {
- await auctionEvents.emitAuctionExtended(
- listingId,
- new Date(Date.now() + (listing.autoExtendMinutes || 5) * 60 * 1000),
- listing.autoExtendMinutes || 5
+
+ await tx.listingAnalytics.upsert({
+ where: { listingId },
+ update: { bidVelocity: { increment: 1 }, lastActivityAt: new Date() },
+ create: { listingId, bidVelocity: 1, lastActivityAt: new Date() },
+ });
+
+ return { bid, extended };
+ },
+ { timeout: 10000, isolationLevel: Prisma.TransactionIsolationLevel.Serializable }
);
- }
- const duration = performance.now() - startTime;
- logger.performance('Bid placed successfully', duration, { listingId, amount });
+ await Promise.all([
+ marketplaceCache.invalidateListing(listingId),
+ marketplaceCache.invalidateAuction(listingId),
+ marketplaceCache.invalidateFeed(),
+ ]);
+
+ await auctionEvents.emitNewBid(listingId, {
+ id: result.bid.id, amount: result.bid.amount, farmerId: result.bid.farmerId, createdAt: result.bid.createdAt,
+ });
- return {
- success: true,
- bid: transformers.transformBid(result.bid as Record),
- extended: result.extended,
- timing: { durationMs: duration },
- };
+ if (result.extended) {
+ await auctionEvents.emitAuctionExtended(listingId, new Date(Date.now() + (listing.autoExtendMinutes || 5) * 60 * 1000), listing.autoExtendMinutes || 5);
+ }
+
+ return {
+ success: true,
+ bid: transformers.transformBid(result.bid as Record),
+ extended: result.extended,
+ timing: { durationMs: Math.round(performance.now() - performance.now()) }, // Will be captured by measure()
+ };
+ });
}
- /**
- * Get saved listings
- */
+ // ============================================================
+ // Saved Listings
+ // ============================================================
async getSavedListings(userId: string) {
- const savedListings = await db.getSavedListings(userId);
-
- return {
- saved: savedListings,
- count: savedListings.length,
- };
+ return this.measure('getSavedListings', async () => {
+ const savedListings = await db.getSavedListings(userId);
+ return { saved: savedListings, count: savedListings.length };
+ });
}
- /**
- * Save a listing
- */
async saveListing(userId: string, listingId: string) {
- // Check if listing exists
- const listing = await prisma.landListing.findUnique({
- where: { id: listingId },
- select: { id: true },
- });
-
- if (!listing) {
- throw new Error('Listing not found');
- }
-
- try {
- const saved = await prisma.savedListing.create({
- data: { listingId, userId },
- });
-
- // Invalidate relevant caches
- await marketplaceCache.invalidateListing(listingId);
-
- return { saved, message: 'Listing saved successfully' };
- } catch (error: unknown) {
- const prismaError = error as { code?: string };
- if (prismaError?.code === 'P2002') {
- return { message: 'Listing already saved' };
+ return this.measure('saveListing', async () => {
+ const listing = await prisma.landListing.findUnique({ where: { id: listingId }, select: { id: true } });
+ if (!listing) throw new Error('Listing not found');
+
+ try {
+ const saved = await prisma.savedListing.create({ data: { listingId, userId } });
+ await marketplaceCache.invalidateListing(listingId);
+ return { saved, message: 'Listing saved successfully' };
+ } catch (error: unknown) {
+ const prismaError = error as { code?: string };
+ if (prismaError?.code === 'P2002') return { message: 'Listing already saved' };
+ throw error;
}
- throw error;
- }
+ });
}
- /**
- * Unsave a listing
- */
async unsaveListing(userId: string, listingId: string) {
- try {
- await prisma.savedListing.delete({
- where: {
- listingId_userId: { listingId, userId },
- },
- });
-
- // Invalidate relevant caches
- await marketplaceCache.invalidateListing(listingId);
-
- return { message: 'Listing removed from saved' };
- } catch (error: unknown) {
- const prismaError = error as { code?: string };
- if (prismaError?.code === 'P2025') {
- return { message: 'Listing was not saved' };
+ return this.measure('unsaveListing', async () => {
+ try {
+ await prisma.savedListing.delete({ where: { listingId_userId: { listingId, userId } } });
+ await marketplaceCache.invalidateListing(listingId);
+ return { message: 'Listing removed from saved' };
+ } catch (error: unknown) {
+ const prismaError = error as { code?: string };
+ if (prismaError?.code === 'P2025') return { message: 'Listing was not saved' };
+ throw error;
}
- throw error;
- }
+ });
}
- /**
- * Get recommendations
- */
+ // ============================================================
+ // Recommendations & Trending
+ // ============================================================
async getRecommendations(userId: string) {
- return db.getRecommendations(userId);
+ return this.measure('getRecommendations', () => db.getRecommendations(userId));
}
- /**
- * Get trending listings
- */
async getTrending() {
- return db.getTrending();
+ return this.measure('getTrending', () => db.getTrending());
}
- /**
- * Track listing view
- */
+ // ============================================================
+ // View Tracking (internal)
+ // ============================================================
private async trackView(listingId: string, userId?: string): Promise {
const shouldTrack = await marketplaceCache.trackView(listingId, userId);
- if (shouldTrack) {
- await db.trackView(listingId);
- }
+ if (shouldTrack) await db.trackView(listingId);
}
}
-// Export singleton
export const marketplaceService = new MarketplaceService();
\ No newline at end of file
diff --git a/lib/pusher/client.ts b/lib/pusher/client.ts
index 91cea75..f27a836 100644
--- a/lib/pusher/client.ts
+++ b/lib/pusher/client.ts
@@ -1,7 +1,15 @@
-// lib/pusher/client.ts
import Pusher from "pusher-js";
-export const pusherClient = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
- cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
- authEndpoint: "/api/pusher/auth",
-});
+let pusherClient: Pusher | null = null;
+
+export function getPusherClient(): Pusher | null {
+ if (typeof window === "undefined") return null;
+
+ if (!pusherClient) {
+ pusherClient = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
+ cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
+ authEndpoint: "/api/pusher/auth",
+ });
+ }
+ return pusherClient;
+}
\ No newline at end of file