Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
475 lines
13 KiB
JavaScript
475 lines
13 KiB
JavaScript
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 });
|
|
}
|
|
};
|