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, } from '../../database/database.js'; import { hasIntegration, createListing as createExternalListing, updateListing as updateExternalListing, deleteListing as deleteExternalListing, publishMarketplaceOfferForSku, withdrawMarketplaceOfferForSku, } from '../../integrations/marketplaceworker.js'; const logger = log4js.getLogger('Listings'); logger.level = config.server.logLevel; 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) { deleteExternalListing(marketplace, user, listingData); } else if (isNew) { createExternalListing(marketplace, user, listingData); } else { 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: ['product', 'vendor', 'stockLocation', 'marketplace'], }); 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 = {} ) => { const result = await listObjectsByProperties({ model: listingModel, properties, filter, populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); 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 getListingRouteHandler = async (req, res) => { const id = req.params.id; const result = await getObject({ model: listingModel, id, populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); 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, url: req.body.url, }; const result = await editObject({ model: listingModel, id, updateData, user: req.user, populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); if (result.error) { logger.error('Error editing listing:', result.error); res.status(result.code).send(result); return; } 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, state: req.body.state || { type: 'draft' }, url: req.body.url, }; 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); } try { await newObject({ model: listingVarientModel, newData: { listing: result._id, state: { type: 'draft' }, product: req.body.product || undefined, }, user: req.user, }); logger.debug(`Created default listing varient for listing ${result._id}`); } catch (err) { logger.warn(`Failed to create default listing varient: ${err.message}`); } const newMarketplaceId = result.marketplace?._id || result.marketplace; if (newMarketplaceId) { pushToMarketplace(newMarketplaceId, { _id: result._id }, req.user, { isNew: true }); } 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); }; 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 syncingCheck = await checkStates({ model: listingModel, id, states: ['syncing'], }); if (syncingCheck === true) { return res.status(400).send({ error: 'Listing is syncing; wait for sync to finish before publishing.', code: 400, }); } const listing = await listingModel .findById(id) .populate(['marketplace', 'vendor', 'stockLocation']) .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, }); } try { let firstListingId; for (const v of toPublish) { const apiResult = await publishMarketplaceOfferForSku( marketplace, req.user, v._reference, listing ); await editObject({ model: listingVarientModel, id: v._id, updateData: { updatedAt: new Date(), state: { type: 'active' }, lastSyncedAt: new Date(), }, user: req.user, }); if (apiResult?.listingId && !firstListingId) { firstListingId = apiResult.listingId; } } const listingUpdate = { updatedAt: new Date(), state: { type: 'active' }, lastSyncedAt: new Date(), }; if (firstListingId) { listingUpdate.url = `https://www.ebay.com/itm/${firstListingId}`; } await editObject({ model: listingModel, id, updateData: listingUpdate, user: req.user, populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); const updated = await getObject({ model: listingModel, id, populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); res.send(updated); } catch (err) { logger.error(`Publish listing failed: ${err.message}`); res.status(500).send({ error: err.message, code: 500 }); } }; export const unpublishListingRouteHandler = async (req, res) => { const id = new mongoose.Types.ObjectId(req.params.id); const syncingCheck = await checkStates({ model: listingModel, id, states: ['syncing'], }); if (syncingCheck === true) { return res.status(400).send({ error: 'Listing is syncing; wait for sync to finish before unpublishing.', 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, }); } try { for (const v of toUnpublish) { await withdrawMarketplaceOfferForSku(marketplace, req.user, v._reference); await editObject({ model: listingVarientModel, id: v._id, updateData: { updatedAt: new Date(), state: { type: 'draft' }, lastSyncedAt: new Date(), }, user: req.user, }); } await editObject({ model: listingModel, id, updateData: { updatedAt: new Date(), state: { type: 'draft' }, lastSyncedAt: new Date(), }, user: req.user, populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); const updated = await getObject({ model: listingModel, id, populate: ['product', 'vendor', 'stockLocation', 'marketplace'], }); res.send(updated); } catch (err) { logger.error(`Unpublish listing failed: ${err.message}`); res.status(500).send({ error: err.message, code: 500 }); } };