Compare commits
No commits in common. "config-updates" and "main" have entirely different histories.
config-upd
...
main
11
.env.example
11
.env.example
@ -1,9 +1,2 @@
|
|||||||
DB_LINK="mongo-link-to-connect"
|
SESSION_SECRET='SECRET'
|
||||||
|
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
Normal file
91
Jenkinsfile
vendored
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,5 +1,7 @@
|
|||||||
# FarmControl API
|
# FarmControl API
|
||||||
|
|
||||||
|
[](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
|
||||||
|
|||||||
4
babel.config.cjs
Normal file
4
babel.config.cjs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
||||||
|
plugins: ['transform-import-meta'],
|
||||||
|
};
|
||||||
61
config.json
61
config.json
@ -9,14 +9,14 @@
|
|||||||
"keycloak": {
|
"keycloak": {
|
||||||
"url": "https://auth.tombutcher.work",
|
"url": "https://auth.tombutcher.work",
|
||||||
"realm": "master",
|
"realm": "master",
|
||||||
"clientId": "farmcontrol-client"
|
"clientId": "farmcontrol-dev"
|
||||||
},
|
},
|
||||||
"requiredRoles": []
|
"requiredRoles": []
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"urlClient": "http://localhost:3000",
|
"urlClient": "https://dev.tombutcher.work",
|
||||||
"urlElectronClient": "http://localhost:3000",
|
"urlElectronClient": "http://localhost:5780",
|
||||||
"urlApi": "http://localhost:8787",
|
"urlApi": "https://dev.tombutcher.work/api",
|
||||||
"devAuthClient": "http://localhost:3500"
|
"devAuthClient": "http://localhost:3500"
|
||||||
},
|
},
|
||||||
"database": {
|
"database": {
|
||||||
@ -48,6 +48,55 @@
|
|||||||
},
|
},
|
||||||
"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,
|
||||||
@ -63,9 +112,9 @@
|
|||||||
"requiredRoles": []
|
"requiredRoles": []
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"urlClient": "http://localhost:3000",
|
"urlClient": "https://web.farmcontrol.app",
|
||||||
"urlElectronClient": "http://localhost:3000",
|
"urlElectronClient": "http://localhost:3000",
|
||||||
"urlApi": "http://localhost:8080",
|
"urlApi": "https://api.farmcontrol.app",
|
||||||
"devAuthClient": "http://localhost:3500"
|
"devAuthClient": "http://localhost:3500"
|
||||||
},
|
},
|
||||||
"database": {
|
"database": {
|
||||||
|
|||||||
14
ecosystem.config.js
Normal file
14
ecosystem.config.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export default {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'farmcontrol-api',
|
||||||
|
script: 'src/index.js',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
},
|
||||||
|
env_production: {
|
||||||
|
NODE_ENV: 'production',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
BIN
images/admin.jpg
BIN
images/admin.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 187 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 79 KiB |
22
jest.config.cjs
Normal file
22
jest.config.cjs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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
12211
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -19,6 +19,7 @@
|
|||||||
"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",
|
||||||
@ -41,20 +42,26 @@
|
|||||||
"@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": "echo \"Error: no test specified\" && exit 1",
|
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
|
||||||
"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
Normal file
10062
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
src/__tests__/users.api.test.js
Normal file
68
src/__tests__/users.api.test.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -2,6 +2,10 @@
|
|||||||
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);
|
||||||
@ -25,7 +29,31 @@ 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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return config[NODE_ENV];
|
const envConfig = 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;
|
||||||
|
|||||||
163
src/database/__tests__/database.test.js
Normal file
163
src/database/__tests__/database.test.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,11 +1,10 @@
|
|||||||
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(() => {
|
||||||
|
|||||||
@ -349,12 +349,14 @@ 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,
|
||||||
@ -363,7 +365,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 = (page - 1) * limit;
|
const skip = pagination ? (page - 1) * limit : 0;
|
||||||
// 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;
|
||||||
|
|
||||||
@ -371,10 +373,6 @@ 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')) {
|
||||||
@ -389,7 +387,7 @@ export const listObjects = async ({
|
|||||||
.find(filter)
|
.find(filter)
|
||||||
.sort({ [sort]: sortOrder })
|
.sort({ [sort]: sortOrder })
|
||||||
.skip(skip)
|
.skip(skip)
|
||||||
.limit(Number(limit));
|
.limit(pagination ? Number(limit) : undefined);
|
||||||
|
|
||||||
// Handle populate (array or single value)
|
// Handle populate (array or single value)
|
||||||
if (populate) {
|
if (populate) {
|
||||||
@ -477,8 +475,6 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -522,7 +518,6 @@ 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 ||
|
||||||
@ -594,12 +589,11 @@ 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) {
|
if (pipeline.length === 0 && masterFilter == {}) {
|
||||||
pipeline.push({ $match: {} });
|
pipeline.push({ $match: {} });
|
||||||
}
|
}
|
||||||
const results = await model.aggregate(pipeline);
|
const results = await model.aggregate(pipeline);
|
||||||
@ -731,8 +725,25 @@ 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 }) => {
|
export const editObject = async ({ model, id, updateData, user, populate, recalculate = true }) => {
|
||||||
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';
|
||||||
@ -810,7 +821,7 @@ export const editObject = async ({ model, id, updateData, user, populate }) => {
|
|||||||
populate,
|
populate,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (model.recalculate) {
|
if (model.recalculate && recalculate == true) {
|
||||||
logger.debug(`Recalculating ${model.modelName}`);
|
logger.debug(`Recalculating ${model.modelName}`);
|
||||||
await model.recalculate(updatedObject, user);
|
await model.recalculate(updatedObject, user);
|
||||||
}
|
}
|
||||||
@ -828,6 +839,33 @@ export const editObject = async ({ model, id, updateData, user, populate }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
|||||||
187
src/database/schemas/finance/invoice.schema.js
Normal file
187
src/database/schemas/finance/invoice.schema.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
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);
|
||||||
105
src/database/schemas/finance/payment.schema.js
Normal file
105
src/database/schemas/finance/payment.schema.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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);
|
||||||
@ -1,6 +1,12 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { purchaseOrderModel } from './purchaseorder.schema.js';
|
import { purchaseOrderModel } from './purchaseorder.schema.js';
|
||||||
import { aggregateRollups, editObject } from '../../database.js';
|
import { taxRateModel } from '../management/taxrate.schema.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;
|
||||||
|
|
||||||
@ -8,20 +14,69 @@ 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: true, default: null },
|
syncAmount: { type: String, required: false, 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') {
|
||||||
@ -33,6 +88,32 @@ 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: {
|
||||||
@ -51,6 +132,11 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'overallCount',
|
||||||
|
rollups: [{ name: 'overallCount', property: '_id', operation: 'count' }],
|
||||||
|
},
|
||||||
|
...rollupConfigs,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -58,14 +144,46 @@ 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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,22 +1,100 @@
|
|||||||
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 },
|
totalAmount: { type: Number, required: true, default: 0 },
|
||||||
totalAmountWithTax: { type: Number, required: true },
|
totalAmountWithTax: { type: Number, required: true, default: 0 },
|
||||||
totalTaxAmount: { type: Number, required: true },
|
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 },
|
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;
|
||||||
|
|||||||
@ -1,43 +1,116 @@
|
|||||||
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';
|
||||||
const shipmentItemSchema = new Schema({
|
import { taxRateModel } from '../management/taxrate.schema.js';
|
||||||
itemType: { type: String, required: true },
|
import { aggregateRollups, editObject, getObject } from '../../database.js';
|
||||||
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()() },
|
||||||
purchaseOrder: { type: Schema.Types.ObjectId, ref: 'purchaseOrder', required: true },
|
orderType: { type: String, required: true },
|
||||||
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
order: { type: Schema.Types.ObjectId, refPath: 'orderType', 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 },
|
||||||
items: [shipmentItemSchema],
|
amount: { type: Number, required: true },
|
||||||
cost: { net: { type: Number, required: true }, gross: { type: Number, required: true } },
|
amountWithTax: { type: Number, required: true },
|
||||||
shippedDate: { type: Date, required: false },
|
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
expectedDeliveryDate: { type: Date, required: false },
|
invoicedAmount: { type: Number, required: false, default: 0 },
|
||||||
actualDeliveryDate: { type: Date, required: false },
|
invoicedAmountWithTax: { type: Number, required: false, default: 0 },
|
||||||
|
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;
|
||||||
|
|||||||
@ -24,8 +24,12 @@ 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/taxrates.schema.js';
|
import { taxRateModel } from './management/taxrate.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 = {
|
||||||
@ -98,4 +102,8 @@ 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' },
|
||||||
};
|
};
|
||||||
|
|||||||
@ -92,8 +92,6 @@ 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;
|
||||||
};
|
};
|
||||||
|
|||||||
34
src/database/schemas/sales/client.schema.js
Normal file
34
src/database/schemas/sales/client.schema.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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);
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
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);
|
||||||
12
src/index.js
12
src/index.js
@ -37,6 +37,10 @@ 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';
|
||||||
@ -139,7 +143,15 @@ 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;
|
||||||
|
|||||||
@ -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 dotenv from 'dotenv';
|
import jwt from 'jsonwebtoken';
|
||||||
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,8 +12,6 @@ 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
|
||||||
|
|
||||||
@ -67,14 +65,14 @@ const keycloakConfig = {
|
|||||||
'use-resource-role-mappings': true,
|
'use-resource-role-mappings': true,
|
||||||
'verify-token-audience': true,
|
'verify-token-audience': true,
|
||||||
credentials: {
|
credentials: {
|
||||||
secret: process.env.KEYCLOAK_CLIENT_SECRET,
|
secret: config.auth.keycloak.clientSecret,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const memoryStore = new session.MemoryStore();
|
const memoryStore = new session.MemoryStore();
|
||||||
|
|
||||||
var expressSession = session({
|
var expressSession = session({
|
||||||
secret: process.env.SESSION_SECRET || 'n00Dl3s23!',
|
secret: config.auth.sessionSecret,
|
||||||
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,
|
||||||
@ -100,7 +98,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: process.env.KEYCLOAK_CLIENT_SECRET,
|
client_secret: config.auth.keycloak.clientSecret,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@ -123,8 +121,6 @@ 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) {
|
||||||
@ -137,7 +133,6 @@ 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()) {
|
||||||
|
|||||||
101
src/routes/finance/invoices.js
Normal file
101
src/routes/finance/invoices.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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;
|
||||||
96
src/routes/finance/payments.js
Normal file
96
src/routes/finance/payments.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
@ -29,6 +29,10 @@ 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 {
|
||||||
@ -64,4 +68,8 @@ export {
|
|||||||
courierServiceRoutes,
|
courierServiceRoutes,
|
||||||
taxRateRoutes,
|
taxRateRoutes,
|
||||||
taxRecordRoutes,
|
taxRecordRoutes,
|
||||||
|
invoiceRoutes,
|
||||||
|
paymentRoutes,
|
||||||
|
clientRoutes,
|
||||||
|
salesOrderRoutes,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
listFilamentStocksRouteHandler,
|
listFilamentStocksRouteHandler,
|
||||||
getFilamentStockRouteHandler,
|
getFilamentStockRouteHandler,
|
||||||
editFilamentStockRouteHandler,
|
editFilamentStockRouteHandler,
|
||||||
|
editMultipleFilamentStocksRouteHandler,
|
||||||
newFilamentStockRouteHandler,
|
newFilamentStockRouteHandler,
|
||||||
deleteFilamentStockRouteHandler,
|
deleteFilamentStockRouteHandler,
|
||||||
listFilamentStocksByPropertiesRouteHandler,
|
listFilamentStocksByPropertiesRouteHandler,
|
||||||
@ -51,6 +52,11 @@ 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
listOrderItemsRouteHandler,
|
listOrderItemsRouteHandler,
|
||||||
getOrderItemRouteHandler,
|
getOrderItemRouteHandler,
|
||||||
editOrderItemRouteHandler,
|
editOrderItemRouteHandler,
|
||||||
|
editMultipleOrderItemsRouteHandler,
|
||||||
newOrderItemRouteHandler,
|
newOrderItemRouteHandler,
|
||||||
deleteOrderItemRouteHandler,
|
deleteOrderItemRouteHandler,
|
||||||
listOrderItemsByPropertiesRouteHandler,
|
listOrderItemsByPropertiesRouteHandler,
|
||||||
@ -17,14 +18,34 @@ 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 = ['itemType', 'item', 'item._id', 'order', 'order._id', 'orderType'];
|
const allowedFilters = [
|
||||||
|
'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 = ['itemType', 'item', 'item._id', 'order', 'order._id', 'orderType'];
|
const allowedFilters = [
|
||||||
|
'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) {
|
||||||
@ -51,6 +72,11 @@ 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
listPartStocksRouteHandler,
|
listPartStocksRouteHandler,
|
||||||
getPartStockRouteHandler,
|
getPartStockRouteHandler,
|
||||||
editPartStockRouteHandler,
|
editPartStockRouteHandler,
|
||||||
|
editMultiplePartStocksRouteHandler,
|
||||||
newPartStockRouteHandler,
|
newPartStockRouteHandler,
|
||||||
deletePartStockRouteHandler,
|
deletePartStockRouteHandler,
|
||||||
listPartStocksByPropertiesRouteHandler,
|
listPartStocksByPropertiesRouteHandler,
|
||||||
@ -51,6 +52,11 @@ 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,11 +7,15 @@ 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
|
||||||
@ -28,7 +32,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 = JSON.parse(req.query.masterFilter);
|
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true);
|
||||||
}
|
}
|
||||||
listPurchaseOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
listPurchaseOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
});
|
});
|
||||||
@ -51,6 +55,11 @@ 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);
|
||||||
});
|
});
|
||||||
@ -59,4 +68,16 @@ 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;
|
||||||
|
|||||||
@ -7,24 +7,21 @@ 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 = [
|
const allowedFilters = ['orderType', 'order', 'state', 'courierService', 'order._id', 'taxRate'];
|
||||||
'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);
|
||||||
});
|
});
|
||||||
@ -32,17 +29,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 = [
|
||||||
'vendor',
|
'orderType',
|
||||||
'purchaseOrder',
|
'order',
|
||||||
'state.type',
|
'state.type',
|
||||||
'courierService',
|
'courierService',
|
||||||
'vendor._id',
|
'order._id',
|
||||||
'purchaseOrder._id',
|
'taxRate',
|
||||||
];
|
];
|
||||||
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 = JSON.parse(req.query.masterFilter);
|
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true);
|
||||||
}
|
}
|
||||||
listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
});
|
});
|
||||||
@ -65,6 +62,11 @@ 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);
|
||||||
});
|
});
|
||||||
@ -73,4 +75,16 @@ 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;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
getStockEventRouteHandler,
|
getStockEventRouteHandler,
|
||||||
newStockEventRouteHandler,
|
newStockEventRouteHandler,
|
||||||
editStockEventRouteHandler,
|
editStockEventRouteHandler,
|
||||||
|
editMultipleStockEventsRouteHandler,
|
||||||
deleteStockEventRouteHandler,
|
deleteStockEventRouteHandler,
|
||||||
listStockEventsByPropertiesRouteHandler,
|
listStockEventsByPropertiesRouteHandler,
|
||||||
getStockEventStatsRouteHandler,
|
getStockEventStatsRouteHandler,
|
||||||
@ -51,6 +52,11 @@ 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
listFilamentsByPropertiesRouteHandler,
|
listFilamentsByPropertiesRouteHandler,
|
||||||
getFilamentRouteHandler,
|
getFilamentRouteHandler,
|
||||||
editFilamentRouteHandler,
|
editFilamentRouteHandler,
|
||||||
|
editMultipleFilamentsRouteHandler,
|
||||||
newFilamentRouteHandler,
|
newFilamentRouteHandler,
|
||||||
getFilamentStatsRouteHandler,
|
getFilamentStatsRouteHandler,
|
||||||
getFilamentHistoryRouteHandler,
|
getFilamentHistoryRouteHandler,
|
||||||
@ -66,6 +67,11 @@ 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);
|
||||||
|
|||||||
59
src/routes/sales/clients.js
Normal file
59
src/routes/sales/clients.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
84
src/routes/sales/salesorders.js
Normal file
84
src/routes/sales/salesorders.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
154
src/services/finance/__tests__/invoices.test.js
Normal file
154
src/services/finance/__tests__/invoices.test.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
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.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
124
src/services/finance/__tests__/payments.test.js
Normal file
124
src/services/finance/__tests__/payments.test.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
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.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
554
src/services/finance/invoices.js
Normal file
554
src/services/finance/invoices.js
Normal file
@ -0,0 +1,554 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
373
src/services/finance/payments.js
Normal file
373
src/services/finance/payments.js
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
86
src/services/inventory/__tests__/filamentstocks.test.js
Normal file
86
src/services/inventory/__tests__/filamentstocks.test.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
100
src/services/inventory/__tests__/orderitems.test.js
Normal file
100
src/services/inventory/__tests__/orderitems.test.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
88
src/services/inventory/__tests__/partstocks.test.js
Normal file
88
src/services/inventory/__tests__/partstocks.test.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
122
src/services/inventory/__tests__/purchaseorders.test.js
Normal file
122
src/services/inventory/__tests__/purchaseorders.test.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
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.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
111
src/services/inventory/__tests__/shipments.test.js
Normal file
111
src/services/inventory/__tests__/shipments.test.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
91
src/services/inventory/__tests__/stockaudits.test.js
Normal file
91
src/services/inventory/__tests__/stockaudits.test.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
84
src/services/inventory/__tests__/stockevents.test.js
Normal file
84
src/services/inventory/__tests__/stockevents.test.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
|
editObjects,
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
@ -114,6 +115,32 @@ 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(),
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
|
editObjects,
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
@ -43,6 +44,10 @@ 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 },
|
||||||
@ -130,6 +135,7 @@ 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,
|
||||||
@ -138,6 +144,7 @@ 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,
|
||||||
};
|
};
|
||||||
@ -160,9 +167,48 @@ 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,
|
||||||
@ -175,6 +221,7 @@ 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,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
|
editObjects,
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
@ -114,6 +115,32 @@ 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(),
|
||||||
|
|||||||
@ -7,11 +7,15 @@ 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;
|
||||||
@ -95,6 +99,20 @@ 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,
|
||||||
@ -118,10 +136,40 @@ 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,
|
||||||
@ -180,3 +228,232 @@ 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);
|
||||||
|
};
|
||||||
|
|||||||
@ -7,13 +7,16 @@ 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,
|
||||||
@ -35,7 +38,7 @@ export const listShipmentsRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['purchaseOrder', 'vendor', 'courierService'],
|
populate: ['order', 'courierService', 'taxRate'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -59,7 +62,7 @@ export const listShipmentsByPropertiesRouteHandler = async (
|
|||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['purchaseOrder', 'vendor', 'courierService'],
|
populate: ['courierService'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -78,7 +81,7 @@ export const getShipmentRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
id,
|
id,
|
||||||
populate: ['purchaseOrder', 'vendor', 'courierService', 'items.item', 'items.taxRate'],
|
populate: ['order', 'courierService', 'taxRate'],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Shipment not found with supplied id.`);
|
logger.warn(`Shipment not found with supplied id.`);
|
||||||
@ -96,15 +99,13 @@ export const editShipmentRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
purchaseOrder: req.body.purchaseOrder,
|
orderType: req.body.orderType,
|
||||||
vendor: req.body.vendor,
|
order: req.body.order,
|
||||||
courierService: req.body.courierService,
|
courierService: req.body.courierService,
|
||||||
trackingNumber: req.body.trackingNumber,
|
trackingNumber: req.body.trackingNumber,
|
||||||
shippedDate: req.body.shippedDate,
|
amount: req.body.amount,
|
||||||
expectedDeliveryDate: req.body.expectedDeliveryDate,
|
amountWithTax: req.body.amountWithTax,
|
||||||
actualDeliveryDate: req.body.actualDeliveryDate,
|
taxRate: req.body.taxRate,
|
||||||
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({
|
||||||
@ -125,20 +126,53 @@ 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(),
|
||||||
purchaseOrder: req.body.purchaseOrder,
|
orderType: req.body.orderType,
|
||||||
vendor: req.body.vendor,
|
order: req.body.order,
|
||||||
courierService: req.body.courierService,
|
courierService: req.body.courierService,
|
||||||
trackingNumber: req.body.trackingNumber,
|
trackingNumber: req.body.trackingNumber,
|
||||||
items: req.body.items,
|
amount: req.body.amount,
|
||||||
cost: req.body.cost,
|
amountWithTax: req.body.amountWithTax,
|
||||||
shippedDate: req.body.shippedDate,
|
taxRate: req.body.taxRate,
|
||||||
expectedDeliveryDate: req.body.expectedDeliveryDate,
|
shippedAt: req.body.shippedAt,
|
||||||
actualDeliveryDate: req.body.actualDeliveryDate,
|
expectedAt: req.body.expectedAt,
|
||||||
state: req.body.state,
|
deliveredAt: req.body.deliveredAt,
|
||||||
notes: req.body.notes,
|
state: { type: 'draft' },
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
@ -197,3 +231,212 @@ 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);
|
||||||
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
|
editObjects,
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
@ -145,6 +146,32 @@ 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);
|
||||||
|
|||||||
87
src/services/management/__tests__/auditlogs.test.js
Normal file
87
src/services/management/__tests__/auditlogs.test.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
84
src/services/management/__tests__/courier.test.js
Normal file
84
src/services/management/__tests__/courier.test.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
88
src/services/management/__tests__/courierservice.test.js
Normal file
88
src/services/management/__tests__/courierservice.test.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
85
src/services/management/__tests__/documentjobs.test.js
Normal file
85
src/services/management/__tests__/documentjobs.test.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
85
src/services/management/__tests__/documentprinters.test.js
Normal file
85
src/services/management/__tests__/documentprinters.test.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
88
src/services/management/__tests__/documentsizes.test.js
Normal file
88
src/services/management/__tests__/documentsizes.test.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
88
src/services/management/__tests__/documenttemplates.test.js
Normal file
88
src/services/management/__tests__/documenttemplates.test.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
98
src/services/management/__tests__/filaments.test.js
Normal file
98
src/services/management/__tests__/filaments.test.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
123
src/services/management/__tests__/files.test.js
Normal file
123
src/services/management/__tests__/files.test.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
99
src/services/management/__tests__/hosts.test.js
Normal file
99
src/services/management/__tests__/hosts.test.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
75
src/services/management/__tests__/materials.test.js
Normal file
75
src/services/management/__tests__/materials.test.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
86
src/services/management/__tests__/notetypes.test.js
Normal file
86
src/services/management/__tests__/notetypes.test.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
124
src/services/management/__tests__/parts.test.js
Normal file
124
src/services/management/__tests__/parts.test.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
98
src/services/management/__tests__/products.test.js
Normal file
98
src/services/management/__tests__/products.test.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
98
src/services/management/__tests__/taxrates.test.js
Normal file
98
src/services/management/__tests__/taxrates.test.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
86
src/services/management/__tests__/taxrecords.test.js
Normal file
86
src/services/management/__tests__/taxrecords.test.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
126
src/services/management/__tests__/users.test.js
Normal file
126
src/services/management/__tests__/users.test.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
99
src/services/management/__tests__/vendors.test.js
Normal file
99
src/services/management/__tests__/vendors.test.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -36,8 +36,6 @@ 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)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
editObject,
|
editObject,
|
||||||
|
editObjects,
|
||||||
newObject,
|
newObject,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
@ -58,7 +59,16 @@ export const listFilamentsByPropertiesRouteHandler = async (
|
|||||||
model: filamentModel,
|
model: filamentModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: 'vendor',
|
populate: [
|
||||||
|
{
|
||||||
|
path: 'vendor',
|
||||||
|
from: 'vendors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'costTaxRate',
|
||||||
|
from: 'taxrates',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -126,6 +136,45 @@ 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(),
|
||||||
|
|||||||
@ -272,7 +272,6 @@ 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!');
|
||||||
|
|||||||
@ -36,8 +36,6 @@ 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(
|
||||||
|
|||||||
@ -126,11 +126,8 @@ 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;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import config from '../../config.js';
|
import config from '../../config.js';
|
||||||
import { taxRateModel } from '../../database/schemas/management/taxrates.schema.js';
|
import { taxRateModel } from '../../database/schemas/management/taxrate.schema.js';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import {
|
import {
|
||||||
|
|||||||
85
src/services/misc/__tests__/notes.test.js
Normal file
85
src/services/misc/__tests__/notes.test.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -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: process.env.KEYCLOAK_CLIENT_SECRET,
|
client_secret: config.auth.keycloak.clientSecret,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
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: process.env.KEYCLOAK_CLIENT_SECRET,
|
client_secret: config.auth.keycloak.clientSecret,
|
||||||
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: process.env.KEYCLOAK_CLIENT_SECRET,
|
client_secret: config.auth.keycloak.clientSecret,
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
{
|
{
|
||||||
|
|||||||
@ -107,11 +107,8 @@ 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;
|
||||||
|
|||||||
106
src/services/production/__tests__/gcodefiles.test.js
Normal file
106
src/services/production/__tests__/gcodefiles.test.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
114
src/services/production/__tests__/jobs.test.js
Normal file
114
src/services/production/__tests__/jobs.test.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
99
src/services/production/__tests__/printers.test.js
Normal file
99
src/services/production/__tests__/printers.test.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
74
src/services/production/__tests__/subjobs.test.js
Normal file
74
src/services/production/__tests__/subjobs.test.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -108,7 +108,6 @@ 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(),
|
||||||
|
|||||||
@ -161,7 +161,6 @@ 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);
|
||||||
|
|||||||
@ -170,7 +170,6 @@ 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);
|
||||||
|
|||||||
112
src/services/sales/__tests__/clients.test.js
Normal file
112
src/services/sales/__tests__/clients.test.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
123
src/services/sales/__tests__/salesorders.test.js
Normal file
123
src/services/sales/__tests__/salesorders.test.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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.' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
189
src/services/sales/clients.js
Normal file
189
src/services/sales/clients.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
460
src/services/sales/salesorders.js
Normal file
460
src/services/sales/salesorders.js
Normal file
@ -0,0 +1,460 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
13
src/utils.js
13
src/utils.js
@ -1,4 +1,4 @@
|
|||||||
import { ObjectId } from 'mongodb';
|
import { mongoose } from 'mongoose';
|
||||||
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,6 +20,9 @@ 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) == ':') {
|
||||||
@ -33,7 +36,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 ObjectId(trimmed) };
|
return { [property]: new mongoose.Types.ObjectId(trimmed) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle numbers
|
// Handle numbers
|
||||||
@ -514,7 +517,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 ObjectId) return true;
|
if (val instanceof mongoose.Types.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;
|
||||||
@ -524,7 +527,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 ObjectId)) {
|
} else if (value && typeof value === 'object' && !(value instanceof mongoose.Types.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') {
|
||||||
@ -558,8 +561,6 @@ 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
870082
uploads/default.gcode
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user