Compare commits

...

47 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
a10b58a7c6 refactor: mail services to nestjs 2025-01-13 16:06:55 +02:00
Ahmed Bouhuolia
72818759a5 refactor: import resource module to nestjs 2025-01-13 10:15:57 +02:00
Ahmed Bouhuolia
270b421a6c refactor: dynamic list to nestjs 2025-01-12 18:22:48 +02:00
Ahmed Bouhuolia
ddaea20d16 fix: e2e test cases 2025-01-11 18:03:59 +02:00
Ahmed Bouhuolia
7e82080cb7 refactor: settings to nestjs 2025-01-11 11:02:57 +02:00
Ahmed Bouhuolia
3bf5f4be86 refactor: events tracker to nestjs 2025-01-08 17:26:11 +02:00
Ahmed Bouhuolia
6f870ea1e1 refactor: save settings service 2025-01-08 17:17:01 +02:00
Ahmed Bouhuolia
ee284196eb refactor: inventory adjustments e2e test cases 2025-01-08 15:43:43 +02:00
Ahmed Bouhuolia
52362a43ab refactor: events tracker to nestjs 2025-01-08 11:59:55 +02:00
Ahmed Bouhuolia
fdfb766587 refactor: inventory adjustments to GL 2025-01-07 23:00:21 +02:00
Ahmed Bouhuolia
1773df1858 refactor: inventory adjustments to nestjs 2025-01-07 22:17:23 +02:00
Ahmed Bouhuolia
abf92ac83f refactor: settings module to Nestjs 2025-01-07 20:43:31 +02:00
Ahmed Bouhuolia
385d84d654 refactor: bank rules e2e test cases 2025-01-06 18:10:24 +02:00
Ahmed Bouhuolia
2bf58d9cb4 refactor: banking modules to nestjs 2025-01-06 11:45:58 +02:00
Ahmed Bouhuolia
ba176394c8 refactor: banking modules to nestjs 2025-01-05 23:06:33 +02:00
Ahmed Bouhuolia
1869ba216f refactor: banking services to Nestjs 2025-01-05 16:26:23 +02:00
Ahmed Bouhuolia
b72f85b394 refactor: e2e 2025-01-01 14:39:25 +02:00
Ahmed Bouhuolia
8bacf3a001 refactor: migrate to nestjs 2025-01-01 13:39:31 +02:00
Ahmed Bouhuolia
505c4b28a5 refactor: migrate ledger writer to nestjs 2025-01-01 12:11:58 +02:00
Ahmed Bouhuolia
3ad34ba56f refactor: migrate ledger subscribers to nestjs 2024-12-31 23:51:24 +02:00
Ahmed Bouhuolia
a819d6c1ba refactor: GL entries 2024-12-31 14:57:24 +02:00
Ahmed Bouhuolia
1b15261adb feat: add attachments support to Expense model and transformer 2024-12-30 22:55:56 +02:00
Ahmed Bouhuolia
4938db704e refactor: expense GL to Nestjs 2024-12-30 22:08:50 +02:00
Ahmed Bouhuolia
3191076762 refactor: e2e tests for payments received 2024-12-30 20:59:19 +02:00
Ahmed Bouhuolia
b046edf337 refactor: e2e tests for payment received 2024-12-30 20:58:56 +02:00
Ahmed Bouhuolia
515a984714 refactor: migrate to Nestjs 2024-12-30 15:54:53 +02:00
Ahmed Bouhuolia
77bbf6828d refactor: credit notes and vendor credits to Nestjs 2024-12-29 22:55:42 +02:00
Ahmed Bouhuolia
caf235e2b5 refactor: migrate credit note and vendor credit services to nestjs 2024-12-29 18:37:33 +02:00
Ahmed Bouhuolia
9f9b75cd31 Merge branch 'develop' into migrate-server-nestjs 2024-12-29 11:14:15 +02:00
Ahmed Bouhuolia
736cedd63d feat: migrate manual journal to nestjs 2024-12-26 20:57:21 +02:00
Ahmed Bouhuolia
cd84872a61 feat: wip migrate to nestjs 2024-12-26 15:40:29 +02:00
Ahmed Bouhuolia
a6932d76f3 refactor: wip to nestjs 2024-12-25 00:43:55 +02:00
Ahmed Bouhuolia
336171081e refactor: sale estimates to nestjs 2024-12-22 14:16:01 +02:00
Ahmed Bouhuolia
8a12caf48d refactor: warehouses to nestjs 2024-12-21 15:07:01 +02:00
Ahmed Bouhuolia
cb8fd68d46 refactor: branches and warehouses to nestjs 2024-12-21 00:10:09 +02:00
Ahmed Bouhuolia
dc52f784b6 refactor: pdf templates to nestjs 2024-12-20 14:53:31 +02:00
Ahmed Bouhuolia
330192c042 refactor: tax rates to nestjs 2024-12-20 12:24:50 +02:00
Ahmed Bouhuolia
1f32a7c59a refactor: migrate item categories to nestjs 2024-12-20 10:40:35 +02:00
Ahmed Bouhuolia
83dfaa00fd refactor: migrate item categories module to nestjs 2024-12-19 19:06:03 +02:00
Ahmed Bouhuolia
93bf6d9d3d refactor: wip migrate ot nestjs 2024-12-19 12:48:24 +02:00
Ahmed Bouhuolia
bfff56c470 refactor: accounts module to Nestjs 2024-12-16 16:45:56 +02:00
Ahmed Bouhuolia
87e9cd64e8 refactor: items services to Nestjs 2024-12-15 15:23:46 +02:00
Ahmed Bouhuolia
0a112c5655 refactor: items services to Nestjs 2024-12-15 13:04:41 +02:00
Ahmed Bouhuolia
70211980aa feat: replace all src/ imports to @ alias 2024-12-15 00:50:10 +02:00
Ahmed Bouhuolia
2ba31148ca feat: initialize the server e2e tests 2024-12-15 00:49:10 +02:00
Ahmed Bouhuolia
05f4b49b58 wip 2024-11-16 11:55:27 +02:00
Ahmed Bouhuolia
19080a67ab feat: wip migrate server to nestjs 2024-11-12 23:08:51 +02:00
1139 changed files with 86709 additions and 1571 deletions

View File

@@ -9,7 +9,9 @@
"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\"",
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"test:e2e": "playwright test",
"server2:start": "lerna run start:dev --scope \"@bigcapital/server2\"",
"test:watch": "lerna run test:watch",
"test:e2e": "lerna run test:e2e",
"prepare": "husky install"
},
"devDependencies": {

View 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=

View 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
View 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

View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -0,0 +1 @@
- Build authentication services.

View File

@@ -0,0 +1 @@
## @bigcapitalhq/server

View 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 }
]
}
}

View File

@@ -0,0 +1,127 @@
{
"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 --watchAll"
},
"dependencies": {
"@bigcapital/email-components": "*",
"@bigcapital/pdf-templates": "*",
"@bigcapital/utils": "*",
"@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",
"@supercharge/promise-pool": "^3.2.0",
"@types/nodemailer": "^6.4.17",
"@types/passport-local": "^1.0.38",
"@types/ramda": "^0.30.2",
"accounting": "^0.4.1",
"async": "^3.2.0",
"axios": "^1.6.0",
"bluebird": "^3.7.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",
"form-data": "^4.0.0",
"fp-ts": "^2.16.9",
"is-my-json-valid": "^2.20.5",
"js-money": "^0.6.3",
"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",
"nodemailer": "^6.3.0",
"object-hash": "^2.0.3",
"objection": "^3.1.5",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"plaid": "^10.3.0",
"pluralize": "^8.0.0",
"posthog-node": "^4.3.2",
"pug": "^3.0.2",
"ramda": "^0.30.1",
"redis": "^4.7.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"serialize-interceptor": "^1.1.7",
"strategy": "^1.1.1",
"uniqid": "^5.2.0",
"uuid": "^10.0.0",
"yup": "^0.28.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",
"@types/yup": "^0.29.13",
"@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"
}
}

View 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,
}));

View 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,
];

View 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,
}));

View File

@@ -0,0 +1,5 @@
import { registerAs } from '@nestjs/config';
export default registerAs('openExchange', () => ({
appId: process.env.OPEN_EXCHANGE_RATE_APP_ID,
}));

View 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,
}));

View 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',
}));

View 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,
}));

View 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',
}));

View 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,
}));

View 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,
}));

View File

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

View File

@@ -0,0 +1,756 @@
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',
onViewed: 'onSaleEstimateViewed',
},
/**
* 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',
},
};

View File

@@ -0,0 +1,6 @@
export class ModelEntityNotFound extends Error {
constructor(entityId, message?) {
message = message || `Entity with id ${entityId} does not exist`;
super(message);
}
}

View File

@@ -0,0 +1,24 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus,
} from '@nestjs/common';
import { Response } from 'express';
import { ServiceError } from '@/modules/Items/ServiceError';
@Catch(ServiceError)
export class ServiceErrorFilter implements ExceptionFilter {
catch(exception: ServiceError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
errorType: exception.errorType,
message: exception.message,
payload: exception.payload,
});
}
}

View File

@@ -0,0 +1,78 @@
import {
type ExecutionContext,
Injectable,
type NestInterceptor,
type CallHandler,
Optional,
} from '@nestjs/common';
import { type Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export function camelToSnake<T = any>(value: T) {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map(camelToSnake);
}
if (typeof value === 'object' && !(value instanceof Date)) {
return Object.fromEntries(
Object.entries(value).map(([key, value]) => [
key
.split(/(?=[A-Z])/)
.join('_')
.toLowerCase(),
camelToSnake(value),
]),
);
}
return value;
}
export function snakeToCamel<T = any>(value: T) {
if (value === null || value === undefined) {
return value;
}
if (Array.isArray(value)) {
return value.map(snakeToCamel);
}
const impl = (str: string) => {
const converted = str.replace(/([-_]\w)/g, (group) =>
group[1].toUpperCase(),
);
return converted[0].toLowerCase() + converted.slice(1);
};
if (typeof value === 'object' && !(value instanceof Date)) {
return Object.fromEntries(
Object.entries(value).map(([key, value]) => [
impl(key),
snakeToCamel(value),
]),
);
}
return value;
}
export const DEFAULT_STRATEGY = {
in: snakeToCamel,
out: camelToSnake,
};
@Injectable()
export class SerializeInterceptor implements NestInterceptor<any, any> {
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) {}
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
const request = context.switchToHttp().getRequest();
request.body = this.strategy.in(request.body);
// handle returns stream..
return next.handle().pipe(map(this.strategy.out));
}
}

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

View File

@@ -0,0 +1,260 @@
// import hashObject from 'object-hash';
// import { EntityRepository } from './EntityRepository';
// export class CachableRepository extends EntityRepository {
// repositoryName: string;
// cache: any;
// i18n: any;
// /**
// * Constructor method.
// * @param {Knex} knex
// * @param {Cache} cache
// */
// constructor(knex, cache, i18n) {
// super(knex);
// this.cache = cache;
// this.i18n = i18n;
// this.repositoryName = this.constructor.name;
// }
// getByCache(key, callback) {
// return callback();
// }
// /**
// * Retrieve the cache key of the method name and arguments.
// * @param {string} method
// * @param {...any} args
// * @return {string}
// */
// getCacheKey(method, ...args) {
// const hashArgs = hashObject({ ...args });
// const repositoryName = this.repositoryName;
// return `${repositoryName}-${method}-${hashArgs}`;
// }
// /**
// * Retrieve all entries with specified relations.
// * @param withRelations
// */
// all(withRelations?, trx?) {
// const cacheKey = this.getCacheKey('all', withRelations);
// return this.getByCache(cacheKey, () => {
// return super.all(withRelations, trx);
// });
// }
// /**
// * Finds list of entities with specified attributes
// * @param {Object} attributeValues - values to filter retrieved entities by
// * @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve.
// * @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
// */
// find(attributeValues = {}, withRelations?) {
// const cacheKey = this.getCacheKey('find', attributeValues, withRelations);
// return this.getByCache(cacheKey, () => {
// return super.find(attributeValues, withRelations);
// });
// }
// /**
// * Finds list of entities with attribute values that are different from specified ones
// * @param {Object} attributeValues - values to filter retrieved entities by
// * @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
// * @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
// */
// findWhereNot(attributeValues = {}, withRelations?) {
// const cacheKey = this.getCacheKey(
// 'findWhereNot',
// attributeValues,
// withRelations
// );
// return this.getByCache(cacheKey, () => {
// return super.findWhereNot(attributeValues, withRelations);
// });
// }
// /**
// * Finds list of entities with specified attributes (any of multiple specified values)
// * Supports both ('attrName', ['value1', 'value2]) and ({attrName: ['value1', 'value2']} formats)
// *
// * @param {string|Object} searchParam - attribute name or search criteria object
// * @param {*[]} [attributeValues] - attribute values to filter retrieved entities by
// * @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
// * @returns {PromiseLike<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
// */
// findWhereIn(searchParam, attributeValues, withRelations?) {
// const cacheKey = this.getCacheKey(
// 'findWhereIn',
// attributeValues,
// withRelations
// );
// return this.getByCache(cacheKey, () => {
// return super.findWhereIn(searchParam, attributeValues, withRelations);
// });
// }
// /**
// * Finds first entity by given parameters
// *
// * @param {Object} attributeValues - values to filter retrieved entities by
// * @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
// * @returns {Promise<Object>}
// */
// findOne(attributeValues = {}, withRelations?) {
// const cacheKey = this.getCacheKey(
// 'findOne',
// attributeValues,
// withRelations
// );
// return this.getByCache(cacheKey, () => {
// return super.findOne(attributeValues, withRelations);
// });
// }
// /**
// * Finds first entity by given parameters
// *
// * @param {string || number} id - value of id column of the entity
// * @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
// * @returns {Promise<Object>}
// */
// findOneById(id, withRelations?) {
// const cacheKey = this.getCacheKey('findOneById', id, withRelations);
// return this.getByCache(cacheKey, () => {
// return super.findOneById(id, withRelations);
// });
// }
// /**
// * Persists new entity or an array of entities.
// * This method does not recursively persist related entities, use createRecursively (to be implemented) for that.
// * Batch insert only works on PostgreSQL
// * @param {Object} entity - model instance or parameters for a new entity
// * @returns {Promise<Object>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
// */
// async create(entity, trx?) {
// const result = await super.create(entity, trx);
// // Flushes the repository cache after insert operation.
// this.flushCache();
// return result;
// }
// /**
// * Persists updated entity. If previously set fields are not present, performs an incremental update (does not remove fields unless explicitly set to null)
// *
// * @param {Object} entity - single entity instance
// * @param {Object} [trx] - knex transaction instance. If not specified, new implicit transaction will be used.
// * @returns {Promise<integer>} number of affected rows
// */
// async update(entity, whereAttributes?, trx?) {
// const result = await super.update(entity, whereAttributes, trx);
// // Flushes the repository cache after update operation.
// this.flushCache();
// return result;
// }
// /**
// * @param {Object} attributeValues - values to filter deleted entities by
// * @param {Object} [trx]
// * @returns {Promise<integer>} Query builder. After promise is resolved, returns count of deleted rows
// */
// async deleteBy(attributeValues, trx?) {
// const result = await super.deleteBy(attributeValues, trx);
// this.flushCache();
// return result;
// }
// /**
// * @param {string || number} id - value of id column of the entity
// * @returns {Promise<integer>} Query builder. After promise is resolved, returns count of deleted rows
// */
// deleteById(id: number | string, trx?) {
// const result = super.deleteById(id, trx);
// // Flushes the repository cache after insert operation.
// this.flushCache();
// return result;
// }
// /**
// *
// * @param {string|number[]} values -
// */
// async deleteWhereIn(field: string, values: string | number[]) {
// const result = await super.deleteWhereIn(field, values);
// // Flushes the repository cache after delete operation.
// this.flushCache();
// return result;
// }
// /**
// *
// * @param {string|number[]} values
// */
// async deleteWhereIdIn(values: string | number[], trx?) {
// const result = await super.deleteWhereIdIn(values, trx);
// // Flushes the repository cache after delete operation.
// this.flushCache();
// return result;
// }
// /**
// *
// * @param graph
// * @param options
// */
// async upsertGraph(graph, options) {
// const result = await super.upsertGraph(graph, options);
// // Flushes the repository cache after insert operation.
// this.flushCache();
// return result;
// }
// /**
// *
// * @param {} whereAttributes
// * @param {string} field
// * @param {number} amount
// */
// async changeNumber(whereAttributes, field: string, amount: number, trx?) {
// const result = await super.changeNumber(
// whereAttributes,
// field,
// amount,
// trx
// );
// // Flushes the repository cache after update operation.
// this.flushCache();
// return result;
// }
// /**
// * Flush repository cache.
// */
// flushCache(): void {
// this.cache.delStartWith(this.repositoryName);
// }
// }

View File

@@ -0,0 +1,232 @@
import { cloneDeep, forOwn, isString } from 'lodash';
import { ModelEntityNotFound } from '../exceptions/ModelEntityNotFound';
import { Model } from 'objection';
function applyGraphFetched(withRelations, builder) {
const relations = Array.isArray(withRelations)
? withRelations
: typeof withRelations === 'string'
? withRelations.split(',').map((relation) => relation.trim())
: [];
relations.forEach((relation) => {
builder.withGraphFetched(relation);
});
}
export class EntityRepository {
idColumn: string = 'id';
knex: any;
/**
* Retrieve the repository model binded it to knex instance.
*/
get model(): typeof Model {
throw new Error("The repository's model is not defined.");
}
/**
* Retrieve all entries with specified relations.
* @param withRelations
*/
all(withRelations?, trx?) {
const builder = this.model.query(trx);
applyGraphFetched(withRelations, builder);
return builder;
}
/**
* Finds list of entities with specified attributes
*
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve.
* @returns {Promise<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
find(attributeValues = {}, withRelations?) {
const builder = this.model.query().where(attributeValues);
applyGraphFetched(withRelations, builder);
return builder;
}
/**
* Finds list of entities with attribute values that are different from specified ones
*
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {PromiseLike<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
findWhereNot(attributeValues = {}, withRelations?) {
const builder = this.model.query().whereNot(attributeValues);
applyGraphFetched(withRelations, builder);
return builder;
}
/**
* Finds list of entities with specified attributes (any of multiple specified values)
* Supports both ('attrName', ['value1', 'value2]) and ({attrName: ['value1', 'value2']} formats)
*
* @param {string|Object} searchParam - attribute name or search criteria object
* @param {*[]} [attributeValues] - attribute values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {PromiseLike<Object[]>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
findWhereIn(searchParam, attributeValues, withRelations?) {
const commonBuilder = (builder) => {
applyGraphFetched(withRelations, builder);
};
if (isString(searchParam)) {
return this.model
.query()
.whereIn(searchParam, attributeValues)
.onBuild(commonBuilder);
} else {
const builder = this.model.query(this.knex).onBuild(commonBuilder);
forOwn(searchParam, (value, key) => {
if (Array.isArray(value)) {
builder.whereIn(key, value);
} else {
builder.where(key, value);
}
});
return builder;
}
}
/**
* Finds first entity by given parameters
*
* @param {Object} attributeValues - values to filter retrieved entities by
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {Promise<Object>}
*/
async findOne(attributeValues = {}, withRelations?) {
const results = await this.find(attributeValues, withRelations);
return results[0] || null;
}
/**
* Finds first entity by given parameters
*
* @param {string || number} id - value of id column of the entity
* @param {string || string[]} [withRelations] - name of relation(s) to eagerly retrieve, as defined in model relationMappings()
* @returns {Promise<Object>}
*/
findOneById(id, withRelations?) {
return this.findOne({ [this.idColumn]: id }, withRelations);
}
/**
* Persists new entity or an array of entities.
* This method does not recursively persist related entities, use createRecursively (to be implemented) for that.
* Batch insert only works on PostgreSQL
*
* @param {Object} entity - model instance or parameters for a new entity
* @returns {Promise<Object>} - query builder. You can chain additional methods to it or call "await" or then() on it to execute
*/
create(entity, trx?) {
// Keep the input parameter immutable
const instanceDTO = cloneDeep(entity);
return this.model.query(trx).insert(instanceDTO);
}
/**
* Persists updated entity. If previously set fields are not present, performs an incremental update (does not remove fields unless explicitly set to null)
*
* @param {Object} entity - single entity instance
* @returns {Promise<integer>} number of affected rows
*/
async update(entity, whereAttributes?, trx?) {
const entityDto = cloneDeep(entity);
const identityClause = {};
if (Array.isArray(this.idColumn)) {
this.idColumn.forEach(
(idColumn) => (identityClause[idColumn] = entityDto[idColumn]),
);
} else {
identityClause[this.idColumn] = entityDto[this.idColumn];
}
const whereConditions = whereAttributes || identityClause;
const modifiedEntitiesCount = await this.model
.query(trx)
.where(whereConditions)
.update(entityDto);
if (modifiedEntitiesCount === 0) {
throw new ModelEntityNotFound(entityDto[this.idColumn]);
}
return modifiedEntitiesCount;
}
/**
*
* @param {Object} attributeValues - values to filter deleted entities by
* @param {Object} [trx]
* @returns {Promise<integer>} Query builder. After promise is resolved, returns count of deleted rows
*/
deleteBy(attributeValues, trx?) {
return this.model.query(trx).delete().where(attributeValues);
}
/**
* @param {string || number} id - value of id column of the entity
* @returns {Promise<integer>} Query builder. After promise is resolved, returns count of deleted rows
*/
deleteById(id: number | string, trx?) {
return this.deleteBy(
{
[this.idColumn]: id,
},
trx,
);
}
/**
* Deletes the given entries in the array on the specific field.
* @param {string} field -
* @param {number|string} values -
*/
deleteWhereIn(field: string, values: (string | number)[], trx) {
return this.model.query(trx).whereIn(field, values).delete();
}
/**
*
* @param {string|number[]} values
*/
deleteWhereIdIn(values: (string | number)[], trx?) {
return this.deleteWhereIn(this.idColumn, values, trx);
}
/**
* Arbitrary relation graphs can be upserted (insert + update + delete)
* using the upsertGraph method.
* @param graph
* @param options
*/
upsertGraph(graph, options) {
// Keep the input grpah immutable
const graphCloned = cloneDeep(graph);
return this.model.query().upsertGraph(graphCloned, options);
}
/**
*
* @param {object} whereAttributes
* @param {string} field
* @param amount
*/
changeNumber(whereAttributes, field: string, amount: number, trx) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.model
.query(trx)
.where(whereAttributes)
[changeMethod](field, Math.abs(amount));
}
}

View File

@@ -0,0 +1,6 @@
// import { CachableRepository } from './CachableRepository';
import { EntityRepository } from './EntityRepository';
export class TenantRepository extends EntityRepository {
}

View File

@@ -0,0 +1,4 @@
export enum DiscountType {
Percentage = 'percentage',
Amount = 'amount',
}

View File

@@ -0,0 +1,16 @@
export enum Features {
WAREHOUSES = 'warehouses',
BRANCHES = 'branches',
BankSyncing = 'BankSyncing',
}
export interface IFeatureAllItem {
name: string;
isAccessible: boolean;
defaultAccessible: boolean;
}
export interface IFeatureConfiugration {
name: string;
defaultValue?: boolean;
}

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

View 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,
};

View File

@@ -0,0 +1,241 @@
// import { getTransactionsLockingSettingsSchema } from '@/api/controllers/TransactionsLocking/utils';
export const SettingsOptions = {
organization: {
name: {
type: 'string',
},
base_currency: {
type: 'string',
},
industry: {
type: 'string',
},
location: {
type: 'string',
},
fiscal_year: {
type: 'string',
},
financial_date_start: {
type: 'string',
},
language: {
type: 'string',
},
time_zone: {
type: 'string',
},
date_format: {
type: 'string',
},
accounting_basis: {
type: 'string',
},
},
manual_journals: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
},
bill_payments: {
withdrawal_account: {
type: 'number',
},
},
sales_estimates: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
customer_notes: {
type: 'string',
},
terms_conditions: {
type: 'string',
},
},
sales_receipts: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
preferred_deposit_account: {
type: 'number',
},
receipt_message: {
type: 'string',
},
terms_conditions: {
type: 'string',
},
},
sales_invoices: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
customer_notes: {
type: 'string',
},
terms_conditions: {
type: 'string',
},
},
payment_receives: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
preferred_deposit_account: {
type: 'number',
},
preferred_advance_deposit: {
type: 'number',
},
},
items: {
preferred_sell_account: {
type: 'number',
},
preferred_cost_account: {
type: 'number',
},
preferred_inventory_account: {
type: 'number',
},
},
expenses: {
preferred_payment_account: {
type: 'number',
},
},
inventory: {
cost_compute_running: {
type: 'boolean',
},
},
accounts: {
account_code_required: {
type: 'boolean',
},
account_code_unique: {
type: 'boolean',
},
},
cashflow: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
},
credit_note: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
customer_notes: {
type: 'string',
},
terms_conditions: {
type: 'string',
},
},
vendor_credit: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
},
warehouse_transfers: {
next_number: {
type: 'string',
},
number_prefix: {
type: 'string',
},
auto_increment: {
type: 'boolean',
},
},
'sms-notification': {
'sms-notification-enable.sale-invoice-details': {
type: 'boolean',
},
'sms-notification-enable.sale-invoice-reminder': {
type: 'boolean',
},
'sms-notification-enable.sale-estimate-details': {
type: 'boolean',
},
'sms-notification-enable.sale-receipt-details': {
type: 'boolean',
},
'sms-notification-enable.payment-receive-details': {
type: 'boolean',
},
'sms-notification-enable.customer-balance': {
type: 'boolean',
},
},
'transactions-locking': {
'locking-type': {
type: 'string',
},
// ...getTransactionsLockingSettingsSchema([
// 'all',
// 'sales',
// 'purchases',
// 'financial',
// ]),
},
features: {
'multi-warehouses': {
type: 'boolean',
},
'multi-branches': {
type: 'boolean',
},
},
};

View 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} }})"
}

View File

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

View File

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

View 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();
}
}

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

View File

@@ -0,0 +1,165 @@
import { ApiProperty } from '@nestjs/swagger';
import { Knex } from 'knex';
import { Item } from '@/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: Item;
}
export interface IItemEventDeletedPayload {
// tenantId: number;
itemId: number;
oldItem: Item;
trx: Knex.Transaction;
}
export enum ItemAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
}
// export type ItemAbility = [ItemAction, AbilitySubject.Item];

View File

@@ -0,0 +1,193 @@
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: Record<string, IModelMetaField>;
fields2: Record<string, IModelMetaField2>;
columns: Record<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);

View File

@@ -0,0 +1,8 @@
export interface SubscriptionPayload {
lemonSqueezyId?: string;
}
export enum SubscriptionPaymentStatus {
Succeed = 'succeed',
Failed = 'failed',
}

View File

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

View File

@@ -0,0 +1,26 @@
import { ChromiumRoute, LibreOfficeRoute, PdfEngineRoute } from './_types';
export class Chromiumly {
public static readonly GOTENBERG_ENDPOINT = process.env.GOTENBERG_URL || '';
public static readonly CHROMIUM_PATH = 'forms/chromium/convert';
public static readonly PDF_ENGINES_PATH = 'forms/pdfengines';
public static readonly LIBRE_OFFICE_PATH = 'forms/libreoffice';
public static readonly GOTENBERG_DOCS_ENDPOINT =
process.env.GOTENBERG_DOCS_URL || '';
public static readonly CHROMIUM_ROUTES = {
url: ChromiumRoute.URL,
html: ChromiumRoute.HTML,
markdown: ChromiumRoute.MARKDOWN,
};
public static readonly PDF_ENGINE_ROUTES = {
merge: PdfEngineRoute.MERGE,
};
public static readonly LIBRE_OFFICE_ROUTES = {
convert: LibreOfficeRoute.CONVERT,
};
}

View File

@@ -0,0 +1,66 @@
import FormData from 'form-data';
import { GotenbergUtils } from './GotenbergUtils';
import { PageProperties } from './_types';
export class ConverterUtils {
public static injectPageProperties(
data: FormData,
pageProperties: PageProperties
): void {
if (pageProperties.size) {
GotenbergUtils.assert(
pageProperties.size.width >= 1.0 && pageProperties.size.height >= 1.5,
'size is smaller than the minimum printing requirements (i.e. 1.0 x 1.5 in)'
);
data.append('paperWidth', pageProperties.size.width);
data.append('paperHeight', pageProperties.size.height);
}
if (pageProperties.margins) {
GotenbergUtils.assert(
pageProperties.margins.top >= 0 &&
pageProperties.margins.bottom >= 0 &&
pageProperties.margins.left >= 0 &&
pageProperties.margins.left >= 0,
'negative margins are not allowed'
);
data.append('marginTop', pageProperties.margins.top);
data.append('marginBottom', pageProperties.margins.bottom);
data.append('marginLeft', pageProperties.margins.left);
data.append('marginRight', pageProperties.margins.right);
}
if (pageProperties.preferCssPageSize) {
data.append(
'preferCssPageSize',
String(pageProperties.preferCssPageSize)
);
}
if (pageProperties.printBackground) {
data.append('printBackground', String(pageProperties.printBackground));
}
if (pageProperties.landscape) {
data.append('landscape', String(pageProperties.landscape));
}
if (pageProperties.scale) {
GotenbergUtils.assert(
pageProperties.scale >= 0.1 && pageProperties.scale <= 2.0,
'scale is outside of [0.1 - 2] range'
);
data.append('scale', pageProperties.scale);
}
if (pageProperties.nativePageRanges) {
GotenbergUtils.assert(
pageProperties.nativePageRanges.from > 0 &&
pageProperties.nativePageRanges.to > 0 &&
pageProperties.nativePageRanges.to >=
pageProperties.nativePageRanges.from,
'page ranges syntax error'
);
data.append(
'nativePageRanges',
`${pageProperties.nativePageRanges.from}-${pageProperties.nativePageRanges.to}`
);
}
}
}

View File

@@ -0,0 +1,10 @@
import { Chromiumly } from './Chromiumly';
import { ChromiumRoute } from './_types';
export abstract class Converter {
readonly endpoint: string;
constructor(route: ChromiumRoute) {
this.endpoint = `${Chromiumly.GOTENBERG_ENDPOINT}/${Chromiumly.CHROMIUM_PATH}/${Chromiumly.CHROMIUM_ROUTES[route]}`;
}
}

View File

@@ -0,0 +1,24 @@
import FormData from 'form-data';
import Axios from 'axios';
export class GotenbergUtils {
public static assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
public static async fetch(endpoint: string, data: FormData): Promise<Buffer> {
try {
const response = await Axios.post(endpoint, data, {
headers: {
...data.getHeaders(),
},
responseType: 'arraybuffer', // This ensures you get a Buffer bac
});
return response.data;
} catch (error) {
console.error(error);
}
}
}

View File

@@ -0,0 +1,38 @@
import { constants, createReadStream, PathLike, promises } from 'fs';
import FormData from 'form-data';
import { GotenbergUtils } from './GotenbergUtils';
import { IConverter, PageProperties } from './_types';
import { PdfFormat, ChromiumRoute } from './_types';
import { ConverterUtils } from './ConvertUtils';
import { Converter } from './Converter';
export class HtmlConverter extends Converter implements IConverter {
constructor() {
super(ChromiumRoute.HTML);
}
async convert({
html,
properties,
pdfFormat,
}: {
html: PathLike;
properties?: PageProperties;
pdfFormat?: PdfFormat;
}): Promise<Buffer> {
try {
await promises.access(html, constants.R_OK);
const data = new FormData();
if (pdfFormat) {
data.append('pdfFormat', pdfFormat);
}
data.append('index.html', createReadStream(html));
if (properties) {
ConverterUtils.injectPageProperties(data, properties);
}
return GotenbergUtils.fetch(this.endpoint, data);
} catch (error) {
throw error;
}
}
}

View File

@@ -0,0 +1,38 @@
import FormData from 'form-data';
import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types';
import { ConverterUtils } from './ConvertUtils';
import { Converter } from './Converter';
import { GotenbergUtils } from './GotenbergUtils';
export class UrlConverter extends Converter implements IConverter {
constructor() {
super(ChromiumRoute.URL);
}
async convert({
url,
properties,
pdfFormat,
}: {
url: string;
properties?: PageProperties;
pdfFormat?: PdfFormat;
}): Promise<Buffer> {
try {
const _url = new URL(url);
const data = new FormData();
if (pdfFormat) {
data.append('pdfFormat', pdfFormat);
}
data.append('url', _url.href);
if (properties) {
ConverterUtils.injectPageProperties(data, properties);
}
return GotenbergUtils.fetch(this.endpoint, data);
} catch (error) {
throw error;
}
}
}

View File

@@ -0,0 +1,51 @@
import { PathLike } from 'fs';
export type PageSize = {
width: number; // Paper width, in inches (default 8.5)
height: number; //Paper height, in inches (default 11)
};
export type PageMargins = {
top: number; // Top margin, in inches (default 0.39)
bottom: number; // Bottom margin, in inches (default 0.39)
left: number; // Left margin, in inches (default 0.39)
right: number; // Right margin, in inches (default 0.39)
};
export type PageProperties = {
size?: PageSize;
margins?: PageMargins;
preferCssPageSize?: boolean; // Define whether to prefer page size as defined by CSS (default false)
printBackground?: boolean; // Print the background graphics (default false)
landscape?: boolean; // Set the paper orientation to landscape (default false)
scale?: number; // The scale of the page rendering (default 1.0)
nativePageRanges?: { from: number; to: number }; // Page ranges to print
};
export interface IConverter {
convert({
...args
}: {
[x: string]: string | PathLike | PageProperties | PdfFormat;
}): Promise<Buffer>;
}
export enum PdfFormat {
A_1a = 'PDF/A-1a',
A_2b = 'PDF/A-2b',
A_3b = 'PDF/A-3b',
}
export enum ChromiumRoute {
URL = 'url',
HTML = 'html',
MARKDOWN = 'markdown',
}
export enum PdfEngineRoute {
MERGE = 'merge',
}
export enum LibreOfficeRoute {
CONVERT = 'convert',
}

View 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;

View File

@@ -0,0 +1,172 @@
const OperationType = {
LOGIC: 'LOGIC',
STRING: 'STRING',
COMPARISON: 'COMPARISON',
MATH: 'MATH',
};
export class Lexer {
// operation table
static get optable() {
return {
'=': OperationType.LOGIC,
'&': OperationType.LOGIC,
'|': OperationType.LOGIC,
'?': OperationType.LOGIC,
':': OperationType.LOGIC,
'\'': OperationType.STRING,
'"': OperationType.STRING,
'!': OperationType.COMPARISON,
'>': OperationType.COMPARISON,
'<': OperationType.COMPARISON,
'(': OperationType.MATH,
')': OperationType.MATH,
'+': OperationType.MATH,
'-': OperationType.MATH,
'*': OperationType.MATH,
'/': OperationType.MATH,
'%': OperationType.MATH,
};
}
/**
* Constructor
* @param {*} expression -
*/
constructor(expression) {
this.currentIndex = 0;
this.input = expression;
this.tokenList = [];
}
getTokens() {
let tok;
do {
// read current token, so step should be -1
tok = this.pickNext(-1);
const pos = this.currentIndex;
switch (Lexer.optable[tok]) {
case OperationType.LOGIC:
// == && || ===
this.readLogicOpt(tok);
break;
case OperationType.STRING:
this.readString(tok);
break;
case OperationType.COMPARISON:
this.readCompare(tok);
break;
case OperationType.MATH:
this.receiveToken();
break;
default:
this.readValue(tok);
}
// if the pos not changed, this loop will go into a infinite loop, every step of while loop,
// we must move the pos forward
// so here we should throw error, for example `1 & 2`
if (pos === this.currentIndex && tok !== undefined) {
const err = new Error(`unkonw token ${tok} from input string ${this.input}`);
err.name = 'UnknowToken';
throw err;
}
} while (tok !== undefined)
return this.tokenList;
}
/**
* read next token, the index param can set next step, default go foward 1 step
*
* @param index next postion
*/
pickNext(index = 0) {
return this.input[index + this.currentIndex + 1];
}
/**
* Store token into result tokenList, and move the pos index
*
* @param index
*/
receiveToken(index = 1) {
const tok = this.input.slice(this.currentIndex, this.currentIndex + index).trim();
// skip empty string
if (tok) {
this.tokenList.push(tok);
}
this.currentIndex += index;
}
// ' or "
readString(tok) {
let next;
let index = 0;
do {
next = this.pickNext(index);
index += 1;
} while (next !== tok && next !== undefined);
this.receiveToken(index + 1);
}
// > or < or >= or <= or !==
// tok in (>, <, !)
readCompare(tok) {
if (this.pickNext() !== '=') {
this.receiveToken(1);
return;
}
// !==
if (tok === '!' && this.pickNext(1) === '=') {
this.receiveToken(3);
return;
}
this.receiveToken(2);
}
// === or ==
// && ||
readLogicOpt(tok) {
if (this.pickNext() === tok) {
// ===
if (tok === '=' && this.pickNext(1) === tok) {
return this.receiveToken(3);
}
// == && ||
return this.receiveToken(2);
}
// handle as &&
// a ? b : c is equal to a && b || c
if (tok === '?' || tok === ':') {
return this.receiveToken(1);
}
}
readValue(tok) {
if (!tok) {
return;
}
let index = 0;
while (!Lexer.optable[tok] && tok !== undefined) {
tok = this.pickNext(index);
index += 1;
}
this.receiveToken(index);
}
}
export default function token(expression) {
const lexer = new Lexer(expression);
return lexer.getTokens();
}

View File

@@ -0,0 +1,159 @@
export const OPERATION = {
'!': 5,
'*': 4,
'/': 4,
'%': 4,
'+': 3,
'-': 3,
'>': 2,
'<': 2,
'>=': 2,
'<=': 2,
'===': 2,
'!==': 2,
'==': 2,
'!=': 2,
'&&': 1,
'||': 1,
'?': 1,
':': 1,
};
// export interface Node {
// left: Node | string | null;
// right: Node | string | null;
// operation: string;
// grouped?: boolean;
// };
export default class Parser {
constructor(token) {
this.index = -1;
this.blockLevel = 0;
this.token = token;
}
/**
*
* @return {Node | string} =-
*/
parse() {
let tok;
let root = {
left: null,
right: null,
operation: null,
};
do {
tok = this.parseStatement();
if (tok === null || tok === undefined) {
break;
}
if (root.left === null) {
root.left = tok;
root.operation = this.nextToken();
if (!root.operation) {
return tok;
}
root.right = this.parseStatement();
} else {
if (typeof tok !== 'string') {
throw new Error('operation must be string, but get ' + JSON.stringify(tok));
}
root = this.addNode(tok, this.parseStatement(), root);
}
} while (tok);
return root;
}
nextToken() {
this.index += 1;
return this.token[this.index];
}
prevToken() {
return this.token[this.index - 1];
}
/**
*
* @param {string} operation
* @param {Node|String|null} right
* @param {Node} root
*/
addNode(operation, right, root) {
let pre = root;
if (this.compare(pre.operation, operation) < 0 && !pre.grouped) {
while (pre.right !== null &&
typeof pre.right !== 'string' &&
this.compare(pre.right.operation, operation) < 0 && !pre.right.grouped) {
pre = pre.right;
}
pre.right = {
operation,
left: pre.right,
right,
};
return root;
}
return {
left: pre,
right,
operation,
}
}
/**
*
* @param {String} a
* @param {String} b
*/
compare(a, b) {
if (!OPERATION.hasOwnProperty(a) || !OPERATION.hasOwnProperty(b)) {
throw new Error(`unknow operation ${a} or ${b}`);
}
return OPERATION[a] - OPERATION[b];
}
/**
* @return string | Node | null
*/
parseStatement() {
const token = this.nextToken();
if (token === '(') {
this.blockLevel += 1;
const node = this.parse();
this.blockLevel -= 1;
if (typeof node !== 'string') {
node.grouped = true;
}
return node;
}
if (token === ')') {
return null;
}
if (token === '!') {
return { left: null, operation: token, right: this.parseStatement() }
}
// 3 > -12 or -12 + 10
if (token === '-' && (OPERATION[this.prevToken()] > 0 || this.prevToken() === undefined)) {
return { left: '0', operation: token, right: this.parseStatement(), grouped: true };
}
return token;
}
}

View File

@@ -0,0 +1,61 @@
import { OPERATION } from './Parser';
export default class QueryParser {
constructor(tree, queries) {
this.tree = tree;
this.queries = queries;
this.query = null;
}
setQuery(query) {
this.query = query.clone();
}
parse() {
return this.parseNode(this.tree);
}
parseNode(node) {
if (typeof node === 'string') {
const nodeQuery = this.getQuery(node);
return (query) => { nodeQuery(query); };
}
if (OPERATION[node.operation] === undefined) {
throw new Error(`unknow expression ${node.operation}`);
}
const leftQuery = this.getQuery(node.left);
const rightQuery = this.getQuery(node.right);
switch (node.operation) {
case '&&':
case 'AND':
default:
return (nodeQuery) => nodeQuery.where((query) => {
query.where((q) => { leftQuery(q); });
query.andWhere((q) => { rightQuery(q); });
});
case '||':
case 'OR':
return (nodeQuery) => nodeQuery.where((query) => {
query.where((q) => { leftQuery(q); });
query.orWhere((q) => { rightQuery(q); });
});
}
}
getQuery(node) {
if (typeof node !== 'string' && node !== null) {
return this.parseNode(node);
}
const value = parseFloat(node);
if (!isNaN(value)) {
if (typeof this.queries[node] === 'undefined') {
throw new Error(`unknow query under index ${node}`);
}
return this.queries[node];
}
return null;
}
}

View File

@@ -0,0 +1,29 @@
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ClsMiddleware } from 'nestjs-cls';
import './utils/moment-mysql';
import { AppModule } from './modules/App/App.module';
import { ServiceErrorFilter } from './common/filters/service-error.filter';
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);
app.useGlobalFilters(new ServiceErrorFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@@ -0,0 +1,11 @@
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) {
// @ts-expect-error
req.test = 'test';
next();
}
}

View File

@@ -0,0 +1,43 @@
import { QueryBuilder, Model } from 'objection';
interface PaginationResult<M extends Model> {
results: M[];
pagination: {
total: number;
page: number;
pageSize: number;
};
}
export type PaginationQueryBuilderType<M extends Model> = QueryBuilder<
M,
PaginationResult<M>
>;
class PaginationQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<
M,
R
> {
pagination(page: number, pageSize: number): PaginationQueryBuilderType<M> {
const query = super.page(page, pageSize);
return query.runAfter(({ results, total }) => {
return {
results,
pagination: {
total,
page: page + 1,
pageSize,
},
};
}) as unknown as PaginationQueryBuilderType<M>;
}
}
export class BaseModel extends Model {
public readonly id: number;
public readonly tableName: string;
QueryBuilderType!: PaginationQueryBuilder<this>;
static QueryBuilder = PaginationQueryBuilder;
}

View File

@@ -0,0 +1,130 @@
import { Transformer } from '../Transformer/Transformer';
import { Account } from './models/Account.model';
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
import { assocDepthLevelToObjectTree } from '@/utils/assoc-depth-level-to-object-tree';
import { nestedArrayToFlatten } from '@/utils/nested-array-to-flatten';
import { IAccountsStructureType } from './Accounts.types';
export class AccountTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'accountTypeLabel',
'accountNormalFormatted',
'formattedAmount',
'flattenName',
'bankBalanceFormatted',
'lastFeedsUpdatedAtFormatted',
'isFeedsPaused',
];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['plaidItem'];
};
/**
* Retrieves the flatten name with all dependants accounts names.
* @param {IAccount} account -
* @returns {string}
*/
public flattenName = (account: Account): string => {
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
account.id,
);
const prefixAccounts = parentDependantsIds.map((dependId) => {
const node = this.options.accountsGraph.getNodeData(dependId);
return `${node.name}: `;
});
return `${prefixAccounts}${account.name}`;
};
/**
* Retrieve formatted account amount.
* @param {IAccount} invoice
* @returns {string}
*/
protected formattedAmount = (account: Account): string => {
return this.formatNumber(account.amount, {
currencyCode: account.currencyCode,
});
};
/**
* Retrieves the formatted bank balance.
* @param {Account} account
* @returns {string}
*/
protected bankBalanceFormatted = (account: Account): string => {
return this.formatNumber(account.bankBalance, {
currencyCode: account.currencyCode,
});
};
/**
* Retrieves the formatted last feeds update at.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedAtFormatted = (account: Account): string => {
return account.lastFeedsUpdatedAt
? this.formatDate(account.lastFeedsUpdatedAt)
: '';
};
/**
* Detarmines whether the bank account connection is paused.
* @param account
* @returns {boolean}
*/
protected isFeedsPaused = (account: Account): boolean => {
// return account.plaidItem?.isPaused || false;
return false;
};
/**
* Retrieves formatted account type label.
* @returns {string}
*/
protected accountTypeLabel = (account: Account): string => {
return this.context.i18n.t(account.accountTypeLabel);
};
/**
* Retrieves formatted account normal.
* @returns {string}
*/
protected accountNormalFormatted = (account: Account): string => {
return this.context.i18n.t(account.accountNormalFormatted);
};
/**
* Transformes the accounts collection to flat or nested array.
* @param {IAccount[]}
* @returns {IAccount[]}
*/
protected postCollectionTransform = (accounts: Account[]) => {
// Transfom the flatten to accounts tree.
const transformed = flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
});
// Associate `accountLevel` attr to indicate object depth.
const transformed2 = assocDepthLevelToObjectTree(
transformed,
1,
'accountLevel',
);
return this.options.structure === IAccountsStructureType.Flat
? nestedArrayToFlatten(transformed2)
: transformed2;
};
}

View File

@@ -0,0 +1,124 @@
import { Transformer } from '../Transformer/Transformer';
import { AccountTransaction } from './models/AccountTransaction.model';
export class AccountTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'date',
'formattedDate',
'transactionType',
'transactionId',
'transactionTypeFormatted',
'credit',
'debit',
'formattedCredit',
'formattedDebit',
'fcCredit',
'fcDebit',
'formattedFcCredit',
'formattedFcDebit',
];
};
/**
* Exclude all attributes of the model.
* @returns {Array<string>}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieves the formatted date.
* @returns {string}
*/
public formattedDate(transaction: AccountTransaction) {
return this.formatDate(transaction.date);
}
/**
* Retrieves the formatted transaction type.
* @returns {string}
*/
public transactionTypeFormatted(transaction: AccountTransaction) {
return this.context.i18n.t(transaction.referenceTypeFormatted);
}
/**
* Retrieves the tranasction type.
* @returns {string}
*/
public transactionType(transaction: AccountTransaction) {
return transaction.referenceType;
}
/**
* Retrieves the transaction id.
* @returns {number}
*/
public transactionId(transaction: AccountTransaction) {
return transaction.referenceId;
}
/**
* Retrieves the credit amount.
* @returns {string}
*/
protected formattedCredit(transaction: AccountTransaction) {
return this.formatMoney(transaction.credit, {
excerptZero: true,
});
}
/**
* Retrieves the credit amount.
* @returns {string}
*/
protected formattedDebit(transaction: AccountTransaction) {
return this.formatMoney(transaction.debit, {
excerptZero: true,
});
}
/**
* Retrieves the foreign credit amount.
* @returns {number}
*/
protected fcCredit(transaction: AccountTransaction) {
return transaction.credit * transaction.exchangeRate;
}
/**
* Retrieves the foreign debit amount.
* @returns {number}
*/
protected fcDebit(transaction: AccountTransaction) {
return transaction.debit * transaction.exchangeRate;
}
/**
* Retrieves the formatted foreign credit amount.
* @returns {string}
*/
protected formattedFcCredit(transaction: AccountTransaction) {
return this.formatMoney(this.fcCredit(transaction), {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
}
/**
* Retrieves the formatted foreign debit amount.
* @returns {string}
*/
protected formattedFcDebit(transaction: AccountTransaction) {
return this.formatMoney(this.fcDebit(transaction), {
currencyCode: transaction.currencyCode,
excerptZero: true,
});
}
}

View File

@@ -0,0 +1,637 @@
export const OtherExpensesAccount = {
name: 'Other Expenses',
slug: 'other-expenses',
account_type: 'other-expense',
code: '40011',
description: '',
active: 1,
index: 1,
predefined: 1,
};
export const TaxPayableAccount = {
name: 'Tax Payable',
slug: 'tax-payable',
account_type: 'tax-payable',
code: '20006',
description: '',
active: 1,
index: 1,
predefined: 1,
};
export const UnearnedRevenueAccount = {
name: 'Unearned Revenue',
slug: 'unearned-revenue',
account_type: 'other-current-liability',
parent_account_id: null,
code: '50005',
active: true,
index: 1,
predefined: true,
};
export const PrepardExpenses = {
name: 'Prepaid Expenses',
slug: 'prepaid-expenses',
account_type: 'other-current-asset',
parent_account_id: null,
code: '100010',
active: true,
index: 1,
predefined: true,
};
export const StripeClearingAccount = {
name: 'Stripe Clearing',
slug: 'stripe-clearing',
account_type: 'other-current-asset',
parent_account_id: null,
code: '100020',
active: true,
index: 1,
predefined: true,
};
export const DiscountExpenseAccount = {
name: 'Discount',
slug: 'discount',
account_type: 'other-income',
code: '40008',
active: true,
index: 1,
predefined: true,
};
export const PurchaseDiscountAccount = {
name: 'Purchase Discount',
slug: 'purchase-discount',
account_type: 'other-expense',
code: '40009',
active: true,
index: 1,
predefined: true,
};
export const OtherChargesAccount = {
name: 'Other Charges',
slug: 'other-charges',
account_type: 'other-income',
code: '40010',
active: true,
index: 1,
predefined: true,
};
export const SeedAccounts = [
{
name: 'Bank Account',
slug: 'bank-account',
account_type: 'bank',
code: '10001',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Saving Bank Account',
slug: 'saving-bank-account',
account_type: 'bank',
code: '10002',
description: '',
active: 1,
index: 1,
predefined: 0,
},
{
name: 'Undeposited Funds',
slug: 'undeposited-funds',
account_type: 'cash',
code: '10003',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Petty Cash',
slug: 'petty-cash',
account_type: 'cash',
code: '10004',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Computer Equipment',
slug: 'computer-equipment',
code: '10005',
account_type: 'fixed-asset',
predefined: 0,
parent_account_id: null,
index: 1,
active: 1,
description: '',
},
{
name: 'Office Equipment',
slug: 'office-equipment',
code: '10006',
account_type: 'fixed-asset',
predefined: 0,
parent_account_id: null,
index: 1,
active: 1,
description: '',
},
{
name: 'Accounts Receivable (A/R)',
slug: 'accounts-receivable',
account_type: 'accounts-receivable',
code: '10007',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Inventory Asset',
slug: 'inventory-asset',
code: '10008',
account_type: 'inventory',
predefined: 1,
parent_account_id: null,
index: 1,
active: 1,
description:
'An account that holds valuation of products or goods that available for sale.',
},
// Libilities
{
name: 'Accounts Payable (A/P)',
slug: 'accounts-payable',
account_type: 'accounts-payable',
parent_account_id: null,
code: '20001',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Owner A Drawings',
slug: 'owner-drawings',
account_type: 'other-current-liability',
parent_account_id: null,
code: '20002',
description: 'Withdrawals by the owners.',
active: 1,
index: 1,
predefined: 0,
},
{
name: 'Loan',
slug: 'owner-drawings',
account_type: 'other-current-liability',
code: '20003',
description: 'Money that has been borrowed from a creditor.',
active: 1,
index: 1,
predefined: 0,
},
{
name: 'Opening Balance Liabilities',
slug: 'opening-balance-liabilities',
account_type: 'other-current-liability',
code: '20004',
description:
'This account will hold the difference in the debits and credits entered during the opening balance..',
active: 1,
index: 1,
predefined: 0,
},
{
name: 'Revenue Received in Advance',
slug: 'revenue-received-in-advance',
account_type: 'other-current-liability',
parent_account_id: null,
code: '20005',
description: 'When customers pay in advance for products/services.',
active: 1,
index: 1,
predefined: 0,
},
TaxPayableAccount,
// Equity
{
name: 'Retained Earnings',
slug: 'retained-earnings',
account_type: 'equity',
code: '30001',
description:
'Retained earnings tracks net income from previous fiscal years.',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Opening Balance Equity',
slug: 'opening-balance-equity',
account_type: 'equity',
code: '30002',
description:
'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.',
active: 1,
index: 1,
predefined: 1,
},
{
name: "Owner's Equity",
slug: 'owner-equity',
account_type: 'equity',
code: '30003',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name: `Drawings`,
slug: 'drawings',
account_type: 'equity',
code: '30003',
description:
'Goods purchased with the intention of selling these to customers',
active: 1,
index: 1,
predefined: 1,
},
// Expenses
OtherExpensesAccount,
{
name: 'Other Expenses',
slug: 'other-expenses',
account_type: 'other-expense',
parent_account_id: null,
code: '40001',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Cost of Goods Sold',
slug: 'cost-of-goods-sold',
account_type: 'cost-of-goods-sold',
parent_account_id: null,
code: '40002',
description: 'Tracks the direct cost of the goods sold.',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Office expenses',
slug: 'office-expenses',
account_type: 'expense',
parent_account_id: null,
code: '40003',
description: '',
active: 1,
index: 1,
predefined: 0,
},
{
name: 'Rent',
slug: 'rent',
account_type: 'expense',
parent_account_id: null,
code: '40004',
description: '',
active: 1,
index: 1,
predefined: 0,
},
{
name: 'Exchange Gain or Loss',
slug: 'exchange-grain-loss',
account_type: 'other-expense',
parent_account_id: null,
code: '40005',
description: 'Tracks the gain and losses of the exchange differences.',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Bank Fees and Charges',
slug: 'bank-fees-and-charges',
account_type: 'expense',
parent_account_id: null,
code: '40006',
description:
'Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.',
active: 1,
index: 1,
predefined: 0,
},
{
name: 'Depreciation Expense',
slug: 'depreciation-expense',
account_type: 'expense',
parent_account_id: null,
code: '40007',
description: '',
active: 1,
index: 1,
predefined: 0,
},
// Income
{
name: 'Sales of Product Income',
slug: 'sales-of-product-income',
account_type: 'income',
predefined: 1,
parent_account_id: null,
code: '50001',
index: 1,
active: 1,
description: '',
},
{
name: 'Sales of Service Income',
slug: 'sales-of-service-income',
account_type: 'income',
predefined: 0,
parent_account_id: null,
code: '50002',
index: 1,
active: 1,
description: '',
},
{
name: 'Uncategorized Income',
slug: 'uncategorized-income',
account_type: 'income',
parent_account_id: null,
code: '50003',
description: '',
active: 1,
index: 1,
predefined: 1,
},
{
name: 'Other Income',
slug: 'other-income',
account_type: 'other-income',
parent_account_id: null,
code: '50004',
description:
'The income activities are not associated to the core business.',
active: 1,
index: 1,
predefined: 0,
},
UnearnedRevenueAccount,
PrepardExpenses,
DiscountExpenseAccount,
PurchaseDiscountAccount,
OtherChargesAccount,
];
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);
};

View File

@@ -0,0 +1,72 @@
import {
Controller,
Post,
Body,
Param,
Delete,
Get,
Query,
ParseIntPipe,
} from '@nestjs/common';
import { AccountsApplication } from './AccountsApplication.service';
import { CreateAccountDTO } from './CreateAccount.dto';
import { EditAccountDTO } from './EditAccount.dto';
import { PublicRoute } from '../Auth/Jwt.guard';
import { IAccountsFilter, IAccountsTransactionsFilter } from './Accounts.types';
// import { IAccountsFilter, IAccountsTransactionsFilter } from './Accounts.types';
// import { ZodValidationPipe } from '@/common/pipes/ZodValidation.pipe';
@Controller('accounts')
@PublicRoute()
export class AccountsController {
constructor(private readonly accountsApplication: AccountsApplication) {}
@Post()
async createAccount(@Body() accountDTO: CreateAccountDTO) {
return this.accountsApplication.createAccount(accountDTO);
}
@Post(':id')
async editAccount(
@Param('id', ParseIntPipe) id: number,
@Body() accountDTO: EditAccountDTO,
) {
return this.accountsApplication.editAccount(id, accountDTO);
}
@Delete(':id')
async deleteAccount(@Param('id', ParseIntPipe) id: number) {
return this.accountsApplication.deleteAccount(id);
}
@Post(':id/activate')
async activateAccount(@Param('id', ParseIntPipe) id: number) {
return this.accountsApplication.activateAccount(id);
}
@Post(':id/inactivate')
async inactivateAccount(@Param('id', ParseIntPipe) id: number) {
return this.accountsApplication.inactivateAccount(id);
}
@Get('types')
async getAccountTypes() {
return this.accountsApplication.getAccountTypes();
}
@Get('transactions')
async getAccountTransactions(@Query() filter: IAccountsTransactionsFilter) {
return this.accountsApplication.getAccountsTransactions(filter);
}
@Get(':id')
async getAccount(@Param('id', ParseIntPipe) id: number) {
return this.accountsApplication.getAccount(id);
}
@Get()
async getAccounts(@Query() filter: IAccountsFilter) {
return this.accountsApplication.getAccounts(filter);
}
}

View File

@@ -0,0 +1,42 @@
import { Module } from '@nestjs/common';
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
import { AccountsController } from './Accounts.controller';
import { AccountsApplication } from './AccountsApplication.service';
import { CreateAccountService } from './CreateAccount.service';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { CommandAccountValidators } from './CommandAccountValidators.service';
import { AccountRepository } from './repositories/Account.repository';
import { EditAccount } from './EditAccount.service';
import { DeleteAccount } from './DeleteAccount.service';
import { GetAccount } from './GetAccount.service';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
import { ActivateAccount } from './ActivateAccount.service';
import { GetAccountTypesService } from './GetAccountTypes.service';
import { GetAccountTransactionsService } from './GetAccountTransactions.service';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { BankAccount } from '../BankingTransactions/models/BankAccount';
// import { GetAccountsService } from './GetAccounts.service';
const models = [RegisterTenancyModel(BankAccount)];
@Module({
imports: [TenancyDatabaseModule],
controllers: [AccountsController],
providers: [
AccountsApplication,
CreateAccountService,
TenancyContext,
CommandAccountValidators,
AccountRepository,
EditAccount,
DeleteAccount,
GetAccount,
TransformerInjectable,
ActivateAccount,
GetAccountTypesService,
GetAccountTransactionsService,
...models,
],
exports: [AccountRepository, CreateAccountService, ...models],
})
export class AccountsModule {}

View File

@@ -0,0 +1,91 @@
import { Knex } from 'knex';
import { Account } from './models/Account.model';
// import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
export enum AccountNormal {
DEBIT = 'debit',
CREDIT = 'credit',
}
export interface IAccountsTransactionsFilter {
accountId?: number;
limit?: number;
}
export enum IAccountsStructureType {
Tree = 'tree',
Flat = 'flat',
}
// export interface IAccountsFilter extends IDynamicListFilterDTO {
// }
export interface IAccountsFilter {
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(): Promise<IAccountType>;
}
export interface IAccountEventCreatingPayload {
accountDTO: any;
trx: Knex.Transaction;
}
export interface IAccountEventCreatedPayload {
account: Account;
accountId: number;
trx: Knex.Transaction;
}
export interface IAccountEventEditedPayload {
account: Account;
oldAccount: Account;
trx: Knex.Transaction;
}
export interface IAccountEventDeletedPayload {
accountId: number;
oldAccount: Account;
trx: Knex.Transaction;
}
export interface IAccountEventDeletePayload {
trx: Knex.Transaction;
oldAccount: Account;
}
export interface IAccountEventActivatedPayload {
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;
}
export interface IGetAccountTransactionPOJO {}

View File

@@ -0,0 +1,123 @@
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { CreateAccountService } from './CreateAccount.service';
import { DeleteAccount } from './DeleteAccount.service';
import { EditAccount } from './EditAccount.service';
import { CreateAccountDTO } from './CreateAccount.dto';
import { Account } from './models/Account.model';
import { EditAccountDTO } from './EditAccount.dto';
import { GetAccount } from './GetAccount.service';
import { ActivateAccount } from './ActivateAccount.service';
import { GetAccountTypesService } from './GetAccountTypes.service';
import { GetAccountTransactionsService } from './GetAccountTransactions.service';
import {
IAccountsFilter,
IAccountsTransactionsFilter,
IGetAccountTransactionPOJO,
} from './Accounts.types';
import { GetAccountsService } from './GetAccounts.service';
import { IFilterMeta } from '@/interfaces/Model';
@Injectable()
export class AccountsApplication {
constructor(
private readonly createAccountService: CreateAccountService,
private readonly editAccountService: EditAccount,
private readonly deleteAccountService: DeleteAccount,
private readonly activateAccountService: ActivateAccount,
private readonly getAccountTypesService: GetAccountTypesService,
private readonly getAccountService: GetAccount,
private readonly getAccountTransactionsService: GetAccountTransactionsService,
private readonly getAccountsService: GetAccountsService,
) {}
/**
* Creates a new account.
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @returns {Promise<IAccount>}
*/
public createAccount = (
accountDTO: CreateAccountDTO,
trx?: Knex.Transaction,
): Promise<Account> => {
return this.createAccountService.createAccount(accountDTO, trx);
};
/**
* Deletes the given account.
* @param {number} tenantId
* @param {number} accountId
* @returns {Promise<void>}
*/
public deleteAccount = (accountId: number) => {
return this.deleteAccountService.deleteAccount(accountId);
};
/**
* Edits the given account.
* @param {number} tenantId
* @param {number} accountId
* @param {IAccountEditDTO} accountDTO
* @returns
*/
public editAccount = (accountId: number, accountDTO: EditAccountDTO) => {
return this.editAccountService.editAccount(accountId, accountDTO);
};
/**
* Activate the given account.
* @param {number} accountId - Account id.
*/
public activateAccount = (accountId: number) => {
return this.activateAccountService.activateAccount(accountId, true);
};
/**
* Inactivate the given account.
* @param {number} accountId - Account id.
*/
public inactivateAccount = (accountId: number) => {
return this.activateAccountService.activateAccount(accountId, false);
};
/**
* Retrieves the account details.
* @param {number} tenantId - Tenant id.
* @param {number} accountId - Account id.
* @returns {Promise<IAccount>}
*/
public getAccount = (accountId: number) => {
return this.getAccountService.getAccount(accountId);
};
/**
* Retrieves all account types.
* @returns {Promise<IAccountType[]>}
*/
public getAccountTypes = () => {
return this.getAccountTypesService.getAccountsTypes();
};
/**
* Retrieves the accounts list.
* @param {IAccountsFilter} filterDTO - Filter DTO.
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
*/
public getAccounts = (
filterDTO: IAccountsFilter,
): Promise<{ accounts: Account[]; filterMeta: IFilterMeta }> => {
return this.getAccountsService.getAccountsList(filterDTO);
};
/**
* Retrieves the given account transactions.
* @param {IAccountsTransactionsFilter} filter
* @returns {Promise<IGetAccountTransactionPOJO[]>}
*/
public getAccountsTransactions = (
filter: IAccountsTransactionsFilter,
): Promise<IGetAccountTransactionPOJO[]> => {
return this.getAccountTransactionsService.getAccountsTransactions(filter);
};
}

View File

@@ -0,0 +1,32 @@
// import { Inject, Service } from 'typedi';
// import { AccountsApplication } from './AccountsApplication.service';
// import { Exportable } from '../Export/Exportable';
// import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
// import { EXPORT_SIZE_LIMIT } from '../Export/constants';
// @Service()
// export class AccountsExportable extends Exportable {
// @Inject()
// private accountsApplication: AccountsApplication;
// /**
// * Retrieves the accounts data to exportable sheet.
// * @param {number} tenantId
// * @returns
// */
// public exportable(tenantId: number, query: IAccountsFilter) {
// const parsedQuery = {
// sortOrder: 'desc',
// columnSortBy: 'created_at',
// inactiveMode: false,
// ...query,
// structure: IAccountsStructureType.Flat,
// pageSize: EXPORT_SIZE_LIMIT,
// page: 1,
// } as IAccountsFilter;
// return this.accountsApplication
// .getAccounts(tenantId, parsedQuery)
// .then((output) => output.accounts);
// }
// }

View File

@@ -0,0 +1,50 @@
export const AccountsSampleData = [
{
'Account Name': 'Utilities Expense',
'Account Code': 9000,
Type: 'Expense',
Description: 'Omnis voluptatum consequatur.',
Active: 'T',
'Currency Code': '',
},
{
'Account Name': 'Unearned Revenue',
'Account Code': 9010,
Type: 'Long Term Liability',
Description: 'Autem odit voluptas nihil unde.',
Active: 'T',
'Currency Code': '',
},
{
'Account Name': 'Long-Term Debt',
'Account Code': 9020,
Type: 'Long Term Liability',
Description: 'In voluptas cumque exercitationem.',
Active: 'T',
'Currency Code': '',
},
{
'Account Name': 'Salaries and Wages Expense',
'Account Code': 9030,
Type: 'Expense',
Description: 'Assumenda aspernatur soluta aliquid perspiciatis quasi.',
Active: 'T',
'Currency Code': '',
},
{
'Account Name': 'Rental Income',
'Account Code': 9040,
Type: 'Income',
Description: 'Omnis possimus amet occaecati inventore.',
Active: 'T',
'Currency Code': '',
},
{
'Account Name': 'Paypal',
'Account Code': 9050,
Type: 'Bank',
Description: 'In voluptas cumque exercitationem.',
Active: 'T',
'Currency Code': '',
},
];

View File

@@ -0,0 +1,45 @@
// import { Inject, Service } from 'typedi';
// import { Knex } from 'knex';
// import { IAccountCreateDTO } from '@/interfaces';
// import { CreateAccount } from './CreateAccount.service';
// import { Importable } from '../Import/Importable';
// import { AccountsSampleData } from './AccountsImportable.SampleData';
// @Service()
// export class AccountsImportable extends Importable {
// @Inject()
// private createAccountService: CreateAccount;
// /**
// * Importing to account service.
// * @param {number} tenantId
// * @param {IAccountCreateDTO} createAccountDTO
// * @returns
// */
// public importable(
// tenantId: number,
// createAccountDTO: IAccountCreateDTO,
// trx?: Knex.Transaction
// ) {
// return this.createAccountService.createAccount(
// tenantId,
// createAccountDTO,
// trx
// );
// }
// /**
// * Concurrrency controlling of the importing process.
// * @returns {number}
// */
// public get concurrency() {
// return 1;
// }
// /**
// * Retrieves the sample data that used to download accounts sample sheet.
// */
// public sampleData(): any[] {
// return AccountsSampleData;
// }
// }

View File

@@ -0,0 +1,53 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { IAccountEventActivatedPayload } from './Accounts.types';
import { Account } from './models/Account.model';
import { AccountRepository } from './repositories/Account.repository';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Injectable()
export class ActivateAccount {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(Account.name)
private readonly accountModel: typeof Account,
private readonly accountRepository: AccountRepository,
) {}
/**
* Activates/Inactivates the given account.
* @param {number} accountId
* @param {boolean} activate
*/
public activateAccount = async (accountId: number, activate?: boolean) => {
// Retrieve the given account or throw not found error.
const oldAccount = await this.accountModel
.query()
.findById(accountId)
.throwIfNotFound();
// Get all children accounts.
const accountsGraph = await this.accountRepository.getDependencyGraph();
const dependenciesAccounts = accountsGraph.dependenciesOf(accountId);
const patchAccountsIds = [...dependenciesAccounts, accountId];
// Activate account and associated transactions under unit-of-work environment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Activate and inactivate the given accounts ids.
activate
? await this.accountRepository.activateByIds(patchAccountsIds, trx)
: await this.accountRepository.inactivateByIds(patchAccountsIds, trx);
// Triggers `onAccountActivated` event.
this.eventEmitter.emitAsync(events.accounts.onActivated, {
accountId,
trx,
} as IAccountEventActivatedPayload);
});
};
}

View File

@@ -0,0 +1,223 @@
// @ts-nocheck
import { Inject, Injectable, Scope } from '@nestjs/common';
// import { IAccountDTO, IAccount, IAccountCreateDTO } from './Accounts.types';
// import AccountTypesUtils from '@/lib/AccountTypes';
import { ServiceError } from '../Items/ServiceError';
import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';
import { Account } from './models/Account.model';
import { AccountRepository } from './repositories/Account.repository';
import { AccountTypesUtils } from './utils/AccountType.utils';
import { CreateAccountDTO } from './CreateAccount.dto';
import { EditAccountDTO } from './EditAccount.dto';
@Injectable({ scope: Scope.REQUEST })
export class CommandAccountValidators {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
private readonly accountRepository: AccountRepository,
) {}
/**
* Throws error if the account was prefined.
* @param {Account} account
*/
public throwErrorIfAccountPredefined(account: Account) {
if (account.predefined) {
throw new ServiceError(ERRORS.ACCOUNT_PREDEFINED);
}
}
/**
* Diff account type between new and old account, throw service error
* if they have different account type.
* @param {Account|CreateAccountDTO|EditAccountDTO} oldAccount
* @param {Account|CreateAccountDTO|EditAccountDTO} newAccount
*/
public async isAccountTypeChangedOrThrowError(
oldAccount: Account | CreateAccountDTO | EditAccountDTO,
newAccount: Account | CreateAccountDTO | EditAccountDTO,
) {
if (oldAccount.accountType !== newAccount.accountType) {
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE);
}
}
/**
* Retrieve account type or throws service error.
* @param {string} accountTypeKey -
* @return {IAccountType}
*/
public getAccountTypeOrThrowError(accountTypeKey: string) {
const accountType = AccountTypesUtils.getType(accountTypeKey);
if (!accountType) {
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_FOUND);
}
return accountType;
}
/**
* Retrieve parent account or throw service error.
* @param {number} accountId - Account id.
* @param {number} notAccountId - Ignore the account id.
*/
public async getParentAccountOrThrowError(
accountId: number,
notAccountId?: number,
) {
const parentAccount = await this.accountModel
.query()
.findById(accountId)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (!parentAccount) {
throw new ServiceError(ERRORS.PARENT_ACCOUNT_NOT_FOUND);
}
return parentAccount;
}
/**
* Throws error if the account type was not unique on the storage.
* @param {string} accountCode - Account code.
* @param {number} notAccountId - Ignore the account id.
*/
public async isAccountCodeUniqueOrThrowError(
accountCode: string,
notAccountId?: number,
) {
const account = await this.accountModel
.query()
.where('code', accountCode)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (account.length > 0) {
throw new ServiceError(
ERRORS.ACCOUNT_CODE_NOT_UNIQUE,
'Account code is not unique.',
);
}
}
/**
* Validates the account name uniquiness.
* @param {string} accountName - Account name.
* @param {number} notAccountId - Ignore the account id.
*/
public async validateAccountNameUniquiness(
accountName: string,
notAccountId?: number,
) {
const foundAccount = await this.accountModel
.query()
.findOne('name', accountName)
.onBuild((query) => {
if (notAccountId) {
query.whereNot('id', notAccountId);
}
});
if (foundAccount) {
throw new ServiceError(
ERRORS.ACCOUNT_NAME_NOT_UNIQUE,
'Account name is not unique.',
);
}
}
/**
* Validates the given account type supports multi-currency.
* @param {CreateAccountDTO | EditAccountDTO} accountDTO -
*/
public validateAccountTypeSupportCurrency = (
accountDTO: CreateAccountDTO | EditAccountDTO,
baseCurrency: string,
) => {
// Can't continue to validate the type has multi-currency feature
// if the given currency equals the base currency or not assigned.
if (accountDTO.currencyCode === baseCurrency || !accountDTO.currencyCode) {
return;
}
const meta = AccountTypesUtils.getType(accountDTO.accountType);
// Throw error if the account type does not support multi-currency.
if (!meta?.multiCurrency) {
throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY);
}
};
/**
* Validates the account DTO currency code whether equals the currency code of
* parent account.
* @param {CreateAccountDTO | EditAccountDTO} accountDTO
* @param {Account} parentAccount
* @param {string} baseCurrency -
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
*/
public validateCurrentSameParentAccount = (
accountDTO: CreateAccountDTO | EditAccountDTO,
parentAccount: Account,
baseCurrency: string,
) => {
// If the account DTO currency not assigned and the parent account has no base currency.
if (
!accountDTO.currencyCode &&
parentAccount.currencyCode !== baseCurrency
) {
throw new ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT);
}
// If the account DTO is assigned and not equals the currency code of parent account.
if (
accountDTO.currencyCode &&
parentAccount.currencyCode !== accountDTO.currencyCode
) {
throw new ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT);
}
};
/**
* Throws service error if parent account has different type.
* @param {IAccountDTO} accountDTO
* @param {IAccount} parentAccount
*/
public throwErrorIfParentHasDiffType(
accountDTO: CreateAccountDTO | EditAccountDTO,
parentAccount: Account,
) {
if (accountDTO.accountType !== parentAccount.accountType) {
throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE);
}
}
/**
* Retrieve account of throw service error in case account not found.
* @param {number} accountId
* @return {IAccount}
*/
public async getAccountOrThrowError(accountId: number) {
const account = await this.accountRepository.findOneById(accountId);
if (!account) {
throw new ServiceError(ERRORS.ACCOUNT_NOT_FOUND);
}
return account;
}
/**
* Validates the max depth level of accounts chart.
* @param {number} parentAccountId - Parent account id.
*/
public async validateMaxParentAccountDepthLevels(parentAccountId: number) {
const accountsGraph = await this.accountRepository.getDependencyGraph();
const parentDependantsIds = accountsGraph.dependantsOf(parentAccountId);
if (parentDependantsIds.length >= MAX_ACCOUNTS_CHART_DEPTH) {
throw new ServiceError(ERRORS.PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL);
}
}
}

View File

@@ -0,0 +1,51 @@
import {
IsString,
IsOptional,
IsInt,
MinLength,
MaxLength,
IsBoolean,
} from 'class-validator';
export class CreateAccountDTO {
@IsString()
@MinLength(3)
@MaxLength(255) // Assuming DATATYPES_LENGTH.STRING is 255
name: string;
@IsOptional()
@IsString()
@MinLength(3)
@MaxLength(6)
code?: string;
@IsOptional()
@IsString()
currencyCode?: string;
@IsString()
@MinLength(3)
@MaxLength(255) // Assuming DATATYPES_LENGTH.STRING is 255
accountType: string;
@IsOptional()
@IsString()
@MaxLength(65535) // Assuming DATATYPES_LENGTH.TEXT is 65535
description?: string;
@IsOptional()
@IsInt()
parentAccountId?: number;
@IsOptional()
@IsBoolean()
active?: boolean;
@IsOptional()
@IsString()
plaidAccountId?: string;
@IsOptional()
@IsString()
plaidItemId?: string;
}

View File

@@ -0,0 +1,139 @@
import { Inject, Injectable } from '@nestjs/common';
import { kebabCase } from 'lodash';
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
// IAccount,
// IAccountEventCreatedPayload,
// IAccountCreateDTO,
IAccountEventCreatingPayload,
CreateAccountParams,
} from './Accounts.types';
import { CommandAccountValidators } from './CommandAccountValidators.service';
import { Account } from './models/Account.model';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { events } from '@/common/events/events';
import { CreateAccountDTO } from './CreateAccount.dto';
@Injectable()
export class CreateAccountService {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly validator: CommandAccountValidators,
private readonly tenancyContext: TenancyContext,
) {}
/**
* Authorize the account creation.
* @param {CreateAccountDTO} accountDTO
*/
private authorize = async (
accountDTO: CreateAccountDTO,
baseCurrency: string,
params?: CreateAccountParams,
) => {
// Validate account name uniquiness.
if (!params.ignoreUniqueName) {
await this.validator.validateAccountNameUniquiness(accountDTO.name);
}
// Validate the account code uniquiness.
if (accountDTO.code) {
await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code);
}
// Retrieve the account type meta or throw service error if not found.
this.validator.getAccountTypeOrThrowError(accountDTO.accountType);
// Ingore the parent account validation if not presented.
if (accountDTO.parentAccountId) {
const parentAccount = await this.validator.getParentAccountOrThrowError(
accountDTO.parentAccountId,
);
this.validator.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
// Inherit active status from parent account.
accountDTO.active = parentAccount.active;
// Validate should currency code be the same currency of parent account.
this.validator.validateCurrentSameParentAccount(
accountDTO,
parentAccount,
baseCurrency,
);
// Validates the max depth level of accounts chart.
await this.validator.validateMaxParentAccountDepthLevels(
accountDTO.parentAccountId,
);
}
// Validates the given account type supports the multi-currency.
this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency);
};
/**
* Transformes the create account DTO to input model.
* @param {IAccountCreateDTO} createAccountDTO
*/
private transformDTOToModel = (
createAccountDTO: CreateAccountDTO,
baseCurrency: string,
) => {
return {
...createAccountDTO,
slug: kebabCase(createAccountDTO.name),
currencyCode: createAccountDTO.currencyCode || baseCurrency,
// Mark the account is Plaid owner since Plaid item/account is defined on creating.
isSyncingOwner: Boolean(
createAccountDTO.plaidAccountId || createAccountDTO.plaidItemId,
),
};
};
/**
* Creates a new account on the storage.
* @param {IAccountCreateDTO} accountDTO
* @returns {Promise<IAccount>}
*/
public createAccount = async (
accountDTO: CreateAccountDTO,
trx?: Knex.Transaction,
params: CreateAccountParams = { ignoreUniqueName: false },
): Promise<Account> => {
// Retrieves the given tenant metadata.
const tenant = await this.tenancyContext.getTenant(true);
// Authorize the account creation.
await this.authorize(accountDTO, tenant.metadata.baseCurrency, params);
// Transformes the DTO to model.
const accountInputModel = this.transformDTOToModel(
accountDTO,
tenant.metadata.baseCurrency,
);
// Creates a new account with associated transactions under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onAccountCreating` event.
await this.eventEmitter.emitAsync(events.accounts.onCreating, {
accountDTO,
trx,
} as IAccountEventCreatingPayload);
// Inserts account to the storage.
const account = await this.accountModel.query().insert({
...accountInputModel,
});
// Triggers `onAccountCreated` event.
// await this.eventEmitter.emitAsync(events.accounts.onCreated, {
// account,
// accountId: account.id,
// trx,
// } as IAccountEventCreatedPayload);
return account;
}, trx);
};
}

View File

@@ -0,0 +1,80 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
// import { IAccountEventDeletedPayload } from '@/interfaces';
import { CommandAccountValidators } from './CommandAccountValidators.service';
import { Account } from './models/Account.model';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { IAccountEventDeletedPayload } from './Accounts.types';
@Injectable()
export class DeleteAccount {
constructor(
@Inject(Account.name) private accountModel: typeof Account,
private eventEmitter: EventEmitter2,
private uow: UnitOfWork,
private validator: CommandAccountValidators,
) {}
/**
* Authorize account delete.
* @param {number} accountId - Account id.
*/
private authorize = async (accountId: number, oldAccount: Account) => {
// Throw error if the account was predefined.
this.validator.throwErrorIfAccountPredefined(oldAccount);
};
/**
* Unlink the given parent account with children accounts.
* @param {number|number[]} parentAccountId -
*/
private async unassociateChildrenAccountsFromParent(
parentAccountId: number | number[],
trx?: Knex.Transaction,
) {
const accountsIds = Array.isArray(parentAccountId)
? parentAccountId
: [parentAccountId];
await this.accountModel
.query(trx)
.whereIn('parent_account_id', accountsIds)
.patch({ parent_account_id: null });
}
/**
* Deletes the account from the storage.
* @param {number} accountId
*/
public deleteAccount = async (accountId: number): Promise<void> => {
// Retrieve account or not found service error.
const oldAccount = await this.accountModel.query().findById(accountId);
// Authorize before delete account.
await this.authorize(accountId, oldAccount);
// Deletes the account and associated transactions under UOW environment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onAccountDelete` event.
await this.eventEmitter.emitAsync(events.accounts.onDelete, {
trx,
oldAccount,
} as IAccountEventDeletedPayload);
// Unlink the parent account from children accounts.
await this.unassociateChildrenAccountsFromParent(accountId, trx);
// Deletes account by the given id.
await this.accountModel.query(trx).deleteById(accountId);
// Triggers `onAccountDeleted` event.
await this.eventEmitter.emitAsync(events.accounts.onDeleted, {
accountId,
oldAccount,
trx,
} as IAccountEventDeletedPayload);
});
};
}

View File

@@ -0,0 +1,34 @@
import {
IsString,
IsOptional,
IsInt,
MinLength,
MaxLength,
} from 'class-validator';
export class EditAccountDTO {
@IsString()
@MinLength(3)
@MaxLength(255) // Assuming DATATYPES_LENGTH.STRING is 255
name: string;
@IsOptional()
@IsString()
@MinLength(3)
@MaxLength(6)
code?: string;
@IsString()
@MinLength(3)
@MaxLength(255) // Assuming DATATYPES_LENGTH.STRING is 255
accountType: string;
@IsOptional()
@IsString()
@MaxLength(65535) // Assuming DATATYPES_LENGTH.TEXT is 65535
description?: string;
@IsOptional()
@IsInt()
parentAccountId?: number;
}

View File

@@ -0,0 +1,100 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { CommandAccountValidators } from './CommandAccountValidators.service';
import { Account } from './models/Account.model';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { EditAccountDTO } from './EditAccount.dto';
@Injectable()
export class EditAccount {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly validator: CommandAccountValidators,
@Inject(Account.name)
private readonly accountModel: typeof Account,
) {}
/**
* Authorize the account editing.
* @param {number} accountId
* @param {IAccountEditDTO} accountDTO
* @param {IAccount} oldAccount -
*/
private authorize = async (
accountId: number,
accountDTO: EditAccountDTO,
oldAccount: Account,
) => {
// Validate account name uniquiness.
await this.validator.validateAccountNameUniquiness(
accountDTO.name,
accountId,
);
// Validate the account type should be not mutated.
await this.validator.isAccountTypeChangedOrThrowError(
oldAccount,
accountDTO,
);
// Validate the account code not exists on the storage.
if (accountDTO.code && accountDTO.code !== oldAccount.code) {
await this.validator.isAccountCodeUniqueOrThrowError(
accountDTO.code,
oldAccount.id,
);
}
// Retrieve the parent account of throw not found service error.
if (accountDTO.parentAccountId) {
const parentAccount = await this.validator.getParentAccountOrThrowError(
accountDTO.parentAccountId,
oldAccount.id,
);
this.validator.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
}
};
/**
* Edits details of the given account.
* @param {number} accountId
* @param {IAccountDTO} accountDTO
*/
public async editAccount(
accountId: number,
accountDTO: EditAccountDTO,
): Promise<Account> {
// Retrieve the old account or throw not found service error.
const oldAccount = await this.accountModel
.query()
.findById(accountId)
.throwIfNotFound();
// Authorize the account editing.
await this.authorize(accountId, accountDTO, oldAccount);
// Edits account and associated transactions under unit-of-work environment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onAccountEditing` event.
await this.eventEmitter.emitAsync(events.accounts.onEditing, {
oldAccount,
accountDTO,
});
// Update the account on the storage.
const account = await this.accountModel
.query(trx)
.findById(accountId)
.updateAndFetch({ ...accountDTO });
// Triggers `onAccountEdited` event.
// await this.eventEmitter.emitAsync(events.accounts.onEdited, {
// account,
// oldAccount,
// trx,
// } as IAccountEventEditedPayload);
return account;
});
}
}

View File

@@ -0,0 +1,46 @@
import { Inject, Injectable } from '@nestjs/common';
import { AccountTransformer } from './Account.transformer';
import { Account } from './models/Account.model';
import { AccountRepository } from './repositories/Account.repository';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Injectable()
export class GetAccount {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
private readonly accountRepository: AccountRepository,
private readonly transformer: TransformerInjectable,
private readonly eventEmitter: EventEmitter2,
) {}
/**
* Retrieve the given account details.
* @param {number} accountId
*/
public getAccount = async (accountId: number) => {
// Find the given account or throw not found error.
const account = await this.accountModel
.query()
.findById(accountId)
.withGraphFetched('plaidItem')
.throwIfNotFound();
const accountsGraph = await this.accountRepository.getDependencyGraph();
// Transforms the account model to POJO.
const transformed = await this.transformer.transform(
account,
new AccountTransformer(),
{ accountsGraph },
);
const eventPayload = { accountId };
// Triggers `onAccountViewed` event.
await this.eventEmitter.emitAsync(events.accounts.onViewed, eventPayload);
return transformed;
};
}

View File

@@ -0,0 +1,52 @@
import {
IAccountsTransactionsFilter,
IGetAccountTransactionPOJO,
} from './Accounts.types';
import { AccountTransactionTransformer } from './AccountTransaction.transformer';
import { AccountTransaction } from './models/AccountTransaction.model';
import { Account } from './models/Account.model';
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
@Injectable()
export class GetAccountTransactionsService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(AccountTransaction.name)
private readonly accountTransaction: typeof AccountTransaction,
@Inject(Account.name)
private readonly account: typeof Account,
) {}
/**
* Retrieve the accounts transactions.
* @param {IAccountsTransactionsFilter} filter -
*/
public getAccountsTransactions = async (
filter: IAccountsTransactionsFilter,
): Promise<IGetAccountTransactionPOJO[]> => {
// Retrieve the given account or throw not found error.
if (filter.accountId) {
await this.account.query().findById(filter.accountId).throwIfNotFound();
}
const transactions = await this.accountTransaction
.query()
.onBuild((query) => {
query.orderBy('date', 'DESC');
if (filter.accountId) {
query.where('account_id', filter.accountId);
}
query.withGraphFetched('account');
query.withGraphFetched('contact');
query.limit(filter.limit || 50);
});
// Transform the account transaction.
return this.transformer.transform(
transactions,
new AccountTransactionTransformer(),
);
};
}

View File

@@ -0,0 +1,17 @@
// import { IAccountType } from './Accounts.types';
import { Injectable } from '@nestjs/common';
import { AccountTypesUtils } from './utils/AccountType.utils';
@Injectable()
export class GetAccountTypesService {
/**
* Retrieve all accounts types.
* @param {number} tenantId -
* @return {IAccountType}
*/
public getAccountsTypes() {
const accountTypes = AccountTypesUtils.getList();
return accountTypes;
}
}

View File

@@ -0,0 +1,66 @@
import { Inject, Injectable } from '@nestjs/common';
import * as R from 'ramda';
import { IAccountsFilter } from './Accounts.types';
import { DynamicListService } from '../DynamicListing/DynamicList.service';
import { AccountTransformer } from './Account.transformer';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
import { Account } from './models/Account.model';
import { AccountRepository } from './repositories/Account.repository';
import { IFilterMeta } from '@/interfaces/Model';
@Injectable()
export class GetAccountsService {
constructor(
private readonly dynamicListService: DynamicListService,
private readonly transformerService: TransformerInjectable,
@Inject(Account.name)
private readonly accountModel: typeof Account,
private readonly accountRepository: AccountRepository,
) {}
/**
* Retrieve accounts datatable list.
* @param {IAccountsFilter} accountsFilter
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
*/
public async getAccountsList(
filterDTO: IAccountsFilter,
): Promise<{ accounts: Account[]; filterMeta: IFilterMeta }> {
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
this.accountModel,
filter,
);
// Retrieve accounts model based on the given query.
const accounts = await this.accountModel.query().onBuild((builder) => {
dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode);
});
const accountsGraph = await this.accountRepository.getDependencyGraph();
// Retrieves the transformed accounts collection.
const transformedAccounts = await this.transformerService.transform(
accounts,
new AccountTransformer(),
{ accountsGraph, structure: filterDTO.structure },
);
return {
accounts: transformedAccounts,
filterMeta: dynamicList.getResponseMeta(),
};
}
/**
* Parsees accounts list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
}

View File

@@ -0,0 +1,22 @@
// import { Inject, Service } from 'typedi';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// @Service()
// export class MutateBaseCurrencyAccounts {
// @Inject()
// tenancy: HasTenancyService;
// /**
// * Mutates the all accounts or the organziation.
// * @param {number} tenantId
// * @param {string} currencyCode
// */
// public mutateAllAccountsCurrency = async (
// tenantId: number,
// currencyCode: string
// ) => {
// const { Account } = this.tenancy.models(tenantId);
// await Account.query().update({ currencyCode });
// };
// }

View File

@@ -0,0 +1,103 @@
export const ERRORS = {
ACCOUNT_NOT_FOUND: 'account_not_found',
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
ACCOUNT_PREDEFINED: 'account_predefined',
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
PREDEFINED_ACCOUNTS: 'predefined_accounts',
ACCOUNTS_HAVE_TRANSACTIONS: 'accounts_have_transactions',
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
'close_account_and_to_account_not_same_type',
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY:
'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY',
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT:
'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT',
PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL:
'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
};
// Default views columns.
export const DEFAULT_VIEW_COLUMNS = [
{ key: 'name', label: 'Account name' },
{ key: 'code', label: 'Account code' },
{ key: 'account_type_label', label: 'Account type' },
{ key: 'account_normal', label: 'Account normal' },
{ key: 'amount', label: 'Balance' },
{ key: 'currencyCode', label: 'Currency' },
];
export const MAX_ACCOUNTS_CHART_DEPTH = 5;
// Accounts default views.
export const DEFAULT_VIEWS = [
{
name: 'Assets',
slug: 'assets',
rolesLogicExpression: '1',
roles: [
{ index: 1, fieldKey: 'root_type', comparator: 'equals', value: 'asset' },
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Liabilities',
slug: 'liabilities',
rolesLogicExpression: '1',
roles: [
{
fieldKey: 'root_type',
index: 1,
comparator: 'equals',
value: 'liability',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Equity',
slug: 'equity',
rolesLogicExpression: '1',
roles: [
{
fieldKey: 'root_type',
index: 1,
comparator: 'equals',
value: 'equity',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Income',
slug: 'income',
rolesLogicExpression: '1',
roles: [
{
fieldKey: 'root_type',
index: 1,
comparator: 'equals',
value: 'income',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
{
name: 'Expenses',
slug: 'expenses',
rolesLogicExpression: '1',
roles: [
{
fieldKey: 'root_type',
index: 1,
comparator: 'equals',
value: 'expense',
},
],
columns: DEFAULT_VIEW_COLUMNS,
},
];

View File

@@ -0,0 +1,458 @@
/* eslint-disable global-require */
// import { mixin, Model } from 'objection';
import { castArray } from 'lodash';
import DependencyGraph from '@/libs/dependency-graph';
import {
ACCOUNT_TYPES,
getAccountsSupportsMultiCurrency,
} from '@/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 { Model } from 'objection';
import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem';
// 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,
// ]) {
export class Account extends TenantModel {
public name!: string;
public slug!: string;
public code!: string;
public index!: number;
public accountType!: string;
public predefined!: boolean;
public currencyCode!: string;
public active!: boolean;
public bankBalance!: number;
public lastFeedsUpdatedAt!: string | null;
public amount!: number;
public plaidItemId!: number;
public plaidItem!: PlaidItem;
/**
* 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('./AccountTransaction.model');
const { Item } = require('../../Items/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('../../BankingPlaid/models/PlaidItem');
return {
/**
* Account model may has many transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'accounts.id',
to: 'accounts_transactions.accountId',
},
},
/**
* Account may has many items as cost account.
*/
itemsCostAccount: {
relation: Model.HasManyRelation,
modelClass: Item,
join: {
from: 'accounts.id',
to: 'items.costAccountId',
},
},
/**
* Account may has many items as sell account.
*/
itemsSellAccount: {
relation: Model.HasManyRelation,
modelClass: Item,
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,
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;
}
}

View File

@@ -0,0 +1,266 @@
import { Model, raw } from 'objection';
import moment, { unitOfTime } from 'moment';
import { isEmpty, castArray } from 'lodash';
import { BaseModel } from '@/models/Model';
import { Account } from './Account.model';
// import { getTransactionTypeLabel } from '@/utils/transactions-types';
export class AccountTransaction extends BaseModel {
referenceType: string;
referenceId: number;
accountId: number;
contactId: number;
credit: number;
debit: number;
exchangeRate: number;
taxRate: number;
date: Date | string;
transactionType: string;
currencyCode: string;
referenceTypeFormatted: string;
transactionNumber!: string;
referenceNumber!: string;
note!: string;
index!: number;
indexGroup!: number;
taxRateId!: number;
branchId!: number;
userId!: number;
itemId!: number;
projectId!: number;
account: Account;
/**
* Table name
*/
static get tableName() {
return 'accounts_transactions';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['referenceTypeFormatted', 'creditLocal', 'debitLocal'];
}
/**
* Retrieves the credit amount in base currency.
* @return {number}
*/
get creditLocal() {
return this.credit * this.exchangeRate;
}
/**
* Retrieves the debit amount in base currency.
* @return {number}
*/
get debitLocal() {
return this.debit * this.exchangeRate;
}
// /**
// * Retrieve formatted reference type.
// * @return {string}
// */
// get referenceTypeFormatted() {
// return getTransactionTypeLabel(this.referenceType, this.transactionType);
// }
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters accounts by the given ids.
* @param {Query} query
* @param {number[]} accountsIds
*/
filterAccounts(query, accountsIds) {
if (Array.isArray(accountsIds) && accountsIds.length > 0) {
query.whereIn('account_id', accountsIds);
}
},
/**
* Filters the transaction types.
* @param {Query} query
* @param {string[]} types
*/
filterTransactionTypes(query, types) {
if (Array.isArray(types) && types.length > 0) {
query.whereIn('reference_type', types);
} else if (typeof types === 'string') {
query.where('reference_type', types);
}
},
/**
* Filters the date range.
* @param {Query} query
* @param {moment.MomentInput} startDate
* @param {moment.MomentInput} endDate
* @param {unitOfTime.StartOf} type
*/
filterDateRange(
query,
startDate: moment.MomentInput,
endDate: moment.MomentInput,
type: unitOfTime.StartOf = 'day',
) {
const dateFormat = 'YYYY-MM-DD';
const fromDate = moment(startDate).startOf(type).format(dateFormat);
const toDate = moment(endDate).endOf(type).format(dateFormat);
if (startDate) {
query.where('date', '>=', fromDate);
}
if (endDate) {
query.where('date', '<=', toDate);
}
},
/**
* Filters the amount range.
* @param {Query} query
* @param {number} fromAmount
* @param {number} toAmount
*/
filterAmountRange(query, fromAmount, toAmount) {
if (fromAmount) {
query.andWhere((q) => {
q.where('credit', '>=', fromAmount);
q.orWhere('debit', '>=', fromAmount);
});
}
if (toAmount) {
query.andWhere((q) => {
q.where('credit', '<=', toAmount);
q.orWhere('debit', '<=', toAmount);
});
}
},
sumationCreditDebit(query) {
query.select(['accountId']);
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('account_id');
},
filterContactType(query, contactType) {
query.where('contact_type', contactType);
},
filterContactIds(query, contactIds) {
query.whereIn('contact_id', contactIds);
},
openingBalance(query, fromDate) {
query.modify('filterDateRange', null, fromDate);
query.modify('sumationCreditDebit');
},
closingBalance(query, toDate) {
query.modify('filterDateRange', null, toDate);
query.modify('sumationCreditDebit');
},
contactsOpeningBalance(
query,
openingDate,
receivableAccounts,
customersIds,
) {
// Filter by date.
query.modify('filterDateRange', null, openingDate);
// Filter by customers.
query.whereNot('contactId', null);
query.whereIn('accountId', castArray(receivableAccounts));
if (!isEmpty(customersIds)) {
query.whereIn('contactId', castArray(customersIds));
}
// Group by the contact transactions.
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
},
creditDebitSummation(query) {
query.sum('credit as credit');
query.sum('debit as debit');
},
groupByDateFormat(query, groupType = 'month') {
const groupBy = {
day: '%Y-%m-%d',
month: '%Y-%m',
year: '%Y',
};
const dateFormat = groupBy[groupType];
query.select(raw(`DATE_FORMAT(DATE, '${dateFormat}')`).as('date'));
query.groupByRaw(`DATE_FORMAT(DATE, '${dateFormat}')`);
},
filterByBranches(query, branchesIds) {
const formattedBranchesIds = castArray(branchesIds);
query.whereIn('branchId', formattedBranchesIds);
},
filterByProjects(query, projectsIds) {
const formattedProjectsIds = castArray(projectsIds);
query.whereIn('projectId', formattedProjectsIds);
},
filterByReference(query, referenceId: number, referenceType: string) {
query.where('reference_id', referenceId);
query.where('reference_type', referenceType);
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { Account } = require('./Account.model');
const { Contact } = require('../../Contacts/models/Contact');
return {
account: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'accounts_transactions.accountId',
to: 'accounts.id',
},
},
contact: {
relation: Model.BelongsToOneRelation,
modelClass: Contact,
join: {
from: 'accounts_transactions.contactId',
to: 'contacts.id',
},
},
};
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,390 @@
import { Knex } from 'knex';
import { Inject, Injectable, Scope } from '@nestjs/common';
import { TenantRepository } from '@/common/repository/TenantRepository';
import { TENANCY_DB_CONNECTION } from '@/modules/Tenancy/TenancyDB/TenancyDB.constants';
import { Account } from '../models/Account.model';
import { I18nService } from 'nestjs-i18n';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import {
DiscountExpenseAccount,
OtherChargesAccount,
OtherExpensesAccount,
PrepardExpenses,
PurchaseDiscountAccount,
StripeClearingAccount,
TaxPayableAccount,
UnearnedRevenueAccount,
} from '../Accounts.constants';
@Injectable({ scope: Scope.REQUEST })
export class AccountRepository extends TenantRepository {
constructor(
private readonly i18n: I18nService,
private readonly tenancyContext: TenancyContext,
@Inject(TENANCY_DB_CONNECTION)
private readonly tenantDBKnex: Knex,
) {
super();
}
/**
* Gets the repository's model.
*/
get model(): typeof Account {
return Account.bindKnex(this.tenantDBKnex);
}
/**
* Retrieve accounts dependency graph.
* @param {string} withRelation
* @param {Knex.Transaction} trx
* @returns {}
*/
public async getDependencyGraph(
withRelation?: string,
trx?: Knex.Transaction,
) {
const accounts = await this.all(withRelation, trx);
return this.model.toDependencyGraph(accounts);
}
/**
* Retrieve account by slug.
* @param {string} slug
* @return {Promise<IAccount>}
*/
public findBySlug(slug: string) {
return this.findOne({ slug });
}
// /**
// * Changes account balance.
// * @param {number} accountId
// * @param {number} amount
// * @return {Promise<void>}
// */
// async balanceChange(accountId: number, amount: number): Promise<void> {
// const method: string = amount < 0 ? 'decrement' : 'increment';
// await this.model.query().where('id', accountId)[method]('amount', amount);
// this.flushCache();
// }
/**
* Activate user by the given id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
activateById(userId: number): Promise<number> {
return super.update({ active: 1 }, { id: userId });
}
/**
* Inactivate user by the given id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
inactivateById(userId: number): Promise<number> {
return super.update({ active: 0 }, { id: userId });
}
/**
* Activate user by the given id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async activateByIds(userIds: number[], trx): Promise<number> {
const results = await this.model
.query(trx)
.whereIn('id', userIds)
.patch({ active: true });
return results;
}
/**
* Inactivate user by the given id.
* @param {number} userId - User id.
* @return {Promise<void>}
*/
async inactivateByIds(userIds: number[], trx): Promise<number> {
const results = await this.model
.query(trx)
.whereIn('id', userIds)
.patch({ active: false });
return results;
}
/**
*
* @param {string} currencyCode
* @param extraAttrs
* @param trx
* @returns
*/
findOrCreateAccountReceivable = async (
currencyCode: string = '',
extraAttrs = {},
trx?: Knex.Transaction,
) => {
let result = await this.model
.query(trx)
.onBuild((query) => {
if (currencyCode) {
query.where('currencyCode', currencyCode);
}
query.where('accountType', 'accounts-receivable');
})
.first();
if (!result) {
result = await this.model.query(trx).insertAndFetch({
name: this.i18n.t('account.accounts_receivable.currency', {
args: { currency: currencyCode },
}),
accountType: 'accounts-receivable',
currencyCode,
active: 1,
...extraAttrs,
});
}
return result;
};
/**
* Find or create tax payable account.
* @param {Record<string, string>}extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
async findOrCreateTaxPayable(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction,
) {
let result = await this.model
.query(trx)
.findOne({ slug: TaxPayableAccount.slug, ...extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...TaxPayableAccount,
...extraAttrs,
});
}
return result;
}
findOrCreateAccountsPayable = async (
currencyCode: string = '',
extraAttrs = {},
trx?: Knex.Transaction,
) => {
let result = await this.model
.query(trx)
.onBuild((query) => {
if (currencyCode) {
query.where('currencyCode', currencyCode);
}
query.where('accountType', 'accounts-payable');
})
.first();
if (!result) {
result = await this.model.query(trx).insertAndFetch({
name: this.i18n.t('account.accounts_payable.currency', {
args: { currency: currencyCode },
}),
accountType: 'accounts-payable',
currencyCode,
active: 1,
...extraAttrs,
});
}
return result;
};
/**
* Finds or creates the unearned revenue.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateUnearnedRevenue(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction,
) {
const tenantMeta = await this.tenancyContext.getTenantMetadata();
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: UnearnedRevenueAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...UnearnedRevenueAccount,
..._extraAttrs,
});
}
return result;
}
/**
* Finds or creates the prepard expenses account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreatePrepardExpenses(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction,
) {
const tenantMeta = await this.tenancyContext.getTenantMetadata();
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: PrepardExpenses.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...PrepardExpenses,
..._extraAttrs,
});
}
return result;
}
/**
* Finds or creates the stripe clearing account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateStripeClearing(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction,
) {
const tenantMeta = await this.tenancyContext.getTenantMetadata();
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: StripeClearingAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...StripeClearingAccount,
..._extraAttrs,
});
}
return result;
}
/**
* Finds or creates the discount expense account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateDiscountAccount(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction,
) {
const tenantMeta = await this.tenancyContext.getTenantMetadata();
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: DiscountExpenseAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...DiscountExpenseAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreatePurchaseDiscountAccount(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction,
) {
const tenantMeta = await this.tenancyContext.getTenantMetadata();
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: PurchaseDiscountAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...PurchaseDiscountAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreateOtherChargesAccount(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction,
) {
const tenantMeta = await this.tenancyContext.getTenantMetadata();
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: OtherChargesAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...OtherChargesAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreateOtherExpensesAccount(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction,
) {
const tenantMeta = await this.tenancyContext.getTenantMetadata();
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: OtherExpensesAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...OtherExpensesAccount,
..._extraAttrs,
});
}
return result;
}
}

View File

@@ -0,0 +1,34 @@
// import { Service, Inject } from 'typedi';
// import events from '@/subscribers/events';
// import { MutateBaseCurrencyAccounts } from '../MutateBaseCurrencyAccounts';
// @Service()
// export class MutateBaseCurrencyAccountsSubscriber {
// @Inject()
// public mutateBaseCurrencyAccounts: MutateBaseCurrencyAccounts;
// /**
// * Attaches the events with handles.
// * @param bus
// */
// attach(bus) {
// bus.subscribe(
// events.organization.baseCurrencyUpdated,
// this.updateAccountsCurrencyOnBaseCurrencyMutated
// );
// }
// /**
// * Updates the all accounts currency once the base currency
// * of the organization is mutated.
// */
// private updateAccountsCurrencyOnBaseCurrencyMutated = async ({
// tenantId,
// organizationDTO,
// }) => {
// await this.mutateBaseCurrencyAccounts.mutateAllAccountsCurrency(
// tenantId,
// organizationDTO.baseCurrency
// );
// };
// }

View File

@@ -0,0 +1,101 @@
import { get } from 'lodash';
import { ACCOUNT_TYPES } from '../Accounts.constants';
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;
});
}
}

View 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!');
});
});
});

View 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();
}
}

View File

@@ -0,0 +1,192 @@
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';
import { TransformerModule } from '../Transformer/Transformer.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { ExpensesModule } from '../Expenses/Expenses.module';
import { ItemCategoryModule } from '../ItemCategories/ItemCategory.module';
import { TaxRatesModule } from '../TaxRates/TaxRate.module';
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { BranchesModule } from '../Branches/Branches.module';
import { WarehousesModule } from '../Warehouses/Warehouses.module';
import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor';
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
import { CustomersModule } from '../Customers/Customers.module';
import { VendorsModule } from '../Vendors/Vendors.module';
import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module';
import { BillsModule } from '../Bills/Bills.module';
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
import { SaleReceiptsModule } from '../SaleReceipts/SaleReceipts.module';
import { ManualJournalsModule } from '../ManualJournals/ManualJournals.module';
import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
import { VendorCreditsModule } from '../VendorCredit/VendorCredits.module';
import { VendorCreditApplyBillsModule } from '../VendorCreditsApplyBills/VendorCreditApplyBills.module';
import { VendorCreditsRefundModule } from '../VendorCreditsRefund/VendorCreditsRefund.module';
import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module';
import { BillPaymentsModule } from '../BillPayments/BillPayments.module';
import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module';
import { LedgerModule } from '../Ledger/Ledger.module';
import { BankRulesModule } from '../BankRules/BankRules.module';
import { BankAccountsModule } from '../BankingAccounts/BankAccounts.module';
import { BankingTransactionsExcludeModule } from '../BankingTransactionsExclude/BankingTransactionsExclude.module';
import { BankingTransactionsRegonizeModule } from '../BankingTranasctionsRegonize/BankingTransactionsRegonize.module';
import { BankingMatchingModule } from '../BankingMatching/BankingMatching.module';
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
import { TransactionsLockingModule } from '../TransactionsLocking/TransactionsLocking.module';
import { SettingsModule } from '../Settings/Settings.module';
import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module';
import { PostHogModule } from '../EventsTracker/postHog.module';
import { EventTrackerModule } from '../EventsTracker/EventTracker.module';
import { MailModule } from '../Mail/Mail.module';
@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,
ChromiumlyTenancyModule,
TransformerModule,
MailModule,
ItemsModule,
ItemCategoryModule,
AccountsModule,
ExpensesModule,
TaxRatesModule,
PdfTemplatesModule,
BranchesModule,
WarehousesModule,
CustomersModule,
VendorsModule,
SaleInvoicesModule,
SaleEstimatesModule,
SaleReceiptsModule,
BillsModule,
ManualJournalsModule,
CreditNotesModule,
VendorCreditsModule,
VendorCreditApplyBillsModule,
VendorCreditsRefundModule,
CreditNoteRefundsModule,
BillPaymentsModule,
PaymentsReceivedModule,
LedgerModule,
BankAccountsModule,
BankRulesModule,
BankingTransactionsModule,
BankingTransactionsExcludeModule,
BankingTransactionsRegonizeModule,
BankingMatchingModule,
TransactionsLockingModule,
SettingsModule,
InventoryAdjustmentsModule,
PostHogModule,
EventTrackerModule,
],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: SerializeInterceptor,
},
{
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 });
}
}

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

View File

@@ -0,0 +1,19 @@
import { Transformer } from "../Transformer/Transformer";
export class AttachmentTransformer extends Transformer {
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['id', 'createdAt'];
};
/**
* Includeded attributes.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [];
};
}

View File

@@ -0,0 +1,3 @@
export interface AttachmentLinkDTO {
key: string;
}

View 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.',
};

View File

@@ -0,0 +1,5 @@
export interface IAuthSignedInEventPayload {}
export interface IAuthSigningInEventPayload {}
export interface IAuthSignInPOJO {}

View File

@@ -0,0 +1,4 @@
export class AuthApplication {
}

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

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

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
import { AutoIncrementOrdersService } from './AutoIncrementOrders.service';
@Module({
imports: [TenancyDatabaseModule],
controllers: [],
providers: [AutoIncrementOrdersService],
exports: [AutoIncrementOrdersService],
})
export class AutoIncrementOrdersModule {}

View File

@@ -0,0 +1,62 @@
import { Injectable } from '@nestjs/common';
/**
* Auto increment orders service.
*/
@Injectable()
export class AutoIncrementOrdersService {
/**
* Check if the auto increment is enabled for the given settings group.
* @param {string} settingsGroup - Settings group.
* @returns {boolean}
*/
public autoIncrementEnabled = (settingsGroup: string): boolean => {
// const settings = this.tenancy.settings(tenantId);
// const group = settingsGroup;
// // Settings service transaction number and prefix.
// return settings.get({ group, key: 'auto_increment' }, false);
return true;
};
/**
* Retrieve the next service transaction number.
* @param {string} settingsGroup
* @param {Function} getMaxTransactionNo
* @return {Promise<string>}
*/
getNextTransactionNumber(group: string): string {
// const settings = this.tenancy.settings(tenantId);
// // Settings service transaction number and prefix.
// const autoIncrement = this.autoIncrementEnabled(tenantId, group);
// const settingNo = settings.get({ group, key: 'next_number' }, '');
// const settingPrefix = settings.get({ group, key: 'number_prefix' }, '');
// return autoIncrement ? `${settingPrefix}${settingNo}` : '';
return '1';
}
/**
* Increment setting next number.
* @param {string} orderGroup - Order group.
* @param {string} orderNumber -Order number.
*/
async incrementSettingsNextNumber(group: string) {
// const settings = this.tenancy.settings(tenantId);
// const settingNo = settings.get({ group, key: 'next_number' });
// const autoIncrement = settings.get({ group, key: 'auto_increment' });
// // Can't continue if the auto-increment of the service was disabled.
// if (!autoIncrement) {
// return;
// }
// settings.set(
// { group, key: 'next_number' },
// transactionIncrement(settingNo)
// );
// await settings.save();
}
}

View File

@@ -0,0 +1,49 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { BankRulesApplication } from './BankRulesApplication';
import { ICreateBankRuleDTO, IEditBankRuleDTO } from './types';
import { PublicRoute } from '../Auth/Jwt.guard';
import { BankRule } from './models/BankRule';
@Controller('banking/rules')
@PublicRoute()
export class BankRulesController {
constructor(private readonly bankRulesApplication: BankRulesApplication) {}
@Post()
async createBankRule(
@Body() createRuleDTO: ICreateBankRuleDTO,
): Promise<BankRule> {
return this.bankRulesApplication.createBankRule(createRuleDTO);
}
@Put(':id')
async editBankRule(
@Param('id') ruleId: number,
@Body() editRuleDTO: IEditBankRuleDTO,
): Promise<void> {
return this.bankRulesApplication.editBankRule(ruleId, editRuleDTO);
}
@Delete(':id')
async deleteBankRule(@Param('id') ruleId: number): Promise<void> {
return this.bankRulesApplication.deleteBankRule(ruleId);
}
@Get(':id')
async getBankRule(@Param('id') ruleId: number): Promise<any> {
return this.bankRulesApplication.getBankRule(ruleId);
}
@Get()
async getBankRules(): Promise<any> {
return this.bankRulesApplication.getBankRules();
}
}

View File

@@ -0,0 +1,37 @@
import { forwardRef, Module } from '@nestjs/common';
import { CreateBankRuleService } from './commands/CreateBankRule.service';
import { EditBankRuleService } from './commands/EditBankRule.service';
import { DeleteBankRuleService } from './commands/DeleteBankRule.service';
import { GetBankRulesService } from './queries/GetBankRules.service';
import { GetBankRuleService } from './queries/GetBankRule.service';
import { BankRulesApplication } from './BankRulesApplication';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { BankRuleCondition } from './models/BankRuleCondition';
import { BankRule } from './models/BankRule';
import { BankRulesController } from './BankRules.controller';
import { UnlinkBankRuleOnDeleteBankRuleSubscriber } from './events/UnlinkBankRuleOnDeleteBankRule';
import { DeleteBankRulesService } from './commands/DeleteBankRules.service';
import { BankingTransactionsRegonizeModule } from '../BankingTranasctionsRegonize/BankingTransactionsRegonize.module';
const models = [
RegisterTenancyModel(BankRule),
RegisterTenancyModel(BankRuleCondition),
];
@Module({
controllers: [BankRulesController],
imports: [forwardRef(() => BankingTransactionsRegonizeModule)],
providers: [
...models,
CreateBankRuleService,
EditBankRuleService,
DeleteBankRuleService,
DeleteBankRulesService,
GetBankRuleService,
GetBankRulesService,
BankRulesApplication,
UnlinkBankRuleOnDeleteBankRuleSubscriber
],
exports: [...models, DeleteBankRuleService, DeleteBankRulesService],
})
export class BankRulesModule {}

View File

@@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import { CreateBankRuleService } from './commands/CreateBankRule.service';
import { DeleteBankRuleService } from './commands/DeleteBankRule.service';
import { EditBankRuleService } from './commands/EditBankRule.service';
import { GetBankRuleService } from './queries/GetBankRule.service';
import { GetBankRulesService } from './queries/GetBankRules.service';
import { ICreateBankRuleDTO, IEditBankRuleDTO } from './types';
import { BankRule } from './models/BankRule';
@Injectable()
export class BankRulesApplication {
constructor(
private readonly createBankRuleService: CreateBankRuleService,
private readonly editBankRuleService: EditBankRuleService,
private readonly deleteBankRuleService: DeleteBankRuleService,
private readonly getBankRuleService: GetBankRuleService,
private readonly getBankRulesService: GetBankRulesService,
) {}
/**
* Creates new bank rule.
* @param {ICreateBankRuleDTO} createRuleDTO - Bank rule data.
* @returns {Promise<void>}
*/
public createBankRule(
createRuleDTO: ICreateBankRuleDTO,
): Promise<BankRule> {
return this.createBankRuleService.createBankRule(createRuleDTO);
}
/**
* Edits the given bank rule.
* @param {number} ruleId - Bank rule identifier.
* @param {IEditBankRuleDTO} editRuleDTO - Bank rule data.
* @returns {Promise<void>}
*/
public editBankRule(
ruleId: number,
editRuleDTO: IEditBankRuleDTO,
): Promise<void> {
return this.editBankRuleService.editBankRule(ruleId, editRuleDTO);
}
/**
* Deletes the given bank rule.
* @param {number} ruleId - Bank rule identifier.
* @returns {Promise<void>}
*/
public deleteBankRule(ruleId: number): Promise<void> {
return this.deleteBankRuleService.deleteBankRule(ruleId);
}
/**
* Retrieves the given bank rule.
* @param {number} ruleId - Bank rule identifier.
* @returns {Promise<any>}
*/
public getBankRule(ruleId: number): Promise<any> {
return this.getBankRuleService.getBankRule(ruleId);
}
/**
* Retrieves the bank rules of the given account.
* @param {number} accountId - Bank account identifier.
* @returns {Promise<any>}
*/
public getBankRules(): Promise<any> {
return this.getBankRulesService.getBankRules();
}
}

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