Compare commits

..

No commits in common. "main" and "config-updates" have entirely different histories.

102 changed files with 889252 additions and 17477 deletions

View File

@ -1,2 +1,9 @@
SESSION_SECRET='SECRET' DB_LINK="mongo-link-to-connect"
KEYCLOAK_CLIENT_SECRET='SECRET'
JWT_SECRET="token"
APP_URL_CLIENT=https://material-dashboard-react-node.creative-tim.com
APP_URL_API=https://node-json-api-free.creative-tim.com/login
MAILTRAP_USER=
MAILTRAP_PASSWORD=

91
Jenkinsfile vendored
View File

@ -1,91 +0,0 @@
pipeline {
agent {
label 'ubuntu'
}
environment {
NODE_ENV = 'production'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Setup Node.js') {
steps {
nodejs(nodeJSInstallationName: 'Node23') {
sh 'node -v'
sh 'pnpm -v'
}
}
}
stage('Install Dependencies') {
steps {
nodejs(nodeJSInstallationName: 'Node23') {
sh 'pnpm install --frozen-lockfile --production=false'
}
}
}
stage('Run Tests') {
steps {
nodejs(nodeJSInstallationName: 'Node23') {
sh '''
export NODE_ENV=test
export SESSION_SECRET=test-session-secret-for-testing-only
pnpm test
'''
}
}
post {
always {
junit 'test-results.xml'
}
}
}
stage('Deploy via SSH') {
steps {
sshPublisher(publishers: [
sshPublisherDesc(
configName: 'farmcontrol.tombutcher.local',
transfers: [
sshTransfer(
cleanRemote: false,
excludes: 'node_modules/**',
execCommand: '''
cd /home/farmcontrol/farmcontrol-api
pnpm install --production
sudo systemctl restart farmcontrol-api
''',
execTimeout: 120000,
flatten: false,
makeEmptyDirs: false,
noDefaultExcludes: false,
patternSeparator: '[, ]+',
remoteDirectory: 'farmcontrol-api',
remoteDirectorySDF: false,
removePrefix: '',
sourceFiles: '**/*'
)
],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: true
)
])
}
}
}
post {
always {
cleanWs()
}
}
}

View File

@ -1,7 +1,5 @@
# FarmControl API # FarmControl API
[![Build Status](https://ci.tombutcher.work/buildStatus/icon?job=farmcontrol%2Ffarmcontrol-api%2Fmain&style=flat-square)](https://ci.tombutcher.work/job/farmcontrol/job/farmcontrol-api/job/main/)
A comprehensive REST API for managing 3D printing farms, inventory, and production workflows. Built with Node.js, Express, and MongoDB, featuring authentication with Keycloak and comprehensive logging. A comprehensive REST API for managing 3D printing farms, inventory, and production workflows. Built with Node.js, Express, and MongoDB, featuring authentication with Keycloak and comprehensive logging.
## 🚀 Features ## 🚀 Features

View File

@ -1,4 +0,0 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
plugins: ['transform-import-meta'],
};

View File

@ -9,14 +9,14 @@
"keycloak": { "keycloak": {
"url": "https://auth.tombutcher.work", "url": "https://auth.tombutcher.work",
"realm": "master", "realm": "master",
"clientId": "farmcontrol-dev" "clientId": "farmcontrol-client"
}, },
"requiredRoles": [] "requiredRoles": []
}, },
"app": { "app": {
"urlClient": "https://dev.tombutcher.work", "urlClient": "http://localhost:3000",
"urlElectronClient": "http://localhost:5780", "urlElectronClient": "http://localhost:3000",
"urlApi": "https://dev.tombutcher.work/api", "urlApi": "http://localhost:8787",
"devAuthClient": "http://localhost:3500" "devAuthClient": "http://localhost:3500"
}, },
"database": { "database": {
@ -48,55 +48,6 @@
}, },
"otpExpiryMins": 0.5 "otpExpiryMins": 0.5
}, },
"test": {
"server": {
"port": 8788,
"logLevel": "error"
},
"auth": {
"enabled": false,
"sessionSecret": "test-session-secret-for-testing-only",
"keycloak": {
"url": "http://localhost:8080",
"realm": "test",
"clientId": "test-client"
},
"requiredRoles": []
},
"app": {
"urlClient": "http://localhost:3000",
"urlElectronClient": "http://localhost:5780",
"urlApi": "http://localhost:8788/api",
"devAuthClient": "http://localhost:3500"
},
"database": {
"mongo": {
"url": "mongodb://127.0.0.1:27017/farmcontrol-test",
"link": "127.0.0.1:27017"
},
"redis": {
"host": "localhost",
"port": 6379,
"password": "",
"cacheTtl": 30
},
"nats": {
"host": "localhost",
"port": 4222
}
},
"storage": {
"fileStorage": "./test-uploads",
"ceph": {
"accessKeyId": "minioadmin",
"secretAccessKey": "minioadmin123",
"endpoint": "http://127.0.0.1:9000",
"region": "us-east-1",
"filesBucket": "farmcontrol-test"
}
},
"otpExpiryMins": 0.5
},
"production": { "production": {
"server": { "server": {
"port": 8080, "port": 8080,
@ -112,9 +63,9 @@
"requiredRoles": [] "requiredRoles": []
}, },
"app": { "app": {
"urlClient": "https://web.farmcontrol.app", "urlClient": "http://localhost:3000",
"urlElectronClient": "http://localhost:3000", "urlElectronClient": "http://localhost:3000",
"urlApi": "https://api.farmcontrol.app", "urlApi": "http://localhost:8080",
"devAuthClient": "http://localhost:3500" "devAuthClient": "http://localhost:3500"
}, },
"database": { "database": {

View File

@ -1,14 +0,0 @@
export default {
apps: [
{
name: 'farmcontrol-api',
script: 'src/index.js',
env: {
NODE_ENV: 'development',
},
env_production: {
NODE_ENV: 'production',
},
},
],
};

BIN
images/admin.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
images/creator.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

BIN
images/member.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -1,22 +0,0 @@
module.exports = {
testEnvironment: 'node',
transform: {},
moduleFileExtensions: ['js', 'json', 'jsx', 'node'],
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
verbose: true,
reporters: [
'default',
[
'jest-junit',
{
outputDirectory: '.',
outputName: 'test-results.xml',
suiteName: 'farmcontrol-api-tests',
classNameTemplate: '{classname}',
titleTemplate: '{title}',
ancestorSeparator: ' ',
usePathForSuiteName: 'true',
},
],
],
};

12211
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,6 @@
"i": "^0.3.7", "i": "^0.3.7",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"keycloak-connect": "^26.1.1", "keycloak-connect": "^26.1.1",
"lodash": "^4.17.23",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"mongodb": "^6.21.0", "mongodb": "^6.21.0",
"mongoose": "^8.19.4", "mongoose": "^8.19.4",
@ -42,26 +41,20 @@
"@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/preset-env": "^7.28.5", "@babel/preset-env": "^7.28.5",
"@babel/register": "^7.28.3", "@babel/register": "^7.28.3",
"@jest/globals": "^30.2.0",
"babel-jest": "^30.2.0",
"babel-plugin-transform-import-meta": "^2.3.3",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"jest": "^30.2.0",
"jest-junit": "^16.0.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sequelize-cli": "^6.6.3", "sequelize-cli": "^6.6.3",
"standard": "^17.1.2", "standard": "^17.1.2"
"supertest": "^7.1.4"
}, },
"scripts": { "scripts": {
"syncModelsWithWS": "node fcdev.js", "syncModelsWithWS": "node fcdev.js",
"watch:schemas": "nodemon --config nodemon.schemas.json", "watch:schemas": "nodemon --config nodemon.schemas.json",
"dev": "concurrently --names \"API,SCHEMAS\" --prefix-colors \"cyan,yellow\" \"nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js\" \"nodemon --config nodemon.schemas.json\"", "dev": "concurrently --names \"API,SCHEMAS\" --prefix-colors \"cyan,yellow\" \"nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js\" \"nodemon --config nodemon.schemas.json\"",
"dev:api": "nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js", "dev:api": "nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js",
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js", "test": "echo \"Error: no test specified\" && exit 1",
"seed": "node src/mongo/seedData.js", "seed": "node src/mongo/seedData.js",
"clear": "node src/mongo/clearDbs.js" "clear": "node src/mongo/clearDbs.js"
}, },

10062
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,68 +0,0 @@
import { jest } from '@jest/globals';
import request from 'supertest';
// Mock Keycloak and Auth
jest.unstable_mockModule('../keycloak.js', () => ({
keycloak: {
middleware: () => (req, res, next) => next(),
protect: () => (req, res, next) => next(),
},
isAuthenticated: (req, res, next) => next(),
expressSession: (req, res, next) => next(),
}));
// Mock database connections and initializations in index.js
jest.unstable_mockModule('../database/mongo.js', () => ({
dbConnect: jest.fn(),
}));
jest.unstable_mockModule('../database/nats.js', () => ({
natsServer: { connect: jest.fn() },
}));
jest.unstable_mockModule('../database/ceph.js', () => ({
initializeBuckets: jest.fn(),
uploadFile: jest.fn(),
downloadFile: jest.fn(),
deleteFile: jest.fn(),
fileExists: jest.fn(),
listFiles: jest.fn(),
getFileMetadata: jest.fn(),
getPresignedUrl: jest.fn(),
BUCKETS: { FILES: 'test-bucket' },
}));
// Mock the service handlers to avoid database calls
jest.unstable_mockModule('../services/management/users.js', () => ({
listUsersRouteHandler: jest.fn((req, res) => res.send([{ id: '1', name: 'Mock User' }])),
listUsersByPropertiesRouteHandler: jest.fn(),
getUserRouteHandler: jest.fn((req, res) => res.send({ id: req.params.id, name: 'Mock User' })),
editUserRouteHandler: jest.fn(),
getUserStatsRouteHandler: jest.fn(),
getUserHistoryRouteHandler: jest.fn(),
}));
const { default: app } = await import('../index.js');
const { listUsersRouteHandler, getUserRouteHandler } = await import(
'../services/management/users.js'
);
describe('Users API Endpoints', () => {
describe('GET /users', () => {
it('should return a list of users', async () => {
const response = await request(app).get('/users');
expect(response.status).toBe(200);
expect(response.body).toEqual([{ id: '1', name: 'Mock User' }]);
expect(listUsersRouteHandler).toHaveBeenCalled();
});
});
describe('GET /users/:id', () => {
it('should return a single user', async () => {
const response = await request(app).get('/users/123');
expect(response.status).toBe(200);
expect(response.body).toEqual({ id: '123', name: 'Mock User' });
expect(getUserRouteHandler).toHaveBeenCalled();
});
});
});

View File

@ -2,10 +2,6 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// Configure paths relative to this file // Configure paths relative to this file
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -29,31 +25,7 @@ function loadConfig() {
throw new Error(`Configuration for environment '${NODE_ENV}' not found in config.json`); throw new Error(`Configuration for environment '${NODE_ENV}' not found in config.json`);
} }
const envConfig = config[NODE_ENV]; return config[NODE_ENV];
// Ensure auth config exists
if (!envConfig.auth) {
envConfig.auth = {};
}
if (!envConfig.auth.keycloak) {
envConfig.auth.keycloak = {};
}
// Override secrets with environment variables if available
if (process.env.KEYCLOAK_CLIENT_SECRET) {
envConfig.auth.keycloak.clientSecret = process.env.KEYCLOAK_CLIENT_SECRET;
}
// Session secret must be set - use env var or throw error
if (process.env.SESSION_SECRET) {
envConfig.auth.sessionSecret = process.env.SESSION_SECRET;
} else if (!envConfig.auth.sessionSecret) {
throw new Error(
'SESSION_SECRET environment variable is required. Please set SESSION_SECRET in your environment.'
);
}
return envConfig;
} catch (err) { } catch (err) {
console.error('Error loading config:', err); console.error('Error loading config:', err);
throw err; throw err;

View File

@ -1,163 +0,0 @@
import { jest } from '@jest/globals';
// Mock src/database/utils.js (where generateId lives)
jest.unstable_mockModule('../utils.js', () => ({
generateId: jest.fn(() => () => 'test-id'),
}));
// Mock src/utils.js (where most database.js helpers live)
jest.unstable_mockModule('../../utils.js', () => ({
deleteAuditLog: jest.fn(),
distributeDelete: jest.fn(),
expandObjectIds: jest.fn((obj) => obj),
modelHasRef: jest.fn(() => false),
getFieldsByRef: jest.fn(() => []),
getQueryToCacheKey: jest.fn(({ model, id }) => `${model}:${id}`),
editAuditLog: jest.fn(),
distributeUpdate: jest.fn(),
newAuditLog: jest.fn(),
distributeNew: jest.fn(),
distributeChildUpdate: jest.fn(),
distributeChildDelete: jest.fn(),
distributeChildNew: jest.fn(),
distributeStats: jest.fn(),
}));
jest.unstable_mockModule('../redis.js', () => ({
redisServer: {
getKey: jest.fn(),
setKey: jest.fn(),
deleteKey: jest.fn(),
getKeysByPattern: jest.fn(() => []), // Return empty array to avoid iterable error
},
}));
jest.unstable_mockModule('../../services/misc/model.js', () => ({
getAllModels: jest.fn(() => []),
}));
jest.unstable_mockModule('../schemas/management/auditlog.schema.js', () => ({
auditLogModel: {
find: jest.fn(),
create: jest.fn(),
},
}));
// Mock fileModel specifically as it's imported by database.js
jest.unstable_mockModule('../schemas/management/file.schema.js', () => ({
fileModel: {
findById: jest.fn(),
},
}));
// Now import the database utilities
const { listObjects, getObject, newObject, editObject, deleteObject } = await import(
'../database.js'
);
describe('Database Utilities (CRUD)', () => {
let mockModel;
beforeEach(() => {
mockModel = {
modelName: 'TestModel',
find: jest.fn().mockReturnThis(),
findById: jest.fn().mockReturnThis(),
findByIdAndUpdate: jest.fn().mockReturnThis(),
findByIdAndDelete: jest.fn().mockReturnThis(),
create: jest.fn(),
sort: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
populate: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
lean: jest.fn().mockReturnThis(),
exec: jest.fn(),
};
jest.clearAllMocks();
});
describe('listObjects', () => {
it('should return a list of objects', async () => {
const mockData = [{ _id: '1', name: 'Test' }];
mockModel.lean.mockResolvedValue(mockData);
const result = await listObjects({ model: mockModel });
expect(mockModel.find).toHaveBeenCalled();
expect(result).toEqual(mockData);
});
it('should handle pagination', async () => {
await listObjects({ model: mockModel, page: 2, limit: 10 });
expect(mockModel.skip).toHaveBeenCalledWith(10);
expect(mockModel.limit).toHaveBeenCalledWith(10);
});
});
describe('getObject', () => {
it('should return a single object by ID', async () => {
const mockData = { _id: '123', name: 'Test' };
mockModel.lean.mockResolvedValue(mockData);
const result = await getObject({ model: mockModel, id: '123' });
expect(mockModel.findById).toHaveBeenCalledWith('123');
expect(result).toEqual(mockData);
});
it('should return 404 if object not found', async () => {
mockModel.lean.mockResolvedValue(null);
const result = await getObject({ model: mockModel, id: '123' });
expect(result).toEqual({ error: 'Object not found.', code: 404 });
});
});
describe('newObject', () => {
it('should create a new object', async () => {
const newData = { name: 'New' };
const createdData = { _id: '456', ...newData };
mockModel.create.mockResolvedValue({
toObject: () => createdData,
_id: '456',
});
const result = await newObject({ model: mockModel, newData });
expect(mockModel.create).toHaveBeenCalledWith(newData);
expect(result).toEqual(createdData);
});
});
describe('editObject', () => {
it('should update an existing object', async () => {
const id = '123';
const updateData = { name: 'Updated' };
const previousData = { _id: id, name: 'Old' };
mockModel.lean.mockResolvedValue(previousData);
const result = await editObject({ model: mockModel, id, updateData });
expect(mockModel.findByIdAndUpdate).toHaveBeenCalledWith(id, updateData);
expect(result).toEqual({ ...previousData, ...updateData });
});
});
describe('deleteObject', () => {
it('should delete an object', async () => {
const id = '123';
const mockData = { _id: id, name: 'To be deleted' };
mockModel.findByIdAndDelete.mockResolvedValue({
toObject: () => mockData,
});
const result = await deleteObject({ model: mockModel, id });
expect(mockModel.findByIdAndDelete).toHaveBeenCalledWith(id);
expect(result).toEqual({ deleted: true, object: mockData });
});
});
});

View File

@ -1,10 +1,11 @@
import mongoose from 'mongoose'; import mongoose from "mongoose";
import { userModel } from '../schemas/user.schema.js'; import { userModel } from "../schemas/user.schema.js";
import { dbConnect } from '../mongo/index.js'; import { dbConnect } from "../mongo/index.js";
async function clear() { async function clear() {
dbConnect(); dbConnect();
await userModel.deleteMany({}); await userModel.deleteMany({});
console.log("DB cleared");
} }
clear().then(() => { clear().then(() => {

View File

@ -349,14 +349,12 @@ export const listObjects = async ({
filter = {}, filter = {},
sort = '', sort = '',
order = 'ascend', order = 'ascend',
pagination = true,
project, // optional: override default projection project, // optional: override default projection
}) => { }) => {
try { try {
logger.trace('Listing object:', { logger.trace('Listing object:', {
model, model,
populate, populate,
pagination,
page, page,
limit, limit,
filter, filter,
@ -365,7 +363,7 @@ export const listObjects = async ({
project, project,
}); });
// Calculate the skip value based on the page number and limit // Calculate the skip value based on the page number and limit
const skip = pagination ? (page - 1) * limit : 0; const skip = (page - 1) * limit;
// Fix: descend should be -1, ascend should be 1 // Fix: descend should be -1, ascend should be 1
const sortOrder = order === 'descend' ? -1 : 1; const sortOrder = order === 'descend' ? -1 : 1;
@ -373,6 +371,10 @@ export const listObjects = async ({
sort = 'createdAt'; sort = 'createdAt';
} }
if (filter) {
console.log('filter', filter);
}
// Translate any key ending with ._id to remove the ._id suffix for Mongoose // Translate any key ending with ._id to remove the ._id suffix for Mongoose
Object.keys(filter).forEach((key) => { Object.keys(filter).forEach((key) => {
if (key.endsWith('._id')) { if (key.endsWith('._id')) {
@ -387,7 +389,7 @@ export const listObjects = async ({
.find(filter) .find(filter)
.sort({ [sort]: sortOrder }) .sort({ [sort]: sortOrder })
.skip(skip) .skip(skip)
.limit(pagination ? Number(limit) : undefined); .limit(Number(limit));
// Handle populate (array or single value) // Handle populate (array or single value)
if (populate) { if (populate) {
@ -475,6 +477,8 @@ function nestGroups(groups, props, filter, idx = 0) {
// Check if any group in this key matches the filter (by _id or name) // Check if any group in this key matches the filter (by _id or name)
const matches = groupList.filter((group) => { const matches = groupList.filter((group) => {
const { filterVals } = getKeyAndFilterVals(group._id[prop]); const { filterVals } = getKeyAndFilterVals(group._id[prop]);
console.log('filterVals', filterVals);
console.log('filterValue', filterValue);
return filterVals.some((val) => val?.toString() === filterValue); return filterVals.some((val) => val?.toString() === filterValue);
}); });
@ -518,6 +522,7 @@ export const listObjectsByProperties = async ({
populate, populate,
}) => { }) => {
try { try {
console.log('Props', properties);
const propertiesPresent = !( const propertiesPresent = !(
!Array.isArray(properties) || !Array.isArray(properties) ||
properties.length === 0 || properties.length === 0 ||
@ -589,11 +594,12 @@ export const listObjectsByProperties = async ({
// Run aggregation // Run aggregation
const results = await model.aggregate(pipeline); const results = await model.aggregate(pipeline);
console.log('results', results);
return nestGroups(results, properties, filter); return nestGroups(results, properties, filter);
} else { } else {
// If no properties specified, just return all objects without grouping // If no properties specified, just return all objects without grouping
// Ensure pipeline is not empty by adding a $match stage if needed // Ensure pipeline is not empty by adding a $match stage if needed
if (pipeline.length === 0 && masterFilter == {}) { if (pipeline.length === 0) {
pipeline.push({ $match: {} }); pipeline.push({ $match: {} });
} }
const results = await model.aggregate(pipeline); const results = await model.aggregate(pipeline);
@ -725,25 +731,8 @@ export const listObjectDependencies = async ({ model, id }) => {
} }
}; };
export const checkStates = async ({ model, id, states }) => {
try {
const object = await getObject({ model, id, cached: true });
if (!object?.state?.type) {
logger.warn(`Object ${id} has no state type.`);
return false;
}
if (states.includes(object?.state?.type)) {
return true;
}
return false;
} catch (error) {
logger.error('checkStates error:', error);
return { error: error.message, code: 500 };
}
};
// Reusable function to edit an object by ID, with audit logging and distribution // Reusable function to edit an object by ID, with audit logging and distribution
export const editObject = async ({ model, id, updateData, user, populate, recalculate = true }) => { export const editObject = async ({ model, id, updateData, user, populate }) => {
try { try {
// Determine parentType from model name // Determine parentType from model name
const parentType = model.modelName ? model.modelName : 'unknown'; const parentType = model.modelName ? model.modelName : 'unknown';
@ -821,7 +810,7 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
populate, populate,
}); });
if (model.recalculate && recalculate == true) { if (model.recalculate) {
logger.debug(`Recalculating ${model.modelName}`); logger.debug(`Recalculating ${model.modelName}`);
await model.recalculate(updatedObject, user); await model.recalculate(updatedObject, user);
} }
@ -839,33 +828,6 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
} }
}; };
// Reusable function to edit multiple objects
export const editObjects = async ({ model, updates, user, populate, recalculate = true }) => {
try {
const results = [];
for (const update of updates) {
const id = update._id || update.id;
const updateData = { ...update };
delete updateData._id;
delete updateData.id;
const result = await editObject({
model,
id,
updateData,
user,
populate,
recalculate,
});
results.push(result);
}
return results;
} catch (error) {
logger.error('editObjects error:', error);
return { error: error.message, code: 500 };
}
};
// Reusable function to create a new object // Reusable function to create a new object
export const newObject = async ({ model, newData, user = null }, distributeChanges = true) => { export const newObject = async ({ model, newData, user = null }, distributeChanges = true) => {
try { try {

View File

@ -1,187 +0,0 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
const invoiceOrderItemSchema = new Schema(
{
orderItem: { type: Schema.Types.ObjectId, ref: 'orderItem', required: true },
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
invoiceAmountWithTax: { type: Number, required: true, default: 0 },
invoiceAmount: { type: Number, required: true, default: 0 },
invoiceQuantity: { type: Number, required: true, default: 0 },
},
{ timestamps: true }
);
const invoiceShipmentSchema = new Schema(
{
shipment: { type: Schema.Types.ObjectId, ref: 'shipment', required: true },
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
invoiceAmountWithTax: { type: Number, required: true, default: 0 },
invoiceAmount: { type: Number, required: true, default: 0 },
},
{ timestamps: true }
);
const invoiceSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
totalAmount: { type: Number, required: true, default: 0 },
totalAmountWithTax: { type: Number, required: true, default: 0 },
shippingAmount: { type: Number, required: true, default: 0 },
shippingAmountWithTax: { type: Number, required: true, default: 0 },
grandTotalAmount: { type: Number, required: true, default: 0 },
totalTaxAmount: { type: Number, required: true, default: 0 },
from: { type: Schema.Types.ObjectId, ref: 'vendor', required: false },
to: { type: Schema.Types.ObjectId, ref: 'client', required: false },
state: {
type: { type: String, required: true, default: 'draft' },
},
orderType: { type: String, required: true },
order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true },
issuedAt: { type: Date, required: false },
dueAt: { type: Date, required: false },
postedAt: { type: Date, required: false },
acknowledgedAt: { type: Date, required: false },
paidAt: { type: Date, required: false },
cancelledAt: { type: Date, required: false },
invoiceOrderItems: [invoiceOrderItemSchema],
invoiceShipments: [invoiceShipmentSchema],
},
{ timestamps: true }
);
const rollupConfigs = [
{
name: 'draft',
filter: { 'state.type': 'draft' },
rollups: [
{ name: 'draftCount', property: 'state.type', operation: 'count' },
{ name: 'draftGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
],
},
{
name: 'sent',
filter: { 'state.type': 'sent' },
rollups: [
{ name: 'sentCount', property: 'state.type', operation: 'count' },
{ name: 'sentGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
],
},
{
name: 'acknowledged',
filter: { 'state.type': 'acknowledged' },
rollups: [
{ name: 'acknowledgedCount', property: 'state.type', operation: 'count' },
{ name: 'acknowledgedGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
],
},
{
name: 'partiallyPaid',
filter: { 'state.type': 'partiallyPaid' },
rollups: [
{ name: 'partiallyPaidCount', property: 'state.type', operation: 'count' },
{ name: 'partiallyPaidGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
],
},
{
name: 'paid',
filter: { 'state.type': 'paid' },
rollups: [{ name: 'paidCount', property: 'state.type', operation: 'count' }],
},
{
name: 'overdue',
filter: { 'state.type': 'overdue' },
rollups: [{ name: 'overdueCount', property: 'state.type', operation: 'count' }],
},
{
name: 'cancelled',
filter: { 'state.type': 'cancelled' },
rollups: [{ name: 'cancelledCount', property: 'state.type', operation: 'count' }],
},
];
invoiceSchema.statics.stats = async function () {
const results = await aggregateRollups({
model: this,
rollupConfigs: rollupConfigs,
});
// Transform the results to match the expected format
return results;
};
invoiceSchema.statics.history = async function (from, to) {
const results = await aggregateRollupsHistory({
model: this,
startDate: from,
endDate: to,
rollupConfigs: rollupConfigs,
});
// Return time-series data array
return results;
};
invoiceSchema.statics.recalculate = async function (invoice, user) {
const invoiceId = invoice._id || invoice;
if (!invoiceId) {
return;
}
// Calculate totals from invoiceOrderItems
let totalAmount = 0;
for (const item of invoice.invoiceOrderItems || []) {
totalAmount += Number.parseFloat(item.invoiceAmount) || 0;
}
let totalAmountWithTax = 0;
for (const item of invoice.invoiceOrderItems || []) {
totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
}
// Calculate shipping totals from invoiceShipments
let shippingAmount = 0;
for (const item of invoice.invoiceShipments || []) {
shippingAmount += Number.parseFloat(item.invoiceAmount) || 0;
}
let shippingAmountWithTax = 0;
for (const item of invoice.invoiceShipments || []) {
shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
}
// Calculate grand total and tax amount
const grandTotalAmount = parseFloat(totalAmountWithTax) + parseFloat(shippingAmountWithTax);
const totalTaxAmount =
parseFloat(totalAmountWithTax) -
parseFloat(totalAmount) +
(parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount));
const updateData = {
totalAmount: parseFloat(totalAmount).toFixed(2),
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
shippingAmount: parseFloat(shippingAmount).toFixed(2),
shippingAmountWithTax: parseFloat(shippingAmountWithTax).toFixed(2),
grandTotalAmount: parseFloat(grandTotalAmount).toFixed(2),
totalTaxAmount: parseFloat(totalTaxAmount).toFixed(2),
};
await editObject({
model: this,
id: invoiceId,
updateData,
user,
recalculate: false,
});
};
// Add virtual id getter
invoiceSchema.virtual('id').get(function () {
return this._id;
});
// Configure JSON serialization to include virtuals
invoiceSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const invoiceModel = mongoose.model('invoice', invoiceSchema);

View File

@ -1,105 +0,0 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
const paymentSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
amount: { type: Number, required: true, default: 0 },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false },
client: { type: Schema.Types.ObjectId, ref: 'client', required: false },
invoice: { type: Schema.Types.ObjectId, ref: 'invoice', required: true },
state: {
type: { type: String, required: true, default: 'draft' },
},
paymentDate: { type: Date, required: false },
postedAt: { type: Date, required: false },
cancelledAt: { type: Date, required: false },
paymentMethod: { type: String, required: false },
notes: { type: String, required: false },
},
{ timestamps: true }
);
const rollupConfigs = [
{
name: 'draft',
filter: { 'state.type': 'draft' },
rollups: [
{ name: 'draftCount', property: 'state.type', operation: 'count' },
{ name: 'draftAmount', property: 'amount', operation: 'sum' },
],
},
{
name: 'posted',
filter: { 'state.type': 'posted' },
rollups: [
{ name: 'postedCount', property: 'state.type', operation: 'count' },
{ name: 'postedAmount', property: 'amount', operation: 'sum' },
],
},
{
name: 'cancelled',
filter: { 'state.type': 'cancelled' },
rollups: [
{ name: 'cancelledCount', property: 'state.type', operation: 'count' },
{ name: 'cancelledAmount', property: 'amount', operation: 'sum' },
],
},
];
paymentSchema.statics.stats = async function () {
const results = await aggregateRollups({
model: this,
rollupConfigs: rollupConfigs,
});
// Transform the results to match the expected format
return results;
};
paymentSchema.statics.history = async function (from, to) {
const results = await aggregateRollupsHistory({
model: this,
startDate: from,
endDate: to,
rollupConfigs: rollupConfigs,
});
// Return time-series data array
return results;
};
paymentSchema.statics.recalculate = async function (payment, user) {
const paymentId = payment._id || payment;
if (!paymentId) {
return;
}
// For payments, the amount is set directly
const amount = payment.amount || 0;
const updateData = {
amount: parseFloat(amount).toFixed(2),
};
await editObject({
model: this,
id: paymentId,
updateData,
user,
recalculate: false,
});
};
// Add virtual id getter
paymentSchema.virtual('id').get(function () {
return this._id;
});
// Configure JSON serialization to include virtuals
paymentSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const paymentModel = mongoose.model('payment', paymentSchema);

View File

@ -1,12 +1,6 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { purchaseOrderModel } from './purchaseorder.schema.js'; import { purchaseOrderModel } from './purchaseorder.schema.js';
import { taxRateModel } from '../management/taxrate.schema.js'; import { aggregateRollups, editObject } from '../../database.js';
import {
aggregateRollups,
aggregateRollupsHistory,
editObject,
getObject,
} from '../../database.js';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
@ -14,69 +8,20 @@ const orderItemSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
orderType: { type: String, required: true }, orderType: { type: String, required: true },
name: { type: String, required: true },
state: {
type: { type: String, required: true, default: 'draft' },
},
order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true }, order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true },
itemType: { type: String, required: true }, itemType: { type: String, required: true },
item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true }, item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true },
syncAmount: { type: String, required: false, default: null }, syncAmount: { type: String, required: true, default: null },
itemAmount: { type: Number, required: true }, itemAmount: { type: Number, required: true },
quantity: { type: Number, required: true }, quantity: { type: Number, required: true },
totalAmount: { type: Number, required: true }, totalAmount: { type: Number, required: true },
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
totalAmountWithTax: { type: Number, required: true }, totalAmountWithTax: { type: Number, required: true },
invoicedAmountWithTax: { type: Number, required: false, default: 0 },
invoicedAmount: { type: Number, required: false, default: 0 },
invoicedQuantity: { type: Number, required: false, default: 0 },
invoicedAmountRemaining: { type: Number, required: false, default: 0 },
invoicedAmountWithTaxRemaining: { type: Number, required: false, default: 0 },
invoicedQuantityRemaining: { type: Number, required: false, default: 0 },
timestamp: { type: Date, default: Date.now }, timestamp: { type: Date, default: Date.now },
shipment: { type: Schema.Types.ObjectId, ref: 'shipment', required: false },
orderedAt: { type: Date, required: false },
receivedAt: { type: Date, required: false },
}, },
{ timestamps: true } { timestamps: true }
); );
const rollupConfigs = [
{
name: 'shipped',
filter: { 'state.type': 'shipped' },
rollups: [{ name: 'shipped', property: 'state.type', operation: 'count' }],
},
{
name: 'received',
filter: { 'state.type': 'received' },
rollups: [{ name: 'received', property: 'state.type', operation: 'count' }],
},
];
orderItemSchema.statics.stats = async function () {
const results = await aggregateRollups({
model: this,
baseFilter: {},
rollupConfigs: rollupConfigs,
});
// Transform the results to match the expected format
return results;
};
orderItemSchema.statics.history = async function (from, to) {
const results = await aggregateRollupsHistory({
model: this,
startDate: from,
endDate: to,
rollupConfigs: rollupConfigs,
});
// Return time-series data array
return results;
};
orderItemSchema.statics.recalculate = async function (orderItem, user) { orderItemSchema.statics.recalculate = async function (orderItem, user) {
// Only purchase orders are supported for now // Only purchase orders are supported for now
if (orderItem.orderType !== 'purchaseOrder') { if (orderItem.orderType !== 'purchaseOrder') {
@ -88,32 +33,6 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
return; return;
} }
var taxRate = orderItem.taxRate;
if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length == 1) {
taxRate = await getObject({
model: taxRateModel,
id: orderItem.taxRate._id,
cached: true,
});
}
const orderTotalAmount = orderItem.itemAmount * orderItem.quantity;
const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100);
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
invoicedAmountRemaining: orderTotalAmount - orderItem.invoicedAmount,
invoicedAmountWithTaxRemaining: orderTotalAmountWithTax - orderItem.invoicedAmountWithTax,
invoicedQuantityRemaining: orderItem.quantity - orderItem.invoicedQuantity,
totalAmount: orderTotalAmount,
totalAmountWithTax: orderTotalAmountWithTax,
},
user,
recalculate: false,
});
const rollupResults = await aggregateRollups({ const rollupResults = await aggregateRollups({
model: this, model: this,
baseFilter: { baseFilter: {
@ -132,11 +51,6 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
}, },
], ],
}, },
{
name: 'overallCount',
rollups: [{ name: 'overallCount', property: '_id', operation: 'count' }],
},
...rollupConfigs,
], ],
}); });
@ -144,46 +58,14 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
const totalAmount = totals.totalAmount.sum?.toFixed(2) || 0; const totalAmount = totals.totalAmount.sum?.toFixed(2) || 0;
const totalAmountWithTax = totals.totalAmountWithTax.sum?.toFixed(2) || 0; const totalAmountWithTax = totals.totalAmountWithTax.sum?.toFixed(2) || 0;
const purchaseOrder = await getObject({
model: purchaseOrderModel,
id: orderId,
cached: true,
});
const grandTotalAmount =
parseFloat(totalAmountWithTax || 0) + parseFloat(purchaseOrder.shippingAmountWithTax || 0);
var updateData = {
totalAmount: parseFloat(totalAmount).toFixed(2),
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
totalTaxAmount: parseFloat((totalAmountWithTax - totalAmount).toFixed(2)),
grandTotalAmount: parseFloat(grandTotalAmount).toFixed(2),
};
const overallCount = rollupResults.overallCount.count || 0;
const shippedCount = rollupResults.shipped.count || 0;
const receivedCount = rollupResults.received.count || 0;
if (shippedCount > 0 && shippedCount < overallCount) {
updateData = { ...updateData, state: { type: 'partiallyShipped' } };
}
if (shippedCount > 0 && shippedCount == overallCount) {
updateData = { ...updateData, state: { type: 'shipped' } };
}
if (receivedCount > 0 && receivedCount < overallCount) {
updateData = { ...updateData, state: { type: 'partiallyReceived' } };
}
if (receivedCount > 0 && receivedCount == overallCount) {
updateData = { ...updateData, state: { type: 'received' } };
}
await editObject({ await editObject({
model: purchaseOrderModel, model: purchaseOrderModel,
id: orderId, id: orderId,
updateData: updateData, updateData: {
totalAmount: parseFloat(totalAmount),
totalAmountWithTax: parseFloat(totalAmountWithTax),
totalTaxAmount: parseFloat(totalAmountWithTax - totalAmount),
},
user, user,
}); });
}; };

View File

@ -1,100 +1,22 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
const purchaseOrderSchema = new Schema( const purchaseOrderSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
totalAmount: { type: Number, required: true, default: 0 }, totalAmount: { type: Number, required: true },
totalAmountWithTax: { type: Number, required: true, default: 0 }, totalAmountWithTax: { type: Number, required: true },
shippingAmount: { type: Number, required: true, default: 0 }, totalTaxAmount: { type: Number, required: true },
shippingAmountWithTax: { type: Number, required: true, default: 0 },
grandTotalAmount: { type: Number, required: true, default: 0 },
totalTaxAmount: { type: Number, required: true, default: 0 },
timestamp: { type: Date, default: Date.now }, timestamp: { type: Date, default: Date.now },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true }, vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
state: { state: {
type: { type: String, required: true, default: 'draft' }, type: { type: String, required: true, default: 'draft' },
}, },
postedAt: { type: Date, required: false },
acknowledgedAt: { type: Date, required: false },
cancelledAt: { type: Date, required: false },
completedAt: { type: Date, required: false },
}, },
{ timestamps: true } { timestamps: true }
); );
const rollupConfigs = [
{
name: 'draft',
filter: { 'state.type': 'draft' },
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
},
{
name: 'sent',
filter: { 'state.type': 'sent' },
rollups: [{ name: 'sent', property: 'state.type', operation: 'count' }],
},
{
name: 'acknowledged',
filter: { 'state.type': 'acknowledged' },
rollups: [{ name: 'acknowledged', property: 'state.type', operation: 'count' }],
},
{
name: 'partiallyShipped',
filter: { 'state.type': 'partiallyShipped' },
rollups: [{ name: 'partiallyShipped', property: 'state.type', operation: 'count' }],
},
{
name: 'shipped',
filter: { 'state.type': 'shipped' },
rollups: [{ name: 'shipped', property: 'state.type', operation: 'count' }],
},
{
name: 'partiallyReceived',
filter: { 'state.type': 'partiallyReceived' },
rollups: [{ name: 'partiallyReceived', property: 'state.type', operation: 'count' }],
},
{
name: 'received',
filter: { 'state.type': 'received' },
rollups: [{ name: 'received', property: 'state.type', operation: 'count' }],
},
{
name: 'cancelled',
filter: { 'state.type': 'cancelled' },
rollups: [{ name: 'cancelled', property: 'state.type', operation: 'count' }],
},
{
name: 'completed',
filter: { 'state.type': 'completed' },
rollups: [{ name: 'completed', property: 'state.type', operation: 'count' }],
},
];
purchaseOrderSchema.statics.stats = async function () {
const results = await aggregateRollups({
model: this,
rollupConfigs: rollupConfigs,
});
// Transform the results to match the expected format
return results;
};
purchaseOrderSchema.statics.history = async function (from, to) {
const results = await aggregateRollupsHistory({
model: this,
startDate: from,
endDate: to,
rollupConfigs: rollupConfigs,
});
// Return time-series data array
return results;
};
// Add virtual id getter // Add virtual id getter
purchaseOrderSchema.virtual('id').get(function () { purchaseOrderSchema.virtual('id').get(function () {
return this._id; return this._id;

View File

@ -1,116 +1,43 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
import { purchaseOrderModel } from './purchaseorder.schema.js';
import { taxRateModel } from '../management/taxrate.schema.js'; const shipmentItemSchema = new Schema({
import { aggregateRollups, editObject, getObject } from '../../database.js'; itemType: { type: String, required: true },
item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true },
quantity: { type: Number, required: true },
itemCost: { type: Number, required: true },
totalCost: { type: Number, required: true },
totalCostWithTax: { type: Number, required: true },
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
});
const shipmentSchema = new Schema( const shipmentSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
orderType: { type: String, required: true }, purchaseOrder: { type: Schema.Types.ObjectId, ref: 'purchaseOrder', required: true },
order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true }, vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
courierService: { type: Schema.Types.ObjectId, ref: 'courierService', required: false }, courierService: { type: Schema.Types.ObjectId, ref: 'courierService', required: false },
trackingNumber: { type: String, required: false }, trackingNumber: { type: String, required: false },
amount: { type: Number, required: true }, items: [shipmentItemSchema],
amountWithTax: { type: Number, required: true }, cost: { net: { type: Number, required: true }, gross: { type: Number, required: true } },
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false }, shippedDate: { type: Date, required: false },
invoicedAmount: { type: Number, required: false, default: 0 }, expectedDeliveryDate: { type: Date, required: false },
invoicedAmountWithTax: { type: Number, required: false, default: 0 }, actualDeliveryDate: { type: Date, required: false },
invoicedAmountRemaining: { type: Number, required: false, default: 0 },
invoicedAmountWithTaxRemaining: { type: Number, required: false, default: 0 },
shippedAt: { type: Date, required: false },
expectedAt: { type: Date, required: false },
deliveredAt: { type: Date, required: false },
cancelledAt: { type: Date, required: false },
state: { state: {
type: { type: {
type: String, type: String,
required: true, required: true,
default: 'pending',
enum: ['pending', 'shipped', 'in_transit', 'delivered', 'cancelled'],
}, },
}, },
notes: { type: String },
timestamp: { type: Date, default: Date.now },
}, },
{ timestamps: true } { timestamps: true }
); );
shipmentSchema.statics.recalculate = async function (shipment, user) {
// Only purchase orders are supported for now
if (shipment.orderType !== 'purchaseOrder') {
return;
}
const orderId = shipment.order?._id || shipment.order;
if (!orderId) {
return;
}
var taxRate = shipment.taxRate;
if (shipment.taxRate?._id && Object.keys(shipment.taxRate).length == 1) {
taxRate = await getObject({
model: taxRateModel,
id: shipment.taxRate._id,
cached: true,
});
}
const amountWithTax = parseFloat(
(shipment.amount || 0) * (1 + (taxRate?.rate || 0) / 100)
).toFixed(2);
await editObject({
model: shipmentModel,
id: shipment._id,
updateData: {
amountWithTax: amountWithTax,
invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0),
invoicedAmountWithTaxRemaining: amountWithTax - (shipment.invoicedAmountWithTax || 0),
},
user,
recalculate: false,
});
const rollupResults = await aggregateRollups({
model: this,
baseFilter: {
order: new mongoose.Types.ObjectId(orderId),
orderType: shipment.orderType,
},
rollupConfigs: [
{
name: 'shipmentTotals',
rollups: [
{ name: 'amount', property: 'amount', operation: 'sum' },
{ name: 'amountWithTax', property: 'amountWithTax', operation: 'sum' },
],
},
],
});
const totals = rollupResults.shipmentTotals || {};
const totalShippingAmount = totals.amount.sum?.toFixed(2) || 0;
const totalShippingAmountWithTax = totals.amountWithTax.sum?.toFixed(2) || 0;
const purchaseOrder = await getObject({
model: purchaseOrderModel,
id: orderId,
cached: true,
});
const grandTotalAmount =
parseFloat(purchaseOrder.totalAmountWithTax || 0) + parseFloat(totalShippingAmountWithTax || 0);
await editObject({
model: purchaseOrderModel,
id: orderId,
updateData: {
shippingAmount: parseFloat(totalShippingAmount).toFixed(2),
shippingAmountWithTax: parseFloat(totalShippingAmountWithTax).toFixed(2),
grandTotalAmount: parseFloat(grandTotalAmount).toFixed(2),
},
user,
recalculate: false,
});
};
// Add virtual id getter // Add virtual id getter
shipmentSchema.virtual('id').get(function () { shipmentSchema.virtual('id').get(function () {
return this._id; return this._id;

View File

@ -24,12 +24,8 @@ import { documentJobModel } from './management/documentjob.schema.js';
import { fileModel } from './management/file.schema.js'; import { fileModel } from './management/file.schema.js';
import { courierServiceModel } from './management/courierservice.schema.js'; import { courierServiceModel } from './management/courierservice.schema.js';
import { courierModel } from './management/courier.schema.js'; import { courierModel } from './management/courier.schema.js';
import { taxRateModel } from './management/taxrate.schema.js'; import { taxRateModel } from './management/taxrates.schema.js';
import { taxRecordModel } from './management/taxrecord.schema.js'; import { taxRecordModel } from './management/taxrecord.schema.js';
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';
// Map prefixes to models and id fields // Map prefixes to models and id fields
export const models = { export const models = {
@ -102,8 +98,4 @@ export const models = {
COR: { model: courierModel, idField: '_id', type: 'courier', referenceField: '_reference' }, COR: { model: courierModel, idField: '_id', type: 'courier', referenceField: '_reference' },
TXR: { model: taxRateModel, idField: '_id', type: 'taxRate', referenceField: '_reference' }, TXR: { model: taxRateModel, idField: '_id', type: 'taxRate', referenceField: '_reference' },
TXD: { model: taxRecordModel, idField: '_id', type: 'taxRecord', referenceField: '_reference' }, TXD: { model: taxRecordModel, idField: '_id', type: 'taxRecord', referenceField: '_reference' },
SHP: { model: shipmentModel, idField: '_id', type: 'shipment', referenceField: '_reference' },
INV: { model: invoiceModel, idField: '_id', type: 'invoice', referenceField: '_reference' },
CLI: { model: clientModel, idField: '_id', type: 'client', referenceField: '_reference' },
SOR: { model: salesOrderModel, idField: '_id', type: 'salesOrder', referenceField: '_reference' },
}; };

View File

@ -92,6 +92,8 @@ printerSchema.statics.stats = async function () {
rollupConfigs: rollupConfigs, rollupConfigs: rollupConfigs,
}); });
console.log(results);
// Transform the results to match the expected format // Transform the results to match the expected format
return results; return results;
}; };

View File

@ -1,34 +0,0 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const addressSchema = new mongoose.Schema({
building: { required: false, type: String },
addressLine1: { required: false, type: String },
addressLine2: { required: false, type: String },
city: { required: false, type: String },
state: { required: false, type: String },
postcode: { required: false, type: String },
country: { required: false, type: String },
});
const clientSchema = new mongoose.Schema(
{
_reference: { type: String, default: () => generateId()() },
name: { required: true, type: String },
email: { required: false, type: String },
phone: { required: false, type: String },
country: { required: false, type: String },
active: { required: true, type: Boolean, default: true },
address: { required: false, type: addressSchema },
tags: [{ required: false, type: String }],
},
{ timestamps: true }
);
clientSchema.virtual('id').get(function () {
return this._id;
});
clientSchema.set('toJSON', { virtuals: true });
export const clientModel = mongoose.model('client', clientSchema);

View File

@ -1,107 +0,0 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
const salesOrderSchema = new Schema(
{
_reference: { type: String, default: () => generateId()() },
totalAmount: { type: Number, required: true, default: 0 },
totalAmountWithTax: { type: Number, required: true, default: 0 },
shippingAmount: { type: Number, required: true, default: 0 },
shippingAmountWithTax: { type: Number, required: true, default: 0 },
grandTotalAmount: { type: Number, required: true, default: 0 },
totalTaxAmount: { type: Number, required: true, default: 0 },
timestamp: { type: Date, default: Date.now },
client: { type: Schema.Types.ObjectId, ref: 'client', required: true },
state: {
type: { type: String, required: true, default: 'draft' },
},
postedAt: { type: Date, required: false },
confirmedAt: { type: Date, required: false },
cancelledAt: { type: Date, required: false },
completedAt: { type: Date, required: false },
},
{ timestamps: true }
);
const rollupConfigs = [
{
name: 'draft',
filter: { 'state.type': 'draft' },
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
},
{
name: 'sent',
filter: { 'state.type': 'sent' },
rollups: [{ name: 'sent', property: 'state.type', operation: 'count' }],
},
{
name: 'confirmed',
filter: { 'state.type': 'confirmed' },
rollups: [{ name: 'confirmed', property: 'state.type', operation: 'count' }],
},
{
name: 'partiallyShipped',
filter: { 'state.type': 'partiallyShipped' },
rollups: [{ name: 'partiallyShipped', property: 'state.type', operation: 'count' }],
},
{
name: 'shipped',
filter: { 'state.type': 'shipped' },
rollups: [{ name: 'shipped', property: 'state.type', operation: 'count' }],
},
{
name: 'partiallyDelivered',
filter: { 'state.type': 'partiallyDelivered' },
rollups: [{ name: 'partiallyDelivered', property: 'state.type', operation: 'count' }],
},
{
name: 'delivered',
filter: { 'state.type': 'delivered' },
rollups: [{ name: 'delivered', property: 'state.type', operation: 'count' }],
},
{
name: 'cancelled',
filter: { 'state.type': 'cancelled' },
rollups: [{ name: 'cancelled', property: 'state.type', operation: 'count' }],
},
{
name: 'completed',
filter: { 'state.type': 'completed' },
rollups: [{ name: 'completed', property: 'state.type', operation: 'count' }],
},
];
salesOrderSchema.statics.stats = async function () {
const results = await aggregateRollups({
model: this,
rollupConfigs: rollupConfigs,
});
// Transform the results to match the expected format
return results;
};
salesOrderSchema.statics.history = async function (from, to) {
const results = await aggregateRollupsHistory({
model: this,
startDate: from,
endDate: to,
rollupConfigs: rollupConfigs,
});
// Return time-series data array
return results;
};
// Add virtual id getter
salesOrderSchema.virtual('id').get(function () {
return this._id;
});
// Configure JSON serialization to include virtuals
salesOrderSchema.set('toJSON', { virtuals: true });
// Create and export the model
export const salesOrderModel = mongoose.model('salesOrder', salesOrderSchema);

View File

@ -37,10 +37,6 @@ import {
courierServiceRoutes, courierServiceRoutes,
taxRateRoutes, taxRateRoutes,
taxRecordRoutes, taxRecordRoutes,
invoiceRoutes,
paymentRoutes,
clientRoutes,
salesOrderRoutes,
} from './routes/index.js'; } from './routes/index.js';
import path from 'path'; import path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
@ -143,15 +139,7 @@ app.use('/couriers', courierRoutes);
app.use('/courierservices', courierServiceRoutes); app.use('/courierservices', courierServiceRoutes);
app.use('/taxrates', taxRateRoutes); app.use('/taxrates', taxRateRoutes);
app.use('/taxrecords', taxRecordRoutes); app.use('/taxrecords', taxRecordRoutes);
app.use('/invoices', invoiceRoutes);
app.use('/payments', paymentRoutes);
app.use('/clients', clientRoutes);
app.use('/salesorders', salesOrderRoutes);
app.use('/notes', noteRoutes); app.use('/notes', noteRoutes);
// Start the application // Start the application
if (process.env.NODE_ENV !== 'test') {
initializeApp(); initializeApp();
}
export default app;

View File

@ -2,7 +2,7 @@ import Keycloak from 'keycloak-connect';
import session from 'express-session'; import session from 'express-session';
import config, { getEnvironment } from './config.js'; import config, { getEnvironment } from './config.js';
import axios from 'axios'; import axios from 'axios';
import jwt from 'jsonwebtoken'; import dotenv from 'dotenv';
import log4js from 'log4js'; import log4js from 'log4js';
import NodeCache from 'node-cache'; import NodeCache from 'node-cache';
import { userModel } from './database/schemas/management/user.schema.js'; import { userModel } from './database/schemas/management/user.schema.js';
@ -12,6 +12,8 @@ import { hostModel } from './database/schemas/management/host.schema.js';
const logger = log4js.getLogger('Keycloak'); const logger = log4js.getLogger('Keycloak');
logger.level = config.server.logLevel || 'info'; logger.level = config.server.logLevel || 'info';
dotenv.config();
// Initialize NodeCache with 5-minute TTL // Initialize NodeCache with 5-minute TTL
const userCache = new NodeCache({ stdTTL: 300 }); // 300 seconds = 5 minutes const userCache = new NodeCache({ stdTTL: 300 }); // 300 seconds = 5 minutes
@ -65,14 +67,14 @@ const keycloakConfig = {
'use-resource-role-mappings': true, 'use-resource-role-mappings': true,
'verify-token-audience': true, 'verify-token-audience': true,
credentials: { credentials: {
secret: config.auth.keycloak.clientSecret, secret: process.env.KEYCLOAK_CLIENT_SECRET,
}, },
}; };
const memoryStore = new session.MemoryStore(); const memoryStore = new session.MemoryStore();
var expressSession = session({ var expressSession = session({
secret: config.auth.sessionSecret, secret: process.env.SESSION_SECRET || 'n00Dl3s23!',
resave: false, resave: false,
saveUninitialized: true, // Set this to true to ensure session is initialized saveUninitialized: true, // Set this to true to ensure session is initialized
store: memoryStore, store: memoryStore,
@ -98,7 +100,7 @@ const isAuthenticated = async (req, res, next) => {
new URLSearchParams({ new URLSearchParams({
token: token, token: token,
client_id: config.auth.keycloak.clientId, client_id: config.auth.keycloak.clientId,
client_secret: config.auth.keycloak.clientSecret, client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
}), }),
{ {
headers: { headers: {
@ -121,6 +123,8 @@ const isAuthenticated = async (req, res, next) => {
} }
} }
console.log('Checking host authentication', req.headers);
const hostId = req.headers['x-host-id']; const hostId = req.headers['x-host-id'];
const authCode = req.headers['x-auth-code']; const authCode = req.headers['x-auth-code'];
if (hostId && authCode) { if (hostId && authCode) {
@ -133,6 +137,7 @@ const isAuthenticated = async (req, res, next) => {
} }
// Fallback to session-based authentication // Fallback to session-based authentication
console.log('Using session token');
if (req.session && req.session['keycloak-token']) { if (req.session && req.session['keycloak-token']) {
const sessionToken = req.session['keycloak-token']; const sessionToken = req.session['keycloak-token'];
if (sessionToken.expires_at > new Date().getTime()) { if (sessionToken.expires_at > new Date().getTime()) {

View File

@ -1,101 +0,0 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listInvoicesRouteHandler,
getInvoiceRouteHandler,
editInvoiceRouteHandler,
editMultipleInvoicesRouteHandler,
newInvoiceRouteHandler,
deleteInvoiceRouteHandler,
listInvoicesByPropertiesRouteHandler,
getInvoiceStatsRouteHandler,
getInvoiceHistoryRouteHandler,
acknowledgeInvoiceRouteHandler,
cancelInvoiceRouteHandler,
postInvoiceRouteHandler,
} from '../../services/finance/invoices.js';
// list of invoices
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = [
'vendor',
'client',
'state',
'vendor._id',
'client._id',
'order',
'order._id',
'orderType',
];
const filter = getFilter(req.query, allowedFilters);
listInvoicesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = [
'vendor',
'client',
'orderType',
'order',
'state.type',
'value',
'vendor._id',
'client._id',
];
const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {};
if (req.query.masterFilter) {
masterFilter = JSON.parse(req.query.masterFilter);
}
listInvoicesByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newInvoiceRouteHandler(req, res);
});
// get invoice stats
router.get('/stats', isAuthenticated, (req, res) => {
getInvoiceStatsRouteHandler(req, res);
});
// get invoice history
router.get('/history', isAuthenticated, (req, res) => {
getInvoiceHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getInvoiceRouteHandler(req, res);
});
// update multiple invoices
router.put('/', isAuthenticated, async (req, res) => {
editMultipleInvoicesRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editInvoiceRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteInvoiceRouteHandler(req, res);
});
router.post('/:id/post', isAuthenticated, async (req, res) => {
postInvoiceRouteHandler(req, res);
});
router.post('/:id/acknowledge', isAuthenticated, async (req, res) => {
acknowledgeInvoiceRouteHandler(req, res);
});
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
cancelInvoiceRouteHandler(req, res);
});
export default router;

View File

@ -1,96 +0,0 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listPaymentsRouteHandler,
getPaymentRouteHandler,
editPaymentRouteHandler,
editMultiplePaymentsRouteHandler,
newPaymentRouteHandler,
deletePaymentRouteHandler,
listPaymentsByPropertiesRouteHandler,
getPaymentStatsRouteHandler,
getPaymentHistoryRouteHandler,
postPaymentRouteHandler,
cancelPaymentRouteHandler,
} from '../../services/finance/payments.js';
// list of payments
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = [
'vendor',
'client',
'state',
'vendor._id',
'client._id',
'invoice',
'invoice._id',
];
const filter = getFilter(req.query, allowedFilters);
listPaymentsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = [
'vendor',
'client',
'invoice',
'state.type',
'value',
'vendor._id',
'client._id',
'invoice._id',
];
const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {};
if (req.query.masterFilter) {
masterFilter = JSON.parse(req.query.masterFilter);
}
listPaymentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newPaymentRouteHandler(req, res);
});
// get payment stats
router.get('/stats', isAuthenticated, (req, res) => {
getPaymentStatsRouteHandler(req, res);
});
// get payment history
router.get('/history', isAuthenticated, (req, res) => {
getPaymentHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getPaymentRouteHandler(req, res);
});
// update multiple payments
router.put('/', isAuthenticated, async (req, res) => {
editMultiplePaymentsRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editPaymentRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deletePaymentRouteHandler(req, res);
});
router.post('/:id/post', isAuthenticated, async (req, res) => {
postPaymentRouteHandler(req, res);
});
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
cancelPaymentRouteHandler(req, res);
});
export default router;

View File

@ -29,10 +29,6 @@ import courierRoutes from './management/courier.js';
import courierServiceRoutes from './management/courierservice.js'; import courierServiceRoutes from './management/courierservice.js';
import taxRateRoutes from './management/taxrates.js'; import taxRateRoutes from './management/taxrates.js';
import taxRecordRoutes from './management/taxrecords.js'; import taxRecordRoutes from './management/taxrecords.js';
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 noteRoutes from './misc/notes.js'; import noteRoutes from './misc/notes.js';
export { export {
@ -68,8 +64,4 @@ export {
courierServiceRoutes, courierServiceRoutes,
taxRateRoutes, taxRateRoutes,
taxRecordRoutes, taxRecordRoutes,
invoiceRoutes,
paymentRoutes,
clientRoutes,
salesOrderRoutes,
}; };

View File

@ -7,7 +7,6 @@ import {
listFilamentStocksRouteHandler, listFilamentStocksRouteHandler,
getFilamentStockRouteHandler, getFilamentStockRouteHandler,
editFilamentStockRouteHandler, editFilamentStockRouteHandler,
editMultipleFilamentStocksRouteHandler,
newFilamentStockRouteHandler, newFilamentStockRouteHandler,
deleteFilamentStockRouteHandler, deleteFilamentStockRouteHandler,
listFilamentStocksByPropertiesRouteHandler, listFilamentStocksByPropertiesRouteHandler,
@ -52,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
getFilamentStockRouteHandler(req, res); getFilamentStockRouteHandler(req, res);
}); });
// update multiple filament stocks
router.put('/', isAuthenticated, async (req, res) => {
editMultipleFilamentStocksRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => { router.put('/:id', isAuthenticated, async (req, res) => {
editFilamentStockRouteHandler(req, res); editFilamentStockRouteHandler(req, res);
}); });

View File

@ -7,7 +7,6 @@ import {
listOrderItemsRouteHandler, listOrderItemsRouteHandler,
getOrderItemRouteHandler, getOrderItemRouteHandler,
editOrderItemRouteHandler, editOrderItemRouteHandler,
editMultipleOrderItemsRouteHandler,
newOrderItemRouteHandler, newOrderItemRouteHandler,
deleteOrderItemRouteHandler, deleteOrderItemRouteHandler,
listOrderItemsByPropertiesRouteHandler, listOrderItemsByPropertiesRouteHandler,
@ -18,34 +17,14 @@ import {
// list of order items // list of order items
router.get('/', isAuthenticated, (req, res) => { router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query; const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = [ const allowedFilters = ['itemType', 'item', 'item._id', 'order', 'order._id', 'orderType'];
'name',
'itemType',
'item',
'item._id',
'order',
'order._id',
'orderType',
'shipment',
'shipment._id',
];
const filter = getFilter(req.query, allowedFilters); const filter = getFilter(req.query, allowedFilters);
listOrderItemsRouteHandler(req, res, page, limit, property, filter, search, sort, order); listOrderItemsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
}); });
router.get('/properties', isAuthenticated, (req, res) => { router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties); let properties = convertPropertiesString(req.query.properties);
const allowedFilters = [ const allowedFilters = ['itemType', 'item', 'item._id', 'order', 'order._id', 'orderType'];
'name',
'itemType',
'item',
'item._id',
'order',
'order._id',
'orderType',
'shipment',
'shipment._id',
];
const filter = getFilter(req.query, allowedFilters, false); const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {}; var masterFilter = {};
if (req.query.masterFilter) { if (req.query.masterFilter) {
@ -72,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
getOrderItemRouteHandler(req, res); getOrderItemRouteHandler(req, res);
}); });
// update multiple order items
router.put('/', isAuthenticated, async (req, res) => {
editMultipleOrderItemsRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => { router.put('/:id', isAuthenticated, async (req, res) => {
editOrderItemRouteHandler(req, res); editOrderItemRouteHandler(req, res);
}); });

View File

@ -7,7 +7,6 @@ import {
listPartStocksRouteHandler, listPartStocksRouteHandler,
getPartStockRouteHandler, getPartStockRouteHandler,
editPartStockRouteHandler, editPartStockRouteHandler,
editMultiplePartStocksRouteHandler,
newPartStockRouteHandler, newPartStockRouteHandler,
deletePartStockRouteHandler, deletePartStockRouteHandler,
listPartStocksByPropertiesRouteHandler, listPartStocksByPropertiesRouteHandler,
@ -52,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
getPartStockRouteHandler(req, res); getPartStockRouteHandler(req, res);
}); });
// update multiple part stocks
router.put('/', isAuthenticated, async (req, res) => {
editMultiplePartStocksRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => { router.put('/:id', isAuthenticated, async (req, res) => {
editPartStockRouteHandler(req, res); editPartStockRouteHandler(req, res);
}); });

View File

@ -7,15 +7,11 @@ import {
listPurchaseOrdersRouteHandler, listPurchaseOrdersRouteHandler,
getPurchaseOrderRouteHandler, getPurchaseOrderRouteHandler,
editPurchaseOrderRouteHandler, editPurchaseOrderRouteHandler,
editMultiplePurchaseOrdersRouteHandler,
newPurchaseOrderRouteHandler, newPurchaseOrderRouteHandler,
deletePurchaseOrderRouteHandler, deletePurchaseOrderRouteHandler,
listPurchaseOrdersByPropertiesRouteHandler, listPurchaseOrdersByPropertiesRouteHandler,
getPurchaseOrderStatsRouteHandler, getPurchaseOrderStatsRouteHandler,
getPurchaseOrderHistoryRouteHandler, getPurchaseOrderHistoryRouteHandler,
postPurchaseOrderRouteHandler,
acknowledgePurchaseOrderRouteHandler,
cancelPurchaseOrderRouteHandler,
} from '../../services/inventory/purchaseorders.js'; } from '../../services/inventory/purchaseorders.js';
// list of purchase orders // list of purchase orders
@ -32,7 +28,7 @@ router.get('/properties', isAuthenticated, (req, res) => {
const filter = getFilter(req.query, allowedFilters, false); const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {}; var masterFilter = {};
if (req.query.masterFilter) { if (req.query.masterFilter) {
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true); masterFilter = JSON.parse(req.query.masterFilter);
} }
listPurchaseOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter); listPurchaseOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
}); });
@ -55,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
getPurchaseOrderRouteHandler(req, res); getPurchaseOrderRouteHandler(req, res);
}); });
// update multiple purchase orders
router.put('/', isAuthenticated, async (req, res) => {
editMultiplePurchaseOrdersRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => { router.put('/:id', isAuthenticated, async (req, res) => {
editPurchaseOrderRouteHandler(req, res); editPurchaseOrderRouteHandler(req, res);
}); });
@ -68,16 +59,4 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
deletePurchaseOrderRouteHandler(req, res); deletePurchaseOrderRouteHandler(req, res);
}); });
router.post('/:id/post', isAuthenticated, async (req, res) => {
postPurchaseOrderRouteHandler(req, res);
});
router.post('/:id/acknowledge', isAuthenticated, async (req, res) => {
acknowledgePurchaseOrderRouteHandler(req, res);
});
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
cancelPurchaseOrderRouteHandler(req, res);
});
export default router; export default router;

View File

@ -7,21 +7,24 @@ import {
listShipmentsRouteHandler, listShipmentsRouteHandler,
getShipmentRouteHandler, getShipmentRouteHandler,
editShipmentRouteHandler, editShipmentRouteHandler,
editMultipleShipmentsRouteHandler,
newShipmentRouteHandler, newShipmentRouteHandler,
deleteShipmentRouteHandler, deleteShipmentRouteHandler,
listShipmentsByPropertiesRouteHandler, listShipmentsByPropertiesRouteHandler,
getShipmentStatsRouteHandler, getShipmentStatsRouteHandler,
getShipmentHistoryRouteHandler, getShipmentHistoryRouteHandler,
shipShipmentRouteHandler,
receiveShipmentRouteHandler,
cancelShipmentRouteHandler,
} from '../../services/inventory/shipments.js'; } from '../../services/inventory/shipments.js';
// list of shipments // list of shipments
router.get('/', isAuthenticated, (req, res) => { router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query; const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['orderType', 'order', 'state', 'courierService', 'order._id', 'taxRate']; const allowedFilters = [
'vendor',
'purchaseOrder',
'state',
'courierService',
'vendor._id',
'purchaseOrder._id',
];
const filter = getFilter(req.query, allowedFilters); const filter = getFilter(req.query, allowedFilters);
listShipmentsRouteHandler(req, res, page, limit, property, filter, search, sort, order); listShipmentsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
}); });
@ -29,17 +32,17 @@ router.get('/', isAuthenticated, (req, res) => {
router.get('/properties', isAuthenticated, (req, res) => { router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties); let properties = convertPropertiesString(req.query.properties);
const allowedFilters = [ const allowedFilters = [
'orderType', 'vendor',
'order', 'purchaseOrder',
'state.type', 'state.type',
'courierService', 'courierService',
'order._id', 'vendor._id',
'taxRate', 'purchaseOrder._id',
]; ];
const filter = getFilter(req.query, allowedFilters, false); const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {}; var masterFilter = {};
if (req.query.masterFilter) { if (req.query.masterFilter) {
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true); masterFilter = JSON.parse(req.query.masterFilter);
} }
listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter); listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
}); });
@ -62,11 +65,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
getShipmentRouteHandler(req, res); getShipmentRouteHandler(req, res);
}); });
// update multiple shipments
router.put('/', isAuthenticated, async (req, res) => {
editMultipleShipmentsRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => { router.put('/:id', isAuthenticated, async (req, res) => {
editShipmentRouteHandler(req, res); editShipmentRouteHandler(req, res);
}); });
@ -75,16 +73,4 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
deleteShipmentRouteHandler(req, res); deleteShipmentRouteHandler(req, res);
}); });
router.post('/:id/ship', isAuthenticated, async (req, res) => {
shipShipmentRouteHandler(req, res);
});
router.post('/:id/receive', isAuthenticated, async (req, res) => {
receiveShipmentRouteHandler(req, res);
});
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
cancelShipmentRouteHandler(req, res);
});
export default router; export default router;

View File

@ -8,7 +8,6 @@ import {
getStockEventRouteHandler, getStockEventRouteHandler,
newStockEventRouteHandler, newStockEventRouteHandler,
editStockEventRouteHandler, editStockEventRouteHandler,
editMultipleStockEventsRouteHandler,
deleteStockEventRouteHandler, deleteStockEventRouteHandler,
listStockEventsByPropertiesRouteHandler, listStockEventsByPropertiesRouteHandler,
getStockEventStatsRouteHandler, getStockEventStatsRouteHandler,
@ -52,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
getStockEventRouteHandler(req, res); getStockEventRouteHandler(req, res);
}); });
// update multiple stock events
router.put('/', isAuthenticated, async (req, res) => {
editMultipleStockEventsRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => { router.put('/:id', isAuthenticated, async (req, res) => {
editStockEventRouteHandler(req, res); editStockEventRouteHandler(req, res);
}); });

View File

@ -8,7 +8,6 @@ import {
listFilamentsByPropertiesRouteHandler, listFilamentsByPropertiesRouteHandler,
getFilamentRouteHandler, getFilamentRouteHandler,
editFilamentRouteHandler, editFilamentRouteHandler,
editMultipleFilamentsRouteHandler,
newFilamentRouteHandler, newFilamentRouteHandler,
getFilamentStatsRouteHandler, getFilamentStatsRouteHandler,
getFilamentHistoryRouteHandler, getFilamentHistoryRouteHandler,
@ -67,11 +66,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
getFilamentRouteHandler(req, res); getFilamentRouteHandler(req, res);
}); });
// update filaments info
router.put('/', isAuthenticated, async (req, res) => {
editMultipleFilamentsRouteHandler(req, res);
});
// update printer info // update printer info
router.put('/:id', isAuthenticated, async (req, res) => { router.put('/:id', isAuthenticated, async (req, res) => {
editFilamentRouteHandler(req, res); editFilamentRouteHandler(req, res);

View File

@ -1,59 +0,0 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listClientsRouteHandler,
getClientRouteHandler,
editClientRouteHandler,
newClientRouteHandler,
deleteClientRouteHandler,
listClientsByPropertiesRouteHandler,
getClientStatsRouteHandler,
getClientHistoryRouteHandler,
} from '../../services/sales/clients.js';
// list of clients
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['country', 'active', 'createdAt', 'updatedAt'];
const filter = getFilter(req.query, allowedFilters);
listClientsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['country', 'active', 'createdAt', 'updatedAt'];
const filter = getFilter(req.query, allowedFilters, false);
listClientsByPropertiesRouteHandler(req, res, properties, filter);
});
router.post('/', isAuthenticated, (req, res) => {
newClientRouteHandler(req, res);
});
// get client stats
router.get('/stats', isAuthenticated, (req, res) => {
getClientStatsRouteHandler(req, res);
});
// get clients history
router.get('/history', isAuthenticated, (req, res) => {
getClientHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getClientRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editClientRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteClientRouteHandler(req, res);
});
export default router;

View File

@ -1,84 +0,0 @@
import express from 'express';
import { isAuthenticated } from '../../keycloak.js';
import { getFilter, convertPropertiesString } from '../../utils.js';
const router = express.Router();
import {
listSalesOrdersRouteHandler,
getSalesOrderRouteHandler,
editSalesOrderRouteHandler,
editMultipleSalesOrdersRouteHandler,
newSalesOrderRouteHandler,
deleteSalesOrderRouteHandler,
listSalesOrdersByPropertiesRouteHandler,
getSalesOrderStatsRouteHandler,
getSalesOrderHistoryRouteHandler,
postSalesOrderRouteHandler,
confirmSalesOrderRouteHandler,
cancelSalesOrderRouteHandler,
} from '../../services/sales/salesorders.js';
// list of sales orders
router.get('/', isAuthenticated, (req, res) => {
const { page, limit, property, search, sort, order } = req.query;
const allowedFilters = ['client', 'state', 'value', 'client._id'];
const filter = getFilter(req.query, allowedFilters);
listSalesOrdersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
});
router.get('/properties', isAuthenticated, (req, res) => {
let properties = convertPropertiesString(req.query.properties);
const allowedFilters = ['client', 'state.type', 'value', 'client._id'];
const filter = getFilter(req.query, allowedFilters, false);
var masterFilter = {};
if (req.query.masterFilter) {
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true);
}
listSalesOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
});
router.post('/', isAuthenticated, (req, res) => {
newSalesOrderRouteHandler(req, res);
});
// get sales order stats
router.get('/stats', isAuthenticated, (req, res) => {
getSalesOrderStatsRouteHandler(req, res);
});
// get sales orders history
router.get('/history', isAuthenticated, (req, res) => {
getSalesOrderHistoryRouteHandler(req, res);
});
router.get('/:id', isAuthenticated, (req, res) => {
getSalesOrderRouteHandler(req, res);
});
// update multiple sales orders
router.put('/', isAuthenticated, async (req, res) => {
editMultipleSalesOrdersRouteHandler(req, res);
});
router.put('/:id', isAuthenticated, async (req, res) => {
editSalesOrderRouteHandler(req, res);
});
router.delete('/:id', isAuthenticated, async (req, res) => {
deleteSalesOrderRouteHandler(req, res);
});
router.post('/:id/post', isAuthenticated, async (req, res) => {
postSalesOrderRouteHandler(req, res);
});
router.post('/:id/confirm', isAuthenticated, async (req, res) => {
confirmSalesOrderRouteHandler(req, res);
});
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
cancelSalesOrderRouteHandler(req, res);
});
export default router;

View File

@ -1,154 +0,0 @@
import { jest } from '@jest/globals';
// Mock dependencies
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
checkStates: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/finance/invoice.schema.js', () => ({
invoiceModel: { modelName: 'Invoice' },
}));
jest.unstable_mockModule('../../../database/schemas/inventory/orderitem.schema.js', () => ({
orderItemModel: { modelName: 'OrderItem' },
}));
jest.unstable_mockModule('../../../database/schemas/inventory/shipment.schema.js', () => ({
shipmentModel: { modelName: 'Shipment' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
// Import handlers after mocking
const {
listInvoicesRouteHandler,
getInvoiceRouteHandler,
newInvoiceRouteHandler,
editInvoiceRouteHandler,
deleteInvoiceRouteHandler,
postInvoiceRouteHandler,
} = await import('../invoices.js');
const { listObjects, getObject, editObject, newObject, deleteObject, checkStates } = await import(
'../../../database/database.js'
);
const { invoiceModel } = await import('../../../database/schemas/finance/invoice.schema.js');
describe('Invoice Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listInvoicesRouteHandler', () => {
it('should list invoices', async () => {
const mockResult = [{ _id: '1', invoiceNumber: 'INV-001' }];
listObjects.mockResolvedValue(mockResult);
await listInvoicesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('getInvoiceRouteHandler', () => {
it('should get an invoice by ID', async () => {
req.params.id = '123';
const mockResult = { _id: '123', invoiceNumber: 'INV-001' };
getObject.mockResolvedValue(mockResult);
await getInvoiceRouteHandler(req, res);
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: '123' }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newInvoiceRouteHandler', () => {
it('should create a new invoice with order items and shipments', async () => {
req.body = { order: 'order123', orderType: 'sales' };
// Mock listObjects for orderItems and shipments
listObjects.mockResolvedValueOnce([{ _id: 'oi1', totalAmount: 100, invoicedAmount: 0 }]); // orderItems
listObjects.mockResolvedValueOnce([{ _id: 's1', amount: 20, invoicedAmount: 0 }]); // shipments
const mockInvoice = { _id: 'inv456' };
newObject.mockResolvedValue(mockInvoice);
await newInvoiceRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockInvoice);
});
});
describe('postInvoiceRouteHandler', () => {
it('should post a draft invoice and update order items/shipments', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(true);
const mockInvoice = {
_id: '507f1f77bcf86cd799439011',
invoiceOrderItems: [{ orderItem: { _id: 'oi1' }, invoiceAmount: 50, invoiceQuantity: 1 }],
invoiceShipments: [{ shipment: { _id: 's1' }, invoiceAmount: 10 }],
};
getObject.mockResolvedValueOnce(mockInvoice);
// Mock getObject for individual orderItems and shipments
getObject.mockResolvedValueOnce({ _id: 'oi1', invoicedAmount: 0, invoicedQuantity: 0 });
getObject.mockResolvedValueOnce({ _id: 's1', invoicedAmount: 0 });
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'sent' } });
await postInvoiceRouteHandler(req, res);
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['draft'] }));
expect(editObject).toHaveBeenCalled(); // Should be called for items, shipments, and the invoice itself
expect(res.send).toHaveBeenCalled();
});
it('should fail if invoice is not in draft state', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(false);
await postInvoiceRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({ error: 'Invoice is not in draft state.' })
);
});
});
});

View File

@ -1,124 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
checkStates: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/finance/payment.schema.js', () => ({
paymentModel: { modelName: 'Payment' },
}));
jest.unstable_mockModule('../../../database/schemas/finance/invoice.schema.js', () => ({
invoiceModel: { modelName: 'Invoice' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listPaymentsRouteHandler,
getPaymentRouteHandler,
newPaymentRouteHandler,
postPaymentRouteHandler,
} = await import('../payments.js');
const { listObjects, getObject, editObject, newObject, checkStates } = await import(
'../../../database/database.js'
);
const { paymentModel } = await import('../../../database/schemas/finance/payment.schema.js');
const { invoiceModel } = await import('../../../database/schemas/finance/invoice.schema.js');
describe('Payment Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listPaymentsRouteHandler', () => {
it('should list payments', async () => {
const mockResult = [{ _id: '1', amount: 100 }];
listObjects.mockResolvedValue(mockResult);
await listPaymentsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newPaymentRouteHandler', () => {
it('should create a new payment with invoice data', async () => {
req.body = { invoice: 'inv123', amount: 100 };
const mockInvoice = { _id: 'inv123', vendor: { _id: 'v1' }, client: { _id: 'c1' } };
getObject.mockResolvedValueOnce(mockInvoice);
const mockPayment = { _id: 'pay456', ...req.body };
newObject.mockResolvedValue(mockPayment);
await newPaymentRouteHandler(req, res);
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: 'inv123' }));
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockPayment);
});
});
describe('postPaymentRouteHandler', () => {
it('should post a draft payment', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(true);
const mockPayment = { _id: '507f1f77bcf86cd799439011', invoice: 'inv123' };
getObject.mockResolvedValueOnce(mockPayment);
getObject.mockResolvedValueOnce({ _id: 'inv123' });
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'posted' } });
await postPaymentRouteHandler(req, res);
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['draft'] }));
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalled();
});
it('should fail if payment is not in draft state', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(false);
await postPaymentRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({ error: 'Payment is not in draft state.' })
);
});
});
});

View File

@ -1,554 +0,0 @@
import config from '../../config.js';
import { invoiceModel } from '../../database/schemas/finance/invoice.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
editObjects,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
checkStates,
} from '../../database/database.js';
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js';
const logger = log4js.getLogger('Invoices');
logger.level = config.server.logLevel;
export const listInvoicesRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const populateFields = [
{ path: 'to', strictPopulate: false, ref: 'client' },
{ path: 'from', strictPopulate: false, ref: 'vendor' },
{ path: 'order' },
];
const result = await listObjects({
model: invoiceModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: populateFields,
});
if (result?.error) {
logger.error('Error listing invoices.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of invoices (Page ${page}, Limit ${limit}). Count: ${result.length}`);
res.send(result);
};
export const listInvoicesByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const populateFields = ['to', 'from', 'order'];
const result = await listObjectsByProperties({
model: invoiceModel,
properties,
filter,
populate: populateFields,
masterFilter,
});
if (result?.error) {
logger.error('Error listing invoices.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of invoices. Count: ${result.length}`);
res.send(result);
};
export const getInvoiceRouteHandler = async (req, res) => {
const id = req.params.id;
const populateFields = [
{ path: 'to', strictPopulate: false },
{ path: 'from', strictPopulate: false },
{ path: 'order' },
{ path: 'invoiceOrderItems.taxRate' },
{ path: 'invoiceShipments.taxRate' },
{ path: 'invoiceOrderItems.orderItem' },
{ path: 'invoiceShipments.shipment' },
];
const result = await getObject({
model: invoiceModel,
id,
populate: populateFields,
});
if (result?.error) {
logger.warn(`Invoice not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retrieved invoice with ID: ${id}`);
res.send(result);
};
export const editInvoiceRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Invoice with ID: ${id}`);
const checkStatesResult = await checkStates({ model: invoiceModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking invoice states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Invoice is not in draft state.');
res.status(400).send({ error: 'Invoice is not in draft state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
vendor: req.body.vendor,
client: req.body.client,
invoiceType: req.body.invoiceType,
invoiceDate: req.body.invoiceDate,
dueAt: req.body.dueDate,
issuedAt: req.body.issuedAt,
orderType: req.body.orderType,
order: req.body.order,
invoiceOrderItems: req.body.invoiceOrderItems,
invoiceShipments: req.body.invoiceShipments,
from: req.body.from,
to: req.body.to,
};
// Create audit log before updating
const result = await editObject({
model: invoiceModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing invoice:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited invoice with ID: ${id}`);
res.send(result);
};
export const editMultipleInvoicesRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
vendor: update.vendor,
client: update.client,
invoiceType: update.invoiceType,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: invoiceModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing invoices:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} invoices`);
res.send(result);
};
export const newInvoiceRouteHandler = async (req, res) => {
const orderItems = await listObjects({
model: orderItemModel,
filter: { order: req.body.order, orderType: req.body.orderType },
});
const shipments = await listObjects({
model: shipmentModel,
filter: { order: req.body.order, orderType: req.body.orderType },
});
if (orderItems.error) {
logger.error('Error getting order items:', orderItems.error);
return res.status(orderItems.code).send(orderItems);
}
if (shipments.error) {
logger.error('Error getting shipments:', shipments.error);
return res.status(shipments.code).send(shipments);
}
const invoiceOrderItems = orderItems
.map((orderItem) => {
const invoicedAmountWithTax = orderItem.invoicedAmountWithTax || 0;
const totalAmountWithTax = orderItem.totalAmountWithTax || 0;
const invoicedAmount = orderItem.invoicedAmount || 0;
const totalAmount = orderItem.totalAmount || 0;
const quantity = (orderItem.quantity || 0) - (orderItem.invoicedQuantity || 0);
const taxRate = orderItem?.taxRate?._id;
if (invoicedAmountWithTax >= totalAmountWithTax || invoicedAmount >= totalAmount) {
return null;
}
var finalQuantity = quantity;
if (finalQuantity <= 0) {
finalQuantity = 1;
}
return {
orderItem: orderItem._id,
taxRate: taxRate,
invoiceAmountWithTax: totalAmountWithTax - invoicedAmountWithTax,
invoiceAmount: totalAmount - invoicedAmount,
invoiceQuantity: finalQuantity,
};
})
.filter((item) => item !== null);
const invoiceShipments = shipments
.map((shipment) => {
const invoicedAmount = shipment.invoicedAmount || 0;
const amountWithTax = shipment.amountWithTax || 0;
const invoicedAmountWithTax = shipment.invoicedAmountWithTax || 0;
const amount = shipment.amount || 0;
const taxRate = shipment?.taxRate || null;
if (invoicedAmountWithTax >= amountWithTax || invoicedAmount >= amount) {
return null;
}
return {
shipment: shipment._id,
taxRate: taxRate,
invoiceAmountWithTax: amountWithTax - invoicedAmountWithTax,
invoiceAmount: amount - invoicedAmount,
};
})
.filter((item) => item !== null);
const newData = {
updatedAt: new Date(),
vendor: req.body.vendor,
client: req.body.client,
issuedAt: req.body.issuedAt || new Date(),
dueAt: req.body.dueAt || new Date(),
orderType: req.body.orderType,
order: req.body.order,
totalAmount: 0,
totalAmountWithTax: 0,
totalTaxAmount: 0,
grandTotalAmount: 0,
shippingAmount: 0,
shippingAmountWithTax: 0,
invoiceOrderItems: invoiceOrderItems,
invoiceShipments: invoiceShipments,
from: req.body.from,
to: req.body.to,
};
const result = await newObject({
model: invoiceModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No invoice created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New invoice with ID: ${result._id}`);
res.send(result);
};
export const deleteInvoiceRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Invoice with ID: ${id}`);
const result = await deleteObject({
model: invoiceModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No invoice deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted invoice with ID: ${result._id}`);
res.send(result);
};
export const getInvoiceStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: invoiceModel });
if (result?.error) {
logger.error('Error fetching invoice stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Invoice stats:', result);
res.send(result);
};
export const getInvoiceHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: invoiceModel, from, to });
if (result?.error) {
logger.error('Error fetching invoice history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Invoice history:', result);
res.send(result);
};
export const acknowledgeInvoiceRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Invoice with ID: ${id}`);
const checkStatesResult = await checkStates({ model: invoiceModel, id, states: ['sent'] });
if (checkStatesResult.error) {
logger.error('Error checking invoice states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Invoice is not in sent state.');
res.status(400).send({ error: 'Invoice is not in sent state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'acknowledged' },
acknowledgedAt: new Date(),
};
const result = await editObject({
model: invoiceModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error acknowledging invoice:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Acknowledged invoice with ID: ${id}`);
res.send(result);
};
export const postInvoiceRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Invoice with ID: ${id}`);
const checkStatesResult = await checkStates({ model: invoiceModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking invoice states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Invoice is not in draft state.');
res.status(400).send({ error: 'Invoice is not in draft state.', code: 400 });
return;
}
const invoice = await getObject({
model: invoiceModel,
id,
});
const invoiceOrderItems = invoice.invoiceOrderItems;
const invoiceShipments = invoice.invoiceShipments;
for (const invoiceOrderItem of invoiceOrderItems) {
const orderItem = await getObject({
model: orderItemModel,
id: invoiceOrderItem.orderItem._id,
});
if (orderItem.error) {
logger.error('Error getting order item:', orderItem.error);
return res.status(orderItem.code).send(orderItem);
}
const invoiceQuantity = invoiceOrderItem.invoiceQuantity || 0;
const invoiceAmount = invoiceOrderItem.invoiceAmount || 0;
const invoiceAmountWithTax = invoiceOrderItem.invoiceAmountWithTax || 0;
const invoicedQuantity = orderItem.invoicedQuantity || 0;
const invoicedAmount = orderItem.invoicedAmount || 0;
const invoicedAmountWithTax = orderItem.invoicedAmountWithTax || 0;
var quantity = (orderItem.invoiceQuantity || 0) + invoiceQuantity;
if (quantity <= orderItem.quantity) {
quantity = orderItem.quantity;
}
const updateData = {
updatedAt: new Date(),
invoicedQuantity: invoicedQuantity + invoiceQuantity,
invoicedAmount: invoicedAmount + invoiceAmount,
invoicedAmountWithTax: invoicedAmountWithTax + invoiceAmountWithTax,
};
const result = await editObject({
model: orderItemModel,
id: orderItem._id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error updating order item:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Updated order item with ID: ${orderItem._id}`);
}
for (const invoiceShipment of invoiceShipments) {
const shipmentId = invoiceShipment.shipment._id || invoiceShipment.shipment;
const shipment = await getObject({
model: shipmentModel,
id: shipmentId,
});
if (shipment.error) {
logger.error('Error getting shipment:', shipment.error);
return res.status(shipment.code).send(shipment);
}
const invoiceAmount = invoiceShipment.invoiceAmount || 0;
const invoiceAmountWithTax = invoiceShipment.invoiceAmountWithTax || 0;
const invoicedAmount = shipment.invoicedAmount || 0;
const invoicedAmountWithTax = shipment.invoicedAmountWithTax || 0;
const updateData = {
updatedAt: new Date(),
invoicedAmount: invoicedAmount + invoiceAmount,
invoicedAmountWithTax: invoicedAmountWithTax + invoiceAmountWithTax,
};
const result = await editObject({
model: shipmentModel,
id: shipment._id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error updating shipment:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Updated shipment with ID: ${shipment._id}`);
}
if (invoiceOrderItems.error) {
logger.error('Error getting invoice order items:', invoiceOrderItems.error);
return res.status(invoiceOrderItems.code).send(invoiceOrderItems);
}
if (invoiceShipments.error) {
logger.error('Error getting invoice shipments:', invoiceShipments.error);
return res.status(invoiceShipments.code).send(invoiceShipments);
}
const updateData = {
updatedAt: new Date(),
state: { type: 'sent' },
postedAt: new Date(),
};
const result = await editObject({
model: invoiceModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error posting invoice:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Posted invoice with ID: ${id}`);
res.send(result);
};
export const cancelInvoiceRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Invoice with ID: ${id}`);
const checkStatesResult = await checkStates({
model: invoiceModel,
id,
states: ['draft', 'sent'],
});
if (checkStatesResult.error) {
logger.error('Error checking invoice states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Invoice is not in a cancellable state.');
res.status(400).send({
error: 'Invoice is not in a cancellable state (must be draft or sent).',
code: 400,
});
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'cancelled' },
cancelledAt: new Date(),
};
const result = await editObject({
model: invoiceModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error cancelling invoice:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Cancelled invoice with ID: ${id}`);
res.send(result);
};

View File

@ -1,373 +0,0 @@
import config from '../../config.js';
import { paymentModel } from '../../database/schemas/finance/payment.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
editObjects,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
checkStates,
} from '../../database/database.js';
import { invoiceModel } from '../../database/schemas/finance/invoice.schema.js';
const logger = log4js.getLogger('Payments');
logger.level = config.server.logLevel;
export const listPaymentsRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const populateFields = ['vendor', 'client', 'invoice'];
const result = await listObjects({
model: paymentModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: populateFields,
});
if (result?.error) {
logger.error('Error listing payments.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of payments (Page ${page}, Limit ${limit}). Count: ${result.length}`);
res.send(result);
};
export const listPaymentsByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const populateFields = ['vendor', 'client', 'invoice'];
const result = await listObjectsByProperties({
model: paymentModel,
properties,
filter,
populate: populateFields,
masterFilter,
});
if (result?.error) {
logger.error('Error listing payments.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of payments. Count: ${result.length}`);
res.send(result);
};
export const getPaymentRouteHandler = async (req, res) => {
const id = req.params.id;
const populateFields = [
{ path: 'vendor' },
{ path: 'client' },
{ path: 'invoice' },
];
const result = await getObject({
model: paymentModel,
id,
populate: populateFields,
});
if (result?.error) {
logger.warn(`Payment not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retrieved payment with ID: ${id}`);
res.send(result);
};
export const editPaymentRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Payment with ID: ${id}`);
const checkStatesResult = await checkStates({ model: paymentModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking payment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Payment is not in draft state.');
res.status(400).send({ error: 'Payment is not in draft state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
vendor: req.body.vendor,
client: req.body.client,
invoice: req.body.invoice,
amount: req.body.amount,
paymentDate: req.body.paymentDate,
paymentMethod: req.body.paymentMethod,
notes: req.body.notes,
};
// Create audit log before updating
const result = await editObject({
model: paymentModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing payment:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited payment with ID: ${id}`);
res.send(result);
};
export const editMultiplePaymentsRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
vendor: update.vendor,
client: update.client,
invoice: update.invoice,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: paymentModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing payments:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} payments`);
res.send(result);
};
export const newPaymentRouteHandler = async (req, res) => {
// Get invoice to populate vendor/client
const invoice = await getObject({
model: invoiceModel,
id: req.body.invoice,
populate: [{ path: 'vendor' }, { path: 'client' }],
});
if (invoice.error) {
logger.error('Error getting invoice:', invoice.error);
return res.status(invoice.code).send(invoice);
}
const newData = {
updatedAt: new Date(),
vendor: invoice.vendor?._id || req.body.vendor,
client: invoice.client?._id || req.body.client,
invoice: req.body.invoice,
amount: req.body.amount || 0,
paymentDate: req.body.paymentDate || new Date(),
paymentMethod: req.body.paymentMethod,
notes: req.body.notes,
};
const result = await newObject({
model: paymentModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No payment created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New payment with ID: ${result._id}`);
res.send(result);
};
export const deletePaymentRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Payment with ID: ${id}`);
const result = await deleteObject({
model: paymentModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No payment deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted payment with ID: ${result._id}`);
res.send(result);
};
export const getPaymentStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: paymentModel });
if (result?.error) {
logger.error('Error fetching payment stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Payment stats:', result);
res.send(result);
};
export const getPaymentHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: paymentModel, from, to });
if (result?.error) {
logger.error('Error fetching payment history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Payment history:', result);
res.send(result);
};
export const postPaymentRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Payment with ID: ${id}`);
const checkStatesResult = await checkStates({ model: paymentModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking payment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Payment is not in draft state.');
res.status(400).send({ error: 'Payment is not in draft state.', code: 400 });
return;
}
const payment = await getObject({
model: paymentModel,
id,
populate: [{ path: 'invoice' }],
});
if (payment.error) {
logger.error('Error getting payment:', payment.error);
return res.status(payment.code).send(payment);
}
// Update invoice paid amounts if needed
if (payment.invoice) {
const invoice = await getObject({
model: invoiceModel,
id: payment.invoice._id || payment.invoice,
});
if (!invoice.error) {
// You can add logic here to update invoice paid amounts
// This is a simplified version - adjust based on your business logic
}
}
const updateData = {
updatedAt: new Date(),
state: { type: 'posted' },
postedAt: new Date(),
};
const result = await editObject({
model: paymentModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error posting payment:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Posted payment with ID: ${id}`);
res.send(result);
};
export const cancelPaymentRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Payment with ID: ${id}`);
const checkStatesResult = await checkStates({
model: paymentModel,
id,
states: ['draft', 'posted'],
});
if (checkStatesResult.error) {
logger.error('Error checking payment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Payment is not in a cancellable state.');
res.status(400).send({
error: 'Payment is not in a cancellable state (must be draft or posted).',
code: 400,
});
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'cancelled' },
cancelledAt: new Date(),
};
const result = await editObject({
model: paymentModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error cancelling payment:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Cancelled payment with ID: ${id}`);
res.send(result);
};

View File

@ -1,86 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/inventory/filamentstock.schema.js', () => ({
filamentStockModel: { modelName: 'FilamentStock' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listFilamentStocksRouteHandler,
getFilamentStockRouteHandler,
newFilamentStockRouteHandler,
} = await import('../filamentstocks.js');
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
const { filamentStockModel } = await import(
'../../../database/schemas/inventory/filamentstock.schema.js'
);
describe('Filament Stock Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listFilamentStocksRouteHandler', () => {
it('should list filament stocks', async () => {
const mockResult = [{ _id: '1', currentWeight: 500 }];
listObjects.mockResolvedValue(mockResult);
await listFilamentStocksRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: filamentStockModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newFilamentStockRouteHandler', () => {
it('should create a new filament stock', async () => {
req.body = { filament: 'filament123', currentWeight: 1000 };
const mockStock = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockStock);
await newFilamentStockRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockStock);
});
});
});

View File

@ -1,100 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/inventory/orderitem.schema.js', () => ({
orderItemModel: { modelName: 'OrderItem' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listOrderItemsRouteHandler,
getOrderItemRouteHandler,
newOrderItemRouteHandler,
editOrderItemRouteHandler,
deleteOrderItemRouteHandler,
} = await import('../orderitems.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { orderItemModel } = await import('../../../database/schemas/inventory/orderitem.schema.js');
describe('Order Item Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listOrderItemsRouteHandler', () => {
it('should list order items', async () => {
const mockResult = [{ _id: '1', name: 'Order Item 1' }];
listObjects.mockResolvedValue(mockResult);
await listOrderItemsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: orderItemModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newOrderItemRouteHandler', () => {
it('should create a new order item', async () => {
req.body = { name: 'New Item', quantity: 10, order: 'order123' };
const mockItem = { _id: '456', ...req.body, state: { type: 'draft' } };
newObject.mockResolvedValue(mockItem);
await newOrderItemRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockItem);
});
});
describe('editOrderItemRouteHandler', () => {
it('should update an order item', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { quantity: 20 };
const mockResult = { _id: '507f1f77bcf86cd799439011', quantity: 20 };
editObject.mockResolvedValue(mockResult);
await editOrderItemRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,88 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/inventory/partstock.schema.js', () => ({
partStockModel: { modelName: 'PartStock' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listPartStocksRouteHandler,
getPartStockRouteHandler,
newPartStockRouteHandler,
editPartStockRouteHandler,
deletePartStockRouteHandler,
} = await import('../partstocks.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { partStockModel } = await import('../../../database/schemas/inventory/partstock.schema.js');
describe('Part Stock Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listPartStocksRouteHandler', () => {
it('should list part stocks', async () => {
const mockResult = [{ _id: '1', currentQuantity: 100 }];
listObjects.mockResolvedValue(mockResult);
await listPartStocksRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: partStockModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newPartStockRouteHandler', () => {
it('should create a new part stock', async () => {
req.body = { part: 'part123', currentQuantity: 50 };
const mockStock = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockStock);
await newPartStockRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockStock);
});
});
});

View File

@ -1,122 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
checkStates: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/inventory/purchaseorder.schema.js', () => ({
purchaseOrderModel: { modelName: 'PurchaseOrder' },
}));
jest.unstable_mockModule('../../../database/schemas/inventory/orderitem.schema.js', () => ({
orderItemModel: { modelName: 'OrderItem' },
}));
jest.unstable_mockModule('../../../database/schemas/inventory/shipment.schema.js', () => ({
shipmentModel: { modelName: 'Shipment' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listPurchaseOrdersRouteHandler,
getPurchaseOrderRouteHandler,
newPurchaseOrderRouteHandler,
postPurchaseOrderRouteHandler,
} = await import('../purchaseorders.js');
const { listObjects, getObject, editObject, newObject, checkStates } = await import(
'../../../database/database.js'
);
const { purchaseOrderModel } = await import(
'../../../database/schemas/inventory/purchaseorder.schema.js'
);
describe('Purchase Order Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listPurchaseOrdersRouteHandler', () => {
it('should list purchase orders', async () => {
const mockResult = [{ _id: '1', reference: 'PO-001' }];
listObjects.mockResolvedValue(mockResult);
await listPurchaseOrdersRouteHandler(req, res);
expect(listObjects).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('postPurchaseOrderRouteHandler', () => {
it('should post a draft purchase order and update items/shipments', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(true);
listObjects.mockResolvedValueOnce([
{ _id: 'oi1', state: { type: 'draft' }, shipment: 's1', _reference: 'ITEM1' },
]);
listObjects.mockResolvedValueOnce([
{ _id: 's1', state: { type: 'draft' }, _reference: 'SHIP1' },
]);
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'sent' } });
await postPurchaseOrderRouteHandler(req, res);
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['draft'] }));
expect(editObject).toHaveBeenCalledTimes(3);
expect(res.send).toHaveBeenCalled();
});
it('should fail if an order item is not in draft state', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(true);
listObjects.mockResolvedValueOnce([
{ _id: 'oi1', state: { type: 'ordered' }, _reference: 'ITEM1' },
]);
listObjects.mockResolvedValueOnce([]);
await postPurchaseOrderRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({ error: 'Order item ITEM1 not in draft state.' })
);
});
});
});

View File

@ -1,111 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
checkStates: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/inventory/shipment.schema.js', () => ({
shipmentModel: { modelName: 'Shipment' },
}));
jest.unstable_mockModule('../../../database/schemas/inventory/orderitem.schema.js', () => ({
orderItemModel: { modelName: 'OrderItem' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listShipmentsRouteHandler,
getShipmentRouteHandler,
newShipmentRouteHandler,
shipShipmentRouteHandler,
} = await import('../shipments.js');
const { listObjects, getObject, editObject, newObject, checkStates } = await import(
'../../../database/database.js'
);
const { shipmentModel } = await import('../../../database/schemas/inventory/shipment.schema.js');
const { orderItemModel } = await import('../../../database/schemas/inventory/orderitem.schema.js');
describe('Shipment Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listShipmentsRouteHandler', () => {
it('should list shipments', async () => {
const mockResult = [{ _id: '1', trackingNumber: 'TRACK123' }];
listObjects.mockResolvedValue(mockResult);
await listShipmentsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: shipmentModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newShipmentRouteHandler', () => {
it('should create a new shipment', async () => {
req.body = { order: 'order123', trackingNumber: 'TRACK456' };
const mockShipment = { _id: '456', ...req.body, state: { type: 'draft' } };
newObject.mockResolvedValue(mockShipment);
await newShipmentRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockShipment);
});
});
describe('shipShipmentRouteHandler', () => {
it('should ship a planned shipment and update order items', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(true);
listObjects.mockResolvedValue([
{ _id: 'oi1', state: { type: 'ordered' } },
{ _id: 'oi2', state: { type: 'ordered' } },
]);
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'shipped' } });
await shipShipmentRouteHandler(req, res);
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['planned'] }));
expect(editObject).toHaveBeenCalledTimes(3); // 2 order items + 1 shipment
expect(res.send).toHaveBeenCalled();
});
});
});

View File

@ -1,91 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../utils.js', () => ({
getAuditLogs: jest.fn(),
}));
jest.unstable_mockModule('../../../database/database.js', () => ({
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/inventory/stockaudit.schema.js', () => ({
stockAuditModel: {
modelName: 'StockAudit',
aggregate: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
},
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listStockAuditsRouteHandler,
getStockAuditRouteHandler,
newStockAuditRouteHandler,
} = await import('../stockaudits.js');
const { getAuditLogs } = await import('../../../utils.js');
const { stockAuditModel } = await import('../../../database/schemas/inventory/stockaudit.schema.js');
describe('Stock Audit Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listStockAuditsRouteHandler', () => {
it('should list stock audits', async () => {
const mockResult = [{ _id: '1', type: 'full' }];
stockAuditModel.aggregate.mockResolvedValue(mockResult);
await listStockAuditsRouteHandler(req, res);
expect(stockAuditModel.aggregate).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('getStockAuditRouteHandler', () => {
it('should get a stock audit by ID with audit logs', async () => {
req.params.id = '507f1f77bcf86cd799439011';
const mockAudit = { _id: '507f1f77bcf86cd799439011', type: 'full', _doc: {} };
stockAuditModel.findOne.mockReturnValue({
populate: jest.fn().mockReturnValue({
populate: jest.fn().mockReturnValue({
populate: jest.fn().mockResolvedValue(mockAudit),
}),
}),
});
getAuditLogs.mockResolvedValue([]);
await getStockAuditRouteHandler(req, res);
expect(getAuditLogs).toHaveBeenCalled();
expect(res.send).toHaveBeenCalled();
});
});
});

View File

@ -1,84 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/inventory/stockevent.schema.js', () => ({
stockEventModel: { modelName: 'StockEvent' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listStockEventsRouteHandler,
getStockEventRouteHandler,
newStockEventRouteHandler,
} = await import('../stockevents.js');
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
const { stockEventModel } = await import('../../../database/schemas/inventory/stockevent.schema.js');
describe('Stock Event Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listStockEventsRouteHandler', () => {
it('should list stock events', async () => {
const mockResult = [{ _id: '1', type: 'adjustment' }];
listObjects.mockResolvedValue(mockResult);
await listStockEventsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: stockEventModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newStockEventRouteHandler', () => {
it('should create a new stock event', async () => {
req.body = { type: 'adjustment', value: 10 };
const mockEvent = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockEvent);
await newStockEventRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockEvent);
});
});
});

View File

@ -7,7 +7,6 @@ import {
listObjects, listObjects,
getObject, getObject,
editObject, editObject,
editObjects,
newObject, newObject,
listObjectsByProperties, listObjectsByProperties,
getModelStats, getModelStats,
@ -115,32 +114,6 @@ export const editFilamentStockRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
export const editMultipleFilamentStocksRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: filamentStockModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing filament stocks:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} filament stocks`);
res.send(result);
};
export const newFilamentStockRouteHandler = async (req, res) => { export const newFilamentStockRouteHandler = async (req, res) => {
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),

View File

@ -7,7 +7,6 @@ import {
listObjects, listObjects,
getObject, getObject,
editObject, editObject,
editObjects,
newObject, newObject,
listObjectsByProperties, listObjectsByProperties,
getModelStats, getModelStats,
@ -44,10 +43,6 @@ export const listOrderItemsRouteHandler = async (
path: 'taxRate', path: 'taxRate',
strictPopulate: false, strictPopulate: false,
}, },
{
path: 'shipment',
strictPopulate: false,
},
{ {
path: 'item', path: 'item',
populate: { path: 'costTaxRate', strictPopulate: false }, populate: { path: 'costTaxRate', strictPopulate: false },
@ -135,7 +130,6 @@ export const editOrderItemRouteHandler = async (req, res) => {
const updateData = { const updateData = {
updatedAt: new Date(), updatedAt: new Date(),
name: req.body.name,
itemType: req.body.itemType, itemType: req.body.itemType,
item: req.body.item, item: req.body.item,
orderType: req.body.orderType, orderType: req.body.orderType,
@ -144,7 +138,6 @@ export const editOrderItemRouteHandler = async (req, res) => {
itemAmount: req.body.itemAmount, itemAmount: req.body.itemAmount,
quantity: req.body.quantity, quantity: req.body.quantity,
totalAmount: req.body.totalAmount, totalAmount: req.body.totalAmount,
shipment: req.body.shipment,
taxRate: req.body.taxRate, taxRate: req.body.taxRate,
totalAmountWithTax: req.body.totalAmountWithTax, totalAmountWithTax: req.body.totalAmountWithTax,
}; };
@ -167,48 +160,9 @@ export const editOrderItemRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
export const editMultipleOrderItemsRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
name: update.name,
itemType: update.itemType,
item: update.item,
orderType: update.orderType,
order: update.order,
syncAmount: update.syncAmount,
itemAmount: update.itemAmount,
quantity: update.quantity,
totalAmount: update.totalAmount,
shipment: update.shipment,
taxRate: update.taxRate,
totalAmountWithTax: update.totalAmountWithTax,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: orderItemModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing order items:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} order items`);
res.send(result);
};
export const newOrderItemRouteHandler = async (req, res) => { export const newOrderItemRouteHandler = async (req, res) => {
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),
name: req.body.name,
purchaseOrder: req.body.purchaseOrder, purchaseOrder: req.body.purchaseOrder,
state: { type: 'draft' }, state: { type: 'draft' },
itemType: req.body.itemType, itemType: req.body.itemType,
@ -221,7 +175,6 @@ export const newOrderItemRouteHandler = async (req, res) => {
totalAmount: req.body.totalAmount, totalAmount: req.body.totalAmount,
taxRate: req.body.taxRate, taxRate: req.body.taxRate,
totalAmountWithTax: req.body.totalAmountWithTax, totalAmountWithTax: req.body.totalAmountWithTax,
shipment: req.body.shipment,
}; };
const result = await newObject({ const result = await newObject({
model: orderItemModel, model: orderItemModel,

View File

@ -7,7 +7,6 @@ import {
listObjects, listObjects,
getObject, getObject,
editObject, editObject,
editObjects,
newObject, newObject,
listObjectsByProperties, listObjectsByProperties,
getModelStats, getModelStats,
@ -115,32 +114,6 @@ export const editPartStockRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
export const editMultiplePartStocksRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: partStockModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing part stocks:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} part stocks`);
res.send(result);
};
export const newPartStockRouteHandler = async (req, res) => { export const newPartStockRouteHandler = async (req, res) => {
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),

View File

@ -7,15 +7,11 @@ import {
listObjects, listObjects,
getObject, getObject,
editObject, editObject,
editObjects,
newObject, newObject,
listObjectsByProperties, listObjectsByProperties,
getModelStats, getModelStats,
getModelHistory, getModelHistory,
checkStates,
} from '../../database/database.js'; } from '../../database/database.js';
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js';
const logger = log4js.getLogger('Purchase Orders'); const logger = log4js.getLogger('Purchase Orders');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
@ -99,20 +95,6 @@ export const editPurchaseOrderRouteHandler = async (req, res) => {
logger.trace(`Purchase Order with ID: ${id}`); logger.trace(`Purchase Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking purchase order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Purchase order is not in draft state.');
res.status(400).send({ error: 'Purchase order is not in draft state.', code: 400 });
return;
}
const updateData = { const updateData = {
updatedAt: new Date(), updatedAt: new Date(),
vendor: req.body.vendor, vendor: req.body.vendor,
@ -136,40 +118,10 @@ export const editPurchaseOrderRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
export const editMultiplePurchaseOrdersRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
vendor: update.vendor,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: purchaseOrderModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing purchase orders:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} purchase orders`);
res.send(result);
};
export const newPurchaseOrderRouteHandler = async (req, res) => { export const newPurchaseOrderRouteHandler = async (req, res) => {
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),
vendor: req.body.vendor, vendor: req.body.vendor,
totalAmount: 0,
totalAmountWithTax: 0,
totalTaxAmount: 0,
}; };
const result = await newObject({ const result = await newObject({
model: purchaseOrderModel, model: purchaseOrderModel,
@ -228,232 +180,3 @@ export const getPurchaseOrderHistoryRouteHandler = async (req, res) => {
logger.trace('Purchase order history:', result); logger.trace('Purchase order history:', result);
res.send(result); res.send(result);
}; };
export const postPurchaseOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Purchase Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking purchase order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Purchase order is not in draft state.');
res.status(400).send({ error: 'Purchase order is not in draft state.', code: 400 });
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { order: id, orderType: 'purchaseOrder' },
pagination: false,
});
const shipmentsResult = await listObjects({
model: shipmentModel,
filter: { order: id, orderType: 'purchaseOrder' },
pagination: false,
});
for (const orderItem of orderItemsResult) {
if (orderItem.state.type != 'draft') {
logger.warn(`Order item ${orderItem._id} is not in draft state.`);
return res
.status(400)
.send({ error: `Order item ${orderItem._reference} not in draft state.`, code: 400 });
}
if (!orderItem?.shipment || orderItem?.shipment == null) {
logger.warn(`Order item ${orderItem._id} does not have a shipment.`);
return res
.status(400)
.send({ error: `Order item ${orderItem._reference} does not have a shipment.`, code: 400 });
}
}
for (const shipment of shipmentsResult) {
if (shipment.state.type != 'draft') {
logger.warn(`Shipment ${shipment._id} is not in draft state.`);
return res
.status(400)
.send({ error: `Shipment ${shipment._reference} not in draft state.`, code: 400 });
}
}
for (const orderItem of orderItemsResult) {
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
state: { type: 'ordered' },
orderedAt: new Date(),
},
user: req.user,
});
}
for (const shipment of shipmentsResult) {
await editObject({
model: shipmentModel,
id: shipment._id,
updateData: {
state: { type: 'planned' },
},
user: req.user,
});
}
const updateData = {
updatedAt: new Date(),
state: { type: 'sent' },
postedAt: new Date(),
};
const result = await editObject({
model: purchaseOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error posting purchase order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Posted purchase order with ID: ${id}`);
res.send(result);
};
export const acknowledgePurchaseOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Purchase Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['sent'] });
if (checkStatesResult.error) {
logger.error('Error checking purchase order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Purchase order is not in sent state.');
res.status(400).send({ error: 'Purchase order is not in sent state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'acknowledged' },
acknowledgedAt: new Date(),
};
const result = await editObject({
model: purchaseOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error acknowledging purchase order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Acknowledged purchase order with ID: ${id}`);
res.send(result);
};
export const cancelPurchaseOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Purchase Order with ID: ${id}`);
const checkStatesResult = await checkStates({
model: purchaseOrderModel,
id,
states: ['sent', 'acknowledged', 'partiallyShipped', 'shipped', 'partiallyReceived'],
});
if (checkStatesResult.error) {
logger.error('Error checking purchase order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Purchase order is not in a cancellable state.');
res.status(400).send({
error: 'Purchase order is not in a cancellable state (must be draft, sent, or acknowledged).',
code: 400,
});
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { order: id, orderType: 'purchaseOrder' },
pagination: false,
});
const shipmentsResult = await listObjects({
model: shipmentModel,
filter: { order: id, orderType: 'purchaseOrder' },
pagination: false,
});
const allowedOrderItemStates = ['ordered', 'shipped'];
const allowedShipmentStates = ['shipped', 'planned'];
for (const orderItem of orderItemsResult) {
if (allowedOrderItemStates.includes(orderItem.state.type)) {
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
state: { type: 'cancelled' },
},
user: req.user,
});
}
}
for (const shipment of shipmentsResult) {
if (allowedShipmentStates.includes(shipment.state.type)) {
await editObject({
model: shipmentModel,
id: shipment._id,
updateData: {
state: { type: 'cancelled' },
},
user: req.user,
});
}
}
const updateData = {
updatedAt: new Date(),
state: { type: 'cancelled' },
cancelledAt: new Date(),
};
const result = await editObject({
model: purchaseOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error cancelling purchase order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Cancelled purchase order with ID: ${id}`);
res.send(result);
};

View File

@ -7,16 +7,13 @@ import {
listObjects, listObjects,
getObject, getObject,
editObject, editObject,
editObjects,
newObject, newObject,
listObjectsByProperties, listObjectsByProperties,
getModelStats, getModelStats,
getModelHistory, getModelHistory,
checkStates,
} from '../../database/database.js'; } from '../../database/database.js';
const logger = log4js.getLogger('Shipments'); const logger = log4js.getLogger('Shipments');
logger.level = config.server.logLevel; logger.level = config.server.logLevel;
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
export const listShipmentsRouteHandler = async ( export const listShipmentsRouteHandler = async (
req, req,
@ -38,7 +35,7 @@ export const listShipmentsRouteHandler = async (
search, search,
sort, sort,
order, order,
populate: ['order', 'courierService', 'taxRate'], populate: ['purchaseOrder', 'vendor', 'courierService'],
}); });
if (result?.error) { if (result?.error) {
@ -62,7 +59,7 @@ export const listShipmentsByPropertiesRouteHandler = async (
model: shipmentModel, model: shipmentModel,
properties, properties,
filter, filter,
populate: ['courierService'], populate: ['purchaseOrder', 'vendor', 'courierService'],
masterFilter, masterFilter,
}); });
@ -81,7 +78,7 @@ export const getShipmentRouteHandler = async (req, res) => {
const result = await getObject({ const result = await getObject({
model: shipmentModel, model: shipmentModel,
id, id,
populate: ['order', 'courierService', 'taxRate'], populate: ['purchaseOrder', 'vendor', 'courierService', 'items.item', 'items.taxRate'],
}); });
if (result?.error) { if (result?.error) {
logger.warn(`Shipment not found with supplied id.`); logger.warn(`Shipment not found with supplied id.`);
@ -99,13 +96,15 @@ export const editShipmentRouteHandler = async (req, res) => {
const updateData = { const updateData = {
updatedAt: new Date(), updatedAt: new Date(),
orderType: req.body.orderType, purchaseOrder: req.body.purchaseOrder,
order: req.body.order, vendor: req.body.vendor,
courierService: req.body.courierService, courierService: req.body.courierService,
trackingNumber: req.body.trackingNumber, trackingNumber: req.body.trackingNumber,
amount: req.body.amount, shippedDate: req.body.shippedDate,
amountWithTax: req.body.amountWithTax, expectedDeliveryDate: req.body.expectedDeliveryDate,
taxRate: req.body.taxRate, actualDeliveryDate: req.body.actualDeliveryDate,
state: req.body.state,
notes: req.body.notes,
}; };
// Create audit log before updating // Create audit log before updating
const result = await editObject({ const result = await editObject({
@ -126,53 +125,20 @@ export const editShipmentRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
export const editMultipleShipmentsRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
orderType: update.orderType,
order: update.order,
courierService: update.courierService,
trackingNumber: update.trackingNumber,
amount: update.amount,
amountWithTax: update.amountWithTax,
taxRate: update.taxRate,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: shipmentModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing shipments:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} shipments`);
res.send(result);
};
export const newShipmentRouteHandler = async (req, res) => { export const newShipmentRouteHandler = async (req, res) => {
const newData = { const newData = {
updatedAt: new Date(), updatedAt: new Date(),
orderType: req.body.orderType, purchaseOrder: req.body.purchaseOrder,
order: req.body.order, vendor: req.body.vendor,
courierService: req.body.courierService, courierService: req.body.courierService,
trackingNumber: req.body.trackingNumber, trackingNumber: req.body.trackingNumber,
amount: req.body.amount, items: req.body.items,
amountWithTax: req.body.amountWithTax, cost: req.body.cost,
taxRate: req.body.taxRate, shippedDate: req.body.shippedDate,
shippedAt: req.body.shippedAt, expectedDeliveryDate: req.body.expectedDeliveryDate,
expectedAt: req.body.expectedAt, actualDeliveryDate: req.body.actualDeliveryDate,
deliveredAt: req.body.deliveredAt, state: req.body.state,
state: { type: 'draft' }, notes: req.body.notes,
}; };
const result = await newObject({ const result = await newObject({
model: shipmentModel, model: shipmentModel,
@ -231,212 +197,3 @@ export const getShipmentHistoryRouteHandler = async (req, res) => {
logger.trace('Shipment history:', result); logger.trace('Shipment history:', result);
res.send(result); res.send(result);
}; };
export const shipShipmentRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Shipment with ID: ${id}`);
const checkStatesResult = await checkStates({ model: shipmentModel, id, states: ['planned'] });
if (checkStatesResult.error) {
logger.error('Error checking shipment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Shipment is not in planned state.');
res.status(400).send({ error: 'Shipment is not in planned state.', code: 400 });
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { shipment: id },
pagination: false,
});
if (orderItemsResult.error) {
logger.error('Error listing order items:', orderItemsResult.error);
res.status(orderItemsResult.code).send(orderItemsResult);
return;
}
for (const orderItem of orderItemsResult) {
if (orderItem.state.type != 'ordered') {
logger.error('Order item is not in ordered state.');
res.status(400).send({ error: 'Order item is not in ordered state.', code: 400 });
return;
}
}
for (const orderItem of orderItemsResult) {
await editObject({
model: orderItemModel,
id: orderItem._id,
user: req.user,
updateData: {
state: { type: 'shipped' },
receivedAt: new Date(),
},
});
}
const updateData = {
state: { type: 'shipped' },
shippedAt: new Date(),
};
const result = await editObject({ model: shipmentModel, id, updateData, user: req.user });
if (result.error) {
logger.error('Error shipping shipment:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Shipped shipment with ID: ${id}`);
res.send(result);
return;
};
export const receiveShipmentRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Shipment with ID: ${id}`);
const checkStatesResult = await checkStates({ model: shipmentModel, id, states: ['shipped'] });
if (checkStatesResult.error) {
logger.error('Error checking shipment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Shipment is not in shipped state.');
res.status(400).send({ error: 'Shipment is not in shipped state.', code: 400 });
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { shipment: id },
pagination: false,
});
if (orderItemsResult.error) {
logger.error('Error listing order items:', orderItemsResult.error);
res.status(orderItemsResult.code).send(orderItemsResult);
return;
}
for (const orderItem of orderItemsResult) {
if (orderItem.state.type != 'shipped') {
logger.error('Order item is not in shipped state.');
res.status(400).send({ error: 'Order item is not in shipped state.', code: 400 });
return;
}
}
for (const orderItem of orderItemsResult) {
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
state: { type: 'received' },
receivedAt: new Date(),
},
user: req.user,
});
}
const result = await editObject({
model: shipmentModel,
id,
updateData: {
state: { type: 'delivered' },
deliveredAt: new Date(),
},
user: req.user,
});
if (result.error) {
logger.error('Error receiving shipment:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Received shipment with ID: ${id}`);
res.send(result);
};
export const cancelShipmentRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Shipment with ID: ${id}`);
const checkStatesResult = await checkStates({
model: shipmentModel,
id,
states: ['planned', 'shipped'],
});
if (checkStatesResult.error) {
logger.error('Error checking shipment states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Shipment is not in a cancellable state.');
res.status(400).send({
error: 'Shipment is not in a cancellable state (must be planned or shipped).',
code: 400,
});
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { shipment: id },
pagination: false,
});
if (orderItemsResult.error) {
logger.error('Error listing order items:', orderItemsResult.error);
res.status(orderItemsResult.code).send(orderItemsResult);
return;
}
// Cancel related order items if they are in cancellable states
for (const orderItem of orderItemsResult) {
if (orderItem.state.type === 'draft' || orderItem.state.type === 'ordered') {
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
state: { type: 'cancelled' },
},
user: req.user,
});
}
}
const updateData = {
updatedAt: new Date(),
state: { type: 'cancelled' },
cancelledAt: new Date(),
};
const result = await editObject({
model: shipmentModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error cancelling shipment:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Cancelled shipment with ID: ${id}`);
res.send(result);
};

View File

@ -7,7 +7,6 @@ import {
listObjects, listObjects,
getObject, getObject,
editObject, editObject,
editObjects,
newObject, newObject,
listObjectsByProperties, listObjectsByProperties,
getModelStats, getModelStats,
@ -146,32 +145,6 @@ export const editStockEventRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
export const editMultipleStockEventsRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: stockEventModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing stock events:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} stock events`);
res.send(result);
};
export const deleteStockEventRouteHandler = async (req, res) => { export const deleteStockEventRouteHandler = async (req, res) => {
// Get ID from params // Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id); const id = new mongoose.Types.ObjectId(req.params.id);

View File

@ -1,87 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/auditlog.schema.js', () => ({
auditLogModel: {
modelName: 'AuditLog',
find: jest.fn(),
findOne: jest.fn(),
},
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const { listAuditLogsRouteHandler, getAuditLogRouteHandler } = await import('../auditlogs.js');
const { auditLogModel } = await import('../../../database/schemas/management/auditlog.schema.js');
describe('Audit Log Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listAuditLogsRouteHandler', () => {
it('should list audit logs', async () => {
const mockResult = [
{ _id: '1', operation: 'edit', parent: 'parent123', _doc: { parent: 'parent123' } },
];
auditLogModel.find.mockReturnValue({
sort: jest.fn().mockReturnThis(),
skip: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
populate: jest.fn().mockResolvedValue(mockResult),
});
await listAuditLogsRouteHandler(req, res);
expect(auditLogModel.find).toHaveBeenCalled();
expect(res.send).toHaveBeenCalled();
});
});
describe('getAuditLogRouteHandler', () => {
it('should get an audit log by ID', async () => {
req.params.id = '507f1f77bcf86cd799439011';
const mockLog = { _id: '507f1f77bcf86cd799439011', operation: 'edit' };
auditLogModel.findOne.mockReturnValue({
populate: jest.fn().mockReturnValue({
populate: jest.fn().mockReturnValue({
populate: jest.fn().mockResolvedValue(mockLog),
}),
}),
});
await getAuditLogRouteHandler(req, res);
expect(auditLogModel.findOne).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockLog);
});
});
});

View File

@ -1,84 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/courier.schema.js', () => ({
courierModel: { modelName: 'Courier' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listCouriersRouteHandler,
getCourierRouteHandler,
newCourierRouteHandler,
editCourierRouteHandler,
} = await import('../courier.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { courierModel } = await import('../../../database/schemas/management/courier.schema.js');
describe('Courier Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listCouriersRouteHandler', () => {
it('should list couriers', async () => {
const mockResult = [{ _id: '1', name: 'FedEx' }];
listObjects.mockResolvedValue(mockResult);
await listCouriersRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: courierModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newCourierRouteHandler', () => {
it('should create a new courier', async () => {
req.body = { name: 'DHL', email: 'contact@dhl.com' };
const mockCourier = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockCourier);
await newCourierRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockCourier);
});
});
});

View File

@ -1,88 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/courierservice.schema.js', () => ({
courierServiceModel: { modelName: 'CourierService' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listCourierServicesRouteHandler,
getCourierServiceRouteHandler,
newCourierServiceRouteHandler,
editCourierServiceRouteHandler,
} = await import('../courierservice.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { courierServiceModel } = await import(
'../../../database/schemas/management/courierservice.schema.js'
);
describe('Courier Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listCourierServicesRouteHandler', () => {
it('should list courier services', async () => {
const mockResult = [{ _id: '1', name: 'Express' }];
listObjects.mockResolvedValue(mockResult);
await listCourierServicesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: courierServiceModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newCourierServiceRouteHandler', () => {
it('should create a new courier service', async () => {
req.body = { courier: 'courier123', name: 'Express Delivery' };
const mockService = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockService);
await newCourierServiceRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockService);
});
});
});

View File

@ -1,85 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/documentjob.schema.js', () => ({
documentJobModel: { modelName: 'DocumentJob' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listDocumentJobsRouteHandler,
getDocumentJobRouteHandler,
newDocumentJobRouteHandler,
} = await import('../documentjobs.js');
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
const { documentJobModel } = await import(
'../../../database/schemas/management/documentjob.schema.js'
);
describe('Document Job Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listDocumentJobsRouteHandler', () => {
it('should list document jobs', async () => {
const mockResult = [{ _id: '1', state: { type: 'pending' } }];
listObjects.mockResolvedValue(mockResult);
await listDocumentJobsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: documentJobModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newDocumentJobRouteHandler', () => {
it('should create a new document job', async () => {
req.body = { documentTemplate: 'template123', documentPrinter: 'printer123' };
const mockJob = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockJob);
await newDocumentJobRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockJob);
});
});
});

View File

@ -1,85 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/documentprinter.schema.js', () => ({
documentPrinterModel: { modelName: 'DocumentPrinter' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listDocumentPrintersRouteHandler,
getDocumentPrinterRouteHandler,
newDocumentPrinterRouteHandler,
} = await import('../documentprinters.js');
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
const { documentPrinterModel } = await import(
'../../../database/schemas/management/documentprinter.schema.js'
);
describe('Document Printer Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listDocumentPrintersRouteHandler', () => {
it('should list document printers', async () => {
const mockResult = [{ _id: '1', name: 'HP LaserJet' }];
listObjects.mockResolvedValue(mockResult);
await listDocumentPrintersRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: documentPrinterModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newDocumentPrinterRouteHandler', () => {
it('should create a new document printer', async () => {
req.body = { name: 'Canon Printer', host: 'host123' };
const mockPrinter = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockPrinter);
await newDocumentPrinterRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockPrinter);
});
});
});

View File

@ -1,88 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/documentsize.schema.js', () => ({
documentSizeModel: { modelName: 'DocumentSize' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listDocumentSizesRouteHandler,
getDocumentSizeRouteHandler,
newDocumentSizeRouteHandler,
editDocumentSizeRouteHandler,
} = await import('../documentsizes.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { documentSizeModel } = await import(
'../../../database/schemas/management/documentsize.schema.js'
);
describe('Document Size Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listDocumentSizesRouteHandler', () => {
it('should list document sizes', async () => {
const mockResult = [{ _id: '1', name: 'A4', width: 210, height: 297 }];
listObjects.mockResolvedValue(mockResult);
await listDocumentSizesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: documentSizeModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newDocumentSizeRouteHandler', () => {
it('should create a new document size', async () => {
req.body = { name: 'Letter', width: 216, height: 279 };
const mockSize = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockSize);
await newDocumentSizeRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockSize);
});
});
});

View File

@ -1,88 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/documenttemplate.schema.js', () => ({
documentTemplateModel: { modelName: 'DocumentTemplate' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listDocumentTemplatesRouteHandler,
getDocumentTemplateRouteHandler,
newDocumentTemplateRouteHandler,
editDocumentTemplateRouteHandler,
} = await import('../documenttemplates.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { documentTemplateModel } = await import(
'../../../database/schemas/management/documenttemplate.schema.js'
);
describe('Document Template Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listDocumentTemplatesRouteHandler', () => {
it('should list document templates', async () => {
const mockResult = [{ _id: '1', name: 'Invoice Template' }];
listObjects.mockResolvedValue(mockResult);
await listDocumentTemplatesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: documentTemplateModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newDocumentTemplateRouteHandler', () => {
it('should create a new document template', async () => {
req.body = { name: 'New Template', documentSize: 'size123' };
const mockTemplate = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockTemplate);
await newDocumentTemplateRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockTemplate);
});
});
});

View File

@ -1,98 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/filament.schema.js', () => ({
filamentModel: { modelName: 'Filament' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listFilamentsRouteHandler,
getFilamentRouteHandler,
newFilamentRouteHandler,
editFilamentRouteHandler,
} = await import('../filaments.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { filamentModel } = await import('../../../database/schemas/management/filament.schema.js');
describe('Filament Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listFilamentsRouteHandler', () => {
it('should list filaments', async () => {
const mockResult = [{ _id: '1', name: 'Filament 1' }];
listObjects.mockResolvedValue(mockResult);
await listFilamentsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: filamentModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newFilamentRouteHandler', () => {
it('should create a new filament', async () => {
req.body = { name: 'PLA Red', diameter: 1.75, cost: 20 };
const mockFilament = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockFilament);
await newFilamentRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockFilament);
});
});
describe('editFilamentRouteHandler', () => {
it('should update a filament', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { name: 'Updated Filament', cost: 25 };
const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body };
editObject.mockResolvedValue(mockResult);
await editFilamentRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,123 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
flushFile: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/file.schema.js', () => ({
fileModel: { modelName: 'File' },
}));
jest.unstable_mockModule('../../../database/ceph.js', () => ({
uploadFile: jest.fn(),
downloadFile: jest.fn(),
deleteFile: jest.fn(),
BUCKETS: { FILES: 'test-bucket' },
}));
jest.unstable_mockModule('../../../utils.js', () => ({
getFileMeta: jest.fn(),
}));
jest.unstable_mockModule('multer', () => {
const mockMemoryStorage = jest.fn();
const mockMulter = jest.fn(() => ({
single: jest.fn(),
}));
mockMulter.memoryStorage = mockMemoryStorage;
return {
default: mockMulter,
memoryStorage: mockMemoryStorage,
};
});
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listFilesRouteHandler,
getFileRouteHandler,
editFileRouteHandler,
flushFileRouteHandler,
} = await import('../files.js');
const { listObjects, getObject, editObject, flushFile } = await import(
'../../../database/database.js'
);
const { fileModel } = await import('../../../database/schemas/management/file.schema.js');
describe('File Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listFilesRouteHandler', () => {
it('should list files', async () => {
const mockResult = [{ _id: '1', name: 'file.pdf' }];
listObjects.mockResolvedValue(mockResult);
await listFilesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: fileModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('getFileRouteHandler', () => {
it('should get a file by ID', async () => {
req.params.id = '123';
const mockFile = { _id: '123', name: 'test.pdf' };
getObject.mockResolvedValue(mockFile);
await getFileRouteHandler(req, res);
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: '123' }));
expect(res.send).toHaveBeenCalledWith(mockFile);
});
});
describe('flushFileRouteHandler', () => {
it('should flush/delete a file', async () => {
req.params.id = '507f1f77bcf86cd799439011';
const mockResult = { success: true };
flushFile.mockResolvedValue(mockResult);
await flushFileRouteHandler(req, res);
expect(flushFile).toHaveBeenCalledWith(
expect.objectContaining({ id: '507f1f77bcf86cd799439011' })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,99 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/host.schema.js', () => ({
hostModel: { modelName: 'Host' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listHostsRouteHandler,
getHostRouteHandler,
newHostRouteHandler,
editHostRouteHandler,
deleteHostRouteHandler,
} = await import('../hosts.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { hostModel } = await import('../../../database/schemas/management/host.schema.js');
describe('Host Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listHostsRouteHandler', () => {
it('should list hosts', async () => {
const mockResult = [{ _id: '1', name: 'Host 1' }];
listObjects.mockResolvedValue(mockResult);
await listHostsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: hostModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newHostRouteHandler', () => {
it('should create a new host', async () => {
req.body = { name: 'New Host', address: '192.168.1.100' };
const mockHost = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockHost);
await newHostRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockHost);
});
});
describe('editHostRouteHandler', () => {
it('should update a host', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { name: 'Updated Host' };
const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body };
editObject.mockResolvedValue(mockResult);
await editHostRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,75 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/schemas/management/material.schema.js', () => ({
materialModel: {
modelName: 'Material',
aggregate: jest.fn(),
findOne: jest.fn(),
updateOne: jest.fn(),
create: jest.fn(),
},
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listMaterialsRouteHandler,
getMaterialRouteHandler,
newMaterialRouteHandler,
} = await import('../materials.js');
const { materialModel } = await import('../../../database/schemas/management/material.schema.js');
describe('Material Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listMaterialsRouteHandler', () => {
it('should list materials', async () => {
const mockResult = [{ name: 'PLA' }];
materialModel.aggregate.mockResolvedValue(mockResult);
await listMaterialsRouteHandler(req, res);
expect(materialModel.aggregate).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('getMaterialRouteHandler', () => {
it('should get a material by ID', async () => {
req.params.id = '507f1f77bcf86cd799439011';
const mockMaterial = { _id: '507f1f77bcf86cd799439011', name: 'PLA' };
materialModel.findOne.mockResolvedValue(mockMaterial);
await getMaterialRouteHandler(req, res);
expect(materialModel.findOne).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockMaterial);
});
});
});

View File

@ -1,86 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/notetype.schema.js', () => ({
noteTypeModel: { modelName: 'NoteType' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listNoteTypesRouteHandler,
getNoteTypeRouteHandler,
newNoteTypeRouteHandler,
editNoteTypeRouteHandler,
} = await import('../notetypes.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { noteTypeModel } = await import('../../../database/schemas/management/notetype.schema.js');
describe('Note Type Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listNoteTypesRouteHandler', () => {
it('should list note types', async () => {
const mockResult = [{ _id: '1', name: 'General' }];
listObjects.mockResolvedValue(mockResult);
await listNoteTypesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: noteTypeModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newNoteTypeRouteHandler', () => {
it('should create a new note type', async () => {
req.body = { name: 'Important', color: '#ff0000' };
const mockNoteType = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockNoteType);
await newNoteTypeRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockNoteType);
});
});
});

View File

@ -1,124 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/part.schema.js', () => ({
partModel: { modelName: 'Part' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listPartsRouteHandler,
getPartRouteHandler,
newPartRouteHandler,
editPartRouteHandler,
deletePartRouteHandler,
} = await import('../parts.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { partModel } = await import('../../../database/schemas/management/part.schema.js');
describe('Part Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listPartsRouteHandler', () => {
it('should list parts', async () => {
const mockResult = [{ _id: '1', name: 'Part 1' }];
listObjects.mockResolvedValue(mockResult);
await listPartsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: partModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('getPartRouteHandler', () => {
it('should get a part by ID', async () => {
req.params.id = '123';
const mockPart = { _id: '123', name: 'Test Part' };
getObject.mockResolvedValue(mockPart);
await getPartRouteHandler(req, res);
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: '123' }));
expect(res.send).toHaveBeenCalledWith(mockPart);
});
});
describe('newPartRouteHandler', () => {
it('should create a new part', async () => {
req.body = { name: 'New Part', price: 10.99 };
const mockPart = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockPart);
await newPartRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockPart);
});
});
describe('editPartRouteHandler', () => {
it('should update a part', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { name: 'Updated Part', price: 15.99 };
const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body };
editObject.mockResolvedValue(mockResult);
await editPartRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('deletePartRouteHandler', () => {
it('should delete a part', async () => {
req.params.id = '507f1f77bcf86cd799439011';
const mockResult = { _id: '507f1f77bcf86cd799439011' };
deleteObject.mockResolvedValue(mockResult);
await deletePartRouteHandler(req, res);
expect(deleteObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,98 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/product.schema.js', () => ({
productModel: { modelName: 'Product' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listProductsRouteHandler,
getProductRouteHandler,
newProductRouteHandler,
editProductRouteHandler,
deleteProductRouteHandler,
} = await import('../products.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { productModel } = await import('../../../database/schemas/management/product.schema.js');
describe('Product Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listProductsRouteHandler', () => {
it('should list products', async () => {
const mockResult = [{ _id: '1', name: 'Product 1' }];
listObjects.mockResolvedValue(mockResult);
await listProductsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: productModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newProductRouteHandler', () => {
it('should create a new product', async () => {
req.body = { name: 'New Product', parts: [] };
const mockProduct = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockProduct);
await newProductRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockProduct);
});
});
describe('editProductRouteHandler', () => {
it('should update a product', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { name: 'Updated Product' };
const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body };
editObject.mockResolvedValue(mockResult);
await editProductRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,98 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/taxrate.schema.js', () => ({
taxRateModel: { modelName: 'TaxRate' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listTaxRatesRouteHandler,
getTaxRateRouteHandler,
newTaxRateRouteHandler,
editTaxRateRouteHandler,
} = await import('../taxrates.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { taxRateModel } = await import('../../../database/schemas/management/taxrate.schema.js');
describe('Tax Rate Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listTaxRatesRouteHandler', () => {
it('should list tax rates', async () => {
const mockResult = [{ _id: '1', name: 'GST', rate: 10 }];
listObjects.mockResolvedValue(mockResult);
await listTaxRatesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: taxRateModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newTaxRateRouteHandler', () => {
it('should create a new tax rate', async () => {
req.body = { name: 'VAT', rate: 20, rateType: 'percentage' };
const mockTaxRate = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockTaxRate);
await newTaxRateRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockTaxRate);
});
});
describe('editTaxRateRouteHandler', () => {
it('should update a tax rate', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { rate: 15 };
const mockResult = { _id: '507f1f77bcf86cd799439011', rate: 15 };
editObject.mockResolvedValue(mockResult);
await editTaxRateRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,86 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/taxrecord.schema.js', () => ({
taxRecordModel: { modelName: 'TaxRecord' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listTaxRecordsRouteHandler,
getTaxRecordRouteHandler,
newTaxRecordRouteHandler,
editTaxRecordRouteHandler,
} = await import('../taxrecords.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { taxRecordModel } = await import('../../../database/schemas/management/taxrecord.schema.js');
describe('Tax Record Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listTaxRecordsRouteHandler', () => {
it('should list tax records', async () => {
const mockResult = [{ _id: '1', amount: 100, taxAmount: 10 }];
listObjects.mockResolvedValue(mockResult);
await listTaxRecordsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: taxRecordModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newTaxRecordRouteHandler', () => {
it('should create a new tax record', async () => {
req.body = { taxRate: 'rate123', amount: 100, taxAmount: 10 };
const mockRecord = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockRecord);
await newTaxRecordRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockRecord);
});
});
});

View File

@ -1,126 +0,0 @@
import { jest } from '@jest/globals';
// Mock dependencies MUST be done before importing the module under test
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
listObjectsByProperties: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/user.schema.js', () => ({
userModel: { findOne: jest.fn() },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
// Now import the modules
const { listUsersRouteHandler, getUserRouteHandler, editUserRouteHandler } = await import(
'../users.js'
);
const { listObjects, getObject, editObject } = await import('../../../database/database.js');
const { userModel } = await import('../../../database/schemas/management/user.schema.js');
describe('User Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listUsersRouteHandler', () => {
it('should list users and send response', async () => {
const mockResult = [{ id: '1', name: 'User 1' }];
listObjects.mockResolvedValue(mockResult);
await listUsersRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({
model: userModel,
})
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
it('should handle errors from listObjects', async () => {
const mockError = { error: 'Database error', code: 500 };
listObjects.mockResolvedValue(mockError);
await listUsersRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.send).toHaveBeenCalledWith(mockError);
});
});
describe('getUserRouteHandler', () => {
it('should get a user by id and send response', async () => {
req.params.id = '123';
const mockUser = { id: '123', name: 'Test User' };
getObject.mockResolvedValue(mockUser);
await getUserRouteHandler(req, res);
expect(getObject).toHaveBeenCalledWith(
expect.objectContaining({
model: userModel,
id: '123',
})
);
expect(res.send).toHaveBeenCalledWith(mockUser);
});
it('should handle user not found', async () => {
req.params.id = 'invalid';
const mockError = { error: 'Not found', code: 404 };
getObject.mockResolvedValue(mockError);
await getUserRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith(mockError);
});
});
describe('editUserRouteHandler', () => {
it('should edit a user and send response', async () => {
req.params.id = '507f1f77bcf86cd799439011'; // valid mongodb id format
req.body = { name: 'New Name' };
const mockResult = { id: '507f1f77bcf86cd799439011', name: 'New Name' };
editObject.mockResolvedValue(mockResult);
await editUserRouteHandler(req, res);
expect(editObject).toHaveBeenCalledWith(
expect.objectContaining({
model: userModel,
updateData: expect.objectContaining({ name: 'New Name' }),
})
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,99 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/management/vendor.schema.js', () => ({
vendorModel: { modelName: 'Vendor' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listVendorsRouteHandler,
getVendorRouteHandler,
newVendorRouteHandler,
editVendorRouteHandler,
deleteVendorRouteHandler,
} = await import('../vendors.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { vendorModel } = await import('../../../database/schemas/management/vendor.schema.js');
describe('Vendor Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listVendorsRouteHandler', () => {
it('should list vendors', async () => {
const mockResult = [{ _id: '1', name: 'Vendor 1' }];
listObjects.mockResolvedValue(mockResult);
await listVendorsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: vendorModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newVendorRouteHandler', () => {
it('should create a new vendor', async () => {
req.body = { name: 'New Vendor', email: 'vendor@example.com' };
const mockVendor = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockVendor);
await newVendorRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockVendor);
});
});
describe('editVendorRouteHandler', () => {
it('should update a vendor', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { name: 'Updated Vendor' };
const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body };
editObject.mockResolvedValue(mockResult);
await editVendorRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -36,6 +36,8 @@ export const listAuditLogsRouteHandler = async (
delete filter['owner._id']; delete filter['owner._id'];
} }
console.log('sort: ', { [sort]: sortOrder });
// Use find with population and filter // Use find with population and filter
let query = auditLogModel let query = auditLogModel
.find(filter) .find(filter)

View File

@ -7,7 +7,6 @@ import {
listObjects, listObjects,
listObjectsByProperties, listObjectsByProperties,
editObject, editObject,
editObjects,
newObject, newObject,
getModelStats, getModelStats,
getModelHistory, getModelHistory,
@ -59,16 +58,7 @@ export const listFilamentsByPropertiesRouteHandler = async (
model: filamentModel, model: filamentModel,
properties, properties,
filter, filter,
populate: [ populate: 'vendor',
{
path: 'vendor',
from: 'vendors',
},
{
path: 'costTaxRate',
from: 'taxrates',
},
],
}); });
if (result?.error) { if (result?.error) {
@ -136,45 +126,6 @@ export const editFilamentRouteHandler = async (req, res) => {
res.send(result); res.send(result);
}; };
export const editMultipleFilamentsRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
name: update.name,
barcode: update.barcode,
url: update.url,
image: update.image,
color: update.color,
vendor: update.vendor,
type: update.type,
cost: update.cost,
costTaxRate: update.costTaxRate,
costWithTax: update.costWithTax,
diameter: update.diameter,
density: update.density,
emptySpoolWeight: update.emptySpoolWeight,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: filamentModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing filaments:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} filaments`);
res.send(result);
};
export const newFilamentRouteHandler = async (req, res) => { export const newFilamentRouteHandler = async (req, res) => {
const newData = { const newData = {
createdAt: new Date(), createdAt: new Date(),

View File

@ -272,6 +272,7 @@ function checkFileType(file, cb) {
const allowedTypes = /.*/; // Allow all file types const allowedTypes = /.*/; // Allow all file types
if (allowedTypes.test(file.mimetype)) { if (allowedTypes.test(file.mimetype)) {
console.log(file);
return cb(null, true); return cb(null, true);
} else { } else {
cb('Error: File type not allowed!'); cb('Error: File type not allowed!');

View File

@ -36,6 +36,8 @@ export const listMaterialsRouteHandler = async (
aggregateCommand.push({ $skip: skip }); aggregateCommand.push({ $skip: skip });
aggregateCommand.push({ $limit: Number(limit) }); aggregateCommand.push({ $limit: Number(limit) });
console.log(aggregateCommand);
material = await materialModel.aggregate(aggregateCommand); material = await materialModel.aggregate(aggregateCommand);
logger.trace( logger.trace(

View File

@ -126,8 +126,11 @@ export const getSpotlightRouteHandler = async (req, res) => {
return; return;
} }
console.log(queryParams);
if (Object.keys(queryParams).length > 0) { if (Object.keys(queryParams).length > 0) {
const prefixEntry = PREFIX_MODEL_MAP[prefix]; const prefixEntry = PREFIX_MODEL_MAP[prefix];
console.log(prefixEntry);
if (!prefixEntry || !prefixEntry.model) { if (!prefixEntry || !prefixEntry.model) {
res.status(400).send({ error: 'Invalid or unsupported prefix' }); res.status(400).send({ error: 'Invalid or unsupported prefix' });
return; return;

View File

@ -1,5 +1,5 @@
import config from '../../config.js'; import config from '../../config.js';
import { taxRateModel } from '../../database/schemas/management/taxrate.schema.js'; import { taxRateModel } from '../../database/schemas/management/taxrates.schema.js';
import log4js from 'log4js'; import log4js from 'log4js';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { import {

View File

@ -1,85 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
recursivelyDeleteChildObjects: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/misc/note.schema.js', () => ({
noteModel: { modelName: 'Note' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listNotesRouteHandler,
getNoteRouteHandler,
newNoteRouteHandler,
editNoteRouteHandler,
} = await import('../../misc/notes.js');
const { listObjects, getObject, editObject, newObject } = await import(
'../../../database/database.js'
);
const { noteModel } = await import('../../../database/schemas/misc/note.schema.js');
describe('Note Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listNotesRouteHandler', () => {
it('should list notes', async () => {
const mockResult = [{ _id: '1', content: 'Test note' }];
listObjects.mockResolvedValue(mockResult);
await listNotesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: noteModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newNoteRouteHandler', () => {
it('should create a new note', async () => {
req.body = { content: 'New note', noteType: 'type123' };
const mockNote = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockNote);
await newNoteRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockNote);
});
});
});

View File

@ -104,7 +104,7 @@ const fetchAndStoreUser = async (req, token) => {
userInfoUrl, userInfoUrl,
new URLSearchParams({ new URLSearchParams({
client_id: config.auth.keycloak.clientId, client_id: config.auth.keycloak.clientId,
client_secret: config.auth.keycloak.clientSecret, client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
}), }),
{ {
headers: { headers: {
@ -161,7 +161,7 @@ export const loginTokenRouteHandler = async (req, res, redirectType = 'web') =>
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
client_id: config.auth.keycloak.clientId, client_id: config.auth.keycloak.clientId,
client_secret: config.auth.keycloak.clientSecret, client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
code: code, code: code,
redirect_uri: callbackUrl, redirect_uri: callbackUrl,
}).toString(), }).toString(),
@ -401,7 +401,7 @@ export const refreshTokenRouteHandler = (req, res) => {
new URLSearchParams({ new URLSearchParams({
grant_type: 'refresh_token', grant_type: 'refresh_token',
client_id: config.auth.keycloak.clientId, client_id: config.auth.keycloak.clientId,
client_secret: config.auth.keycloak.clientSecret, client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
refresh_token: refreshToken, refresh_token: refreshToken,
}).toString(), }).toString(),
{ {

View File

@ -107,8 +107,11 @@ export const getSpotlightRouteHandler = async (req, res) => {
return; return;
} }
console.log(queryParams);
if (Object.keys(queryParams).length > 0) { if (Object.keys(queryParams).length > 0) {
const prefixEntry = getModelByPrefix(prefix); const prefixEntry = getModelByPrefix(prefix);
console.log(prefixEntry);
if (!prefixEntry || !prefixEntry.model) { if (!prefixEntry || !prefixEntry.model) {
res.status(400).send({ error: 'Invalid or unsupported prefix' }); res.status(400).send({ error: 'Invalid or unsupported prefix' });
return; return;

View File

@ -1,106 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/production/gcodefile.schema.js', () => ({
gcodeFileModel: {
modelName: 'GCodeFile',
aggregate: jest.fn(),
},
}));
jest.unstable_mockModule('../../management/files.js', () => ({
getFileContentRouteHandler: jest.fn(),
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listGCodeFilesRouteHandler,
getGCodeFileRouteHandler,
newGCodeFileRouteHandler,
editGCodeFileRouteHandler,
deleteGCodeFileRouteHandler,
} = await import('../gcodefiles.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { gcodeFileModel } = await import('../../../database/schemas/production/gcodefile.schema.js');
describe('GCodeFile Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listGCodeFilesRouteHandler', () => {
it('should list gcode files', async () => {
const mockResult = [{ _id: '1', name: 'file.gcode' }];
listObjects.mockResolvedValue(mockResult);
await listGCodeFilesRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(
expect.objectContaining({ model: gcodeFileModel })
);
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newGCodeFileRouteHandler', () => {
it('should create a new gcode file', async () => {
req.body = { name: 'newfile.gcode', file: 'file123' };
const mockFile = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockFile);
await newGCodeFileRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockFile);
});
});
describe('editGCodeFileRouteHandler', () => {
it('should update a gcode file', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { name: 'updated.gcode' };
const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body };
editObject.mockResolvedValue(mockResult);
await editGCodeFileRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,114 +0,0 @@
import { jest } from '@jest/globals';
// Mock dependencies
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/production/job.schema.js', () => ({
jobModel: { modelName: 'Job' },
}));
jest.unstable_mockModule('../../../database/schemas/production/subjob.schema.js', () => ({
subJobModel: { modelName: 'SubJob' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
// Import handlers after mocking
const { listJobsRouteHandler, getJobRouteHandler, newJobRouteHandler, deleteJobRouteHandler } =
await import('../jobs.js');
const { listObjects, getObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { jobModel } = await import('../../../database/schemas/production/job.schema.js');
const { subJobModel } = await import('../../../database/schemas/production/subjob.schema.js');
describe('Job Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listJobsRouteHandler', () => {
it('should list jobs', async () => {
const mockResult = [{ _id: '1', name: 'Job 1' }];
listObjects.mockResolvedValue(mockResult);
await listJobsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newJobRouteHandler', () => {
it('should create a new job and corresponding subjobs', async () => {
req.body = {
quantity: 2,
printers: ['p1', 'p2'],
gcodeFile: 'file123',
};
const mockJob = { _id: 'job123' };
newObject.mockResolvedValueOnce(mockJob); // For Job
newObject.mockResolvedValue({ _id: 'subjob' }); // For SubJobs
await newJobRouteHandler(req, res);
expect(newObject).toHaveBeenCalledTimes(3); // 1 Job + 2 SubJobs
expect(res.send).toHaveBeenCalledWith(mockJob);
});
it('should handle errors during job creation', async () => {
req.body = { quantity: 1, printers: ['p1'] };
newObject.mockResolvedValueOnce({ error: 'Failed', code: 500 });
await newJobRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.send).toHaveBeenCalledWith(expect.objectContaining({ error: 'Failed' }));
});
});
describe('deleteJobRouteHandler', () => {
it('should delete a job', async () => {
req.params.id = '507f1f77bcf86cd799439011';
const mockResult = { _id: '507f1f77bcf86cd799439011' };
deleteObject.mockResolvedValue(mockResult);
await deleteJobRouteHandler(req, res);
expect(deleteObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,99 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/production/printer.schema.js', () => ({
printerModel: { modelName: 'Printer' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listPrintersRouteHandler,
getPrinterRouteHandler,
newPrinterRouteHandler,
editPrinterRouteHandler,
deletePrinterRouteHandler,
} = await import('../printers.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { printerModel } = await import('../../../database/schemas/production/printer.schema.js');
describe('Printer Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listPrintersRouteHandler', () => {
it('should list printers', async () => {
const mockResult = [{ _id: '1', name: 'Printer 1' }];
listObjects.mockResolvedValue(mockResult);
await listPrintersRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: printerModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newPrinterRouteHandler', () => {
it('should create a new printer', async () => {
req.body = { name: 'New Printer', host: 'host123' };
const mockPrinter = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockPrinter);
await newPrinterRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockPrinter);
});
});
describe('editPrinterRouteHandler', () => {
it('should update a printer', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { name: 'Updated Printer' };
const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body };
editObject.mockResolvedValue(mockResult);
await editPrinterRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,74 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/production/subjob.schema.js', () => ({
subJobModel: { modelName: 'SubJob' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const { listSubJobsRouteHandler, getSubJobRouteHandler } = await import('../subjobs.js');
const { listObjects, getObject } = await import('../../../database/database.js');
const { subJobModel } = await import('../../../database/schemas/production/subjob.schema.js');
describe('SubJob Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listSubJobsRouteHandler', () => {
it('should list sub jobs', async () => {
const mockResult = [{ _id: '1', number: 1 }];
listObjects.mockResolvedValue(mockResult);
await listSubJobsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: subJobModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('getSubJobRouteHandler', () => {
it('should get a sub job by ID', async () => {
req.params.id = '123';
const mockSubJob = { _id: '123', number: 1 };
getObject.mockResolvedValue(mockSubJob);
await getSubJobRouteHandler(req, res);
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: '123' }));
expect(res.send).toHaveBeenCalledWith(mockSubJob);
});
});
});

View File

@ -108,6 +108,7 @@ export const editGCodeFileRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id); const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`GCodeFile with ID: ${id}`); logger.trace(`GCodeFile with ID: ${id}`);
console.log('REQ.BODY', req.body);
const updateData = { const updateData = {
updatedAt: new Date(), updatedAt: new Date(),

View File

@ -161,6 +161,7 @@ export const deleteJobRouteHandler = async (req, res) => {
}; };
export const getJobStatsRouteHandler = async (req, res) => { export const getJobStatsRouteHandler = async (req, res) => {
console.log('Getting job stats');
const result = await getModelStats({ model: jobModel }); const result = await getModelStats({ model: jobModel });
if (result?.error) { if (result?.error) {
logger.error('Error fetching job stats:', result.error); logger.error('Error fetching job stats:', result.error);

View File

@ -170,6 +170,7 @@ export const deletePrinterRouteHandler = async (req, res) => {
export const getPrinterStatsRouteHandler = async (req, res) => { export const getPrinterStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: printerModel }); const result = await getModelStats({ model: printerModel });
console.log(result);
if (!result) { if (!result) {
logger.error('Error fetching printer stats:', result.error); logger.error('Error fetching printer stats:', result.error);
return res.status(result.code).send(result); return res.status(result.code).send(result);

View File

@ -1,112 +0,0 @@
import { jest } from '@jest/globals';
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/sales/client.schema.js', () => ({
clientModel: { modelName: 'Client' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
const {
listClientsRouteHandler,
getClientRouteHandler,
newClientRouteHandler,
editClientRouteHandler,
deleteClientRouteHandler,
} = await import('../clients.js');
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
'../../../database/database.js'
);
const { clientModel } = await import('../../../database/schemas/sales/client.schema.js');
describe('Client Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listClientsRouteHandler', () => {
it('should list clients', async () => {
const mockResult = [{ _id: '1', name: 'Client 1' }];
listObjects.mockResolvedValue(mockResult);
await listClientsRouteHandler(req, res);
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: clientModel }));
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('newClientRouteHandler', () => {
it('should create a new client', async () => {
req.body = { name: 'New Client', email: 'client@example.com' };
const mockClient = { _id: '456', ...req.body };
newObject.mockResolvedValue(mockClient);
await newClientRouteHandler(req, res);
expect(newObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockClient);
});
});
describe('editClientRouteHandler', () => {
it('should update a client', async () => {
req.params.id = '507f1f77bcf86cd799439011';
req.body = { name: 'Updated Client' };
const mockResult = { _id: '507f1f77bcf86cd799439011', ...req.body };
editObject.mockResolvedValue(mockResult);
await editClientRouteHandler(req, res);
expect(editObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('deleteClientRouteHandler', () => {
it('should delete a client', async () => {
req.params.id = '507f1f77bcf86cd799439011';
const mockResult = { _id: '507f1f77bcf86cd799439011' };
deleteObject.mockResolvedValue(mockResult);
await deleteClientRouteHandler(req, res);
expect(deleteObject).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
});

View File

@ -1,123 +0,0 @@
import { jest } from '@jest/globals';
// Mock dependencies
jest.unstable_mockModule('../../../database/database.js', () => ({
listObjects: jest.fn(),
getObject: jest.fn(),
editObject: jest.fn(),
editObjects: jest.fn(),
newObject: jest.fn(),
deleteObject: jest.fn(),
listObjectsByProperties: jest.fn(),
getModelStats: jest.fn(),
getModelHistory: jest.fn(),
checkStates: jest.fn(),
}));
jest.unstable_mockModule('../../../database/schemas/sales/salesorder.schema.js', () => ({
salesOrderModel: { modelName: 'SalesOrder' },
}));
jest.unstable_mockModule('../../../database/schemas/inventory/orderitem.schema.js', () => ({
orderItemModel: { modelName: 'OrderItem' },
}));
jest.unstable_mockModule('../../../database/schemas/inventory/shipment.schema.js', () => ({
shipmentModel: { modelName: 'Shipment' },
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
}),
},
}));
// Import handlers after mocking
const {
listSalesOrdersRouteHandler,
getSalesOrderRouteHandler,
newSalesOrderRouteHandler,
postSalesOrderRouteHandler,
} = await import('../salesorders.js');
const { listObjects, getObject, editObject, newObject, checkStates } = await import(
'../../../database/database.js'
);
const { salesOrderModel } = await import('../../../database/schemas/sales/salesorder.schema.js');
describe('Sales Order Service Route Handlers', () => {
let req, res;
beforeEach(() => {
req = {
params: {},
query: {},
body: {},
user: { id: 'test-user-id' },
};
res = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
jest.clearAllMocks();
});
describe('listSalesOrdersRouteHandler', () => {
it('should list sales orders', async () => {
const mockResult = [{ _id: '1', reference: 'SO-001' }];
listObjects.mockResolvedValue(mockResult);
await listSalesOrdersRouteHandler(req, res);
expect(listObjects).toHaveBeenCalled();
expect(res.send).toHaveBeenCalledWith(mockResult);
});
});
describe('postSalesOrderRouteHandler', () => {
it('should post a draft sales order and update items/shipments', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(true);
// Mock listObjects for orderItems and shipments
listObjects.mockResolvedValueOnce([
{ _id: 'oi1', state: { type: 'draft' }, shipment: 's1', _reference: 'ITEM1' },
]); // orderItems
listObjects.mockResolvedValueOnce([
{ _id: 's1', state: { type: 'draft' }, _reference: 'SHIP1' },
]); // shipments
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'sent' } });
await postSalesOrderRouteHandler(req, res);
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['draft'] }));
expect(editObject).toHaveBeenCalledTimes(3); // 1 OrderItem + 1 Shipment + 1 SalesOrder
expect(res.send).toHaveBeenCalled();
});
it('should fail if an order item is not in draft state', async () => {
req.params.id = '507f1f77bcf86cd799439011';
checkStates.mockResolvedValue(true);
listObjects.mockResolvedValueOnce([
{ _id: 'oi1', state: { type: 'ordered' }, _reference: 'ITEM1' },
]);
listObjects.mockResolvedValueOnce([]);
await postSalesOrderRouteHandler(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({ error: 'Order item ITEM1 not in draft state.' })
);
});
});
});

View File

@ -1,189 +0,0 @@
import config from '../../config.js';
import { clientModel } from '../../database/schemas/sales/client.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('Clients');
logger.level = config.server.logLevel;
export const listClientsRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: clientModel,
page,
limit,
property,
filter,
search,
sort,
order,
});
if (result?.error) {
logger.error('Error listing clients.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of clients (Page ${page}, Limit ${limit}). Count: ${result.length}.`);
res.send(result);
};
export const listClientsByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {}
) => {
const result = await listObjectsByProperties({
model: clientModel,
properties,
filter,
});
if (result?.error) {
logger.error('Error listing clients.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of clients. Count: ${result.length}`);
res.send(result);
};
export const getClientRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: clientModel,
id,
});
if (result?.error) {
logger.warn(`Client not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retreived client with ID: ${id}`);
res.send(result);
};
export const editClientRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Client with ID: ${id}`);
const updateData = {
updatedAt: new Date(),
country: req.body.country,
name: req.body.name,
phone: req.body.phone,
email: req.body.email,
address: req.body.address,
active: req.body.active,
tags: req.body.tags,
};
// Create audit log before updating
const result = await editObject({
model: clientModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing client:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited client with ID: ${id}`);
res.send(result);
};
export const newClientRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
country: req.body.country,
name: req.body.name,
phone: req.body.phone,
email: req.body.email,
address: req.body.address,
active: req.body.active,
tags: req.body.tags,
};
const result = await newObject({
model: clientModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No client created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New client with ID: ${result._id}`);
res.send(result);
};
export const deleteClientRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Client with ID: ${id}`);
const result = await deleteObject({
model: clientModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No client deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted client with ID: ${result._id}`);
res.send(result);
};
export const getClientStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: clientModel });
if (result?.error) {
logger.error('Error fetching client stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Client stats:', result);
res.send(result);
};
export const getClientHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: clientModel, from, to });
if (result?.error) {
logger.error('Error fetching client history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Client history:', result);
res.send(result);
};

View File

@ -1,460 +0,0 @@
import config from '../../config.js';
import { salesOrderModel } from '../../database/schemas/sales/salesorder.schema.js';
import log4js from 'log4js';
import mongoose from 'mongoose';
import {
deleteObject,
listObjects,
getObject,
editObject,
editObjects,
newObject,
listObjectsByProperties,
getModelStats,
getModelHistory,
checkStates,
} from '../../database/database.js';
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js';
const logger = log4js.getLogger('Sales Orders');
logger.level = config.server.logLevel;
export const listSalesOrdersRouteHandler = async (
req,
res,
page = 1,
limit = 25,
property = '',
filter = {},
search = '',
sort = '',
order = 'ascend'
) => {
const result = await listObjects({
model: salesOrderModel,
page,
limit,
property,
filter,
search,
sort,
order,
populate: ['client'],
});
if (result?.error) {
logger.error('Error listing sales orders.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of sales orders (Page ${page}, Limit ${limit}). Count: ${result.length}`);
res.send(result);
};
export const listSalesOrdersByPropertiesRouteHandler = async (
req,
res,
properties = '',
filter = {},
masterFilter = {}
) => {
const result = await listObjectsByProperties({
model: salesOrderModel,
properties,
filter,
populate: ['client'],
masterFilter,
});
if (result?.error) {
logger.error('Error listing sales orders.');
res.status(result.code).send(result);
return;
}
logger.debug(`List of sales orders. Count: ${result.length}`);
res.send(result);
};
export const getSalesOrderRouteHandler = async (req, res) => {
const id = req.params.id;
const result = await getObject({
model: salesOrderModel,
id,
populate: ['client'],
});
if (result?.error) {
logger.warn(`Sales Order not found with supplied id.`);
return res.status(result.code).send(result);
}
logger.debug(`Retreived sales order with ID: ${id}`);
res.send(result);
};
export const editSalesOrderRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking sales order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Sales order is not in draft state.');
res.status(400).send({ error: 'Sales order is not in draft state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
client: req.body.client,
};
// Create audit log before updating
const result = await editObject({
model: salesOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error editing sales order:', result.error);
res.status(result).send(result);
return;
}
logger.debug(`Edited sales order with ID: ${id}`);
res.send(result);
};
export const editMultipleSalesOrdersRouteHandler = async (req, res) => {
const updates = req.body.map((update) => ({
_id: update._id,
client: update.client,
}));
if (!Array.isArray(updates)) {
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
}
const result = await editObjects({
model: salesOrderModel,
updates,
user: req.user,
});
if (result.error) {
logger.error('Error editing sales orders:', result.error);
res.status(result.code || 500).send(result);
return;
}
logger.debug(`Edited ${updates.length} sales orders`);
res.send(result);
};
export const newSalesOrderRouteHandler = async (req, res) => {
const newData = {
updatedAt: new Date(),
client: req.body.client,
totalAmount: 0,
totalAmountWithTax: 0,
totalTaxAmount: 0,
};
const result = await newObject({
model: salesOrderModel,
newData,
user: req.user,
});
if (result.error) {
logger.error('No sales order created:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`New sales order with ID: ${result._id}`);
res.send(result);
};
export const deleteSalesOrderRouteHandler = async (req, res) => {
// Get ID from params
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const result = await deleteObject({
model: salesOrderModel,
id,
user: req.user,
});
if (result.error) {
logger.error('No sales order deleted:', result.error);
return res.status(result.code).send(result);
}
logger.debug(`Deleted sales order with ID: ${result._id}`);
res.send(result);
};
export const getSalesOrderStatsRouteHandler = async (req, res) => {
const result = await getModelStats({ model: salesOrderModel });
if (result?.error) {
logger.error('Error fetching sales order stats:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Sales order stats:', result);
res.send(result);
};
export const getSalesOrderHistoryRouteHandler = async (req, res) => {
const from = req.query.from;
const to = req.query.to;
const result = await getModelHistory({ model: salesOrderModel, from, to });
if (result?.error) {
logger.error('Error fetching sales order history:', result.error);
return res.status(result.code).send(result);
}
logger.trace('Sales order history:', result);
res.send(result);
};
export const postSalesOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['draft'] });
if (checkStatesResult.error) {
logger.error('Error checking sales order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Sales order is not in draft state.');
res.status(400).send({ error: 'Sales order is not in draft state.', code: 400 });
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { order: id, orderType: 'salesOrder' },
pagination: false,
});
const shipmentsResult = await listObjects({
model: shipmentModel,
filter: { order: id, orderType: 'salesOrder' },
pagination: false,
});
for (const orderItem of orderItemsResult) {
if (orderItem.state.type != 'draft') {
logger.warn(`Order item ${orderItem._id} is not in draft state.`);
return res
.status(400)
.send({ error: `Order item ${orderItem._reference} not in draft state.`, code: 400 });
}
if (!orderItem?.shipment || orderItem?.shipment == null) {
logger.warn(`Order item ${orderItem._id} does not have a shipment.`);
return res
.status(400)
.send({ error: `Order item ${orderItem._reference} does not have a shipment.`, code: 400 });
}
}
for (const shipment of shipmentsResult) {
if (shipment.state.type != 'draft') {
logger.warn(`Shipment ${shipment._id} is not in draft state.`);
return res
.status(400)
.send({ error: `Shipment ${shipment._reference} not in draft state.`, code: 400 });
}
}
for (const orderItem of orderItemsResult) {
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
state: { type: 'ordered' },
orderedAt: new Date(),
},
user: req.user,
});
}
for (const shipment of shipmentsResult) {
await editObject({
model: shipmentModel,
id: shipment._id,
updateData: {
state: { type: 'planned' },
},
user: req.user,
});
}
const updateData = {
updatedAt: new Date(),
state: { type: 'sent' },
postedAt: new Date(),
};
const result = await editObject({
model: salesOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error posting sales order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Posted sales order with ID: ${id}`);
res.send(result);
};
export const confirmSalesOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const checkStatesResult = await checkStates({ model: salesOrderModel, id, states: ['sent'] });
if (checkStatesResult.error) {
logger.error('Error checking sales order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Sales order is not in sent state.');
res.status(400).send({ error: 'Sales order is not in sent state.', code: 400 });
return;
}
const updateData = {
updatedAt: new Date(),
state: { type: 'confirmed' },
confirmedAt: new Date(),
};
const result = await editObject({
model: salesOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error confirming sales order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Confirmed sales order with ID: ${id}`);
res.send(result);
};
export const cancelSalesOrderRouteHandler = async (req, res) => {
const id = new mongoose.Types.ObjectId(req.params.id);
logger.trace(`Sales Order with ID: ${id}`);
const checkStatesResult = await checkStates({
model: salesOrderModel,
id,
states: ['sent', 'confirmed', 'partiallyShipped', 'shipped', 'partiallyDelivered'],
});
if (checkStatesResult.error) {
logger.error('Error checking sales order states:', checkStatesResult.error);
res.status(checkStatesResult.code).send(checkStatesResult);
return;
}
if (checkStatesResult === false) {
logger.error('Sales order is not in a cancellable state.');
res.status(400).send({
error: 'Sales order is not in a cancellable state (must be sent, confirmed, partiallyShipped, shipped, or partiallyDelivered).',
code: 400,
});
return;
}
const orderItemsResult = await listObjects({
model: orderItemModel,
filter: { order: id, orderType: 'salesOrder' },
pagination: false,
});
const shipmentsResult = await listObjects({
model: shipmentModel,
filter: { order: id, orderType: 'salesOrder' },
pagination: false,
});
const allowedOrderItemStates = ['ordered', 'shipped'];
const allowedShipmentStates = ['shipped', 'planned'];
for (const orderItem of orderItemsResult) {
if (allowedOrderItemStates.includes(orderItem.state.type)) {
await editObject({
model: orderItemModel,
id: orderItem._id,
updateData: {
state: { type: 'cancelled' },
},
user: req.user,
});
}
}
for (const shipment of shipmentsResult) {
if (allowedShipmentStates.includes(shipment.state.type)) {
await editObject({
model: shipmentModel,
id: shipment._id,
updateData: {
state: { type: 'cancelled' },
},
user: req.user,
});
}
}
const updateData = {
updatedAt: new Date(),
state: { type: 'cancelled' },
cancelledAt: new Date(),
};
const result = await editObject({
model: salesOrderModel,
id,
updateData,
user: req.user,
});
if (result.error) {
logger.error('Error cancelling sales order:', result.error);
res.status(result.code).send(result);
return;
}
logger.debug(`Cancelled sales order with ID: ${id}`);
res.send(result);
};

View File

@ -1,4 +1,4 @@
import { mongoose } from 'mongoose'; import { ObjectId } from 'mongodb';
import { auditLogModel } from './database/schemas/management/auditlog.schema.js'; import { auditLogModel } from './database/schemas/management/auditlog.schema.js';
import exifr from 'exifr'; import exifr from 'exifr';
import { natsServer } from './database/nats.js'; import { natsServer } from './database/nats.js';
@ -20,9 +20,6 @@ function buildWildcardRegexPattern(input) {
} }
function parseFilter(property, value) { function parseFilter(property, value) {
if (value?._id !== undefined && value?._id !== null) {
return { [property]: { _id: new mongoose.Types.ObjectId(value._id) } };
}
if (typeof value === 'string') { if (typeof value === 'string') {
var trimmed = value.trim(); var trimmed = value.trim();
if (trimmed.charAt(3) == ':') { if (trimmed.charAt(3) == ':') {
@ -36,7 +33,7 @@ function parseFilter(property, value) {
// Handle ObjectId (24-char hex) // Handle ObjectId (24-char hex)
if (/^[a-f\d]{24}$/i.test(trimmed) && trimmed.length >= 24) { if (/^[a-f\d]{24}$/i.test(trimmed) && trimmed.length >= 24) {
return { [property]: new mongoose.Types.ObjectId(trimmed) }; return { [property]: new ObjectId(trimmed) };
} }
// Handle numbers // Handle numbers
@ -517,7 +514,7 @@ function expandObjectIds(input) {
// Helper to check if a value is an ObjectId or a 24-char hex string // Helper to check if a value is an ObjectId or a 24-char hex string
function isObjectId(val) { function isObjectId(val) {
// Check for MongoDB ObjectId instance // Check for MongoDB ObjectId instance
if (val instanceof mongoose.Types.ObjectId) return true; if (val instanceof ObjectId) return true;
// Check for exactly 24 hex characters (no special characters) // Check for exactly 24 hex characters (no special characters)
if (typeof val === 'string' && /^[a-fA-F\d]{24}$/.test(val)) return true; if (typeof val === 'string' && /^[a-fA-F\d]{24}$/.test(val)) return true;
return false; return false;
@ -527,7 +524,7 @@ function expandObjectIds(input) {
function expand(value) { function expand(value) {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(expand); return value.map(expand);
} else if (value && typeof value === 'object' && !(value instanceof mongoose.Types.ObjectId)) { } else if (value && typeof value === 'object' && !(value instanceof ObjectId)) {
var result = {}; var result = {};
for (const [key, val] of Object.entries(value)) { for (const [key, val] of Object.entries(value)) {
if (key === '_id') { if (key === '_id') {
@ -561,6 +558,8 @@ function getFilter(query, allowedFilters, parse = true) {
let filter = {}; let filter = {};
for (const [key, value] of Object.entries(query)) { for (const [key, value] of Object.entries(query)) {
if (allowedFilters.includes(key)) { if (allowedFilters.includes(key)) {
console.log('key', key);
console.log('value', value);
const parsedFilter = parse ? parseFilter(key, value) : { [key]: value }; const parsedFilter = parse ? parseFilter(key, value) : { [key]: value };
filter = { ...filter, ...parsedFilter }; filter = { ...filter, ...parsedFilter };
} }

870082
uploads/default.gcode Normal file

File diff suppressed because it is too large Load Diff

BIN
uploads/default.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Some files were not shown because too many files have changed in this diff Show More