// Listing Service for Marketplace // Handles CRUD operations for marketplace listings import { Listing, ListingStatus, CreateListingInput, UpdateListingInput, } from './types'; import { listingStore, generateId } from './store'; export class ListingService { /** * Create a new listing */ async createListing( sellerId: string, sellerName: string, input: CreateListingInput ): Promise { const listing: Listing = { id: generateId(), sellerId, sellerName, plantId: input.plantId, title: input.title, description: input.description, price: input.price, currency: input.currency || 'USD', quantity: input.quantity, category: input.category, status: ListingStatus.DRAFT, location: input.location, tags: input.tags || [], images: [], viewCount: 0, createdAt: new Date(), updatedAt: new Date(), expiresAt: input.expiresAt, }; return listingStore.create(listing); } /** * Get a listing by ID */ async getListingById(id: string): Promise { const listing = listingStore.getById(id); return listing || null; } /** * Get a listing and increment view count */ async viewListing(id: string): Promise { listingStore.incrementViewCount(id); return this.getListingById(id); } /** * Get all listings by seller */ async getListingsBySeller(sellerId: string): Promise { return listingStore.getBySellerId(sellerId); } /** * Get all active listings */ async getActiveListings(): Promise { return listingStore.getAll().filter(l => l.status === ListingStatus.ACTIVE); } /** * Update a listing */ async updateListing( id: string, sellerId: string, updates: UpdateListingInput ): Promise { const listing = listingStore.getById(id); if (!listing) { return null; } // Verify ownership if (listing.sellerId !== sellerId) { throw new Error('Unauthorized: You do not own this listing'); } const updated = listingStore.update(id, updates); return updated || null; } /** * Publish a draft listing (make it active) */ async publishListing(id: string, sellerId: string): Promise { const listing = listingStore.getById(id); if (!listing) { return null; } if (listing.sellerId !== sellerId) { throw new Error('Unauthorized: You do not own this listing'); } if (listing.status !== ListingStatus.DRAFT) { throw new Error('Only draft listings can be published'); } return listingStore.update(id, { status: ListingStatus.ACTIVE }) || null; } /** * Cancel a listing */ async cancelListing(id: string, sellerId: string): Promise { const listing = listingStore.getById(id); if (!listing) { return null; } if (listing.sellerId !== sellerId) { throw new Error('Unauthorized: You do not own this listing'); } if (listing.status === ListingStatus.SOLD) { throw new Error('Cannot cancel a sold listing'); } return listingStore.update(id, { status: ListingStatus.CANCELLED }) || null; } /** * Mark listing as sold */ async markAsSold(id: string, sellerId: string): Promise { const listing = listingStore.getById(id); if (!listing) { return null; } if (listing.sellerId !== sellerId) { throw new Error('Unauthorized: You do not own this listing'); } return listingStore.update(id, { status: ListingStatus.SOLD, quantity: 0 }) || null; } /** * Delete a listing (only drafts or cancelled) */ async deleteListing(id: string, sellerId: string): Promise { const listing = listingStore.getById(id); if (!listing) { return false; } if (listing.sellerId !== sellerId) { throw new Error('Unauthorized: You do not own this listing'); } if (listing.status === ListingStatus.ACTIVE || listing.status === ListingStatus.SOLD) { throw new Error('Cannot delete active or sold listings. Cancel first.'); } return listingStore.delete(id); } /** * Get listing statistics for a seller */ async getSellerStats(sellerId: string): Promise<{ totalListings: number; activeListings: number; soldListings: number; totalViews: number; averagePrice: number; }> { const listings = listingStore.getBySellerId(sellerId); const activeListings = listings.filter(l => l.status === ListingStatus.ACTIVE); const soldListings = listings.filter(l => l.status === ListingStatus.SOLD); const totalViews = listings.reduce((sum, l) => sum + l.viewCount, 0); const averagePrice = listings.length > 0 ? listings.reduce((sum, l) => sum + l.price, 0) / listings.length : 0; return { totalListings: listings.length, activeListings: activeListings.length, soldListings: soldListings.length, totalViews, averagePrice: Math.round(averagePrice * 100) / 100, }; } /** * Check and expire old listings */ async expireOldListings(): Promise { const now = new Date(); let expiredCount = 0; const activeListings = listingStore.getAll().filter( l => l.status === ListingStatus.ACTIVE && l.expiresAt ); for (const listing of activeListings) { if (listing.expiresAt && listing.expiresAt < now) { listingStore.update(listing.id, { status: ListingStatus.EXPIRED }); expiredCount++; } } return expiredCount; } } // Export singleton instance export const listingService = new ListingService();