mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-23 08:10:32 +00:00
feat: wip migrate server to nestjs
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
|
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
|
||||||
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
|
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
|
||||||
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
|
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
|
||||||
|
"server2:start": "lerna run start:dev --scope \"@bigcapital/server2\"",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
|
|||||||
102
packages/server-nest/.env.example
Normal file
102
packages/server-nest/.env.example
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Mail
|
||||||
|
MAIL_HOST=
|
||||||
|
MAIL_USERNAME=
|
||||||
|
MAIL_PASSWORD=
|
||||||
|
MAIL_PORT=
|
||||||
|
MAIL_SECURE=
|
||||||
|
MAIL_FROM_NAME=
|
||||||
|
MAIL_FROM_ADDRESS=
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_USER=bigcapital
|
||||||
|
DB_PASSWORD=bigcapital
|
||||||
|
DB_ROOT_PASSWORD=root
|
||||||
|
DB_CHARSET=utf8
|
||||||
|
|
||||||
|
# System database
|
||||||
|
SYSTEM_DB_NAME=bigcapital_system
|
||||||
|
# SYSTEM_DB_USER=
|
||||||
|
# SYSTEM_DB_PASSWORD=
|
||||||
|
# SYSTEM_DB_NAME=
|
||||||
|
# SYSTEM_DB_CHARSET=
|
||||||
|
|
||||||
|
# Tenant databases
|
||||||
|
TENANT_DB_NAME_PERFIX=bigcapital_tenant_
|
||||||
|
# TENANT_DB_HOST=
|
||||||
|
# TENANT_DB_USER=
|
||||||
|
# TENANT_DB_PASSWORD=
|
||||||
|
# TENANT_DB_CHARSET=
|
||||||
|
|
||||||
|
# Application
|
||||||
|
BASE_URL=http://example.com
|
||||||
|
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
|
||||||
|
|
||||||
|
# Jobs MongoDB
|
||||||
|
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
|
||||||
|
|
||||||
|
# App proxy
|
||||||
|
PUBLIC_PROXY_PORT=80
|
||||||
|
PUBLIC_PROXY_SSL_PORT=443
|
||||||
|
|
||||||
|
# Agendash
|
||||||
|
AGENDASH_AUTH_USER=agendash
|
||||||
|
AGENDASH_AUTH_PASSWORD=123123
|
||||||
|
|
||||||
|
# Sign-up restrictions
|
||||||
|
SIGNUP_DISABLED=false
|
||||||
|
SIGNUP_ALLOWED_DOMAINS=
|
||||||
|
SIGNUP_ALLOWED_EMAILS=
|
||||||
|
|
||||||
|
# Sign-up Email Confirmation
|
||||||
|
SIGNUP_EMAIL_CONFIRMATION=false
|
||||||
|
|
||||||
|
# API rate limit (points,duration,block duration).
|
||||||
|
API_RATE_LIMIT=120,60,600
|
||||||
|
|
||||||
|
# Gotenberg API for PDF printing - (production).
|
||||||
|
GOTENBERG_URL=http://gotenberg:3000
|
||||||
|
GOTENBERG_DOCS_URL=http://server:3000/public/
|
||||||
|
|
||||||
|
# Gotenberg API - (development)
|
||||||
|
# GOTENBERG_URL=http://localhost:9000
|
||||||
|
# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/
|
||||||
|
|
||||||
|
# Exchange Rate Service
|
||||||
|
EXCHANGE_RATE_SERVICE=open-exchange-rate
|
||||||
|
|
||||||
|
# Open Exchange Rate
|
||||||
|
OPEN_EXCHANGE_RATE_APP_ID=
|
||||||
|
|
||||||
|
# The Plaid environment to use ('sandbox' or 'development').
|
||||||
|
# https://plaid.com/docs/#api-host
|
||||||
|
PLAID_ENV=sandbox
|
||||||
|
|
||||||
|
# Your Plaid keys, which can be found in the Plaid Dashboard.
|
||||||
|
# https://dashboard.plaid.com/account/keys
|
||||||
|
PLAID_CLIENT_ID=
|
||||||
|
PLAID_SECRET=
|
||||||
|
PLAID_LINK_WEBHOOK=
|
||||||
|
|
||||||
|
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
|
||||||
|
LEMONSQUEEZY_API_KEY=
|
||||||
|
LEMONSQUEEZY_STORE_ID=
|
||||||
|
LEMONSQUEEZY_WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
# S3 documents and attachments
|
||||||
|
S3_REGION=US
|
||||||
|
S3_ACCESS_KEY_ID=
|
||||||
|
S3_SECRET_ACCESS_KEY=
|
||||||
|
S3_ENDPOINT=
|
||||||
|
S3_BUCKET=
|
||||||
|
|
||||||
|
# PostHog
|
||||||
|
POSTHOG_API_KEY=
|
||||||
|
POSTHOG_HOST=
|
||||||
|
|
||||||
|
# Stripe Payment
|
||||||
|
STRIPE_PAYMENT_SECRET_KEY=
|
||||||
|
STRIPE_PAYMENT_PUBLISHABLE_KEY=
|
||||||
|
STRIPE_PAYMENT_CLIENT_ID=
|
||||||
|
STRIPE_PAYMENT_WEBHOOKS_SECRET=
|
||||||
|
STRIPE_PAYMENT_REDIRECT_URL=
|
||||||
25
packages/server-nest/.eslintrc.js
Normal file
25
packages/server-nest/.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
56
packages/server-nest/.gitignore
vendored
Normal file
56
packages/server-nest/.gitignore
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
4
packages/server-nest/.prettierrc
Normal file
4
packages/server-nest/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
1
packages/server-nest/.todo
Normal file
1
packages/server-nest/.todo
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- Build authentication services.
|
||||||
1
packages/server-nest/README.md
Normal file
1
packages/server-nest/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
## @bigcapitalhq/server
|
||||||
11
packages/server-nest/nest-cli.json
Normal file
11
packages/server-nest/nest-cli.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true,
|
||||||
|
"assets": [
|
||||||
|
{ "include": "i18n/**/*", "watchAssets": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
103
packages/server-nest/package.json
Normal file
103
packages/server-nest/package.json
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"name": "@bigcapital/server2",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/bull": "^10.2.1",
|
||||||
|
"@nestjs/bullmq": "^10.2.1",
|
||||||
|
"@nestjs/cache-manager": "^2.2.2",
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/config": "^3.2.3",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/event-emitter": "^2.0.4",
|
||||||
|
"@nestjs/jwt": "^10.2.0",
|
||||||
|
"@nestjs/passport": "^10.0.3",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestjs/swagger": "^7.4.2",
|
||||||
|
"@nestjs/throttler": "^6.2.1",
|
||||||
|
"@types/passport-local": "^1.0.38",
|
||||||
|
"@types/ramda": "^0.30.2",
|
||||||
|
"bull": "^4.16.3",
|
||||||
|
"bullmq": "^5.21.1",
|
||||||
|
"cache-manager": "^6.1.1",
|
||||||
|
"cache-manager-redis-store": "^3.0.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
|
"express-validator": "^7.2.0",
|
||||||
|
"fp-ts": "^2.16.9",
|
||||||
|
"knex": "^3.1.0",
|
||||||
|
"lamda": "^0.4.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"mysql2": "^3.11.3",
|
||||||
|
"nestjs-cls": "^4.4.1",
|
||||||
|
"nestjs-i18n": "^10.4.9",
|
||||||
|
"objection": "^3.1.5",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
|
"ramda": "^0.30.1",
|
||||||
|
"redis": "^4.7.0",
|
||||||
|
"reflect-metadata": "^0.2.0",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
"@nestjs/schematics": "^10.0.0",
|
||||||
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/supertest": "^6.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-loader": "^9.4.3",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/server-nest/src/common/config/gotenberg.ts
Normal file
6
packages/server-nest/src/common/config/gotenberg.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('gotenberg', () => ({
|
||||||
|
url: process.env.GOTENBERG_URL,
|
||||||
|
docsUrl: process.env.GOTENBERG_DOCS_URL,
|
||||||
|
}));
|
||||||
21
packages/server-nest/src/common/config/index.ts
Normal file
21
packages/server-nest/src/common/config/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import systemDatabase from './system-database';
|
||||||
|
import tenantDatabase from './tenant-database';
|
||||||
|
import signup from './signup';
|
||||||
|
import gotenberg from './gotenberg';
|
||||||
|
import plaid from './plaid';
|
||||||
|
import lemonsqueezy from './lemonsqueezy';
|
||||||
|
import s3 from './s3';
|
||||||
|
import openExchange from './open-exchange';
|
||||||
|
import posthog from './posthog';
|
||||||
|
|
||||||
|
export const config = [
|
||||||
|
systemDatabase,
|
||||||
|
tenantDatabase,
|
||||||
|
signup,
|
||||||
|
gotenberg,
|
||||||
|
plaid,
|
||||||
|
lemonsqueezy,
|
||||||
|
s3,
|
||||||
|
openExchange,
|
||||||
|
posthog,
|
||||||
|
];
|
||||||
7
packages/server-nest/src/common/config/lemonsqueezy.ts
Normal file
7
packages/server-nest/src/common/config/lemonsqueezy.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('lemonsqueezy', () => ({
|
||||||
|
apiKey: process.env.LEMONSQUEEZY_API_KEY,
|
||||||
|
storeId: process.env.LEMONSQUEEZY_STORE_ID,
|
||||||
|
webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
|
||||||
|
}));
|
||||||
5
packages/server-nest/src/common/config/open-exchange.ts
Normal file
5
packages/server-nest/src/common/config/open-exchange.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('openExchange', () => ({
|
||||||
|
appId: process.env.OPEN_EXCHANGE_RATE_APP_ID,
|
||||||
|
}));
|
||||||
8
packages/server-nest/src/common/config/plaid.ts
Normal file
8
packages/server-nest/src/common/config/plaid.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('plaid', () => ({
|
||||||
|
env: process.env.PLAID_ENV || 'sandbox',
|
||||||
|
clientId: process.env.PLAID_CLIENT_ID,
|
||||||
|
secret: process.env.PLAID_SECRET,
|
||||||
|
linkWebhook: process.env.PLAID_LINK_WEBHOOK,
|
||||||
|
}));
|
||||||
6
packages/server-nest/src/common/config/posthog.ts
Normal file
6
packages/server-nest/src/common/config/posthog.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('posthog', () => ({
|
||||||
|
apiKey: process.env.POSTHOG_API_KEY,
|
||||||
|
host: process.env.POSTHOG_HOST || 'https://us.i.posthog.com',
|
||||||
|
}));
|
||||||
9
packages/server-nest/src/common/config/s3.ts
Normal file
9
packages/server-nest/src/common/config/s3.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('s3', () => ({
|
||||||
|
region: process.env.S3_REGION || 'US',
|
||||||
|
accessKeyId: process.env.S3_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||||
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
|
bucket: process.env.S3_BUCKET,
|
||||||
|
}));
|
||||||
12
packages/server-nest/src/common/config/signup.ts
Normal file
12
packages/server-nest/src/common/config/signup.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('signup', () => ({
|
||||||
|
disabled: process.env.SIGNUP_DISABLED === 'true',
|
||||||
|
allowedDomains: process.env.SIGNUP_ALLOWED_DOMAINS
|
||||||
|
? process.env.SIGNUP_ALLOWED_DOMAINS.split(',')
|
||||||
|
: [],
|
||||||
|
allowedEmails: process.env.SIGNUP_ALLOWED_EMAILS
|
||||||
|
? process.env.SIGNUP_ALLOWED_EMAILS.split(',')
|
||||||
|
: [],
|
||||||
|
emailConfirmation: process.env.SIGNUP_EMAIL_CONFIRMATION === 'true',
|
||||||
|
}));
|
||||||
9
packages/server-nest/src/common/config/stripe-payment.ts
Normal file
9
packages/server-nest/src/common/config/stripe-payment.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('stripePayment', () => ({
|
||||||
|
secretKey: process.env.STRIPE_PAYMENT_SECRET_KEY,
|
||||||
|
publishableKey: process.env.STRIPE_PAYMENT_PUBLISHABLE_KEY,
|
||||||
|
clientId: process.env.STRIPE_PAYMENT_CLIENT_ID,
|
||||||
|
webhooksSecret: process.env.STRIPE_PAYMENT_WEBHOOKS_SECRET,
|
||||||
|
redirectUrl: process.env.STRIPE_PAYMENT_REDIRECT_URL,
|
||||||
|
}));
|
||||||
10
packages/server-nest/src/common/config/system-database.ts
Normal file
10
packages/server-nest/src/common/config/system-database.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('systemDatabase', () => ({
|
||||||
|
client: 'mysql',
|
||||||
|
host: process.env.SYSTEM_DB_HOST || process.env.DB_HOST,
|
||||||
|
port: process.env.SYSTEM_DB_PORT || process.env.DB_PORT || 5432,
|
||||||
|
user: process.env.SYSTEM_DB_USER || process.env.DB_USER,
|
||||||
|
password: process.env.SYSTEM_DB_PASSWORD || process.env.DB_PASSWORD,
|
||||||
|
databaseName: process.env.SYSTEM_DB_NAME || process.env.DB_NAME,
|
||||||
|
}));
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('tenantDatabase', () => ({
|
||||||
|
client: 'mysql',
|
||||||
|
host: process.env.TENANT_DB_HOST || process.env.DB_HOST,
|
||||||
|
port: process.env.TENANT_DB_PORT || process.env.DB_PORT || 5432,
|
||||||
|
user: process.env.TENANT_DB_USER || process.env.DB_USER,
|
||||||
|
password: process.env.TENANT_DB_PASSWORD || process.env.DB_PASSWORD,
|
||||||
|
}));
|
||||||
754
packages/server-nest/src/common/events/events.ts
Normal file
754
packages/server-nest/src/common/events/events.ts
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
export const events = {
|
||||||
|
/**
|
||||||
|
* Authentication service.
|
||||||
|
*/
|
||||||
|
auth: {
|
||||||
|
signIn: 'onSignIn',
|
||||||
|
signingIn: 'onSigningIn',
|
||||||
|
|
||||||
|
signUp: 'onSignUp',
|
||||||
|
signingUp: 'onSigningUp',
|
||||||
|
|
||||||
|
signUpConfirming: 'signUpConfirming',
|
||||||
|
signUpConfirmed: 'signUpConfirmed',
|
||||||
|
|
||||||
|
sendingResetPassword: 'onSendingResetPassword',
|
||||||
|
sendResetPassword: 'onSendResetPassword',
|
||||||
|
|
||||||
|
resetPassword: 'onResetPassword',
|
||||||
|
resetingPassword: 'onResetingPassword',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invite users service.
|
||||||
|
*/
|
||||||
|
inviteUser: {
|
||||||
|
acceptInvite: 'onUserAcceptInvite',
|
||||||
|
sendInvite: 'onUserSendInvite',
|
||||||
|
resendInvite: 'onUserInviteResend',
|
||||||
|
checkInvite: 'onUserCheckInvite',
|
||||||
|
sendInviteTenantSynced: 'onUserSendInviteTenantSynced',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization managment service.
|
||||||
|
*/
|
||||||
|
organization: {
|
||||||
|
build: 'onOrganizationBuild',
|
||||||
|
built: 'onOrganizationBuilt',
|
||||||
|
|
||||||
|
seeded: 'onOrganizationSeeded',
|
||||||
|
|
||||||
|
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization subscription.
|
||||||
|
*/
|
||||||
|
subscription: {
|
||||||
|
onSubscriptionCancel: 'onSubscriptionCancel',
|
||||||
|
onSubscriptionCancelled: 'onSubscriptionCancelled',
|
||||||
|
|
||||||
|
onSubscriptionResume: 'onSubscriptionResume',
|
||||||
|
onSubscriptionResumed: 'onSubscriptionResumed',
|
||||||
|
|
||||||
|
onSubscriptionPlanChange: 'onSubscriptionPlanChange',
|
||||||
|
onSubscriptionPlanChanged: 'onSubscriptionPlanChanged',
|
||||||
|
|
||||||
|
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
|
||||||
|
|
||||||
|
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
|
||||||
|
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tenants managment service.
|
||||||
|
*/
|
||||||
|
tenantManager: {
|
||||||
|
databaseCreated: 'onDatabaseCreated',
|
||||||
|
tenantMigrated: 'onTenantMigrated',
|
||||||
|
tenantSeeded: 'onTenantSeeded',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accounts service.
|
||||||
|
*/
|
||||||
|
accounts: {
|
||||||
|
onViewed: 'onAccountViewed',
|
||||||
|
onListViewed: 'onAccountsListViewed',
|
||||||
|
|
||||||
|
onCreating: 'onAccountCreating',
|
||||||
|
onCreated: 'onAccountCreated',
|
||||||
|
|
||||||
|
onEditing: 'onAccountEditing',
|
||||||
|
onEdited: 'onAccountEdited',
|
||||||
|
|
||||||
|
onDelete: 'onAccountDelete',
|
||||||
|
onDeleted: 'onAccountDeleted',
|
||||||
|
|
||||||
|
onBulkDeleted: 'onBulkDeleted',
|
||||||
|
onBulkActivated: 'onAccountBulkActivated',
|
||||||
|
|
||||||
|
onActivated: 'onAccountActivated',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual journals service.
|
||||||
|
*/
|
||||||
|
manualJournals: {
|
||||||
|
onCreating: 'onManualJournalCreating',
|
||||||
|
onCreated: 'onManualJournalCreated',
|
||||||
|
|
||||||
|
onEditing: 'onManualJournalEditing',
|
||||||
|
onEdited: 'onManualJournalEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onManualJournalDeleting',
|
||||||
|
onDeleted: 'onManualJournalDeleted',
|
||||||
|
|
||||||
|
onPublished: 'onManualJournalPublished',
|
||||||
|
onPublishing: 'onManualJournalPublishing',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expenses service.
|
||||||
|
*/
|
||||||
|
expenses: {
|
||||||
|
onCreating: 'onExpenseCreating',
|
||||||
|
onCreated: 'onExpenseCreated',
|
||||||
|
|
||||||
|
onEditing: 'onExpenseEditing',
|
||||||
|
onEdited: 'onExpenseEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onExpenseDeleting',
|
||||||
|
onDeleted: 'onExpenseDeleted',
|
||||||
|
|
||||||
|
onPublishing: 'onExpensePublishing',
|
||||||
|
onPublished: 'onExpensePublished',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sales invoices service.
|
||||||
|
*/
|
||||||
|
saleInvoice: {
|
||||||
|
onViewed: 'onSaleInvoiceItemViewed',
|
||||||
|
onListViewed: 'onSaleInvoiceListViewed',
|
||||||
|
|
||||||
|
onPdfViewed: 'onSaleInvoicePdfViewed',
|
||||||
|
|
||||||
|
onCreate: 'onSaleInvoiceCreate',
|
||||||
|
onCreating: 'onSaleInvoiceCreating',
|
||||||
|
onCreated: 'onSaleInvoiceCreated',
|
||||||
|
|
||||||
|
onEdit: 'onSaleInvoiceEdit',
|
||||||
|
onEditing: 'onSaleInvoiceEditing',
|
||||||
|
onEdited: 'onSaleInvoiceEdited',
|
||||||
|
|
||||||
|
onDelete: 'onSaleInvoiceDelete',
|
||||||
|
onDeleting: 'onSaleInvoiceDeleting',
|
||||||
|
onDeleted: 'onSaleInvoiceDeleted',
|
||||||
|
|
||||||
|
onDelivering: 'onSaleInvoiceDelivering',
|
||||||
|
onDeliver: 'onSaleInvoiceDeliver',
|
||||||
|
onDelivered: 'onSaleInvoiceDelivered',
|
||||||
|
|
||||||
|
onPublish: 'onSaleInvoicePublish',
|
||||||
|
onPublished: 'onSaleInvoicePublished',
|
||||||
|
|
||||||
|
onWriteoff: 'onSaleInvoiceWriteoff',
|
||||||
|
onWrittenoff: 'onSaleInvoiceWrittenoff',
|
||||||
|
onWrittenoffCancel: 'onSaleInvoiceWrittenoffCancel',
|
||||||
|
onWrittenoffCanceled: 'onSaleInvoiceWrittenoffCanceled',
|
||||||
|
|
||||||
|
onNotifySms: 'onSaleInvoiceNotifySms',
|
||||||
|
onNotifiedSms: 'onSaleInvoiceNotifiedSms',
|
||||||
|
|
||||||
|
onNotifyMail: 'onSaleInvoiceNotifyMail',
|
||||||
|
onNotifyReminderMail: 'onSaleInvoiceNotifyReminderMail',
|
||||||
|
|
||||||
|
onPreMailSend: 'onSaleInvoicePreMailSend',
|
||||||
|
onMailSend: 'onSaleInvoiceMailSend',
|
||||||
|
onMailSent: 'onSaleInvoiceMailSent',
|
||||||
|
|
||||||
|
onMailReminderSend: 'onSaleInvoiceMailReminderSend',
|
||||||
|
onMailReminderSent: 'onSaleInvoiceMailReminderSent',
|
||||||
|
|
||||||
|
onPublicLinkGenerating: 'onPublicSharableLinkGenerating',
|
||||||
|
onPublicLinkGenerated: 'onPublicSharableLinkGenerated',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sales estimates service.
|
||||||
|
*/
|
||||||
|
saleEstimate: {
|
||||||
|
onPdfViewed: 'onSaleEstimatePdfViewed',
|
||||||
|
|
||||||
|
onCreating: 'onSaleEstimateCreating',
|
||||||
|
onCreated: 'onSaleEstimateCreated',
|
||||||
|
|
||||||
|
onEditing: 'onSaleEstimateEditing',
|
||||||
|
onEdited: 'onSaleEstimateEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onSaleEstimatedDeleting',
|
||||||
|
onDeleted: 'onSaleEstimatedDeleted',
|
||||||
|
|
||||||
|
onPublishing: 'onSaleEstimatedPublishing',
|
||||||
|
onPublished: 'onSaleEstimatedPublished',
|
||||||
|
|
||||||
|
onNotifySms: 'onSaleEstimateNotifySms',
|
||||||
|
onNotifiedSms: 'onSaleEstimateNotifiedSms',
|
||||||
|
|
||||||
|
onDelivering: 'onSaleEstimateDelivering',
|
||||||
|
onDelivered: 'onSaleEstimateDelivered',
|
||||||
|
|
||||||
|
onConvertedToInvoice: 'onSaleEstimateConvertedToInvoice',
|
||||||
|
|
||||||
|
onApproving: 'onSaleEstimateApproving',
|
||||||
|
onApproved: 'onSaleEstimateApproved',
|
||||||
|
|
||||||
|
onRejecting: 'onSaleEstimateRejecting',
|
||||||
|
onRejected: 'onSaleEstimateRejected',
|
||||||
|
|
||||||
|
onNotifyMail: 'onSaleEstimateNotifyMail',
|
||||||
|
|
||||||
|
onPreMailSend: 'onSaleEstimatePreMailSend',
|
||||||
|
onMailSend: 'onSaleEstimateMailSend',
|
||||||
|
onMailSent: 'onSaleEstimateMailSend',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sales receipts service.
|
||||||
|
*/
|
||||||
|
saleReceipt: {
|
||||||
|
onPdfViewed: 'onSaleReceiptPdfViewed',
|
||||||
|
|
||||||
|
onCreating: 'onSaleReceiptsCreating',
|
||||||
|
onCreated: 'onSaleReceiptsCreated',
|
||||||
|
|
||||||
|
onEditing: 'onSaleReceiptsEditing',
|
||||||
|
onEdited: 'onSaleReceiptsEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onSaleReceiptsDeleting',
|
||||||
|
onDeleted: 'onSaleReceiptsDeleted',
|
||||||
|
|
||||||
|
onPublishing: 'onSaleReceiptPublishing',
|
||||||
|
onPublished: 'onSaleReceiptPublished',
|
||||||
|
|
||||||
|
onClosed: 'onSaleReceiptClosed',
|
||||||
|
onClosing: 'onSaleReceiptClosing',
|
||||||
|
|
||||||
|
onNotifySms: 'onSaleReceiptNotifySms',
|
||||||
|
onNotifiedSms: 'onSaleReceiptNotifiedSms',
|
||||||
|
|
||||||
|
onPreMailSend: 'onSaleReceiptPreMailSend',
|
||||||
|
onMailSend: 'onSaleReceiptMailSend',
|
||||||
|
onMailSent: 'onSaleReceiptMailSent',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment receipts service.
|
||||||
|
*/
|
||||||
|
paymentReceive: {
|
||||||
|
onPdfViewed: 'onPaymentReceivedPdfViewed',
|
||||||
|
|
||||||
|
onCreated: 'onPaymentReceiveCreated',
|
||||||
|
onCreating: 'onPaymentReceiveCreating',
|
||||||
|
|
||||||
|
onEditing: 'onPaymentReceiveEditing',
|
||||||
|
onEdited: 'onPaymentReceiveEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onPaymentReceiveDeleting',
|
||||||
|
onDeleted: 'onPaymentReceiveDeleted',
|
||||||
|
|
||||||
|
onPublishing: 'onPaymentReceivePublishing',
|
||||||
|
onPublished: 'onPaymentReceivePublished',
|
||||||
|
|
||||||
|
onNotifySms: 'onPaymentReceiveNotifySms',
|
||||||
|
onNotifiedSms: 'onPaymentReceiveNotifiedSms',
|
||||||
|
|
||||||
|
onPreMailSend: 'onPaymentReceivePreMailSend',
|
||||||
|
onMailSend: 'onPaymentReceiveMailSend',
|
||||||
|
onMailSent: 'onPaymentReceiveMailSent',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bills service.
|
||||||
|
*/
|
||||||
|
bill: {
|
||||||
|
onCreating: 'onBillCreating',
|
||||||
|
onCreated: 'onBillCreated',
|
||||||
|
|
||||||
|
onEditing: 'onBillEditing',
|
||||||
|
onEdited: 'onBillEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onBillDeleting',
|
||||||
|
onDeleted: 'onBillDeleted',
|
||||||
|
|
||||||
|
onPublishing: 'onBillPublishing',
|
||||||
|
onPublished: 'onBillPublished',
|
||||||
|
|
||||||
|
onOpening: 'onBillOpening',
|
||||||
|
onOpened: 'onBillOpened',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bill payments service.
|
||||||
|
*/
|
||||||
|
billPayment: {
|
||||||
|
onCreating: 'onBillPaymentCreating',
|
||||||
|
onCreated: 'onBillPaymentCreated',
|
||||||
|
|
||||||
|
onEditing: 'onBillPaymentEditing',
|
||||||
|
onEdited: 'onBillPaymentEdited',
|
||||||
|
|
||||||
|
onDeleted: 'onBillPaymentDeleted',
|
||||||
|
onDeleting: 'onBillPaymentDeleting',
|
||||||
|
|
||||||
|
onPublishing: 'onBillPaymentPublishing',
|
||||||
|
onPublished: 'onBillPaymentPublished',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customers services.
|
||||||
|
*/
|
||||||
|
customers: {
|
||||||
|
onCreating: 'onCustomerCreating',
|
||||||
|
onCreated: 'onCustomerCreated',
|
||||||
|
|
||||||
|
onEdited: 'onCustomerEdited',
|
||||||
|
onEditing: 'onCustomerEditing',
|
||||||
|
|
||||||
|
onDeleted: 'onCustomerDeleted',
|
||||||
|
onDeleting: 'onCustomerDeleting',
|
||||||
|
onBulkDeleted: 'onBulkDeleted',
|
||||||
|
|
||||||
|
onOpeningBalanceChanging: 'onCustomerOpeningBalanceChanging',
|
||||||
|
onOpeningBalanceChanged: 'onCustomerOpeingBalanceChanged',
|
||||||
|
|
||||||
|
onActivating: 'onCustomerActivating',
|
||||||
|
onActivated: 'onCustomerActivated',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendors services.
|
||||||
|
*/
|
||||||
|
vendors: {
|
||||||
|
onCreated: 'onVendorCreated',
|
||||||
|
onCreating: 'onVendorCreating',
|
||||||
|
|
||||||
|
onEdited: 'onVendorEdited',
|
||||||
|
onEditing: 'onVendorEditing',
|
||||||
|
|
||||||
|
onDeleted: 'onVendorDeleted',
|
||||||
|
onDeleting: 'onVendorDeleting',
|
||||||
|
|
||||||
|
onOpeningBalanceChanging: 'onVendorOpeingBalanceChanging',
|
||||||
|
onOpeningBalanceChanged: 'onVendorOpeingBalanceChanged',
|
||||||
|
|
||||||
|
onActivating: 'onVendorActivating',
|
||||||
|
onActivated: 'onVendorActivated',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Items service.
|
||||||
|
*/
|
||||||
|
item: {
|
||||||
|
onViewed: 'onItemViewed',
|
||||||
|
|
||||||
|
onCreated: 'onItemCreated',
|
||||||
|
onCreating: 'onItemCreating',
|
||||||
|
|
||||||
|
onEditing: 'onItemEditing',
|
||||||
|
onEdited: 'onItemEdited',
|
||||||
|
|
||||||
|
onDeleted: 'onItemDeleted',
|
||||||
|
onDeleting: 'onItemDeleting',
|
||||||
|
|
||||||
|
onActivating: 'onItemActivating',
|
||||||
|
onActivated: 'onItemActivated',
|
||||||
|
|
||||||
|
onInactivating: 'onInactivating',
|
||||||
|
onInactivated: 'onItemInactivated',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item category service.
|
||||||
|
*/
|
||||||
|
itemCategory: {
|
||||||
|
onCreated: 'onItemCategoryCreated',
|
||||||
|
onEdited: 'onItemCategoryEdited',
|
||||||
|
onDeleted: 'onItemCategoryDeleted',
|
||||||
|
onBulkDeleted: 'onItemCategoryBulkDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inventory service.
|
||||||
|
*/
|
||||||
|
inventory: {
|
||||||
|
onInventoryTransactionsCreated: 'onInventoryTransactionsCreated',
|
||||||
|
onInventoryTransactionsDeleted: 'onInventoryTransactionsDeleted',
|
||||||
|
|
||||||
|
onComputeItemCostJobScheduled: 'onComputeItemCostJobScheduled',
|
||||||
|
onComputeItemCostJobStarted: 'onComputeItemCostJobStarted',
|
||||||
|
onComputeItemCostJobCompleted: 'onComputeItemCostJobCompleted',
|
||||||
|
|
||||||
|
onInventoryCostEntriesWritten: 'onInventoryCostEntriesWritten',
|
||||||
|
|
||||||
|
onCostLotsGLEntriesBeforeWrite: 'onInventoryCostLotsGLEntriesBeforeWrite',
|
||||||
|
onCostLotsGLEntriesWrite: 'onInventoryCostLotsGLEntriesWrite',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inventory adjustment service.
|
||||||
|
*/
|
||||||
|
inventoryAdjustment: {
|
||||||
|
onQuickCreating: 'onInventoryAdjustmentCreating',
|
||||||
|
onQuickCreated: 'onInventoryAdjustmentQuickCreated',
|
||||||
|
|
||||||
|
onCreated: 'onInventoryAdjustmentCreated',
|
||||||
|
|
||||||
|
onDeleting: 'onInventoryAdjustmentDeleting',
|
||||||
|
onDeleted: 'onInventoryAdjustmentDeleted',
|
||||||
|
|
||||||
|
onPublishing: 'onInventoryAdjustmentPublishing',
|
||||||
|
onPublished: 'onInventoryAdjustmentPublished',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bill landed cost.
|
||||||
|
*/
|
||||||
|
billLandedCost: {
|
||||||
|
onCreate: 'onBillLandedCostCreate',
|
||||||
|
onCreated: 'onBillLandedCostCreated',
|
||||||
|
onDelete: 'onBillLandedCostDelete',
|
||||||
|
onDeleted: 'onBillLandedCostDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
cashflow: {
|
||||||
|
onOwnerContributionCreate: 'onCashflowOwnerContributionCreate',
|
||||||
|
onOwnerContributionCreated: 'onCashflowOwnerContributionCreated',
|
||||||
|
|
||||||
|
onOtherIncomeCreate: 'onCashflowOtherIncomeCreate',
|
||||||
|
onOtherIncomeCreated: 'onCashflowOtherIncomeCreated',
|
||||||
|
|
||||||
|
onTransactionCreating: 'onCashflowTransactionCreating',
|
||||||
|
onTransactionCreated: 'onCashflowTransactionCreated',
|
||||||
|
|
||||||
|
onTransactionDeleting: 'onCashflowTransactionDeleting',
|
||||||
|
onTransactionDeleted: 'onCashflowTransactionDeleted',
|
||||||
|
|
||||||
|
onTransactionCategorizing: 'onTransactionCategorizing',
|
||||||
|
onTransactionCategorized: 'onCashflowTransactionCategorized',
|
||||||
|
|
||||||
|
onTransactionUncategorizedCreating: 'onTransactionUncategorizedCreating',
|
||||||
|
onTransactionUncategorizedCreated: 'onTransactionUncategorizedCreated',
|
||||||
|
|
||||||
|
onTransactionUncategorizing: 'onTransactionUncategorizing',
|
||||||
|
onTransactionUncategorized: 'onTransactionUncategorized',
|
||||||
|
|
||||||
|
onTransactionCategorizingAsExpense: 'onTransactionCategorizingAsExpense',
|
||||||
|
onTransactionCategorizedAsExpense: 'onTransactionCategorizedAsExpense',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roles service events.
|
||||||
|
*/
|
||||||
|
roles: {
|
||||||
|
onCreate: 'onRoleCreate',
|
||||||
|
onCreated: 'onRoleCreated',
|
||||||
|
onEdit: 'onRoleEdit',
|
||||||
|
onEdited: 'onRoleEdited',
|
||||||
|
onDelete: 'onRoleDelete',
|
||||||
|
onDeleted: 'onRoleDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
tenantUser: {
|
||||||
|
onEdited: 'onTenantUserEdited',
|
||||||
|
onDeleted: 'onTenantUserDeleted',
|
||||||
|
onActivated: 'onTenantUserActivated',
|
||||||
|
onInactivated: 'onTenantUserInactivated',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit note service.
|
||||||
|
*/
|
||||||
|
creditNote: {
|
||||||
|
onPdfViewed: 'onCreditNotePdfViewed',
|
||||||
|
|
||||||
|
onCreate: 'onCreditNoteCreate',
|
||||||
|
onCreating: 'onCreditNoteCreating',
|
||||||
|
onCreated: 'onCreditNoteCreated',
|
||||||
|
|
||||||
|
onEditing: 'onCreditNoteEditing',
|
||||||
|
onEdit: 'onCreditNoteEdit',
|
||||||
|
onEdited: 'onCreditNoteEdited',
|
||||||
|
|
||||||
|
onDelete: 'onCreditNoteDelete',
|
||||||
|
onDeleting: 'onCreditNoteDeleting',
|
||||||
|
onDeleted: 'onCreditNoteDeleted',
|
||||||
|
|
||||||
|
onOpen: 'onCreditNoteOpen',
|
||||||
|
onOpening: 'onCreditNoteOpening',
|
||||||
|
onOpened: 'onCreditNoteOpened',
|
||||||
|
|
||||||
|
onRefundCreate: 'onCreditNoteRefundCreate',
|
||||||
|
onRefundCreating: 'onCreditNoteRefundCreating',
|
||||||
|
onRefundCreated: 'onCreditNoteRefundCreated',
|
||||||
|
|
||||||
|
onRefundDelete: 'onCreditNoteRefundDelete',
|
||||||
|
onRefundDeleting: 'onCreditNoteRefundDeleting',
|
||||||
|
onRefundDeleted: 'onCreditNoteRefundDeleted',
|
||||||
|
|
||||||
|
onApplyToInvoicesCreated: 'onCreditNoteApplyToInvoiceCreated',
|
||||||
|
onApplyToInvoicesCreate: 'onCreditNoteApplyToInvoiceCreate',
|
||||||
|
onApplyToInvoicesDeleted: 'onCreditNoteApplyToInvoiceDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor credit service.
|
||||||
|
*/
|
||||||
|
vendorCredit: {
|
||||||
|
onCreate: 'onVendorCreditCreate',
|
||||||
|
onCreating: 'onVendorCreditCreating',
|
||||||
|
onCreated: 'onVendorCreditCreated',
|
||||||
|
|
||||||
|
onEdit: 'onVendorCreditEdit',
|
||||||
|
onEditing: 'onVendorCreditEditing',
|
||||||
|
onEdited: 'onVendorCreditEdited',
|
||||||
|
|
||||||
|
onDelete: 'onVendorCreditDelete',
|
||||||
|
onDeleting: 'onVendorCreditDeleting',
|
||||||
|
onDeleted: 'onVendorCreditDeleted',
|
||||||
|
|
||||||
|
onOpen: 'onVendorCreditOpen',
|
||||||
|
onOpened: 'onVendorCreditOpened',
|
||||||
|
|
||||||
|
onRefundCreating: 'onVendorCreditRefundCreating',
|
||||||
|
onRefundCreate: 'onVendorCreditRefundCreate',
|
||||||
|
onRefundCreated: 'onVendorCreditRefundCreated',
|
||||||
|
|
||||||
|
onRefundDelete: 'onVendorCreditRefundDelete',
|
||||||
|
onRefundDeleting: 'onVendorCreditRefundDeleting',
|
||||||
|
onRefundDeleted: 'onVendorCreditRefundDeleted',
|
||||||
|
|
||||||
|
onApplyToInvoicesCreated: 'onVendorCreditApplyToInvoiceCreated',
|
||||||
|
onApplyToInvoicesCreate: 'onVendorCreditApplyToInvoiceCreate',
|
||||||
|
onApplyToInvoicesDeleted: 'onVendorCreditApplyToInvoiceDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
transactionsLocking: {
|
||||||
|
locked: 'onTransactionLockingLocked',
|
||||||
|
lockCanceled: 'onTransactionLockingLockCanceled',
|
||||||
|
partialUnlocked: 'onTransactionLockingPartialUnlocked',
|
||||||
|
partialUnlockCanceled: 'onTransactionLockingPartialUnlockCanceled',
|
||||||
|
},
|
||||||
|
|
||||||
|
warehouse: {
|
||||||
|
onCreate: 'onWarehouseCreate',
|
||||||
|
onCreated: 'onWarehouseCreated',
|
||||||
|
|
||||||
|
onEdit: 'onWarehouseEdit',
|
||||||
|
onEdited: 'onWarehouseEdited',
|
||||||
|
|
||||||
|
onDelete: 'onWarehouseDelete',
|
||||||
|
onDeleted: 'onWarehouseDeleted',
|
||||||
|
|
||||||
|
onActivate: 'onWarehouseActivate',
|
||||||
|
onActivated: 'onWarehouseActivated',
|
||||||
|
|
||||||
|
onMarkPrimary: 'onWarehouseMarkPrimary',
|
||||||
|
onMarkedPrimary: 'onWarehouseMarkedPrimary',
|
||||||
|
},
|
||||||
|
|
||||||
|
warehouseTransfer: {
|
||||||
|
onCreate: 'onWarehouseTransferCreate',
|
||||||
|
onCreated: 'onWarehouseTransferCreated',
|
||||||
|
|
||||||
|
onEdit: 'onWarehouseTransferEdit',
|
||||||
|
onEdited: 'onWarehouseTransferEdited',
|
||||||
|
|
||||||
|
onDelete: 'onWarehouseTransferDelete',
|
||||||
|
onDeleted: 'onWarehouseTransferDeleted',
|
||||||
|
|
||||||
|
onInitiate: 'onWarehouseTransferInitiate',
|
||||||
|
onInitiated: 'onWarehouseTransferInitated',
|
||||||
|
|
||||||
|
onTransfer: 'onWarehouseTransferInitiate',
|
||||||
|
onTransferred: 'onWarehouseTransferTransferred',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Branches.
|
||||||
|
*/
|
||||||
|
branch: {
|
||||||
|
onActivate: 'onBranchActivate',
|
||||||
|
onActivated: 'onBranchActivated',
|
||||||
|
|
||||||
|
onMarkPrimary: 'onBranchMarkPrimary',
|
||||||
|
onMarkedPrimary: 'onBranchMarkedPrimary',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Projects.
|
||||||
|
*/
|
||||||
|
project: {
|
||||||
|
onCreate: 'onProjectCreate',
|
||||||
|
onCreating: 'onProjectCreating',
|
||||||
|
onCreated: 'onProjectCreated',
|
||||||
|
|
||||||
|
onEdit: 'onEditProject',
|
||||||
|
onEditing: 'onEditingProject',
|
||||||
|
onEdited: 'onEditedProject',
|
||||||
|
|
||||||
|
onEditStatus: 'onEditStatusProject',
|
||||||
|
onEditingStatus: 'onEditingStatusProject',
|
||||||
|
onEditedStatus: 'onEditedStatusProject',
|
||||||
|
|
||||||
|
onDelete: 'onDeleteProject',
|
||||||
|
onDeleting: 'onDeletingProject',
|
||||||
|
onDeleted: 'onDeletedProject',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project Tasks.
|
||||||
|
*/
|
||||||
|
projectTask: {
|
||||||
|
onCreate: 'onProjectTaskCreate',
|
||||||
|
onCreating: 'onProjectTaskCreating',
|
||||||
|
onCreated: 'onProjectTaskCreated',
|
||||||
|
|
||||||
|
onEdit: 'onProjectTaskEdit',
|
||||||
|
onEditing: 'onProjectTaskEditing',
|
||||||
|
onEdited: 'onProjectTaskEdited',
|
||||||
|
|
||||||
|
onDelete: 'onProjectTaskDelete',
|
||||||
|
onDeleting: 'onProjectTaskDeleting',
|
||||||
|
onDeleted: 'onProjectTaskDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project Times.
|
||||||
|
*/
|
||||||
|
projectTime: {
|
||||||
|
onCreate: 'onProjectTimeCreate',
|
||||||
|
onCreating: 'onProjectTimeCreating',
|
||||||
|
onCreated: 'onProjectTimeCreated',
|
||||||
|
|
||||||
|
onEdit: 'onProjectTimeEdit',
|
||||||
|
onEditing: 'onProjectTimeEditing',
|
||||||
|
onEdited: 'onProjectTimeEdited',
|
||||||
|
|
||||||
|
onDelete: 'onProjectTimeDelete',
|
||||||
|
onDeleting: 'onProjectTimeDeleting',
|
||||||
|
onDeleted: 'onProjectTimeDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
taxRates: {
|
||||||
|
onCreating: 'onTaxRateCreating',
|
||||||
|
onCreated: 'onTaxRateCreated',
|
||||||
|
|
||||||
|
onEditing: 'onTaxRateEditing',
|
||||||
|
onEdited: 'onTaxRateEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onTaxRateDeleting',
|
||||||
|
onDeleted: 'onTaxRateDeleted',
|
||||||
|
|
||||||
|
onActivating: 'onTaxRateActivating',
|
||||||
|
onActivated: 'onTaxRateActivated',
|
||||||
|
|
||||||
|
onInactivating: 'onTaxRateInactivating',
|
||||||
|
onInactivated: 'onTaxRateInactivated',
|
||||||
|
},
|
||||||
|
|
||||||
|
plaid: {
|
||||||
|
onItemCreated: 'onPlaidItemCreated',
|
||||||
|
onTransactionsSynced: 'onPlaidTransactionsSynced',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bank rules.
|
||||||
|
bankRules: {
|
||||||
|
onCreating: 'onBankRuleCreating',
|
||||||
|
onCreated: 'onBankRuleCreated',
|
||||||
|
|
||||||
|
onEditing: 'onBankRuleEditing',
|
||||||
|
onEdited: 'onBankRuleEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onBankRuleDeleting',
|
||||||
|
onDeleted: 'onBankRuleDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bank matching.
|
||||||
|
bankMatch: {
|
||||||
|
onMatching: 'onBankTransactionMatching',
|
||||||
|
onMatched: 'onBankTransactionMatched',
|
||||||
|
|
||||||
|
onUnmatching: 'onBankTransactionUnmathcing',
|
||||||
|
onUnmatched: 'onBankTransactionUnmathced',
|
||||||
|
},
|
||||||
|
|
||||||
|
bankTransactions: {
|
||||||
|
onExcluding: 'onBankTransactionExclude',
|
||||||
|
onExcluded: 'onBankTransactionExcluded',
|
||||||
|
|
||||||
|
onUnexcluding: 'onBankTransactionUnexcluding',
|
||||||
|
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||||
|
|
||||||
|
onPendingRemoving: 'onBankTransactionPendingRemoving',
|
||||||
|
onPendingRemoved: 'onBankTransactionPendingRemoved',
|
||||||
|
},
|
||||||
|
|
||||||
|
bankAccount: {
|
||||||
|
onDisconnecting: 'onBankAccountDisconnecting',
|
||||||
|
onDisconnected: 'onBankAccountDisconnected',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Import files.
|
||||||
|
import: {
|
||||||
|
onImportCommitted: 'onImportFileCommitted',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Branding templates
|
||||||
|
pdfTemplate: {
|
||||||
|
onCreating: 'onPdfTemplateCreating',
|
||||||
|
onCreated: 'onPdfTemplateCreated',
|
||||||
|
|
||||||
|
onEditing: 'onPdfTemplateEditing',
|
||||||
|
onEdited: 'onPdfTemplatedEdited',
|
||||||
|
|
||||||
|
onDeleting: 'onPdfTemplateDeleting',
|
||||||
|
onDeleted: 'onPdfTemplateDeleted',
|
||||||
|
|
||||||
|
onAssignedDefault: 'onPdfTemplateAssignedDefault',
|
||||||
|
onAssigningDefault: 'onPdfTemplateAssigningDefault',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Payment method.
|
||||||
|
paymentMethod: {
|
||||||
|
onEditing: 'onPaymentMethodEditing',
|
||||||
|
onEdited: 'onPaymentMethodEdited',
|
||||||
|
|
||||||
|
onDeleted: 'onPaymentMethodDeleted',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Payment methods integrations
|
||||||
|
paymentIntegrationLink: {
|
||||||
|
onPaymentIntegrationLink: 'onPaymentIntegrationLink',
|
||||||
|
onPaymentIntegrationDeleteLink: 'onPaymentIntegrationDeleteLink',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Stripe Payment Integration
|
||||||
|
stripeIntegration: {
|
||||||
|
onAccountCreated: 'onStripeIntegrationAccountCreated',
|
||||||
|
onAccountDeleted: 'onStripeIntegrationAccountDeleted',
|
||||||
|
|
||||||
|
onPaymentLinkCreated: 'onStripePaymentLinkCreated',
|
||||||
|
onPaymentLinkInactivated: 'onStripePaymentLinkInactivated',
|
||||||
|
|
||||||
|
onOAuthCodeGranted: 'onStripeOAuthCodeGranted',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Stripe Payment Webhooks
|
||||||
|
stripeWebhooks: {
|
||||||
|
onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted',
|
||||||
|
onAccountUpdated: 'onStripeAccountUpdated',
|
||||||
|
},
|
||||||
|
};
|
||||||
20
packages/server-nest/src/common/pipes/ZodValidation.pipe.ts
Normal file
20
packages/server-nest/src/common/pipes/ZodValidation.pipe.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
PipeTransform,
|
||||||
|
ArgumentMetadata,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ZodSchema } from 'zod';
|
||||||
|
|
||||||
|
export class ZodValidationPipe implements PipeTransform {
|
||||||
|
constructor(private schema: ZodSchema) {}
|
||||||
|
|
||||||
|
transform(value: unknown, metadata: ArgumentMetadata) {
|
||||||
|
try {
|
||||||
|
const parsedValue = this.schema.parse(value);
|
||||||
|
return parsedValue;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw new BadRequestException(error.errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
230
packages/server-nest/src/constants/accounts.ts
Normal file
230
packages/server-nest/src/constants/accounts.ts
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
export const ACCOUNT_TYPE = {
|
||||||
|
CASH: 'cash',
|
||||||
|
BANK: 'bank',
|
||||||
|
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
|
||||||
|
INVENTORY: 'inventory',
|
||||||
|
OTHER_CURRENT_ASSET: 'other-current-asset',
|
||||||
|
FIXED_ASSET: 'fixed-asset',
|
||||||
|
NON_CURRENT_ASSET: 'none-current-asset',
|
||||||
|
|
||||||
|
ACCOUNTS_PAYABLE: 'accounts-payable',
|
||||||
|
CREDIT_CARD: 'credit-card',
|
||||||
|
TAX_PAYABLE: 'tax-payable',
|
||||||
|
OTHER_CURRENT_LIABILITY: 'other-current-liability',
|
||||||
|
LOGN_TERM_LIABILITY: 'long-term-liability',
|
||||||
|
NON_CURRENT_LIABILITY: 'non-current-liability',
|
||||||
|
|
||||||
|
EQUITY: 'equity',
|
||||||
|
INCOME: 'income',
|
||||||
|
OTHER_INCOME: 'other-income',
|
||||||
|
COST_OF_GOODS_SOLD: 'cost-of-goods-sold',
|
||||||
|
EXPENSE: 'expense',
|
||||||
|
OTHER_EXPENSE: 'other-expense',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACCOUNT_PARENT_TYPE = {
|
||||||
|
CURRENT_ASSET: 'current-asset',
|
||||||
|
FIXED_ASSET: 'fixed-asset',
|
||||||
|
NON_CURRENT_ASSET: 'non-current-asset',
|
||||||
|
|
||||||
|
CURRENT_LIABILITY: 'current-liability',
|
||||||
|
LOGN_TERM_LIABILITY: 'long-term-liability',
|
||||||
|
NON_CURRENT_LIABILITY: 'non-current-liability',
|
||||||
|
|
||||||
|
EQUITY: 'equity',
|
||||||
|
EXPENSE: 'expense',
|
||||||
|
INCOME: 'income',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACCOUNT_ROOT_TYPE = {
|
||||||
|
ASSET: 'asset',
|
||||||
|
LIABILITY: 'liability',
|
||||||
|
EQUITY: 'equity',
|
||||||
|
EXPENSE: 'expense',
|
||||||
|
INCOME: 'income',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ACCOUNT_NORMAL = {
|
||||||
|
CREDIT: 'credit',
|
||||||
|
DEBIT: 'debit',
|
||||||
|
};
|
||||||
|
export const ACCOUNT_TYPES = [
|
||||||
|
{
|
||||||
|
label: 'Cash',
|
||||||
|
key: ACCOUNT_TYPE.CASH,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||||
|
multiCurrency: true,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bank',
|
||||||
|
key: ACCOUNT_TYPE.BANK,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||||
|
multiCurrency: true,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Receivable',
|
||||||
|
key: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Inventory',
|
||||||
|
key: ACCOUNT_TYPE.INVENTORY,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Other Current Asset',
|
||||||
|
key: ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fixed Asset',
|
||||||
|
key: ACCOUNT_TYPE.FIXED_ASSET,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Non-Current Asset',
|
||||||
|
key: ACCOUNT_TYPE.NON_CURRENT_ASSET,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.ASSET,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Accounts Payable',
|
||||||
|
key: ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Credit Card',
|
||||||
|
key: ACCOUNT_TYPE.CREDIT_CARD,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||||
|
multiCurrency: true,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tax Payable',
|
||||||
|
key: ACCOUNT_TYPE.TAX_PAYABLE,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Other Current Liability',
|
||||||
|
key: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
|
||||||
|
balanceSheet: false,
|
||||||
|
incomeSheet: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Long Term Liability',
|
||||||
|
key: ACCOUNT_TYPE.LOGN_TERM_LIABILITY,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.LOGN_TERM_LIABILITY,
|
||||||
|
balanceSheet: false,
|
||||||
|
incomeSheet: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Non-Current Liability',
|
||||||
|
key: ACCOUNT_TYPE.NON_CURRENT_LIABILITY,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.NON_CURRENT_LIABILITY,
|
||||||
|
balanceSheet: false,
|
||||||
|
incomeSheet: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Equity',
|
||||||
|
key: ACCOUNT_TYPE.EQUITY,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.EQUITY,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.EQUITY,
|
||||||
|
balanceSheet: true,
|
||||||
|
incomeSheet: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Income',
|
||||||
|
key: ACCOUNT_TYPE.INCOME,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.INCOME,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.INCOME,
|
||||||
|
balanceSheet: false,
|
||||||
|
incomeSheet: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Other Income',
|
||||||
|
key: ACCOUNT_TYPE.OTHER_INCOME,
|
||||||
|
normal: ACCOUNT_NORMAL.CREDIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.INCOME,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.INCOME,
|
||||||
|
balanceSheet: false,
|
||||||
|
incomeSheet: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Cost of Goods Sold',
|
||||||
|
key: ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
|
||||||
|
balanceSheet: false,
|
||||||
|
incomeSheet: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Expense',
|
||||||
|
key: ACCOUNT_TYPE.EXPENSE,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
|
||||||
|
balanceSheet: false,
|
||||||
|
incomeSheet: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Other Expense',
|
||||||
|
key: ACCOUNT_TYPE.OTHER_EXPENSE,
|
||||||
|
normal: ACCOUNT_NORMAL.DEBIT,
|
||||||
|
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
|
||||||
|
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
|
||||||
|
balanceSheet: false,
|
||||||
|
incomeSheet: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getAccountsSupportsMultiCurrency = () => {
|
||||||
|
return ACCOUNT_TYPES.filter((account) => account.multiCurrency);
|
||||||
|
};
|
||||||
7
packages/server-nest/src/constants/data-types.ts
Normal file
7
packages/server-nest/src/constants/data-types.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const DATATYPES_LENGTH = {
|
||||||
|
STRING: 255,
|
||||||
|
TEXT: 65535,
|
||||||
|
INT_10: 4294967295,
|
||||||
|
DECIMAL_13_3: 9999999999.999,
|
||||||
|
DECIMAL_15_5: 999999999999.999,
|
||||||
|
};
|
||||||
21
packages/server-nest/src/i18n/en/test.json
Normal file
21
packages/server-nest/src/i18n/en/test.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"HELLO": "Hello",
|
||||||
|
"PRODUCT": {
|
||||||
|
"NEW": "New Product: {name}"
|
||||||
|
},
|
||||||
|
"ENGLISH": "English",
|
||||||
|
"ARRAY": ["ONE", "TWO", "THREE"],
|
||||||
|
"cat": "Cat",
|
||||||
|
"cat_name": "Cat: {name}",
|
||||||
|
"set-up-password": {
|
||||||
|
"heading": "Hello, {username}",
|
||||||
|
"title": "Forgot password",
|
||||||
|
"followLink": "Please follow the link to set up your password"
|
||||||
|
},
|
||||||
|
"day_interval": {
|
||||||
|
"one": "Every day",
|
||||||
|
"other": "Every {count} days",
|
||||||
|
"zero": "Never"
|
||||||
|
},
|
||||||
|
"nested": "We go shopping: $t(test.day_interval, {{\"count\": {count} }})"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExcludeNullInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
return next.handle().pipe(map((value) => (value === null ? '' : value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GlobalPrefixInterceptor implements NestInterceptor {
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
request.url = `/api${request.url}`;
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((data) => {
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/server-nest/src/interceptors/user-ip.interceptor.ts
Normal file
21
packages/server-nest/src/interceptors/user-ip.interceptor.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import {
|
||||||
|
CallHandler,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserIpInterceptor implements NestInterceptor {
|
||||||
|
constructor(private readonly cls: ClsService) {}
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const userIp = request.connection.remoteAddress;
|
||||||
|
this.cls.set('ip', userIp);
|
||||||
|
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
174
packages/server-nest/src/interfaces/Account.ts
Normal file
174
packages/server-nest/src/interfaces/Account.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
export interface IAccountDTO {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
accountType: string;
|
||||||
|
parentAccountId?: number;
|
||||||
|
active: boolean;
|
||||||
|
bankBalance?: number;
|
||||||
|
accountMask?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountCreateDTO extends IAccountDTO {
|
||||||
|
currencyCode?: string;
|
||||||
|
plaidAccountId?: string;
|
||||||
|
plaidItemId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountEditDTO extends IAccountDTO {}
|
||||||
|
|
||||||
|
export interface IAccount {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
code: string;
|
||||||
|
index: number;
|
||||||
|
description: string;
|
||||||
|
accountType: string;
|
||||||
|
parentAccountId: number;
|
||||||
|
active: boolean;
|
||||||
|
predefined: boolean;
|
||||||
|
amount: number;
|
||||||
|
currencyCode: string;
|
||||||
|
transactions?: any[];
|
||||||
|
type?: any[];
|
||||||
|
accountNormal: string;
|
||||||
|
accountParentType: string;
|
||||||
|
bankBalance: string;
|
||||||
|
plaidItemId: number | null;
|
||||||
|
lastFeedsUpdatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AccountNormal {
|
||||||
|
DEBIT = 'debit',
|
||||||
|
CREDIT = 'credit',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountsTransactionsFilter {
|
||||||
|
accountId?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountTransaction {
|
||||||
|
id?: number;
|
||||||
|
|
||||||
|
credit: number;
|
||||||
|
debit: number;
|
||||||
|
currencyCode: string;
|
||||||
|
exchangeRate: number;
|
||||||
|
|
||||||
|
accountId: number;
|
||||||
|
contactId?: number | null;
|
||||||
|
date: string | Date;
|
||||||
|
|
||||||
|
referenceType: string;
|
||||||
|
referenceTypeFormatted: string;
|
||||||
|
referenceId: number;
|
||||||
|
|
||||||
|
referenceNumber?: string;
|
||||||
|
|
||||||
|
transactionNumber?: string;
|
||||||
|
transactionType?: string;
|
||||||
|
|
||||||
|
note?: string;
|
||||||
|
|
||||||
|
index: number;
|
||||||
|
indexGroup?: number;
|
||||||
|
|
||||||
|
costable?: boolean;
|
||||||
|
|
||||||
|
userId?: number;
|
||||||
|
itemId?: number;
|
||||||
|
branchId?: number;
|
||||||
|
projectId?: number;
|
||||||
|
|
||||||
|
account?: IAccount;
|
||||||
|
|
||||||
|
taxRateId?: number;
|
||||||
|
taxRate?: number;
|
||||||
|
}
|
||||||
|
export interface IAccountResponse extends IAccount {}
|
||||||
|
|
||||||
|
export enum IAccountsStructureType {
|
||||||
|
Tree = 'tree',
|
||||||
|
Flat = 'flat',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountsFilter {
|
||||||
|
stringifiedFilterRoles?: string;
|
||||||
|
onlyInactive: boolean;
|
||||||
|
structure?: IAccountsStructureType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountType {
|
||||||
|
label: string;
|
||||||
|
key: string;
|
||||||
|
normal: string;
|
||||||
|
rootType: string;
|
||||||
|
childType: string;
|
||||||
|
balanceSheet: boolean;
|
||||||
|
incomeSheet: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountsTypesService {
|
||||||
|
getAccountsTypes(tenantId: number): Promise<IAccountType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountEventCreatingPayload {
|
||||||
|
tenantId: number;
|
||||||
|
accountDTO: any;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
export interface IAccountEventCreatedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
account: IAccount;
|
||||||
|
accountId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountEventEditedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
account: IAccount;
|
||||||
|
oldAccount: IAccount;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountEventDeletedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
accountId: number;
|
||||||
|
oldAccount: IAccount;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountEventDeletePayload {
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
oldAccount: IAccount;
|
||||||
|
tenantId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccountEventActivatedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
accountId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AccountAction {
|
||||||
|
CREATE = 'Create',
|
||||||
|
EDIT = 'Edit',
|
||||||
|
DELETE = 'Delete',
|
||||||
|
VIEW = 'View',
|
||||||
|
TransactionsLocking = 'TransactionsLocking',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TaxRateAction {
|
||||||
|
CREATE = 'Create',
|
||||||
|
EDIT = 'Edit',
|
||||||
|
DELETE = 'Delete',
|
||||||
|
VIEW = 'View',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAccountParams {
|
||||||
|
ignoreUniqueName: boolean;
|
||||||
|
}
|
||||||
165
packages/server-nest/src/interfaces/Item.ts
Normal file
165
packages/server-nest/src/interfaces/Item.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Item } from 'src/modules/Items/models/Item';
|
||||||
|
// import { AbilitySubject } from '@/interfaces';
|
||||||
|
// import { IFilterRole } from '@/interfaces/DynamicFilter';
|
||||||
|
|
||||||
|
export interface IItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
sellable: boolean;
|
||||||
|
purchasable: boolean;
|
||||||
|
|
||||||
|
costPrice: number;
|
||||||
|
sellPrice: number;
|
||||||
|
currencyCode: string;
|
||||||
|
|
||||||
|
costAccountId: number;
|
||||||
|
sellAccountId: number;
|
||||||
|
inventoryAccountId: number;
|
||||||
|
|
||||||
|
sellDescription: string;
|
||||||
|
purchaseDescription: string;
|
||||||
|
|
||||||
|
sellTaxRateId: number;
|
||||||
|
purchaseTaxRateId: number;
|
||||||
|
|
||||||
|
quantityOnHand: number;
|
||||||
|
|
||||||
|
note: string;
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
categoryId: number;
|
||||||
|
userId: number;
|
||||||
|
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IItemDTO {
|
||||||
|
@ApiProperty()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
sellable: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
purchasable: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
costPrice: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
sellPrice: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
currencyCode: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
costAccountId: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
sellAccountId: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
inventoryAccountId: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
sellDescription: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
purchaseDescription: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
sellTaxRateId: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
purchaseTaxRateId: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
quantityOnHand: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
note: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
categoryId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IItemCreateDTO extends IItemDTO {}
|
||||||
|
export interface IItemEditDTO extends IItemDTO {}
|
||||||
|
|
||||||
|
// export interface IItemsService {
|
||||||
|
// getItem(tenantId: number, itemId: number): Promise<IItem>;
|
||||||
|
// deleteItem(tenantId: number, itemId: number): Promise<void>;
|
||||||
|
// editItem(tenantId: number, itemId: number, itemDTO: IItemDTO): Promise<IItem>;
|
||||||
|
// newItem(tenantId: number, itemDTO: IItemDTO): Promise<IItem>;
|
||||||
|
// itemsList(
|
||||||
|
// tenantId: number,
|
||||||
|
// itemsFilter: IItemsFilter,
|
||||||
|
// ): Promise<{ items: IItem[] }>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface IItemsFilter extends IDynamicListFilterDTO {
|
||||||
|
// stringifiedFilterRoles?: string;
|
||||||
|
// page: number;
|
||||||
|
// pageSize: number;
|
||||||
|
// inactiveMode: boolean;
|
||||||
|
// viewSlug?: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface IItemsAutoCompleteFilter {
|
||||||
|
// limit: number;
|
||||||
|
// keyword: string;
|
||||||
|
// filterRoles?: IFilterRole[];
|
||||||
|
// columnSortBy: string;
|
||||||
|
// sortOrder: string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface IItemEventCreatedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
item: Item;
|
||||||
|
itemId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IItemEventEditedPayload {
|
||||||
|
item: Item;
|
||||||
|
oldItem: Item;
|
||||||
|
itemId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IItemEventDeletingPayload {
|
||||||
|
tenantId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
oldItem: IItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IItemEventDeletedPayload {
|
||||||
|
tenantId: number;
|
||||||
|
oldItem: IItem;
|
||||||
|
itemId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ItemAction {
|
||||||
|
CREATE = 'Create',
|
||||||
|
EDIT = 'Edit',
|
||||||
|
DELETE = 'Delete',
|
||||||
|
VIEW = 'View',
|
||||||
|
}
|
||||||
|
|
||||||
|
// export type ItemAbility = [ItemAction, AbilitySubject.Item];
|
||||||
192
packages/server-nest/src/interfaces/Model.ts
Normal file
192
packages/server-nest/src/interfaces/Model.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
export interface IModel {
|
||||||
|
name: string;
|
||||||
|
tableName: string;
|
||||||
|
fields: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFilterMeta {
|
||||||
|
sortOrder: string;
|
||||||
|
sortBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPaginationMeta {
|
||||||
|
pageSize: number;
|
||||||
|
page: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaDefaultSort {
|
||||||
|
sortOrder: ISortOrder;
|
||||||
|
sortField: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IModelColumnType =
|
||||||
|
| 'text'
|
||||||
|
| 'number'
|
||||||
|
| 'enumeration'
|
||||||
|
| 'boolean'
|
||||||
|
| 'relation';
|
||||||
|
|
||||||
|
export type ISortOrder = 'DESC' | 'ASC';
|
||||||
|
|
||||||
|
export interface IModelMetaFieldCommon {
|
||||||
|
name: string;
|
||||||
|
column: string;
|
||||||
|
columnable?: boolean;
|
||||||
|
customQuery?: Function;
|
||||||
|
required?: boolean;
|
||||||
|
importHint?: string;
|
||||||
|
importableRelationLabel?: string;
|
||||||
|
order?: number;
|
||||||
|
unique?: number;
|
||||||
|
dataTransferObjectKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaFieldText {
|
||||||
|
fieldType: 'text';
|
||||||
|
minLength?: number;
|
||||||
|
maxLength?: number;
|
||||||
|
}
|
||||||
|
export interface IModelMetaFieldBoolean {
|
||||||
|
fieldType: 'boolean';
|
||||||
|
}
|
||||||
|
export interface IModelMetaFieldNumber {
|
||||||
|
fieldType: 'number';
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
export interface IModelMetaFieldDate {
|
||||||
|
fieldType: 'date';
|
||||||
|
}
|
||||||
|
export interface IModelMetaFieldUrl {
|
||||||
|
fieldType: 'url';
|
||||||
|
}
|
||||||
|
export type IModelMetaField = IModelMetaFieldCommon &
|
||||||
|
(
|
||||||
|
| IModelMetaFieldText
|
||||||
|
| IModelMetaFieldNumber
|
||||||
|
| IModelMetaFieldBoolean
|
||||||
|
| IModelMetaFieldDate
|
||||||
|
| IModelMetaFieldUrl
|
||||||
|
| IModelMetaEnumerationField
|
||||||
|
| IModelMetaRelationField
|
||||||
|
| IModelMetaCollectionField
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface IModelMetaEnumerationOption {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaEnumerationField {
|
||||||
|
fieldType: 'enumeration';
|
||||||
|
options: IModelMetaEnumerationOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaRelationFieldCommon {
|
||||||
|
fieldType: 'relation';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaRelationEnumerationField {
|
||||||
|
relationType: 'enumeration';
|
||||||
|
relationKey: string;
|
||||||
|
relationEntityLabel: string;
|
||||||
|
relationEntityKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaFieldWithFields {
|
||||||
|
fields: IModelMetaFieldCommon2 &
|
||||||
|
(
|
||||||
|
| IModelMetaFieldText
|
||||||
|
| IModelMetaFieldNumber
|
||||||
|
| IModelMetaFieldBoolean
|
||||||
|
| IModelMetaFieldDate
|
||||||
|
| IModelMetaFieldUrl
|
||||||
|
| IModelMetaEnumerationField
|
||||||
|
| IModelMetaRelationField
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IModelMetaCollectionObjectField extends IModelMetaFieldWithFields {
|
||||||
|
collectionOf: 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaCollectionFieldCommon {
|
||||||
|
fieldType: 'collection';
|
||||||
|
collectionMinLength?: number;
|
||||||
|
collectionMaxLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
|
||||||
|
IModelMetaCollectionObjectField;
|
||||||
|
|
||||||
|
export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
|
||||||
|
IModelMetaRelationEnumerationField;
|
||||||
|
|
||||||
|
interface IModelPrintMeta {
|
||||||
|
pageTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMeta {
|
||||||
|
defaultFilterField: string;
|
||||||
|
defaultSort: IModelMetaDefaultSort;
|
||||||
|
|
||||||
|
exportable?: boolean;
|
||||||
|
exportFlattenOn?: string;
|
||||||
|
|
||||||
|
importable?: boolean;
|
||||||
|
importAggregator?: string;
|
||||||
|
importAggregateOn?: string;
|
||||||
|
importAggregateBy?: string;
|
||||||
|
|
||||||
|
print?: IModelPrintMeta;
|
||||||
|
|
||||||
|
fields: { [key: string]: IModelMetaField };
|
||||||
|
columns: { [key: string]: IModelMetaColumn };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----
|
||||||
|
export interface IModelMetaFieldCommon2 {
|
||||||
|
name: string;
|
||||||
|
required?: boolean;
|
||||||
|
importHint?: string;
|
||||||
|
order?: number;
|
||||||
|
unique?: number;
|
||||||
|
features?: Array<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IModelMetaRelationField2 {
|
||||||
|
fieldType: 'relation';
|
||||||
|
relationModel: string;
|
||||||
|
importableRelationLabel: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IModelMetaField2 = IModelMetaFieldCommon2 &
|
||||||
|
(
|
||||||
|
| IModelMetaFieldText
|
||||||
|
| IModelMetaFieldNumber
|
||||||
|
| IModelMetaFieldBoolean
|
||||||
|
| IModelMetaFieldDate
|
||||||
|
| IModelMetaFieldUrl
|
||||||
|
| IModelMetaEnumerationField
|
||||||
|
| IModelMetaRelationField2
|
||||||
|
| IModelMetaCollectionField
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ImodelMetaColumnMeta {
|
||||||
|
name: string;
|
||||||
|
accessor?: string;
|
||||||
|
exportable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IModelMetaColumnText {
|
||||||
|
type: 'text;';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IModelMetaColumnCollection {
|
||||||
|
type: 'collection';
|
||||||
|
collectionOf: 'object';
|
||||||
|
columns: { [key: string]: ImodelMetaColumnMeta & IModelMetaColumnText };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IModelMetaColumn = ImodelMetaColumnMeta &
|
||||||
|
(IModelMetaColumnText | IModelMetaColumnCollection);
|
||||||
8
packages/server-nest/src/interfaces/SubscriptionPlan.ts
Normal file
8
packages/server-nest/src/interfaces/SubscriptionPlan.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface SubscriptionPayload {
|
||||||
|
lemonSqueezyId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SubscriptionPaymentStatus {
|
||||||
|
Succeed = 'succeed',
|
||||||
|
Failed = 'failed',
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { get } from 'lodash';
|
||||||
|
import { ACCOUNT_TYPES } from '@/constants/accounts';
|
||||||
|
|
||||||
|
export class AccountTypesUtils {
|
||||||
|
/**
|
||||||
|
* Retrieve account types list.
|
||||||
|
*/
|
||||||
|
static getList() {
|
||||||
|
return ACCOUNT_TYPES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve accounts types by the given root type.
|
||||||
|
* @param {string} rootType -
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
static getTypesByRootType(rootType: string) {
|
||||||
|
return ACCOUNT_TYPES.filter((type) => type.rootType === rootType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve account type by the given account type key.
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} accessor
|
||||||
|
*/
|
||||||
|
static getType(key: string, accessor?: string) {
|
||||||
|
const type = ACCOUNT_TYPES.find((type) => type.key === key);
|
||||||
|
|
||||||
|
if (accessor) {
|
||||||
|
return get(type, accessor);
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve accounts types by the parent account type.
|
||||||
|
* @param {string} parentType
|
||||||
|
*/
|
||||||
|
static getTypesByParentType(parentType: string) {
|
||||||
|
return ACCOUNT_TYPES.filter((type) => type.parentType === parentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve accounts types by the given account normal.
|
||||||
|
* @param {string} normal
|
||||||
|
*/
|
||||||
|
static getTypesByNormal(normal: string) {
|
||||||
|
return ACCOUNT_TYPES.filter((type) => type.normal === normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the root type equals the account type.
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} rootType
|
||||||
|
*/
|
||||||
|
static isRootTypeEqualsKey(key: string, rootType: string): boolean {
|
||||||
|
return ACCOUNT_TYPES.some((type) => {
|
||||||
|
const isType = type.key === key;
|
||||||
|
const isRootType = type.rootType === rootType;
|
||||||
|
|
||||||
|
return isType && isRootType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the parent account type equals the account type key.
|
||||||
|
* @param {string} key - Account type key.
|
||||||
|
* @param {string} parentType - Account parent type.
|
||||||
|
*/
|
||||||
|
static isParentTypeEqualsKey(key: string, parentType: string): boolean {
|
||||||
|
return ACCOUNT_TYPES.some((type) => {
|
||||||
|
const isType = type.key === key;
|
||||||
|
const isParentType = type.parentType === parentType;
|
||||||
|
|
||||||
|
return isType && isParentType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether account type has balance sheet.
|
||||||
|
* @param {string} key - Account type key.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
static isTypeBalanceSheet(key: string): boolean {
|
||||||
|
return ACCOUNT_TYPES.some((type) => {
|
||||||
|
const isType = type.key === key;
|
||||||
|
return isType && type.balanceSheet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether account type has profit/loss sheet.
|
||||||
|
* @param {string} key - Account type key.
|
||||||
|
*/
|
||||||
|
static isTypePLSheet(key: string): boolean {
|
||||||
|
return ACCOUNT_TYPES.some((type) => {
|
||||||
|
const isType = type.key === key;
|
||||||
|
return isType && type.incomeSheet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
350
packages/server-nest/src/libs/dependency-graph/index.ts
Normal file
350
packages/server-nest/src/libs/dependency-graph/index.ts
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* A simple dependency graph
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Helper for creating a Topological Sort using Depth-First-Search on a set of edges.
|
||||||
|
*
|
||||||
|
* Detects cycles and throws an Error if one is detected (unless the "circular"
|
||||||
|
* parameter is "true" in which case it ignores them).
|
||||||
|
*
|
||||||
|
* @param edges The set of edges to DFS through
|
||||||
|
* @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges)
|
||||||
|
* @param result An array in which the results will be populated
|
||||||
|
* @param circular A boolean to allow circular dependencies
|
||||||
|
*/
|
||||||
|
function createDFS(edges, leavesOnly, result, circular) {
|
||||||
|
var visited = {};
|
||||||
|
return function (start) {
|
||||||
|
if (visited[start]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var inCurrentPath = {};
|
||||||
|
var currentPath = [];
|
||||||
|
var todo = []; // used as a stack
|
||||||
|
todo.push({ node: start, processed: false });
|
||||||
|
while (todo.length > 0) {
|
||||||
|
var current = todo[todo.length - 1]; // peek at the todo stack
|
||||||
|
var processed = current.processed;
|
||||||
|
var node = current.node;
|
||||||
|
if (!processed) {
|
||||||
|
// Haven't visited edges yet (visiting phase)
|
||||||
|
if (visited[node]) {
|
||||||
|
todo.pop();
|
||||||
|
continue;
|
||||||
|
} else if (inCurrentPath[node]) {
|
||||||
|
// It's not a DAG
|
||||||
|
if (circular) {
|
||||||
|
todo.pop();
|
||||||
|
// If we're tolerating cycles, don't revisit the node
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
currentPath.push(node);
|
||||||
|
throw new DepGraphCycleError(currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
inCurrentPath[node] = true;
|
||||||
|
currentPath.push(node);
|
||||||
|
var nodeEdges = edges[node];
|
||||||
|
// (push edges onto the todo stack in reverse order to be order-compatible with the old DFS implementation)
|
||||||
|
for (var i = nodeEdges.length - 1; i >= 0; i--) {
|
||||||
|
todo.push({ node: nodeEdges[i], processed: false });
|
||||||
|
}
|
||||||
|
current.processed = true;
|
||||||
|
} else {
|
||||||
|
// Have visited edges (stack unrolling phase)
|
||||||
|
todo.pop();
|
||||||
|
currentPath.pop();
|
||||||
|
inCurrentPath[node] = false;
|
||||||
|
visited[node] = true;
|
||||||
|
if (!leavesOnly || edges[node].length === 0) {
|
||||||
|
result.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Dependency Graph
|
||||||
|
*/
|
||||||
|
var DepGraph = (DepGraph = function DepGraph(opts) {
|
||||||
|
this.nodes = {}; // Node -> Node/Data (treated like a Set)
|
||||||
|
this.outgoingEdges = {}; // Node -> [Dependency Node]
|
||||||
|
this.incomingEdges = {}; // Node -> [Dependant Node]
|
||||||
|
this.circular = opts && !!opts.circular; // Allows circular deps
|
||||||
|
});
|
||||||
|
|
||||||
|
DepGraph.fromArray = (
|
||||||
|
items,
|
||||||
|
options = { itemId: 'id', parentItemId: 'parent_id' }
|
||||||
|
) => {
|
||||||
|
const depGraph = new DepGraph();
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
depGraph.addNode(item[options.itemId], item);
|
||||||
|
});
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item[options.parentItemId]) {
|
||||||
|
depGraph.addDependency(item[options.parentItemId], item[options.itemId]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return depGraph;
|
||||||
|
};
|
||||||
|
|
||||||
|
DepGraph.prototype = {
|
||||||
|
/**
|
||||||
|
* The number of nodes in the graph.
|
||||||
|
*/
|
||||||
|
size: function () {
|
||||||
|
return Object.keys(this.nodes).length;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Add a node to the dependency graph. If a node already exists, this method will do nothing.
|
||||||
|
*/
|
||||||
|
addNode: function (node, data) {
|
||||||
|
if (!this.hasNode(node)) {
|
||||||
|
// Checking the arguments length allows the user to add a node with undefined data
|
||||||
|
if (arguments.length === 2) {
|
||||||
|
this.nodes[node] = data;
|
||||||
|
} else {
|
||||||
|
this.nodes[node] = node;
|
||||||
|
}
|
||||||
|
this.outgoingEdges[node] = [];
|
||||||
|
this.incomingEdges[node] = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Remove a node from the dependency graph. If a node does not exist, this method will do nothing.
|
||||||
|
*/
|
||||||
|
removeNode: function (node) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
delete this.nodes[node];
|
||||||
|
delete this.outgoingEdges[node];
|
||||||
|
delete this.incomingEdges[node];
|
||||||
|
[this.incomingEdges, this.outgoingEdges].forEach(function (edgeList) {
|
||||||
|
Object.keys(edgeList).forEach(function (key) {
|
||||||
|
var idx = edgeList[key].indexOf(node);
|
||||||
|
if (idx >= 0) {
|
||||||
|
edgeList[key].splice(idx, 1);
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Check if a node exists in the graph
|
||||||
|
*/
|
||||||
|
hasNode: function (node) {
|
||||||
|
return this.nodes.hasOwnProperty(node);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the data associated with a node name
|
||||||
|
*/
|
||||||
|
getNodeData: function (node) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
return this.nodes[node];
|
||||||
|
} else {
|
||||||
|
throw new Error('Node does not exist: ' + node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the associated data for a given node name. If the node does not exist, this method will throw an error
|
||||||
|
*/
|
||||||
|
setNodeData: function (node, data) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
this.nodes[node] = data;
|
||||||
|
} else {
|
||||||
|
throw new Error('Node does not exist: ' + node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Add a dependency between two nodes. If either of the nodes does not exist,
|
||||||
|
* an Error will be thrown.
|
||||||
|
*/
|
||||||
|
addDependency: function (from, to) {
|
||||||
|
if (!this.hasNode(from)) {
|
||||||
|
throw new Error('Node does not exist: ' + from);
|
||||||
|
}
|
||||||
|
if (!this.hasNode(to)) {
|
||||||
|
throw new Error('Node does not exist: ' + to);
|
||||||
|
}
|
||||||
|
if (this.outgoingEdges[from].indexOf(to) === -1) {
|
||||||
|
this.outgoingEdges[from].push(to);
|
||||||
|
}
|
||||||
|
if (this.incomingEdges[to].indexOf(from) === -1) {
|
||||||
|
this.incomingEdges[to].push(from);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Remove a dependency between two nodes.
|
||||||
|
*/
|
||||||
|
removeDependency: function (from, to) {
|
||||||
|
var idx;
|
||||||
|
if (this.hasNode(from)) {
|
||||||
|
idx = this.outgoingEdges[from].indexOf(to);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.outgoingEdges[from].splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasNode(to)) {
|
||||||
|
idx = this.incomingEdges[to].indexOf(from);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.incomingEdges[to].splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Return a clone of the dependency graph. If any custom data is attached
|
||||||
|
* to the nodes, it will only be shallow copied.
|
||||||
|
*/
|
||||||
|
clone: function () {
|
||||||
|
var source = this;
|
||||||
|
var result = new DepGraph();
|
||||||
|
var keys = Object.keys(source.nodes);
|
||||||
|
keys.forEach(function (n) {
|
||||||
|
result.nodes[n] = source.nodes[n];
|
||||||
|
result.outgoingEdges[n] = source.outgoingEdges[n].slice(0);
|
||||||
|
result.incomingEdges[n] = source.incomingEdges[n].slice(0);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get an array containing the nodes that the specified node depends on (transitively).
|
||||||
|
*
|
||||||
|
* Throws an Error if the graph has a cycle, or the specified node does not exist.
|
||||||
|
*
|
||||||
|
* If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned
|
||||||
|
* in the array.
|
||||||
|
*/
|
||||||
|
dependenciesOf: function (node, leavesOnly) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
var result = [];
|
||||||
|
var DFS = createDFS(
|
||||||
|
this.outgoingEdges,
|
||||||
|
leavesOnly,
|
||||||
|
result,
|
||||||
|
this.circular
|
||||||
|
);
|
||||||
|
DFS(node);
|
||||||
|
var idx = result.indexOf(node);
|
||||||
|
if (idx >= 0) {
|
||||||
|
result.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new Error('Node does not exist: ' + node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* get an array containing the nodes that depend on the specified node (transitively).
|
||||||
|
*
|
||||||
|
* Throws an Error if the graph has a cycle, or the specified node does not exist.
|
||||||
|
*
|
||||||
|
* If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
|
||||||
|
*/
|
||||||
|
dependantsOf: function (node, leavesOnly) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
var result = [];
|
||||||
|
var DFS = createDFS(
|
||||||
|
this.incomingEdges,
|
||||||
|
leavesOnly,
|
||||||
|
result,
|
||||||
|
this.circular
|
||||||
|
);
|
||||||
|
DFS(node);
|
||||||
|
var idx = result.indexOf(node);
|
||||||
|
if (idx >= 0) {
|
||||||
|
result.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new Error('Node does not exist: ' + node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Construct the overall processing order for the dependency graph.
|
||||||
|
*
|
||||||
|
* Throws an Error if the graph has a cycle.
|
||||||
|
*
|
||||||
|
* If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned.
|
||||||
|
*/
|
||||||
|
overallOrder: function (leavesOnly) {
|
||||||
|
var self = this;
|
||||||
|
var result = [];
|
||||||
|
var keys = Object.keys(this.nodes);
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return result; // Empty graph
|
||||||
|
} else {
|
||||||
|
if (!this.circular) {
|
||||||
|
// Look for cycles - we run the DFS starting at all the nodes in case there
|
||||||
|
// are several disconnected subgraphs inside this dependency graph.
|
||||||
|
var CycleDFS = createDFS(this.outgoingEdges, false, [], this.circular);
|
||||||
|
keys.forEach(function (n) {
|
||||||
|
CycleDFS(n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var DFS = createDFS(
|
||||||
|
this.outgoingEdges,
|
||||||
|
leavesOnly,
|
||||||
|
result,
|
||||||
|
this.circular
|
||||||
|
);
|
||||||
|
// Find all potential starting points (nodes with nothing depending on them) an
|
||||||
|
// run a DFS starting at these points to get the order
|
||||||
|
keys
|
||||||
|
.filter(function (node) {
|
||||||
|
return self.incomingEdges[node].length === 0;
|
||||||
|
})
|
||||||
|
.forEach(function (n) {
|
||||||
|
DFS(n);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we're allowing cycles - we need to run the DFS against any remaining
|
||||||
|
// nodes that did not end up in the initial result (as they are part of a
|
||||||
|
// subgraph that does not have a clear starting point)
|
||||||
|
if (this.circular) {
|
||||||
|
keys
|
||||||
|
.filter(function (node) {
|
||||||
|
return result.indexOf(node) === -1;
|
||||||
|
})
|
||||||
|
.forEach(function (n) {
|
||||||
|
DFS(n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mapNodes(mapper) {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycle error, including the path of the cycle.
|
||||||
|
*/
|
||||||
|
var DepGraphCycleError = (exports.DepGraphCycleError = function (cyclePath) {
|
||||||
|
var message = 'Dependency Cycle Found: ' + cyclePath.join(' -> ');
|
||||||
|
var instance = new Error(message);
|
||||||
|
instance.cyclePath = cyclePath;
|
||||||
|
Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(instance, DepGraphCycleError);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
});
|
||||||
|
DepGraphCycleError.prototype = Object.create(Error.prototype, {
|
||||||
|
constructor: {
|
||||||
|
value: Error,
|
||||||
|
enumerable: false,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.setPrototypeOf(DepGraphCycleError, Error);
|
||||||
|
|
||||||
|
export default DepGraph;
|
||||||
25
packages/server-nest/src/main.ts
Normal file
25
packages/server-nest/src/main.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
import { AppModule } from './modules/App/App.module';
|
||||||
|
import { ClsMiddleware } from 'nestjs-cls';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.setGlobalPrefix('/api');
|
||||||
|
|
||||||
|
// create and mount the middleware manually here
|
||||||
|
app.use(new ClsMiddleware({}).use);
|
||||||
|
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('Bigcapital')
|
||||||
|
.setDescription('Financial accounting software')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.addTag('cats')
|
||||||
|
.build();
|
||||||
|
|
||||||
|
const documentFactory = () => SwaggerModule.createDocument(app, config);
|
||||||
|
SwaggerModule.setup('swagger', app, documentFactory);
|
||||||
|
|
||||||
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
13
packages/server-nest/src/middleware/logger.middleware.ts
Normal file
13
packages/server-nest/src/middleware/logger.middleware.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoggerMiddleware implements NestMiddleware {
|
||||||
|
use(req: Request, res: Response, next: NextFunction) {
|
||||||
|
console.log(`Request...`);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
req.test = 'test';
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/server-nest/src/models/Model.ts
Normal file
5
packages/server-nest/src/models/Model.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Model } from 'objection';
|
||||||
|
|
||||||
|
export class BaseModel extends Model {
|
||||||
|
public readonly id: number;
|
||||||
|
}
|
||||||
441
packages/server-nest/src/modules/Accounts/models/Account.ts
Normal file
441
packages/server-nest/src/modules/Accounts/models/Account.ts
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
/* eslint-disable global-require */
|
||||||
|
import { mixin, Model } from 'objection';
|
||||||
|
import { castArray } from 'lodash';
|
||||||
|
import DependencyGraph from '@/libs/dependency-graph';
|
||||||
|
import {
|
||||||
|
ACCOUNT_TYPES,
|
||||||
|
getAccountsSupportsMultiCurrency,
|
||||||
|
} from 'src/constants/accounts';
|
||||||
|
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||||
|
import { SearchableModel } from '@/modules/Search/SearchableMdel';
|
||||||
|
import { CustomViewBaseModel } from '@/modules/CustomViews/CustomViewBaseModel';
|
||||||
|
import { ModelSettings } from '@/modules/Settings/ModelSettings';
|
||||||
|
import { AccountTypesUtils } from '@/libs/accounts-utils/AccountTypesUtils';
|
||||||
|
// import AccountSettings from './Account.Settings';
|
||||||
|
// import { DEFAULT_VIEWS } from '@/modules/Accounts/constants';
|
||||||
|
// import { buildFilterQuery, buildSortColumnQuery } from '@/lib/ViewRolesBuilder';
|
||||||
|
// import { flatToNestedArray } from 'utils';
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
export class Account extends mixin(TenantModel, [
|
||||||
|
ModelSettings,
|
||||||
|
CustomViewBaseModel,
|
||||||
|
SearchableModel,
|
||||||
|
]) {
|
||||||
|
accountType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'accounts';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
static get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return [
|
||||||
|
'accountTypeLabel',
|
||||||
|
'accountParentType',
|
||||||
|
'accountRootType',
|
||||||
|
'accountNormal',
|
||||||
|
'accountNormalFormatted',
|
||||||
|
'isBalanceSheetAccount',
|
||||||
|
'isPLSheet',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account normal.
|
||||||
|
*/
|
||||||
|
get accountNormal() {
|
||||||
|
return AccountTypesUtils.getType(this.accountType, 'normal');
|
||||||
|
}
|
||||||
|
|
||||||
|
get accountNormalFormatted() {
|
||||||
|
const paris = {
|
||||||
|
credit: 'Credit',
|
||||||
|
debit: 'Debit',
|
||||||
|
};
|
||||||
|
return paris[this.accountNormal] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve account type label.
|
||||||
|
*/
|
||||||
|
get accountTypeLabel() {
|
||||||
|
return AccountTypesUtils.getType(this.accountType, 'label');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve account parent type.
|
||||||
|
*/
|
||||||
|
get accountParentType() {
|
||||||
|
return AccountTypesUtils.getType(this.accountType, 'parentType');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve account root type.
|
||||||
|
*/
|
||||||
|
get accountRootType() {
|
||||||
|
return AccountTypesUtils.getType(this.accountType, 'rootType');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve whether the account is balance sheet account.
|
||||||
|
*/
|
||||||
|
get isBalanceSheetAccount() {
|
||||||
|
return this.isBalanceSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve whether the account is profit/loss sheet account.
|
||||||
|
*/
|
||||||
|
get isPLSheet() {
|
||||||
|
return this.isProfitLossSheet();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Allows to mark model as resourceable to viewable and filterable.
|
||||||
|
*/
|
||||||
|
static get resourceable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
const TABLE_NAME = Account.tableName;
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Inactive/Active mode.
|
||||||
|
*/
|
||||||
|
inactiveMode(query, active = false) {
|
||||||
|
query.where('accounts.active', !active);
|
||||||
|
},
|
||||||
|
|
||||||
|
filterAccounts(query, accountIds) {
|
||||||
|
if (accountIds.length > 0) {
|
||||||
|
query.whereIn(`${TABLE_NAME}.id`, accountIds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filterAccountTypes(query, typesIds) {
|
||||||
|
if (typesIds.length > 0) {
|
||||||
|
query.whereIn('account_types.account_type_id', typesIds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewRolesBuilder(query, conditionals, expression) {
|
||||||
|
// buildFilterQuery(Account.tableName, conditionals, expression)(query);
|
||||||
|
},
|
||||||
|
sortColumnBuilder(query, columnKey, direction) {
|
||||||
|
// buildSortColumnQuery(Account.tableName, columnKey, direction)(query);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by root type.
|
||||||
|
*/
|
||||||
|
filterByRootType(query, rootType) {
|
||||||
|
const filterTypes = ACCOUNT_TYPES.filter(
|
||||||
|
(accountType) => accountType.rootType === rootType,
|
||||||
|
).map((accountType) => accountType.key);
|
||||||
|
|
||||||
|
query.whereIn('account_type', filterTypes);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by account normal
|
||||||
|
*/
|
||||||
|
filterByAccountNormal(query, accountNormal) {
|
||||||
|
const filterTypes = ACCOUNT_TYPES.filter(
|
||||||
|
(accountType) => accountType.normal === accountNormal,
|
||||||
|
).map((accountType) => accountType.key);
|
||||||
|
|
||||||
|
query.whereIn('account_type', filterTypes);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds account by the given slug.
|
||||||
|
* @param {*} query
|
||||||
|
* @param {*} slug
|
||||||
|
*/
|
||||||
|
findBySlug(query, slug) {
|
||||||
|
query.where('slug', slug).first();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} query
|
||||||
|
* @param {*} baseCyrrency
|
||||||
|
*/
|
||||||
|
preventMutateBaseCurrency(query) {
|
||||||
|
const accountsTypes = getAccountsSupportsMultiCurrency();
|
||||||
|
const accountsTypesKeys = accountsTypes.map((type) => type.key);
|
||||||
|
|
||||||
|
query
|
||||||
|
.whereIn('accountType', accountsTypesKeys)
|
||||||
|
.where('seededAt', null)
|
||||||
|
.first();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
// const AccountTransaction = require('models/AccountTransaction');
|
||||||
|
// const Item = require('models/Item');
|
||||||
|
// const InventoryAdjustment = require('models/InventoryAdjustment');
|
||||||
|
// const ManualJournalEntry = require('models/ManualJournalEntry');
|
||||||
|
// const Expense = require('models/Expense');
|
||||||
|
// const ExpenseEntry = require('models/ExpenseCategory');
|
||||||
|
// const ItemEntry = require('models/ItemEntry');
|
||||||
|
// const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
|
||||||
|
// const PlaidItem = require('models/PlaidItem');
|
||||||
|
|
||||||
|
return {
|
||||||
|
// /**
|
||||||
|
// * Account model may has many transactions.
|
||||||
|
// */
|
||||||
|
// transactions: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: AccountTransaction.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'accounts_transactions.accountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// itemsCostAccount: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: Item.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'items.costAccountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// itemsSellAccount: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: Item.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'items.sellAccountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// inventoryAdjustments: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: InventoryAdjustment.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'inventory_adjustments.adjustmentAccountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// manualJournalEntries: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: ManualJournalEntry.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'manual_journals_entries.accountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// expensePayments: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: Expense.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'expenses_transactions.paymentAccountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// expenseEntries: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: ExpenseEntry.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'expense_transaction_categories.expenseAccountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// entriesCostAccount: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: ItemEntry.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'items_entries.costAccountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// */
|
||||||
|
// entriesSellAccount: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: ItemEntry.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'items_entries.sellAccountId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// * Associated uncategorized transactions.
|
||||||
|
// */
|
||||||
|
// uncategorizedTransactions: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: UncategorizedTransaction.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.id',
|
||||||
|
// to: 'uncategorized_cashflow_transactions.accountId',
|
||||||
|
// },
|
||||||
|
// filter: (query) => {
|
||||||
|
// query.where('categorized', false);
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// /**
|
||||||
|
// * Account model may belongs to a Plaid item.
|
||||||
|
// */
|
||||||
|
// plaidItem: {
|
||||||
|
// relation: Model.BelongsToOneRelation,
|
||||||
|
// modelClass: PlaidItem.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'accounts.plaidItemId',
|
||||||
|
// to: 'plaid_items.plaidItemId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the given type equals the account type.
|
||||||
|
* @param {string} accountType
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isAccountType(accountType) {
|
||||||
|
const types = castArray(accountType);
|
||||||
|
return types.indexOf(this.accountType) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the given root type equals the account type.
|
||||||
|
* @param {string} rootType
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isRootType(rootType) {
|
||||||
|
return AccountTypesUtils.isRootTypeEqualsKey(this.accountType, rootType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmine whether the given parent type equals the account type.
|
||||||
|
* @param {string} parentType
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isParentType(parentType) {
|
||||||
|
return AccountTypesUtils.isParentTypeEqualsKey(
|
||||||
|
this.accountType,
|
||||||
|
parentType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the account is balance sheet account.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isBalanceSheet() {
|
||||||
|
return AccountTypesUtils.isTypeBalanceSheet(this.accountType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the account is profit/loss account.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isProfitLossSheet() {
|
||||||
|
return AccountTypesUtils.isTypePLSheet(this.accountType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the account is income statement account
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
isIncomeSheet() {
|
||||||
|
return this.isProfitLossSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts flatten accounts list to nested array.
|
||||||
|
* @param {Array} accounts
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
static toNestedArray(accounts, options = { children: 'children' }) {
|
||||||
|
// return flatToNestedArray(accounts, {
|
||||||
|
// id: 'id',
|
||||||
|
// parentId: 'parentAccountId',
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the accounts list to depenedency graph structure.
|
||||||
|
* @param {IAccount[]} accounts
|
||||||
|
*/
|
||||||
|
static toDependencyGraph(accounts) {
|
||||||
|
return DependencyGraph.fromArray(accounts, {
|
||||||
|
itemId: 'id',
|
||||||
|
parentItemId: 'parentAccountId',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model settings.
|
||||||
|
*/
|
||||||
|
// static get meta() {
|
||||||
|
// return AccountSettings;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the default custom views, roles and columns.
|
||||||
|
*/
|
||||||
|
// static get defaultViews() {
|
||||||
|
// return DEFAULT_VIEWS;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model search roles.
|
||||||
|
*/
|
||||||
|
static get searchRoles() {
|
||||||
|
return [
|
||||||
|
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
|
||||||
|
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents mutate base currency since the model is not empty.
|
||||||
|
*/
|
||||||
|
static get preventMutateBaseCurrency() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/server-nest/src/modules/App/App.controller.spec.ts
Normal file
22
packages/server-nest/src/modules/App/App.controller.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './App.controller';
|
||||||
|
import { AppService } from './App.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
packages/server-nest/src/modules/App/App.controller.ts
Normal file
12
packages/server-nest/src/modules/App/App.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './App.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
||||||
115
packages/server-nest/src/modules/App/App.module.ts
Normal file
115
packages/server-nest/src/modules/App/App.module.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { join } from 'path';
|
||||||
|
import {
|
||||||
|
AcceptLanguageResolver,
|
||||||
|
CookieResolver,
|
||||||
|
HeaderResolver,
|
||||||
|
I18nModule,
|
||||||
|
QueryResolver,
|
||||||
|
} from 'nestjs-i18n';
|
||||||
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { ClsModule } from 'nestjs-cls';
|
||||||
|
import { AppController } from './App.controller';
|
||||||
|
import { AppService } from './App.service';
|
||||||
|
import { ItemsModule } from '../Items/items.module';
|
||||||
|
import { config } from '../../common/config';
|
||||||
|
import { SystemDatabaseModule } from '../System/SystemDB/SystemDB.module';
|
||||||
|
import { SystemModelsModule } from '../System/SystemModels/SystemModels.module';
|
||||||
|
import { JwtStrategy } from '../Auth/Jwt.strategy';
|
||||||
|
import { jwtConstants } from '../Auth/Auth.constants';
|
||||||
|
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||||
|
import { TenancyModelsModule } from '../Tenancy/TenancyModels/Tenancy.module';
|
||||||
|
import { LoggerMiddleware } from '@/middleware/logger.middleware';
|
||||||
|
import { ExcludeNullInterceptor } from '@/interceptors/ExcludeNull.interceptor';
|
||||||
|
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
import { JwtAuthGuard } from '../Auth/Jwt.guard';
|
||||||
|
import { UserIpInterceptor } from '@/interceptors/user-ip.interceptor';
|
||||||
|
import { TenancyGlobalMiddleware } from '../Tenancy/TenancyGlobal.middleware';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
envFilePath: '.env',
|
||||||
|
load: config,
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
SystemDatabaseModule,
|
||||||
|
SystemModelsModule,
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
I18nModule.forRootAsync({
|
||||||
|
useFactory: () => ({
|
||||||
|
fallbackLanguage: 'en',
|
||||||
|
loaderOptions: {
|
||||||
|
path: join(__dirname, '/../../i18n/'),
|
||||||
|
watch: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
resolvers: [
|
||||||
|
new QueryResolver(),
|
||||||
|
new HeaderResolver(),
|
||||||
|
new CookieResolver(),
|
||||||
|
AcceptLanguageResolver,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
PassportModule,
|
||||||
|
JwtModule.register({
|
||||||
|
secret: jwtConstants.secret,
|
||||||
|
signOptions: { expiresIn: '60s' },
|
||||||
|
}),
|
||||||
|
BullModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
connection: {
|
||||||
|
host: configService.get('QUEUE_HOST'),
|
||||||
|
port: configService.get('QUEUE_PORT'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
ClsModule.forRoot({
|
||||||
|
global: true,
|
||||||
|
middleware: {
|
||||||
|
mount: true,
|
||||||
|
setup: (cls, req: Request, res: Response) => {
|
||||||
|
cls.set('organizationId', req.headers['organization-id']);
|
||||||
|
cls.set('userId', 1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TenancyDatabaseModule,
|
||||||
|
TenancyModelsModule,
|
||||||
|
ItemsModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: JwtAuthGuard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: UserIpInterceptor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: ExcludeNullInterceptor,
|
||||||
|
},
|
||||||
|
AppService,
|
||||||
|
JwtStrategy,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {
|
||||||
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
consumer
|
||||||
|
.apply(LoggerMiddleware)
|
||||||
|
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
||||||
|
|
||||||
|
consumer
|
||||||
|
.apply(TenancyGlobalMiddleware)
|
||||||
|
.forRoutes({ path: '*', method: RequestMethod.ALL });
|
||||||
|
}
|
||||||
|
}
|
||||||
24
packages/server-nest/src/modules/App/App.service.ts
Normal file
24
packages/server-nest/src/modules/App/App.service.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
// configService: ConfigService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private configService: ConfigService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getHello(): string {
|
||||||
|
console.log(this.configService.get('DATABASE_PORT'));
|
||||||
|
const payload = {};
|
||||||
|
|
||||||
|
const accessToken = this.jwtService.sign(payload);
|
||||||
|
|
||||||
|
console.log(accessToken);
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/server-nest/src/modules/Auth/Auth.constants.ts
Normal file
4
packages/server-nest/src/modules/Auth/Auth.constants.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const jwtConstants = {
|
||||||
|
secret:
|
||||||
|
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
|
||||||
32
packages/server-nest/src/modules/Auth/Jwt.guard.ts
Normal file
32
packages/server-nest/src/modules/Auth/Jwt.guard.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
Scope,
|
||||||
|
SetMetadata,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = 'isPublic';
|
||||||
|
export const PublicRoute = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
constructor(
|
||||||
|
private reflector: Reflector,
|
||||||
|
private readonly cls: ClsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/server-nest/src/modules/Auth/Jwt.strategy.ts
Normal file
19
packages/server-nest/src/modules/Auth/Jwt.strategy.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { jwtConstants } from './Auth.constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: jwtConstants.secret,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: any) {
|
||||||
|
return { userId: payload.sub, username: payload.username };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Model as ObjectionModel } from 'objection';
|
||||||
|
|
||||||
|
export const CustomViewBaseModel = (Model) =>
|
||||||
|
class extends Model {
|
||||||
|
/**
|
||||||
|
* Retrieve the default custom views, roles and columns.
|
||||||
|
*/
|
||||||
|
static get defaultViews() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the default view by the given slug.
|
||||||
|
*/
|
||||||
|
static getDefaultViewBySlug(viewSlug) {
|
||||||
|
return this.defaultViews.find((view) => view.slug === viewSlug) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDefaultViews() {
|
||||||
|
return this.defaultViews;
|
||||||
|
}
|
||||||
|
};
|
||||||
120
packages/server-nest/src/modules/Items/CreateItem.service.ts
Normal file
120
packages/server-nest/src/modules/Items/CreateItem.service.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { defaultTo } from 'lodash';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
||||||
|
import { IItemDTO, IItemEventCreatedPayload } from '@/interfaces/Item';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { ItemsValidators } from './ItemValidator.service';
|
||||||
|
import { Item } from './models/Item';
|
||||||
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
|
@Injectable({ scope: Scope.REQUEST })
|
||||||
|
export class CreateItemService {
|
||||||
|
/**
|
||||||
|
* Constructor for the CreateItemService class.
|
||||||
|
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item creation events.
|
||||||
|
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||||
|
* @param {ItemsValidators} validators - Service for validating item data.
|
||||||
|
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
private readonly validators: ItemsValidators,
|
||||||
|
|
||||||
|
@Inject(Item.name)
|
||||||
|
private readonly itemModel: typeof Item,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize the creating item.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IItemDTO} itemDTO
|
||||||
|
*/
|
||||||
|
async authorize(itemDTO: IItemDTO) {
|
||||||
|
// Validate whether the given item name already exists on the storage.
|
||||||
|
await this.validators.validateItemNameUniquiness(itemDTO.name);
|
||||||
|
|
||||||
|
if (itemDTO.categoryId) {
|
||||||
|
await this.validators.validateItemCategoryExistance(itemDTO.categoryId);
|
||||||
|
}
|
||||||
|
if (itemDTO.sellAccountId) {
|
||||||
|
await this.validators.validateItemSellAccountExistance(
|
||||||
|
itemDTO.sellAccountId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Validate the income account id existance if the item is sellable.
|
||||||
|
this.validators.validateIncomeAccountExistance(
|
||||||
|
itemDTO.sellable,
|
||||||
|
itemDTO.sellAccountId,
|
||||||
|
);
|
||||||
|
if (itemDTO.costAccountId) {
|
||||||
|
await this.validators.validateItemCostAccountExistance(
|
||||||
|
itemDTO.costAccountId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Validate the cost account id existance if the item is purchasable.
|
||||||
|
this.validators.validateCostAccountExistance(
|
||||||
|
itemDTO.purchasable,
|
||||||
|
itemDTO.costAccountId,
|
||||||
|
);
|
||||||
|
if (itemDTO.inventoryAccountId) {
|
||||||
|
await this.validators.validateItemInventoryAccountExistance(
|
||||||
|
itemDTO.inventoryAccountId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (itemDTO.purchaseTaxRateId) {
|
||||||
|
await this.validators.validatePurchaseTaxRateExistance(
|
||||||
|
itemDTO.purchaseTaxRateId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (itemDTO.sellTaxRateId) {
|
||||||
|
await this.validators.validateSellTaxRateExistance(itemDTO.sellTaxRateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the item DTO to model.
|
||||||
|
* @param {IItemDTO} itemDTO - Item DTO.
|
||||||
|
* @return {IItem}
|
||||||
|
*/
|
||||||
|
private transformNewItemDTOToModel(itemDTO: IItemDTO) {
|
||||||
|
return {
|
||||||
|
...itemDTO,
|
||||||
|
active: defaultTo(itemDTO.active, 1),
|
||||||
|
quantityOnHand: itemDTO.type === 'inventory' ? 0 : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new item.
|
||||||
|
* @param {IItemDTO} itemDTO
|
||||||
|
* @return {Promise<IItem>}
|
||||||
|
*/
|
||||||
|
public async createItem(
|
||||||
|
itemDTO: IItemDTO,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<Item> {
|
||||||
|
// Authorize the item before creating.
|
||||||
|
await this.authorize(itemDTO);
|
||||||
|
|
||||||
|
// Creates a new item with associated transactions under unit-of-work envirement.
|
||||||
|
return this.uow.withTransaction<Item>(async (trx: Knex.Transaction) => {
|
||||||
|
const itemInsert = this.transformNewItemDTOToModel(itemDTO);
|
||||||
|
|
||||||
|
// Inserts a new item and fetch the created item.
|
||||||
|
const item = await this.itemModel.query(trx).insertAndFetch({
|
||||||
|
...itemInsert,
|
||||||
|
});
|
||||||
|
// Triggers `onItemCreated` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.item.onCreated, {
|
||||||
|
item,
|
||||||
|
itemId: item.id,
|
||||||
|
trx,
|
||||||
|
} as IItemEventCreatedPayload);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}, trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
packages/server-nest/src/modules/Items/DeleteItem.service.ts
Normal file
67
packages/server-nest/src/modules/Items/DeleteItem.service.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import {
|
||||||
|
IItemEventDeletedPayload,
|
||||||
|
IItemEventDeletingPayload,
|
||||||
|
} from 'src/interfaces/Item';
|
||||||
|
import { events } from 'src/common/events/events';
|
||||||
|
import { Item } from './models/Item';
|
||||||
|
import { ERRORS } from './Items.constants';
|
||||||
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeleteItemService {
|
||||||
|
/**
|
||||||
|
* Constructor for the class.
|
||||||
|
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item deletion events.
|
||||||
|
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||||
|
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
|
||||||
|
@Inject(Item.name)
|
||||||
|
private readonly itemModel: typeof Item,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the given item from the storage.
|
||||||
|
* @param {number} itemId - Item id.
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async deleteItem(
|
||||||
|
itemId: number,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<void> {
|
||||||
|
// Retrieve the given item or throw not found service error.
|
||||||
|
const oldItem = await this.itemModel
|
||||||
|
.query()
|
||||||
|
.findById(itemId)
|
||||||
|
.throwIfNotFound()
|
||||||
|
// @ts-expect-error
|
||||||
|
.queryAndThrowIfHasRelations({
|
||||||
|
type: ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTIONS,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete item in unit of work.
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onItemDeleting` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.item.onDeleting, {
|
||||||
|
trx,
|
||||||
|
oldItem,
|
||||||
|
} as IItemEventDeletingPayload);
|
||||||
|
|
||||||
|
// Deletes the item.
|
||||||
|
await this.itemModel.query(trx).findById(itemId).delete();
|
||||||
|
|
||||||
|
// Triggers `onItemDeleted` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.item.onDeleted, {
|
||||||
|
itemId,
|
||||||
|
oldItem,
|
||||||
|
trx,
|
||||||
|
} as IItemEventDeletedPayload);
|
||||||
|
}, trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
143
packages/server-nest/src/modules/Items/EditItem.service.ts
Normal file
143
packages/server-nest/src/modules/Items/EditItem.service.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { IItemDTO, IItemEventEditedPayload } from 'src/interfaces/Item';
|
||||||
|
import { events } from 'src/common/events/events';
|
||||||
|
import { ItemsValidators } from './ItemValidator.service';
|
||||||
|
import { Item } from './models/Item';
|
||||||
|
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EditItemService {
|
||||||
|
/**
|
||||||
|
* Constructor for the class.
|
||||||
|
* @param {EventEmitter2} eventEmitter - Event emitter for publishing item edit events.
|
||||||
|
* @param {UnitOfWork} uow - Unit of Work for tenant database transactions.
|
||||||
|
* @param {ItemsValidators} validators - Service for validating item data.
|
||||||
|
* @param {typeof Item} itemModel - The Item model class for database operations.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
private readonly validators: ItemsValidators,
|
||||||
|
|
||||||
|
@Inject(Item.name)
|
||||||
|
private readonly itemModel: typeof Item,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize the editing item.
|
||||||
|
* @param {IItemDTO} itemDTO
|
||||||
|
* @param {Item} oldItem
|
||||||
|
*/
|
||||||
|
async authorize(itemDTO: IItemDTO, oldItem: Item) {
|
||||||
|
// Validate edit item type from inventory type.
|
||||||
|
this.validators.validateEditItemFromInventory(itemDTO, oldItem);
|
||||||
|
|
||||||
|
// Validate edit item type to inventory type.
|
||||||
|
await this.validators.validateEditItemTypeToInventory(oldItem, itemDTO);
|
||||||
|
|
||||||
|
// Validate whether the given item name already exists on the storage.
|
||||||
|
await this.validators.validateItemNameUniquiness(itemDTO.name, oldItem.id);
|
||||||
|
|
||||||
|
if (itemDTO.categoryId) {
|
||||||
|
await this.validators.validateItemCategoryExistance(itemDTO.categoryId);
|
||||||
|
}
|
||||||
|
if (itemDTO.sellAccountId) {
|
||||||
|
await this.validators.validateItemSellAccountExistance(
|
||||||
|
itemDTO.sellAccountId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Validate the income account id existance if the item is sellable.
|
||||||
|
this.validators.validateIncomeAccountExistance(
|
||||||
|
itemDTO.sellable,
|
||||||
|
itemDTO.sellAccountId,
|
||||||
|
);
|
||||||
|
if (itemDTO.costAccountId) {
|
||||||
|
await this.validators.validateItemCostAccountExistance(
|
||||||
|
itemDTO.costAccountId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Validate the cost account id existance if the item is purchasable.
|
||||||
|
this.validators.validateCostAccountExistance(
|
||||||
|
itemDTO.purchasable,
|
||||||
|
itemDTO.costAccountId,
|
||||||
|
);
|
||||||
|
if (itemDTO.inventoryAccountId) {
|
||||||
|
await this.validators.validateItemInventoryAccountExistance(
|
||||||
|
itemDTO.inventoryAccountId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (itemDTO.purchaseTaxRateId) {
|
||||||
|
await this.validators.validatePurchaseTaxRateExistance(
|
||||||
|
itemDTO.purchaseTaxRateId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (itemDTO.sellTaxRateId) {
|
||||||
|
await this.validators.validateSellTaxRateExistance(itemDTO.sellTaxRateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the edit item DTO to model.
|
||||||
|
* @param {IItemDTO} itemDTO - Item DTO.
|
||||||
|
* @param {Item} oldItem - Old item.
|
||||||
|
* @return {Partial<Item>}
|
||||||
|
*/
|
||||||
|
private transformEditItemDTOToModel(
|
||||||
|
itemDTO: IItemDTO,
|
||||||
|
oldItem: Item,
|
||||||
|
): Partial<Item> {
|
||||||
|
return {
|
||||||
|
...itemDTO,
|
||||||
|
...(itemDTO.type === 'inventory' && oldItem.type !== 'inventory'
|
||||||
|
? {
|
||||||
|
quantityOnHand: 0,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits the item metadata.
|
||||||
|
* @param {number} itemId
|
||||||
|
* @param {IItemDTO} itemDTO
|
||||||
|
*/
|
||||||
|
public async editItem(
|
||||||
|
itemId: number,
|
||||||
|
itemDTO: IItemDTO,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<Item> {
|
||||||
|
// Validates the given item existance on the storage.
|
||||||
|
const oldItem = await this.itemModel
|
||||||
|
.query()
|
||||||
|
.findById(itemId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Authorize before editing item.
|
||||||
|
await this.authorize(itemDTO, oldItem);
|
||||||
|
|
||||||
|
// Transform the edit item DTO to model.
|
||||||
|
const itemModel = this.transformEditItemDTOToModel(itemDTO, oldItem);
|
||||||
|
|
||||||
|
// Edits the item with associated transactions under unit-of-work environment.
|
||||||
|
return this.uow.withTransaction<Item>(async (trx: Knex.Transaction) => {
|
||||||
|
// Updates the item on the storage and fetches the updated one.
|
||||||
|
const newItem = await this.itemModel
|
||||||
|
.query(trx)
|
||||||
|
.patchAndFetchById(itemId, itemModel);
|
||||||
|
|
||||||
|
// Edit event payload.
|
||||||
|
const eventPayload: IItemEventEditedPayload = {
|
||||||
|
item: newItem,
|
||||||
|
oldItem,
|
||||||
|
itemId: newItem.id,
|
||||||
|
trx,
|
||||||
|
};
|
||||||
|
// Triggers `onItemEdited` event.
|
||||||
|
await this.eventEmitter.emitAsync(events.item.onEdited, eventPayload);
|
||||||
|
|
||||||
|
return newItem;
|
||||||
|
}, trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
packages/server-nest/src/modules/Items/Item.controller.ts
Normal file
44
packages/server-nest/src/modules/Items/Item.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
UsePipes,
|
||||||
|
UseInterceptors,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ZodValidationPipe } from 'src/common/pipes/ZodValidation.pipe';
|
||||||
|
import { createItemSchema } from './Item.schema';
|
||||||
|
import { CreateItemService } from './CreateItem.service';
|
||||||
|
import { Item } from './models/Item';
|
||||||
|
import { DeleteItemService } from './DeleteItem.service';
|
||||||
|
import { TenantController } from '../Tenancy/Tenant.controller';
|
||||||
|
import { SubscriptionGuard } from '../Subscription/interceptors/Subscription.guard';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import { PublicRoute } from '../Auth/Jwt.guard';
|
||||||
|
|
||||||
|
@Controller('/items')
|
||||||
|
@UseGuards(SubscriptionGuard)
|
||||||
|
@PublicRoute()
|
||||||
|
export class ItemsController extends TenantController {
|
||||||
|
constructor(
|
||||||
|
private readonly createItemService: CreateItemService,
|
||||||
|
private readonly deleteItemService: DeleteItemService,
|
||||||
|
private readonly cls: ClsService,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UsePipes(new ZodValidationPipe(createItemSchema))
|
||||||
|
async createItem(@Body() createItemDto: any): Promise<Item> {
|
||||||
|
return this.createItemService.createItem(createItemDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
async deleteItem(@Param('id') id: string): Promise<void> {
|
||||||
|
const itemId = parseInt(id, 10);
|
||||||
|
return this.deleteItemService.deleteItem(itemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
111
packages/server-nest/src/modules/Items/Item.schema.ts
Normal file
111
packages/server-nest/src/modules/Items/Item.schema.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { DATATYPES_LENGTH } from 'src/constants/data-types';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
export const createItemSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().max(DATATYPES_LENGTH.STRING),
|
||||||
|
type: z.enum(['service', 'non-inventory', 'inventory']),
|
||||||
|
code: z.string().max(DATATYPES_LENGTH.STRING).nullable().optional(),
|
||||||
|
purchasable: z.boolean().optional(),
|
||||||
|
cost_price: z
|
||||||
|
.number()
|
||||||
|
.min(0)
|
||||||
|
.max(DATATYPES_LENGTH.DECIMAL_13_3)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
cost_account_id: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.max(DATATYPES_LENGTH.INT_10)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
sellable: z.boolean().optional(),
|
||||||
|
sell_price: z
|
||||||
|
.number()
|
||||||
|
.min(0)
|
||||||
|
.max(DATATYPES_LENGTH.DECIMAL_13_3)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
sell_account_id: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.max(DATATYPES_LENGTH.INT_10)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
inventory_account_id: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.max(DATATYPES_LENGTH.INT_10)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
sell_description: z
|
||||||
|
.string()
|
||||||
|
.max(DATATYPES_LENGTH.TEXT)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
purchase_description: z
|
||||||
|
.string()
|
||||||
|
.max(DATATYPES_LENGTH.TEXT)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
sell_tax_rate_id: z.number().int().nullable().optional(),
|
||||||
|
purchase_tax_rate_id: z.number().int().nullable().optional(),
|
||||||
|
category_id: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.max(DATATYPES_LENGTH.INT_10)
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
|
note: z.string().max(DATATYPES_LENGTH.TEXT).optional(),
|
||||||
|
active: z.boolean().optional(),
|
||||||
|
media_ids: z.array(z.number().int()).optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.purchasable) {
|
||||||
|
return (
|
||||||
|
data.cost_price !== undefined && data.cost_account_id !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Cost price and cost account ID are required when item is purchasable',
|
||||||
|
path: ['cost_price', 'cost_account_id'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.sellable) {
|
||||||
|
return (
|
||||||
|
data.sell_price !== undefined && data.sell_account_id !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Sell price and sell account ID are required when item is sellable',
|
||||||
|
path: ['sell_price', 'sell_account_id'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.type === 'inventory') {
|
||||||
|
return data.inventory_account_id !== undefined;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Inventory account ID is required for inventory items',
|
||||||
|
path: ['inventory_account_id'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export type createItemDTO = z.infer<typeof createItemSchema>;
|
||||||
285
packages/server-nest/src/modules/Items/ItemValidator.service.ts
Normal file
285
packages/server-nest/src/modules/Items/ItemValidator.service.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ACCOUNT_PARENT_TYPE,
|
||||||
|
ACCOUNT_ROOT_TYPE,
|
||||||
|
ACCOUNT_TYPE,
|
||||||
|
} from 'src/constants/accounts';
|
||||||
|
import { ServiceError } from './ServiceError';
|
||||||
|
import { IItem, IItemDTO } from 'src/interfaces/Item';
|
||||||
|
import { ERRORS } from './Items.constants';
|
||||||
|
import { Item } from './models/Item';
|
||||||
|
import { Account } from '../Accounts/models/Account';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ItemsValidators {
|
||||||
|
constructor(
|
||||||
|
@Inject(Item.name) private itemModel: typeof Item,
|
||||||
|
@Inject(Account.name) private accountModel: typeof Account,
|
||||||
|
@Inject(Item.name) private taxRateModel: typeof Item,
|
||||||
|
@Inject(Item.name) private itemEntryModel: typeof Item,
|
||||||
|
@Inject(Item.name) private itemCategoryModel: typeof Item,
|
||||||
|
@Inject(Item.name) private accountTransactionModel: typeof Item,
|
||||||
|
@Inject(Item.name) private inventoryAdjustmentEntryModel: typeof Item,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate wether the given item name already exists on the storage.
|
||||||
|
* @param {string} itemName
|
||||||
|
* @param {number} notItemId
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async validateItemNameUniquiness(
|
||||||
|
itemName: string,
|
||||||
|
notItemId?: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const foundItems = await this.itemModel.query().onBuild((builder: any) => {
|
||||||
|
builder.where('name', itemName);
|
||||||
|
if (notItemId) {
|
||||||
|
builder.whereNot('id', notItemId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundItems.length > 0) {
|
||||||
|
throw new ServiceError(
|
||||||
|
ERRORS.ITEM_NAME_EXISTS,
|
||||||
|
'The item name is already exist.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate item COGS account existance and type.
|
||||||
|
* @param {number} costAccountId
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async validateItemCostAccountExistance(
|
||||||
|
costAccountId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const foundAccount = await this.accountModel
|
||||||
|
.query()
|
||||||
|
.findById(costAccountId);
|
||||||
|
|
||||||
|
if (!foundAccount) {
|
||||||
|
throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_FOUMD);
|
||||||
|
|
||||||
|
// Detarmines the cost of goods sold account.
|
||||||
|
} else if (!foundAccount.isParentType(ACCOUNT_PARENT_TYPE.EXPENSE)) {
|
||||||
|
throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_COGS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate item sell account existance and type.
|
||||||
|
* @param {number} sellAccountId - Sell account id.
|
||||||
|
*/
|
||||||
|
public async validateItemSellAccountExistance(sellAccountId: number) {
|
||||||
|
const foundAccount = await this.accountModel
|
||||||
|
.query()
|
||||||
|
.findById(sellAccountId);
|
||||||
|
|
||||||
|
if (!foundAccount) {
|
||||||
|
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND);
|
||||||
|
} else if (!foundAccount.isParentType(ACCOUNT_ROOT_TYPE.INCOME)) {
|
||||||
|
throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_INCOME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates income account existance.
|
||||||
|
* @param {number|null} sellable - Detarmines if the item sellable.
|
||||||
|
* @param {number|null} incomeAccountId - Income account id.
|
||||||
|
* @throws {ServiceError(ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM)}
|
||||||
|
*/
|
||||||
|
public validateIncomeAccountExistance(
|
||||||
|
sellable?: boolean,
|
||||||
|
incomeAccountId?: number,
|
||||||
|
) {
|
||||||
|
if (sellable && !incomeAccountId) {
|
||||||
|
throw new ServiceError(
|
||||||
|
ERRORS.INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM,
|
||||||
|
'Income account is require with sellable item.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the cost account existance.
|
||||||
|
* @param {boolean|null} purchasable - Detarmines if the item purchasble.
|
||||||
|
* @param {number|null} costAccountId - Cost account id.
|
||||||
|
* @throws {ServiceError(ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM)}
|
||||||
|
*/
|
||||||
|
public validateCostAccountExistance(
|
||||||
|
purchasable: boolean,
|
||||||
|
costAccountId?: number,
|
||||||
|
) {
|
||||||
|
if (purchasable && !costAccountId) {
|
||||||
|
throw new ServiceError(
|
||||||
|
ERRORS.COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM,
|
||||||
|
'The cost account is required with purchasable item.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate item inventory account existance and type.
|
||||||
|
* @param {number} inventoryAccountId
|
||||||
|
*/
|
||||||
|
public async validateItemInventoryAccountExistance(
|
||||||
|
inventoryAccountId: number,
|
||||||
|
) {
|
||||||
|
const foundAccount = await this.accountModel
|
||||||
|
.query()
|
||||||
|
.findById(inventoryAccountId);
|
||||||
|
|
||||||
|
if (!foundAccount) {
|
||||||
|
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND);
|
||||||
|
} else if (!foundAccount.isAccountType(ACCOUNT_TYPE.INVENTORY)) {
|
||||||
|
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_INVENTORY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate item category existance.
|
||||||
|
* @param {number} itemCategoryId
|
||||||
|
*/
|
||||||
|
public async validateItemCategoryExistance(itemCategoryId: number) {
|
||||||
|
const foundCategory = await this.itemCategoryModel
|
||||||
|
.query()
|
||||||
|
.findById(itemCategoryId);
|
||||||
|
|
||||||
|
if (!foundCategory) {
|
||||||
|
throw new ServiceError(ERRORS.ITEM_CATEOGRY_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given item or items have no associated invoices or bills.
|
||||||
|
* @param {number|number[]} itemId - Item id.
|
||||||
|
* @throws {ServiceError}
|
||||||
|
*/
|
||||||
|
public async validateHasNoInvoicesOrBills(itemId: number[] | number) {
|
||||||
|
const ids = Array.isArray(itemId) ? itemId : [itemId];
|
||||||
|
const foundItemEntries = await this.itemEntryModel
|
||||||
|
.query()
|
||||||
|
.whereIn('item_id', ids);
|
||||||
|
|
||||||
|
if (foundItemEntries.length > 0) {
|
||||||
|
throw new ServiceError(
|
||||||
|
ids.length > 1
|
||||||
|
? ERRORS.ITEMS_HAVE_ASSOCIATED_TRANSACTIONS
|
||||||
|
: ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTINS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given item has no associated inventory adjustment transactions.
|
||||||
|
* @param {number} itemId -
|
||||||
|
*/
|
||||||
|
public async validateHasNoInventoryAdjustments(
|
||||||
|
itemId: number[] | number,
|
||||||
|
): Promise<void> {
|
||||||
|
const itemsIds = Array.isArray(itemId) ? itemId : [itemId];
|
||||||
|
const inventoryAdjEntries = await this.inventoryAdjustmentEntryModel
|
||||||
|
.query()
|
||||||
|
.whereIn('item_id', itemsIds);
|
||||||
|
|
||||||
|
if (inventoryAdjEntries.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates edit item type from service/non-inventory to inventory.
|
||||||
|
* Should item has no any relations with accounts transactions.
|
||||||
|
* @param {number} itemId - Item id.
|
||||||
|
*/
|
||||||
|
public async validateEditItemTypeToInventory(
|
||||||
|
oldItem: Item,
|
||||||
|
newItemDTO: IItemDTO,
|
||||||
|
) {
|
||||||
|
// We have no problem in case the item type not modified.
|
||||||
|
if (newItemDTO.type === oldItem.type || oldItem.type === 'inventory') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Retrieve all transactions that associated to the given item id.
|
||||||
|
const itemTransactionsCount = await this.accountTransactionModel
|
||||||
|
.query()
|
||||||
|
.where('item_id', oldItem.id)
|
||||||
|
.count('item_id', { as: 'transactions' })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (itemTransactionsCount.transactions > 0) {
|
||||||
|
throw new ServiceError(
|
||||||
|
ERRORS.TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the item inventory account whether modified and item
|
||||||
|
* has associated inventory transactions.
|
||||||
|
* @param {Item} oldItem
|
||||||
|
* @param {IItemDTO} newItemDTO
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async validateItemInvnetoryAccountModified(
|
||||||
|
oldItem: Item,
|
||||||
|
newItemDTO: IItemDTO,
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
newItemDTO.type !== 'inventory' ||
|
||||||
|
oldItem.inventoryAccountId === newItemDTO.inventoryAccountId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Inventory transactions associated to the given item id.
|
||||||
|
const transactions = await this.accountTransactionModel.query().where({
|
||||||
|
itemId: oldItem.id,
|
||||||
|
});
|
||||||
|
// Throw the service error in case item has associated inventory transactions.
|
||||||
|
if (transactions.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_CANNOT_MODIFIED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate edit item type from inventory to another type that not allowed.
|
||||||
|
* @param {IItemDTO} itemDTO - Item DTO.
|
||||||
|
* @param {IItem} oldItem - Old item.
|
||||||
|
*/
|
||||||
|
public validateEditItemFromInventory(itemDTO: IItemDTO, oldItem: Item) {
|
||||||
|
if (
|
||||||
|
itemDTO.type &&
|
||||||
|
oldItem.type === 'inventory' &&
|
||||||
|
itemDTO.type !== oldItem.type
|
||||||
|
) {
|
||||||
|
throw new ServiceError(ERRORS.ITEM_CANNOT_CHANGE_INVENTORY_TYPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the purchase tax rate id existance.
|
||||||
|
* @param {number} taxRateId -
|
||||||
|
*/
|
||||||
|
public async validatePurchaseTaxRateExistance(taxRateId: number) {
|
||||||
|
const foundTaxRate = await this.taxRateModel.query().findById(taxRateId);
|
||||||
|
|
||||||
|
if (!foundTaxRate) {
|
||||||
|
throw new ServiceError(ERRORS.PURCHASE_TAX_RATE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the sell tax rate id existance.
|
||||||
|
* @param {number} taxRateId
|
||||||
|
*/
|
||||||
|
public async validateSellTaxRateExistance(taxRateId: number) {
|
||||||
|
const foundTaxRate = await this.taxRateModel.query().findById(taxRateId);
|
||||||
|
|
||||||
|
if (!foundTaxRate) {
|
||||||
|
throw new ServiceError(ERRORS.SELL_TAX_RATE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
packages/server-nest/src/modules/Items/Items.constants.ts
Normal file
141
packages/server-nest/src/modules/Items/Items.constants.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
export const ERRORS = {
|
||||||
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
|
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||||
|
|
||||||
|
ITEM_NAME_EXISTS: 'ITEM_NAME_EXISTS',
|
||||||
|
ITEM_CATEOGRY_NOT_FOUND: 'ITEM_CATEOGRY_NOT_FOUND',
|
||||||
|
COST_ACCOUNT_NOT_COGS: 'COST_ACCOUNT_NOT_COGS',
|
||||||
|
COST_ACCOUNT_NOT_FOUMD: 'COST_ACCOUNT_NOT_FOUMD',
|
||||||
|
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
|
||||||
|
SELL_ACCOUNT_NOT_INCOME: 'SELL_ACCOUNT_NOT_INCOME',
|
||||||
|
|
||||||
|
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
|
||||||
|
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||||
|
|
||||||
|
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
|
||||||
|
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
|
||||||
|
|
||||||
|
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
|
||||||
|
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
||||||
|
ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
||||||
|
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS:
|
||||||
|
'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||||
|
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||||
|
|
||||||
|
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||||
|
|
||||||
|
PURCHASE_TAX_RATE_NOT_FOUND: 'PURCHASE_TAX_RATE_NOT_FOUND',
|
||||||
|
SELL_TAX_RATE_NOT_FOUND: 'SELL_TAX_RATE_NOT_FOUND',
|
||||||
|
|
||||||
|
INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM:
|
||||||
|
'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
|
||||||
|
COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM:
|
||||||
|
'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
|
export const DEFAULT_VIEWS = [
|
||||||
|
{
|
||||||
|
name: 'Services',
|
||||||
|
slug: 'services',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'service' },
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Inventory',
|
||||||
|
slug: 'inventory',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'inventory' },
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Non Inventory',
|
||||||
|
slug: 'non-inventory',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
fieldKey: 'type',
|
||||||
|
comparator: 'equals',
|
||||||
|
value: 'non-inventory',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ItemsSampleData = [
|
||||||
|
{
|
||||||
|
'Item Type': 'Inventory',
|
||||||
|
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||||
|
'Item Code': '1000',
|
||||||
|
Sellable: 'T',
|
||||||
|
Purchasable: 'T',
|
||||||
|
'Cost Price': '10000',
|
||||||
|
'Sell Price': '1000',
|
||||||
|
'Cost Account': 'Cost of Goods Sold',
|
||||||
|
'Sell Account': 'Other Income',
|
||||||
|
'Inventory Account': 'Inventory Asset',
|
||||||
|
'Sell Description': 'Description ....',
|
||||||
|
'Purchase Description': 'Description ....',
|
||||||
|
Category: 'sdafasdfsadf',
|
||||||
|
Note: 'At dolor est non tempore et quisquam.',
|
||||||
|
Active: 'TRUE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Item Type': 'Inventory',
|
||||||
|
'Item Name': 'Schmitt Group',
|
||||||
|
'Item Code': '1001',
|
||||||
|
Sellable: 'T',
|
||||||
|
Purchasable: 'T',
|
||||||
|
'Cost Price': '10000',
|
||||||
|
'Sell Price': '1000',
|
||||||
|
'Cost Account': 'Cost of Goods Sold',
|
||||||
|
'Sell Account': 'Other Income',
|
||||||
|
'Inventory Account': 'Inventory Asset',
|
||||||
|
'Sell Description': 'Description ....',
|
||||||
|
'Purchase Description': 'Description ....',
|
||||||
|
Category: 'sdafasdfsadf',
|
||||||
|
Note: 'Id perspiciatis at adipisci minus accusamus dolor iure dolore.',
|
||||||
|
Active: 'TRUE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Item Type': 'Inventory',
|
||||||
|
'Item Name': 'Marks - Carroll',
|
||||||
|
'Item Code': '1002',
|
||||||
|
Sellable: 'T',
|
||||||
|
Purchasable: 'T',
|
||||||
|
'Cost Price': '10000',
|
||||||
|
'Sell Price': '1000',
|
||||||
|
'Cost Account': 'Cost of Goods Sold',
|
||||||
|
'Sell Account': 'Other Income',
|
||||||
|
'Inventory Account': 'Inventory Asset',
|
||||||
|
'Sell Description': 'Description ....',
|
||||||
|
'Purchase Description': 'Description ....',
|
||||||
|
Category: 'sdafasdfsadf',
|
||||||
|
Note: 'Odio odio minus similique.',
|
||||||
|
Active: 'TRUE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Item Type': 'Inventory',
|
||||||
|
'Item Name': 'VonRueden, Ruecker and Hettinger',
|
||||||
|
'Item Code': '1003',
|
||||||
|
Sellable: 'T',
|
||||||
|
Purchasable: 'T',
|
||||||
|
'Cost Price': '10000',
|
||||||
|
'Sell Price': '1000',
|
||||||
|
'Cost Account': 'Cost of Goods Sold',
|
||||||
|
'Sell Account': 'Other Income',
|
||||||
|
'Inventory Account': 'Inventory Asset',
|
||||||
|
'Sell Description': 'Description ....',
|
||||||
|
'Purchase Description': 'Description ....',
|
||||||
|
Category: 'sdafasdfsadf',
|
||||||
|
Note: 'Quibusdam dolores illo.',
|
||||||
|
Active: 'TRUE',
|
||||||
|
},
|
||||||
|
];
|
||||||
19
packages/server-nest/src/modules/Items/Items.module.ts
Normal file
19
packages/server-nest/src/modules/Items/Items.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ItemsController } from './Item.controller';
|
||||||
|
import { CreateItemService } from './CreateItem.service';
|
||||||
|
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||||
|
import { ItemsValidators } from './ItemValidator.service';
|
||||||
|
import { DeleteItemService } from './DeleteItem.service';
|
||||||
|
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TenancyDatabaseModule],
|
||||||
|
controllers: [ItemsController],
|
||||||
|
providers: [
|
||||||
|
ItemsValidators,
|
||||||
|
CreateItemService,
|
||||||
|
DeleteItemService,
|
||||||
|
TenancyContext,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ItemsModule {}
|
||||||
13
packages/server-nest/src/modules/Items/ServiceError.ts
Normal file
13
packages/server-nest/src/modules/Items/ServiceError.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
export class ServiceError {
|
||||||
|
errorType: string;
|
||||||
|
message: string;
|
||||||
|
payload: any;
|
||||||
|
|
||||||
|
constructor(errorType: string, message?: string, payload?: any) {
|
||||||
|
this.errorType = errorType;
|
||||||
|
this.message = message || null;
|
||||||
|
|
||||||
|
this.payload = payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export class ItemCreatedEvent {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
import { ItemCreatedEvent } from '../events/ItemCreated.event';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ItemCreatedListener {
|
||||||
|
|
||||||
|
@OnEvent('order.created')
|
||||||
|
handleItemCreatedEvent(event: ItemCreatedEvent) {
|
||||||
|
// handle and process "OrderCreatedEvent" event
|
||||||
|
console.log(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/server-nest/src/modules/Items/models/Item.ts
Normal file
26
packages/server-nest/src/modules/Items/models/Item.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as F from 'fp-ts/function';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { SearchableModel } from '@/modules/Search/SearchableMdel';
|
||||||
|
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||||
|
|
||||||
|
const Extend = R.compose(SearchableModel)(TenantModel);
|
||||||
|
|
||||||
|
export class Item extends Extend {
|
||||||
|
public readonly quantityOnHand: number;
|
||||||
|
public readonly name: string;
|
||||||
|
public readonly active: boolean;
|
||||||
|
public readonly type: string;
|
||||||
|
public readonly code: string;
|
||||||
|
public readonly sellable: boolean;
|
||||||
|
public readonly purchasable: boolean;
|
||||||
|
public readonly costPrice: number;
|
||||||
|
public readonly sellPrice: number;
|
||||||
|
public readonly currencyCode: string;
|
||||||
|
public readonly costAccountId: number;
|
||||||
|
public readonly inventoryAccountId: number;
|
||||||
|
public readonly categoryId: number;
|
||||||
|
|
||||||
|
static get tableName() {
|
||||||
|
return 'items';
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/server-nest/src/modules/Search/SearchableMdel.ts
Normal file
22
packages/server-nest/src/modules/Search/SearchableMdel.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as O from 'objection';
|
||||||
|
import { IModelMeta, } from '@/interfaces/Model';
|
||||||
|
|
||||||
|
export const SearchableModel: O.Plugin = (Model) =>
|
||||||
|
// @ts-ignore
|
||||||
|
class extends Model {
|
||||||
|
additionalProperty: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searchable model.
|
||||||
|
*/
|
||||||
|
static get searchable(): IModelMeta {
|
||||||
|
throw true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search roles.
|
||||||
|
*/
|
||||||
|
// static get searchRoles(): ISearchRole[] {
|
||||||
|
// return [];
|
||||||
|
// }
|
||||||
|
};
|
||||||
77
packages/server-nest/src/modules/Settings/ModelSettings.ts
Normal file
77
packages/server-nest/src/modules/Settings/ModelSettings.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { get } from 'lodash';
|
||||||
|
import {
|
||||||
|
IModelMeta,
|
||||||
|
IModelMetaField,
|
||||||
|
IModelMetaDefaultSort,
|
||||||
|
} from '@/interfaces/Model';
|
||||||
|
|
||||||
|
const defaultModelMeta = {
|
||||||
|
fields: {},
|
||||||
|
fields2: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModelSettings = (Model) =>
|
||||||
|
class extends Model {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {IModelMeta}
|
||||||
|
*/
|
||||||
|
static get meta(): IModelMeta {
|
||||||
|
throw new Error('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed meta merged with default emta.
|
||||||
|
* @returns {IModelMeta}
|
||||||
|
*/
|
||||||
|
static get parsedMeta(): IModelMeta {
|
||||||
|
return {
|
||||||
|
...defaultModelMeta,
|
||||||
|
...this.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve specific model field meta of the given field key.
|
||||||
|
* @param {string} key
|
||||||
|
* @returns {IModelMetaField}
|
||||||
|
*/
|
||||||
|
public static getField(key: string, attribute?: string): IModelMetaField {
|
||||||
|
const field = get(this.meta.fields, key);
|
||||||
|
|
||||||
|
return attribute ? get(field, attribute) : field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the specific model meta.
|
||||||
|
* @param {string} key
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public static getMeta(key?: string) {
|
||||||
|
return key ? get(this.parsedMeta, key) : this.parsedMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the model meta fields.
|
||||||
|
* @return {{ [key: string]: IModelMetaField }}
|
||||||
|
*/
|
||||||
|
public static get fields(): { [key: string]: IModelMetaField } {
|
||||||
|
return this.getMeta('fields');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the model default sort settings.
|
||||||
|
* @return {IModelMetaDefaultSort}
|
||||||
|
*/
|
||||||
|
public static get defaultSort(): IModelMetaDefaultSort {
|
||||||
|
return this.getMeta('defaultSort');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the default filter field key.
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
public static get defaultFilterField(): string {
|
||||||
|
return this.getMeta('defaultFilterField');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import moment, { unitOfTime } from 'moment';
|
||||||
|
|
||||||
|
export class SubscriptionPeriod {
|
||||||
|
private start: Date;
|
||||||
|
private end: Date;
|
||||||
|
private interval: string;
|
||||||
|
private count: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {string} interval -
|
||||||
|
* @param {number} count -
|
||||||
|
* @param {Date} start -
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
interval: unitOfTime.DurationConstructor = 'month',
|
||||||
|
count: number,
|
||||||
|
start?: Date
|
||||||
|
) {
|
||||||
|
this.interval = interval;
|
||||||
|
this.count = count;
|
||||||
|
this.start = start;
|
||||||
|
|
||||||
|
if (!start) {
|
||||||
|
this.start = moment().toDate();
|
||||||
|
}
|
||||||
|
if (count === Infinity) {
|
||||||
|
this.end = null;
|
||||||
|
} else {
|
||||||
|
this.end = moment(start).add(count, interval).toDate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStartDate() {
|
||||||
|
return this.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndDate() {
|
||||||
|
return this.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInterval() {
|
||||||
|
return this.interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIntervalCount() {
|
||||||
|
return this.count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Inject,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { PlanSubscription } from '../models/PlanSubscription';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SubscriptionGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
@Inject(PlanSubscription.name)
|
||||||
|
private readonly planSubscriptionModel: typeof PlanSubscription,
|
||||||
|
private readonly tenancyContext: TenancyContext,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the tenant's subscription is exists and not inactive
|
||||||
|
* @param {ExecutionContext} context
|
||||||
|
* @param {string} subscriptionSlug
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
subscriptionSlug: string = 'main', // Default value
|
||||||
|
): Promise<boolean> {
|
||||||
|
const tenant = await this.tenancyContext.getTenant();
|
||||||
|
const subscription = await this.planSubscriptionModel
|
||||||
|
.query()
|
||||||
|
.findOne('slug', subscriptionSlug)
|
||||||
|
.where('tenant_id', tenant.id);
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
throw new UnauthorizedException('Tenant has no subscription.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSubscriptionInactive = subscription.inactive();
|
||||||
|
|
||||||
|
if (isSubscriptionInactive) {
|
||||||
|
throw new UnauthorizedException('Organization subscription is inactive.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import { Model, mixin } from 'objection';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { SubscriptionPeriod } from '../SubscriptionPeriod';
|
||||||
|
import { SystemModel } from '@/modules/System/models/SystemModel';
|
||||||
|
import { SubscriptionPaymentStatus } from '@/interfaces/SubscriptionPlan';
|
||||||
|
|
||||||
|
export class PlanSubscription extends mixin(SystemModel) {
|
||||||
|
public readonly lemonSubscriptionId: number;
|
||||||
|
public readonly endsAt: Date;
|
||||||
|
public readonly startsAt: Date;
|
||||||
|
public readonly canceledAt: Date;
|
||||||
|
public readonly trialEndsAt: Date;
|
||||||
|
public readonly paymentStatus: SubscriptionPaymentStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'subscription_plan_subscriptions';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defined virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return [
|
||||||
|
'active',
|
||||||
|
'inactive',
|
||||||
|
'ended',
|
||||||
|
'canceled',
|
||||||
|
'onTrial',
|
||||||
|
'status',
|
||||||
|
'isPaymentFailed',
|
||||||
|
'isPaymentSucceed',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifiers queries.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
activeSubscriptions(builder) {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const now = moment().format(dateFormat);
|
||||||
|
|
||||||
|
builder.where('ends_at', '>', now);
|
||||||
|
builder.where('trial_ends_at', '>', now);
|
||||||
|
},
|
||||||
|
|
||||||
|
inactiveSubscriptions(builder) {
|
||||||
|
builder.modify('endedTrial');
|
||||||
|
builder.modify('endedPeriod');
|
||||||
|
},
|
||||||
|
|
||||||
|
subscriptionBySlug(builder, subscriptionSlug) {
|
||||||
|
builder.where('slug', subscriptionSlug);
|
||||||
|
},
|
||||||
|
|
||||||
|
endedTrial(builder) {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const endDate = moment().format(dateFormat);
|
||||||
|
|
||||||
|
builder.where('ends_at', '<=', endDate);
|
||||||
|
},
|
||||||
|
|
||||||
|
endedPeriod(builder) {
|
||||||
|
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
const endDate = moment().format(dateFormat);
|
||||||
|
|
||||||
|
builder.where('trial_ends_at', '<=', endDate);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the failed payment.
|
||||||
|
* @param builder
|
||||||
|
*/
|
||||||
|
failedPayment(builder) {
|
||||||
|
builder.where('payment_status', SubscriptionPaymentStatus.Failed);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the succeed payment.
|
||||||
|
* @param builder
|
||||||
|
*/
|
||||||
|
succeedPayment(builder) {
|
||||||
|
builder.where('payment_status', SubscriptionPaymentStatus.Succeed);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relations mappings.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const Tenant = require('system/models/Tenant');
|
||||||
|
const Plan = require('system/models/Subscriptions/Plan');
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Plan subscription belongs to tenant.
|
||||||
|
*/
|
||||||
|
tenant: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Tenant.default,
|
||||||
|
join: {
|
||||||
|
from: 'subscription_plan_subscriptions.tenantId',
|
||||||
|
to: 'tenants.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan description belongs to plan.
|
||||||
|
*/
|
||||||
|
plan: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Plan.default,
|
||||||
|
join: {
|
||||||
|
from: 'subscription_plan_subscriptions.planId',
|
||||||
|
to: 'subscription_plans.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the subscription is active.
|
||||||
|
* Crtiria should be active:
|
||||||
|
* - During the trial period should NOT be canceled.
|
||||||
|
* - Out of trial period should NOT be ended.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
public active() {
|
||||||
|
return this.onTrial() ? !this.canceled() : !this.ended();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the subscription is inactive.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
public inactive() {
|
||||||
|
return !this.active();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if paid subscription period has ended.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
public ended() {
|
||||||
|
return this.endsAt ? moment().isAfter(this.endsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the paid subscription has started.
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
public started() {
|
||||||
|
return this.startsAt ? moment().isAfter(this.startsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if subscription is currently on trial.
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
public onTrial() {
|
||||||
|
return this.trialEndsAt ? moment().isBefore(this.trialEndsAt) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the subscription is canceled.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public canceled() {
|
||||||
|
return !!this.canceledAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the subscription status.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
public status() {
|
||||||
|
return this.canceled()
|
||||||
|
? 'canceled'
|
||||||
|
: this.onTrial()
|
||||||
|
? 'on_trial'
|
||||||
|
: this.active()
|
||||||
|
? 'active'
|
||||||
|
: 'inactive';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set new period from the given details.
|
||||||
|
* @param {string} invoiceInterval
|
||||||
|
* @param {number} invoicePeriod
|
||||||
|
* @param {string} start
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
|
static setNewPeriod(invoiceInterval: any, invoicePeriod: any, start?: any) {
|
||||||
|
const period = new SubscriptionPeriod(
|
||||||
|
invoiceInterval,
|
||||||
|
invoicePeriod,
|
||||||
|
start,
|
||||||
|
);
|
||||||
|
|
||||||
|
const startsAt = period.getStartDate();
|
||||||
|
const endsAt = period.getEndDate();
|
||||||
|
|
||||||
|
return { startsAt, endsAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renews subscription period.
|
||||||
|
* @Promise
|
||||||
|
*/
|
||||||
|
renew(invoiceInterval, invoicePeriod) {
|
||||||
|
const { startsAt, endsAt } = PlanSubscription.setNewPeriod(
|
||||||
|
invoiceInterval,
|
||||||
|
invoicePeriod,
|
||||||
|
);
|
||||||
|
return this.$query().update({ startsAt, endsAt });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the subscription payment whether is failed.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isPaymentFailed() {
|
||||||
|
return this.paymentStatus === SubscriptionPaymentStatus.Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines the subscription payment whether is succeed.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public isPaymentSucceed() {
|
||||||
|
return this.paymentStatus === SubscriptionPaymentStatus.Succeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const SystemKnexConnection ='SystemKnexConnection';
|
||||||
|
export const SystemKnexConnectionConfigure = 'SystemKnexConnectionConfigure';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get, Post } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('/system_db')
|
||||||
|
export class SystemDatabaseController {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Get()
|
||||||
|
ping(){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import Knex from 'knex';
|
||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import {
|
||||||
|
SystemKnexConnection,
|
||||||
|
SystemKnexConnectionConfigure,
|
||||||
|
} from './SystemDB.constants';
|
||||||
|
import { SystemDatabaseController } from './SystemDB.controller';
|
||||||
|
import { knexSnakeCaseMappers } from 'objection';
|
||||||
|
|
||||||
|
const providers = [
|
||||||
|
{
|
||||||
|
provide: SystemKnexConnectionConfigure,
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
client: configService.get('systemDatabase.client'),
|
||||||
|
connection: {
|
||||||
|
host: configService.get('systemDatabase.host'),
|
||||||
|
user: configService.get('systemDatabase.user'),
|
||||||
|
password: configService.get('systemDatabase.password'),
|
||||||
|
database: configService.get('systemDatabase.databaseName'),
|
||||||
|
charset: 'utf8',
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
directory: configService.get('systemDatabase.migrationDir'),
|
||||||
|
},
|
||||||
|
seeds: {
|
||||||
|
directory: configService.get('systemDatabase.seedsDir'),
|
||||||
|
},
|
||||||
|
pool: { min: 0, max: 7 },
|
||||||
|
...knexSnakeCaseMappers({ upperCase: true }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SystemKnexConnection,
|
||||||
|
inject: [SystemKnexConnectionConfigure],
|
||||||
|
useFactory: (knexConfig) => {
|
||||||
|
return Knex(knexConfig);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [...providers],
|
||||||
|
exports: [...providers],
|
||||||
|
controllers: [SystemDatabaseController],
|
||||||
|
})
|
||||||
|
export class SystemDatabaseModule {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const SystemModelsConnection = 'SystemModelsConnection';
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Model } from 'objection';
|
||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { PlanSubscription } from '@/modules/Subscription/models/PlanSubscription';
|
||||||
|
import { TenantModel } from '@/modules/System/models/TenantModel';
|
||||||
|
import { SystemKnexConnection } from '../SystemDB/SystemDB.constants';
|
||||||
|
import { SystemModelsConnection } from './SystemModels.constants';
|
||||||
|
import { SystemUser } from '../models/SystemUser';
|
||||||
|
|
||||||
|
const models = [SystemUser, PlanSubscription, TenantModel];
|
||||||
|
const modelProviders = models.map((model) => {
|
||||||
|
return {
|
||||||
|
provide: model.name,
|
||||||
|
useValue: model,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const providers = [
|
||||||
|
...modelProviders,
|
||||||
|
{
|
||||||
|
provide: SystemModelsConnection,
|
||||||
|
inject: [SystemKnexConnection],
|
||||||
|
useFactory: async (systemKnex: Knex) => {
|
||||||
|
Model.knex(systemKnex);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [...providers],
|
||||||
|
exports: [...providers],
|
||||||
|
})
|
||||||
|
export class SystemModelsModule {}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { BaseModel } from 'src/models/Model';
|
||||||
|
|
||||||
|
export class SystemModel extends BaseModel {}
|
||||||
14
packages/server-nest/src/modules/System/models/SystemUser.ts
Normal file
14
packages/server-nest/src/modules/System/models/SystemUser.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { BaseModel } from 'src/models/Model';
|
||||||
|
|
||||||
|
export class SystemUser extends BaseModel {
|
||||||
|
public readonly firstName: string;
|
||||||
|
public readonly lastName: string;
|
||||||
|
public readonly active: boolean;
|
||||||
|
public readonly password: string;
|
||||||
|
public readonly email: string;
|
||||||
|
public readonly tenantId: number;
|
||||||
|
|
||||||
|
static get tableName() {
|
||||||
|
return 'users';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { BaseModel } from 'src/models/Model';
|
||||||
|
|
||||||
|
export class TenantModel extends BaseModel {
|
||||||
|
public readonly organizationId: string;
|
||||||
|
public readonly initializedAt: string;
|
||||||
|
public readonly seededAt: boolean;
|
||||||
|
public readonly builtAt: string;
|
||||||
|
|
||||||
|
static get tableName() {
|
||||||
|
return 'tenants';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { TenancyContext } from './TenancyContext.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EnsureTenantIsInitializedGuard implements CanActivate {
|
||||||
|
constructor(private readonly tenancyContext: TenancyContext) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the tenant of the current request is initialized..
|
||||||
|
* @param {ExecutionContext} context
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const tenant = await this.tenancyContext.getTenant();
|
||||||
|
|
||||||
|
if (!tenant?.initializedAt) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
statusCode: 400,
|
||||||
|
error: 'Bad Request',
|
||||||
|
message: 'Tenant database is not migrated with application schema yet.',
|
||||||
|
errors: [{ type: 'TENANT.DATABASE.NOT.INITALIZED' }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
CanActivate,
|
||||||
|
ExecutionContext,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { TenancyContext } from './TenancyContext.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EnsureTenantIsSeededGuard implements CanActivate {
|
||||||
|
constructor(private readonly tenancyContext: TenancyContext) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the tenant of the current request is seeded.
|
||||||
|
* @param {ExecutionContext} context
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const tenant = await this.tenancyContext.getTenant();
|
||||||
|
|
||||||
|
if (!tenant.seededAt) {
|
||||||
|
throw new UnauthorizedException({
|
||||||
|
message: 'Tenant database is not seeded with initial data yet.',
|
||||||
|
errors: [{ type: 'TENANT.DATABASE.NOT.SEED' }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { RedisClientOptions } from 'redis';
|
||||||
|
import { DynamicModule, Module } from '@nestjs/common';
|
||||||
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
|
|
||||||
|
interface TenancyCacheModuleConfig {
|
||||||
|
tenantId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class TenancyCacheModule {
|
||||||
|
static register(config: TenancyCacheModuleConfig): DynamicModule {
|
||||||
|
return {
|
||||||
|
module: TenancyCacheModule,
|
||||||
|
imports: [CacheModule.register<RedisClientOptions>({})],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import { SystemUser } from '../System/models/SystemUser';
|
||||||
|
import { TenantModel } from '../System/models/TenantModel';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TenancyContext {
|
||||||
|
constructor(
|
||||||
|
private readonly cls: ClsService,
|
||||||
|
|
||||||
|
@Inject(SystemUser.name)
|
||||||
|
private readonly systemUserModel: typeof SystemUser,
|
||||||
|
|
||||||
|
@Inject(TenantModel.name)
|
||||||
|
private readonly systemTenantModel: typeof TenantModel,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current tenant.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getTenant() {
|
||||||
|
// Get the tenant from the request headers.
|
||||||
|
const organizationId = this.cls.get('organizationId');
|
||||||
|
|
||||||
|
return this.systemTenantModel.query().findOne({ organizationId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
getSystemUser() {
|
||||||
|
// Get the user from the request headers.
|
||||||
|
const userId = this.cls.get('userId');
|
||||||
|
|
||||||
|
return this.systemUserModel.query().findOne({ id: userId });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const TENANCY_DB_CONNECTION = 'TENANCY_DB_CONNECTION';
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import knex from 'knex';
|
||||||
|
import { Global, Module, Scope } from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TENANCY_DB_CONNECTION } from './TenancyDB.constants';
|
||||||
|
import { UnitOfWork } from './UnitOfWork.service';
|
||||||
|
import { knexSnakeCaseMappers } from 'objection';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
|
||||||
|
const connectionFactory = {
|
||||||
|
provide: TENANCY_DB_CONNECTION,
|
||||||
|
scope: Scope.REQUEST,
|
||||||
|
useFactory: async (
|
||||||
|
request: Request,
|
||||||
|
configService: ConfigService,
|
||||||
|
cls: ClsService,
|
||||||
|
) => {
|
||||||
|
const organizationId = cls.get('organizationId');
|
||||||
|
|
||||||
|
return knex({
|
||||||
|
client: configService.get('tenantDatabase.client'),
|
||||||
|
connection: {
|
||||||
|
host: configService.get('tenantDatabase.host'),
|
||||||
|
user: configService.get('tenantDatabase.user'),
|
||||||
|
password: configService.get('tenantDatabase.password'),
|
||||||
|
database: `bigcapital_tenant_${organizationId}`,
|
||||||
|
charset: 'utf8',
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
directory: configService.get('tenantDatabase.migrationDir'),
|
||||||
|
},
|
||||||
|
seeds: {
|
||||||
|
directory: configService.get('tenantDatabase.seedsDir'),
|
||||||
|
},
|
||||||
|
pool: { min: 0, max: 7 },
|
||||||
|
...knexSnakeCaseMappers({ upperCase: true }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
inject: [REQUEST, ConfigService, ClsService],
|
||||||
|
};
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [connectionFactory, UnitOfWork],
|
||||||
|
exports: [TENANCY_DB_CONNECTION, UnitOfWork],
|
||||||
|
})
|
||||||
|
export class TenancyDatabaseModule {}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Enumeration that represents transaction isolation levels for use with the {@link Transactional} annotation
|
||||||
|
*/
|
||||||
|
export enum IsolationLevel {
|
||||||
|
/**
|
||||||
|
* A constant indicating that dirty reads, non-repeatable reads and phantom reads can occur.
|
||||||
|
*/
|
||||||
|
READ_UNCOMMITTED = 'read uncommitted',
|
||||||
|
/**
|
||||||
|
* A constant indicating that dirty reads are prevented; non-repeatable reads and phantom reads can occur.
|
||||||
|
*/
|
||||||
|
READ_COMMITTED = 'read committed',
|
||||||
|
/**
|
||||||
|
* A constant indicating that dirty reads and non-repeatable reads are prevented; phantom reads can occur.
|
||||||
|
*/
|
||||||
|
REPEATABLE_READ = 'repeatable read',
|
||||||
|
/**
|
||||||
|
* A constant indicating that dirty reads, non-repeatable reads and phantom reads are prevented.
|
||||||
|
*/
|
||||||
|
SERIALIZABLE = 'serializable',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} maybeTrx
|
||||||
|
* @returns {maybeTrx is import('objection').TransactionOrKnex & { executionPromise: Promise<any> }}
|
||||||
|
*/
|
||||||
|
function checkIsTransaction(maybeTrx) {
|
||||||
|
return Boolean(maybeTrx && maybeTrx.executionPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a transaction to be complete.
|
||||||
|
* @param {import('objection').TransactionOrKnex} [trx]
|
||||||
|
*/
|
||||||
|
export async function waitForTransaction(trx) {
|
||||||
|
return Promise.resolve(checkIsTransaction(trx) ? trx.executionPromise : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a callback when the transaction is done.
|
||||||
|
* @param {import('objection').TransactionOrKnex | undefined} trx
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
export function runAfterTransaction(trx, callback) {
|
||||||
|
waitForTransaction(trx).then(
|
||||||
|
() => {
|
||||||
|
// If transaction success, then run action
|
||||||
|
return Promise.resolve(callback()).catch((error) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// Ignore transaction error
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Transaction } from 'objection';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { IsolationLevel } from './TransactionsHooks';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UnitOfWork {
|
||||||
|
constructor(
|
||||||
|
@Inject(TENANCY_DB_CONNECTION)
|
||||||
|
private readonly tenantKex: Knex,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {} work
|
||||||
|
* @param {IsolationLevel} isolationLevel
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
public withTransaction = async <T>(
|
||||||
|
work: (knex: Knex.Transaction) => Promise<T> | T,
|
||||||
|
trx?: Transaction,
|
||||||
|
isolationLevel: IsolationLevel = IsolationLevel.READ_UNCOMMITTED,
|
||||||
|
): Promise<T> => {
|
||||||
|
const knex = this.tenantKex;
|
||||||
|
let _trx = trx;
|
||||||
|
|
||||||
|
if (!_trx) {
|
||||||
|
_trx = await knex.transaction({ isolationLevel });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await work(_trx);
|
||||||
|
|
||||||
|
if (!trx) {
|
||||||
|
_trx.commit();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (!trx) {
|
||||||
|
_trx.rollback();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestMiddleware,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { ClsService, ClsServiceManager } from 'nestjs-cls';
|
||||||
|
|
||||||
|
export class TenancyGlobalMiddleware implements NestMiddleware {
|
||||||
|
constructor(private readonly cls: ClsService) {}
|
||||||
|
/**
|
||||||
|
* Validates the organization ID in the request headers.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
public use(req: Request, res: Response, next: NextFunction) {
|
||||||
|
const organizationId = req.headers['organization-id'];
|
||||||
|
|
||||||
|
if (!organizationId) {
|
||||||
|
throw new UnauthorizedException('Organization ID is required.');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
CallHandler,
|
||||||
|
ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TenancyIdClsInterceptor implements NestInterceptor {
|
||||||
|
constructor(private readonly cls: ClsService) {}
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const organizationId = request.headers['organization-id'];
|
||||||
|
// this.cls.get('organizationId');
|
||||||
|
|
||||||
|
// console.log(organizationId, 'organizationId22');
|
||||||
|
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const TenancyModelsConnection = 'TenancyModelsConnection';
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Global, Module, Scope } from '@nestjs/common';
|
||||||
|
import { TENANCY_DB_CONNECTION } from '../TenancyDB/TenancyDB.constants';
|
||||||
|
import { Item } from '../../../modules/Items/models/Item';
|
||||||
|
import { Account } from '@/modules/Accounts/models/Account';
|
||||||
|
|
||||||
|
const models = [Item, Account];
|
||||||
|
const modelProviders = models.map((model) => {
|
||||||
|
return {
|
||||||
|
provide: model.name,
|
||||||
|
inject: [TENANCY_DB_CONNECTION],
|
||||||
|
scope: Scope.REQUEST,
|
||||||
|
useFactory: async (tenantKnex: Knex) => {
|
||||||
|
return model.bindKnex(tenantKnex);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [...modelProviders],
|
||||||
|
exports: [...modelProviders],
|
||||||
|
})
|
||||||
|
export class TenancyModelsModule {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { UseGuards } from '@nestjs/common';
|
||||||
|
import { EnsureTenantIsSeededGuard } from '../Tenancy/EnsureTenantIsSeeded.guards';
|
||||||
|
import { EnsureTenantIsInitializedGuard } from '../Tenancy/EnsureTenantIsInitialized.guard';
|
||||||
|
|
||||||
|
@UseGuards(EnsureTenantIsInitializedGuard)
|
||||||
|
@UseGuards(EnsureTenantIsSeededGuard)
|
||||||
|
export class TenantController {}
|
||||||
24
packages/server-nest/test/app.e2e-spec.ts
Normal file
24
packages/server-nest/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { AppModule } from '../src/modules/App/App.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
9
packages/server-nest/test/jest-e2e.json
Normal file
9
packages/server-nest/test/jest-e2e.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/server-nest/tsconfig.build.json
Normal file
4
packages/server-nest/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
24
packages/server-nest/tsconfig.json
Normal file
24
packages/server-nest/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2498
pnpm-lock.yaml
generated
2498
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user