Compare commits
No commits in common. "main" and "config-updates" have entirely different histories.
main
...
config-upd
11
.env.example
11
.env.example
@ -1,2 +1,9 @@
|
|||||||
SESSION_SECRET='SECRET'
|
DB_LINK="mongo-link-to-connect"
|
||||||
KEYCLOAK_CLIENT_SECRET='SECRET'
|
|
||||||
|
JWT_SECRET="token"
|
||||||
|
|
||||||
|
APP_URL_CLIENT=https://material-dashboard-react-node.creative-tim.com
|
||||||
|
APP_URL_API=https://node-json-api-free.creative-tim.com/login
|
||||||
|
|
||||||
|
MAILTRAP_USER=
|
||||||
|
MAILTRAP_PASSWORD=
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -140,5 +140,3 @@ gocdefile/*
|
|||||||
gcodefile
|
gcodefile
|
||||||
gcodefiles/*
|
gcodefiles/*
|
||||||
gcodefiles
|
gcodefiles
|
||||||
|
|
||||||
test-results.xml
|
|
||||||
118
Jenkinsfile
vendored
118
Jenkinsfile
vendored
@ -1,118 +0,0 @@
|
|||||||
pipeline {
|
|
||||||
agent {
|
|
||||||
label 'ubuntu'
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
NODE_ENV = 'production'
|
|
||||||
}
|
|
||||||
|
|
||||||
stages {
|
|
||||||
stage('Checkout') {
|
|
||||||
steps {
|
|
||||||
checkout scm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Setup Node.js') {
|
|
||||||
steps {
|
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
|
||||||
sh 'node -v'
|
|
||||||
sh 'pnpm -v'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Install Dependencies') {
|
|
||||||
steps {
|
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
|
||||||
sh 'pnpm install --frozen-lockfile --production=false'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Run Tests') {
|
|
||||||
steps {
|
|
||||||
nodejs(nodeJSInstallationName: 'Node23') {
|
|
||||||
sh '''
|
|
||||||
export NODE_ENV=test
|
|
||||||
export SESSION_SECRET=test-session-secret-for-testing-only
|
|
||||||
pnpm test
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
post {
|
|
||||||
always {
|
|
||||||
junit 'test-results.xml'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Deploy via SSH') {
|
|
||||||
steps {
|
|
||||||
sshPublisher(publishers: [
|
|
||||||
sshPublisherDesc(
|
|
||||||
configName: 'farmcontrol.tombutcher.local',
|
|
||||||
transfers: [
|
|
||||||
sshTransfer(
|
|
||||||
cleanRemote: false,
|
|
||||||
excludes: '*.*',
|
|
||||||
execCommand: '''
|
|
||||||
if [ -e /home/farmcontrol/farmcontrol-api/src ]; then
|
|
||||||
rm -rf /home/farmcontrol/farmcontrol-api/src
|
|
||||||
fi
|
|
||||||
if [ -e /home/farmcontrol/farmcontrol-api/package.json ]; then
|
|
||||||
rm -rf /home/farmcontrol/farmcontrol-api/package.json
|
|
||||||
fi
|
|
||||||
if [ -e /home/farmcontrol/farmcontrol-api/package-lock.json ]; then
|
|
||||||
rm -rf /home/farmcontrol/farmcontrol-api/package-lock.json
|
|
||||||
fi
|
|
||||||
if [ -e /home/farmcontrol/farmcontrol-api/node_modules ]; then
|
|
||||||
rm -rf /home/farmcontrol/farmcontrol-api/node_modules
|
|
||||||
fi
|
|
||||||
''',
|
|
||||||
execTimeout: 120000,
|
|
||||||
flatten: true,
|
|
||||||
makeEmptyDirs: true,
|
|
||||||
noDefaultExcludes: false,
|
|
||||||
patternSeparator: '[, ]+',
|
|
||||||
remoteDirectory: 'farmcontrol-api',
|
|
||||||
remoteDirectorySDF: false,
|
|
||||||
removePrefix: '',
|
|
||||||
sourceFiles: ''
|
|
||||||
),
|
|
||||||
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: true,
|
|
||||||
noDefaultExcludes: false,
|
|
||||||
patternSeparator: '[, ]+',
|
|
||||||
remoteDirectory: 'farmcontrol-api',
|
|
||||||
remoteDirectorySDF: false,
|
|
||||||
removePrefix: '',
|
|
||||||
sourceFiles: '**/*'
|
|
||||||
)
|
|
||||||
],
|
|
||||||
usePromotionTimestamp: false,
|
|
||||||
useWorkspaceInPromotion: false,
|
|
||||||
verbose: true
|
|
||||||
)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
post {
|
|
||||||
always {
|
|
||||||
cleanWs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,7 +1,5 @@
|
|||||||
# 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
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
|
|
||||||
plugins: ['transform-import-meta'],
|
|
||||||
};
|
|
||||||
97
config.json
97
config.json
@ -9,14 +9,14 @@
|
|||||||
"keycloak": {
|
"keycloak": {
|
||||||
"url": "https://auth.tombutcher.work",
|
"url": "https://auth.tombutcher.work",
|
||||||
"realm": "master",
|
"realm": "master",
|
||||||
"clientId": "farmcontrol-dev"
|
"clientId": "farmcontrol-client"
|
||||||
},
|
},
|
||||||
"requiredRoles": []
|
"requiredRoles": []
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"urlClient": "https://dev.tombutcher.work",
|
"urlClient": "http://localhost:3000",
|
||||||
"urlElectronClient": "http://localhost:5780",
|
"urlElectronClient": "http://localhost:3000",
|
||||||
"urlApi": "https://dev.tombutcher.work/api",
|
"urlApi": "http://localhost:8787",
|
||||||
"devAuthClient": "http://localhost:3500"
|
"devAuthClient": "http://localhost:3500"
|
||||||
},
|
},
|
||||||
"database": {
|
"database": {
|
||||||
@ -46,75 +46,6 @@
|
|||||||
"filesBucket": "farmcontrol"
|
"filesBucket": "farmcontrol"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"smtp": {
|
|
||||||
"host": "mail.tombutcher.work",
|
|
||||||
"port": 465,
|
|
||||||
"secure": true,
|
|
||||||
"auth": {
|
|
||||||
"user": "farmcontrol",
|
|
||||||
"pass": "XwV5u3jWufuo5E5U4N9hBHfNfwk28D7fNdFN"
|
|
||||||
},
|
|
||||||
"from": "FarmControl <farmcontrol@tombutcher.work>"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"smtp": {
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 587,
|
|
||||||
"secure": false,
|
|
||||||
"auth": {
|
|
||||||
"user": "",
|
|
||||||
"pass": ""
|
|
||||||
},
|
|
||||||
"from": "FarmControl <farmcontrol@tombutcher.work>"
|
|
||||||
},
|
|
||||||
"otpExpiryMins": 0.5
|
"otpExpiryMins": 0.5
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
@ -132,15 +63,15 @@
|
|||||||
"requiredRoles": []
|
"requiredRoles": []
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"urlClient": "https://web.farmcontrol.app",
|
"urlClient": "http://localhost:3000",
|
||||||
"urlElectronClient": "http://localhost:3000",
|
"urlElectronClient": "http://localhost:3000",
|
||||||
"urlApi": "https://api.farmcontrol.app",
|
"urlApi": "http://localhost:8080",
|
||||||
"devAuthClient": "http://localhost:3500"
|
"devAuthClient": "http://localhost:3500"
|
||||||
},
|
},
|
||||||
"database": {
|
"database": {
|
||||||
"mongo": {
|
"mongo": {
|
||||||
"url": "mongodb://192.168.68.38:27017/farmcontrol",
|
"url": "mongodb://localhost:27017/farmcontrol",
|
||||||
"link": "192.168.68.38:27017"
|
"link": "localhost:27017"
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
"url": "",
|
"url": "",
|
||||||
@ -159,20 +90,10 @@
|
|||||||
"ceph": {
|
"ceph": {
|
||||||
"accessKeyId": "minioadmin",
|
"accessKeyId": "minioadmin",
|
||||||
"secretAccessKey": "minioadmin123",
|
"secretAccessKey": "minioadmin123",
|
||||||
"endpoint": "http://192.168.68.39:9000",
|
"endpoint": "http://127.0.0.1:9000",
|
||||||
"region": "us-east-1",
|
"region": "us-east-1",
|
||||||
"filesBucket": "farmcontrol"
|
"filesBucket": "farmcontrol"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"smtp": {
|
|
||||||
"host": "mail.tombutcher.work",
|
|
||||||
"port": 465,
|
|
||||||
"secure": true,
|
|
||||||
"auth": {
|
|
||||||
"user": "farmcontrol",
|
|
||||||
"pass": "XwV5u3jWufuo5E5U4N9hBHfNfwk28D7fNdFN"
|
|
||||||
},
|
|
||||||
"from": "FarmControl <noreply@farmcontrol.app>"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
export default {
|
|
||||||
apps: [
|
|
||||||
{
|
|
||||||
name: 'farmcontrol-api',
|
|
||||||
script: 'src/index.js',
|
|
||||||
env: {
|
|
||||||
NODE_ENV: 'development',
|
|
||||||
},
|
|
||||||
env_production: {
|
|
||||||
NODE_ENV: 'production',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
BIN
images/admin.jpg
Normal file
BIN
images/admin.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
BIN
images/creator.jpg
Normal file
BIN
images/creator.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
BIN
images/member.jpg
Normal file
BIN
images/member.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@ -1,22 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
testEnvironment: 'node',
|
|
||||||
transform: {},
|
|
||||||
moduleFileExtensions: ['js', 'json', 'jsx', 'node'],
|
|
||||||
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
|
|
||||||
verbose: true,
|
|
||||||
reporters: [
|
|
||||||
'default',
|
|
||||||
[
|
|
||||||
'jest-junit',
|
|
||||||
{
|
|
||||||
outputDirectory: '.',
|
|
||||||
outputName: 'test-results.xml',
|
|
||||||
suiteName: 'farmcontrol-api-tests',
|
|
||||||
classNameTemplate: '{classname}',
|
|
||||||
titleTemplate: '{title}',
|
|
||||||
ancestorSeparator: ' › ',
|
|
||||||
usePathForSuiteName: 'true',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
12211
package-lock.json
generated
Normal file
12211
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -13,14 +13,12 @@
|
|||||||
"canonical-json": "^0.2.0",
|
"canonical-json": "^0.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"exceljs": "^4.4.0",
|
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"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",
|
||||||
@ -31,7 +29,6 @@
|
|||||||
"nodemailer": "*",
|
"nodemailer": "*",
|
||||||
"nodemon": "^3.1.11",
|
"nodemon": "^3.1.11",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"puppeteer": "^24.37.5",
|
|
||||||
"redis": "^5.10.0",
|
"redis": "^5.10.0",
|
||||||
"sequelize": "^6.37.7"
|
"sequelize": "^6.37.7"
|
||||||
},
|
},
|
||||||
@ -44,29 +41,22 @@
|
|||||||
"@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 --kill-others --names \"API,SCHEMAS\" --prefix-colors \"cyan,yellow\" \"nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js\" \"nodemon --config nodemon.schemas.json\"",
|
"dev": "concurrently --names \"API,SCHEMAS\" --prefix-colors \"cyan,yellow\" \"nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js\" \"nodemon --config nodemon.schemas.json\"",
|
||||||
"dev:api": "nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js",
|
"dev:api": "nodemon --exec babel-node --experimental-specifier-resolution=node src/index.js",
|
||||||
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"seed": "node src/mongo/seedData.js",
|
"seed": "node src/mongo/seedData.js",
|
||||||
"clear": "node src/mongo/clearDbs.js",
|
"clear": "node src/mongo/clearDbs.js"
|
||||||
"start": "node src/index.js"
|
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
|
|||||||
10819
pnpm-lock.yaml
generated
10819
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,70 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
import request from 'supertest';
|
|
||||||
|
|
||||||
// Mock Keycloak and Auth
|
|
||||||
jest.unstable_mockModule('../keycloak.js', () => ({
|
|
||||||
keycloak: {
|
|
||||||
middleware: () => (req, res, next) => next(),
|
|
||||||
protect: () => (req, res, next) => next(),
|
|
||||||
},
|
|
||||||
isAuthenticated: (req, res, next) => next(),
|
|
||||||
isAppAuthenticated: (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(),
|
|
||||||
setAppPasswordRouteHandler: 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,10 +2,6 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import dotenv from 'dotenv';
|
|
||||||
|
|
||||||
// Load environment variables from .env file
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// Configure paths relative to this file
|
// Configure paths relative to this file
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@ -29,47 +25,7 @@ function loadConfig() {
|
|||||||
throw new Error(`Configuration for environment '${NODE_ENV}' not found in config.json`);
|
throw new Error(`Configuration for environment '${NODE_ENV}' not found in config.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const envConfig = config[NODE_ENV];
|
return config[NODE_ENV];
|
||||||
|
|
||||||
// Ensure auth config exists
|
|
||||||
if (!envConfig.auth) {
|
|
||||||
envConfig.auth = {};
|
|
||||||
}
|
|
||||||
if (!envConfig.auth.keycloak) {
|
|
||||||
envConfig.auth.keycloak = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override secrets with environment variables if available
|
|
||||||
if (process.env.KEYCLOAK_CLIENT_SECRET) {
|
|
||||||
envConfig.auth.keycloak.clientSecret = process.env.KEYCLOAK_CLIENT_SECRET;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session secret must be set - use env var or throw error
|
|
||||||
if (process.env.SESSION_SECRET) {
|
|
||||||
envConfig.auth.sessionSecret = process.env.SESSION_SECRET;
|
|
||||||
} else if (!envConfig.auth.sessionSecret) {
|
|
||||||
throw new Error(
|
|
||||||
'SESSION_SECRET environment variable is required. Please set SESSION_SECRET in your environment.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure smtp config exists and override with env vars if available
|
|
||||||
if (!envConfig.smtp) {
|
|
||||||
envConfig.smtp = {};
|
|
||||||
}
|
|
||||||
if (process.env.SMTP_HOST) envConfig.smtp.host = process.env.SMTP_HOST;
|
|
||||||
if (process.env.SMTP_PORT) envConfig.smtp.port = parseInt(process.env.SMTP_PORT, 10);
|
|
||||||
if (process.env.SMTP_SECURE) envConfig.smtp.secure = process.env.SMTP_SECURE === 'true';
|
|
||||||
if (process.env.SMTP_USER || process.env.SMTP_PASS) {
|
|
||||||
envConfig.smtp.auth = {
|
|
||||||
...(envConfig.smtp.auth || {}),
|
|
||||||
...(process.env.SMTP_USER && { user: process.env.SMTP_USER }),
|
|
||||||
...(process.env.SMTP_PASS && { pass: process.env.SMTP_PASS }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (process.env.SMTP_FROM) envConfig.smtp.from = process.env.SMTP_FROM;
|
|
||||||
|
|
||||||
return envConfig;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading config:', err);
|
console.error('Error loading config:', err);
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@ -1,166 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
// Mock src/database/utils.js (where generateId and convertObjectIdStringsInFilter live)
|
|
||||||
jest.unstable_mockModule('../utils.js', () => ({
|
|
||||||
generateId: jest.fn(() => () => 'test-id'),
|
|
||||||
convertObjectIdStringsInFilter: jest.fn((filter) => filter),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock src/utils.js (where most database.js helpers live)
|
|
||||||
jest.unstable_mockModule('../../utils.js', () => ({
|
|
||||||
deleteAuditLog: jest.fn(),
|
|
||||||
deleteNotification: jest.fn(),
|
|
||||||
distributeDelete: jest.fn(),
|
|
||||||
editAuditLog: jest.fn(),
|
|
||||||
editNotification: jest.fn(),
|
|
||||||
expandObjectIds: jest.fn((obj) => obj),
|
|
||||||
getFieldsByRef: jest.fn(() => []),
|
|
||||||
getQueryToCacheKey: jest.fn(({ model, id }) => `${model}:${id}`),
|
|
||||||
modelHasRef: jest.fn(() => false),
|
|
||||||
newAuditLog: jest.fn(),
|
|
||||||
distributeNew: jest.fn(),
|
|
||||||
distributeUpdate: 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,10 +1,11 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from "mongoose";
|
||||||
import { userModel } from '../schemas/user.schema.js';
|
import { userModel } from "../schemas/user.schema.js";
|
||||||
import { dbConnect } from '../mongo/index.js';
|
import { dbConnect } from "../mongo/index.js";
|
||||||
|
|
||||||
async function clear() {
|
async function clear() {
|
||||||
dbConnect();
|
dbConnect();
|
||||||
await userModel.deleteMany({});
|
await userModel.deleteMany({});
|
||||||
|
console.log("DB cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
clear().then(() => {
|
clear().then(() => {
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* Convert a value to a CSV cell-friendly format.
|
|
||||||
* Primitives pass through; objects/arrays are stringified; dates are formatted.
|
|
||||||
*/
|
|
||||||
function toCsvValue(val) {
|
|
||||||
if (val === null || val === undefined) return '';
|
|
||||||
if (val instanceof Date) return val.toISOString();
|
|
||||||
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
||||||
if (typeof val === 'string') return val;
|
|
||||||
if (typeof val === 'object') {
|
|
||||||
if (Array.isArray(val)) return val.map(toCsvValue).join(', ');
|
|
||||||
return JSON.stringify(val);
|
|
||||||
}
|
|
||||||
return String(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape a CSV field per RFC 4180: wrap in double quotes, escape internal quotes by doubling.
|
|
||||||
*/
|
|
||||||
function escapeCsvField(str) {
|
|
||||||
if (str == null) return '""';
|
|
||||||
const s = String(str);
|
|
||||||
if (s.includes('"') || s.includes(',') || s.includes('\n') || s.includes('\r')) {
|
|
||||||
return '"' + s.replace(/"/g, '""') + '"';
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a CSV buffer from tabular data.
|
|
||||||
* @param {Array<Object>} data - Array of row objects (keys = column headers)
|
|
||||||
* @param {Object} options - Options
|
|
||||||
* @param {string[]} [options.columnOrder] - Optional column order (uses Object.keys of first row if not provided)
|
|
||||||
* @returns {Buffer} CSV file as buffer
|
|
||||||
*/
|
|
||||||
export function generateCsvTable(data, options = {}) {
|
|
||||||
const { columnOrder } = options;
|
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
|
||||||
return Buffer.from('', 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = columnOrder || Object.keys(data[0]).filter((k) => !k.startsWith('@'));
|
|
||||||
const headerRow = keys.map((k) => escapeCsvField(k)).join(',');
|
|
||||||
const lines = [headerRow];
|
|
||||||
|
|
||||||
for (const row of data) {
|
|
||||||
const values = keys.map((key) => escapeCsvField(toCsvValue(row[key])));
|
|
||||||
lines.push(values.join(','));
|
|
||||||
}
|
|
||||||
|
|
||||||
const csv = lines.join('\n');
|
|
||||||
return Buffer.from(csv, 'utf8');
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import config from '../config.js';
|
import config from '../config.js';
|
||||||
import { fileModel } from './schemas/management/file.schema.js';
|
import { fileModel } from './schemas/management/file.schema.js';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import log4js from 'log4js';
|
|
||||||
import {
|
import {
|
||||||
deleteAuditLog,
|
deleteAuditLog,
|
||||||
distributeDelete,
|
distributeDelete,
|
||||||
@ -9,6 +8,9 @@ import {
|
|||||||
modelHasRef,
|
modelHasRef,
|
||||||
getFieldsByRef,
|
getFieldsByRef,
|
||||||
getQueryToCacheKey,
|
getQueryToCacheKey,
|
||||||
|
} from '../utils.js';
|
||||||
|
import log4js from 'log4js';
|
||||||
|
import {
|
||||||
editAuditLog,
|
editAuditLog,
|
||||||
distributeUpdate,
|
distributeUpdate,
|
||||||
newAuditLog,
|
newAuditLog,
|
||||||
@ -17,13 +19,10 @@ import {
|
|||||||
distributeChildDelete,
|
distributeChildDelete,
|
||||||
distributeChildNew,
|
distributeChildNew,
|
||||||
distributeStats,
|
distributeStats,
|
||||||
editNotification,
|
|
||||||
deleteNotification,
|
|
||||||
} from '../utils.js';
|
} from '../utils.js';
|
||||||
import { getAllModels } from '../services/misc/model.js';
|
import { getAllModels } from '../services/misc/model.js';
|
||||||
import { redisServer } from './redis.js';
|
import { redisServer } from './redis.js';
|
||||||
import { auditLogModel } from './schemas/management/auditlog.schema.js';
|
import { auditLogModel } from './schemas/management/auditlog.schema.js';
|
||||||
import { convertObjectIdStringsInFilter } from './utils.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Database');
|
const logger = log4js.getLogger('Database');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -350,14 +349,12 @@ export const listObjects = async ({
|
|||||||
filter = {},
|
filter = {},
|
||||||
sort = '',
|
sort = '',
|
||||||
order = 'ascend',
|
order = 'ascend',
|
||||||
pagination = true,
|
|
||||||
project, // optional: override default projection
|
project, // optional: override default projection
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
logger.trace('Listing object:', {
|
logger.trace('Listing object:', {
|
||||||
model,
|
model,
|
||||||
populate,
|
populate,
|
||||||
pagination,
|
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
filter,
|
filter,
|
||||||
@ -366,7 +363,7 @@ export const listObjects = async ({
|
|||||||
project,
|
project,
|
||||||
});
|
});
|
||||||
// Calculate the skip value based on the page number and limit
|
// Calculate the skip value based on the page number and limit
|
||||||
const skip = pagination ? (page - 1) * limit : 0;
|
const skip = (page - 1) * limit;
|
||||||
// Fix: descend should be -1, ascend should be 1
|
// Fix: descend should be -1, ascend should be 1
|
||||||
const sortOrder = order === 'descend' ? -1 : 1;
|
const sortOrder = order === 'descend' ? -1 : 1;
|
||||||
|
|
||||||
@ -374,6 +371,10 @@ export const listObjects = async ({
|
|||||||
sort = 'createdAt';
|
sort = 'createdAt';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
console.log('filter', filter);
|
||||||
|
}
|
||||||
|
|
||||||
// Translate any key ending with ._id to remove the ._id suffix for Mongoose
|
// Translate any key ending with ._id to remove the ._id suffix for Mongoose
|
||||||
Object.keys(filter).forEach((key) => {
|
Object.keys(filter).forEach((key) => {
|
||||||
if (key.endsWith('._id')) {
|
if (key.endsWith('._id')) {
|
||||||
@ -388,7 +389,7 @@ export const listObjects = async ({
|
|||||||
.find(filter)
|
.find(filter)
|
||||||
.sort({ [sort]: sortOrder })
|
.sort({ [sort]: sortOrder })
|
||||||
.skip(skip)
|
.skip(skip)
|
||||||
.limit(pagination ? Number(limit) : undefined);
|
.limit(Number(limit));
|
||||||
|
|
||||||
// Handle populate (array or single value)
|
// Handle populate (array or single value)
|
||||||
if (populate) {
|
if (populate) {
|
||||||
@ -476,6 +477,8 @@ function nestGroups(groups, props, filter, idx = 0) {
|
|||||||
// Check if any group in this key matches the filter (by _id or name)
|
// Check if any group in this key matches the filter (by _id or name)
|
||||||
const matches = groupList.filter((group) => {
|
const matches = groupList.filter((group) => {
|
||||||
const { filterVals } = getKeyAndFilterVals(group._id[prop]);
|
const { filterVals } = getKeyAndFilterVals(group._id[prop]);
|
||||||
|
console.log('filterVals', filterVals);
|
||||||
|
console.log('filterValue', filterValue);
|
||||||
return filterVals.some((val) => val?.toString() === filterValue);
|
return filterVals.some((val) => val?.toString() === filterValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -519,6 +522,7 @@ export const listObjectsByProperties = async ({
|
|||||||
populate,
|
populate,
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('Props', properties);
|
||||||
const propertiesPresent = !(
|
const propertiesPresent = !(
|
||||||
!Array.isArray(properties) ||
|
!Array.isArray(properties) ||
|
||||||
properties.length === 0 ||
|
properties.length === 0 ||
|
||||||
@ -571,12 +575,8 @@ export const listObjectsByProperties = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Master filter:', masterFilter);
|
if (masterFilter != {}) {
|
||||||
|
pipeline.push({ $match: { ...masterFilter } });
|
||||||
if (Object.keys(masterFilter).length > 0) {
|
|
||||||
const convertedFilter = convertObjectIdStringsInFilter(masterFilter);
|
|
||||||
logger.debug('Converted filter:', convertedFilter);
|
|
||||||
pipeline.push({ $match: convertedFilter });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (propertiesPresent) {
|
if (propertiesPresent) {
|
||||||
@ -594,17 +594,15 @@ 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 && Object.keys(masterFilter).length === 0) {
|
if (pipeline.length === 0) {
|
||||||
console.log('Adding empty match stage');
|
|
||||||
pipeline.push({ $match: {} });
|
pipeline.push({ $match: {} });
|
||||||
}
|
}
|
||||||
console.log('Running pipeline:', pipeline);
|
|
||||||
const results = await model.aggregate(pipeline);
|
const results = await model.aggregate(pipeline);
|
||||||
console.log('Results:', results);
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -733,25 +731,8 @@ export const listObjectDependencies = async ({ model, id }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkStates = async ({ model, id, states }) => {
|
|
||||||
try {
|
|
||||||
const object = await getObject({ model, id, cached: true });
|
|
||||||
if (!object?.state?.type) {
|
|
||||||
logger.warn(`Object ${id} has no state type.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (states.includes(object?.state?.type)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('checkStates error:', error);
|
|
||||||
return { error: error.message, code: 500 };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reusable function to edit an object by ID, with audit logging and distribution
|
// Reusable function to edit an object by ID, with audit logging and distribution
|
||||||
export const editObject = async ({ model, id, updateData, user, populate, recalculate = true }) => {
|
export const editObject = async ({ model, id, updateData, user, populate }) => {
|
||||||
try {
|
try {
|
||||||
// Determine parentType from model name
|
// Determine parentType from model name
|
||||||
const parentType = model.modelName ? model.modelName : 'unknown';
|
const parentType = model.modelName ? model.modelName : 'unknown';
|
||||||
@ -810,20 +791,6 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
|||||||
parentType,
|
parentType,
|
||||||
user
|
user
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
parentType !== 'notification' &&
|
|
||||||
parentType !== 'auditLog' &&
|
|
||||||
parentType !== 'userNotifier'
|
|
||||||
) {
|
|
||||||
await editNotification(
|
|
||||||
previousExpandedObject,
|
|
||||||
{ ...previousExpandedObject, ...updateData },
|
|
||||||
id,
|
|
||||||
parentType,
|
|
||||||
user
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Distribute update
|
// Distribute update
|
||||||
await distributeUpdate(updateData, id, parentType);
|
await distributeUpdate(updateData, id, parentType);
|
||||||
// Call childUpdate event for any child objects
|
// Call childUpdate event for any child objects
|
||||||
@ -843,7 +810,7 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
|||||||
populate,
|
populate,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (model.recalculate && recalculate == true) {
|
if (model.recalculate) {
|
||||||
logger.debug(`Recalculating ${model.modelName}`);
|
logger.debug(`Recalculating ${model.modelName}`);
|
||||||
await model.recalculate(updatedObject, user);
|
await model.recalculate(updatedObject, user);
|
||||||
}
|
}
|
||||||
@ -861,33 +828,6 @@ export const editObject = async ({ model, id, updateData, user, populate, recalc
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reusable function to edit multiple objects
|
|
||||||
export const editObjects = async ({ model, updates, user, populate, recalculate = true }) => {
|
|
||||||
try {
|
|
||||||
const results = [];
|
|
||||||
for (const update of updates) {
|
|
||||||
const id = update._id || update.id;
|
|
||||||
const updateData = { ...update };
|
|
||||||
delete updateData._id;
|
|
||||||
delete updateData.id;
|
|
||||||
|
|
||||||
const result = await editObject({
|
|
||||||
model,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user,
|
|
||||||
populate,
|
|
||||||
recalculate,
|
|
||||||
});
|
|
||||||
results.push(result);
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('editObjects error:', error);
|
|
||||||
return { error: error.message, code: 500 };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reusable function to create a new object
|
// Reusable function to create a new object
|
||||||
export const newObject = async ({ model, newData, user = null }, distributeChanges = true) => {
|
export const newObject = async ({ model, newData, user = null }, distributeChanges = true) => {
|
||||||
try {
|
try {
|
||||||
@ -900,7 +840,6 @@ export const newObject = async ({ model, newData, user = null }, distributeChang
|
|||||||
const created = expandObjectIds(result.toObject());
|
const created = expandObjectIds(result.toObject());
|
||||||
|
|
||||||
await newAuditLog(newData, created._id, parentType, user);
|
await newAuditLog(newData, created._id, parentType, user);
|
||||||
|
|
||||||
if (distributeChanges == true) {
|
if (distributeChanges == true) {
|
||||||
await distributeNew(created, parentType);
|
await distributeNew(created, parentType);
|
||||||
}
|
}
|
||||||
@ -933,28 +872,9 @@ export const newObject = async ({ model, newData, user = null }, distributeChang
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Reusable function to delete an object by ID, with audit logging and distribution
|
// Reusable function to delete an object by ID, with audit logging and distribution
|
||||||
export const deleteObject = async (
|
export const deleteObject = async ({ model, id, user = null }, distributeChanges = true) => {
|
||||||
{ model, id, user = null, checkUnused = false },
|
|
||||||
distributeChanges = true
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const parentType = model.modelName ? model.modelName : 'unknown';
|
const parentType = model.modelName ? model.modelName : 'unknown';
|
||||||
|
|
||||||
if (checkUnused) {
|
|
||||||
const dependencies = await listObjectDependencies({ model, id });
|
|
||||||
if (dependencies?.error) {
|
|
||||||
return { error: dependencies.error, code: dependencies.code || 500 };
|
|
||||||
}
|
|
||||||
if (dependencies?.length > 0) {
|
|
||||||
return {
|
|
||||||
error: 'Object is in use and cannot be deleted',
|
|
||||||
code: 409,
|
|
||||||
dependencies: dependencies.length,
|
|
||||||
dependencyDetails: dependencies,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the object
|
// Delete the object
|
||||||
const result = await model.findByIdAndDelete(id);
|
const result = await model.findByIdAndDelete(id);
|
||||||
|
|
||||||
@ -964,15 +884,7 @@ export const deleteObject = async (
|
|||||||
|
|
||||||
const deleted = expandObjectIds(result.toObject());
|
const deleted = expandObjectIds(result.toObject());
|
||||||
// Audit log the deletion
|
// Audit log the deletion
|
||||||
await deleteAuditLog(deleted, id, parentType, user);
|
await deleteAuditLog(deleted, id, parentType, user, 'delete');
|
||||||
|
|
||||||
if (
|
|
||||||
parentType !== 'notification' &&
|
|
||||||
parentType !== 'auditLog' &&
|
|
||||||
parentType !== 'userNotifier'
|
|
||||||
) {
|
|
||||||
await deleteNotification(deleted, id, parentType, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distributeChanges == true) {
|
if (distributeChanges == true) {
|
||||||
await distributeDelete(deleted, parentType);
|
await distributeDelete(deleted, parentType);
|
||||||
|
|||||||
@ -1,160 +0,0 @@
|
|||||||
import { customAlphabet } from 'nanoid';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import ExcelJS from 'exceljs';
|
|
||||||
import config from '../config.js';
|
|
||||||
import { redisServer } from './redis.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Excel');
|
|
||||||
const EXCEL_TEMP_KEY_PREFIX = 'excel:temp:';
|
|
||||||
const EXCEL_TEMP_TTL_SECONDS = 15; // 15 seconds
|
|
||||||
|
|
||||||
const excelNanoid = customAlphabet(
|
|
||||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
|
|
||||||
12
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a temp token and store export params in Redis.
|
|
||||||
* @param {Object} params - { objectType, filter, sort, order }
|
|
||||||
* @returns {Promise<{ token: string, url: string }>}
|
|
||||||
*/
|
|
||||||
export async function createExcelTempToken(params) {
|
|
||||||
const baseUrl = config.app?.urlApi?.replace(/\/$/, '') || '';
|
|
||||||
if (!baseUrl) {
|
|
||||||
throw new Error('config.app.urlApi is not set; required for Excel temp URLs');
|
|
||||||
}
|
|
||||||
const objectType = params.objectType || 'Export';
|
|
||||||
const now = new Date();
|
|
||||||
const datetime =
|
|
||||||
now.getFullYear() +
|
|
||||||
String(now.getMonth() + 1).padStart(2, '0') +
|
|
||||||
String(now.getDate()).padStart(2, '0') +
|
|
||||||
String(now.getHours()).padStart(2, '0') +
|
|
||||||
String(now.getMinutes()).padStart(2, '0');
|
|
||||||
const token = `${objectType}s-${datetime}-${excelNanoid()}`;
|
|
||||||
const key = EXCEL_TEMP_KEY_PREFIX + token;
|
|
||||||
const stored = { ...params, requestCount: 0 };
|
|
||||||
await redisServer.setKey(key, stored, EXCEL_TEMP_TTL_SECONDS);
|
|
||||||
logger.debug('Stored excel temp token in Redis:', key);
|
|
||||||
const url = `${baseUrl}/excel/temp/${token}.xlsx`;
|
|
||||||
return { token, url };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get export params for a temp token (supports up to 2 requests; requestCount stored in Redis).
|
|
||||||
* @param {string} token
|
|
||||||
* @returns {Promise<Object|null>} { objectType, filter, sort, order } or null
|
|
||||||
*/
|
|
||||||
export async function getExcelTempParams(token) {
|
|
||||||
if (!token) return null;
|
|
||||||
const key = EXCEL_TEMP_KEY_PREFIX + token;
|
|
||||||
const params = await redisServer.getKey(key);
|
|
||||||
if (!params) {
|
|
||||||
logger.debug('Excel temp token not found in Redis:', key);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a value to an Excel cell-friendly format.
|
|
||||||
* Primitives pass through; objects/arrays are stringified; dates are preserved.
|
|
||||||
*/
|
|
||||||
function toExcelValue(val) {
|
|
||||||
if (val === null || val === undefined) return null;
|
|
||||||
if (val instanceof Date) return val;
|
|
||||||
if (typeof val === 'number' || typeof val === 'boolean') return val;
|
|
||||||
if (typeof val === 'string') return val;
|
|
||||||
if (typeof val === 'object') {
|
|
||||||
if (Array.isArray(val)) return val.map(toExcelValue).join(', ');
|
|
||||||
return JSON.stringify(val);
|
|
||||||
}
|
|
||||||
return String(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate an Excel workbook from tabular data.
|
|
||||||
* @param {Array<Object>} data - Array of row objects (keys = column headers)
|
|
||||||
* @param {Object} options - Options
|
|
||||||
* @param {string} [options.sheetName='Export'] - Worksheet name
|
|
||||||
* @param {string[]} [options.columnOrder] - Optional column order (uses Object.keys of first row if not provided)
|
|
||||||
* @returns {Promise<Buffer>} Excel file as buffer
|
|
||||||
*/
|
|
||||||
export async function generateExcelTable(data, options = {}) {
|
|
||||||
const { sheetName = 'Export', columnOrder } = options;
|
|
||||||
|
|
||||||
const workbook = new ExcelJS.Workbook();
|
|
||||||
const worksheet = workbook.addWorksheet(sheetName, {
|
|
||||||
views: [{ state: 'frozen', ySplit: 1 }],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
|
||||||
return Buffer.from(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = columnOrder || Object.keys(data[0]).filter((k) => !k.startsWith('@'));
|
|
||||||
const colCount = keys.length;
|
|
||||||
const rowCount = data.length + 1;
|
|
||||||
|
|
||||||
const toColLetter = (n) => {
|
|
||||||
let s = '';
|
|
||||||
while (n >= 0) {
|
|
||||||
s = String.fromCharCode((n % 26) + 65) + s;
|
|
||||||
n = Math.floor(n / 26) - 1;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
const endCol = toColLetter(colCount - 1);
|
|
||||||
|
|
||||||
const tableRows = data.map((row) => keys.map((key) => toExcelValue(row[key])));
|
|
||||||
worksheet.addTable({
|
|
||||||
name: 'DataTable',
|
|
||||||
ref: `A1:${endCol}${rowCount}`,
|
|
||||||
headerRow: true,
|
|
||||||
style: {
|
|
||||||
theme: 'TableStyleLight1',
|
|
||||||
showRowStripes: true,
|
|
||||||
},
|
|
||||||
columns: keys.map((key) => ({ name: key, filterButton: true })),
|
|
||||||
rows: tableRows,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-fit columns (approximate)
|
|
||||||
worksheet.columns.forEach((col, i) => {
|
|
||||||
let maxLen = keys[i]?.length || 10;
|
|
||||||
worksheet.eachRow({ includeEmpty: false }, (row) => {
|
|
||||||
const cell = row.getCell(i + 1);
|
|
||||||
const val = cell.value;
|
|
||||||
const len = val != null ? String(val).length : 0;
|
|
||||||
maxLen = Math.min(Math.max(maxLen, len), 50);
|
|
||||||
});
|
|
||||||
col.width = maxLen + 2;
|
|
||||||
});
|
|
||||||
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
|
||||||
return Buffer.from(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increment request count for a temp token. Returns new count or null if token not found.
|
|
||||||
* @param {string} token
|
|
||||||
* @returns {Promise<number|null>} New requestCount or null
|
|
||||||
*/
|
|
||||||
export async function incrementExcelTempRequestCount(token) {
|
|
||||||
if (!token) return null;
|
|
||||||
const key = EXCEL_TEMP_KEY_PREFIX + token;
|
|
||||||
const data = await redisServer.getKey(key);
|
|
||||||
if (!data) return null;
|
|
||||||
const requestCount = (data.requestCount ?? 0) + 1;
|
|
||||||
const updated = { ...data, requestCount };
|
|
||||||
await redisServer.setKey(key, updated, EXCEL_TEMP_TTL_SECONDS);
|
|
||||||
logger.debug('Incremented excel temp request count:', key, requestCount);
|
|
||||||
return requestCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteExcelTempToken(token) {
|
|
||||||
const key = EXCEL_TEMP_KEY_PREFIX + token;
|
|
||||||
await redisServer.deleteKey(key);
|
|
||||||
logger.debug('Deleted excel temp token from Redis:', key);
|
|
||||||
}
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
import log4js from 'log4js';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
import config from '../config.js';
|
|
||||||
import { listObjects } from './database.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('DatabaseOData');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
const EXCLUDED_PATHS = ['appPasswordHash', 'secret', '__v'];
|
|
||||||
|
|
||||||
/** Check if path is an embedded object (has nested schema, not ObjectId ref). Excludes arrays. */
|
|
||||||
function isEmbeddedObject(path) {
|
|
||||||
if (!path) return false;
|
|
||||||
if (path.options?.type === mongoose.Schema.Types.ObjectId) return false;
|
|
||||||
if (path.instance === 'Array') return false;
|
|
||||||
return path.schema != null || path.caster?.schema != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get all top-level property keys declared in OData metadata for a schema. */
|
|
||||||
function getDeclaredPropertyKeys(schema) {
|
|
||||||
if (!schema?.paths) return [];
|
|
||||||
return Object.keys(schema.paths)
|
|
||||||
.filter((k) => !k.includes('.'))
|
|
||||||
.filter((k) => !EXCLUDED_PATHS.includes(k))
|
|
||||||
.filter((k) => schema.paths[k]?.options?.select !== false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure all declared properties are present in each document. OData clients (e.g. Excel)
|
|
||||||
* expect every metadata property to exist; omit none. Use null for missing/invalid values.
|
|
||||||
*/
|
|
||||||
function normalizeODataResponse(data, schema) {
|
|
||||||
const declaredKeys = getDeclaredPropertyKeys(schema);
|
|
||||||
if (declaredKeys.length === 0) return data;
|
|
||||||
|
|
||||||
function normalize(doc) {
|
|
||||||
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return doc;
|
|
||||||
const result = { ...doc };
|
|
||||||
for (const key of declaredKeys) {
|
|
||||||
const val = result[key];
|
|
||||||
if (isEmbeddedObject(schema.paths[key])) {
|
|
||||||
// ComplexType: object or null only
|
|
||||||
const isValid = val != null && typeof val === 'object' && !Array.isArray(val);
|
|
||||||
result[key] = isValid ? val : null;
|
|
||||||
} else {
|
|
||||||
// Primitive, array, etc.: value or null (never omit)
|
|
||||||
result[key] = val !== undefined && val !== null ? val : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.isArray(data) ? data.map(normalize) : normalize(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert expanded ObjectId refs ({ _id: x }) back to primitive strings for OData.
|
|
||||||
* OData metadata declares refs as Edm.String; clients expect primitives, not objects.
|
|
||||||
*/
|
|
||||||
function compactObjectIdsForOData(data) {
|
|
||||||
function isExpandedRef(val) {
|
|
||||||
return val && typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length === 1 && '_id' in val;
|
|
||||||
}
|
|
||||||
function compact(val) {
|
|
||||||
if (Array.isArray(val)) return val.map(compact);
|
|
||||||
if (isExpandedRef(val)) return val._id?.toString?.() ?? val._id;
|
|
||||||
if (val instanceof Date || val instanceof Buffer) return val; // pass through primitives
|
|
||||||
if (val && typeof val === 'object') {
|
|
||||||
const result = {};
|
|
||||||
for (const [k, v] of Object.entries(val)) result[k] = compact(v);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
return Array.isArray(data) ? data.map(compact) : compact(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate filter keys ending with ._id for Mongoose compatibility
|
|
||||||
* @param {Object} filter - Filter object (will be mutated)
|
|
||||||
*/
|
|
||||||
function translateFilterKeys(filter) {
|
|
||||||
Object.keys(filter).forEach((key) => {
|
|
||||||
if (key.endsWith('._id')) {
|
|
||||||
const baseKey = key.slice(0, -4);
|
|
||||||
filter[baseKey] = filter[key];
|
|
||||||
delete filter[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List objects with OData-compatible response format.
|
|
||||||
* Works like listObjects but returns { @odata.context, @odata.count?, value }.
|
|
||||||
*
|
|
||||||
* @param {Object} options - Same options as listObjects, plus:
|
|
||||||
* @param {boolean} [options.count=false] - When true, include @odata.count (total before $skip/$top)
|
|
||||||
* @returns {Promise<Object>} OData-formatted response or { error, code } on failure
|
|
||||||
*/
|
|
||||||
export const listObjectsOData = async ({
|
|
||||||
model,
|
|
||||||
populate = [],
|
|
||||||
page = 1,
|
|
||||||
limit = 25,
|
|
||||||
filter = {},
|
|
||||||
sort = '',
|
|
||||||
order = 'ascend',
|
|
||||||
pagination = true,
|
|
||||||
project,
|
|
||||||
count = false,
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
const filterCopy = JSON.parse(JSON.stringify(filter));
|
|
||||||
translateFilterKeys(filterCopy);
|
|
||||||
|
|
||||||
const [data, total] = await Promise.all([
|
|
||||||
listObjects({
|
|
||||||
model,
|
|
||||||
populate,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
filter: filterCopy,
|
|
||||||
sort,
|
|
||||||
order,
|
|
||||||
pagination,
|
|
||||||
project,
|
|
||||||
}),
|
|
||||||
count ? model.countDocuments(filterCopy) : Promise.resolve(undefined),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (data?.error) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip __v from each document (Mongoose version key - causes OData client issues)
|
|
||||||
const sanitizedData = Array.isArray(data)
|
|
||||||
? data.map(({ __v, ...doc }) => doc)
|
|
||||||
: data;
|
|
||||||
|
|
||||||
// Convert expanded ObjectId refs ({ _id: x }) to primitive strings - OData clients expect Edm.String
|
|
||||||
let odataData = compactObjectIdsForOData(sanitizedData);
|
|
||||||
|
|
||||||
// Ensure all declared properties are present; use null for missing values (Excel requires this)
|
|
||||||
odataData = normalizeODataResponse(odataData, model.schema);
|
|
||||||
|
|
||||||
const baseUrl = config.app?.urlApi || '';
|
|
||||||
const modelName = model.modelName;
|
|
||||||
const context = `${baseUrl}/odata/$metadata#${modelName}`;
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
'@odata.context': context,
|
|
||||||
value: odataData,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (count && total !== undefined) {
|
|
||||||
response['@odata.count'] = total;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('OData list error:', error);
|
|
||||||
return { error, code: 500 };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
|
||||||
|
|
||||||
const invoiceOrderItemSchema = new Schema(
|
|
||||||
{
|
|
||||||
orderItem: { type: Schema.Types.ObjectId, ref: 'orderItem', required: true },
|
|
||||||
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
||||||
invoiceAmountWithTax: { type: Number, required: true, default: 0 },
|
|
||||||
invoiceAmount: { type: Number, required: true, default: 0 },
|
|
||||||
invoiceQuantity: { type: Number, required: true, default: 0 },
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const invoiceShipmentSchema = new Schema(
|
|
||||||
{
|
|
||||||
shipment: { type: Schema.Types.ObjectId, ref: 'shipment', required: true },
|
|
||||||
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
||||||
invoiceAmountWithTax: { type: Number, required: true, default: 0 },
|
|
||||||
invoiceAmount: { type: Number, required: true, default: 0 },
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const invoiceSchema = new Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
totalAmount: { type: Number, required: true, default: 0 },
|
|
||||||
totalAmountWithTax: { type: Number, required: true, default: 0 },
|
|
||||||
shippingAmount: { type: Number, required: true, default: 0 },
|
|
||||||
shippingAmountWithTax: { type: Number, required: true, default: 0 },
|
|
||||||
grandTotalAmount: { type: Number, required: true, default: 0 },
|
|
||||||
totalTaxAmount: { type: Number, required: true, default: 0 },
|
|
||||||
from: { type: Schema.Types.ObjectId, ref: 'vendor', required: false },
|
|
||||||
to: { type: Schema.Types.ObjectId, ref: 'client', required: false },
|
|
||||||
state: {
|
|
||||||
type: { type: String, required: true, default: 'draft' },
|
|
||||||
},
|
|
||||||
orderType: { type: String, required: true },
|
|
||||||
order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true },
|
|
||||||
issuedAt: { type: Date, required: false },
|
|
||||||
dueAt: { type: Date, required: false },
|
|
||||||
postedAt: { type: Date, required: false },
|
|
||||||
acknowledgedAt: { type: Date, required: false },
|
|
||||||
paidAt: { type: Date, required: false },
|
|
||||||
cancelledAt: { type: Date, required: false },
|
|
||||||
invoiceOrderItems: [invoiceOrderItemSchema],
|
|
||||||
invoiceShipments: [invoiceShipmentSchema],
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const rollupConfigs = [
|
|
||||||
{
|
|
||||||
name: 'draft',
|
|
||||||
filter: { 'state.type': 'draft' },
|
|
||||||
rollups: [
|
|
||||||
{ name: 'draftCount', property: 'state.type', operation: 'count' },
|
|
||||||
{ name: 'draftGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sent',
|
|
||||||
filter: { 'state.type': 'sent' },
|
|
||||||
rollups: [
|
|
||||||
{ name: 'sentCount', property: 'state.type', operation: 'count' },
|
|
||||||
{ name: 'sentGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'acknowledged',
|
|
||||||
filter: { 'state.type': 'acknowledged' },
|
|
||||||
rollups: [
|
|
||||||
{ name: 'acknowledgedCount', property: 'state.type', operation: 'count' },
|
|
||||||
{ name: 'acknowledgedGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'partiallyPaid',
|
|
||||||
filter: { 'state.type': 'partiallyPaid' },
|
|
||||||
rollups: [
|
|
||||||
{ name: 'partiallyPaidCount', property: 'state.type', operation: 'count' },
|
|
||||||
{ name: 'partiallyPaidGrandTotalAmount', property: 'grandTotalAmount', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'paid',
|
|
||||||
filter: { 'state.type': 'paid' },
|
|
||||||
rollups: [{ name: 'paidCount', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'overdue',
|
|
||||||
filter: { 'state.type': 'overdue' },
|
|
||||||
rollups: [{ name: 'overdueCount', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cancelled',
|
|
||||||
filter: { 'state.type': 'cancelled' },
|
|
||||||
rollups: [{ name: 'cancelledCount', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
invoiceSchema.statics.stats = async function () {
|
|
||||||
const results = await aggregateRollups({
|
|
||||||
model: this,
|
|
||||||
rollupConfigs: rollupConfigs,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transform the results to match the expected format
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
invoiceSchema.statics.history = async function (from, to) {
|
|
||||||
const results = await aggregateRollupsHistory({
|
|
||||||
model: this,
|
|
||||||
startDate: from,
|
|
||||||
endDate: to,
|
|
||||||
rollupConfigs: rollupConfigs,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return time-series data array
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
invoiceSchema.statics.recalculate = async function (invoice, user) {
|
|
||||||
const invoiceId = invoice._id || invoice;
|
|
||||||
if (!invoiceId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate totals from invoiceOrderItems
|
|
||||||
let totalAmount = 0;
|
|
||||||
for (const item of invoice.invoiceOrderItems || []) {
|
|
||||||
totalAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
|
||||||
}
|
|
||||||
let totalAmountWithTax = 0;
|
|
||||||
for (const item of invoice.invoiceOrderItems || []) {
|
|
||||||
totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate shipping totals from invoiceShipments
|
|
||||||
let shippingAmount = 0;
|
|
||||||
for (const item of invoice.invoiceShipments || []) {
|
|
||||||
shippingAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
|
||||||
}
|
|
||||||
let shippingAmountWithTax = 0;
|
|
||||||
for (const item of invoice.invoiceShipments || []) {
|
|
||||||
shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate grand total and tax amount
|
|
||||||
const grandTotalAmount = parseFloat(totalAmountWithTax) + parseFloat(shippingAmountWithTax);
|
|
||||||
const totalTaxAmount =
|
|
||||||
parseFloat(totalAmountWithTax) -
|
|
||||||
parseFloat(totalAmount) +
|
|
||||||
(parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount));
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
totalAmount: parseFloat(totalAmount).toFixed(2),
|
|
||||||
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
|
|
||||||
shippingAmount: parseFloat(shippingAmount).toFixed(2),
|
|
||||||
shippingAmountWithTax: parseFloat(shippingAmountWithTax).toFixed(2),
|
|
||||||
grandTotalAmount: parseFloat(grandTotalAmount).toFixed(2),
|
|
||||||
totalTaxAmount: parseFloat(totalTaxAmount).toFixed(2),
|
|
||||||
};
|
|
||||||
|
|
||||||
await editObject({
|
|
||||||
model: this,
|
|
||||||
id: invoiceId,
|
|
||||||
updateData,
|
|
||||||
user,
|
|
||||||
recalculate: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
invoiceSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
invoiceSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const invoiceModel = mongoose.model('invoice', invoiceSchema);
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js';
|
|
||||||
|
|
||||||
const paymentSchema = new Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
amount: { type: Number, required: true, default: 0 },
|
|
||||||
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false },
|
|
||||||
client: { type: Schema.Types.ObjectId, ref: 'client', required: false },
|
|
||||||
invoice: { type: Schema.Types.ObjectId, ref: 'invoice', required: true },
|
|
||||||
state: {
|
|
||||||
type: { type: String, required: true, default: 'draft' },
|
|
||||||
},
|
|
||||||
paymentDate: { type: Date, required: false },
|
|
||||||
postedAt: { type: Date, required: false },
|
|
||||||
cancelledAt: { type: Date, required: false },
|
|
||||||
paymentMethod: { type: String, required: false },
|
|
||||||
notes: { type: String, required: false },
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const rollupConfigs = [
|
|
||||||
{
|
|
||||||
name: 'draft',
|
|
||||||
filter: { 'state.type': 'draft' },
|
|
||||||
rollups: [
|
|
||||||
{ name: 'draftCount', property: 'state.type', operation: 'count' },
|
|
||||||
{ name: 'draftAmount', property: 'amount', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'posted',
|
|
||||||
filter: { 'state.type': 'posted' },
|
|
||||||
rollups: [
|
|
||||||
{ name: 'postedCount', property: 'state.type', operation: 'count' },
|
|
||||||
{ name: 'postedAmount', property: 'amount', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cancelled',
|
|
||||||
filter: { 'state.type': 'cancelled' },
|
|
||||||
rollups: [
|
|
||||||
{ name: 'cancelledCount', property: 'state.type', operation: 'count' },
|
|
||||||
{ name: 'cancelledAmount', property: 'amount', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
paymentSchema.statics.stats = async function () {
|
|
||||||
const results = await aggregateRollups({
|
|
||||||
model: this,
|
|
||||||
rollupConfigs: rollupConfigs,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transform the results to match the expected format
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
paymentSchema.statics.history = async function (from, to) {
|
|
||||||
const results = await aggregateRollupsHistory({
|
|
||||||
model: this,
|
|
||||||
startDate: from,
|
|
||||||
endDate: to,
|
|
||||||
rollupConfigs: rollupConfigs,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return time-series data array
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
paymentSchema.statics.recalculate = async function (payment, user) {
|
|
||||||
const paymentId = payment._id || payment;
|
|
||||||
if (!paymentId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For payments, the amount is set directly
|
|
||||||
const amount = payment.amount || 0;
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
amount: parseFloat(amount).toFixed(2),
|
|
||||||
};
|
|
||||||
|
|
||||||
await editObject({
|
|
||||||
model: this,
|
|
||||||
id: paymentId,
|
|
||||||
updateData,
|
|
||||||
user,
|
|
||||||
recalculate: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
paymentSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
paymentSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const paymentModel = mongoose.model('payment', paymentSchema);
|
|
||||||
@ -19,7 +19,7 @@ const filamentStockSchema = new Schema(
|
|||||||
net: { type: Number, required: true },
|
net: { type: Number, required: true },
|
||||||
gross: { type: Number, required: true },
|
gross: { type: Number, required: true },
|
||||||
},
|
},
|
||||||
filamentSku: { type: mongoose.Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
filament: { type: mongoose.Schema.Types.ObjectId, ref: 'filament', required: true },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,112 +1,30 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { purchaseOrderModel } from './purchaseorder.schema.js';
|
import { purchaseOrderModel } from './purchaseorder.schema.js';
|
||||||
import { salesOrderModel } from '../sales/salesorder.schema.js';
|
import { aggregateRollups, editObject } from '../../database.js';
|
||||||
import { taxRateModel } from '../management/taxrate.schema.js';
|
|
||||||
import { filamentModel } from '../management/filament.schema.js';
|
|
||||||
import { filamentSkuModel } from '../management/filamentsku.schema.js';
|
|
||||||
import { partModel } from '../management/part.schema.js';
|
|
||||||
import { partSkuModel } from '../management/partsku.schema.js';
|
|
||||||
import { productModel } from '../management/product.schema.js';
|
|
||||||
import { productSkuModel } from '../management/productsku.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;
|
||||||
|
|
||||||
const skuModelsByItemType = {
|
|
||||||
filament: filamentSkuModel,
|
|
||||||
part: partSkuModel,
|
|
||||||
product: productSkuModel,
|
|
||||||
};
|
|
||||||
|
|
||||||
const parentModelsByItemType = {
|
|
||||||
filament: filamentModel,
|
|
||||||
part: partModel,
|
|
||||||
product: productModel,
|
|
||||||
};
|
|
||||||
|
|
||||||
const orderItemSchema = new Schema(
|
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: false },
|
item: { type: Schema.Types.ObjectId, refPath: 'itemType', required: true },
|
||||||
sku: {
|
syncAmount: { type: String, required: true, default: null },
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: function () {
|
|
||||||
return ['filament', 'part', 'product'].includes(this.itemType)
|
|
||||||
? this.itemType + 'Sku'
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
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) {
|
||||||
if (orderItem.orderType !== 'purchaseOrder' && orderItem.orderType !== 'salesOrder') {
|
// Only purchase orders are supported for now
|
||||||
|
if (orderItem.orderType !== 'purchaseOrder') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,85 +33,6 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If SKU present and syncAmount is set, check if override is on for the price mode and use that price instead
|
|
||||||
let effectiveItemAmount = orderItem.itemAmount;
|
|
||||||
const syncAmount = orderItem.syncAmount;
|
|
||||||
const skuId = orderItem.sku?._id || orderItem.sku;
|
|
||||||
const itemType = orderItem.itemType;
|
|
||||||
if (syncAmount && skuId && itemType && ['filament', 'part', 'product'].includes(itemType)) {
|
|
||||||
const skuModel = skuModelsByItemType[itemType];
|
|
||||||
const parentModel = parentModelsByItemType[itemType];
|
|
||||||
if (skuModel && parentModel) {
|
|
||||||
const sku = await getObject({
|
|
||||||
model: skuModel,
|
|
||||||
id: skuId,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
if (sku) {
|
|
||||||
const parentId = sku.part?._id || sku.part || sku.product?._id || sku.product || sku.filament?._id || sku.filament;
|
|
||||||
if (syncAmount === 'itemCost') {
|
|
||||||
if (sku.overrideCost && sku.cost != null) {
|
|
||||||
effectiveItemAmount = sku.cost;
|
|
||||||
} else if (parentId) {
|
|
||||||
const parent = await getObject({
|
|
||||||
model: parentModel,
|
|
||||||
id: parentId,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
if (parent && parent.cost != null) {
|
|
||||||
effectiveItemAmount = parent.cost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (syncAmount === 'itemPrice' && itemType !== 'filament') {
|
|
||||||
if (sku.overridePrice && sku.price != null) {
|
|
||||||
effectiveItemAmount = sku.price;
|
|
||||||
} else if (parentId) {
|
|
||||||
const parent = await getObject({
|
|
||||||
model: parentModel,
|
|
||||||
id: parentId,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
if (parent && parent.price != null) {
|
|
||||||
effectiveItemAmount = parent.price;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let 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 = effectiveItemAmount * orderItem.quantity;
|
|
||||||
const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100);
|
|
||||||
|
|
||||||
const orderItemUpdateData = {
|
|
||||||
totalAmount: orderTotalAmount,
|
|
||||||
totalAmountWithTax: orderTotalAmountWithTax,
|
|
||||||
invoicedAmountRemaining: orderTotalAmount - orderItem.invoicedAmount,
|
|
||||||
invoicedAmountWithTaxRemaining: orderTotalAmountWithTax - orderItem.invoicedAmountWithTax,
|
|
||||||
invoicedQuantityRemaining: orderItem.quantity - orderItem.invoicedQuantity,
|
|
||||||
};
|
|
||||||
if (effectiveItemAmount !== orderItem.itemAmount) {
|
|
||||||
orderItemUpdateData.itemAmount = effectiveItemAmount;
|
|
||||||
orderItem.itemAmount = effectiveItemAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
await editObject({
|
|
||||||
model: this,
|
|
||||||
id: orderItem._id,
|
|
||||||
updateData: orderItemUpdateData,
|
|
||||||
user,
|
|
||||||
recalculate: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rollupResults = await aggregateRollups({
|
const rollupResults = await aggregateRollups({
|
||||||
model: this,
|
model: this,
|
||||||
baseFilter: {
|
baseFilter: {
|
||||||
@ -212,11 +51,6 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'overallCount',
|
|
||||||
rollups: [{ name: 'overallCount', property: '_id', operation: 'count' }],
|
|
||||||
},
|
|
||||||
...rollupConfigs,
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -224,59 +58,14 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
|
|||||||
const totalAmount = totals.totalAmount.sum?.toFixed(2) || 0;
|
const totalAmount = totals.totalAmount.sum?.toFixed(2) || 0;
|
||||||
const totalAmountWithTax = totals.totalAmountWithTax.sum?.toFixed(2) || 0;
|
const totalAmountWithTax = totals.totalAmountWithTax.sum?.toFixed(2) || 0;
|
||||||
|
|
||||||
const orderModel = orderItem.orderType === 'purchaseOrder' ? purchaseOrderModel : salesOrderModel;
|
|
||||||
const order = await getObject({
|
|
||||||
model: orderModel,
|
|
||||||
id: orderId,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const grandTotalAmount =
|
|
||||||
parseFloat(totalAmountWithTax || 0) + parseFloat(order.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 (orderItem.orderType === 'purchaseOrder') {
|
|
||||||
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' } };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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: 'partiallyDelivered' } };
|
|
||||||
}
|
|
||||||
if (receivedCount > 0 && receivedCount == overallCount) {
|
|
||||||
updateData = { ...updateData, state: { type: 'delivered' } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await editObject({
|
await editObject({
|
||||||
model: orderModel,
|
model: purchaseOrderModel,
|
||||||
id: orderId,
|
id: orderId,
|
||||||
updateData: updateData,
|
updateData: {
|
||||||
|
totalAmount: parseFloat(totalAmount),
|
||||||
|
totalAmountWithTax: parseFloat(totalAmountWithTax),
|
||||||
|
totalTaxAmount: parseFloat(totalAmountWithTax - totalAmount),
|
||||||
|
},
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,7 +11,7 @@ const partStockSchema = new Schema(
|
|||||||
type: { type: String, required: true },
|
type: { type: String, required: true },
|
||||||
progress: { type: Number, required: false },
|
progress: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
partSku: { type: mongoose.Schema.Types.ObjectId, ref: 'partSku', required: true },
|
part: { type: mongoose.Schema.Types.ObjectId, ref: 'part', required: true },
|
||||||
currentQuantity: { type: Number, required: true },
|
currentQuantity: { type: Number, required: true },
|
||||||
sourceType: { type: String, required: true },
|
sourceType: { type: String, required: true },
|
||||||
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },
|
source: { type: Schema.Types.ObjectId, refPath: 'sourceType', required: true },
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
|
||||||
|
|
||||||
const partStockUsageSchema = new Schema({
|
|
||||||
partStock: { type: Schema.Types.ObjectId, ref: 'partStock', required: false },
|
|
||||||
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
|
||||||
quantity: { type: Number, required: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the main productStock schema - tracks assembled products consisting of part stocks
|
|
||||||
const productStockSchema = new Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
state: {
|
|
||||||
type: { type: String, required: true, default: 'draft' },
|
|
||||||
progress: { type: Number, required: false },
|
|
||||||
},
|
|
||||||
postedAt: { type: Date, required: false },
|
|
||||||
productSku: { type: mongoose.Schema.Types.ObjectId, ref: 'productSku', required: true },
|
|
||||||
currentQuantity: { type: Number, required: true },
|
|
||||||
partStocks: [partStockUsageSchema],
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const rollupConfigs = [
|
|
||||||
{
|
|
||||||
name: 'totalCurrentQuantity',
|
|
||||||
filter: {},
|
|
||||||
rollups: [{ name: 'totalCurrentQuantity', property: 'currentQuantity', operation: 'sum' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'draft',
|
|
||||||
filter: { 'state.type': 'draft' },
|
|
||||||
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'posted',
|
|
||||||
filter: { 'state.type': 'posted' },
|
|
||||||
rollups: [{ name: 'posted', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
productStockSchema.statics.stats = async function () {
|
|
||||||
const results = await aggregateRollups({
|
|
||||||
model: this,
|
|
||||||
rollupConfigs: rollupConfigs,
|
|
||||||
});
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
productStockSchema.statics.history = async function (from, to) {
|
|
||||||
const results = await aggregateRollupsHistory({
|
|
||||||
model: this,
|
|
||||||
startDate: from,
|
|
||||||
endDate: to,
|
|
||||||
rollupConfigs: rollupConfigs,
|
|
||||||
});
|
|
||||||
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
productStockSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
productStockSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const productStockModel = mongoose.model('productStock', productStockSchema);
|
|
||||||
@ -1,100 +1,22 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { aggregateRollups, aggregateRollupsHistory } from '../../database.js';
|
|
||||||
|
|
||||||
const purchaseOrderSchema = new Schema(
|
const purchaseOrderSchema = new Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
totalAmount: { type: Number, required: true, default: 0 },
|
totalAmount: { type: Number, required: true },
|
||||||
totalAmountWithTax: { type: Number, required: true, default: 0 },
|
totalAmountWithTax: { type: Number, required: true },
|
||||||
shippingAmount: { type: Number, required: true, default: 0 },
|
totalTaxAmount: { type: Number, required: true },
|
||||||
shippingAmountWithTax: { type: Number, required: true, default: 0 },
|
|
||||||
grandTotalAmount: { type: Number, required: true, default: 0 },
|
|
||||||
totalTaxAmount: { type: Number, required: true, default: 0 },
|
|
||||||
timestamp: { type: Date, default: Date.now },
|
timestamp: { type: Date, default: Date.now },
|
||||||
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
||||||
state: {
|
state: {
|
||||||
type: { type: String, required: true, default: 'draft' },
|
type: { type: String, required: true, default: 'draft' },
|
||||||
},
|
},
|
||||||
postedAt: { type: Date, required: false },
|
|
||||||
acknowledgedAt: { type: Date, required: false },
|
|
||||||
cancelledAt: { type: Date, required: false },
|
|
||||||
completedAt: { type: Date, required: false },
|
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const rollupConfigs = [
|
|
||||||
{
|
|
||||||
name: 'draft',
|
|
||||||
filter: { 'state.type': 'draft' },
|
|
||||||
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sent',
|
|
||||||
filter: { 'state.type': 'sent' },
|
|
||||||
rollups: [{ name: 'sent', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'acknowledged',
|
|
||||||
filter: { 'state.type': 'acknowledged' },
|
|
||||||
rollups: [{ name: 'acknowledged', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'partiallyShipped',
|
|
||||||
filter: { 'state.type': 'partiallyShipped' },
|
|
||||||
rollups: [{ name: 'partiallyShipped', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'shipped',
|
|
||||||
filter: { 'state.type': 'shipped' },
|
|
||||||
rollups: [{ name: 'shipped', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'partiallyReceived',
|
|
||||||
filter: { 'state.type': 'partiallyReceived' },
|
|
||||||
rollups: [{ name: 'partiallyReceived', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'received',
|
|
||||||
filter: { 'state.type': 'received' },
|
|
||||||
rollups: [{ name: 'received', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'cancelled',
|
|
||||||
filter: { 'state.type': 'cancelled' },
|
|
||||||
rollups: [{ name: 'cancelled', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'completed',
|
|
||||||
filter: { 'state.type': 'completed' },
|
|
||||||
rollups: [{ name: 'completed', property: 'state.type', operation: 'count' }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
purchaseOrderSchema.statics.stats = async function () {
|
|
||||||
const results = await aggregateRollups({
|
|
||||||
model: this,
|
|
||||||
rollupConfigs: rollupConfigs,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Transform the results to match the expected format
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
purchaseOrderSchema.statics.history = async function (from, to) {
|
|
||||||
const results = await aggregateRollupsHistory({
|
|
||||||
model: this,
|
|
||||||
startDate: from,
|
|
||||||
endDate: to,
|
|
||||||
rollupConfigs: rollupConfigs,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return time-series data array
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add virtual id getter
|
// Add virtual id getter
|
||||||
purchaseOrderSchema.virtual('id').get(function () {
|
purchaseOrderSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
|
|||||||
@ -1,117 +1,43 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
import { purchaseOrderModel } from './purchaseorder.schema.js';
|
|
||||||
import { salesOrderModel } from '../sales/salesorder.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()() },
|
||||||
orderType: { type: String, required: true },
|
purchaseOrder: { type: Schema.Types.ObjectId, ref: 'purchaseOrder', required: true },
|
||||||
order: { type: Schema.Types.ObjectId, refPath: 'orderType', required: true },
|
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
||||||
courierService: { type: Schema.Types.ObjectId, ref: 'courierService', required: false },
|
courierService: { type: Schema.Types.ObjectId, ref: 'courierService', required: false },
|
||||||
trackingNumber: { type: String, required: false },
|
trackingNumber: { type: String, required: false },
|
||||||
amount: { type: Number, required: true },
|
items: [shipmentItemSchema],
|
||||||
amountWithTax: { type: Number, required: true },
|
cost: { net: { type: Number, required: true }, gross: { type: Number, required: true } },
|
||||||
taxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
shippedDate: { type: Date, required: false },
|
||||||
invoicedAmount: { type: Number, required: false, default: 0 },
|
expectedDeliveryDate: { type: Date, required: false },
|
||||||
invoicedAmountWithTax: { type: Number, required: false, default: 0 },
|
actualDeliveryDate: { type: Date, required: false },
|
||||||
invoicedAmountRemaining: { type: Number, required: false, default: 0 },
|
|
||||||
invoicedAmountWithTaxRemaining: { type: Number, required: false, default: 0 },
|
|
||||||
shippedAt: { type: Date, required: false },
|
|
||||||
expectedAt: { type: Date, required: false },
|
|
||||||
deliveredAt: { type: Date, required: false },
|
|
||||||
cancelledAt: { type: Date, required: false },
|
|
||||||
state: {
|
state: {
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
default: 'pending',
|
||||||
|
enum: ['pending', 'shipped', 'in_transit', 'delivered', 'cancelled'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
notes: { type: String },
|
||||||
|
timestamp: { type: Date, default: Date.now },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
shipmentSchema.statics.recalculate = async function (shipment, user) {
|
|
||||||
if (shipment.orderType !== 'purchaseOrder' && shipment.orderType !== 'salesOrder') {
|
|
||||||
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 orderModel = shipment.orderType === 'purchaseOrder' ? purchaseOrderModel : salesOrderModel;
|
|
||||||
const order = await getObject({
|
|
||||||
model: orderModel,
|
|
||||||
id: orderId,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const grandTotalAmount =
|
|
||||||
parseFloat(order.totalAmountWithTax || 0) + parseFloat(totalShippingAmountWithTax || 0);
|
|
||||||
await editObject({
|
|
||||||
model: orderModel,
|
|
||||||
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;
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const appPasswordSchema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
name: { type: String, required: true },
|
|
||||||
user: { type: Schema.Types.ObjectId, ref: 'user', required: true },
|
|
||||||
active: { type: Boolean, required: true, default: true },
|
|
||||||
secret: { type: String, required: true, select: false },
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
appPasswordSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
appPasswordSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
export const appPasswordModel = mongoose.model('appPassword', appPasswordSchema);
|
|
||||||
@ -2,21 +2,24 @@ import mongoose from 'mongoose';
|
|||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
// Filament base - cost and tax; color and cost override at FilamentSKU
|
|
||||||
const filamentSchema = new mongoose.Schema({
|
const filamentSchema = new mongoose.Schema({
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
name: { required: true, type: String },
|
name: { required: true, type: String },
|
||||||
barcode: { required: false, type: String },
|
barcode: { required: false, type: String },
|
||||||
url: { required: false, type: String },
|
url: { required: false, type: String },
|
||||||
image: { required: false, type: Buffer },
|
image: { required: false, type: Buffer },
|
||||||
material: { type: Schema.Types.ObjectId, ref: 'material', required: true },
|
color: { required: true, type: String },
|
||||||
|
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
||||||
|
type: { required: true, type: String },
|
||||||
|
cost: { required: true, type: Number },
|
||||||
|
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: true },
|
||||||
|
costWithTax: { required: true, type: Number },
|
||||||
diameter: { required: true, type: Number },
|
diameter: { required: true, type: Number },
|
||||||
density: { required: true, type: Number },
|
density: { required: true, type: Number },
|
||||||
|
createdAt: { required: true, type: Date },
|
||||||
|
updatedAt: { required: true, type: Date },
|
||||||
emptySpoolWeight: { required: true, type: Number },
|
emptySpoolWeight: { required: true, type: Number },
|
||||||
cost: { type: Number, required: false },
|
});
|
||||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
||||||
costWithTax: { type: Number, required: false },
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
filamentSchema.virtual('id').get(function () {
|
filamentSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
@ -24,21 +27,4 @@ filamentSchema.virtual('id').get(function () {
|
|||||||
|
|
||||||
filamentSchema.set('toJSON', { virtuals: true });
|
filamentSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
filamentSchema.statics.recalculate = async function (filament, user) {
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
|
||||||
const itemId = filament._id;
|
|
||||||
const draftOrderItems = await orderItemModel
|
|
||||||
.find({
|
|
||||||
'state.type': 'draft',
|
|
||||||
itemType: 'filament',
|
|
||||||
item: itemId,
|
|
||||||
})
|
|
||||||
.populate('order')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
for (const orderItem of draftOrderItems) {
|
|
||||||
await orderItemModel.recalculate(orderItem, user);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filamentModel = mongoose.model('filament', filamentSchema);
|
export const filamentModel = mongoose.model('filament', filamentSchema);
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
// Define the main filament SKU schema - color and cost live at SKU level
|
|
||||||
const filamentSkuSchema = new Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
barcode: { type: String, required: false },
|
|
||||||
filament: { type: Schema.Types.ObjectId, ref: 'filament', required: true },
|
|
||||||
name: { type: String, required: true },
|
|
||||||
description: { type: String, required: false },
|
|
||||||
color: { type: String, required: true },
|
|
||||||
cost: { type: Number, required: false },
|
|
||||||
overrideCost: { type: Boolean, default: false },
|
|
||||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
||||||
costWithTax: { type: Number, required: false },
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
filamentSkuSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
filamentSkuSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
filamentSkuSchema.statics.recalculate = async function (filamentSku, user) {
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
|
||||||
const skuId = filamentSku._id;
|
|
||||||
const draftOrderItems = await orderItemModel
|
|
||||||
.find({
|
|
||||||
'state.type': 'draft',
|
|
||||||
itemType: 'filament',
|
|
||||||
sku: skuId,
|
|
||||||
})
|
|
||||||
.populate('order')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
for (const orderItem of draftOrderItems) {
|
|
||||||
await orderItemModel.recalculate(orderItem, user);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const filamentSkuModel = mongoose.model('filamentSku', filamentSkuSchema);
|
|
||||||
@ -1,15 +1,13 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
|
|
||||||
const materialSchema = new mongoose.Schema(
|
const materialSchema = new mongoose.Schema({
|
||||||
{
|
_reference: { type: String, default: () => generateId()() },
|
||||||
_reference: { type: String, default: () => generateId()() },
|
name: { required: true, type: String },
|
||||||
name: { required: true, type: String },
|
url: { required: false, type: String },
|
||||||
url: { required: false, type: String },
|
image: { required: false, type: Buffer },
|
||||||
tags: [{ type: String }],
|
tags: [{ type: String }],
|
||||||
},
|
});
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
materialSchema.virtual('id').get(function () {
|
materialSchema.virtual('id').get(function () {
|
||||||
return this._id;
|
return this._id;
|
||||||
|
|||||||
@ -2,22 +2,22 @@ import mongoose from 'mongoose';
|
|||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
// Define the main part schema - cost/price and tax; override at PartSku
|
// Define the main part schema
|
||||||
const partSchema = new Schema(
|
const partSchema = new Schema(
|
||||||
{
|
{
|
||||||
_reference: { type: String, default: () => generateId()() },
|
_reference: { type: String, default: () => generateId()() },
|
||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
fileName: { type: String, required: false },
|
fileName: { type: String, required: false },
|
||||||
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
|
|
||||||
cost: { type: Number, required: false },
|
|
||||||
price: { type: Number, required: false },
|
|
||||||
priceMode: { type: String, default: 'margin' },
|
priceMode: { type: String, default: 'margin' },
|
||||||
|
price: { type: Number, required: true },
|
||||||
|
cost: { type: Number, required: true },
|
||||||
margin: { type: Number, required: false },
|
margin: { type: Number, required: false },
|
||||||
amount: { type: Number, required: false },
|
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
||||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
|
||||||
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
costWithTax: { type: Number, required: false },
|
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
priceWithTax: { type: Number, required: false },
|
priceWithTax: { type: Number, required: false },
|
||||||
|
costWithTax: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
@ -30,22 +30,5 @@ partSchema.virtual('id').get(function () {
|
|||||||
// Configure JSON serialization to include virtuals
|
// Configure JSON serialization to include virtuals
|
||||||
partSchema.set('toJSON', { virtuals: true });
|
partSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
partSchema.statics.recalculate = async function (part, user) {
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
|
||||||
const itemId = part._id;
|
|
||||||
const draftOrderItems = await orderItemModel
|
|
||||||
.find({
|
|
||||||
'state.type': 'draft',
|
|
||||||
itemType: 'part',
|
|
||||||
item: itemId,
|
|
||||||
})
|
|
||||||
.populate('order')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
for (const orderItem of draftOrderItems) {
|
|
||||||
await orderItemModel.recalculate(orderItem, user);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create and export the model
|
// Create and export the model
|
||||||
export const partModel = mongoose.model('part', partSchema);
|
export const partModel = mongoose.model('part', partSchema);
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
// Define the main part SKU schema - pricing lives at SKU level
|
|
||||||
const partSkuSchema = new Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
barcode: { type: String, required: false },
|
|
||||||
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
|
||||||
name: { type: String, required: true },
|
|
||||||
description: { type: String, required: false },
|
|
||||||
priceMode: { type: String, default: 'margin' },
|
|
||||||
price: { type: Number, required: false },
|
|
||||||
cost: { type: Number, required: false },
|
|
||||||
overrideCost: { type: Boolean, default: false },
|
|
||||||
overridePrice: { type: Boolean, default: false },
|
|
||||||
margin: { type: Number, required: false },
|
|
||||||
amount: { type: Number, required: false },
|
|
||||||
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
||||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
||||||
priceWithTax: { type: Number, required: false },
|
|
||||||
costWithTax: { type: Number, required: false },
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
partSkuSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
partSkuSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
partSkuSchema.statics.recalculate = async function (partSku, user) {
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
|
||||||
const skuId = partSku._id;
|
|
||||||
const draftOrderItems = await orderItemModel
|
|
||||||
.find({
|
|
||||||
'state.type': 'draft',
|
|
||||||
itemType: 'part',
|
|
||||||
sku: skuId,
|
|
||||||
})
|
|
||||||
.populate('order')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
for (const orderItem of draftOrderItems) {
|
|
||||||
await orderItemModel.recalculate(orderItem, user);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const partSkuModel = mongoose.model('partSku', partSkuSchema);
|
|
||||||
@ -2,6 +2,11 @@ import mongoose from 'mongoose';
|
|||||||
import { generateId } from '../../utils.js';
|
import { generateId } from '../../utils.js';
|
||||||
const { Schema } = mongoose;
|
const { Schema } = mongoose;
|
||||||
|
|
||||||
|
const partSchema = new Schema({
|
||||||
|
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
||||||
|
quantity: { type: Number, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
// Define the main product schema
|
// Define the main product schema
|
||||||
const productSchema = new Schema(
|
const productSchema = new Schema(
|
||||||
{
|
{
|
||||||
@ -9,16 +14,13 @@ const productSchema = new Schema(
|
|||||||
name: { type: String, required: true },
|
name: { type: String, required: true },
|
||||||
tags: [{ type: String }],
|
tags: [{ type: String }],
|
||||||
version: { type: String },
|
version: { type: String },
|
||||||
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
|
||||||
cost: { type: Number, required: false },
|
|
||||||
price: { type: Number, required: false },
|
|
||||||
priceMode: { type: String, default: 'margin' },
|
priceMode: { type: String, default: 'margin' },
|
||||||
margin: { type: Number, required: false },
|
margin: { type: Number, required: false },
|
||||||
amount: { type: Number, required: false },
|
amount: { type: Number, required: false },
|
||||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: true },
|
||||||
|
parts: [partSchema],
|
||||||
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
costWithTax: { type: Number, required: false },
|
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
||||||
priceWithTax: { type: Number, required: false },
|
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
@ -30,22 +32,5 @@ productSchema.virtual('id').get(function () {
|
|||||||
// Configure JSON serialization to include virtuals
|
// Configure JSON serialization to include virtuals
|
||||||
productSchema.set('toJSON', { virtuals: true });
|
productSchema.set('toJSON', { virtuals: true });
|
||||||
|
|
||||||
productSchema.statics.recalculate = async function (product, user) {
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
|
||||||
const itemId = product._id;
|
|
||||||
const draftOrderItems = await orderItemModel
|
|
||||||
.find({
|
|
||||||
'state.type': 'draft',
|
|
||||||
itemType: 'product',
|
|
||||||
item: itemId,
|
|
||||||
})
|
|
||||||
.populate('order')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
for (const orderItem of draftOrderItems) {
|
|
||||||
await orderItemModel.recalculate(orderItem, user);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create and export the model
|
// Create and export the model
|
||||||
export const productModel = mongoose.model('product', productSchema);
|
export const productModel = mongoose.model('product', productSchema);
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const partSkuUsageSchema = new Schema({
|
|
||||||
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
|
||||||
quantity: { type: Number, required: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the main product SKU schema
|
|
||||||
const productSkuSchema = new Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
barcode: { type: String, required: false },
|
|
||||||
product: { type: Schema.Types.ObjectId, ref: 'product', required: true },
|
|
||||||
name: { type: String, required: true },
|
|
||||||
description: { type: String, required: false },
|
|
||||||
priceMode: { type: String, default: 'margin' },
|
|
||||||
price: { type: Number, required: false },
|
|
||||||
cost: { type: Number, required: false },
|
|
||||||
overrideCost: { type: Boolean, default: false },
|
|
||||||
overridePrice: { type: Boolean, default: false },
|
|
||||||
margin: { type: Number, required: false },
|
|
||||||
amount: { type: Number, required: false },
|
|
||||||
parts: [partSkuUsageSchema],
|
|
||||||
priceTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
||||||
costTaxRate: { type: Schema.Types.ObjectId, ref: 'taxRate', required: false },
|
|
||||||
priceWithTax: { type: Number, required: false },
|
|
||||||
costWithTax: { type: Number, required: false },
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add virtual id getter
|
|
||||||
productSkuSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure JSON serialization to include virtuals
|
|
||||||
productSkuSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
productSkuSchema.statics.recalculate = async function (productSku, user) {
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
|
||||||
const skuId = productSku._id;
|
|
||||||
const draftOrderItems = await orderItemModel
|
|
||||||
.find({
|
|
||||||
'state.type': 'draft',
|
|
||||||
itemType: 'product',
|
|
||||||
sku: skuId,
|
|
||||||
})
|
|
||||||
.populate('order')
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
for (const orderItem of draftOrderItems) {
|
|
||||||
await orderItemModel.recalculate(orderItem, user);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create and export the model
|
|
||||||
export const productSkuModel = mongoose.model('productSku', productSkuSchema);
|
|
||||||
@ -10,7 +10,6 @@ const userSchema = new mongoose.Schema(
|
|||||||
lastName: { required: false, type: String },
|
lastName: { required: false, type: String },
|
||||||
email: { required: true, type: String },
|
email: { required: true, type: String },
|
||||||
profileImage: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
|
profileImage: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
|
||||||
appPasswordHash: { type: String, required: false, select: false },
|
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const notificationSchema = new mongoose.Schema({
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
user: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: 'user',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
default: 'info',
|
|
||||||
},
|
|
||||||
read: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: Date,
|
|
||||||
required: true,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: Date,
|
|
||||||
required: true,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
export const notificationModel = mongoose.model('notification', notificationSchema);
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
|
|
||||||
const userNotifierSchema = new mongoose.Schema({
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
user: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: 'user',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
object: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
refPath: 'objectType',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
objectType: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
createdAt: {
|
|
||||||
type: Date,
|
|
||||||
required: true,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
updatedAt: {
|
|
||||||
type: Date,
|
|
||||||
required: true,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
userNotifierSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
userNotifierSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
export const userNotifierModel = mongoose.model('userNotifier', userNotifierSchema);
|
|
||||||
@ -2,28 +2,20 @@ import { jobModel } from './production/job.schema.js';
|
|||||||
import { subJobModel } from './production/subjob.schema.js';
|
import { subJobModel } from './production/subjob.schema.js';
|
||||||
import { printerModel } from './production/printer.schema.js';
|
import { printerModel } from './production/printer.schema.js';
|
||||||
import { filamentModel } from './management/filament.schema.js';
|
import { filamentModel } from './management/filament.schema.js';
|
||||||
import { filamentSkuModel } from './management/filamentsku.schema.js';
|
|
||||||
import { gcodeFileModel } from './production/gcodefile.schema.js';
|
import { gcodeFileModel } from './production/gcodefile.schema.js';
|
||||||
import { partModel } from './management/part.schema.js';
|
import { partModel } from './management/part.schema.js';
|
||||||
import { partSkuModel } from './management/partsku.schema.js';
|
|
||||||
import { productModel } from './management/product.schema.js';
|
import { productModel } from './management/product.schema.js';
|
||||||
import { productSkuModel } from './management/productsku.schema.js';
|
|
||||||
import { vendorModel } from './management/vendor.schema.js';
|
import { vendorModel } from './management/vendor.schema.js';
|
||||||
import { materialModel } from './management/material.schema.js';
|
|
||||||
import { filamentStockModel } from './inventory/filamentstock.schema.js';
|
import { filamentStockModel } from './inventory/filamentstock.schema.js';
|
||||||
import { purchaseOrderModel } from './inventory/purchaseorder.schema.js';
|
import { purchaseOrderModel } from './inventory/purchaseorder.schema.js';
|
||||||
import { orderItemModel } from './inventory/orderitem.schema.js';
|
import { orderItemModel } from './inventory/orderitem.schema.js';
|
||||||
import { stockEventModel } from './inventory/stockevent.schema.js';
|
import { stockEventModel } from './inventory/stockevent.schema.js';
|
||||||
import { stockAuditModel } from './inventory/stockaudit.schema.js';
|
import { stockAuditModel } from './inventory/stockaudit.schema.js';
|
||||||
import { partStockModel } from './inventory/partstock.schema.js';
|
import { partStockModel } from './inventory/partstock.schema.js';
|
||||||
import { productStockModel } from './inventory/productstock.schema.js';
|
|
||||||
import { auditLogModel } from './management/auditlog.schema.js';
|
import { auditLogModel } from './management/auditlog.schema.js';
|
||||||
import { userModel } from './management/user.schema.js';
|
import { userModel } from './management/user.schema.js';
|
||||||
import { appPasswordModel } from './management/apppassword.schema.js';
|
|
||||||
import { noteTypeModel } from './management/notetype.schema.js';
|
import { noteTypeModel } from './management/notetype.schema.js';
|
||||||
import { noteModel } from './misc/note.schema.js';
|
import { noteModel } from './misc/note.schema.js';
|
||||||
import { notificationModel } from './misc/notification.schema.js';
|
|
||||||
import { userNotifierModel } from './misc/usernotifier.schema.js';
|
|
||||||
import { documentSizeModel } from './management/documentsize.schema.js';
|
import { documentSizeModel } from './management/documentsize.schema.js';
|
||||||
import { documentTemplateModel } from './management/documenttemplate.schema.js';
|
import { documentTemplateModel } from './management/documenttemplate.schema.js';
|
||||||
import { hostModel } from './management/host.schema.js';
|
import { hostModel } from './management/host.schema.js';
|
||||||
@ -32,295 +24,78 @@ import { documentJobModel } from './management/documentjob.schema.js';
|
|||||||
import { fileModel } from './management/file.schema.js';
|
import { fileModel } from './management/file.schema.js';
|
||||||
import { courierServiceModel } from './management/courierservice.schema.js';
|
import { courierServiceModel } from './management/courierservice.schema.js';
|
||||||
import { courierModel } from './management/courier.schema.js';
|
import { courierModel } from './management/courier.schema.js';
|
||||||
import { taxRateModel } from './management/taxrate.schema.js';
|
import { taxRateModel } from './management/taxrates.schema.js';
|
||||||
import { taxRecordModel } from './management/taxrecord.schema.js';
|
import { taxRecordModel } from './management/taxrecord.schema.js';
|
||||||
import { shipmentModel } from './inventory/shipment.schema.js';
|
|
||||||
import { invoiceModel } from './finance/invoice.schema.js';
|
|
||||||
import { clientModel } from './sales/client.schema.js';
|
|
||||||
import { salesOrderModel } from './sales/salesorder.schema.js';
|
|
||||||
import { marketplaceModel } from './sales/marketplace.schema.js';
|
|
||||||
|
|
||||||
// Map prefixes to models and id fields
|
// Map prefixes to models and id fields
|
||||||
export const models = {
|
export const models = {
|
||||||
PRN: {
|
PRN: { model: printerModel, idField: '_id', type: 'printer', referenceField: '_reference' },
|
||||||
model: printerModel,
|
FIL: { model: filamentModel, idField: '_id', type: 'filament', referenceField: '_reference' },
|
||||||
idField: '_id',
|
GCF: { model: gcodeFileModel, idField: '_id', type: 'gcodeFile', referenceField: '_reference' },
|
||||||
type: 'printer',
|
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference' },
|
||||||
referenceField: '_reference',
|
PRT: { model: partModel, idField: '_id', type: 'part', referenceField: '_reference' },
|
||||||
label: 'Printer',
|
PRD: { model: productModel, idField: '_id', type: 'product', referenceField: '_reference' },
|
||||||
},
|
VEN: { model: vendorModel, idField: '_id', type: 'vendor', referenceField: '_reference' },
|
||||||
FIL: {
|
SJB: { model: subJobModel, idField: '_id', type: 'subJob', referenceField: '_reference' },
|
||||||
model: filamentModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'filament',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Filament',
|
|
||||||
},
|
|
||||||
FSU: {
|
|
||||||
model: filamentSkuModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'filamentSku',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Filament SKU',
|
|
||||||
},
|
|
||||||
GCF: {
|
|
||||||
model: gcodeFileModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'gcodeFile',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'G-Code File',
|
|
||||||
},
|
|
||||||
JOB: { model: jobModel, idField: '_id', type: 'job', referenceField: '_reference', label: 'Job' },
|
|
||||||
PRT: {
|
|
||||||
model: partModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'part',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Part',
|
|
||||||
},
|
|
||||||
PSU: {
|
|
||||||
model: partSkuModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'partSku',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Part SKU',
|
|
||||||
},
|
|
||||||
PRD: {
|
|
||||||
model: productModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'product',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Product',
|
|
||||||
},
|
|
||||||
SKU: {
|
|
||||||
model: productSkuModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'productSku',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Product SKU',
|
|
||||||
},
|
|
||||||
VEN: {
|
|
||||||
model: vendorModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'vendor',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Vendor',
|
|
||||||
},
|
|
||||||
MAT: {
|
|
||||||
model: materialModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'material',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Material',
|
|
||||||
},
|
|
||||||
SJB: {
|
|
||||||
model: subJobModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'subJob',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Sub Job',
|
|
||||||
},
|
|
||||||
FLS: {
|
FLS: {
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'filamentStock',
|
type: 'filamentStock',
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
label: 'Filament Stock',
|
|
||||||
},
|
|
||||||
SEV: {
|
|
||||||
model: stockEventModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'stockEvent',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Stock Event',
|
|
||||||
},
|
|
||||||
SAU: {
|
|
||||||
model: stockAuditModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'stockAudit',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Stock Audit',
|
|
||||||
},
|
|
||||||
PTS: {
|
|
||||||
model: partStockModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'partStock',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Part Stock',
|
|
||||||
},
|
|
||||||
PDS: {
|
|
||||||
model: productStockModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'productStock',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Product Stock',
|
|
||||||
},
|
|
||||||
ADL: {
|
|
||||||
model: auditLogModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'auditLog',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Audit Log',
|
|
||||||
},
|
|
||||||
USR: {
|
|
||||||
model: userModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'user',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'User',
|
|
||||||
},
|
|
||||||
APP: {
|
|
||||||
model: appPasswordModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'appPassword',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'App Password',
|
|
||||||
},
|
|
||||||
NTY: {
|
|
||||||
model: noteTypeModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'noteType',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Note Type',
|
|
||||||
},
|
|
||||||
NTE: {
|
|
||||||
model: noteModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'note',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Note',
|
|
||||||
},
|
|
||||||
NTF: {
|
|
||||||
model: notificationModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'notification',
|
|
||||||
label: 'Notification',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
ONF: {
|
|
||||||
model: userNotifierModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'userNotifier',
|
|
||||||
label: 'User Notifier',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
},
|
||||||
|
SEV: { model: stockEventModel, idField: '_id', type: 'stockEvent', referenceField: '_reference' },
|
||||||
|
SAU: { model: stockAuditModel, idField: '_id', type: 'stockAudit', referenceField: '_reference' },
|
||||||
|
PTS: { model: partStockModel, idField: '_id', type: 'partStock', referenceField: '_reference' },
|
||||||
|
PDS: { model: null, idField: '_id', type: 'productStock', referenceField: '_reference' }, // No productStockModel found
|
||||||
|
ADL: { model: auditLogModel, idField: '_id', type: 'auditLog', referenceField: '_reference' },
|
||||||
|
USR: { model: userModel, idField: '_id', type: 'user', referenceField: '_reference' },
|
||||||
|
NTY: { model: noteTypeModel, idField: '_id', type: 'noteType', referenceField: '_reference' },
|
||||||
|
NTE: { model: noteModel, idField: '_id', type: 'note', referenceField: '_reference' },
|
||||||
DSZ: {
|
DSZ: {
|
||||||
model: documentSizeModel,
|
model: documentSizeModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentSize',
|
type: 'documentSize',
|
||||||
label: 'Document Size',
|
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DTP: {
|
DTP: {
|
||||||
model: documentTemplateModel,
|
model: documentTemplateModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentTemplate',
|
type: 'documentTemplate',
|
||||||
label: 'Document Template',
|
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DPR: {
|
DPR: {
|
||||||
model: documentPrinterModel,
|
model: documentPrinterModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentPrinter',
|
type: 'documentPrinter',
|
||||||
label: 'Document Printer',
|
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
DJB: {
|
DJB: {
|
||||||
model: documentJobModel,
|
model: documentJobModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'documentJob',
|
type: 'documentJob',
|
||||||
label: 'Document Job',
|
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
HST: {
|
HST: { model: hostModel, idField: '_id', type: 'host', referenceField: '_reference' },
|
||||||
model: hostModel,
|
FLE: { model: fileModel, idField: '_id', type: 'file', referenceField: '_reference' },
|
||||||
idField: '_id',
|
|
||||||
type: 'host',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'Host',
|
|
||||||
},
|
|
||||||
FLE: {
|
|
||||||
model: fileModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'file',
|
|
||||||
referenceField: '_reference',
|
|
||||||
label: 'File',
|
|
||||||
},
|
|
||||||
POR: {
|
POR: {
|
||||||
model: purchaseOrderModel,
|
model: purchaseOrderModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'purchaseOrder',
|
type: 'purchaseOrder',
|
||||||
label: 'Purchase Order',
|
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
ODI: {
|
ODI: {
|
||||||
model: orderItemModel,
|
model: orderItemModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'orderItem',
|
type: 'orderItem',
|
||||||
label: 'Order Item',
|
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
COS: {
|
COS: {
|
||||||
model: courierServiceModel,
|
model: courierServiceModel,
|
||||||
idField: '_id',
|
idField: '_id',
|
||||||
type: 'courierService',
|
type: 'courierService',
|
||||||
label: 'Courier Service',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
COR: {
|
|
||||||
model: courierModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'courier',
|
|
||||||
label: 'Courier',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
TXR: {
|
|
||||||
model: taxRateModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'taxRate',
|
|
||||||
label: 'Tax Rate',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
TXD: {
|
|
||||||
model: taxRecordModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'taxRecord',
|
|
||||||
label: 'Tax Record',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
SHP: {
|
|
||||||
model: shipmentModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'shipment',
|
|
||||||
label: 'Shipment',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
INV: {
|
|
||||||
model: invoiceModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'invoice',
|
|
||||||
label: 'Invoice',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
CLI: {
|
|
||||||
model: clientModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'client',
|
|
||||||
label: 'Client',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
SOR: {
|
|
||||||
model: salesOrderModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'salesOrder',
|
|
||||||
label: 'Sales Order',
|
|
||||||
referenceField: '_reference',
|
|
||||||
},
|
|
||||||
MKT: {
|
|
||||||
model: marketplaceModel,
|
|
||||||
idField: '_id',
|
|
||||||
type: 'marketplace',
|
|
||||||
label: 'Marketplace',
|
|
||||||
referenceField: '_reference',
|
referenceField: '_reference',
|
||||||
},
|
},
|
||||||
|
COR: { model: courierModel, idField: '_id', type: 'courier', referenceField: '_reference' },
|
||||||
|
TXR: { model: taxRateModel, idField: '_id', type: 'taxRate', referenceField: '_reference' },
|
||||||
|
TXD: { model: taxRecordModel, idField: '_id', type: 'taxRecord', referenceField: '_reference' },
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,6 @@ const { Schema } = mongoose;
|
|||||||
|
|
||||||
const partSchema = new mongoose.Schema({
|
const partSchema = new mongoose.Schema({
|
||||||
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
part: { type: Schema.Types.ObjectId, ref: 'part', required: true },
|
||||||
partSku: { type: Schema.Types.ObjectId, ref: 'partSku', required: true },
|
|
||||||
quantity: { type: Number, required: true },
|
quantity: { type: Number, required: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -14,7 +13,7 @@ const gcodeFileSchema = new mongoose.Schema(
|
|||||||
name: { required: true, type: String },
|
name: { required: true, type: String },
|
||||||
gcodeFileName: { required: false, type: String },
|
gcodeFileName: { required: false, type: String },
|
||||||
size: { type: Number, required: false },
|
size: { type: Number, required: false },
|
||||||
filamentSku: { type: Schema.Types.ObjectId, ref: 'filamentSku', required: true },
|
filament: { type: Schema.Types.ObjectId, ref: 'filament', required: true },
|
||||||
parts: [partSchema],
|
parts: [partSchema],
|
||||||
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
|
file: { type: mongoose.SchemaTypes.ObjectId, ref: 'file', required: false },
|
||||||
cost: { type: Number, required: false },
|
cost: { type: Number, required: false },
|
||||||
|
|||||||
@ -92,6 +92,8 @@ printerSchema.statics.stats = async function () {
|
|||||||
rollupConfigs: rollupConfigs,
|
rollupConfigs: rollupConfigs,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(results);
|
||||||
|
|
||||||
// Transform the results to match the expected format
|
// Transform the results to match the expected format
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,35 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
|
|
||||||
const addressSchema = new mongoose.Schema({
|
|
||||||
building: { required: false, type: String },
|
|
||||||
addressLine1: { required: false, type: String },
|
|
||||||
addressLine2: { required: false, type: String },
|
|
||||||
city: { required: false, type: String },
|
|
||||||
state: { required: false, type: String },
|
|
||||||
postcode: { required: false, type: String },
|
|
||||||
country: { required: false, type: String },
|
|
||||||
});
|
|
||||||
|
|
||||||
const clientSchema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
name: { required: true, type: String },
|
|
||||||
marketplace: { type: mongoose.Schema.Types.ObjectId, ref: 'marketplace', required: false },
|
|
||||||
email: { required: false, type: String },
|
|
||||||
phone: { required: false, type: String },
|
|
||||||
country: { required: false, type: String },
|
|
||||||
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);
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
|
|
||||||
const marketplaceSchema = new mongoose.Schema(
|
|
||||||
{
|
|
||||||
_reference: { type: String, default: () => generateId()() },
|
|
||||||
name: { required: true, type: String },
|
|
||||||
provider: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
enum: ['ebay', 'etsy', 'tiktokShop'],
|
|
||||||
},
|
|
||||||
active: { required: true, type: Boolean, default: true },
|
|
||||||
// Provider-specific API configuration (flexible for eBay, Etsy, TikTok Shop)
|
|
||||||
config: { type: mongoose.Schema.Types.Mixed, default: {} },
|
|
||||||
},
|
|
||||||
{ timestamps: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
marketplaceSchema.virtual('id').get(function () {
|
|
||||||
return this._id;
|
|
||||||
});
|
|
||||||
|
|
||||||
marketplaceSchema.set('toJSON', { virtuals: true });
|
|
||||||
|
|
||||||
export const marketplaceModel = mongoose.model('marketplace', marketplaceSchema);
|
|
||||||
@ -1,215 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
import { generateId } from '../../utils.js';
|
|
||||||
const { Schema } = mongoose;
|
|
||||||
import {
|
|
||||||
aggregateRollups,
|
|
||||||
aggregateRollupsHistory,
|
|
||||||
editObject,
|
|
||||||
} 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 },
|
|
||||||
marketplace: { type: Schema.Types.ObjectId, ref: 'marketplace', required: false },
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
salesOrderSchema.statics.recalculate = async function (salesOrder, user) {
|
|
||||||
const orderId = salesOrder._id || salesOrder;
|
|
||||||
if (!orderId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderItemModel = mongoose.model('orderItem');
|
|
||||||
const shipmentModel = mongoose.model('shipment');
|
|
||||||
|
|
||||||
const orderIdObj = new mongoose.Types.ObjectId(orderId);
|
|
||||||
const baseFilter = { order: orderIdObj, orderType: 'salesOrder' };
|
|
||||||
|
|
||||||
const orderItemRollupResults = await aggregateRollups({
|
|
||||||
model: orderItemModel,
|
|
||||||
baseFilter,
|
|
||||||
rollupConfigs: [
|
|
||||||
{
|
|
||||||
name: 'orderTotals',
|
|
||||||
rollups: [
|
|
||||||
{ name: 'totalAmount', property: 'totalAmount', operation: 'sum' },
|
|
||||||
{ name: 'totalAmountWithTax', property: 'totalAmountWithTax', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'overallCount',
|
|
||||||
rollups: [{ name: 'overallCount', property: '_id', operation: 'count' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const shipmentRollupResults = await aggregateRollups({
|
|
||||||
model: shipmentModel,
|
|
||||||
baseFilter,
|
|
||||||
rollupConfigs: [
|
|
||||||
{
|
|
||||||
name: 'shipmentTotals',
|
|
||||||
rollups: [
|
|
||||||
{ name: 'amount', property: 'amount', operation: 'sum' },
|
|
||||||
{ name: 'amountWithTax', property: 'amountWithTax', operation: 'sum' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const orderTotals = orderItemRollupResults.orderTotals || {};
|
|
||||||
const totalAmount = orderTotals.totalAmount?.sum?.toFixed(2) || 0;
|
|
||||||
const totalAmountWithTax = orderTotals.totalAmountWithTax?.sum?.toFixed(2) || 0;
|
|
||||||
|
|
||||||
const shipmentTotals = shipmentRollupResults.shipmentTotals || {};
|
|
||||||
const totalShippingAmount = shipmentTotals.amount?.sum?.toFixed(2) || 0;
|
|
||||||
const totalShippingAmountWithTax = shipmentTotals.amountWithTax?.sum?.toFixed(2) || 0;
|
|
||||||
|
|
||||||
const grandTotalAmount =
|
|
||||||
parseFloat(totalAmountWithTax || 0) + parseFloat(totalShippingAmountWithTax || 0);
|
|
||||||
|
|
||||||
const overallCount = orderItemRollupResults.overallCount?.count || 0;
|
|
||||||
const shippedCount = orderItemRollupResults.shipped?.count || 0;
|
|
||||||
const receivedCount = orderItemRollupResults.received?.count || 0;
|
|
||||||
|
|
||||||
let updateData = {
|
|
||||||
totalAmount: parseFloat(totalAmount).toFixed(2),
|
|
||||||
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
|
|
||||||
totalTaxAmount: parseFloat((totalAmountWithTax - totalAmount).toFixed(2)),
|
|
||||||
shippingAmount: parseFloat(totalShippingAmount).toFixed(2),
|
|
||||||
shippingAmountWithTax: parseFloat(totalShippingAmountWithTax).toFixed(2),
|
|
||||||
grandTotalAmount: parseFloat(grandTotalAmount).toFixed(2),
|
|
||||||
};
|
|
||||||
|
|
||||||
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: 'partiallyDelivered' } };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receivedCount > 0 && receivedCount === overallCount) {
|
|
||||||
updateData = { ...updateData, state: { type: 'delivered' } };
|
|
||||||
}
|
|
||||||
|
|
||||||
await editObject({
|
|
||||||
model: this,
|
|
||||||
id: orderId,
|
|
||||||
updateData,
|
|
||||||
user,
|
|
||||||
recalculate: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
@ -1,50 +1,7 @@
|
|||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
import mongoose from 'mongoose';
|
|
||||||
|
|
||||||
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
export const generateId = () => {
|
export const generateId = () => {
|
||||||
// 10 characters
|
// 10 characters
|
||||||
return customAlphabet(ALPHABET, 12);
|
return customAlphabet(ALPHABET, 12);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Check if a value is a string that looks like a MongoDB ObjectId (24 hex chars). */
|
|
||||||
export function isObjectIdString(value) {
|
|
||||||
return typeof value === 'string' && /^[a-f\d]{24}$/i.test(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Convert a value to ObjectId if it's a valid ObjectId string; otherwise return as-is. */
|
|
||||||
export function toObjectIdIfValid(value) {
|
|
||||||
if (isObjectIdString(value)) {
|
|
||||||
return new mongoose.Types.ObjectId(value);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Recursively convert ObjectId strings to ObjectId in a filter object for MongoDB $match. */
|
|
||||||
export function convertObjectIdStringsInFilter(filter) {
|
|
||||||
if (!filter || typeof filter !== 'object') return filter;
|
|
||||||
|
|
||||||
const result = {};
|
|
||||||
for (const [key, value] of Object.entries(filter)) {
|
|
||||||
if (key.startsWith('$')) {
|
|
||||||
if ((key === '$in' || key === '$nin') && Array.isArray(value)) {
|
|
||||||
result[key] = value.map((v) => (isObjectIdString(v) ? new mongoose.Types.ObjectId(v) : v));
|
|
||||||
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
||||||
result[key] = convertObjectIdStringsInFilter(value);
|
|
||||||
} else {
|
|
||||||
result[key] = toObjectIdIfValid(value);
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
value &&
|
|
||||||
typeof value === 'object' &&
|
|
||||||
!Array.isArray(value) &&
|
|
||||||
!(value instanceof mongoose.Types.ObjectId) &&
|
|
||||||
!(value instanceof Date)
|
|
||||||
) {
|
|
||||||
result[key] = convertObjectIdStringsInFilter(value);
|
|
||||||
} else {
|
|
||||||
result[key] = toObjectIdIfValid(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|||||||
43
src/index.js
43
src/index.js
@ -2,28 +2,23 @@ import express from 'express';
|
|||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import config from './config.js';
|
import config from './config.js';
|
||||||
|
import { expressSession, keycloak } from './keycloak.js';
|
||||||
import { dbConnect } from './database/mongo.js';
|
import { dbConnect } from './database/mongo.js';
|
||||||
import { redisServer } from './database/redis.js';
|
|
||||||
import {
|
import {
|
||||||
authRoutes,
|
authRoutes,
|
||||||
userRoutes,
|
userRoutes,
|
||||||
appPasswordRoutes,
|
|
||||||
fileRoutes,
|
fileRoutes,
|
||||||
printerRoutes,
|
printerRoutes,
|
||||||
jobRoutes,
|
jobRoutes,
|
||||||
subJobRoutes,
|
subJobRoutes,
|
||||||
gcodeFileRoutes,
|
gcodeFileRoutes,
|
||||||
filamentRoutes,
|
filamentRoutes,
|
||||||
filamentSkuRoutes,
|
|
||||||
spotlightRoutes,
|
spotlightRoutes,
|
||||||
partRoutes,
|
partRoutes,
|
||||||
partSkuRoutes,
|
|
||||||
productRoutes,
|
productRoutes,
|
||||||
productSkuRoutes,
|
|
||||||
vendorRoutes,
|
vendorRoutes,
|
||||||
materialRoutes,
|
materialRoutes,
|
||||||
partStockRoutes,
|
partStockRoutes,
|
||||||
productStockRoutes,
|
|
||||||
filamentStockRoutes,
|
filamentStockRoutes,
|
||||||
purchaseOrderRoutes,
|
purchaseOrderRoutes,
|
||||||
orderItemRoutes,
|
orderItemRoutes,
|
||||||
@ -42,16 +37,6 @@ import {
|
|||||||
courierServiceRoutes,
|
courierServiceRoutes,
|
||||||
taxRateRoutes,
|
taxRateRoutes,
|
||||||
taxRecordRoutes,
|
taxRecordRoutes,
|
||||||
invoiceRoutes,
|
|
||||||
paymentRoutes,
|
|
||||||
clientRoutes,
|
|
||||||
salesOrderRoutes,
|
|
||||||
marketplaceRoutes,
|
|
||||||
userNotifierRoutes,
|
|
||||||
notificationRoutes,
|
|
||||||
odataRoutes,
|
|
||||||
excelRoutes,
|
|
||||||
csvRoutes,
|
|
||||||
} 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';
|
||||||
@ -91,9 +76,6 @@ async function initializeApp() {
|
|||||||
// Connect to database
|
// Connect to database
|
||||||
await dbConnect();
|
await dbConnect();
|
||||||
|
|
||||||
// Connect to Redis (required for excel temp tokens, sessions, cache)
|
|
||||||
await redisServer.connect();
|
|
||||||
|
|
||||||
// Connect to NATS
|
// Connect to NATS
|
||||||
await natsServer.connect();
|
await natsServer.connect();
|
||||||
|
|
||||||
@ -117,6 +99,8 @@ async function initializeApp() {
|
|||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
app.use(bodyParser.json({ type: 'application/json', strict: false, limit: '50mb' }));
|
app.use(bodyParser.json({ type: 'application/json', strict: false, limit: '50mb' }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(expressSession);
|
||||||
|
app.use(keycloak.middleware());
|
||||||
app.use(populateUserMiddleware);
|
app.use(populateUserMiddleware);
|
||||||
|
|
||||||
app.get('/', function (req, res) {
|
app.get('/', function (req, res) {
|
||||||
@ -126,7 +110,6 @@ app.get('/', function (req, res) {
|
|||||||
|
|
||||||
app.use('/auth', authRoutes);
|
app.use('/auth', authRoutes);
|
||||||
app.use('/users', userRoutes);
|
app.use('/users', userRoutes);
|
||||||
app.use('/apppasswords', appPasswordRoutes);
|
|
||||||
app.use('/files', fileRoutes);
|
app.use('/files', fileRoutes);
|
||||||
app.use('/spotlight', spotlightRoutes);
|
app.use('/spotlight', spotlightRoutes);
|
||||||
app.use('/printers', printerRoutes);
|
app.use('/printers', printerRoutes);
|
||||||
@ -135,15 +118,11 @@ app.use('/jobs', jobRoutes);
|
|||||||
app.use('/subjobs', subJobRoutes);
|
app.use('/subjobs', subJobRoutes);
|
||||||
app.use('/gcodefiles', gcodeFileRoutes);
|
app.use('/gcodefiles', gcodeFileRoutes);
|
||||||
app.use('/filaments', filamentRoutes);
|
app.use('/filaments', filamentRoutes);
|
||||||
app.use('/filamentskus', filamentSkuRoutes);
|
|
||||||
app.use('/parts', partRoutes);
|
app.use('/parts', partRoutes);
|
||||||
app.use('/partskus', partSkuRoutes);
|
|
||||||
app.use('/products', productRoutes);
|
app.use('/products', productRoutes);
|
||||||
app.use('/productskus', productSkuRoutes);
|
|
||||||
app.use('/vendors', vendorRoutes);
|
app.use('/vendors', vendorRoutes);
|
||||||
app.use('/materials', materialRoutes);
|
app.use('/materials', materialRoutes);
|
||||||
app.use('/partstocks', partStockRoutes);
|
app.use('/partstocks', partStockRoutes);
|
||||||
app.use('/productstocks', productStockRoutes);
|
|
||||||
app.use('/filamentstocks', filamentStockRoutes);
|
app.use('/filamentstocks', filamentStockRoutes);
|
||||||
app.use('/purchaseorders', purchaseOrderRoutes);
|
app.use('/purchaseorders', purchaseOrderRoutes);
|
||||||
app.use('/orderitems', orderItemRoutes);
|
app.use('/orderitems', orderItemRoutes);
|
||||||
@ -160,21 +139,7 @@ app.use('/couriers', courierRoutes);
|
|||||||
app.use('/courierservices', courierServiceRoutes);
|
app.use('/courierservices', courierServiceRoutes);
|
||||||
app.use('/taxrates', taxRateRoutes);
|
app.use('/taxrates', taxRateRoutes);
|
||||||
app.use('/taxrecords', taxRecordRoutes);
|
app.use('/taxrecords', taxRecordRoutes);
|
||||||
app.use('/invoices', invoiceRoutes);
|
|
||||||
app.use('/payments', paymentRoutes);
|
|
||||||
app.use('/clients', clientRoutes);
|
|
||||||
app.use('/salesorders', salesOrderRoutes);
|
|
||||||
app.use('/marketplaces', marketplaceRoutes);
|
|
||||||
app.use('/notes', noteRoutes);
|
app.use('/notes', noteRoutes);
|
||||||
app.use('/usernotifiers', userNotifierRoutes);
|
|
||||||
app.use('/notifications', notificationRoutes);
|
|
||||||
app.use('/odata', odataRoutes);
|
|
||||||
app.use('/excel', excelRoutes);
|
|
||||||
app.use('/csv', csvRoutes);
|
|
||||||
|
|
||||||
// Start the application
|
// Start the application
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
initializeApp();
|
||||||
initializeApp();
|
|
||||||
}
|
|
||||||
|
|
||||||
export default app;
|
|
||||||
|
|||||||
167
src/keycloak.js
167
src/keycloak.js
@ -1,22 +1,23 @@
|
|||||||
/**
|
import Keycloak from 'keycloak-connect';
|
||||||
* Authentication middleware - uses Redis session store.
|
import session from 'express-session';
|
||||||
* Keycloak is used only for login/refresh; session validation is done via Redis.
|
|
||||||
*/
|
|
||||||
import config, { getEnvironment } from './config.js';
|
import config, { getEnvironment } from './config.js';
|
||||||
|
import axios from 'axios';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import NodeCache from 'node-cache';
|
import NodeCache from 'node-cache';
|
||||||
import bcrypt from 'bcrypt';
|
|
||||||
import { userModel } from './database/schemas/management/user.schema.js';
|
import { userModel } from './database/schemas/management/user.schema.js';
|
||||||
import { appPasswordModel } from './database/schemas/management/apppassword.schema.js';
|
|
||||||
import { getObject } from './database/database.js';
|
import { getObject } from './database/database.js';
|
||||||
import { hostModel } from './database/schemas/management/host.schema.js';
|
import { hostModel } from './database/schemas/management/host.schema.js';
|
||||||
import { getSession, lookupUserByToken } from './services/misc/auth.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Keycloak');
|
const logger = log4js.getLogger('Keycloak');
|
||||||
logger.level = config.server.logLevel || 'info';
|
logger.level = config.server.logLevel || 'info';
|
||||||
|
|
||||||
const userCache = new NodeCache({ stdTTL: 300 });
|
dotenv.config();
|
||||||
|
|
||||||
|
// Initialize NodeCache with 5-minute TTL
|
||||||
|
const userCache = new NodeCache({ stdTTL: 300 }); // 300 seconds = 5 minutes
|
||||||
|
|
||||||
|
// Cache event listeners for monitoring
|
||||||
userCache.on('expired', (key, value) => {
|
userCache.on('expired', (key, value) => {
|
||||||
logger.debug(`Cache entry expired: ${key}`);
|
logger.debug(`Cache entry expired: ${key}`);
|
||||||
});
|
});
|
||||||
@ -25,18 +26,22 @@ userCache.on('flush', () => {
|
|||||||
logger.info('Cache flushed');
|
logger.info('Cache flushed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// User lookup function with caching
|
||||||
const lookupUser = async (preferredUsername) => {
|
const lookupUser = async (preferredUsername) => {
|
||||||
try {
|
try {
|
||||||
|
// Check cache first
|
||||||
const cachedUser = userCache.get(preferredUsername);
|
const cachedUser = userCache.get(preferredUsername);
|
||||||
if (cachedUser) {
|
if (cachedUser) {
|
||||||
logger.debug(`User found in cache: ${preferredUsername}`);
|
logger.debug(`User found in cache: ${preferredUsername}`);
|
||||||
return cachedUser;
|
return cachedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If not in cache, query database
|
||||||
logger.debug(`User not in cache, querying database: ${preferredUsername}`);
|
logger.debug(`User not in cache, querying database: ${preferredUsername}`);
|
||||||
const user = await userModel.findOne({ username: preferredUsername });
|
const user = await userModel.findOne({ username: preferredUsername });
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
// Store in cache
|
||||||
userCache.set(preferredUsername, user);
|
userCache.set(preferredUsername, user);
|
||||||
logger.debug(`User stored in cache: ${preferredUsername}`);
|
logger.debug(`User stored in cache: ${preferredUsername}`);
|
||||||
return user;
|
return user;
|
||||||
@ -50,83 +55,121 @@ const lookupUser = async (preferredUsername) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Initialize Keycloak
|
||||||
* Middleware to check if the user is authenticated.
|
const keycloakConfig = {
|
||||||
* Supports: 1) Bearer token (Redis session), 2) Bearer token (email-render JWT for Puppeteer),
|
realm: config.auth.keycloak.realm,
|
||||||
* 3) HTTP Basic Auth (username + app password), 4) x-host-id + x-auth-code (host auth)
|
'auth-server-url': config.auth.keycloak.url,
|
||||||
*/
|
'ssl-required': getEnvironment() === 'production' ? 'external' : 'none',
|
||||||
|
resource: config.auth.keycloak.clientId,
|
||||||
|
'confidential-port': 0,
|
||||||
|
'bearer-only': true,
|
||||||
|
'public-client': false,
|
||||||
|
'use-resource-role-mappings': true,
|
||||||
|
'verify-token-audience': true,
|
||||||
|
credentials: {
|
||||||
|
secret: process.env.KEYCLOAK_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const memoryStore = new session.MemoryStore();
|
||||||
|
|
||||||
|
var expressSession = session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'n00Dl3s23!',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: true, // Set this to true to ensure session is initialized
|
||||||
|
store: memoryStore,
|
||||||
|
cookie: {
|
||||||
|
maxAge: 1800000, // 30 minutes
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var keycloak = new Keycloak({ store: memoryStore }, keycloakConfig);
|
||||||
|
|
||||||
|
// Custom middleware to check if the user is authenticated
|
||||||
const isAuthenticated = async (req, res, next) => {
|
const isAuthenticated = async (req, res, next) => {
|
||||||
|
let token = null;
|
||||||
|
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
const authHeader = req.headers.authorization || req.headers.Authorization;
|
||||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
const token = authHeader.substring(7);
|
token = authHeader.substring(7);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const session = await getSession(token);
|
// Verify token with Keycloak introspection endpoint
|
||||||
if (session && session.expiresAt > Date.now()) {
|
const response = await axios.post(
|
||||||
req.user = session.user;
|
`${config.auth.keycloak.url}/realms/${config.auth.keycloak.realm}/protocol/openid-connect/token/introspect`,
|
||||||
req.session = session;
|
new URLSearchParams({
|
||||||
return next();
|
token: token,
|
||||||
|
client_id: config.auth.keycloak.clientId,
|
||||||
|
client_secret: process.env.KEYCLOAK_CLIENT_SECRET,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const introspection = response.data;
|
||||||
|
if (!introspection.active) {
|
||||||
|
logger.info('Token is not active');
|
||||||
|
logger.debug('Token:', token);
|
||||||
|
return res.status(401).json({ error: 'Session Inactive', code: 'UNAUTHORIZED' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try email-render JWT (short-lived token for Puppeteer email notifications)
|
return next();
|
||||||
const user = await lookupUserByToken(token);
|
|
||||||
if (user) {
|
|
||||||
req.user = user;
|
|
||||||
req.session = { user };
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Session lookup error:', error.message);
|
logger.error('Token verification error:', error.message);
|
||||||
|
return res.status(401).json({ error: 'Verification Error', code: 'UNAUTHORIZED' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
const host = await getObject({ model: hostModel, id: hostId });
|
const host = await getObject({ model: hostModel, id: hostId });
|
||||||
if (host && host.authCode === authCode) {
|
if (host && host.authCode == authCode) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to session-based authentication
|
||||||
|
console.log('Using session token');
|
||||||
|
if (req.session && req.session['keycloak-token']) {
|
||||||
|
const sessionToken = req.session['keycloak-token'];
|
||||||
|
if (sessionToken.expires_at > new Date().getTime()) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug('Not authenticated', { hostId, authCode }, 'req.headers', req.headers);
|
|
||||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAppAuthenticated = async (req, res, next) => {
|
// Helper function to extract roles from token
|
||||||
const authHeader = req.headers.authorization || req.headers.Authorization;
|
function extractRoles(token) {
|
||||||
// Try HTTP Basic Auth (username + app password secret)
|
const roles = [];
|
||||||
if (authHeader?.startsWith('Basic ')) {
|
|
||||||
try {
|
// Extract realm roles
|
||||||
logger.debug('Basic auth header present');
|
if (token.realm_access && token.realm_access.roles) {
|
||||||
const base64Credentials = authHeader.substring(6);
|
roles.push(...token.realm_access.roles);
|
||||||
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
|
}
|
||||||
const colonIndex = credentials.indexOf(':');
|
|
||||||
const username = credentials.substring(0, colonIndex).trim();
|
// Extract client roles
|
||||||
const secret = credentials.substring(colonIndex + 1).trim();
|
if (token.resource_access) {
|
||||||
if (username && secret) {
|
for (const client in token.resource_access) {
|
||||||
const user = await userModel.findOne({ username });
|
if (token.resource_access[client].roles) {
|
||||||
if (user) {
|
roles.push(...token.resource_access[client].roles.map((role) => `${client}:${role}`));
|
||||||
const appPasswords = await appPasswordModel
|
|
||||||
.find({ user: user._id, active: true })
|
|
||||||
.select('+secret')
|
|
||||||
.lean();
|
|
||||||
for (const appPassword of appPasswords) {
|
|
||||||
const storedHash = appPassword.secret;
|
|
||||||
if (storedHash && (await bcrypt.compare(secret, storedHash))) {
|
|
||||||
req.user = user;
|
|
||||||
req.session = { user };
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
logger.error('Basic auth error:', error.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.status(401).json({ error: 'Not Authenticated', code: 'UNAUTHORIZED' });
|
|
||||||
};
|
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache management utility functions
|
||||||
const clearUserCache = () => {
|
const clearUserCache = () => {
|
||||||
userCache.flushAll();
|
userCache.flushAll();
|
||||||
logger.info('User cache cleared');
|
logger.info('User cache cleared');
|
||||||
@ -142,11 +185,11 @@ const removeUserFromCache = (username) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
keycloak,
|
||||||
|
expressSession,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isAppAuthenticated,
|
|
||||||
lookupUser,
|
lookupUser,
|
||||||
clearUserCache,
|
clearUserCache,
|
||||||
getUserCacheStats,
|
getUserCacheStats,
|
||||||
removeUserFromCache,
|
removeUserFromCache,
|
||||||
getEnvironment,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,151 +0,0 @@
|
|||||||
/**
|
|
||||||
* Worker thread for sending email notifications asynchronously.
|
|
||||||
* Receives payloads from the main thread and performs Puppeteer render + nodemailer send.
|
|
||||||
*/
|
|
||||||
import { parentPort } from 'worker_threads';
|
|
||||||
import puppeteer from 'puppeteer';
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import config from './config.js';
|
|
||||||
|
|
||||||
const baseUrl = (urlClient) => (urlClient || 'http://localhost:3000').replace(/\/$/, '');
|
|
||||||
|
|
||||||
async function fetchAndInlineStyles(html, urlClient) {
|
|
||||||
const base = baseUrl(urlClient);
|
|
||||||
const linkMatches = [...html.matchAll(/<link[^>]+>/g)];
|
|
||||||
const stylesheetLinks = linkMatches
|
|
||||||
.map((m) => {
|
|
||||||
const tag = m[0];
|
|
||||||
if (!/rel=["']stylesheet["']/i.test(tag)) return null;
|
|
||||||
const hrefMatch = tag.match(/href=["']([^"']+)["']/);
|
|
||||||
return hrefMatch ? { tag, href: hrefMatch[1] } : null;
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
let inlined = html;
|
|
||||||
for (const { tag, href } of stylesheetLinks) {
|
|
||||||
const url = href.startsWith('http') ? href : `${base}${href.startsWith('/') ? '' : '/'}${href}`;
|
|
||||||
try {
|
|
||||||
const res = await fetch(url);
|
|
||||||
if (res.ok) {
|
|
||||||
const css = await res.text();
|
|
||||||
inlined = inlined.replace(tag, `<style>${css}</style>`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.trace('Could not fetch stylesheet:', url, e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return inlined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('MailWorker');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
async function sendEmail(payload) {
|
|
||||||
const {
|
|
||||||
email,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
type,
|
|
||||||
metadata,
|
|
||||||
smtpConfig,
|
|
||||||
urlClient,
|
|
||||||
createdAt,
|
|
||||||
updatedAt,
|
|
||||||
authCode,
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
if (!email || !smtpConfig?.host) {
|
|
||||||
logger.warn('Missing email or SMTP config, skipping...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
title: title || '',
|
|
||||||
message: message || '',
|
|
||||||
type: type || 'info',
|
|
||||||
email: email || '',
|
|
||||||
createdAt: createdAt || new Date(),
|
|
||||||
updatedAt: updatedAt || new Date(),
|
|
||||||
authCode: authCode || '',
|
|
||||||
metadata: JSON.stringify(metadata || {}),
|
|
||||||
});
|
|
||||||
const templateUrl = `${baseUrl(urlClient)}/email/notification?${params.toString()}`;
|
|
||||||
|
|
||||||
logger.debug('Rendering template...');
|
|
||||||
logger.trace('Template URL:', templateUrl);
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
let browser;
|
|
||||||
try {
|
|
||||||
browser = await puppeteer.launch({
|
|
||||||
headless: 'new',
|
|
||||||
args: [
|
|
||||||
'--no-sandbox',
|
|
||||||
'--disable-setuid-sandbox',
|
|
||||||
'--disable-features=SameSiteByDefaultCookies',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const page = await browser.newPage();
|
|
||||||
page.on('console', (msg) => {
|
|
||||||
const text = msg.text();
|
|
||||||
const type = msg.type();
|
|
||||||
logger.trace(`Puppeteer [${type}]: ${text}`);
|
|
||||||
});
|
|
||||||
await page.goto(templateUrl, { waitUntil: 'networkidle0', timeout: 30000 });
|
|
||||||
await page.waitForSelector('#email-notification-root[data-rendered="true"]', { timeout: 5000 });
|
|
||||||
// Wait for Ant Design CSS-in-JS to finish injecting styles
|
|
||||||
logger.debug('Waiting for 1.5 seconds for page to render...');
|
|
||||||
await new Promise((r) => setTimeout(r, 1500));
|
|
||||||
html = await page.evaluate(() => {
|
|
||||||
const root = document.getElementById('email-notification-root');
|
|
||||||
if (!root) return document.documentElement.outerHTML;
|
|
||||||
const origin = document.location.origin;
|
|
||||||
const styleTags = Array.from(document.querySelectorAll('style'))
|
|
||||||
.map((s) => s.outerHTML)
|
|
||||||
.join('\n');
|
|
||||||
const linkTags = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
||||||
.map((link) => {
|
|
||||||
const href = link.getAttribute('href');
|
|
||||||
const abs = href?.startsWith('http')
|
|
||||||
? href
|
|
||||||
: `${origin}${href?.startsWith('/') ? '' : '/'}${href || ''}`;
|
|
||||||
return `<link rel="stylesheet" href="${abs}">`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${styleTags}${linkTags}</head><body>${root.outerHTML}</body></html>`;
|
|
||||||
});
|
|
||||||
html = await fetchAndInlineStyles(html, urlClient);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('MailWorker: Puppeteer error', err.message);
|
|
||||||
html = `<div style="font-family:sans-serif;padding:20px"><h2>${title || 'Notification'}</h2><p>${message || ''}</p></div>`;
|
|
||||||
} finally {
|
|
||||||
if (browser) await browser.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: smtpConfig.host,
|
|
||||||
port: smtpConfig.port || 587,
|
|
||||||
secure: smtpConfig.secure || false,
|
|
||||||
auth: smtpConfig.auth?.user ? smtpConfig.auth : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const subject = title ? `${title} - FarmControl` : 'FarmControl Notification';
|
|
||||||
|
|
||||||
const mailOptions = {
|
|
||||||
from: smtpConfig.from || 'FarmControl <noreply@tombutcher.work>',
|
|
||||||
to: email,
|
|
||||||
subject: subject,
|
|
||||||
html,
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug('Sending email...');
|
|
||||||
logger.trace('Mail options:', mailOptions);
|
|
||||||
const info = await transporter.sendMail(mailOptions);
|
|
||||||
logger.debug('Email sent successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
parentPort.on('message', (payload) => {
|
|
||||||
sendEmail(payload).catch((err) => {
|
|
||||||
logger.error('MailWorker: send failed', err.message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listInvoicesRouteHandler,
|
|
||||||
getInvoiceRouteHandler,
|
|
||||||
editInvoiceRouteHandler,
|
|
||||||
editMultipleInvoicesRouteHandler,
|
|
||||||
newInvoiceRouteHandler,
|
|
||||||
deleteInvoiceRouteHandler,
|
|
||||||
listInvoicesByPropertiesRouteHandler,
|
|
||||||
getInvoiceStatsRouteHandler,
|
|
||||||
getInvoiceHistoryRouteHandler,
|
|
||||||
acknowledgeInvoiceRouteHandler,
|
|
||||||
cancelInvoiceRouteHandler,
|
|
||||||
postInvoiceRouteHandler,
|
|
||||||
} from '../../services/finance/invoices.js';
|
|
||||||
|
|
||||||
// list of invoices
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = [
|
|
||||||
'vendor',
|
|
||||||
'client',
|
|
||||||
'state',
|
|
||||||
'vendor._id',
|
|
||||||
'client._id',
|
|
||||||
'order',
|
|
||||||
'order._id',
|
|
||||||
'orderType',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listInvoicesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = [
|
|
||||||
'vendor',
|
|
||||||
'client',
|
|
||||||
'orderType',
|
|
||||||
'order',
|
|
||||||
'state.type',
|
|
||||||
'value',
|
|
||||||
'vendor._id',
|
|
||||||
'client._id',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
var masterFilter = {};
|
|
||||||
if (req.query.masterFilter) {
|
|
||||||
masterFilter = JSON.parse(req.query.masterFilter);
|
|
||||||
}
|
|
||||||
listInvoicesByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newInvoiceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get invoice stats
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getInvoiceStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get invoice history
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getInvoiceHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getInvoiceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update multiple invoices
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultipleInvoicesRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editInvoiceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteInvoiceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/post', isAuthenticated, async (req, res) => {
|
|
||||||
postInvoiceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/acknowledge', isAuthenticated, async (req, res) => {
|
|
||||||
acknowledgeInvoiceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
|
|
||||||
cancelInvoiceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listPaymentsRouteHandler,
|
|
||||||
getPaymentRouteHandler,
|
|
||||||
editPaymentRouteHandler,
|
|
||||||
editMultiplePaymentsRouteHandler,
|
|
||||||
newPaymentRouteHandler,
|
|
||||||
deletePaymentRouteHandler,
|
|
||||||
listPaymentsByPropertiesRouteHandler,
|
|
||||||
getPaymentStatsRouteHandler,
|
|
||||||
getPaymentHistoryRouteHandler,
|
|
||||||
postPaymentRouteHandler,
|
|
||||||
cancelPaymentRouteHandler,
|
|
||||||
} from '../../services/finance/payments.js';
|
|
||||||
|
|
||||||
// list of payments
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = [
|
|
||||||
'vendor',
|
|
||||||
'client',
|
|
||||||
'state',
|
|
||||||
'vendor._id',
|
|
||||||
'client._id',
|
|
||||||
'invoice',
|
|
||||||
'invoice._id',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listPaymentsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = [
|
|
||||||
'vendor',
|
|
||||||
'client',
|
|
||||||
'invoice',
|
|
||||||
'state.type',
|
|
||||||
'value',
|
|
||||||
'vendor._id',
|
|
||||||
'client._id',
|
|
||||||
'invoice._id',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
var masterFilter = {};
|
|
||||||
if (req.query.masterFilter) {
|
|
||||||
masterFilter = JSON.parse(req.query.masterFilter);
|
|
||||||
}
|
|
||||||
listPaymentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newPaymentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get payment stats
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getPaymentStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get payment history
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getPaymentHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getPaymentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update multiple payments
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultiplePaymentsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editPaymentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deletePaymentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/post', isAuthenticated, async (req, res) => {
|
|
||||||
postPaymentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
|
|
||||||
cancelPaymentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import userRoutes from './management/users.js';
|
import userRoutes from './management/users.js';
|
||||||
import appPasswordRoutes from './management/apppasswords.js';
|
|
||||||
import fileRoutes from './management/files.js';
|
import fileRoutes from './management/files.js';
|
||||||
import authRoutes from './misc/auth.js';
|
import authRoutes from './misc/auth.js';
|
||||||
import printerRoutes from './production/printers.js';
|
import printerRoutes from './production/printers.js';
|
||||||
@ -8,16 +7,12 @@ import jobRoutes from './production/jobs.js';
|
|||||||
import subJobRoutes from './production/subjobs.js';
|
import subJobRoutes from './production/subjobs.js';
|
||||||
import gcodeFileRoutes from './production/gcodefiles.js';
|
import gcodeFileRoutes from './production/gcodefiles.js';
|
||||||
import filamentRoutes from './management/filaments.js';
|
import filamentRoutes from './management/filaments.js';
|
||||||
import filamentSkuRoutes from './management/filamentskus.js';
|
|
||||||
import spotlightRoutes from './misc/spotlight.js';
|
import spotlightRoutes from './misc/spotlight.js';
|
||||||
import partRoutes from './management/parts.js';
|
import partRoutes from './management/parts.js';
|
||||||
import partSkuRoutes from './management/partskus.js';
|
|
||||||
import productRoutes from './management/products.js';
|
import productRoutes from './management/products.js';
|
||||||
import productSkuRoutes from './management/productskus.js';
|
|
||||||
import vendorRoutes from './management/vendors.js';
|
import vendorRoutes from './management/vendors.js';
|
||||||
import materialRoutes from './management/materials.js';
|
import materialRoutes from './management/materials.js';
|
||||||
import partStockRoutes from './inventory/partstocks.js';
|
import partStockRoutes from './inventory/partstocks.js';
|
||||||
import productStockRoutes from './inventory/productstocks.js';
|
|
||||||
import filamentStockRoutes from './inventory/filamentstocks.js';
|
import filamentStockRoutes from './inventory/filamentstocks.js';
|
||||||
import purchaseOrderRoutes from './inventory/purchaseorders.js';
|
import purchaseOrderRoutes from './inventory/purchaseorders.js';
|
||||||
import orderItemRoutes from './inventory/orderitems.js';
|
import orderItemRoutes from './inventory/orderitems.js';
|
||||||
@ -34,21 +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 marketplaceRoutes from './sales/marketplaces.js';
|
|
||||||
import noteRoutes from './misc/notes.js';
|
import noteRoutes from './misc/notes.js';
|
||||||
import userNotifierRoutes from './misc/usernotifiers.js';
|
|
||||||
import notificationRoutes from './misc/notifications.js';
|
|
||||||
import odataRoutes from './misc/odata.js';
|
|
||||||
import excelRoutes from './misc/excel.js';
|
|
||||||
import csvRoutes from './misc/csv.js';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
userRoutes,
|
userRoutes,
|
||||||
appPasswordRoutes,
|
|
||||||
fileRoutes,
|
fileRoutes,
|
||||||
authRoutes,
|
authRoutes,
|
||||||
printerRoutes,
|
printerRoutes,
|
||||||
@ -57,16 +41,12 @@ export {
|
|||||||
subJobRoutes,
|
subJobRoutes,
|
||||||
gcodeFileRoutes,
|
gcodeFileRoutes,
|
||||||
filamentRoutes,
|
filamentRoutes,
|
||||||
filamentSkuRoutes,
|
|
||||||
spotlightRoutes,
|
spotlightRoutes,
|
||||||
partRoutes,
|
partRoutes,
|
||||||
partSkuRoutes,
|
|
||||||
productRoutes,
|
productRoutes,
|
||||||
productSkuRoutes,
|
|
||||||
vendorRoutes,
|
vendorRoutes,
|
||||||
materialRoutes,
|
materialRoutes,
|
||||||
partStockRoutes,
|
partStockRoutes,
|
||||||
productStockRoutes,
|
|
||||||
filamentStockRoutes,
|
filamentStockRoutes,
|
||||||
purchaseOrderRoutes,
|
purchaseOrderRoutes,
|
||||||
orderItemRoutes,
|
orderItemRoutes,
|
||||||
@ -84,14 +64,4 @@ export {
|
|||||||
courierServiceRoutes,
|
courierServiceRoutes,
|
||||||
taxRateRoutes,
|
taxRateRoutes,
|
||||||
taxRecordRoutes,
|
taxRecordRoutes,
|
||||||
invoiceRoutes,
|
|
||||||
paymentRoutes,
|
|
||||||
clientRoutes,
|
|
||||||
salesOrderRoutes,
|
|
||||||
marketplaceRoutes,
|
|
||||||
userNotifierRoutes,
|
|
||||||
notificationRoutes,
|
|
||||||
odataRoutes,
|
|
||||||
excelRoutes,
|
|
||||||
csvRoutes,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
listFilamentStocksRouteHandler,
|
listFilamentStocksRouteHandler,
|
||||||
getFilamentStockRouteHandler,
|
getFilamentStockRouteHandler,
|
||||||
editFilamentStockRouteHandler,
|
editFilamentStockRouteHandler,
|
||||||
editMultipleFilamentStocksRouteHandler,
|
|
||||||
newFilamentStockRouteHandler,
|
newFilamentStockRouteHandler,
|
||||||
deleteFilamentStockRouteHandler,
|
deleteFilamentStockRouteHandler,
|
||||||
listFilamentStocksByPropertiesRouteHandler,
|
listFilamentStocksByPropertiesRouteHandler,
|
||||||
@ -18,14 +17,14 @@ import {
|
|||||||
// list of filament stocks
|
// list of filament stocks
|
||||||
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 = ['filamentSku', 'state', 'startingWeight', 'currentWeight', 'filamentSku._id'];
|
const allowedFilters = ['filament', 'state', 'startingWeight', 'currentWeight', 'filament._id'];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listFilamentStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listFilamentStocksRouteHandler(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 = ['filamentSku', 'state.type'];
|
const allowedFilters = ['filament', 'state.type'];
|
||||||
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) {
|
||||||
@ -52,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
|
|||||||
getFilamentStockRouteHandler(req, res);
|
getFilamentStockRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update multiple filament stocks
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultipleFilamentStocksRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
editFilamentStockRouteHandler(req, res);
|
editFilamentStockRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
listOrderItemsRouteHandler,
|
listOrderItemsRouteHandler,
|
||||||
getOrderItemRouteHandler,
|
getOrderItemRouteHandler,
|
||||||
editOrderItemRouteHandler,
|
editOrderItemRouteHandler,
|
||||||
editMultipleOrderItemsRouteHandler,
|
|
||||||
newOrderItemRouteHandler,
|
newOrderItemRouteHandler,
|
||||||
deleteOrderItemRouteHandler,
|
deleteOrderItemRouteHandler,
|
||||||
listOrderItemsByPropertiesRouteHandler,
|
listOrderItemsByPropertiesRouteHandler,
|
||||||
@ -18,34 +17,14 @@ import {
|
|||||||
// list of order items
|
// list of order items
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = [
|
const allowedFilters = ['itemType', 'item', 'item._id', 'order', 'order._id', 'orderType'];
|
||||||
'name',
|
|
||||||
'itemType',
|
|
||||||
'item',
|
|
||||||
'item._id',
|
|
||||||
'order',
|
|
||||||
'order._id',
|
|
||||||
'orderType',
|
|
||||||
'shipment',
|
|
||||||
'shipment._id',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listOrderItemsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listOrderItemsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
const allowedFilters = [
|
const allowedFilters = ['itemType', 'item', 'item._id', 'order', 'order._id', 'orderType'];
|
||||||
'name',
|
|
||||||
'itemType',
|
|
||||||
'item',
|
|
||||||
'item._id',
|
|
||||||
'order',
|
|
||||||
'order._id',
|
|
||||||
'orderType',
|
|
||||||
'shipment',
|
|
||||||
'shipment._id',
|
|
||||||
];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
var masterFilter = {};
|
var masterFilter = {};
|
||||||
if (req.query.masterFilter) {
|
if (req.query.masterFilter) {
|
||||||
@ -72,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
|
|||||||
getOrderItemRouteHandler(req, res);
|
getOrderItemRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update multiple order items
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultipleOrderItemsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
editOrderItemRouteHandler(req, res);
|
editOrderItemRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
listPartStocksRouteHandler,
|
listPartStocksRouteHandler,
|
||||||
getPartStockRouteHandler,
|
getPartStockRouteHandler,
|
||||||
editPartStockRouteHandler,
|
editPartStockRouteHandler,
|
||||||
editMultiplePartStocksRouteHandler,
|
|
||||||
newPartStockRouteHandler,
|
newPartStockRouteHandler,
|
||||||
deletePartStockRouteHandler,
|
deletePartStockRouteHandler,
|
||||||
listPartStocksByPropertiesRouteHandler,
|
listPartStocksByPropertiesRouteHandler,
|
||||||
@ -18,7 +17,7 @@ import {
|
|||||||
// list of part stocks
|
// list of part stocks
|
||||||
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 = ['partSku', 'state', 'startingQuantity', 'currentQuantity', 'partSku._id'];
|
const allowedFilters = ['part', 'state', 'startingQuantity', 'currentQuantity', 'part._id'];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listPartStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listPartStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
@ -52,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
|
|||||||
getPartStockRouteHandler(req, res);
|
getPartStockRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update multiple part stocks
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultiplePartStocksRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
editPartStockRouteHandler(req, res);
|
editPartStockRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listProductStocksRouteHandler,
|
|
||||||
getProductStockRouteHandler,
|
|
||||||
editProductStockRouteHandler,
|
|
||||||
editMultipleProductStocksRouteHandler,
|
|
||||||
newProductStockRouteHandler,
|
|
||||||
deleteProductStockRouteHandler,
|
|
||||||
postProductStockRouteHandler,
|
|
||||||
listProductStocksByPropertiesRouteHandler,
|
|
||||||
getProductStockStatsRouteHandler,
|
|
||||||
getProductStockHistoryRouteHandler,
|
|
||||||
} from '../../services/inventory/productstocks.js';
|
|
||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['productSku', 'state', 'currentQuantity', 'productSku._id'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listProductStocksRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['productSku', 'state.type'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
var masterFilter = {};
|
|
||||||
if (req.query.masterFilter) {
|
|
||||||
masterFilter = JSON.parse(req.query.masterFilter);
|
|
||||||
}
|
|
||||||
listProductStocksByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newProductStockRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getProductStockStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getProductStockHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getProductStockRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultipleProductStocksRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editProductStockRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteProductStockRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/post', isAuthenticated, async (req, res) => {
|
|
||||||
postProductStockRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -7,15 +7,11 @@ import {
|
|||||||
listPurchaseOrdersRouteHandler,
|
listPurchaseOrdersRouteHandler,
|
||||||
getPurchaseOrderRouteHandler,
|
getPurchaseOrderRouteHandler,
|
||||||
editPurchaseOrderRouteHandler,
|
editPurchaseOrderRouteHandler,
|
||||||
editMultiplePurchaseOrdersRouteHandler,
|
|
||||||
newPurchaseOrderRouteHandler,
|
newPurchaseOrderRouteHandler,
|
||||||
deletePurchaseOrderRouteHandler,
|
deletePurchaseOrderRouteHandler,
|
||||||
listPurchaseOrdersByPropertiesRouteHandler,
|
listPurchaseOrdersByPropertiesRouteHandler,
|
||||||
getPurchaseOrderStatsRouteHandler,
|
getPurchaseOrderStatsRouteHandler,
|
||||||
getPurchaseOrderHistoryRouteHandler,
|
getPurchaseOrderHistoryRouteHandler,
|
||||||
postPurchaseOrderRouteHandler,
|
|
||||||
acknowledgePurchaseOrderRouteHandler,
|
|
||||||
cancelPurchaseOrderRouteHandler,
|
|
||||||
} from '../../services/inventory/purchaseorders.js';
|
} from '../../services/inventory/purchaseorders.js';
|
||||||
|
|
||||||
// list of purchase orders
|
// list of purchase orders
|
||||||
@ -32,7 +28,7 @@ router.get('/properties', isAuthenticated, (req, res) => {
|
|||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
var masterFilter = {};
|
var masterFilter = {};
|
||||||
if (req.query.masterFilter) {
|
if (req.query.masterFilter) {
|
||||||
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true);
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
}
|
}
|
||||||
listPurchaseOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
listPurchaseOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
});
|
});
|
||||||
@ -55,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
|
|||||||
getPurchaseOrderRouteHandler(req, res);
|
getPurchaseOrderRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update multiple purchase orders
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultiplePurchaseOrdersRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
editPurchaseOrderRouteHandler(req, res);
|
editPurchaseOrderRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
@ -68,16 +59,4 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
|
|||||||
deletePurchaseOrderRouteHandler(req, res);
|
deletePurchaseOrderRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/:id/post', isAuthenticated, async (req, res) => {
|
|
||||||
postPurchaseOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/acknowledge', isAuthenticated, async (req, res) => {
|
|
||||||
acknowledgePurchaseOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
|
|
||||||
cancelPurchaseOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -7,21 +7,24 @@ import {
|
|||||||
listShipmentsRouteHandler,
|
listShipmentsRouteHandler,
|
||||||
getShipmentRouteHandler,
|
getShipmentRouteHandler,
|
||||||
editShipmentRouteHandler,
|
editShipmentRouteHandler,
|
||||||
editMultipleShipmentsRouteHandler,
|
|
||||||
newShipmentRouteHandler,
|
newShipmentRouteHandler,
|
||||||
deleteShipmentRouteHandler,
|
deleteShipmentRouteHandler,
|
||||||
listShipmentsByPropertiesRouteHandler,
|
listShipmentsByPropertiesRouteHandler,
|
||||||
getShipmentStatsRouteHandler,
|
getShipmentStatsRouteHandler,
|
||||||
getShipmentHistoryRouteHandler,
|
getShipmentHistoryRouteHandler,
|
||||||
shipShipmentRouteHandler,
|
|
||||||
receiveShipmentRouteHandler,
|
|
||||||
cancelShipmentRouteHandler,
|
|
||||||
} from '../../services/inventory/shipments.js';
|
} from '../../services/inventory/shipments.js';
|
||||||
|
|
||||||
// list of shipments
|
// list of shipments
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property, search, sort, order } = req.query;
|
||||||
const allowedFilters = ['orderType', 'order', 'state', 'courierService', 'order._id', 'taxRate'];
|
const allowedFilters = [
|
||||||
|
'vendor',
|
||||||
|
'purchaseOrder',
|
||||||
|
'state',
|
||||||
|
'courierService',
|
||||||
|
'vendor._id',
|
||||||
|
'purchaseOrder._id',
|
||||||
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
const filter = getFilter(req.query, allowedFilters);
|
||||||
listShipmentsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listShipmentsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
||||||
});
|
});
|
||||||
@ -29,17 +32,17 @@ router.get('/', isAuthenticated, (req, res) => {
|
|||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
router.get('/properties', isAuthenticated, (req, res) => {
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
let properties = convertPropertiesString(req.query.properties);
|
||||||
const allowedFilters = [
|
const allowedFilters = [
|
||||||
'orderType',
|
'vendor',
|
||||||
'order',
|
'purchaseOrder',
|
||||||
'state.type',
|
'state.type',
|
||||||
'courierService',
|
'courierService',
|
||||||
'order._id',
|
'vendor._id',
|
||||||
'taxRate',
|
'purchaseOrder._id',
|
||||||
];
|
];
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
var masterFilter = {};
|
var masterFilter = {};
|
||||||
if (req.query.masterFilter) {
|
if (req.query.masterFilter) {
|
||||||
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true);
|
masterFilter = JSON.parse(req.query.masterFilter);
|
||||||
}
|
}
|
||||||
listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
listShipmentsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||||
});
|
});
|
||||||
@ -62,11 +65,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
|
|||||||
getShipmentRouteHandler(req, res);
|
getShipmentRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update multiple shipments
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultipleShipmentsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
editShipmentRouteHandler(req, res);
|
editShipmentRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
@ -75,16 +73,4 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
|
|||||||
deleteShipmentRouteHandler(req, res);
|
deleteShipmentRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/:id/ship', isAuthenticated, async (req, res) => {
|
|
||||||
shipShipmentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/receive', isAuthenticated, async (req, res) => {
|
|
||||||
receiveShipmentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
|
|
||||||
cancelShipmentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import {
|
|||||||
getStockEventRouteHandler,
|
getStockEventRouteHandler,
|
||||||
newStockEventRouteHandler,
|
newStockEventRouteHandler,
|
||||||
editStockEventRouteHandler,
|
editStockEventRouteHandler,
|
||||||
editMultipleStockEventsRouteHandler,
|
|
||||||
deleteStockEventRouteHandler,
|
deleteStockEventRouteHandler,
|
||||||
listStockEventsByPropertiesRouteHandler,
|
listStockEventsByPropertiesRouteHandler,
|
||||||
getStockEventStatsRouteHandler,
|
getStockEventStatsRouteHandler,
|
||||||
@ -52,11 +51,6 @@ router.get('/:id', isAuthenticated, (req, res) => {
|
|||||||
getStockEventRouteHandler(req, res);
|
getStockEventRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update multiple stock events
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultipleStockEventsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
editStockEventRouteHandler(req, res);
|
editStockEventRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listAppPasswordsRouteHandler,
|
|
||||||
getAppPasswordRouteHandler,
|
|
||||||
editAppPasswordRouteHandler,
|
|
||||||
newAppPasswordRouteHandler,
|
|
||||||
deleteAppPasswordRouteHandler,
|
|
||||||
listAppPasswordsByPropertiesRouteHandler,
|
|
||||||
getAppPasswordStatsRouteHandler,
|
|
||||||
getAppPasswordHistoryRouteHandler,
|
|
||||||
regenerateSecretRouteHandler,
|
|
||||||
} from '../../services/management/apppasswords.js';
|
|
||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['_id', 'name', 'user', 'active', 'user._id'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listAppPasswordsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
const properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['name', 'user', 'active'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
const masterFilter = req.query.masterFilter ? JSON.parse(req.query.masterFilter) : {};
|
|
||||||
listAppPasswordsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newAppPasswordRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getAppPasswordStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getAppPasswordHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/regenerateSecret', isAuthenticated, async (req, res) => {
|
|
||||||
regenerateSecretRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getAppPasswordRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editAppPasswordRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteAppPasswordRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -8,9 +8,7 @@ import {
|
|||||||
listFilamentsByPropertiesRouteHandler,
|
listFilamentsByPropertiesRouteHandler,
|
||||||
getFilamentRouteHandler,
|
getFilamentRouteHandler,
|
||||||
editFilamentRouteHandler,
|
editFilamentRouteHandler,
|
||||||
editMultipleFilamentsRouteHandler,
|
|
||||||
newFilamentRouteHandler,
|
newFilamentRouteHandler,
|
||||||
deleteFilamentRouteHandler,
|
|
||||||
getFilamentStatsRouteHandler,
|
getFilamentStatsRouteHandler,
|
||||||
getFilamentHistoryRouteHandler,
|
getFilamentHistoryRouteHandler,
|
||||||
} from '../../services/management/filaments.js';
|
} from '../../services/management/filaments.js';
|
||||||
@ -21,10 +19,12 @@ router.get('/', isAuthenticated, (req, res) => {
|
|||||||
|
|
||||||
const allowedFilters = [
|
const allowedFilters = [
|
||||||
'_id',
|
'_id',
|
||||||
'material',
|
'type',
|
||||||
'material._id',
|
'vendor.name',
|
||||||
'diameter',
|
'diameter',
|
||||||
|
'color',
|
||||||
'name',
|
'name',
|
||||||
|
'vendor._id',
|
||||||
'cost',
|
'cost',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ 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 = ['diameter', 'material'];
|
const allowedFilters = ['diameter', 'type', 'vendor'];
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
const filter = getFilter(req.query, allowedFilters, false);
|
||||||
listFilamentsByPropertiesRouteHandler(req, res, properties, filter);
|
listFilamentsByPropertiesRouteHandler(req, res, properties, filter);
|
||||||
});
|
});
|
||||||
@ -66,18 +66,9 @@ router.get('/:id', isAuthenticated, (req, res) => {
|
|||||||
getFilamentRouteHandler(req, res);
|
getFilamentRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update filaments info
|
// update printer info
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultipleFilamentsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update filament info
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
editFilamentRouteHandler(req, res);
|
editFilamentRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteFilamentRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listFilamentSkusRouteHandler,
|
|
||||||
getFilamentSkuRouteHandler,
|
|
||||||
editFilamentSkuRouteHandler,
|
|
||||||
newFilamentSkuRouteHandler,
|
|
||||||
deleteFilamentSkuRouteHandler,
|
|
||||||
listFilamentSkusByPropertiesRouteHandler,
|
|
||||||
getFilamentSkuStatsRouteHandler,
|
|
||||||
getFilamentSkuHistoryRouteHandler,
|
|
||||||
} from '../../services/management/filamentskus.js';
|
|
||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['_id', 'barcode', 'filament', 'filament._id', 'name', 'color', 'cost'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listFilamentSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['filament', 'filament._id'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
let masterFilter = {};
|
|
||||||
if (req.query.masterFilter) {
|
|
||||||
masterFilter = JSON.parse(req.query.masterFilter);
|
|
||||||
}
|
|
||||||
listFilamentSkusByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newFilamentSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getFilamentSkuStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getFilamentSkuHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getFilamentSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editFilamentSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteFilamentSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
import { isAuthenticated } from '../../keycloak.js';
|
||||||
import { convertPropertiesString, getFilter, parseFilter } from '../../utils.js';
|
import { parseFilter } from '../../utils.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import {
|
import {
|
||||||
listMaterialsRouteHandler,
|
listMaterialsRouteHandler,
|
||||||
listMaterialsByPropertiesRouteHandler,
|
|
||||||
getMaterialRouteHandler,
|
getMaterialRouteHandler,
|
||||||
editMaterialRouteHandler,
|
editMaterialRouteHandler,
|
||||||
newMaterialRouteHandler,
|
newMaterialRouteHandler,
|
||||||
@ -15,26 +14,22 @@ import {
|
|||||||
|
|
||||||
// list of materials
|
// list of materials
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
router.get('/', isAuthenticated, (req, res) => {
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
const { page, limit, property } = req.query;
|
||||||
|
|
||||||
const allowedFilters = ['_id', 'name', 'tags'];
|
const allowedFilters = ['type', 'brand', 'diameter', 'color'];
|
||||||
|
|
||||||
var filter = {};
|
var filter = {};
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(req.query)) {
|
for (const [key, value] of Object.entries(req.query)) {
|
||||||
if (allowedFilters.includes(key)) {
|
for (var i = 0; i < allowedFilters.length; i++) {
|
||||||
filter = { ...filter, ...parseFilter(key, value) };
|
if (key == allowedFilters[i]) {
|
||||||
|
const parsedFilter = parseFilter(key, value);
|
||||||
|
filter = { ...filter, ...parsedFilter };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listMaterialsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
listMaterialsRouteHandler(req, res, page, limit, property, filter);
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['name', 'tags'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
listMaterialsByPropertiesRouteHandler(req, res, properties, filter);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
router.post('/', isAuthenticated, (req, res) => {
|
||||||
@ -55,7 +50,7 @@ router.get('/:id', isAuthenticated, (req, res) => {
|
|||||||
getMaterialRouteHandler(req, res);
|
getMaterialRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// update material info
|
// update printer info
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
router.put('/:id', isAuthenticated, async (req, res) => {
|
||||||
editMaterialRouteHandler(req, res);
|
editMaterialRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listPartSkusRouteHandler,
|
|
||||||
getPartSkuRouteHandler,
|
|
||||||
editPartSkuRouteHandler,
|
|
||||||
newPartSkuRouteHandler,
|
|
||||||
deletePartSkuRouteHandler,
|
|
||||||
listPartSkusByPropertiesRouteHandler,
|
|
||||||
getPartSkuStatsRouteHandler,
|
|
||||||
getPartSkuHistoryRouteHandler,
|
|
||||||
} from '../../services/management/partskus.js';
|
|
||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['_id', 'barcode', 'part', 'part._id', 'name', 'cost', 'price'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listPartSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['part', 'part._id'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
let masterFilter = {};
|
|
||||||
if (req.query.masterFilter) {
|
|
||||||
masterFilter = JSON.parse(req.query.masterFilter);
|
|
||||||
}
|
|
||||||
listPartSkusByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newPartSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getPartSkuStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getPartSkuHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getPartSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editPartSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deletePartSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listProductSkusRouteHandler,
|
|
||||||
getProductSkuRouteHandler,
|
|
||||||
editProductSkuRouteHandler,
|
|
||||||
newProductSkuRouteHandler,
|
|
||||||
deleteProductSkuRouteHandler,
|
|
||||||
listProductSkusByPropertiesRouteHandler,
|
|
||||||
getProductSkuStatsRouteHandler,
|
|
||||||
getProductSkuHistoryRouteHandler,
|
|
||||||
} from '../../services/management/productskus.js';
|
|
||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['_id', 'barcode', 'product', 'product._id', 'name', 'cost', 'price'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listProductSkusRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['product', 'product._id'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
let masterFilter = {};
|
|
||||||
if (req.query.masterFilter) {
|
|
||||||
masterFilter = JSON.parse(req.query.masterFilter);
|
|
||||||
}
|
|
||||||
listProductSkusByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newProductSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getProductSkuStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getProductSkuHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getProductSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editProductSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteProductSkuRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
editUserRouteHandler,
|
editUserRouteHandler,
|
||||||
getUserStatsRouteHandler,
|
getUserStatsRouteHandler,
|
||||||
getUserHistoryRouteHandler,
|
getUserHistoryRouteHandler,
|
||||||
setAppPasswordRouteHandler,
|
|
||||||
} from '../../services/management/users.js';
|
} from '../../services/management/users.js';
|
||||||
|
|
||||||
// list of document templates
|
// list of document templates
|
||||||
@ -51,8 +50,4 @@ router.put('/:id', isAuthenticated, async (req, res) => {
|
|||||||
editUserRouteHandler(req, res);
|
editUserRouteHandler(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/:id/setAppPassword', isAuthenticated, async (req, res) => {
|
|
||||||
setAppPasswordRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { csvExportRouteHandler } from '../../services/misc/csv.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/:objectType', isAuthenticated, csvExportRouteHandler);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import {
|
|
||||||
excelExportRouteHandler,
|
|
||||||
excelOpenRouteHandler,
|
|
||||||
excelTempRouteHandler,
|
|
||||||
} from '../../services/misc/excel.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// Temp route must come before /:objectType so "temp" is not matched as objectType
|
|
||||||
router.get('/temp/:token', excelTempRouteHandler);
|
|
||||||
router.post('/:objectType/open', isAuthenticated, excelOpenRouteHandler);
|
|
||||||
router.get('/:objectType', isAuthenticated, excelExportRouteHandler);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import {
|
|
||||||
listNotificationsRouteHandler,
|
|
||||||
markNotificationAsReadRouteHandler,
|
|
||||||
markAllNotificationsAsReadRouteHandler,
|
|
||||||
deleteNotificationRouteHandler,
|
|
||||||
deleteAllNotificationsRouteHandler,
|
|
||||||
} from '../../services/misc/notifications.js';
|
|
||||||
import { getFilter } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page = 1, limit = 50, sort = 'createdAt', order = 'descend' } = req.query;
|
|
||||||
const allowedFilters = ['user'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listNotificationsRouteHandler(req, res, page, limit, filter, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/read-all', isAuthenticated, (req, res) => {
|
|
||||||
markAllNotificationsAsReadRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id/read', isAuthenticated, (req, res) => {
|
|
||||||
markNotificationAsReadRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/', isAuthenticated, (req, res) => {
|
|
||||||
deleteAllNotificationsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, (req, res) => {
|
|
||||||
deleteNotificationRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAppAuthenticated } from '../../keycloak.js';
|
|
||||||
import { listODataRouteHandler, metadataODataRouteHandler } from '../../services/misc/odata.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/$metadata', isAppAuthenticated, metadataODataRouteHandler);
|
|
||||||
router.get('/:objectType', isAppAuthenticated, listODataRouteHandler);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import {
|
|
||||||
listUserNotifiersRouteHandler,
|
|
||||||
getUserNotifierRouteHandler,
|
|
||||||
newUserNotifierRouteHandler,
|
|
||||||
deleteUserNotifierRouteHandler,
|
|
||||||
editUserNotifierRouteHandler,
|
|
||||||
} from '../../services/misc/usernotifiers.js';
|
|
||||||
import { getFilter } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['user', 'object', 'objectType'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listUserNotifiersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newUserNotifierRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getUserNotifierRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, (req, res) => {
|
|
||||||
editUserNotifierRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, (req, res) => {
|
|
||||||
deleteUserNotifierRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listClientsRouteHandler,
|
|
||||||
getClientRouteHandler,
|
|
||||||
editClientRouteHandler,
|
|
||||||
newClientRouteHandler,
|
|
||||||
deleteClientRouteHandler,
|
|
||||||
listClientsByPropertiesRouteHandler,
|
|
||||||
getClientStatsRouteHandler,
|
|
||||||
getClientHistoryRouteHandler,
|
|
||||||
} from '../../services/sales/clients.js';
|
|
||||||
|
|
||||||
// list of clients
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['country', 'active', 'createdAt', 'updatedAt'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listClientsRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['country', 'active', 'createdAt', 'updatedAt'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
listClientsByPropertiesRouteHandler(req, res, properties, filter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newClientRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get client stats
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getClientStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get clients history
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getClientHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getClientRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editClientRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteClientRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listMarketplacesRouteHandler,
|
|
||||||
getMarketplaceRouteHandler,
|
|
||||||
editMarketplaceRouteHandler,
|
|
||||||
newMarketplaceRouteHandler,
|
|
||||||
deleteMarketplaceRouteHandler,
|
|
||||||
listMarketplacesByPropertiesRouteHandler,
|
|
||||||
getMarketplaceStatsRouteHandler,
|
|
||||||
getMarketplaceHistoryRouteHandler,
|
|
||||||
} from '../../services/sales/marketplaces.js';
|
|
||||||
|
|
||||||
// list of marketplaces
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['name', 'provider', 'active', 'createdAt', 'updatedAt'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listMarketplacesRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['name', 'provider', 'active', 'createdAt', 'updatedAt'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
listMarketplacesByPropertiesRouteHandler(req, res, properties, filter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newMarketplaceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get marketplace stats
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getMarketplaceStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get marketplaces history
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getMarketplaceHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getMarketplaceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editMarketplaceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteMarketplaceRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { isAuthenticated } from '../../keycloak.js';
|
|
||||||
import { getFilter, convertPropertiesString } from '../../utils.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
import {
|
|
||||||
listSalesOrdersRouteHandler,
|
|
||||||
getSalesOrderRouteHandler,
|
|
||||||
editSalesOrderRouteHandler,
|
|
||||||
editMultipleSalesOrdersRouteHandler,
|
|
||||||
newSalesOrderRouteHandler,
|
|
||||||
deleteSalesOrderRouteHandler,
|
|
||||||
listSalesOrdersByPropertiesRouteHandler,
|
|
||||||
getSalesOrderStatsRouteHandler,
|
|
||||||
getSalesOrderHistoryRouteHandler,
|
|
||||||
postSalesOrderRouteHandler,
|
|
||||||
confirmSalesOrderRouteHandler,
|
|
||||||
cancelSalesOrderRouteHandler,
|
|
||||||
} from '../../services/sales/salesorders.js';
|
|
||||||
|
|
||||||
// list of sales orders
|
|
||||||
router.get('/', isAuthenticated, (req, res) => {
|
|
||||||
const { page, limit, property, search, sort, order } = req.query;
|
|
||||||
const allowedFilters = ['client', 'state', 'value', 'client._id'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters);
|
|
||||||
listSalesOrdersRouteHandler(req, res, page, limit, property, filter, search, sort, order);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/properties', isAuthenticated, (req, res) => {
|
|
||||||
let properties = convertPropertiesString(req.query.properties);
|
|
||||||
const allowedFilters = ['client', 'state.type', 'value', 'client._id'];
|
|
||||||
const filter = getFilter(req.query, allowedFilters, false);
|
|
||||||
var masterFilter = {};
|
|
||||||
if (req.query.masterFilter) {
|
|
||||||
masterFilter = getFilter(JSON.parse(req.query.masterFilter), allowedFilters, true);
|
|
||||||
}
|
|
||||||
listSalesOrdersByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', isAuthenticated, (req, res) => {
|
|
||||||
newSalesOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get sales order stats
|
|
||||||
router.get('/stats', isAuthenticated, (req, res) => {
|
|
||||||
getSalesOrderStatsRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// get sales orders history
|
|
||||||
router.get('/history', isAuthenticated, (req, res) => {
|
|
||||||
getSalesOrderHistoryRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', isAuthenticated, (req, res) => {
|
|
||||||
getSalesOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update multiple sales orders
|
|
||||||
router.put('/', isAuthenticated, async (req, res) => {
|
|
||||||
editMultipleSalesOrdersRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
editSalesOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id', isAuthenticated, async (req, res) => {
|
|
||||||
deleteSalesOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/post', isAuthenticated, async (req, res) => {
|
|
||||||
postSalesOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/confirm', isAuthenticated, async (req, res) => {
|
|
||||||
confirmSalesOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/cancel', isAuthenticated, async (req, res) => {
|
|
||||||
cancelSalesOrderRouteHandler(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|
||||||
@ -1,154 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
// Mock dependencies
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
editObjects: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
checkStates: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/finance/invoice.schema.js', () => ({
|
|
||||||
invoiceModel: { modelName: 'Invoice' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/orderitem.schema.js', () => ({
|
|
||||||
orderItemModel: { modelName: 'OrderItem' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/shipment.schema.js', () => ({
|
|
||||||
shipmentModel: { modelName: 'Shipment' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Import handlers after mocking
|
|
||||||
const {
|
|
||||||
listInvoicesRouteHandler,
|
|
||||||
getInvoiceRouteHandler,
|
|
||||||
newInvoiceRouteHandler,
|
|
||||||
editInvoiceRouteHandler,
|
|
||||||
deleteInvoiceRouteHandler,
|
|
||||||
postInvoiceRouteHandler,
|
|
||||||
} = await import('../invoices.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, editObject, newObject, deleteObject, checkStates } = await import(
|
|
||||||
'../../../database/database.js'
|
|
||||||
);
|
|
||||||
const { invoiceModel } = await import('../../../database/schemas/finance/invoice.schema.js');
|
|
||||||
|
|
||||||
describe('Invoice Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listInvoicesRouteHandler', () => {
|
|
||||||
it('should list invoices', async () => {
|
|
||||||
const mockResult = [{ _id: '1', invoiceNumber: 'INV-001' }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listInvoicesRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getInvoiceRouteHandler', () => {
|
|
||||||
it('should get an invoice by ID', async () => {
|
|
||||||
req.params.id = '123';
|
|
||||||
const mockResult = { _id: '123', invoiceNumber: 'INV-001' };
|
|
||||||
getObject.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await getInvoiceRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: '123' }));
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newInvoiceRouteHandler', () => {
|
|
||||||
it('should create a new invoice with order items and shipments', async () => {
|
|
||||||
req.body = { order: 'order123', orderType: 'sales' };
|
|
||||||
|
|
||||||
// Mock listObjects for orderItems and shipments
|
|
||||||
listObjects.mockResolvedValueOnce([{ _id: 'oi1', totalAmount: 100, invoicedAmount: 0 }]); // orderItems
|
|
||||||
listObjects.mockResolvedValueOnce([{ _id: 's1', amount: 20, invoicedAmount: 0 }]); // shipments
|
|
||||||
|
|
||||||
const mockInvoice = { _id: 'inv456' };
|
|
||||||
newObject.mockResolvedValue(mockInvoice);
|
|
||||||
|
|
||||||
await newInvoiceRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockInvoice);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('postInvoiceRouteHandler', () => {
|
|
||||||
it('should post a draft invoice and update order items/shipments', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
|
|
||||||
checkStates.mockResolvedValue(true);
|
|
||||||
|
|
||||||
const mockInvoice = {
|
|
||||||
_id: '507f1f77bcf86cd799439011',
|
|
||||||
invoiceOrderItems: [{ orderItem: { _id: 'oi1' }, invoiceAmount: 50, invoiceQuantity: 1 }],
|
|
||||||
invoiceShipments: [{ shipment: { _id: 's1' }, invoiceAmount: 10 }],
|
|
||||||
};
|
|
||||||
getObject.mockResolvedValueOnce(mockInvoice);
|
|
||||||
|
|
||||||
// Mock getObject for individual orderItems and shipments
|
|
||||||
getObject.mockResolvedValueOnce({ _id: 'oi1', invoicedAmount: 0, invoicedQuantity: 0 });
|
|
||||||
getObject.mockResolvedValueOnce({ _id: 's1', invoicedAmount: 0 });
|
|
||||||
|
|
||||||
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'sent' } });
|
|
||||||
|
|
||||||
await postInvoiceRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['draft'] }));
|
|
||||||
expect(editObject).toHaveBeenCalled(); // Should be called for items, shipments, and the invoice itself
|
|
||||||
expect(res.send).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if invoice is not in draft state', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
checkStates.mockResolvedValue(false);
|
|
||||||
|
|
||||||
await postInvoiceRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(400);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ error: 'Invoice is not in draft state.' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
editObjects: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
checkStates: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/finance/payment.schema.js', () => ({
|
|
||||||
paymentModel: { modelName: 'Payment' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/finance/invoice.schema.js', () => ({
|
|
||||||
invoiceModel: { modelName: 'Invoice' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listPaymentsRouteHandler,
|
|
||||||
getPaymentRouteHandler,
|
|
||||||
newPaymentRouteHandler,
|
|
||||||
postPaymentRouteHandler,
|
|
||||||
} = await import('../payments.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, editObject, newObject, checkStates } = await import(
|
|
||||||
'../../../database/database.js'
|
|
||||||
);
|
|
||||||
const { paymentModel } = await import('../../../database/schemas/finance/payment.schema.js');
|
|
||||||
const { invoiceModel } = await import('../../../database/schemas/finance/invoice.schema.js');
|
|
||||||
|
|
||||||
describe('Payment Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listPaymentsRouteHandler', () => {
|
|
||||||
it('should list payments', async () => {
|
|
||||||
const mockResult = [{ _id: '1', amount: 100 }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listPaymentsRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newPaymentRouteHandler', () => {
|
|
||||||
it('should create a new payment with invoice data', async () => {
|
|
||||||
req.body = { invoice: 'inv123', amount: 100 };
|
|
||||||
const mockInvoice = { _id: 'inv123', vendor: { _id: 'v1' }, client: { _id: 'c1' } };
|
|
||||||
getObject.mockResolvedValueOnce(mockInvoice);
|
|
||||||
const mockPayment = { _id: 'pay456', ...req.body };
|
|
||||||
newObject.mockResolvedValue(mockPayment);
|
|
||||||
|
|
||||||
await newPaymentRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(getObject).toHaveBeenCalledWith(expect.objectContaining({ id: 'inv123' }));
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockPayment);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('postPaymentRouteHandler', () => {
|
|
||||||
it('should post a draft payment', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
checkStates.mockResolvedValue(true);
|
|
||||||
|
|
||||||
const mockPayment = { _id: '507f1f77bcf86cd799439011', invoice: 'inv123' };
|
|
||||||
getObject.mockResolvedValueOnce(mockPayment);
|
|
||||||
getObject.mockResolvedValueOnce({ _id: 'inv123' });
|
|
||||||
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'posted' } });
|
|
||||||
|
|
||||||
await postPaymentRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['draft'] }));
|
|
||||||
expect(editObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if payment is not in draft state', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
checkStates.mockResolvedValue(false);
|
|
||||||
|
|
||||||
await postPaymentRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(400);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ error: 'Payment is not in draft state.' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,554 +0,0 @@
|
|||||||
import config from '../../config.js';
|
|
||||||
import { invoiceModel } from '../../database/schemas/finance/invoice.schema.js';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
import {
|
|
||||||
deleteObject,
|
|
||||||
listObjects,
|
|
||||||
getObject,
|
|
||||||
editObject,
|
|
||||||
editObjects,
|
|
||||||
newObject,
|
|
||||||
listObjectsByProperties,
|
|
||||||
getModelStats,
|
|
||||||
getModelHistory,
|
|
||||||
checkStates,
|
|
||||||
} from '../../database/database.js';
|
|
||||||
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
|
|
||||||
import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Invoices');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
export const listInvoicesRouteHandler = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
page = 1,
|
|
||||||
limit = 25,
|
|
||||||
property = '',
|
|
||||||
filter = {},
|
|
||||||
search = '',
|
|
||||||
sort = '',
|
|
||||||
order = 'ascend'
|
|
||||||
) => {
|
|
||||||
const populateFields = [
|
|
||||||
{ path: 'to', strictPopulate: false, ref: 'client' },
|
|
||||||
{ path: 'from', strictPopulate: false, ref: 'vendor' },
|
|
||||||
{ path: 'order' },
|
|
||||||
];
|
|
||||||
const result = await listObjects({
|
|
||||||
model: invoiceModel,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
property,
|
|
||||||
filter,
|
|
||||||
search,
|
|
||||||
sort,
|
|
||||||
order,
|
|
||||||
populate: populateFields,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error listing invoices.');
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`List of invoices (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listInvoicesByPropertiesRouteHandler = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
properties = '',
|
|
||||||
filter = {},
|
|
||||||
masterFilter = {}
|
|
||||||
) => {
|
|
||||||
const populateFields = ['to', 'from', 'order'];
|
|
||||||
const result = await listObjectsByProperties({
|
|
||||||
model: invoiceModel,
|
|
||||||
properties,
|
|
||||||
filter,
|
|
||||||
populate: populateFields,
|
|
||||||
masterFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error listing invoices.');
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`List of invoices. Count: ${result.length}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getInvoiceRouteHandler = async (req, res) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
const populateFields = [
|
|
||||||
{ path: 'to', strictPopulate: false },
|
|
||||||
{ path: 'from', strictPopulate: false },
|
|
||||||
{ path: 'order' },
|
|
||||||
{ path: 'invoiceOrderItems.taxRate' },
|
|
||||||
{ path: 'invoiceShipments.taxRate' },
|
|
||||||
{ path: 'invoiceOrderItems.orderItem' },
|
|
||||||
{ path: 'invoiceShipments.shipment' },
|
|
||||||
];
|
|
||||||
const result = await getObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id,
|
|
||||||
populate: populateFields,
|
|
||||||
});
|
|
||||||
if (result?.error) {
|
|
||||||
logger.warn(`Invoice not found with supplied id.`);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.debug(`Retrieved invoice with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const editInvoiceRouteHandler = async (req, res) => {
|
|
||||||
// Get ID from params
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Invoice with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: invoiceModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking invoice states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Invoice is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Invoice is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
vendor: req.body.vendor,
|
|
||||||
client: req.body.client,
|
|
||||||
invoiceType: req.body.invoiceType,
|
|
||||||
invoiceDate: req.body.invoiceDate,
|
|
||||||
dueAt: req.body.dueDate,
|
|
||||||
issuedAt: req.body.issuedAt,
|
|
||||||
orderType: req.body.orderType,
|
|
||||||
order: req.body.order,
|
|
||||||
invoiceOrderItems: req.body.invoiceOrderItems,
|
|
||||||
invoiceShipments: req.body.invoiceShipments,
|
|
||||||
from: req.body.from,
|
|
||||||
to: req.body.to,
|
|
||||||
};
|
|
||||||
// Create audit log before updating
|
|
||||||
const result = await editObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing invoice:', result.error);
|
|
||||||
res.status(result).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited invoice with ID: ${id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const editMultipleInvoicesRouteHandler = async (req, res) => {
|
|
||||||
const updates = req.body.map((update) => ({
|
|
||||||
_id: update._id,
|
|
||||||
vendor: update.vendor,
|
|
||||||
client: update.client,
|
|
||||||
invoiceType: update.invoiceType,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!Array.isArray(updates)) {
|
|
||||||
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await editObjects({
|
|
||||||
model: invoiceModel,
|
|
||||||
updates,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing invoices:', result.error);
|
|
||||||
res.status(result.code || 500).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited ${updates.length} invoices`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newInvoiceRouteHandler = async (req, res) => {
|
|
||||||
const orderItems = await listObjects({
|
|
||||||
model: orderItemModel,
|
|
||||||
filter: { order: req.body.order, orderType: req.body.orderType },
|
|
||||||
});
|
|
||||||
|
|
||||||
const shipments = await listObjects({
|
|
||||||
model: shipmentModel,
|
|
||||||
filter: { order: req.body.order, orderType: req.body.orderType },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (orderItems.error) {
|
|
||||||
logger.error('Error getting order items:', orderItems.error);
|
|
||||||
return res.status(orderItems.code).send(orderItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shipments.error) {
|
|
||||||
logger.error('Error getting shipments:', shipments.error);
|
|
||||||
return res.status(shipments.code).send(shipments);
|
|
||||||
}
|
|
||||||
|
|
||||||
const invoiceOrderItems = orderItems
|
|
||||||
.map((orderItem) => {
|
|
||||||
const invoicedAmountWithTax = orderItem.invoicedAmountWithTax || 0;
|
|
||||||
const totalAmountWithTax = orderItem.totalAmountWithTax || 0;
|
|
||||||
const invoicedAmount = orderItem.invoicedAmount || 0;
|
|
||||||
const totalAmount = orderItem.totalAmount || 0;
|
|
||||||
const quantity = (orderItem.quantity || 0) - (orderItem.invoicedQuantity || 0);
|
|
||||||
const taxRate = orderItem?.taxRate?._id;
|
|
||||||
|
|
||||||
if (invoicedAmountWithTax >= totalAmountWithTax || invoicedAmount >= totalAmount) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var finalQuantity = quantity;
|
|
||||||
if (finalQuantity <= 0) {
|
|
||||||
finalQuantity = 1;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
orderItem: orderItem._id,
|
|
||||||
taxRate: taxRate,
|
|
||||||
invoiceAmountWithTax: totalAmountWithTax - invoicedAmountWithTax,
|
|
||||||
invoiceAmount: totalAmount - invoicedAmount,
|
|
||||||
invoiceQuantity: finalQuantity,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((item) => item !== null);
|
|
||||||
|
|
||||||
const invoiceShipments = shipments
|
|
||||||
.map((shipment) => {
|
|
||||||
const invoicedAmount = shipment.invoicedAmount || 0;
|
|
||||||
const amountWithTax = shipment.amountWithTax || 0;
|
|
||||||
const invoicedAmountWithTax = shipment.invoicedAmountWithTax || 0;
|
|
||||||
const amount = shipment.amount || 0;
|
|
||||||
const taxRate = shipment?.taxRate || null;
|
|
||||||
|
|
||||||
if (invoicedAmountWithTax >= amountWithTax || invoicedAmount >= amount) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
shipment: shipment._id,
|
|
||||||
taxRate: taxRate,
|
|
||||||
invoiceAmountWithTax: amountWithTax - invoicedAmountWithTax,
|
|
||||||
invoiceAmount: amount - invoicedAmount,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((item) => item !== null);
|
|
||||||
|
|
||||||
const newData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
vendor: req.body.vendor,
|
|
||||||
client: req.body.client,
|
|
||||||
issuedAt: req.body.issuedAt || new Date(),
|
|
||||||
dueAt: req.body.dueAt || new Date(),
|
|
||||||
orderType: req.body.orderType,
|
|
||||||
order: req.body.order,
|
|
||||||
totalAmount: 0,
|
|
||||||
totalAmountWithTax: 0,
|
|
||||||
totalTaxAmount: 0,
|
|
||||||
grandTotalAmount: 0,
|
|
||||||
shippingAmount: 0,
|
|
||||||
shippingAmountWithTax: 0,
|
|
||||||
invoiceOrderItems: invoiceOrderItems,
|
|
||||||
invoiceShipments: invoiceShipments,
|
|
||||||
from: req.body.from,
|
|
||||||
to: req.body.to,
|
|
||||||
};
|
|
||||||
const result = await newObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
newData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('No invoice created:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`New invoice with ID: ${result._id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteInvoiceRouteHandler = async (req, res) => {
|
|
||||||
// Get ID from params
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Invoice with ID: ${id}`);
|
|
||||||
|
|
||||||
const result = await deleteObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('No invoice deleted:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Deleted invoice with ID: ${result._id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getInvoiceStatsRouteHandler = async (req, res) => {
|
|
||||||
const result = await getModelStats({ model: invoiceModel });
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error fetching invoice stats:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.trace('Invoice stats:', result);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getInvoiceHistoryRouteHandler = async (req, res) => {
|
|
||||||
const from = req.query.from;
|
|
||||||
const to = req.query.to;
|
|
||||||
const result = await getModelHistory({ model: invoiceModel, from, to });
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error fetching invoice history:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.trace('Invoice history:', result);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const acknowledgeInvoiceRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Invoice with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: invoiceModel, id, states: ['sent'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking invoice states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Invoice is not in sent state.');
|
|
||||||
res.status(400).send({ error: 'Invoice is not in sent state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'acknowledged' },
|
|
||||||
acknowledgedAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error acknowledging invoice:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Acknowledged invoice with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const postInvoiceRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Invoice with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: invoiceModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking invoice states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Invoice is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Invoice is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const invoice = await getObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const invoiceOrderItems = invoice.invoiceOrderItems;
|
|
||||||
const invoiceShipments = invoice.invoiceShipments;
|
|
||||||
|
|
||||||
for (const invoiceOrderItem of invoiceOrderItems) {
|
|
||||||
const orderItem = await getObject({
|
|
||||||
model: orderItemModel,
|
|
||||||
id: invoiceOrderItem.orderItem._id,
|
|
||||||
});
|
|
||||||
if (orderItem.error) {
|
|
||||||
logger.error('Error getting order item:', orderItem.error);
|
|
||||||
return res.status(orderItem.code).send(orderItem);
|
|
||||||
}
|
|
||||||
const invoiceQuantity = invoiceOrderItem.invoiceQuantity || 0;
|
|
||||||
const invoiceAmount = invoiceOrderItem.invoiceAmount || 0;
|
|
||||||
const invoiceAmountWithTax = invoiceOrderItem.invoiceAmountWithTax || 0;
|
|
||||||
const invoicedQuantity = orderItem.invoicedQuantity || 0;
|
|
||||||
const invoicedAmount = orderItem.invoicedAmount || 0;
|
|
||||||
const invoicedAmountWithTax = orderItem.invoicedAmountWithTax || 0;
|
|
||||||
var quantity = (orderItem.invoiceQuantity || 0) + invoiceQuantity;
|
|
||||||
if (quantity <= orderItem.quantity) {
|
|
||||||
quantity = orderItem.quantity;
|
|
||||||
}
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
invoicedQuantity: invoicedQuantity + invoiceQuantity,
|
|
||||||
invoicedAmount: invoicedAmount + invoiceAmount,
|
|
||||||
invoicedAmountWithTax: invoicedAmountWithTax + invoiceAmountWithTax,
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: orderItemModel,
|
|
||||||
id: orderItem._id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error updating order item:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.debug(`Updated order item with ID: ${orderItem._id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const invoiceShipment of invoiceShipments) {
|
|
||||||
const shipmentId = invoiceShipment.shipment._id || invoiceShipment.shipment;
|
|
||||||
const shipment = await getObject({
|
|
||||||
model: shipmentModel,
|
|
||||||
id: shipmentId,
|
|
||||||
});
|
|
||||||
if (shipment.error) {
|
|
||||||
logger.error('Error getting shipment:', shipment.error);
|
|
||||||
return res.status(shipment.code).send(shipment);
|
|
||||||
}
|
|
||||||
const invoiceAmount = invoiceShipment.invoiceAmount || 0;
|
|
||||||
const invoiceAmountWithTax = invoiceShipment.invoiceAmountWithTax || 0;
|
|
||||||
const invoicedAmount = shipment.invoicedAmount || 0;
|
|
||||||
const invoicedAmountWithTax = shipment.invoicedAmountWithTax || 0;
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
invoicedAmount: invoicedAmount + invoiceAmount,
|
|
||||||
invoicedAmountWithTax: invoicedAmountWithTax + invoiceAmountWithTax,
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: shipmentModel,
|
|
||||||
id: shipment._id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error updating shipment:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.debug(`Updated shipment with ID: ${shipment._id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invoiceOrderItems.error) {
|
|
||||||
logger.error('Error getting invoice order items:', invoiceOrderItems.error);
|
|
||||||
return res.status(invoiceOrderItems.code).send(invoiceOrderItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invoiceShipments.error) {
|
|
||||||
logger.error('Error getting invoice shipments:', invoiceShipments.error);
|
|
||||||
return res.status(invoiceShipments.code).send(invoiceShipments);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'sent' },
|
|
||||||
postedAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error posting invoice:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Posted invoice with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelInvoiceRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Invoice with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({
|
|
||||||
model: invoiceModel,
|
|
||||||
id,
|
|
||||||
states: ['draft', 'sent'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking invoice states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Invoice is not in a cancellable state.');
|
|
||||||
res.status(400).send({
|
|
||||||
error: 'Invoice is not in a cancellable state (must be draft or sent).',
|
|
||||||
code: 400,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'cancelled' },
|
|
||||||
cancelledAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error cancelling invoice:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Cancelled invoice with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
@ -1,373 +0,0 @@
|
|||||||
import config from '../../config.js';
|
|
||||||
import { paymentModel } from '../../database/schemas/finance/payment.schema.js';
|
|
||||||
import log4js from 'log4js';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
import {
|
|
||||||
deleteObject,
|
|
||||||
listObjects,
|
|
||||||
getObject,
|
|
||||||
editObject,
|
|
||||||
editObjects,
|
|
||||||
newObject,
|
|
||||||
listObjectsByProperties,
|
|
||||||
getModelStats,
|
|
||||||
getModelHistory,
|
|
||||||
checkStates,
|
|
||||||
} from '../../database/database.js';
|
|
||||||
import { invoiceModel } from '../../database/schemas/finance/invoice.schema.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Payments');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
export const listPaymentsRouteHandler = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
page = 1,
|
|
||||||
limit = 25,
|
|
||||||
property = '',
|
|
||||||
filter = {},
|
|
||||||
search = '',
|
|
||||||
sort = '',
|
|
||||||
order = 'ascend'
|
|
||||||
) => {
|
|
||||||
const populateFields = ['vendor', 'client', 'invoice'];
|
|
||||||
const result = await listObjects({
|
|
||||||
model: paymentModel,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
property,
|
|
||||||
filter,
|
|
||||||
search,
|
|
||||||
sort,
|
|
||||||
order,
|
|
||||||
populate: populateFields,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error listing payments.');
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`List of payments (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listPaymentsByPropertiesRouteHandler = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
properties = '',
|
|
||||||
filter = {},
|
|
||||||
masterFilter = {}
|
|
||||||
) => {
|
|
||||||
const populateFields = ['vendor', 'client', 'invoice'];
|
|
||||||
const result = await listObjectsByProperties({
|
|
||||||
model: paymentModel,
|
|
||||||
properties,
|
|
||||||
filter,
|
|
||||||
populate: populateFields,
|
|
||||||
masterFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error listing payments.');
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`List of payments. Count: ${result.length}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPaymentRouteHandler = async (req, res) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
const populateFields = [
|
|
||||||
{ path: 'vendor' },
|
|
||||||
{ path: 'client' },
|
|
||||||
{ path: 'invoice' },
|
|
||||||
];
|
|
||||||
const result = await getObject({
|
|
||||||
model: paymentModel,
|
|
||||||
id,
|
|
||||||
populate: populateFields,
|
|
||||||
});
|
|
||||||
if (result?.error) {
|
|
||||||
logger.warn(`Payment not found with supplied id.`);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.debug(`Retrieved payment with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const editPaymentRouteHandler = async (req, res) => {
|
|
||||||
// Get ID from params
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Payment with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: paymentModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking payment states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Payment is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Payment is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
vendor: req.body.vendor,
|
|
||||||
client: req.body.client,
|
|
||||||
invoice: req.body.invoice,
|
|
||||||
amount: req.body.amount,
|
|
||||||
paymentDate: req.body.paymentDate,
|
|
||||||
paymentMethod: req.body.paymentMethod,
|
|
||||||
notes: req.body.notes,
|
|
||||||
};
|
|
||||||
// Create audit log before updating
|
|
||||||
const result = await editObject({
|
|
||||||
model: paymentModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing payment:', result.error);
|
|
||||||
res.status(result).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited payment with ID: ${id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const editMultiplePaymentsRouteHandler = async (req, res) => {
|
|
||||||
const updates = req.body.map((update) => ({
|
|
||||||
_id: update._id,
|
|
||||||
vendor: update.vendor,
|
|
||||||
client: update.client,
|
|
||||||
invoice: update.invoice,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!Array.isArray(updates)) {
|
|
||||||
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await editObjects({
|
|
||||||
model: paymentModel,
|
|
||||||
updates,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing payments:', result.error);
|
|
||||||
res.status(result.code || 500).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited ${updates.length} payments`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newPaymentRouteHandler = async (req, res) => {
|
|
||||||
// Get invoice to populate vendor/client
|
|
||||||
const invoice = await getObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id: req.body.invoice,
|
|
||||||
populate: [{ path: 'vendor' }, { path: 'client' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (invoice.error) {
|
|
||||||
logger.error('Error getting invoice:', invoice.error);
|
|
||||||
return res.status(invoice.code).send(invoice);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
vendor: invoice.vendor?._id || req.body.vendor,
|
|
||||||
client: invoice.client?._id || req.body.client,
|
|
||||||
invoice: req.body.invoice,
|
|
||||||
amount: req.body.amount || 0,
|
|
||||||
paymentDate: req.body.paymentDate || new Date(),
|
|
||||||
paymentMethod: req.body.paymentMethod,
|
|
||||||
notes: req.body.notes,
|
|
||||||
};
|
|
||||||
const result = await newObject({
|
|
||||||
model: paymentModel,
|
|
||||||
newData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('No payment created:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`New payment with ID: ${result._id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deletePaymentRouteHandler = async (req, res) => {
|
|
||||||
// Get ID from params
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Payment with ID: ${id}`);
|
|
||||||
|
|
||||||
const result = await deleteObject({
|
|
||||||
model: paymentModel,
|
|
||||||
id,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('No payment deleted:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Deleted payment with ID: ${result._id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPaymentStatsRouteHandler = async (req, res) => {
|
|
||||||
const result = await getModelStats({ model: paymentModel });
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error fetching payment stats:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.trace('Payment stats:', result);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPaymentHistoryRouteHandler = async (req, res) => {
|
|
||||||
const from = req.query.from;
|
|
||||||
const to = req.query.to;
|
|
||||||
const result = await getModelHistory({ model: paymentModel, from, to });
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error fetching payment history:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.trace('Payment history:', result);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const postPaymentRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Payment with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: paymentModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking payment states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Payment is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Payment is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const payment = await getObject({
|
|
||||||
model: paymentModel,
|
|
||||||
id,
|
|
||||||
populate: [{ path: 'invoice' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (payment.error) {
|
|
||||||
logger.error('Error getting payment:', payment.error);
|
|
||||||
return res.status(payment.code).send(payment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update invoice paid amounts if needed
|
|
||||||
if (payment.invoice) {
|
|
||||||
const invoice = await getObject({
|
|
||||||
model: invoiceModel,
|
|
||||||
id: payment.invoice._id || payment.invoice,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!invoice.error) {
|
|
||||||
// You can add logic here to update invoice paid amounts
|
|
||||||
// This is a simplified version - adjust based on your business logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'posted' },
|
|
||||||
postedAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: paymentModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error posting payment:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Posted payment with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelPaymentRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Payment with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({
|
|
||||||
model: paymentModel,
|
|
||||||
id,
|
|
||||||
states: ['draft', 'posted'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking payment states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Payment is not in a cancellable state.');
|
|
||||||
res.status(400).send({
|
|
||||||
error: 'Payment is not in a cancellable state (must be draft or posted).',
|
|
||||||
code: 400,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'cancelled' },
|
|
||||||
cancelledAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: paymentModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error cancelling payment:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Cancelled payment with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
editObjects: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/filamentstock.schema.js', () => ({
|
|
||||||
filamentStockModel: { modelName: 'FilamentStock' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listFilamentStocksRouteHandler,
|
|
||||||
getFilamentStockRouteHandler,
|
|
||||||
newFilamentStockRouteHandler,
|
|
||||||
} = await import('../filamentstocks.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
|
|
||||||
const { filamentStockModel } = await import(
|
|
||||||
'../../../database/schemas/inventory/filamentstock.schema.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Filament Stock Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listFilamentStocksRouteHandler', () => {
|
|
||||||
it('should list filament stocks', async () => {
|
|
||||||
const mockResult = [{ _id: '1', currentWeight: 500 }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listFilamentStocksRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ model: filamentStockModel })
|
|
||||||
);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newFilamentStockRouteHandler', () => {
|
|
||||||
it('should create a new filament stock', async () => {
|
|
||||||
req.body = { filament: 'filament123', currentWeight: 1000 };
|
|
||||||
const mockStock = { _id: '456', ...req.body };
|
|
||||||
newObject.mockResolvedValue(mockStock);
|
|
||||||
|
|
||||||
await newFilamentStockRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockStock);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
editObjects: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
aggregateRollups: jest.fn(),
|
|
||||||
aggregateRollupsHistory: 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
editObjects: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/partstock.schema.js', () => ({
|
|
||||||
partStockModel: { modelName: 'PartStock' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listPartStocksRouteHandler,
|
|
||||||
getPartStockRouteHandler,
|
|
||||||
newPartStockRouteHandler,
|
|
||||||
editPartStockRouteHandler,
|
|
||||||
deletePartStockRouteHandler,
|
|
||||||
} = await import('../partstocks.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, editObject, newObject, deleteObject } = await import(
|
|
||||||
'../../../database/database.js'
|
|
||||||
);
|
|
||||||
const { partStockModel } = await import('../../../database/schemas/inventory/partstock.schema.js');
|
|
||||||
|
|
||||||
describe('Part Stock Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listPartStocksRouteHandler', () => {
|
|
||||||
it('should list part stocks', async () => {
|
|
||||||
const mockResult = [{ _id: '1', currentQuantity: 100 }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listPartStocksRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ model: partStockModel })
|
|
||||||
);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newPartStockRouteHandler', () => {
|
|
||||||
it('should create a new part stock', async () => {
|
|
||||||
req.body = { part: 'part123', currentQuantity: 50 };
|
|
||||||
const mockStock = { _id: '456', ...req.body };
|
|
||||||
newObject.mockResolvedValue(mockStock);
|
|
||||||
|
|
||||||
await newPartStockRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockStock);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
editObjects: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
checkStates: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/purchaseorder.schema.js', () => ({
|
|
||||||
purchaseOrderModel: { modelName: 'PurchaseOrder' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/orderitem.schema.js', () => ({
|
|
||||||
orderItemModel: { modelName: 'OrderItem' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/shipment.schema.js', () => ({
|
|
||||||
shipmentModel: { modelName: 'Shipment' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listPurchaseOrdersRouteHandler,
|
|
||||||
getPurchaseOrderRouteHandler,
|
|
||||||
newPurchaseOrderRouteHandler,
|
|
||||||
postPurchaseOrderRouteHandler,
|
|
||||||
} = await import('../purchaseorders.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, editObject, newObject, checkStates } = await import(
|
|
||||||
'../../../database/database.js'
|
|
||||||
);
|
|
||||||
const { purchaseOrderModel } = await import(
|
|
||||||
'../../../database/schemas/inventory/purchaseorder.schema.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Purchase Order Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listPurchaseOrdersRouteHandler', () => {
|
|
||||||
it('should list purchase orders', async () => {
|
|
||||||
const mockResult = [{ _id: '1', reference: 'PO-001' }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listPurchaseOrdersRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('postPurchaseOrderRouteHandler', () => {
|
|
||||||
it('should post a draft purchase order and update items/shipments', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
|
|
||||||
checkStates.mockResolvedValue(true);
|
|
||||||
|
|
||||||
listObjects.mockResolvedValueOnce([
|
|
||||||
{ _id: 'oi1', state: { type: 'draft' }, shipment: 's1', _reference: 'ITEM1' },
|
|
||||||
]);
|
|
||||||
listObjects.mockResolvedValueOnce([
|
|
||||||
{ _id: 's1', state: { type: 'draft' }, _reference: 'SHIP1' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'sent' } });
|
|
||||||
|
|
||||||
await postPurchaseOrderRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['draft'] }));
|
|
||||||
expect(editObject).toHaveBeenCalledTimes(3);
|
|
||||||
expect(res.send).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail if an order item is not in draft state', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
checkStates.mockResolvedValue(true);
|
|
||||||
|
|
||||||
listObjects.mockResolvedValueOnce([
|
|
||||||
{ _id: 'oi1', state: { type: 'ordered' }, _reference: 'ITEM1' },
|
|
||||||
]);
|
|
||||||
listObjects.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
await postPurchaseOrderRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(400);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ error: 'Order item ITEM1 not in draft state.' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
editObjects: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
checkStates: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/shipment.schema.js', () => ({
|
|
||||||
shipmentModel: { modelName: 'Shipment' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/orderitem.schema.js', () => ({
|
|
||||||
orderItemModel: { modelName: 'OrderItem' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listShipmentsRouteHandler,
|
|
||||||
getShipmentRouteHandler,
|
|
||||||
newShipmentRouteHandler,
|
|
||||||
shipShipmentRouteHandler,
|
|
||||||
} = await import('../shipments.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, editObject, newObject, checkStates } = await import(
|
|
||||||
'../../../database/database.js'
|
|
||||||
);
|
|
||||||
const { shipmentModel } = await import('../../../database/schemas/inventory/shipment.schema.js');
|
|
||||||
const { orderItemModel } = await import('../../../database/schemas/inventory/orderitem.schema.js');
|
|
||||||
|
|
||||||
describe('Shipment Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listShipmentsRouteHandler', () => {
|
|
||||||
it('should list shipments', async () => {
|
|
||||||
const mockResult = [{ _id: '1', trackingNumber: 'TRACK123' }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listShipmentsRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: shipmentModel }));
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newShipmentRouteHandler', () => {
|
|
||||||
it('should create a new shipment', async () => {
|
|
||||||
req.body = { order: 'order123', trackingNumber: 'TRACK456' };
|
|
||||||
const mockShipment = { _id: '456', ...req.body, state: { type: 'draft' } };
|
|
||||||
newObject.mockResolvedValue(mockShipment);
|
|
||||||
|
|
||||||
await newShipmentRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockShipment);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('shipShipmentRouteHandler', () => {
|
|
||||||
it('should ship a planned shipment and update order items', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
checkStates.mockResolvedValue(true);
|
|
||||||
|
|
||||||
listObjects.mockResolvedValue([
|
|
||||||
{ _id: 'oi1', state: { type: 'ordered' } },
|
|
||||||
{ _id: 'oi2', state: { type: 'ordered' } },
|
|
||||||
]);
|
|
||||||
|
|
||||||
editObject.mockResolvedValue({ _id: '507f1f77bcf86cd799439011', state: { type: 'shipped' } });
|
|
||||||
|
|
||||||
await shipShipmentRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(checkStates).toHaveBeenCalledWith(expect.objectContaining({ states: ['planned'] }));
|
|
||||||
expect(editObject).toHaveBeenCalledTimes(3); // 2 order items + 1 shipment
|
|
||||||
expect(res.send).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../utils.js', () => ({
|
|
||||||
getAuditLogs: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/stockaudit.schema.js', () => ({
|
|
||||||
stockAuditModel: {
|
|
||||||
modelName: 'StockAudit',
|
|
||||||
aggregate: jest.fn(),
|
|
||||||
findOne: jest.fn(),
|
|
||||||
create: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listStockAuditsRouteHandler,
|
|
||||||
getStockAuditRouteHandler,
|
|
||||||
newStockAuditRouteHandler,
|
|
||||||
} = await import('../stockaudits.js');
|
|
||||||
|
|
||||||
const { getAuditLogs } = await import('../../../utils.js');
|
|
||||||
const { stockAuditModel } = await import('../../../database/schemas/inventory/stockaudit.schema.js');
|
|
||||||
|
|
||||||
describe('Stock Audit Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listStockAuditsRouteHandler', () => {
|
|
||||||
it('should list stock audits', async () => {
|
|
||||||
const mockResult = [{ _id: '1', type: 'full' }];
|
|
||||||
stockAuditModel.aggregate.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listStockAuditsRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(stockAuditModel.aggregate).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getStockAuditRouteHandler', () => {
|
|
||||||
it('should get a stock audit by ID with audit logs', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
const mockAudit = { _id: '507f1f77bcf86cd799439011', type: 'full', _doc: {} };
|
|
||||||
stockAuditModel.findOne.mockReturnValue({
|
|
||||||
populate: jest.fn().mockReturnValue({
|
|
||||||
populate: jest.fn().mockReturnValue({
|
|
||||||
populate: jest.fn().mockResolvedValue(mockAudit),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
getAuditLogs.mockResolvedValue([]);
|
|
||||||
|
|
||||||
await getStockAuditRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(getAuditLogs).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
editObjects: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/inventory/stockevent.schema.js', () => ({
|
|
||||||
stockEventModel: { modelName: 'StockEvent' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listStockEventsRouteHandler,
|
|
||||||
getStockEventRouteHandler,
|
|
||||||
newStockEventRouteHandler,
|
|
||||||
} = await import('../stockevents.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
|
|
||||||
const { stockEventModel } = await import('../../../database/schemas/inventory/stockevent.schema.js');
|
|
||||||
|
|
||||||
describe('Stock Event Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listStockEventsRouteHandler', () => {
|
|
||||||
it('should list stock events', async () => {
|
|
||||||
const mockResult = [{ _id: '1', type: 'adjustment' }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listStockEventsRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ model: stockEventModel })
|
|
||||||
);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newStockEventRouteHandler', () => {
|
|
||||||
it('should create a new stock event', async () => {
|
|
||||||
req.body = { type: 'adjustment', value: 10 };
|
|
||||||
const mockEvent = { _id: '456', ...req.body };
|
|
||||||
newObject.mockResolvedValue(mockEvent);
|
|
||||||
|
|
||||||
await newStockEventRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockEvent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -7,7 +7,6 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
editObjects,
|
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
@ -36,7 +35,7 @@ export const listFilamentStocksRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: [{ path: 'filamentSku' }],
|
populate: [{ path: 'filament' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -60,7 +59,7 @@ export const listFilamentStocksByPropertiesRouteHandler = async (
|
|||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['filamentSku'],
|
populate: ['filament'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,7 +78,7 @@ export const getFilamentStockRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: filamentStockModel,
|
model: filamentStockModel,
|
||||||
id,
|
id,
|
||||||
populate: [{ path: 'filamentSku' }],
|
populate: [{ path: 'filament' }],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Filament Stock not found with supplied id.`);
|
logger.warn(`Filament Stock not found with supplied id.`);
|
||||||
@ -115,38 +114,12 @@ 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(),
|
||||||
startingWeight: req.body.startingWeight,
|
startingWeight: req.body.startingWeight,
|
||||||
currentWeight: req.body.currentWeight,
|
currentWeight: req.body.currentWeight,
|
||||||
filamentSku: req.body.filamentSku,
|
filament: req.body.filament,
|
||||||
state: req.body.state,
|
state: req.body.state,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import config from '../../config.js';
|
import config from '../../config.js';
|
||||||
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
|
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
|
||||||
import { getModelByName } from '../misc/model.js';
|
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import {
|
import {
|
||||||
@ -8,7 +7,6 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
editObjects,
|
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
@ -46,44 +44,12 @@ export const listOrderItemsRouteHandler = async (
|
|||||||
strictPopulate: false,
|
strictPopulate: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'shipment',
|
path: 'item',
|
||||||
strictPopulate: false,
|
populate: { path: 'costTaxRate', strictPopulate: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'item',
|
path: 'item',
|
||||||
populate: [
|
populate: { path: 'priceTaxRate', strictPopulate: false },
|
||||||
{ path: 'costTaxRate', strictPopulate: false },
|
|
||||||
{ path: 'priceTaxRate', strictPopulate: false },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'sku',
|
|
||||||
strictPopulate: false,
|
|
||||||
populate: [
|
|
||||||
{
|
|
||||||
path: 'filament',
|
|
||||||
populate: { path: 'costTaxRate', strictPopulate: false },
|
|
||||||
strictPopulate: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'part',
|
|
||||||
populate: [
|
|
||||||
{ path: 'costTaxRate', strictPopulate: false },
|
|
||||||
{ path: 'priceTaxRate', strictPopulate: false },
|
|
||||||
],
|
|
||||||
strictPopulate: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'product',
|
|
||||||
populate: [
|
|
||||||
{ path: 'costTaxRate', strictPopulate: false },
|
|
||||||
{ path: 'priceTaxRate', strictPopulate: false },
|
|
||||||
],
|
|
||||||
strictPopulate: false,
|
|
||||||
},
|
|
||||||
{ path: 'costTaxRate', strictPopulate: false },
|
|
||||||
{ path: 'priceTaxRate', strictPopulate: false },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -138,40 +104,13 @@ export const getOrderItemRouteHandler = async (req, res) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'item',
|
path: 'item',
|
||||||
populate: [
|
populate: { path: 'costTaxRate', strictPopulate: false },
|
||||||
{ path: 'costTaxRate', strictPopulate: false },
|
|
||||||
{ path: 'priceTaxRate', strictPopulate: false },
|
|
||||||
],
|
|
||||||
strictPopulate: false,
|
strictPopulate: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'sku',
|
path: 'item',
|
||||||
|
populate: { path: 'priceTaxRate', strictPopulate: false },
|
||||||
strictPopulate: false,
|
strictPopulate: false,
|
||||||
populate: [
|
|
||||||
{
|
|
||||||
path: 'filament',
|
|
||||||
populate: { path: 'costTaxRate', strictPopulate: false },
|
|
||||||
strictPopulate: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'part',
|
|
||||||
populate: [
|
|
||||||
{ path: 'costTaxRate', strictPopulate: false },
|
|
||||||
{ path: 'priceTaxRate', strictPopulate: false },
|
|
||||||
],
|
|
||||||
strictPopulate: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'product',
|
|
||||||
populate: [
|
|
||||||
{ path: 'costTaxRate', strictPopulate: false },
|
|
||||||
{ path: 'priceTaxRate', strictPopulate: false },
|
|
||||||
],
|
|
||||||
strictPopulate: false,
|
|
||||||
},
|
|
||||||
{ path: 'costTaxRate', strictPopulate: false },
|
|
||||||
{ path: 'priceTaxRate', strictPopulate: false },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -189,46 +128,16 @@ export const editOrderItemRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
logger.trace(`Order Item with ID: ${id}`);
|
logger.trace(`Order Item with ID: ${id}`);
|
||||||
|
|
||||||
const skuType =
|
|
||||||
req.body.sku && req.body.itemType ? req.body.itemType + 'Sku' : null;
|
|
||||||
|
|
||||||
let name = req.body.name;
|
|
||||||
if (!name && req.body.sku && skuType) {
|
|
||||||
const skuEntry = getModelByName(skuType);
|
|
||||||
if (skuEntry?.model) {
|
|
||||||
const sku = await getObject({
|
|
||||||
model: skuEntry.model,
|
|
||||||
id: req.body.sku,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
name = sku?.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!name && req.body.item && req.body.itemType) {
|
|
||||||
const itemEntry = getModelByName(req.body.itemType);
|
|
||||||
if (itemEntry?.model) {
|
|
||||||
const item = await getObject({
|
|
||||||
model: itemEntry.model,
|
|
||||||
id: req.body.item,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
name = item?.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: req.body.name ?? name,
|
|
||||||
itemType: req.body.itemType,
|
itemType: req.body.itemType,
|
||||||
item: req.body.item,
|
item: req.body.item,
|
||||||
sku: req.body.sku,
|
|
||||||
orderType: req.body.orderType,
|
orderType: req.body.orderType,
|
||||||
order: req.body.order,
|
order: req.body.order,
|
||||||
syncAmount: req.body.syncAmount,
|
syncAmount: req.body.syncAmount,
|
||||||
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,
|
||||||
};
|
};
|
||||||
@ -242,7 +151,7 @@ export const editOrderItemRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
logger.error('Error editing order item:', result.error);
|
logger.error('Error editing order item:', result.error);
|
||||||
res.status(result.code || 500).send(result);
|
res.status(result).send(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,81 +160,13 @@ 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,
|
|
||||||
sku: update.sku,
|
|
||||||
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 skuType =
|
|
||||||
req.body.sku && req.body.itemType ? req.body.itemType + 'Sku' : null;
|
|
||||||
|
|
||||||
let name = req.body.name;
|
|
||||||
if (!name && req.body.sku && skuType) {
|
|
||||||
const skuEntry = getModelByName(skuType);
|
|
||||||
if (skuEntry?.model) {
|
|
||||||
const sku = await getObject({
|
|
||||||
model: skuEntry.model,
|
|
||||||
id: req.body.sku,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
name = sku?.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!name && req.body.item && req.body.itemType) {
|
|
||||||
const itemEntry = getModelByName(req.body.itemType);
|
|
||||||
if (itemEntry?.model) {
|
|
||||||
const item = await getObject({
|
|
||||||
model: itemEntry.model,
|
|
||||||
id: req.body.item,
|
|
||||||
cached: true,
|
|
||||||
});
|
|
||||||
name = item?.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
name: name || 'Order Item',
|
|
||||||
purchaseOrder: req.body.purchaseOrder,
|
purchaseOrder: req.body.purchaseOrder,
|
||||||
state: { type: 'draft' },
|
state: { type: 'draft' },
|
||||||
itemType: req.body.itemType,
|
itemType: req.body.itemType,
|
||||||
item: req.body.item,
|
item: req.body.item,
|
||||||
sku: req.body.sku,
|
|
||||||
orderType: req.body.orderType,
|
orderType: req.body.orderType,
|
||||||
order: req.body.order,
|
order: req.body.order,
|
||||||
syncAmount: req.body.syncAmount,
|
syncAmount: req.body.syncAmount,
|
||||||
@ -334,7 +175,6 @@ export const newOrderItemRouteHandler = async (req, res) => {
|
|||||||
totalAmount: req.body.totalAmount,
|
totalAmount: req.body.totalAmount,
|
||||||
taxRate: req.body.taxRate,
|
taxRate: req.body.taxRate,
|
||||||
totalAmountWithTax: req.body.totalAmountWithTax,
|
totalAmountWithTax: req.body.totalAmountWithTax,
|
||||||
shipment: req.body.shipment,
|
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: orderItemModel,
|
model: orderItemModel,
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
editObjects,
|
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
@ -36,7 +35,7 @@ export const listPartStocksRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: [{ path: 'partSku' }],
|
populate: [{ path: 'part' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -60,7 +59,7 @@ export const listPartStocksByPropertiesRouteHandler = async (
|
|||||||
model: partStockModel,
|
model: partStockModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['partSku'],
|
populate: ['part'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -79,7 +78,7 @@ export const getPartStockRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: partStockModel,
|
model: partStockModel,
|
||||||
id,
|
id,
|
||||||
populate: [{ path: 'partSku' }],
|
populate: [{ path: 'part' }],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Part Stock not found with supplied id.`);
|
logger.warn(`Part Stock not found with supplied id.`);
|
||||||
@ -115,38 +114,12 @@ 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(),
|
||||||
startingQuantity: req.body.startingQuantity,
|
startingQuantity: req.body.startingQuantity,
|
||||||
currentQuantity: req.body.currentQuantity,
|
currentQuantity: req.body.currentQuantity,
|
||||||
partSku: req.body.partSku,
|
part: req.body.part,
|
||||||
state: req.body.state,
|
state: req.body.state,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
|
|||||||
@ -1,292 +0,0 @@
|
|||||||
import config from '../../config.js';
|
|
||||||
import { productStockModel } from '../../database/schemas/inventory/productstock.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 { productSkuModel } from '../../database/schemas/management/productsku.schema.js';
|
|
||||||
const logger = log4js.getLogger('Product Stocks');
|
|
||||||
logger.level = config.server.logLevel;
|
|
||||||
|
|
||||||
export const listProductStocksRouteHandler = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
page = 1,
|
|
||||||
limit = 25,
|
|
||||||
property = '',
|
|
||||||
filter = {},
|
|
||||||
search = '',
|
|
||||||
sort = '',
|
|
||||||
order = 'ascend'
|
|
||||||
) => {
|
|
||||||
const result = await listObjects({
|
|
||||||
model: productStockModel,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
property,
|
|
||||||
filter,
|
|
||||||
search,
|
|
||||||
sort,
|
|
||||||
order,
|
|
||||||
populate: [{ path: 'productSku' }, { path: 'partStocks.partStock' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error listing product stocks.');
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`List of product stocks (Page ${page}, Limit ${limit}). Count: ${result.length}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listProductStocksByPropertiesRouteHandler = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
properties = '',
|
|
||||||
filter = {},
|
|
||||||
masterFilter = {}
|
|
||||||
) => {
|
|
||||||
const result = await listObjectsByProperties({
|
|
||||||
model: productStockModel,
|
|
||||||
properties,
|
|
||||||
filter,
|
|
||||||
populate: ['productSku', 'partStocks.partStock'],
|
|
||||||
masterFilter,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error listing product stocks.');
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`List of product stocks. Count: ${result.length}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getProductStockRouteHandler = async (req, res) => {
|
|
||||||
const id = req.params.id;
|
|
||||||
const result = await getObject({
|
|
||||||
model: productStockModel,
|
|
||||||
id,
|
|
||||||
populate: [{ path: 'partStocks.partSku' }, { path: 'partStocks.partStock' }, { path: 'productSku' }],
|
|
||||||
});
|
|
||||||
if (result?.error) {
|
|
||||||
logger.warn(`Product Stock not found with supplied id.`);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.debug(`Retrieved product stock with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const editProductStockRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Product Stock with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: productStockModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking product stock states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Product stock is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Product stock is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
partStocks: req.body?.partStocks?.map((partStock) => ({
|
|
||||||
quantity: partStock.quantity,
|
|
||||||
partStock: partStock.partStock,
|
|
||||||
partSku: partStock.partSku,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await editObject({
|
|
||||||
model: productStockModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing product stock:', result.error);
|
|
||||||
res.status(result).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited product stock with ID: ${id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const editMultipleProductStocksRouteHandler = 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: productStockModel,
|
|
||||||
updates,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing product stocks:', result.error);
|
|
||||||
res.status(result.code || 500).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited ${updates.length} product stocks`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newProductStockRouteHandler = async (req, res) => {
|
|
||||||
const productSkuId = new mongoose.Types.ObjectId(req.body.productSku?._id);
|
|
||||||
const productSku = await getObject({
|
|
||||||
model: productSkuModel,
|
|
||||||
id: productSkuId,
|
|
||||||
});
|
|
||||||
const newData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
currentQuantity: req.body.currentQuantity,
|
|
||||||
productSku: req.body.productSku,
|
|
||||||
state: req.body.state ?? { type: 'draft' },
|
|
||||||
partStocks: (productSku.parts || []).map((part) => ({
|
|
||||||
partSku: part.partSku,
|
|
||||||
quantity: part.quantity,
|
|
||||||
partStock: undefined,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
const result = await newObject({
|
|
||||||
model: productStockModel,
|
|
||||||
newData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('No product stock created:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`New product stock with ID: ${result._id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteProductStockRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Product Stock with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: productStockModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking product stock states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Product stock is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Product stock is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await deleteObject({
|
|
||||||
model: productStockModel,
|
|
||||||
id,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('No product stock deleted:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Deleted product stock with ID: ${result._id}`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getProductStockStatsRouteHandler = async (req, res) => {
|
|
||||||
const result = await getModelStats({ model: productStockModel });
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error fetching product stock stats:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.trace('Product stock stats:', result);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getProductStockHistoryRouteHandler = async (req, res) => {
|
|
||||||
const from = req.query.from;
|
|
||||||
const to = req.query.to;
|
|
||||||
const result = await getModelHistory({ model: productStockModel, from, to });
|
|
||||||
if (result?.error) {
|
|
||||||
logger.error('Error fetching product stock history:', result.error);
|
|
||||||
return res.status(result.code).send(result);
|
|
||||||
}
|
|
||||||
logger.trace('Product stock history:', result);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const postProductStockRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Product Stock with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: productStockModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking product stock states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Product stock is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Product stock is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'posted' },
|
|
||||||
postedAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: productStockModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error posting product stock:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Posted product stock with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
@ -7,15 +7,11 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
editObjects,
|
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
checkStates,
|
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
|
|
||||||
import { shipmentModel } from '../../database/schemas/inventory/shipment.schema.js';
|
|
||||||
|
|
||||||
const logger = log4js.getLogger('Purchase Orders');
|
const logger = log4js.getLogger('Purchase Orders');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
@ -99,20 +95,6 @@ export const editPurchaseOrderRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
logger.trace(`Purchase Order with ID: ${id}`);
|
logger.trace(`Purchase Order with ID: ${id}`);
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking purchase order states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Purchase order is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Purchase order is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
vendor: req.body.vendor,
|
vendor: req.body.vendor,
|
||||||
@ -136,40 +118,10 @@ export const editPurchaseOrderRouteHandler = async (req, res) => {
|
|||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editMultiplePurchaseOrdersRouteHandler = async (req, res) => {
|
|
||||||
const updates = req.body.map((update) => ({
|
|
||||||
_id: update._id,
|
|
||||||
vendor: update.vendor,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!Array.isArray(updates)) {
|
|
||||||
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await editObjects({
|
|
||||||
model: purchaseOrderModel,
|
|
||||||
updates,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing purchase orders:', result.error);
|
|
||||||
res.status(result.code || 500).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited ${updates.length} purchase orders`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newPurchaseOrderRouteHandler = async (req, res) => {
|
export const newPurchaseOrderRouteHandler = async (req, res) => {
|
||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
vendor: req.body.vendor,
|
vendor: req.body.vendor,
|
||||||
totalAmount: 0,
|
|
||||||
totalAmountWithTax: 0,
|
|
||||||
totalTaxAmount: 0,
|
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: purchaseOrderModel,
|
model: purchaseOrderModel,
|
||||||
@ -228,232 +180,3 @@ export const getPurchaseOrderHistoryRouteHandler = async (req, res) => {
|
|||||||
logger.trace('Purchase order history:', result);
|
logger.trace('Purchase order history:', result);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const postPurchaseOrderRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Purchase Order with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['draft'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking purchase order states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Purchase order is not in draft state.');
|
|
||||||
res.status(400).send({ error: 'Purchase order is not in draft state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderItemsResult = await listObjects({
|
|
||||||
model: orderItemModel,
|
|
||||||
filter: { order: id, orderType: 'purchaseOrder' },
|
|
||||||
pagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const shipmentsResult = await listObjects({
|
|
||||||
model: shipmentModel,
|
|
||||||
filter: { order: id, orderType: 'purchaseOrder' },
|
|
||||||
pagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const orderItem of orderItemsResult) {
|
|
||||||
if (orderItem.state.type != 'draft') {
|
|
||||||
logger.warn(`Order item ${orderItem._id} is not in draft state.`);
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.send({ error: `Order item ${orderItem._reference} not in draft state.`, code: 400 });
|
|
||||||
}
|
|
||||||
if (!orderItem?.shipment || orderItem?.shipment == null) {
|
|
||||||
logger.warn(`Order item ${orderItem._id} does not have a shipment.`);
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.send({ error: `Order item ${orderItem._reference} does not have a shipment.`, code: 400 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const shipment of shipmentsResult) {
|
|
||||||
if (shipment.state.type != 'draft') {
|
|
||||||
logger.warn(`Shipment ${shipment._id} is not in draft state.`);
|
|
||||||
return res
|
|
||||||
.status(400)
|
|
||||||
.send({ error: `Shipment ${shipment._reference} not in draft state.`, code: 400 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const orderItem of orderItemsResult) {
|
|
||||||
await editObject({
|
|
||||||
model: orderItemModel,
|
|
||||||
id: orderItem._id,
|
|
||||||
updateData: {
|
|
||||||
state: { type: 'ordered' },
|
|
||||||
orderedAt: new Date(),
|
|
||||||
},
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const shipment of shipmentsResult) {
|
|
||||||
await editObject({
|
|
||||||
model: shipmentModel,
|
|
||||||
id: shipment._id,
|
|
||||||
updateData: {
|
|
||||||
state: { type: 'planned' },
|
|
||||||
},
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'sent' },
|
|
||||||
postedAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: purchaseOrderModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error posting purchase order:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Posted purchase order with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const acknowledgePurchaseOrderRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Purchase Order with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: purchaseOrderModel, id, states: ['sent'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking purchase order states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Purchase order is not in sent state.');
|
|
||||||
res.status(400).send({ error: 'Purchase order is not in sent state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'acknowledged' },
|
|
||||||
acknowledgedAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: purchaseOrderModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error acknowledging purchase order:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Acknowledged purchase order with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelPurchaseOrderRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Purchase Order with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({
|
|
||||||
model: purchaseOrderModel,
|
|
||||||
id,
|
|
||||||
states: ['sent', 'acknowledged', 'partiallyShipped', 'shipped', 'partiallyReceived'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking purchase order states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Purchase order is not in a cancellable state.');
|
|
||||||
res.status(400).send({
|
|
||||||
error: 'Purchase order is not in a cancellable state (must be draft, sent, or acknowledged).',
|
|
||||||
code: 400,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderItemsResult = await listObjects({
|
|
||||||
model: orderItemModel,
|
|
||||||
filter: { order: id, orderType: 'purchaseOrder' },
|
|
||||||
pagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const shipmentsResult = await listObjects({
|
|
||||||
model: shipmentModel,
|
|
||||||
filter: { order: id, orderType: 'purchaseOrder' },
|
|
||||||
pagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allowedOrderItemStates = ['ordered', 'shipped'];
|
|
||||||
const allowedShipmentStates = ['shipped', 'planned'];
|
|
||||||
|
|
||||||
for (const orderItem of orderItemsResult) {
|
|
||||||
if (allowedOrderItemStates.includes(orderItem.state.type)) {
|
|
||||||
await editObject({
|
|
||||||
model: orderItemModel,
|
|
||||||
id: orderItem._id,
|
|
||||||
updateData: {
|
|
||||||
state: { type: 'cancelled' },
|
|
||||||
},
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const shipment of shipmentsResult) {
|
|
||||||
if (allowedShipmentStates.includes(shipment.state.type)) {
|
|
||||||
await editObject({
|
|
||||||
model: shipmentModel,
|
|
||||||
id: shipment._id,
|
|
||||||
updateData: {
|
|
||||||
state: { type: 'cancelled' },
|
|
||||||
},
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'cancelled' },
|
|
||||||
cancelledAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: purchaseOrderModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error cancelling purchase order:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Cancelled purchase order with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -7,16 +7,13 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
editObjects,
|
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
getModelHistory,
|
getModelHistory,
|
||||||
checkStates,
|
|
||||||
} from '../../database/database.js';
|
} from '../../database/database.js';
|
||||||
const logger = log4js.getLogger('Shipments');
|
const logger = log4js.getLogger('Shipments');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
import { orderItemModel } from '../../database/schemas/inventory/orderitem.schema.js';
|
|
||||||
|
|
||||||
export const listShipmentsRouteHandler = async (
|
export const listShipmentsRouteHandler = async (
|
||||||
req,
|
req,
|
||||||
@ -38,7 +35,7 @@ export const listShipmentsRouteHandler = async (
|
|||||||
search,
|
search,
|
||||||
sort,
|
sort,
|
||||||
order,
|
order,
|
||||||
populate: ['order', 'courierService', 'taxRate'],
|
populate: ['purchaseOrder', 'vendor', 'courierService'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@ -62,7 +59,7 @@ export const listShipmentsByPropertiesRouteHandler = async (
|
|||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
properties,
|
properties,
|
||||||
filter,
|
filter,
|
||||||
populate: ['courierService'],
|
populate: ['purchaseOrder', 'vendor', 'courierService'],
|
||||||
masterFilter,
|
masterFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,7 +78,7 @@ export const getShipmentRouteHandler = async (req, res) => {
|
|||||||
const result = await getObject({
|
const result = await getObject({
|
||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
id,
|
id,
|
||||||
populate: ['order', 'courierService', 'taxRate'],
|
populate: ['purchaseOrder', 'vendor', 'courierService', 'items.item', 'items.taxRate'],
|
||||||
});
|
});
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
logger.warn(`Shipment not found with supplied id.`);
|
logger.warn(`Shipment not found with supplied id.`);
|
||||||
@ -99,13 +96,15 @@ export const editShipmentRouteHandler = async (req, res) => {
|
|||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
orderType: req.body.orderType,
|
purchaseOrder: req.body.purchaseOrder,
|
||||||
order: req.body.order,
|
vendor: req.body.vendor,
|
||||||
courierService: req.body.courierService,
|
courierService: req.body.courierService,
|
||||||
trackingNumber: req.body.trackingNumber,
|
trackingNumber: req.body.trackingNumber,
|
||||||
amount: req.body.amount,
|
shippedDate: req.body.shippedDate,
|
||||||
amountWithTax: req.body.amountWithTax,
|
expectedDeliveryDate: req.body.expectedDeliveryDate,
|
||||||
taxRate: req.body.taxRate,
|
actualDeliveryDate: req.body.actualDeliveryDate,
|
||||||
|
state: req.body.state,
|
||||||
|
notes: req.body.notes,
|
||||||
};
|
};
|
||||||
// Create audit log before updating
|
// Create audit log before updating
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
@ -126,53 +125,20 @@ export const editShipmentRouteHandler = async (req, res) => {
|
|||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editMultipleShipmentsRouteHandler = async (req, res) => {
|
|
||||||
const updates = req.body.map((update) => ({
|
|
||||||
_id: update._id,
|
|
||||||
orderType: update.orderType,
|
|
||||||
order: update.order,
|
|
||||||
courierService: update.courierService,
|
|
||||||
trackingNumber: update.trackingNumber,
|
|
||||||
amount: update.amount,
|
|
||||||
amountWithTax: update.amountWithTax,
|
|
||||||
taxRate: update.taxRate,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!Array.isArray(updates)) {
|
|
||||||
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await editObjects({
|
|
||||||
model: shipmentModel,
|
|
||||||
updates,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing shipments:', result.error);
|
|
||||||
res.status(result.code || 500).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited ${updates.length} shipments`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const newShipmentRouteHandler = async (req, res) => {
|
export const newShipmentRouteHandler = async (req, res) => {
|
||||||
const newData = {
|
const newData = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
orderType: req.body.orderType,
|
purchaseOrder: req.body.purchaseOrder,
|
||||||
order: req.body.order,
|
vendor: req.body.vendor,
|
||||||
courierService: req.body.courierService,
|
courierService: req.body.courierService,
|
||||||
trackingNumber: req.body.trackingNumber,
|
trackingNumber: req.body.trackingNumber,
|
||||||
amount: req.body.amount,
|
items: req.body.items,
|
||||||
amountWithTax: req.body.amountWithTax,
|
cost: req.body.cost,
|
||||||
taxRate: req.body.taxRate,
|
shippedDate: req.body.shippedDate,
|
||||||
shippedAt: req.body.shippedAt,
|
expectedDeliveryDate: req.body.expectedDeliveryDate,
|
||||||
expectedAt: req.body.expectedAt,
|
actualDeliveryDate: req.body.actualDeliveryDate,
|
||||||
deliveredAt: req.body.deliveredAt,
|
state: req.body.state,
|
||||||
state: { type: 'draft' },
|
notes: req.body.notes,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: shipmentModel,
|
model: shipmentModel,
|
||||||
@ -231,212 +197,3 @@ export const getShipmentHistoryRouteHandler = async (req, res) => {
|
|||||||
logger.trace('Shipment history:', result);
|
logger.trace('Shipment history:', result);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shipShipmentRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Shipment with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: shipmentModel, id, states: ['planned'] });
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking shipment states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Shipment is not in planned state.');
|
|
||||||
res.status(400).send({ error: 'Shipment is not in planned state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderItemsResult = await listObjects({
|
|
||||||
model: orderItemModel,
|
|
||||||
filter: { shipment: id },
|
|
||||||
pagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (orderItemsResult.error) {
|
|
||||||
logger.error('Error listing order items:', orderItemsResult.error);
|
|
||||||
res.status(orderItemsResult.code).send(orderItemsResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const orderItem of orderItemsResult) {
|
|
||||||
if (orderItem.state.type != 'ordered') {
|
|
||||||
logger.error('Order item is not in ordered state.');
|
|
||||||
res.status(400).send({ error: 'Order item is not in ordered state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const orderItem of orderItemsResult) {
|
|
||||||
await editObject({
|
|
||||||
model: orderItemModel,
|
|
||||||
id: orderItem._id,
|
|
||||||
user: req.user,
|
|
||||||
updateData: {
|
|
||||||
state: { type: 'shipped' },
|
|
||||||
receivedAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
state: { type: 'shipped' },
|
|
||||||
shippedAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({ model: shipmentModel, id, updateData, user: req.user });
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error shipping shipment:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.debug(`Shipped shipment with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const receiveShipmentRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Shipment with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({ model: shipmentModel, id, states: ['shipped'] });
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking shipment states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Shipment is not in shipped state.');
|
|
||||||
res.status(400).send({ error: 'Shipment is not in shipped state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderItemsResult = await listObjects({
|
|
||||||
model: orderItemModel,
|
|
||||||
filter: { shipment: id },
|
|
||||||
pagination: false,
|
|
||||||
});
|
|
||||||
if (orderItemsResult.error) {
|
|
||||||
logger.error('Error listing order items:', orderItemsResult.error);
|
|
||||||
res.status(orderItemsResult.code).send(orderItemsResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const orderItem of orderItemsResult) {
|
|
||||||
if (orderItem.state.type != 'shipped') {
|
|
||||||
logger.error('Order item is not in shipped state.');
|
|
||||||
res.status(400).send({ error: 'Order item is not in shipped state.', code: 400 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const orderItem of orderItemsResult) {
|
|
||||||
await editObject({
|
|
||||||
model: orderItemModel,
|
|
||||||
id: orderItem._id,
|
|
||||||
updateData: {
|
|
||||||
state: { type: 'received' },
|
|
||||||
receivedAt: new Date(),
|
|
||||||
},
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await editObject({
|
|
||||||
model: shipmentModel,
|
|
||||||
id,
|
|
||||||
updateData: {
|
|
||||||
state: { type: 'delivered' },
|
|
||||||
deliveredAt: new Date(),
|
|
||||||
},
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error receiving shipment:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Received shipment with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cancelShipmentRouteHandler = async (req, res) => {
|
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
|
||||||
|
|
||||||
logger.trace(`Shipment with ID: ${id}`);
|
|
||||||
|
|
||||||
const checkStatesResult = await checkStates({
|
|
||||||
model: shipmentModel,
|
|
||||||
id,
|
|
||||||
states: ['planned', 'shipped'],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (checkStatesResult.error) {
|
|
||||||
logger.error('Error checking shipment states:', checkStatesResult.error);
|
|
||||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkStatesResult === false) {
|
|
||||||
logger.error('Shipment is not in a cancellable state.');
|
|
||||||
res.status(400).send({
|
|
||||||
error: 'Shipment is not in a cancellable state (must be planned or shipped).',
|
|
||||||
code: 400,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderItemsResult = await listObjects({
|
|
||||||
model: orderItemModel,
|
|
||||||
filter: { shipment: id },
|
|
||||||
pagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (orderItemsResult.error) {
|
|
||||||
logger.error('Error listing order items:', orderItemsResult.error);
|
|
||||||
res.status(orderItemsResult.code).send(orderItemsResult);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel related order items if they are in cancellable states
|
|
||||||
for (const orderItem of orderItemsResult) {
|
|
||||||
if (orderItem.state.type === 'draft' || orderItem.state.type === 'ordered') {
|
|
||||||
await editObject({
|
|
||||||
model: orderItemModel,
|
|
||||||
id: orderItem._id,
|
|
||||||
updateData: {
|
|
||||||
state: { type: 'cancelled' },
|
|
||||||
},
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
updatedAt: new Date(),
|
|
||||||
state: { type: 'cancelled' },
|
|
||||||
cancelledAt: new Date(),
|
|
||||||
};
|
|
||||||
const result = await editObject({
|
|
||||||
model: shipmentModel,
|
|
||||||
id,
|
|
||||||
updateData,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error cancelling shipment:', result.error);
|
|
||||||
res.status(result.code).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Cancelled shipment with ID: ${id}`);
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import {
|
|||||||
listObjects,
|
listObjects,
|
||||||
getObject,
|
getObject,
|
||||||
editObject,
|
editObject,
|
||||||
editObjects,
|
|
||||||
newObject,
|
newObject,
|
||||||
listObjectsByProperties,
|
listObjectsByProperties,
|
||||||
getModelStats,
|
getModelStats,
|
||||||
@ -146,32 +145,6 @@ export const editStockEventRouteHandler = async (req, res) => {
|
|||||||
res.send(result);
|
res.send(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editMultipleStockEventsRouteHandler = async (req, res) => {
|
|
||||||
const updates = req.body.map((update) => ({
|
|
||||||
_id: update._id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!Array.isArray(updates)) {
|
|
||||||
return res.status(400).send({ error: 'Body must be an array of updates.', code: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await editObjects({
|
|
||||||
model: stockEventModel,
|
|
||||||
updates,
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
logger.error('Error editing stock events:', result.error);
|
|
||||||
res.status(result.code || 500).send(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`Edited ${updates.length} stock events`);
|
|
||||||
|
|
||||||
res.send(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteStockEventRouteHandler = async (req, res) => {
|
export const deleteStockEventRouteHandler = async (req, res) => {
|
||||||
// Get ID from params
|
// Get ID from params
|
||||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/management/auditlog.schema.js', () => ({
|
|
||||||
auditLogModel: {
|
|
||||||
modelName: 'AuditLog',
|
|
||||||
find: jest.fn(),
|
|
||||||
findOne: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { listAuditLogsRouteHandler, getAuditLogRouteHandler } = await import('../auditlogs.js');
|
|
||||||
|
|
||||||
const { auditLogModel } = await import('../../../database/schemas/management/auditlog.schema.js');
|
|
||||||
|
|
||||||
describe('Audit Log Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listAuditLogsRouteHandler', () => {
|
|
||||||
it('should list audit logs', async () => {
|
|
||||||
const mockResult = [
|
|
||||||
{ _id: '1', operation: 'edit', parent: 'parent123', _doc: { parent: 'parent123' } },
|
|
||||||
];
|
|
||||||
auditLogModel.find.mockReturnValue({
|
|
||||||
sort: jest.fn().mockReturnThis(),
|
|
||||||
skip: jest.fn().mockReturnThis(),
|
|
||||||
limit: jest.fn().mockReturnThis(),
|
|
||||||
populate: jest.fn().mockResolvedValue(mockResult),
|
|
||||||
});
|
|
||||||
|
|
||||||
await listAuditLogsRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(auditLogModel.find).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAuditLogRouteHandler', () => {
|
|
||||||
it('should get an audit log by ID', async () => {
|
|
||||||
req.params.id = '507f1f77bcf86cd799439011';
|
|
||||||
const mockLog = { _id: '507f1f77bcf86cd799439011', operation: 'edit' };
|
|
||||||
auditLogModel.findOne.mockReturnValue({
|
|
||||||
populate: jest.fn().mockReturnValue({
|
|
||||||
populate: jest.fn().mockReturnValue({
|
|
||||||
populate: jest.fn().mockResolvedValue(mockLog),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
await getAuditLogRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(auditLogModel.findOne).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockLog);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/management/courier.schema.js', () => ({
|
|
||||||
courierModel: { modelName: 'Courier' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listCouriersRouteHandler,
|
|
||||||
getCourierRouteHandler,
|
|
||||||
newCourierRouteHandler,
|
|
||||||
editCourierRouteHandler,
|
|
||||||
} = await import('../courier.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, editObject, newObject } = await import(
|
|
||||||
'../../../database/database.js'
|
|
||||||
);
|
|
||||||
const { courierModel } = await import('../../../database/schemas/management/courier.schema.js');
|
|
||||||
|
|
||||||
describe('Courier Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listCouriersRouteHandler', () => {
|
|
||||||
it('should list couriers', async () => {
|
|
||||||
const mockResult = [{ _id: '1', name: 'FedEx' }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listCouriersRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalledWith(expect.objectContaining({ model: courierModel }));
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newCourierRouteHandler', () => {
|
|
||||||
it('should create a new courier', async () => {
|
|
||||||
req.body = { name: 'DHL', email: 'contact@dhl.com' };
|
|
||||||
const mockCourier = { _id: '456', ...req.body };
|
|
||||||
newObject.mockResolvedValue(mockCourier);
|
|
||||||
|
|
||||||
await newCourierRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockCourier);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/management/courierservice.schema.js', () => ({
|
|
||||||
courierServiceModel: { modelName: 'CourierService' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listCourierServicesRouteHandler,
|
|
||||||
getCourierServiceRouteHandler,
|
|
||||||
newCourierServiceRouteHandler,
|
|
||||||
editCourierServiceRouteHandler,
|
|
||||||
} = await import('../courierservice.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, editObject, newObject } = await import(
|
|
||||||
'../../../database/database.js'
|
|
||||||
);
|
|
||||||
const { courierServiceModel } = await import(
|
|
||||||
'../../../database/schemas/management/courierservice.schema.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Courier Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listCourierServicesRouteHandler', () => {
|
|
||||||
it('should list courier services', async () => {
|
|
||||||
const mockResult = [{ _id: '1', name: 'Express' }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listCourierServicesRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ model: courierServiceModel })
|
|
||||||
);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newCourierServiceRouteHandler', () => {
|
|
||||||
it('should create a new courier service', async () => {
|
|
||||||
req.body = { courier: 'courier123', name: 'Express Delivery' };
|
|
||||||
const mockService = { _id: '456', ...req.body };
|
|
||||||
newObject.mockResolvedValue(mockService);
|
|
||||||
|
|
||||||
await newCourierServiceRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockService);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/management/documentjob.schema.js', () => ({
|
|
||||||
documentJobModel: { modelName: 'DocumentJob' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listDocumentJobsRouteHandler,
|
|
||||||
getDocumentJobRouteHandler,
|
|
||||||
newDocumentJobRouteHandler,
|
|
||||||
} = await import('../documentjobs.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
|
|
||||||
const { documentJobModel } = await import(
|
|
||||||
'../../../database/schemas/management/documentjob.schema.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Document Job Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listDocumentJobsRouteHandler', () => {
|
|
||||||
it('should list document jobs', async () => {
|
|
||||||
const mockResult = [{ _id: '1', state: { type: 'pending' } }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listDocumentJobsRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ model: documentJobModel })
|
|
||||||
);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newDocumentJobRouteHandler', () => {
|
|
||||||
it('should create a new document job', async () => {
|
|
||||||
req.body = { documentTemplate: 'template123', documentPrinter: 'printer123' };
|
|
||||||
const mockJob = { _id: '456', ...req.body };
|
|
||||||
newObject.mockResolvedValue(mockJob);
|
|
||||||
|
|
||||||
await newDocumentJobRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockJob);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import { jest } from '@jest/globals';
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/database.js', () => ({
|
|
||||||
listObjects: jest.fn(),
|
|
||||||
getObject: jest.fn(),
|
|
||||||
editObject: jest.fn(),
|
|
||||||
newObject: jest.fn(),
|
|
||||||
deleteObject: jest.fn(),
|
|
||||||
listObjectsByProperties: jest.fn(),
|
|
||||||
getModelStats: jest.fn(),
|
|
||||||
getModelHistory: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../../../database/schemas/management/documentprinter.schema.js', () => ({
|
|
||||||
documentPrinterModel: { modelName: 'DocumentPrinter' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.unstable_mockModule('log4js', () => ({
|
|
||||||
default: {
|
|
||||||
getLogger: () => ({
|
|
||||||
level: 'info',
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
trace: jest.fn(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
|
||||||
listDocumentPrintersRouteHandler,
|
|
||||||
getDocumentPrinterRouteHandler,
|
|
||||||
newDocumentPrinterRouteHandler,
|
|
||||||
} = await import('../documentprinters.js');
|
|
||||||
|
|
||||||
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
|
|
||||||
const { documentPrinterModel } = await import(
|
|
||||||
'../../../database/schemas/management/documentprinter.schema.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Document Printer Service Route Handlers', () => {
|
|
||||||
let req, res;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
req = {
|
|
||||||
params: {},
|
|
||||||
query: {},
|
|
||||||
body: {},
|
|
||||||
user: { id: 'test-user-id' },
|
|
||||||
};
|
|
||||||
res = {
|
|
||||||
send: jest.fn(),
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('listDocumentPrintersRouteHandler', () => {
|
|
||||||
it('should list document printers', async () => {
|
|
||||||
const mockResult = [{ _id: '1', name: 'HP LaserJet' }];
|
|
||||||
listObjects.mockResolvedValue(mockResult);
|
|
||||||
|
|
||||||
await listDocumentPrintersRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(listObjects).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({ model: documentPrinterModel })
|
|
||||||
);
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockResult);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('newDocumentPrinterRouteHandler', () => {
|
|
||||||
it('should create a new document printer', async () => {
|
|
||||||
req.body = { name: 'Canon Printer', host: 'host123' };
|
|
||||||
const mockPrinter = { _id: '456', ...req.body };
|
|
||||||
newObject.mockResolvedValue(mockPrinter);
|
|
||||||
|
|
||||||
await newDocumentPrinterRouteHandler(req, res);
|
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
|
||||||
expect(res.send).toHaveBeenCalledWith(mockPrinter);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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