Implement schema synchronization and enhance invoice handling
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good

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.
This commit is contained in:
Tom Butcher 2026-09-06 22:58:06 +01:00
parent 9defa3e30b
commit cb63d8dabb
5 changed files with 129 additions and 55 deletions

View File

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

View File

@ -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) {

View File

@ -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) => {

View File

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

View File

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