farmcontrol-api/src/services/sales/listingvarients.js
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

436 lines
12 KiB
JavaScript

import config from '../../config.js';
import { listingVarientModel } from '../../database/schemas/sales/listingvarient.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 { listingModel } from '../../database/schemas/sales/listing.schema.js';
import {
hasIntegration,
publishMarketplaceOfferForSku,
withdrawMarketplaceOfferForSku,
} from '../../integrations/marketplaceworker.js';
const logger = log4js.getLogger('ListingVarients');
logger.level = config.server.logLevel;
const POPULATE_FIELDS = ['listing', 'product', 'productSku', 'priceTaxRate'];
export const listListingVarientsRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: listingVarientModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: POPULATE_FIELDS,
});
if (result?.error) {
logger.error('Error listing listing varients.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of listing varients (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
res.send(result);
};
export const listListingVarientsByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {}
) => {
const result = await listObjectsByProperties({
model: listingVarientModel,
properties,
filter,
populate: POPULATE_FIELDS,
});
if (result?.error) {
logger.error('Error listing listing varients.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of listing varients. Count: ${result.length}`);
res.send(result);
};
export const getListingVarientRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: listingVarientModel,
id,
populate: POPULATE_FIELDS,
});
if (result?.error) {
logger.warn(`Listing varient not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retrieved listing varient with ID: ${id}`);
res.send(result);
};
export const editListingVarientRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Listing varient with ID: ${id}`);
const updateData = {
updatedAt: new Date(),
listing: req.body.listing,
product: req.body.product,
productSku: req.body.productSku,
price: req.body.price,
currency: req.body.currency,
priceTaxRate: req.body.priceTaxRate,
priceWithTax: req.body.priceWithTax,
};
const result = await editObject({
model: listingVarientModel,
id,
updateData,
user: req.user,
populate: POPULATE_FIELDS,
});
if (result.error) {
logger.error('Error editing listing varient:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Edited listing varient with ID: ${id}`);
res.send(result);
};
export const newListingVarientRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
listing: req.body.listing,
product: req.body.product,
productSku: req.body.productSku,
state: req.body.state || { type: 'draft' },
price: req.body.price,
currency: req.body.currency,
priceTaxRate: req.body.priceTaxRate,
priceWithTax: req.body.priceWithTax,
};
const result = await newObject({
model: listingVarientModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No listing varient created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New listing varient with ID: ${result._id}`);
res.send(result);
};
export const deleteListingVarientRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Listing varient with ID: ${id}`);
const result = await deleteObject({
model: listingVarientModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No listing varient deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted listing varient with ID: ${result._id}`);
res.send(result);
};
export const getListingVarientStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: listingVarientModel });
if (result?.error) {
logger.error('Error fetching listing varient stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Listing varient stats:', result);
res.send(result);
};
export const getListingVarientHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: listingVarientModel, from, to });
if (result?.error) {
logger.error('Error fetching listing varient history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Listing varient history:', result);
res.send(result);
};
const VARIENT_POPULATE_MARKETPLACE = [
{ path: 'listing', populate: { path: 'marketplace' } },
'product',
'productSku',
'priceTaxRate',
];
export const publishListingVarientRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
const stateOk = await checkStates({
model: listingVarientModel,
id,
states: ['draft', 'inactive'],
});
if (stateOk?.error) {
logger.error('Error checking listing varient state:', stateOk.error);
return res.status(stateOk.code).send(stateOk);
}
if (stateOk === false) {
return res.status(400).send({
error: 'Listing varient must be in draft or inactive state to publish.',
code: 400,
});
}
const syncingCheck = await checkStates({
model: listingVarientModel,
id,
states: ['syncing'],
});
if (syncingCheck === true) {
return res.status(400).send({
error: 'Listing varient is syncing; wait for sync to finish before publishing.',
code: 400,
});
}
const doc = await listingVarientModel
.findById(id)
.populate({
path: 'listing',
populate: [{ path: 'marketplace' }, { path: 'vendor' }, { path: 'stockLocation' }],
})
.lean();
if (!doc) {
return res.status(404).send({ error: 'Listing varient not found.', code: 404 });
}
if (!doc._reference) {
return res.status(400).send({
error: 'Listing varient must have a reference (SKU) before publishing.',
code: 400,
});
}
const marketplace = doc.listing?.marketplace;
const marketplaceId = marketplace?._id || marketplace;
if (!marketplaceId) {
return res.status(400).send({
error: 'Listing has no marketplace; cannot publish offer.',
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,
});
}
try {
if (!doc.listing?.stockLocation) {
return res.status(400).send({
error: 'Listing must have a stock location before publishing.',
code: 400,
});
}
const apiResult = await publishMarketplaceOfferForSku(
marketplace,
req.user,
doc._reference,
doc.listing
);
await editObject({
model: listingVarientModel,
id,
updateData: {
updatedAt: new Date(),
state: { type: 'active' },
lastSyncedAt: new Date(),
},
user: req.user,
});
const listingId = doc.listing?._id || doc.listing;
if (listingId) {
const listingUpdate = {
updatedAt: new Date(),
state: { type: 'active' },
lastSyncedAt: new Date(),
};
if (apiResult?.listingId) {
listingUpdate.url = `https://www.ebay.com/itm/${apiResult.listingId}`;
}
await editObject({
model: listingModel,
id: listingId,
updateData: listingUpdate,
user: req.user,
});
}
const updated = await getObject({
model: listingVarientModel,
id,
populate: VARIENT_POPULATE_MARKETPLACE,
});
res.send(updated);
} catch (err) {
logger.error(`Publish listing varient failed: ${err.message}`);
res.status(500).send({ error: err.message, code: 500 });
}
};
export const unpublishListingVarientRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
const stateOk = await checkStates({
model: listingVarientModel,
id,
states: ['active'],
});
if (stateOk?.error) {
logger.error('Error checking listing varient state:', stateOk.error);
return res.status(stateOk.code).send(stateOk);
}
if (stateOk === false) {
return res.status(400).send({
error: 'Listing varient must be in active state to unpublish.',
code: 400,
});
}
const syncingCheck = await checkStates({
model: listingVarientModel,
id,
states: ['syncing'],
});
if (syncingCheck === true) {
return res.status(400).send({
error: 'Listing varient is syncing; wait for sync to finish before unpublishing.',
code: 400,
});
}
const doc = await listingVarientModel
.findById(id)
.populate({ path: 'listing', populate: { path: 'marketplace' } })
.lean();
if (!doc) {
return res.status(404).send({ error: 'Listing varient not found.', code: 404 });
}
if (!doc._reference) {
return res.status(400).send({
error: 'Listing varient must have a reference (SKU) before unpublishing.',
code: 400,
});
}
const marketplace = doc.listing?.marketplace;
const marketplaceId = marketplace?._id || marketplace;
if (!marketplaceId) {
return res.status(400).send({
error: 'Listing has no marketplace; cannot withdraw offer.',
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,
});
}
try {
await withdrawMarketplaceOfferForSku(marketplace, req.user, doc._reference);
await editObject({
model: listingVarientModel,
id,
updateData: {
updatedAt: new Date(),
state: { type: 'draft' },
lastSyncedAt: new Date(),
},
user: req.user,
});
const parentListingId = doc.listing?._id || doc.listing;
if (parentListingId) {
const siblings = await listingVarientModel.find({ listing: parentListingId }).lean();
const anyActive = siblings.some(
(v) => String(v._id) !== String(id) && v.state?.type === 'active'
);
if (!anyActive) {
await editObject({
model: listingModel,
id: parentListingId,
updateData: {
updatedAt: new Date(),
state: { type: 'draft' },
lastSyncedAt: new Date(),
},
user: req.user,
});
}
}
const updated = await getObject({
model: listingVarientModel,
id,
populate: VARIENT_POPULATE_MARKETPLACE,
});
res.send(updated);
} catch (err) {
logger.error(`Unpublish listing varient failed: ${err.message}`);
res.status(500).send({ error: err.message, code: 500 });
}
};