Tom Butcher 57f057e3aa
Some checks failed
farmcontrol/farmcontrol-api/pipeline/head There was a failure building this commit
Implemented stock locations.
2026-05-17 16:55:01 +01:00

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 });
}
};