// 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();