// Matching Service for Marketplace // Matches buyers with sellers based on preferences and listings import { Listing, ListingCategory, ListingStatus } from './types'; import { listingStore, sellerProfileStore } from './store'; export interface BuyerPreferences { categories: ListingCategory[]; maxPrice?: number; preferredLocation?: { lat: number; lng: number; maxDistanceKm: number; }; preferredTags?: string[]; preferVerifiedSellers?: boolean; } export interface MatchResult { listing: Listing; score: number; matchReasons: string[]; } export class MatchingService { /** * Find listings that match buyer preferences */ async findMatchesForBuyer( buyerId: string, preferences: BuyerPreferences, limit: number = 10 ): Promise { const activeListings = listingStore .getAll() .filter(l => l.status === ListingStatus.ACTIVE && l.sellerId !== buyerId); const matches: MatchResult[] = []; for (const listing of activeListings) { const { score, reasons } = this.calculateMatchScore(listing, preferences); if (score > 0) { matches.push({ listing, score, matchReasons: reasons, }); } } // Sort by score descending matches.sort((a, b) => b.score - a.score); return matches.slice(0, limit); } /** * Find buyers who might be interested in a listing * (In a real system, this would query user preferences) */ async findPotentialBuyers(listingId: string): Promise { // Placeholder - would match against stored buyer preferences // For now, return empty array as we don't have buyer preference storage return []; } /** * Get recommended listings for a user based on their history */ async getRecommendedListings( userId: string, limit: number = 8 ): Promise { // Get user's purchase history and viewed listings // For now, return featured listings as recommendations const activeListings = listingStore .getAll() .filter(l => l.status === ListingStatus.ACTIVE && l.sellerId !== userId); // Sort by a combination of view count and recency return activeListings .sort((a, b) => { const aScore = a.viewCount * 0.7 + this.recencyScore(a.createdAt) * 0.3; const bScore = b.viewCount * 0.7 + this.recencyScore(b.createdAt) * 0.3; return bScore - aScore; }) .slice(0, limit); } /** * Find sellers with good ratings in a category */ async findTopSellersInCategory( category: ListingCategory, limit: number = 5 ): Promise<{ userId: string; displayName: string; rating: number; listingCount: number }[]> { // Get all active listings in category const categoryListings = listingStore .getAll() .filter(l => l.status === ListingStatus.ACTIVE && l.category === category); // Group by seller const sellerStats: Record = {}; for (const listing of categoryListings) { if (!sellerStats[listing.sellerId]) { sellerStats[listing.sellerId] = { count: 0, sellerId: listing.sellerId }; } sellerStats[listing.sellerId].count++; } // Get seller profiles and sort by rating const topSellers = Object.values(sellerStats) .map(stat => { const profile = sellerProfileStore.getByUserId(stat.sellerId); return { userId: stat.sellerId, displayName: profile?.displayName || 'Unknown Seller', rating: profile?.rating || 0, listingCount: stat.count, }; }) .sort((a, b) => b.rating - a.rating) .slice(0, limit); return topSellers; } /** * Calculate match score between a listing and buyer preferences */ private calculateMatchScore( listing: Listing, preferences: BuyerPreferences ): { score: number; reasons: string[] } { let score = 0; const reasons: string[] = []; // Category match (highest weight) if (preferences.categories.includes(listing.category)) { score += 40; reasons.push(`Matches preferred category: ${listing.category}`); } // Price within budget if (preferences.maxPrice && listing.price <= preferences.maxPrice) { score += 20; reasons.push('Within budget'); } // Location match if (preferences.preferredLocation && listing.location) { const distance = this.calculateDistance( preferences.preferredLocation.lat, preferences.preferredLocation.lng, listing.location.lat, listing.location.lng ); if (distance <= preferences.preferredLocation.maxDistanceKm) { score += 25; reasons.push(`Nearby (${Math.round(distance)}km away)`); } } // Tag matches if (preferences.preferredTags && preferences.preferredTags.length > 0) { const matchingTags = listing.tags.filter(tag => preferences.preferredTags!.includes(tag.toLowerCase()) ); if (matchingTags.length > 0) { score += matchingTags.length * 5; reasons.push(`Matching tags: ${matchingTags.join(', ')}`); } } // Verified seller preference if (preferences.preferVerifiedSellers) { const profile = sellerProfileStore.getByUserId(listing.sellerId); if (profile?.verified) { score += 10; reasons.push('Verified seller'); } } return { score, reasons }; } /** * Calculate recency score (0-100, higher for more recent) */ private recencyScore(date: Date): number { const now = Date.now(); const created = date.getTime(); const daysOld = (now - created) / (1000 * 60 * 60 * 24); // Exponential decay: 100 for today, ~37 for 7 days, ~14 for 14 days return 100 * Math.exp(-daysOld / 10); } /** * Calculate distance between two points (Haversine formula) */ private calculateDistance( lat1: number, lng1: number, lat2: number, lng2: number ): number { const R = 6371; // Earth's radius in km const dLat = this.toRad(lat2 - lat1); const dLng = this.toRad(lng2 - lng1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRad(lat1)) * Math.cos(this.toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } private toRad(deg: number): number { return deg * (Math.PI / 180); } } // Export singleton instance export const matchingService = new MatchingService();