From cb63d8dabbc91dbc5380239622ac91e050f9abde Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 6 Sep 2026 22:58:06 +0100 Subject: [PATCH] Implement schema synchronization and enhance invoice handling This commit introduces a new function, `syncModelsWithScheduler`, to synchronize database schemas with the farm control scheduler. It also adds scheduled properties and event handling for invoices, allowing for automatic state updates based on due dates. Additionally, minor refactoring is performed in the sales listings routes and services to improve code readability and maintainability. The changes enhance the application's data synchronization and invoice management capabilities. --- fcdev.js | 22 +++- .../schemas/finance/invoice.schema.js | 34 ++++- src/routes/sales/listings.js | 116 +++++++++++------- src/services/finance/invoices.js | 3 +- src/services/sales/listings.js | 9 +- 5 files changed, 129 insertions(+), 55 deletions(-) diff --git a/fcdev.js b/fcdev.js index bddf84b..f74408f 100644 --- a/fcdev.js +++ b/fcdev.js @@ -25,6 +25,25 @@ async function syncModelsWithWS() { } } +async function syncModelsWithScheduler() { + const sourceDir = path.resolve(__dirname, 'src/database/schemas'); + const targetDir = path.resolve(__dirname, '../farmcontrol-scheduler/src/database/schemas'); + + console.log(`Syncing schemas from ${sourceDir} to ${targetDir}...`); + + const stats = { copied: 0, added: 0, skipped: 0 }; + + try { + await syncDirectory(sourceDir, targetDir, sourceDir, stats); + console.log( + `✅ Schema sync completed successfully! (${stats.added} added, ${stats.copied} updated, ${stats.skipped} unchanged)` + ); + } catch (error) { + console.error('❌ Error syncing schemas:', error); + process.exit(1); + } +} + function fileHasChanges(oldContent, newContent) { return diffLines(oldContent, newContent).some((part) => part.added || part.removed); } @@ -77,5 +96,6 @@ async function syncDirectory(source, target, rootSource, stats) { // Run the sync function when executed directly syncModelsWithWS(); +syncModelsWithScheduler(); -export { syncModelsWithWS }; +export { syncModelsWithWS, syncModelsWithScheduler }; diff --git a/src/database/schemas/finance/invoice.schema.js b/src/database/schemas/finance/invoice.schema.js index 954d09a..c09bbb7 100644 --- a/src/database/schemas/finance/invoice.schema.js +++ b/src/database/schemas/finance/invoice.schema.js @@ -1,7 +1,12 @@ import mongoose from 'mongoose'; import { generateId } from '../../utils.js'; const { Schema } = mongoose; -import { aggregateRollups, aggregateRollupsHistory, editObject, getObject } from '../../database.js'; +import { + aggregateRollups, + aggregateRollupsHistory, + editObject, + getObject, +} from '../../database.js'; import { taxRateModel } from '../management/taxrate.schema.js'; import { amountWithTax, resolveTaxRate } from '../../tax.js'; @@ -136,6 +141,33 @@ invoiceSchema.statics.history = async function (from, to) { return results; }; +invoiceSchema.statics.scheduledProperties = [ + { + name: 'dueAt', + filter: { state: 'due|sent|acknowledged' }, + type: 'dateTime', + }, +]; + +invoiceSchema.statics.onSchedulerEvent = async function (invoice, property) { + const invoiceId = invoice._id || invoice; + if (!invoiceId) { + return; + } + + if (property === 'dueAt' && invoice.dueAt && invoice.dueAt < new Date()) { + await editObject({ + model: this, + id: invoiceId, + updateData: { + state: { type: 'overdue' }, + }, + user: 'system', + recalculate: false, + }); + } +}; + invoiceSchema.statics.recalculate = async function (invoice, user) { const invoiceId = invoice._id || invoice; if (!invoiceId) { diff --git a/src/routes/sales/listings.js b/src/routes/sales/listings.js index 766edd7..d0a67cf 100644 --- a/src/routes/sales/listings.js +++ b/src/routes/sales/listings.js @@ -38,21 +38,21 @@ const listAllowedSorters = [ '_id', ]; const propertiesAllowedFilters = [ - 'product', - 'vendor', - 'stockLocation', - 'stockQuantity', - 'marketplace', - 'courierServices', - 'fulfillmentPolicy', - 'paymentPolicy', - 'returnPolicy', - 'state', - 'state.type', - 'condition', - 'createdAt', - 'updatedAt', - ]; + 'product', + 'vendor', + 'stockLocation', + 'stockQuantity', + 'marketplace', + 'courierServices', + 'fulfillmentPolicy', + 'paymentPolicy', + 'returnPolicy', + 'state', + 'state.type', + 'condition', + 'createdAt', + 'updatedAt', +]; import { listListingsRouteHandler, getListingRouteHandler, @@ -66,44 +66,49 @@ import { unpublishListingRouteHandler, searchListingsRouteHandler, getListingPropertyValuesRouteHandler, - - getListingNeighborsRouteHandler + getListingNeighborsRouteHandler, } from '../../services/sales/listings.js'; router.get('/', isAuthenticated, checkPermissions('listing', 'list'), async (req, res) => { const { page, limit, property, search, sortProperty, sortOrder } = req.query; - const filter = await getFilter(req.query, listAllowedFilters); - listListingsRouteHandler(req, res, page, limit, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder); -}); - -router.get('/properties', checkPermissions('listing', 'list'), isAuthenticated, async (req, res) => { - let properties = convertPropertiesString(req.query.properties); - const filter = await getFilter(req.query, propertiesAllowedFilters, false); - var masterFilter = {}; - if (req.query.masterFilter) { - masterFilter = JSON.parse(req.query.masterFilter); - } - listListingsByPropertiesRouteHandler(req, res, properties, filter, masterFilter); + const filter = await getFilter(req.query, listAllowedFilters); + listListingsRouteHandler( + req, + res, + page, + limit, + property, + filter, + search, + getSort(sortProperty, listAllowedSorters), + sortOrder + ); }); router.get( - '/values', - checkPermissions('listing', 'list'), + '/properties', isAuthenticated, + checkPermissions('listing', 'list'), async (req, res) => { - const { property } = req.query; - const filter = await getFilter(req.query, listAllowedFilters, true); + let properties = convertPropertiesString(req.query.properties); + const filter = await getFilter(req.query, propertiesAllowedFilters, false); var masterFilter = {}; if (req.query.masterFilter) { - masterFilter = await getFilter( - JSON.parse(req.query.masterFilter), - listAllowedFilters, - true - ); + masterFilter = JSON.parse(req.query.masterFilter); } - getListingPropertyValuesRouteHandler(req, res, property, filter, masterFilter); + listListingsByPropertiesRouteHandler(req, res, properties, filter, masterFilter); } ); + +router.get('/values', checkPermissions('listing', 'list'), isAuthenticated, async (req, res) => { + const { property } = req.query; + const filter = await getFilter(req.query, listAllowedFilters, true); + var masterFilter = {}; + if (req.query.masterFilter) { + masterFilter = await getFilter(JSON.parse(req.query.masterFilter), listAllowedFilters, true); + } + getListingPropertyValuesRouteHandler(req, res, property, filter, masterFilter); +}); router.get('/search', checkPermissions('listing', 'list'), isAuthenticated, async (req, res) => { const { search } = req.query; searchListingsRouteHandler(req, res, search); @@ -121,18 +126,37 @@ router.get('/history', isAuthenticated, async (req, res) => { getListingHistoryRouteHandler(req, res); }); -router.post('/:id/publish', isAuthenticated, checkPermissions('listing', 'publish'), async (req, res) => { - publishListingRouteHandler(req, res); -}); +router.post( + '/:id/publish', + isAuthenticated, + checkPermissions('listing', 'publish'), + async (req, res) => { + publishListingRouteHandler(req, res); + } +); -router.post('/:id/unpublish', isAuthenticated, checkPermissions('listing', 'unpublish'), async (req, res) => { - unpublishListingRouteHandler(req, res); -}); +router.post( + '/:id/unpublish', + isAuthenticated, + checkPermissions('listing', 'unpublish'), + async (req, res) => { + unpublishListingRouteHandler(req, res); + } +); router.get('/neighbors', isAuthenticated, async (req, res) => { const { property, search, sortProperty, sortOrder, id } = req.query; const filter = await getFilter(req.query, listAllowedFilters); - getListingNeighborsRouteHandler(req, res, property, filter, search, getSort(sortProperty, listAllowedSorters), sortOrder, id); + getListingNeighborsRouteHandler( + req, + res, + property, + filter, + search, + getSort(sortProperty, listAllowedSorters), + sortOrder, + id + ); }); router.get('/:id', isAuthenticated, checkPermissions('listing', 'info'), async (req, res) => { diff --git a/src/services/finance/invoices.js b/src/services/finance/invoices.js index 2a650f6..0ba7f40 100644 --- a/src/services/finance/invoices.js +++ b/src/services/finance/invoices.js @@ -159,7 +159,7 @@ export const editInvoiceRouteHandler = async (req, res) => { client: req.body.client, invoiceType: req.body.invoiceType, invoiceDate: req.body.invoiceDate, - dueAt: req.body.dueDate, + dueAt: req.body.dueAt, issuedAt: req.body.issuedAt, orderType: req.body.orderType, order: req.body.order, @@ -613,4 +613,3 @@ export const getInvoiceNeighborsRouteHandler = async ( logger.debug(`Retrieved invoice neighbors for ID: ${id}`); res.send(result); }; - diff --git a/src/services/sales/listings.js b/src/services/sales/listings.js index 1418765..1079c0c 100644 --- a/src/services/sales/listings.js +++ b/src/services/sales/listings.js @@ -58,6 +58,8 @@ const LISTING_POPULATE = [ }, ]; +const LISTING_PROPERTIES_POPULATE = ['marketplace', 'vendor', 'stockLocation', 'product']; + function pushToMarketplace( marketplaceId, listingData, @@ -131,7 +133,7 @@ export const listListingsByPropertiesRouteHandler = async ( properties, filter, masterFilter, - populate: LISTING_POPULATE, + populate: LISTING_PROPERTIES_POPULATE, }); if (result?.error) { @@ -393,10 +395,7 @@ export const publishListingRouteHandler = async (req, res) => { }); } - const listing = await listingModel - .findById(id) - .populate(LISTING_POPULATE) - .lean(); + const listing = await listingModel.findById(id).populate(LISTING_POPULATE).lean(); if (!listing) { return res.status(404).send({ error: 'Listing not found.', code: 404 }); }