import config from '../../config.js'; import { listingModel } from '../../database/schemas/sales/listing.schema.js'; import { listingVarientModel } from '../../database/schemas/sales/listingvarient.schema.js'; import { marketplaceModel } from '../../database/schemas/sales/marketplace.schema.js'; import log4js from 'log4js'; import mongoose from 'mongoose'; import { deleteObject, listObjects, getObject, editObject, newObject, listObjectsByProperties, getModelStats, getModelHistory, checkStates, searchObjects, getPropertyValues, getObjectNeighbors, } from '../../database/database.js'; import { hasIntegration, createListing as createExternalListing, updateListing as updateExternalListing, deleteListing as deleteExternalListing, publishListing as publishExternalListing, unpublishListing as unpublishExternalListing, MARKETPLACE_BUSY_STATES, } from '../../integrations/marketplace.js'; const logger = log4js.getLogger('Listings'); logger.level = config.server.logLevel; const LISTING_POPULATE = [ 'product', 'vendor', 'stockLocation', 'courierServices', 'fulfillmentPolicy', 'paymentPolicy', 'returnPolicy', 'listingImages', { path: 'marketplace', populate: ['defaultFulfillmentPolicy', 'defaultPaymentPolicy', 'defaultReturnPolicy'], }, { path: 'fulfillmentPolicy', populate: ['courierServices', 'marketplaces.marketplace'], }, { path: 'paymentPolicy', populate: ['marketplaces.marketplace'], }, { path: 'returnPolicy', populate: ['marketplaces.marketplace'], }, ]; function pushToMarketplace( marketplaceId, listingData, user, { isNew = false, isDelete = false } = {} ) { const run = async () => { try { const marketplace = await marketplaceModel.findById(marketplaceId); if (!marketplace || !marketplace.active || !hasIntegration(marketplace.provider)) { return; } if (isDelete) { await deleteExternalListing(marketplace, user, listingData); } else if (isNew) { await createExternalListing(marketplace, user, listingData); } else { await updateExternalListing(marketplace, user, listingData); } } catch (err) { logger.warn(`Failed to initiate marketplace sync for listing: ${err.message}`); } }; run(); } export const listListingsRouteHandler = async ( req, res, page = 1, limit = 25, property = '', filter = {}, search = '', sort = '', order = 'ascend' ) => { const result = await listObjects({ model: listingModel, page, limit, property, filter, search, sort, order, populate: LISTING_POPULATE, }); if (result?.error) { logger.error('Error listing listings.'); res.status(result.code).send(result); return; } logger.debug(`List of listings (Page ${page}, Limit ${limit}). Count: ${result.length}.`); res.send(result); }; export const listListingsByPropertiesRouteHandler = async ( req, res, properties = '', filter = {}, masterFilter = {} ) => { const result = await listObjectsByProperties({ model: listingModel, properties, filter, masterFilter, populate: LISTING_POPULATE, }); if (result?.error) { logger.error('Error listing listings.'); res.status(result.code).send(result); return; } logger.debug(`List of listings. Count: ${result.length}`); res.send(result); }; export const getListingPropertyValuesRouteHandler = async ( req, res, property, filter, masterFilter ) => { const result = await getPropertyValues({ model: listingModel, property, filter: { ...filter, ...masterFilter }, }); res.send(result); }; export const searchListingsRouteHandler = async (req, res, search) => { const result = await searchObjects({ model: listingModel, search, }); res.send(result); }; export const getListingRouteHandler = async (req, res) => { const id = req.params.id; const result = await getObject({ model: listingModel, id, populate: LISTING_POPULATE, }); if (result?.error) { logger.warn(`Listing not found with supplied id.`); return res.status(result.code).send(result); } logger.debug(`Retrieved listing with ID: ${id}`); res.send(result); }; export const editListingRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Listing with ID: ${id}`); const updateData = { updatedAt: new Date(), product: req.body.product, vendor: req.body.vendor, stockLocation: req.body.stockLocation, marketplace: req.body.marketplace, title: req.body.title, description: req.body.description, url: req.body.url, condition: req.body.condition, courierServices: req.body.courierServices, fulfillmentPolicy: req.body.fulfillmentPolicy, paymentPolicy: req.body.paymentPolicy, returnPolicy: req.body.returnPolicy, listingImages: Array.isArray(req.body.listingImages) ? req.body.listingImages.map((item) => item?._id || item) : req.body.listingImages, }; const result = await editObject({ model: listingModel, id, updateData, user: req.user, populate: LISTING_POPULATE, }); if (result.error) { logger.error('Error editing listing:', result.error); res.status(result.code).send(result); return; } const checkStatesResult = await checkStates({ model: listingModel, id, states: ['draft'] }); if (checkStatesResult.error) { logger.error('Error checking listing states:', checkStatesResult.error); res.status(checkStatesResult.code).send(checkStatesResult); return; } if (checkStatesResult == false) { const marketplaceId = result.marketplace?._id || result.marketplace; if (marketplaceId) { pushToMarketplace(marketplaceId, { _id: id }, req.user, { isNew: false }); } } logger.debug(`Edited listing with ID: ${id}`); res.send(result); }; export const newListingRouteHandler = async (req, res) => { const newData = { updatedAt: new Date(), product: req.body.product, vendor: req.body.vendor, stockLocation: req.body.stockLocation, marketplace: req.body.marketplace, title: req.body.title, description: req.body.description, state: req.body.state || { type: 'draft' }, url: req.body.url, condition: req.body.condition, courierServices: req.body.courierServices, fulfillmentPolicy: req.body.fulfillmentPolicy, paymentPolicy: req.body.paymentPolicy, returnPolicy: req.body.returnPolicy, listingImages: Array.isArray(req.body.listingImages) ? req.body.listingImages.map((item) => item?._id || item) : req.body.listingImages, }; const result = await newObject({ model: listingModel, newData, user: req.user, }); if (result.error) { logger.error('No listing created:', result.error); return res.status(result.code).send(result); } logger.debug(`New listing with ID: ${result._id}`); res.send(result); }; export const deleteListingRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); logger.trace(`Listing with ID: ${id}`); const listing = await getObject({ model: listingModel, id }); if (listing?.error) { logger.warn('Listing not found for deletion.'); return res.status(listing.code).send(listing); } const varients = await listingVarientModel.find({ listing: id }); for (const varient of varients) { try { await deleteObject({ model: listingVarientModel, id: varient._id, user: req.user }); } catch (err) { logger.warn(`Failed to delete listing varient ${varient._id}: ${err.message}`); } } const result = await deleteObject({ model: listingModel, id, user: req.user, }); if (result.error) { logger.error('No listing deleted:', result.error); return res.status(result.code).send(result); } const delMarketplaceId = listing.marketplace?._id || listing.marketplace; if (delMarketplaceId) { pushToMarketplace(delMarketplaceId, listing, req.user, { isDelete: true }); } logger.debug(`Deleted listing with ID: ${result._id}`); res.send(result); }; export const getListingStatsRouteHandler = async (req, res) => { const result = await getModelStats({ model: listingModel }); if (result?.error) { logger.error('Error fetching listing stats:', result.error); return res.status(result.code).send(result); } logger.trace('Listing stats:', result); res.send(result); }; export const getListingHistoryRouteHandler = async (req, res) => { const from = req.query.from; const to = req.query.to; const result = await getModelHistory({ model: listingModel, from, to }); if (result?.error) { logger.error('Error fetching listing history:', result.error); return res.status(result.code).send(result); } logger.trace('Listing history:', result); res.send(result); }; async function setListingBusyState(listingId, stateType, user, varientIds = []) { const listingResult = await editObject({ model: listingModel, id: listingId, updateData: { updatedAt: new Date(), state: { type: stateType, progress: 0.05 }, }, user, recalculate: false, populate: LISTING_POPULATE, }); for (const varientId of varientIds) { await editObject({ model: listingVarientModel, id: varientId, updateData: { updatedAt: new Date(), state: { type: stateType }, }, user, recalculate: false, }).catch((err) => { logger.warn(`Failed to set listing varient ${stateType} state: ${err.message}`); }); } return listingResult; } export const publishListingRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); const stateOk = await checkStates({ model: listingModel, id, states: ['draft', 'inactive'], }); if (stateOk?.error) { logger.error('Error checking listing state:', stateOk.error); return res.status(stateOk.code).send(stateOk); } if (stateOk === false) { return res.status(400).send({ error: 'Listing must be in draft or inactive state to publish offers.', code: 400, }); } const busyCheck = await checkStates({ model: listingModel, id, states: MARKETPLACE_BUSY_STATES, }); if (busyCheck === true) { return res.status(400).send({ error: 'Listing is publishing, unpublishing, or syncing; wait for it to finish.', code: 400, }); } const listing = await listingModel .findById(id) .populate(LISTING_POPULATE) .lean(); if (!listing) { return res.status(404).send({ error: 'Listing not found.', code: 404 }); } if (!listing.stockLocation) { return res.status(400).send({ error: 'Listing must have a stock location before publishing.', code: 400, }); } const marketplace = listing.marketplace; if (!marketplace?._id) { return res.status(400).send({ error: 'Listing has no marketplace; cannot publish offers.', code: 400, }); } if (!marketplace.active) { return res.status(400).send({ error: 'Marketplace is not active.', code: 400 }); } if (!hasIntegration(marketplace.provider)) { return res.status(400).send({ error: 'No integration is configured for this marketplace.', code: 400, }); } const varients = await listingVarientModel.find({ listing: id }).lean(); const toPublish = varients.filter((v) => v._reference && v.state?.type !== 'active'); if (toPublish.length === 0) { return res.status(400).send({ error: 'No variants to publish (all are already active or missing SKU).', code: 400, }); } const restoreStateType = listing.state?.type || 'draft'; const updated = await setListingBusyState( id, 'publishing', req.user, toPublish.map((v) => v._id) ); if (updated?.error) { return res.status(updated.code).send(updated); } try { await publishExternalListing(marketplace, req.user, listing, { varientIds: toPublish.map((v) => v._id), restoreStateType, varientRestoreStateType: 'draft', }); } catch (err) { logger.error(`Failed to enqueue listing publish: ${err.message}`); return res.status(500).send({ error: err.message, code: 500 }); } logger.debug(`Publish listing started for ID: ${id}`); res.send(updated); }; export const unpublishListingRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); const busyCheck = await checkStates({ model: listingModel, id, states: MARKETPLACE_BUSY_STATES, }); if (busyCheck === true) { return res.status(400).send({ error: 'Listing is publishing, unpublishing, or syncing; wait for it to finish.', code: 400, }); } const listing = await listingModel.findById(id).populate('marketplace').lean(); if (!listing) { return res.status(404).send({ error: 'Listing not found.', code: 404 }); } const marketplace = listing.marketplace; if (!marketplace?._id) { return res.status(400).send({ error: 'Listing has no marketplace; cannot withdraw offers.', code: 400, }); } if (!marketplace.active) { return res.status(400).send({ error: 'Marketplace is not active.', code: 400 }); } if (!hasIntegration(marketplace.provider)) { return res.status(400).send({ error: 'No integration is configured for this marketplace.', code: 400, }); } const varients = await listingVarientModel.find({ listing: id }).lean(); const toUnpublish = varients.filter((v) => v._reference && v.state?.type === 'active'); if (toUnpublish.length === 0) { return res.status(400).send({ error: 'No active variants to unpublish.', code: 400, }); } const restoreStateType = listing.state?.type || 'active'; const updated = await setListingBusyState( id, 'unpublishing', req.user, toUnpublish.map((v) => v._id) ); if (updated?.error) { return res.status(updated.code).send(updated); } try { await unpublishExternalListing(marketplace, req.user, listing, { varientIds: toUnpublish.map((v) => v._id), restoreStateType, varientRestoreStateType: 'active', }); } catch (err) { logger.error(`Failed to enqueue listing unpublish: ${err.message}`); return res.status(500).send({ error: err.message, code: 500 }); } logger.debug(`Unpublish listing started for ID: ${id}`); res.send(updated); }; export const getListingNeighborsRouteHandler = async ( req, res, property = '', filter = {}, search = '', sort = '', order = 'ascend', id ) => { if (!id) { return res.status(400).send({ error: 'Missing id parameter', code: 400 }); } const result = await getObjectNeighbors({ model: listingModel, id, filter, search, sort, order, }); if (result?.error) { logger.error('Error fetching listing neighbors.'); return res.status(result.code).send(result); } logger.debug(`Retrieved listing neighbors for ID: ${id}`); res.send(result); };