// Offer Service for Marketplace // Handles offer management for marketplace listings import { Offer, OfferStatus, Listing, ListingStatus, CreateOfferInput, } from './types'; import { offerStore, listingStore, generateId } from './store'; export class OfferService { /** * Create a new offer on a listing */ async createOffer( buyerId: string, buyerName: string, input: CreateOfferInput ): Promise { const listing = listingStore.getById(input.listingId); if (!listing) { throw new Error('Listing not found'); } if (listing.status !== ListingStatus.ACTIVE) { throw new Error('Cannot make offers on inactive listings'); } if (listing.sellerId === buyerId) { throw new Error('Cannot make an offer on your own listing'); } // Check for existing pending offer from this buyer const existingOffer = offerStore.getByListingId(input.listingId) .find(o => o.buyerId === buyerId && o.status === OfferStatus.PENDING); if (existingOffer) { throw new Error('You already have a pending offer on this listing'); } const offer: Offer = { id: generateId(), listingId: input.listingId, buyerId, buyerName, amount: input.amount, message: input.message, status: OfferStatus.PENDING, createdAt: new Date(), updatedAt: new Date(), expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days }; return offerStore.create(offer); } /** * Get an offer by ID */ async getOfferById(id: string): Promise { return offerStore.getById(id) || null; } /** * Get all offers for a listing */ async getOffersForListing(listingId: string): Promise { return offerStore.getByListingId(listingId); } /** * Get all offers made by a buyer */ async getOffersByBuyer(buyerId: string): Promise { return offerStore.getByBuyerId(buyerId); } /** * Get all offers for a seller's listings */ async getOffersForSeller(sellerId: string): Promise<(Offer & { listing?: Listing })[]> { const sellerListings = listingStore.getBySellerId(sellerId); const listingIds = new Set(sellerListings.map(l => l.id)); const offers = offerStore.getAll().filter(o => listingIds.has(o.listingId)); return offers.map(offer => ({ ...offer, listing: listingStore.getById(offer.listingId), })); } /** * Accept an offer */ async acceptOffer(offerId: string, sellerId: string): Promise { const offer = offerStore.getById(offerId); if (!offer) { throw new Error('Offer not found'); } const listing = listingStore.getById(offer.listingId); if (!listing) { throw new Error('Listing not found'); } if (listing.sellerId !== sellerId) { throw new Error('Unauthorized: You do not own this listing'); } if (offer.status !== OfferStatus.PENDING) { throw new Error('Can only accept pending offers'); } // Accept this offer const acceptedOffer = offerStore.update(offerId, { status: OfferStatus.ACCEPTED }); // Reject all other pending offers for this listing const otherOffers = offerStore.getByListingId(offer.listingId) .filter(o => o.id !== offerId && o.status === OfferStatus.PENDING); for (const otherOffer of otherOffers) { offerStore.update(otherOffer.id, { status: OfferStatus.REJECTED }); } // Mark listing as sold listingStore.update(offer.listingId, { status: ListingStatus.SOLD, quantity: 0 }); if (!acceptedOffer) { throw new Error('Failed to accept offer'); } return acceptedOffer; } /** * Reject an offer */ async rejectOffer(offerId: string, sellerId: string): Promise { const offer = offerStore.getById(offerId); if (!offer) { throw new Error('Offer not found'); } const listing = listingStore.getById(offer.listingId); if (!listing) { throw new Error('Listing not found'); } if (listing.sellerId !== sellerId) { throw new Error('Unauthorized: You do not own this listing'); } if (offer.status !== OfferStatus.PENDING) { throw new Error('Can only reject pending offers'); } const rejectedOffer = offerStore.update(offerId, { status: OfferStatus.REJECTED }); if (!rejectedOffer) { throw new Error('Failed to reject offer'); } return rejectedOffer; } /** * Withdraw an offer (buyer action) */ async withdrawOffer(offerId: string, buyerId: string): Promise { const offer = offerStore.getById(offerId); if (!offer) { throw new Error('Offer not found'); } if (offer.buyerId !== buyerId) { throw new Error('Unauthorized: This is not your offer'); } if (offer.status !== OfferStatus.PENDING) { throw new Error('Can only withdraw pending offers'); } const withdrawnOffer = offerStore.update(offerId, { status: OfferStatus.WITHDRAWN }); if (!withdrawnOffer) { throw new Error('Failed to withdraw offer'); } return withdrawnOffer; } /** * Counter offer (update amount) */ async updateOfferAmount( offerId: string, buyerId: string, newAmount: number ): Promise { const offer = offerStore.getById(offerId); if (!offer) { throw new Error('Offer not found'); } if (offer.buyerId !== buyerId) { throw new Error('Unauthorized: This is not your offer'); } if (offer.status !== OfferStatus.PENDING) { throw new Error('Can only update pending offers'); } const updatedOffer = offerStore.update(offerId, { amount: newAmount }); if (!updatedOffer) { throw new Error('Failed to update offer'); } return updatedOffer; } /** * Get offer statistics */ async getOfferStats(userId: string, role: 'buyer' | 'seller'): Promise<{ totalOffers: number; pendingOffers: number; acceptedOffers: number; rejectedOffers: number; }> { let offers: Offer[]; if (role === 'buyer') { offers = offerStore.getByBuyerId(userId); } else { const sellerListings = listingStore.getBySellerId(userId); const listingIds = new Set(sellerListings.map(l => l.id)); offers = offerStore.getAll().filter(o => listingIds.has(o.listingId)); } return { totalOffers: offers.length, pendingOffers: offers.filter(o => o.status === OfferStatus.PENDING).length, acceptedOffers: offers.filter(o => o.status === OfferStatus.ACCEPTED).length, rejectedOffers: offers.filter(o => o.status === OfferStatus.REJECTED).length, }; } /** * Expire old pending offers */ async expireOldOffers(): Promise { const now = new Date(); let expiredCount = 0; const pendingOffers = offerStore.getAll().filter( o => o.status === OfferStatus.PENDING && o.expiresAt ); for (const offer of pendingOffers) { if (offer.expiresAt && offer.expiresAt < now) { offerStore.update(offer.id, { status: OfferStatus.EXPIRED }); expiredCount++; } } return expiredCount; } } // Export singleton instance export const offerService = new OfferService();