diff --git a/src/database/schemas/models.js b/src/database/schemas/models.js index 1d7c3ed..0829ea4 100644 --- a/src/database/schemas/models.js +++ b/src/database/schemas/models.js @@ -38,6 +38,7 @@ import { shipmentModel } from './inventory/shipment.schema.js'; import { invoiceModel } from './finance/invoice.schema.js'; import { clientModel } from './sales/client.schema.js'; import { salesOrderModel } from './sales/salesorder.schema.js'; +import { marketplaceModel } from './sales/marketplace.schema.js'; // Map prefixes to models and id fields export const models = { @@ -315,4 +316,11 @@ export const models = { label: 'Sales Order', referenceField: '_reference', }, + MKT: { + model: marketplaceModel, + idField: '_id', + type: 'marketplace', + label: 'Marketplace', + referenceField: '_reference', + }, }; diff --git a/src/database/schemas/sales/client.schema.js b/src/database/schemas/sales/client.schema.js index 711ab6f..005c820 100644 --- a/src/database/schemas/sales/client.schema.js +++ b/src/database/schemas/sales/client.schema.js @@ -15,6 +15,7 @@ const clientSchema = new mongoose.Schema( { _reference: { type: String, default: () => generateId()() }, name: { required: true, type: String }, + marketplace: { type: mongoose.Schema.Types.ObjectId, ref: 'marketplace', required: false }, email: { required: false, type: String }, phone: { required: false, type: String }, country: { required: false, type: String }, diff --git a/src/database/schemas/sales/marketplace.schema.js b/src/database/schemas/sales/marketplace.schema.js new file mode 100644 index 0000000..17c9ce5 --- /dev/null +++ b/src/database/schemas/sales/marketplace.schema.js @@ -0,0 +1,26 @@ +import mongoose from 'mongoose'; +import { generateId } from '../../utils.js'; + +const marketplaceSchema = new mongoose.Schema( + { + _reference: { type: String, default: () => generateId()() }, + name: { required: true, type: String }, + provider: { + type: String, + required: true, + enum: ['ebay', 'etsy', 'tiktokShop'], + }, + active: { required: true, type: Boolean, default: true }, + // Provider-specific API configuration (flexible for eBay, Etsy, TikTok Shop) + config: { type: mongoose.Schema.Types.Mixed, default: {} }, + }, + { timestamps: true } +); + +marketplaceSchema.virtual('id').get(function () { + return this._id; +}); + +marketplaceSchema.set('toJSON', { virtuals: true }); + +export const marketplaceModel = mongoose.model('marketplace', marketplaceSchema); diff --git a/src/database/schemas/sales/salesorder.schema.js b/src/database/schemas/sales/salesorder.schema.js index 20f18c8..6a2ade7 100644 --- a/src/database/schemas/sales/salesorder.schema.js +++ b/src/database/schemas/sales/salesorder.schema.js @@ -18,6 +18,7 @@ const salesOrderSchema = new Schema( totalTaxAmount: { type: Number, required: true, default: 0 }, timestamp: { type: Date, default: Date.now }, client: { type: Schema.Types.ObjectId, ref: 'client', required: true }, + marketplace: { type: Schema.Types.ObjectId, ref: 'marketplace', required: false }, state: { type: { type: String, required: true, default: 'draft' }, }, diff --git a/src/index.js b/src/index.js index 2b5b537..f6f0c96 100644 --- a/src/index.js +++ b/src/index.js @@ -46,6 +46,7 @@ import { paymentRoutes, clientRoutes, salesOrderRoutes, + marketplaceRoutes, userNotifierRoutes, notificationRoutes, odataRoutes, @@ -163,6 +164,7 @@ app.use('/invoices', invoiceRoutes); app.use('/payments', paymentRoutes); app.use('/clients', clientRoutes); app.use('/salesorders', salesOrderRoutes); +app.use('/marketplaces', marketplaceRoutes); app.use('/notes', noteRoutes); app.use('/usernotifiers', userNotifierRoutes); app.use('/notifications', notificationRoutes); diff --git a/src/routes/index.js b/src/routes/index.js index 1839922..c193483 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -38,6 +38,7 @@ import invoiceRoutes from './finance/invoices.js'; import paymentRoutes from './finance/payments.js'; import clientRoutes from './sales/clients.js'; import salesOrderRoutes from './sales/salesorders.js'; +import marketplaceRoutes from './sales/marketplaces.js'; import noteRoutes from './misc/notes.js'; import userNotifierRoutes from './misc/usernotifiers.js'; import notificationRoutes from './misc/notifications.js'; @@ -87,6 +88,7 @@ export { paymentRoutes, clientRoutes, salesOrderRoutes, + marketplaceRoutes, userNotifierRoutes, notificationRoutes, odataRoutes, diff --git a/src/routes/sales/marketplaces.js b/src/routes/sales/marketplaces.js new file mode 100644 index 0000000..0faec3d --- /dev/null +++ b/src/routes/sales/marketplaces.js @@ -0,0 +1,58 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { getFilter, convertPropertiesString } from '../../utils.js'; + +const router = express.Router(); +import { + listMarketplacesRouteHandler, + getMarketplaceRouteHandler, + editMarketplaceRouteHandler, + newMarketplaceRouteHandler, + deleteMarketplaceRouteHandler, + listMarketplacesByPropertiesRouteHandler, + getMarketplaceStatsRouteHandler, + getMarketplaceHistoryRouteHandler, +} from '../../services/sales/marketplaces.js'; + +// list of marketplaces +router.get('/', isAuthenticated, (req, res) => { + const { page, limit, property, search, sort, order } = req.query; + const allowedFilters = ['name', 'provider', 'active', 'createdAt', 'updatedAt']; + const filter = getFilter(req.query, allowedFilters); + listMarketplacesRouteHandler(req, res, page, limit, property, filter, search, sort, order); +}); + +router.get('/properties', isAuthenticated, (req, res) => { + let properties = convertPropertiesString(req.query.properties); + const allowedFilters = ['name', 'provider', 'active', 'createdAt', 'updatedAt']; + const filter = getFilter(req.query, allowedFilters, false); + listMarketplacesByPropertiesRouteHandler(req, res, properties, filter); +}); + +router.post('/', isAuthenticated, (req, res) => { + newMarketplaceRouteHandler(req, res); +}); + +// get marketplace stats +router.get('/stats', isAuthenticated, (req, res) => { + getMarketplaceStatsRouteHandler(req, res); +}); + +// get marketplaces history +router.get('/history', isAuthenticated, (req, res) => { + getMarketplaceHistoryRouteHandler(req, res); +}); + +router.get('/:id', isAuthenticated, (req, res) => { + getMarketplaceRouteHandler(req, res); +}); + +router.put('/:id', isAuthenticated, async (req, res) => { + editMarketplaceRouteHandler(req, res); +}); + +router.delete('/:id', isAuthenticated, async (req, res) => { + deleteMarketplaceRouteHandler(req, res); +}); + +export default router; diff --git a/src/services/sales/clients.js b/src/services/sales/clients.js index 075f920..92fb626 100644 --- a/src/services/sales/clients.js +++ b/src/services/sales/clients.js @@ -98,6 +98,7 @@ export const editClientRouteHandler = async (req, res) => { address: req.body.address, active: req.body.active, tags: req.body.tags, + marketplace: req.body.marketplace, }; // Create audit log before updating const result = await editObject({ @@ -128,6 +129,7 @@ export const newClientRouteHandler = async (req, res) => { address: req.body.address, active: req.body.active, tags: req.body.tags, + marketplace: req.body.marketplace, }; const result = await newObject({ model: clientModel, diff --git a/src/services/sales/marketplaces.js b/src/services/sales/marketplaces.js new file mode 100644 index 0000000..56bb6a2 --- /dev/null +++ b/src/services/sales/marketplaces.js @@ -0,0 +1,176 @@ +import config from '../../config.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, +} from '../../database/database.js'; +const logger = log4js.getLogger('Marketplaces'); +logger.level = config.server.logLevel; + +export const listMarketplacesRouteHandler = async ( + req, + res, + page = 1, + limit = 25, + property = '', + filter = {}, + search = '', + sort = '', + order = 'ascend' +) => { + const result = await listObjects({ + model: marketplaceModel, + page, + limit, + property, + filter, + search, + sort, + order, + }); + + if (result?.error) { + logger.error('Error listing marketplaces.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of marketplaces (Page ${page}, Limit ${limit}). Count: ${result.length}.`); + res.send(result); +}; + +export const listMarketplacesByPropertiesRouteHandler = async ( + req, + res, + properties = '', + filter = {} +) => { + const result = await listObjectsByProperties({ + model: marketplaceModel, + properties, + filter, + }); + + if (result?.error) { + logger.error('Error listing marketplaces.'); + res.status(result.code).send(result); + return; + } + + logger.debug(`List of marketplaces. Count: ${result.length}`); + res.send(result); +}; + +export const getMarketplaceRouteHandler = async (req, res) => { + const id = req.params.id; + const result = await getObject({ + model: marketplaceModel, + id, + }); + if (result?.error) { + logger.warn(`Marketplace not found with supplied id.`); + return res.status(result.code).send(result); + } + logger.debug(`Retrieved marketplace with ID: ${id}`); + res.send(result); +}; + +export const editMarketplaceRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Marketplace with ID: ${id}`); + + const updateData = { + updatedAt: new Date(), + name: req.body.name, + provider: req.body.provider, + active: req.body.active, + config: req.body.config || {}, + }; + const result = await editObject({ + model: marketplaceModel, + id, + updateData, + user: req.user, + }); + + if (result.error) { + logger.error('Error editing marketplace:', result.error); + res.status(result.code).send(result); + return; + } + + logger.debug(`Edited marketplace with ID: ${id}`); + res.send(result); +}; + +export const newMarketplaceRouteHandler = async (req, res) => { + const newData = { + updatedAt: new Date(), + name: req.body.name, + provider: req.body.provider, + active: req.body.active !== false, + config: req.body.config || {}, + }; + const result = await newObject({ + model: marketplaceModel, + newData, + user: req.user, + }); + if (result.error) { + logger.error('No marketplace created:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`New marketplace with ID: ${result._id}`); + res.send(result); +}; + +export const deleteMarketplaceRouteHandler = async (req, res) => { + const id = new mongoose.Types.ObjectId(req.params.id); + + logger.trace(`Marketplace with ID: ${id}`); + + const result = await deleteObject({ + model: marketplaceModel, + id, + user: req.user, + }); + if (result.error) { + logger.error('No marketplace deleted:', result.error); + return res.status(result.code).send(result); + } + + logger.debug(`Deleted marketplace with ID: ${result._id}`); + res.send(result); +}; + +export const getMarketplaceStatsRouteHandler = async (req, res) => { + const result = await getModelStats({ model: marketplaceModel }); + if (result?.error) { + logger.error('Error fetching marketplace stats:', result.error); + return res.status(result.code).send(result); + } + logger.trace('Marketplace stats:', result); + res.send(result); +}; + +export const getMarketplaceHistoryRouteHandler = async (req, res) => { + const from = req.query.from; + const to = req.query.to; + const result = await getModelHistory({ model: marketplaceModel, from, to }); + if (result?.error) { + logger.error('Error fetching marketplace history:', result.error); + return res.status(result.code).send(result); + } + logger.trace('Marketplace history:', result); + res.send(result); +}; diff --git a/src/services/sales/salesorders.js b/src/services/sales/salesorders.js index dcc0a98..b17c37a 100644 --- a/src/services/sales/salesorders.js +++ b/src/services/sales/salesorders.js @@ -40,7 +40,7 @@ export const listSalesOrdersRouteHandler = async ( search, sort, order, - populate: ['client'], + populate: ['client', 'marketplace'], }); if (result?.error) { @@ -64,7 +64,7 @@ export const listSalesOrdersByPropertiesRouteHandler = async ( model: salesOrderModel, properties, filter, - populate: ['client'], + populate: ['client', 'marketplace'], masterFilter, }); @@ -83,7 +83,7 @@ export const getSalesOrderRouteHandler = async (req, res) => { const result = await getObject({ model: salesOrderModel, id, - populate: ['client'], + populate: ['client', 'marketplace'], }); if (result?.error) { logger.warn(`Sales Order not found with supplied id.`); @@ -116,6 +116,7 @@ export const editSalesOrderRouteHandler = async (req, res) => { const updateData = { updatedAt: new Date(), client: req.body.client, + marketplace: req.body.marketplace, }; // Create audit log before updating const result = await editObject({ @@ -140,6 +141,7 @@ export const editMultipleSalesOrdersRouteHandler = async (req, res) => { const updates = req.body.map((update) => ({ _id: update._id, client: update.client, + marketplace: update.marketplace, })); if (!Array.isArray(updates)) { @@ -167,6 +169,7 @@ export const newSalesOrderRouteHandler = async (req, res) => { const newData = { updatedAt: new Date(), client: req.body.client, + marketplace: req.body.marketplace, totalAmount: 0, totalAmountWithTax: 0, totalTaxAmount: 0,