Compare commits

..

105 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
1130975efd refactor(nestjs): landed cost 2025-06-10 17:08:32 +02:00
Ahmed Bouhuolia
fa180b3ac5 refactor: gl entries 2025-06-10 12:29:46 +02:00
Ahmed Bouhuolia
90d6bea9b9 fix: mail state 2025-06-09 15:37:20 +02:00
Ahmed Bouhuolia
4366bf478a refactor: mail templates 2025-06-08 16:49:03 +02:00
Ahmed Bouhuolia
0a57b6e20e fix: cashflow statement localization 2025-06-06 20:40:56 +02:00
Ahmed Bouhuolia
9a685ffe5d refactor: financial reports query dtos 2025-06-06 00:11:51 +02:00
Ahmed Bouhuolia
51988dba3b refactor(nestjs): bank transactions matching 2025-06-05 14:41:26 +02:00
Ahmed Bouhuolia
f87bd341e9 refactor(nestjs): banking modules 2025-06-03 21:42:09 +02:00
Ahmed Bouhuolia
5595478e19 refactor(nestjs): banking module 2025-06-02 21:32:53 +02:00
Ahmed Bouhuolia
7247b52fe5 refactor(nestjs): banking module 2025-06-02 15:41:41 +02:00
Ahmed Bouhuolia
deadd5ac80 refactor(nestjs): plaid banking syncing 2025-06-01 18:38:44 +02:00
Ahmed Bouhuolia
66a2261e50 refactor(nestjs): wip 2025-05-28 21:32:48 +02:00
Ahmed Bouhuolia
c51347d3ec refactor(nestjs): wip import module 2025-05-28 17:01:46 +02:00
Ahmed Bouhuolia
b7a3c42074 refactor(nestjs): wip 2025-05-27 15:42:27 +02:00
Ahmed Bouhuolia
83c9392b74 refactor(nestjs): wip dtos validation schema 2025-05-26 17:04:53 +02:00
Ahmed Bouhuolia
24bf3dd06d refactor(nestjs): validation schema dtos 2025-05-25 23:39:54 +02:00
Ahmed Bouhuolia
2b3f98d8fe refactor(nestjs): hook the new endpoints 2025-05-22 19:55:55 +02:00
Ahmed Bouhuolia
4e64a9eadb refactor(nestjs): pdf templates 2025-05-22 13:36:10 +02:00
Ahmed Bouhuolia
0823bfc4e9 refactor(nestjs): contacts module 2025-05-20 23:55:39 +02:00
Ahmed Bouhuolia
99fe5a6b0d refactor(nestjs): Implement users module 2025-05-20 17:55:58 +02:00
Ahmed Bouhuolia
ce058b9416 refactor(nestjs): currencies module 2025-05-17 12:14:02 +02:00
Ahmed Bouhuolia
4de1ef71ca refactor(nestjs): hook up new endpoints 2025-05-16 01:41:11 +02:00
Ahmed Bouhuolia
ecb80b2cf2 refactor(nestjs): hook up the client with new endpoints 2025-05-14 21:45:13 +02:00
Ahmed Bouhuolia
aef208b9d8 refactor(nestjs): resource meta endpoint 2025-05-12 15:44:39 +02:00
Ahmed Bouhuolia
c096135d9f fix: the reports endpoint url 2025-05-11 23:53:59 +02:00
Ahmed Bouhuolia
0c9d961272 fix: financial reports i18n 2025-05-11 17:26:55 +02:00
Ahmed Bouhuolia
c10cad4256 fix: financial reports responses 2025-05-11 15:06:03 +02:00
Ahmed Bouhuolia
9ebd967fe7 fix: return wrong response 2025-05-11 00:40:43 +02:00
Ahmed Bouhuolia
a42143a996 fix: retrieve the build org job state 2025-05-10 22:33:54 +02:00
Ahmed Bouhuolia
7506c2f37f refactor(nestjs): replace the reports endpoints 2025-05-09 18:55:16 +02:00
Ahmed Bouhuolia
3c8b7c92fe feat(nestjs): resend the auth confirmation message 2025-05-08 19:01:43 +02:00
Ahmed Bouhuolia
f78d6efe27 refactor(nestjs): hook up auth endpoints 2025-05-08 18:10:02 +02:00
Ahmed Bouhuolia
401b3dc111 refactor(nestjs): add sale receipts retrieval and metadata configuration 2025-05-04 19:42:35 +02:00
Ahmed Bouhuolia
c9d752d102 refactor(nestjs): list resources 2025-05-04 11:19:34 +02:00
Ahmed Bouhuolia
4f6ad2b293 feat: apply credit note to invoice module 2025-05-04 01:32:08 +02:00
Ahmed Bouhuolia
1d53063e09 refactor(nestjs): add importable service to other modules 2025-04-12 19:26:15 +02:00
Ahmed Bouhuolia
51de3631fc Merge pull request #807 from bigcapitalhq/import-module
refactor(nestjs): Import module
2025-04-12 13:40:11 +02:00
Ahmed Bouhuolia
b9755ff01c refactor(nestjs): import module 2025-04-12 13:39:17 +02:00
Ahmed Bouhuolia
5bfff51093 refactor(nestjs): import module 2025-04-12 08:38:29 +02:00
Ahmed Bouhuolia
1bcee9293c Merge pull request #806 from bigcapitalhq/refactor-export-module
refactor(nestjs): export module
2025-04-10 23:35:27 +02:00
Ahmed Bouhuolia
c953c48c39 refactor(nestjs): export module 2025-04-10 23:34:42 +02:00
Ahmed Bouhuolia
ab49113d5a refactor(nestjs): export and import module 2025-04-09 18:35:17 +02:00
Ahmed Bouhuolia
d851e5b646 refactor(nestjs): import module 2025-04-09 10:39:08 +02:00
Ahmed Bouhuolia
e8f1fedf35 refactor(nestjs): exportable modules 2025-04-08 22:44:24 +02:00
Ahmed Bouhuolia
04c25bd31a refactor(nestjs): export module 2025-04-08 16:19:35 +02:00
Ahmed Bouhuolia
6287f8b6e3 refactor(nestjs): fix the failed e2e test cases 2025-04-07 22:50:11 +02:00
Ahmed Bouhuolia
4febc4e502 refactor(nestjs): transaction locking 2025-04-07 13:35:02 +02:00
Ahmed Bouhuolia
443fbdd89e feat: update the README.md file 2025-04-07 11:51:49 +02:00
Ahmed Bouhuolia
55fcc908ef feat(nestjs): migrate to NestJS 2025-04-07 11:51:24 +02:00
Ahmed Bouhuolia
f068218a16 refactor(nestjs): e2e test cases 2025-04-07 00:09:58 +02:00
Ahmed Bouhuolia
842a862b87 refactor(nestjs): attachments module 2025-04-06 21:13:46 +02:00
Ahmed Bouhuolia
1ed77dd5ed refactor(nestjs): attachments and s3 modules 2025-04-04 20:56:31 +02:00
Ahmed Bouhuolia
e47ca98171 refactor(nestjs): organization module e2e 2025-04-04 20:29:08 +02:00
Ahmed Bouhuolia
503d0016ea refactor(nestjs): loops module 2025-04-03 20:03:55 +02:00
Ahmed Bouhuolia
0a2ac4ee56 refactor(nestjs): seed migrations 2025-04-03 19:57:11 +02:00
Ahmed Bouhuolia
8eb23d3a6f refactor(nestjs): seed migrations 2025-04-02 20:57:13 +02:00
Ahmed Bouhuolia
18017d25d5 refactor(nestjs): authentication 2025-04-02 15:50:00 +02:00
Ahmed Bouhuolia
f11b09cd87 refactor(nestjs): organization build 2025-04-02 12:04:03 +02:00
Ahmed Bouhuolia
ed81d4c1e0 refactor(nestjs): auth module 2025-04-01 09:13:12 +02:00
Ahmed Bouhuolia
88f66f1c1c refactor(nestjs): auth module 2025-03-31 13:49:57 +02:00
Ahmed Bouhuolia
ab717b96ac refactor(nestjs): e2e test cases 2025-03-31 00:39:00 +02:00
Ahmed Bouhuolia
caff6ce47c refactor: tenant models to nestjs 2025-03-30 21:22:54 +02:00
Ahmed Bouhuolia
682be715ae refactor: auth module to nestjs 2025-03-30 05:20:50 +02:00
Ahmed Bouhuolia
85946d3161 refactor: authentication module to nestjs 2025-03-29 22:29:12 +02:00
Ahmed Bouhuolia
173610d0fa refactor: payment services to nestjs 2025-03-28 23:54:40 +02:00
Ahmed Bouhuolia
f20f07a42f refactor: payment services to nestjs 2025-03-28 06:00:58 +02:00
Ahmed Bouhuolia
6251831741 refactor: nestjs 2025-03-28 04:32:57 +02:00
Ahmed Bouhuolia
1cfddf2b4d refactor 2025-03-28 04:08:27 +02:00
Ahmed Bouhuolia
6461a2318f refactor: implement tenant database management and seeding utilities 2025-03-27 23:13:17 +02:00
Ahmed Bouhuolia
92d98ce1d3 refactor: organization service to nestjs 2025-03-25 04:34:22 +02:00
Ahmed Bouhuolia
ef22b9ddaf refactor: subscriptions to nestjs 2025-03-24 23:38:43 +02:00
Ahmed Bouhuolia
4c42515613 refactor: dtos openapi 2025-03-22 23:21:52 +02:00
Ahmed Bouhuolia
2eb56e5850 refactor: nestjs 2025-03-22 20:36:48 +02:00
Ahmed Bouhuolia
136cc907bb refactor: dtos validation 2025-03-20 05:42:19 +02:00
Ahmed Bouhuolia
fd65ee9428 refactor: api validation schema 2025-03-14 23:24:59 +02:00
Ahmed Bouhuolia
08de50e2b1 refactor: inventory cost process 2025-03-14 03:51:45 +02:00
Ahmed Bouhuolia
197d173db9 refactor: warehouse transfers 2025-03-13 02:40:09 +02:00
Ahmed Bouhuolia
cf496909a5 refactor: inventory transfers to nestjs 2025-03-13 00:44:11 +02:00
Ahmed Bouhuolia
67ae7ad037 refactor: inventory cost to nestjs 2025-03-11 22:12:08 +02:00
Ahmed Bouhuolia
40b7daa2e3 refactor: settings module 2025-03-07 04:05:24 +02:00
Ahmed Bouhuolia
b7d0b6c24a refactor: branches and warehouses modules 2025-02-26 14:19:47 +02:00
Ahmed Bouhuolia
95bb4fc8e3 refactor: nestjs 2025-02-18 19:26:58 +02:00
Ahmed Bouhuolia
5c0bb52b59 refactor: tenant proxy providers 2025-02-15 23:52:12 +02:00
Ahmed Bouhuolia
36851d3209 refactor 2025-02-12 10:15:00 +02:00
Ahmed Bouhuolia
9eee0b384d refactor: nestjs 2025-02-07 20:28:35 +02:00
Ahmed Bouhuolia
9539003cac refactor: customer/vendor balance summary to nestjs 2025-02-05 10:38:47 +02:00
Ahmed Bouhuolia
2017539032 refactor: nestjs 2025-02-04 13:17:25 +02:00
Ahmed Bouhuolia
c4692d1716 refactor: balance sheet to nestjs 2025-01-30 01:57:29 +02:00
Ahmed Bouhuolia
7b81d0c8e5 refactor: financial statements to nestjs 2025-01-29 00:55:53 +02:00
Ahmed Bouhuolia
9a5110aa38 refactor: reports to nestjs 2025-01-21 23:29:31 +02:00
Ahmed Bouhuolia
2e1c57438c refactor: reports to nestjs 2025-01-21 11:53:29 +02:00
Ahmed Bouhuolia
b46f2a91c3 refactor: financial statements to nestjs 2025-01-21 11:38:07 +02:00
Ahmed Bouhuolia
8e36aab529 refator: reports to nestjs 2025-01-20 15:44:06 +02:00
Ahmed Bouhuolia
9eec60ea22 refactor: financial statements to nestjs 2025-01-20 01:05:33 +02:00
Ahmed Bouhuolia
6550e88af3 refactor: financial reports to nestjs 2025-01-18 23:51:29 +02:00
Ahmed Bouhuolia
dfc5674088 refactor: financial reports to nestjs 2025-01-18 22:32:45 +02:00
Ahmed Bouhuolia
6dd854178d refactor: financial reports to nestjs 2025-01-16 12:58:45 +02:00
Ahmed Bouhuolia
520d053b36 refactor: document api endpoints 2025-01-15 17:18:42 +02:00
Ahmed Bouhuolia
108d286f62 refactor: e2e test cases 2025-01-15 17:02:42 +02:00
Ahmed Bouhuolia
271c46ea3b refactor 2025-01-15 15:52:18 +02:00
Ahmed Bouhuolia
7bcd578c11 refactor 2025-01-15 15:28:39 +02:00
Ahmed Bouhuolia
936800600b refactor: inventory to nestjs 2025-01-15 14:14:44 +02:00
Ahmed Bouhuolia
e7e7a95aa1 refactor: dynamic list to nestjs 2025-01-14 22:57:54 +02:00
Ahmed Bouhuolia
081fdebee0 refaqctor: document openapi endpoints 2025-01-14 00:01:59 +02:00
Ahmed Bouhuolia
4ab20ac76a refactor: mail services to nestjs 2025-01-13 16:07:05 +02:00
4308 changed files with 91856 additions and 220754 deletions

View File

@@ -1,3 +1,6 @@
# App
APP_JWT_SECRET=123123
# Mail
MAIL_HOST=
MAIL_USERNAME=

3
.gitignore vendored
View File

@@ -6,4 +6,5 @@ node_modules/
# Production env file
.env
test-results/
test-results/
.qodo

View File

@@ -34,6 +34,8 @@
</p>
</p>
> We are currently in the process of migrating all server-side API endpoints to NestJS to establish a more solid architecture. Some endpoints in development mode may be temporarily do not work during this stabilization phase. However, this migration doesn't affect the production Docker images, which remain on the latest stable version.
# What's Bigcapital?
Bigcapital is a smart and open-source accounting and inventory software, Bigcapital keeps all business finances in right place and automates accounting processes to give the business powerful and intelligent financial statements and reports to help in making decisions.

View File

@@ -41,6 +41,8 @@ services:
context: ./docker/redis
expose:
- "6379"
ports:
- "6379:6379"
volumes:
- redis:/data
deploy:

View File

@@ -1,4 +1,4 @@
FROM redis:4.0
FROM redis:6.2.0
COPY redis.conf /usr/local/etc/redis/redis.conf

24
launch.json Normal file
View File

@@ -0,0 +1,24 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Nest Framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"start:debug",
"--",
"--inspect-brk"
],
"autoAttachChildProcesses": true,
"restart": true,
"sourceMaps": true,
"stopOnEntry": false,
"console": "integratedTerminal"
}
]
}

View File

@@ -12,6 +12,7 @@
"server2:start": "lerna run start:dev --scope \"@bigcapital/server2\"",
"test:watch": "lerna run test:watch",
"test:e2e": "lerna run test:e2e",
"start:debug": "lerna run start:debug",
"prepare": "husky install"
},
"devDependencies": {

View File

@@ -1,25 +0,0 @@
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',
},
};

View File

@@ -1,56 +0,0 @@
# 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

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

View File

@@ -1,127 +0,0 @@
{
"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

@@ -1,21 +0,0 @@
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

@@ -1,9 +0,0 @@
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

@@ -1,756 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,21 +0,0 @@
{
"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

@@ -1,174 +0,0 @@
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

@@ -1,165 +0,0 @@
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

@@ -1,193 +0,0 @@
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

@@ -1,66 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,172 +0,0 @@
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

@@ -1,159 +0,0 @@
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

@@ -1,61 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,43 +0,0 @@
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

@@ -1,72 +0,0 @@
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

@@ -1,32 +0,0 @@
// 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

@@ -1,45 +0,0 @@
// 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

@@ -1,51 +0,0 @@
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

@@ -1,34 +0,0 @@
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

@@ -1,66 +0,0 @@
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

@@ -1,22 +0,0 @@
// 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

@@ -1,34 +0,0 @@
// 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

@@ -1,192 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,4 +0,0 @@
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

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

View File

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

View File

@@ -1,32 +0,0 @@
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

@@ -1,19 +0,0 @@
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

@@ -1,62 +0,0 @@
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

@@ -1,49 +0,0 @@
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

@@ -1,70 +0,0 @@
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();
}
}

View File

@@ -1,62 +0,0 @@
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import {
IBankRuleEventCreatedPayload,
IBankRuleEventCreatingPayload,
ICreateBankRuleDTO,
} from '../types';
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { BankRule } from '../models/BankRule';
@Injectable()
export class CreateBankRuleService {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
@Inject(BankRule.name) private readonly bankRuleModel: typeof BankRule,
) {}
/**
* Transforms the DTO to model.
* @param {ICreateBankRuleDTO} createDTO
*/
private transformDTO(createDTO: ICreateBankRuleDTO) {
return {
...createDTO,
};
}
/**
* Creates a new bank rule.
* @param {ICreateBankRuleDTO} createRuleDTO
* @returns {Promise<BankRule>}
*/
public async createBankRule(
createRuleDTO: ICreateBankRuleDTO,
): Promise<BankRule> {
const transformDTO = this.transformDTO(createRuleDTO);
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onBankRuleCreating` event.
await this.eventPublisher.emitAsync(events.bankRules.onCreating, {
createRuleDTO,
trx,
} as IBankRuleEventCreatingPayload);
const bankRule = await this.bankRuleModel.query(trx).upsertGraphAndFetch({
...transformDTO,
});
// Triggers `onBankRuleCreated` event.
await this.eventPublisher.emitAsync(events.bankRules.onCreated, {
createRuleDTO,
bankRule,
trx,
} as IBankRuleEventCreatedPayload);
return bankRule;
});
}
}

View File

@@ -1,71 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import {
IBankRuleEventEditedPayload,
IBankRuleEventEditingPayload,
IEditBankRuleDTO,
} from '../types';
import { BankRule } from '../models/BankRule';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
@Injectable()
export class EditBankRuleService {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
@Inject(BankRule.name) private bankRuleModel: typeof BankRule,
) {}
/**
*
* @param createDTO
* @returns
*/
private transformDTO(createDTO: IEditBankRuleDTO) {
return {
...createDTO,
};
}
/**
* Edits the given bank rule.
* @param {number} ruleId -
* @param {IEditBankRuleDTO} editBankDTO
*/
public async editBankRule(
ruleId: number,
editRuleDTO: IEditBankRuleDTO
) {
const oldBankRule = await this.bankRuleModel.query()
.findById(ruleId)
.withGraphFetched('conditions')
.throwIfNotFound();
const tranformDTO = this.transformDTO(editRuleDTO);
return this.uow.withTransaction(async (trx) => {
// Triggers `onBankRuleEditing` event.
await this.eventPublisher.emitAsync(events.bankRules.onEditing, {
oldBankRule,
ruleId,
editRuleDTO,
trx,
} as IBankRuleEventEditingPayload);
// Updates the given bank rule.
const bankRule = await this.bankRuleModel.query(trx).upsertGraphAndFetch({
...tranformDTO,
id: ruleId,
});
// Triggers `onBankRuleEdited` event.
await this.eventPublisher.emitAsync(events.bankRules.onEdited, {
oldBankRule,
bankRule,
editRuleDTO,
trx,
} as IBankRuleEventEditedPayload);
});
}
}

View File

@@ -1,74 +0,0 @@
import { BaseModel } from '@/models/Model';
import { Model } from 'objection';
import { BankRuleCondition } from './BankRuleCondition';
import { BankRuleAssignCategory, BankRuleConditionType } from '../types';
export class BankRule extends BaseModel {
public id!: number;
public name!: string;
public order!: number;
public applyIfAccountId!: number;
public applyIfTransactionType!: string;
public assignCategory!: BankRuleAssignCategory;
public assignAccountId!: number;
public assignPayee!: string;
public assignMemo!: string;
public conditionsType!: BankRuleConditionType;
conditions!: BankRuleCondition[];
/**
* Table name
*/
static get tableName() {
return 'bank_rules';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['created_at', 'updated_at'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { BankRuleCondition } = require('./BankRuleCondition');
const { Account } = require('../../Accounts/models/Account.model');
return {
/**
* Sale invoice associated entries.
*/
conditions: {
relation: Model.HasManyRelation,
modelClass: BankRuleCondition,
join: {
from: 'bank_rules.id',
to: 'bank_rule_conditions.ruleId',
},
},
/**
* Bank rule may associated to the assign account.
*/
assignAccount: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'bank_rules.assignAccountId',
to: 'accounts.id',
},
},
};
}
}

View File

@@ -1,31 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetBankRuleTransformer } from './GetBankRuleTransformer';
import { TransformerInjectable } from '../../Transformer/TransformerInjectable.service';
import { BankRule } from '../models/BankRule';
import { GetBankRulesTransformer } from './GetBankRulesTransformer';
@Injectable()
export class GetBankRuleService {
constructor(
@Inject(BankRule.name) private bankRuleModel: typeof BankRule,
private transformer: TransformerInjectable,
) {}
/**
* Retrieves the bank rule.
* @param {number} ruleId
* @returns {Promise<any>}
*/
async getBankRule(ruleId: number): Promise<any> {
const bankRule = await this.bankRuleModel
.query()
.findById(ruleId)
.withGraphFetched('conditions')
.withGraphFetched('assignAccount');
return this.transformer.transform(
bankRule,
new GetBankRulesTransformer()
);
}
}

View File

@@ -1,30 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetBankRulesTransformer } from './GetBankRulesTransformer';
import { BankRule } from '../models/BankRule';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
@Injectable()
export class GetBankRulesService {
constructor(
private transformer: TransformerInjectable,
@Inject(BankRule.name)
private bankRuleModel: typeof BankRule,
) {}
/**
* Retrieves the bank rules of the given account.
* @returns {Promise<any>}
*/
public async getBankRules(): Promise<any> {
const bankRule = await this.bankRuleModel
.query()
.withGraphFetched('conditions')
.withGraphFetched('assignAccount');
return this.transformer.transform(
bankRule,
new GetBankRulesTransformer()
);
}
}

View File

@@ -1,126 +0,0 @@
import { Knex } from 'knex';
import { BankRule } from './models/BankRule';
export enum BankRuleConditionField {
Amount = 'amount',
Description = 'description',
Payee = 'payee',
}
export enum BankRuleConditionComparator {
Contains = 'contains',
Equals = 'equals',
Equal = 'equal',
NotContain = 'not_contains',
Bigger = 'bigger',
BiggerOrEqual = 'bigger_or_equal',
Smaller = 'smaller',
SmallerOrEqual = 'smaller_or_equal',
}
export interface IBankRuleCondition {
id?: number;
field: BankRuleConditionField;
comparator: BankRuleConditionComparator;
value: string;
}
export enum BankRuleConditionType {
Or = 'or',
And = 'and',
}
export enum BankRuleApplyIfTransactionType {
Deposit = 'deposit',
Withdrawal = 'withdrawal',
}
// export interface BankRule {
// id?: number;
// name: string;
// order?: number;
// applyIfAccountId: number;
// applyIfTransactionType: BankRuleApplyIfTransactionType;
// conditionsType: BankRuleConditionType;
// conditions: IBankRuleCondition[];
// assignCategory: BankRuleAssignCategory;
// assignAccountId: number;
// assignPayee?: string;
// assignMemo?: string;
// }
export enum BankRuleAssignCategory {
InterestIncome = 'InterestIncome',
OtherIncome = 'OtherIncome',
Deposit = 'Deposit',
Expense = 'Expense',
OwnerDrawings = 'OwnerDrawings',
}
export type BankRuleComparator =
| 'contains'
| 'equals'
| 'not_contains'
| 'equal'
| 'bigger'
| 'bigger_or_equal'
| 'smaller'
| 'smaller_or_equal';
export interface IBankRuleConditionDTO {
id?: number;
field: string;
comparator: BankRuleComparator;
value: string;
}
export interface IBankRuleCommonDTO {
name: string;
order?: number;
applyIfAccountId: number;
applyIfTransactionType: string;
conditions: IBankRuleConditionDTO[];
assignCategory: BankRuleAssignCategory;
assignAccountId: number;
assignPayee?: string;
assignMemo?: string;
}
export interface ICreateBankRuleDTO extends IBankRuleCommonDTO {}
export interface IEditBankRuleDTO extends IBankRuleCommonDTO {}
export interface IBankRuleEventCreatingPayload {
createRuleDTO: ICreateBankRuleDTO;
trx?: Knex.Transaction;
}
export interface IBankRuleEventCreatedPayload {
createRuleDTO: ICreateBankRuleDTO;
bankRule: BankRule;
trx?: Knex.Transaction;
}
export interface IBankRuleEventEditingPayload {
ruleId: number;
oldBankRule: any;
editRuleDTO: IEditBankRuleDTO;
trx?: Knex.Transaction;
}
export interface IBankRuleEventEditedPayload {
oldBankRule: BankRule;
bankRule: BankRule;
editRuleDTO: IEditBankRuleDTO;
trx?: Knex.Transaction;
}
export interface IBankRuleEventDeletingPayload {
oldBankRule: any;
trx?: Knex.Transaction;
}
export interface IBankRuleEventDeletedPayload {
ruleId: number;
trx?: Knex.Transaction;
}

View File

@@ -1,27 +0,0 @@
import { Controller, Param, Post } from '@nestjs/common';
import { BankAccountsApplication } from './BankAccountsApplication.service';
@Controller('banking/accounts')
export class BankAccountsController {
constructor(private bankAccountsApplication: BankAccountsApplication) {}
@Post(':id/disconnect')
async disconnectBankAccount(@Param('id') bankAccountId: number) {
return this.bankAccountsApplication.disconnectBankAccount(bankAccountId);
}
@Post(':id/refresh')
async refreshBankAccount(@Param('id') bankAccountId: number) {
return this.bankAccountsApplication.refreshBankAccount(bankAccountId);
}
@Post(':id/pause')
async pauseBankAccount(@Param('id') bankAccountId: number) {
return this.bankAccountsApplication.pauseBankAccount(bankAccountId);
}
@Post(':id/resume')
async resumeBankAccount(@Param('id') bankAccountId: number) {
return this.bankAccountsApplication.resumeBankAccount(bankAccountId);
}
}

View File

@@ -1,28 +0,0 @@
import { Module } from '@nestjs/common';
import { BankAccountsApplication } from './BankAccountsApplication.service';
import { DisconnectBankAccountService } from './commands/DisconnectBankAccount.service';
import { RefreshBankAccountService } from './commands/RefreshBankAccount.service';
import { ResumeBankAccountFeedsService } from './commands/ResumeBankAccountFeeds.service';
import { PauseBankAccountFeeds } from './commands/PauseBankAccountFeeds.service';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from './subscribers/DeleteUncategorizedTransactionsOnAccountDeleting';
import { DisconnectPlaidItemOnAccountDeleted } from './subscribers/DisconnectPlaidItemOnAccountDeleted';
import { BankAccountsController } from './BankAccounts.controller';
import { BankingPlaidModule } from '../BankingPlaid/BankingPlaid.module';
import { PlaidModule } from '../Plaid/Plaid.module';
import { BankRulesModule } from '../BankRules/BankRules.module';
@Module({
imports: [BankingPlaidModule, PlaidModule, BankRulesModule],
providers: [
DisconnectBankAccountService,
RefreshBankAccountService,
ResumeBankAccountFeedsService,
PauseBankAccountFeeds,
// DeleteUncategorizedTransactionsOnAccountDeleting,
DisconnectPlaidItemOnAccountDeleted,
BankAccountsApplication
],
exports: [BankAccountsApplication],
controllers: [BankAccountsController],
})
export class BankAccountsModule {}

View File

@@ -1,57 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DisconnectBankAccountService } from './commands/DisconnectBankAccount.service';
import { RefreshBankAccountService } from './commands/RefreshBankAccount.service';
import { ResumeBankAccountFeedsService } from './commands/ResumeBankAccountFeeds.service';
import { PauseBankAccountFeeds } from './commands/PauseBankAccountFeeds.service';
@Injectable()
export class BankAccountsApplication {
constructor(
private disconnectBankAccountService: DisconnectBankAccountService,
private readonly refreshBankAccountService: RefreshBankAccountService,
private readonly resumeBankAccountFeedsService: ResumeBankAccountFeedsService,
private readonly pauseBankAccountFeedsService: PauseBankAccountFeeds,
) {}
/**
* Disconnects the given bank account.
* @param {number} bankAccountId - Bank account identifier.
* @returns {Promise<void>}
*/
async disconnectBankAccount(bankAccountId: number) {
return this.disconnectBankAccountService.disconnectBankAccount(
bankAccountId,
);
}
/**
* Refresh the bank transactions of the given bank account.
* @param {number} bankAccountId - Bank account identifier.
* @returns {Promise<void>}
*/
async refreshBankAccount(bankAccountId: number) {
return this.refreshBankAccountService.refreshBankAccount(bankAccountId);
}
/**
* Pauses the feeds sync of the given bank account.
* @param {number} bankAccountId - Bank account identifier.
* @returns {Promise<void>}
*/
async pauseBankAccount(bankAccountId: number) {
return this.pauseBankAccountFeedsService.pauseBankAccountFeeds(
bankAccountId,
);
}
/**
* Resumes the feeds sync of the given bank account.
* @param {number} bankAccountId - Bank account identifier.
* @returns {Promise<void>}
*/
async resumeBankAccount(bankAccountId: number) {
return this.resumeBankAccountFeedsService.resumeBankAccountFeeds(
bankAccountId,
);
}
}

View File

@@ -1,117 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Account } from '@/modules/Accounts/models/Account.model';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { BaseModel } from '@/models/Model';
@Injectable()
export class GetBankAccountSummary {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Retrieves the bank account meta summary
* @param {number} bankAccountId - The bank account id.
* @returns {Promise<IBankAccountSummary>}
*/
public async getBankAccountSummary(bankAccountId: number) {
const bankAccount = await this.accountModel
.query()
.findById(bankAccountId)
.throwIfNotFound();
const commonQuery = (q) => {
// Include just the given account.
q.where('accountId', bankAccountId);
// Only the not excluded.
q.modify('notExcluded');
// Only the not categorized.
q.modify('notCategorized');
};
interface UncategorizedTransactionsCount {
total: number;
}
// Retrieves the uncategorized transactions count of the given bank account.
const uncategorizedTranasctionsCount =
await this.uncategorizedBankTransactionModel.query().onBuild((q) => {
commonQuery(q);
// Only the not matched bank transactions.
q.withGraphJoined('matchedBankTransactions');
q.whereNull('matchedBankTransactions.id');
// Exclude the pending transactions.
q.modify('notPending');
// Count the results.
q.count('uncategorized_cashflow_transactions.id as total');
q.first();
});
// Retrives the recognized transactions count.
const recognizedTransactionsCount =
await this.uncategorizedBankTransactionModel.query().onBuild((q) => {
commonQuery(q);
q.withGraphJoined('recognizedTransaction');
q.whereNotNull('recognizedTransaction.id');
// Exclude the pending transactions.
q.modify('notPending');
// Count the results.
q.count('uncategorized_cashflow_transactions.id as total');
q.first();
});
// Retrieves excluded transactions count.
const excludedTransactionsCount =
await this.uncategorizedBankTransactionModel.query().onBuild((q) => {
q.where('accountId', bankAccountId);
q.modify('excluded');
// Exclude the pending transactions.
q.modify('notPending');
// Count the results.
q.count('uncategorized_cashflow_transactions.id as total');
q.first();
});
// Retrieves the pending transactions count.
const pendingTransactionsCount =
await this.uncategorizedBankTransactionModel.query().onBuild((q) => {
q.where('accountId', bankAccountId);
q.modify('pending');
// Count the results.
q.count('uncategorized_cashflow_transactions.id as total');
q.first();
});
const totalUncategorizedTransactions =
// @ts-ignore
uncategorizedTranasctionsCount?.total || 0;
// @ts-ignore
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
// @ts-ignore
const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
// @ts-ignore
const totalPendingTransactions = pendingTransactionsCount?.total || 0;
return {
name: bankAccount.name,
totalUncategorizedTransactions,
totalRecognizedTransactions,
totalExcludedTransactions,
totalPendingTransactions,
};
}
}

View File

@@ -1,55 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { IAccountEventDeletePayload } from '@/interfaces/Account';
import { RevertRecognizedTransactionsService } from '@/modules/BankingTranasctionsRegonize/commands/RevertRecognizedTransactions.service';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { DeleteBankRulesService } from '@/modules/BankRules/commands/DeleteBankRules.service';
import { BankRule } from '@/modules/BankRules/models/BankRule';
@Injectable()
export class DeleteUncategorizedTransactionsOnAccountDeleting {
constructor(
private readonly deleteBankRules: DeleteBankRulesService,
private readonly revertRecognizedTransactins: RevertRecognizedTransactionsService,
@Inject(BankRule.name) private bankRuleModel: typeof BankRule,
@Inject(UncategorizedBankTransaction.name)
private uncategorizedCashflowTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Handles revert the recognized transactions and delete all the bank rules
* associated to the deleted bank account.
* @param {IAccountEventDeletePayload}
*/
@OnEvent(events.accounts.onDelete)
public async handleDeleteBankRulesOnAccountDeleting({
oldAccount,
trx,
}: IAccountEventDeletePayload) {
const foundAssociatedRules = await this.bankRuleModel.query(trx).where(
'applyIfAccountId',
oldAccount.id,
);
const foundAssociatedRulesIds = foundAssociatedRules.map((rule) => rule.id);
// Revert the recognized transactions of the given bank rules.
await this.revertRecognizedTransactins.revertRecognizedTransactions(
foundAssociatedRulesIds,
null,
trx,
);
// Delete the associated uncategorized transactions.
await this.uncategorizedCashflowTransactionModel
.query(trx)
.where('accountId', oldAccount.id)
.delete();
// Delete the given bank rules.
await this.deleteBankRules.deleteBankRules(
foundAssociatedRulesIds,
trx,
);
}
}

View File

@@ -1,57 +0,0 @@
import { OnEvent } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import { IAccountEventDeletedPayload } from '@/interfaces/Account';
import { events } from '@/common/events/events';
import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem';
import { Account } from '@/modules/Accounts/models/Account.model';
import { PlaidApi } from 'plaid';
import { PLAID_CLIENT } from '@/modules/Plaid/Plaid.module';
@Injectable()
export class DisconnectPlaidItemOnAccountDeleted {
constructor(
@Inject(PLAID_CLIENT) private plaidClient: PlaidApi,
@Inject(PlaidItem.name) private plaidItemModel: typeof PlaidItem,
@Inject(Account.name) private accountModel: typeof Account,
) {}
/**
* Deletes Plaid item from the system and Plaid once the account deleted.
* @param {IAccountEventDeletedPayload} payload
* @returns {Promise<void>}
*/
@OnEvent(events.accounts.onDeleted)
public async handleDisconnectPlaidItemOnAccountDelete({
tenantId,
oldAccount,
trx,
}: IAccountEventDeletedPayload) {
// Can't continue if the deleted account is not linked to Plaid item.
if (!oldAccount.plaidItemId) return;
// Retrieves the Plaid item that associated to the deleted account.
const oldPlaidItem = await this.plaidItemModel
.query(trx)
.findOne('plaidItemId', oldAccount.plaidItemId);
// Unlink the Plaid item from all account before deleting it.
await this.accountModel
.query(trx)
.where('plaidItemId', oldAccount.plaidItemId)
.patch({
plaidAccountId: null,
plaidItemId: null,
});
// Remove the Plaid item from the system.
await this.plaidItemModel
.query(trx)
.findOne('plaidItemId', oldAccount.plaidItemId)
.delete();
// Remove Plaid item once the transaction resolve.
if (oldPlaidItem) {
// Remove the Plaid item.
await this.plaidClient.itemRemove({
access_token: oldPlaidItem.plaidAccessToken,
});
}
}
}

View File

@@ -1,17 +0,0 @@
import { Knex } from 'knex';
export interface IBankAccountDisconnectingEventPayload {
bankAccountId: number;
trx: Knex.Transaction;
}
export interface IBankAccountDisconnectedEventPayload {
bankAccountId: number;
trx: Knex.Transaction;
}
export const ERRORS = {
BANK_ACCOUNT_NOT_CONNECTED: 'BANK_ACCOUNT_NOT_CONNECTED',
BANK_ACCOUNT_FEEDS_ALREADY_PAUSED: 'BANK_ACCOUNT_FEEDS_ALREADY_PAUSED',
BANK_ACCOUNT_FEEDS_ALREADY_RESUMED: 'BANK_ACCOUNT_FEEDS_ALREADY_RESUMED',
};

View File

@@ -1,18 +0,0 @@
import { Module } from '@nestjs/common';
import { CreateUncategorizedTransactionService } from './commands/CreateUncategorizedTransaction.service';
import { CategorizeTransactionAsExpense } from './commands/CategorizeTransactionAsExpense';
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
import { ExpensesModule } from '../Expenses/Expenses.module';
@Module({
imports: [BankingTransactionsModule, ExpensesModule],
providers: [
CreateUncategorizedTransactionService,
CategorizeTransactionAsExpense,
],
exports: [
CreateUncategorizedTransactionService,
CategorizeTransactionAsExpense,
],
})
export class BankingCategorizeModule {}

View File

@@ -1,110 +0,0 @@
import { castArray } from 'lodash';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
ICashflowTransactionCategorizedPayload,
ICashflowTransactionUncategorizingPayload,
ICategorizeCashflowTransactioDTO,
} from '../types/BankingCategorize.types';
import {
transformCategorizeTransToCashflow,
validateUncategorizedTransactionsNotExcluded,
} from '../../BankingTransactions/utils';
import { CommandBankTransactionValidator } from '../../BankingTransactions/commands/CommandCasflowValidator.service';
import { CreateBankTransactionService } from '../../BankingTransactions/commands/CreateBankTransaction.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { events } from '@/common/events/events';
@Injectable()
export class CategorizeCashflowTransaction {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
private readonly commandValidators: CommandBankTransactionValidator,
private readonly createBankTransaction: CreateBankTransactionService,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Categorize the given cashflow transaction.
* @param {ICategorizeCashflowTransactioDTO} categorizeDTO - Categorize DTO.
*/
public async categorize(
uncategorizedTransactionId: number | Array<number>,
categorizeDTO: ICategorizeCashflowTransactioDTO,
) {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
// Retrieves the uncategorized transaction or throw an error.
const oldUncategorizedTransactions =
await this.uncategorizedBankTransactionModel.query()
.whereIn('id', uncategorizedTransactionIds)
.throwIfNotFound();
// Validate cannot categorize excluded transaction.
validateUncategorizedTransactionsNotExcluded(oldUncategorizedTransactions);
// Validates the transaction shouldn't be categorized before.
this.commandValidators.validateTransactionsShouldNotCategorized(
oldUncategorizedTransactions,
);
// Validate the uncateogirzed transaction if it's deposit the transaction direction
// should `IN` and the same thing if it's withdrawal the direction should be OUT.
this.commandValidators.validateUncategorizeTransactionType(
oldUncategorizedTransactions,
categorizeDTO.transactionType,
);
// Edits the cashflow transaction under UOW env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onTransactionCategorizing` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorizing,
{
// tenantId,
oldUncategorizedTransactions,
trx,
} as ICashflowTransactionUncategorizingPayload,
);
// Transformes the categorize DTO to the cashflow transaction.
const cashflowTransactionDTO = transformCategorizeTransToCashflow(
oldUncategorizedTransactions,
categorizeDTO,
);
// Creates a new cashflow transaction.
const cashflowTransaction =
await this.createBankTransaction.newCashflowTransaction(
cashflowTransactionDTO,
);
// Updates the uncategorized transaction as categorized.
await this.uncategorizedBankTransactionModel.query(trx)
.whereIn('id', uncategorizedTransactionIds)
.patch({
categorized: true,
categorizeRefType: 'CashflowTransaction',
categorizeRefId: cashflowTransaction.id,
});
// Fetch the new updated uncategorized transactions.
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel.query(trx).whereIn(
'id',
uncategorizedTransactionIds,
);
// Triggers `onCashflowTransactionCategorized` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorized,
{
cashflowTransaction,
uncategorizedTransactions,
oldUncategorizedTransactions,
categorizeDTO,
trx,
} as ICashflowTransactionCategorizedPayload,
);
});
}
}

View File

@@ -1,73 +0,0 @@
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { BankTransaction } from '@/modules/BankingTransactions/models/BankTransaction';
import { CreateExpense } from '@/modules/Expenses/commands/CreateExpense.service';
import { Inject } from '@nestjs/common';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import {
ICashflowTransactionCategorizedPayload,
ICategorizeCashflowTransactioDTO,
} from '../types/BankingCategorize.types';
@Injectable()
export class CategorizeTransactionAsExpense {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
private readonly createExpenseService: CreateExpense,
@Inject(BankTransaction.name)
private readonly bankTransactionModel: typeof BankTransaction,
) {}
/**
* Categorize the transaction as expense transaction.
* @param {number} cashflowTransactionId
* @param {CategorizeTransactionAsExpenseDTO} transactionDTO
*/
public async categorize(
cashflowTransactionId: number,
transactionDTO: ICategorizeCashflowTransactioDTO,
) {
const transaction = await this.bankTransactionModel
.query()
.findById(cashflowTransactionId)
.throwIfNotFound();
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onTransactionUncategorizing` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorizingAsExpense,
{
trx,
} as ICashflowTransactionCategorizedPayload,
);
// Creates a new expense transaction.
// TODO: the DTO is not complete, we need to add the missing properties.
// @ts-ignore
const expenseTransaction = await this.createExpenseService.newExpense({
// ...transactionDTO,
// publishedAt: transaction.publishedAt,
});
// Updates the item on the storage and fetches the updated once.
const cashflowTransaction = await this.bankTransactionModel
.query(trx)
.patchAndFetchById(cashflowTransactionId, {
categorizeRefType: 'Expense',
categorizeRefId: expenseTransaction.id,
uncategorized: true,
});
// Triggers `onTransactionUncategorized` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorizedAsExpense,
{
cashflowTransaction,
trx,
},
);
});
}
}

View File

@@ -1,61 +0,0 @@
import { Knex } from 'knex';
import {
CreateUncategorizedTransactionDTO,
IUncategorizedTransactionCreatedEventPayload,
IUncategorizedTransactionCreatingEventPayload,
} from '../types/BankingCategorize.types';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UncategorizedBankTransaction } from '../../BankingTransactions/models/UncategorizedBankTransaction';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
@Injectable()
export class CreateUncategorizedTransactionService {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransaction: typeof UncategorizedBankTransaction,
) {}
/**
* Creates an uncategorized cashflow transaction.
* @param {CreateUncategorizedTransactionDTO} createDTO - Create uncategorized transaction DTO.
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<UncategorizedBankTransaction>}
*/
public create(
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO,
trx?: Knex.Transaction
) {
return this.uow.withTransaction(
async (trx: Knex.Transaction) => {
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionUncategorizedCreating,
{
createUncategorizedTransactionDTO,
trx,
} as IUncategorizedTransactionCreatingEventPayload
);
const uncategorizedTransaction =
await this.uncategorizedBankTransaction.query(trx).insertAndFetch({
...createUncategorizedTransactionDTO,
});
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionUncategorizedCreated,
{
uncategorizedTransaction,
createUncategorizedTransactionDTO,
trx,
} as IUncategorizedTransactionCreatedEventPayload
);
return uncategorizedTransaction;
},
trx
);
}
}

View File

@@ -1,98 +0,0 @@
import { Knex } from 'knex';
import {
ICashflowTransactionUncategorizedPayload,
ICashflowTransactionUncategorizingPayload,
} from '../types/BankingCategorize.types';
import { validateTransactionShouldBeCategorized } from '../../BankingTransactions/utils';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { UncategorizedBankTransaction } from '../../BankingTransactions/models/UncategorizedBankTransaction';
@Injectable()
export class UncategorizeCashflowTransactionService {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Uncategorizes the given cashflow transaction.
* @param {number} cashflowTransactionId - The id of the cashflow transaction to be uncategorized.
* @returns {Promise<Array<number>>}
*/
public async uncategorize(
uncategorizedTransactionId: number,
): Promise<Array<number>> {
const oldMainUncategorizedTransaction =
await this.uncategorizedBankTransactionModel
.query()
.findById(uncategorizedTransactionId)
.throwIfNotFound();
validateTransactionShouldBeCategorized(oldMainUncategorizedTransaction);
const associatedUncategorizedTransactions =
await this.uncategorizedBankTransactionModel
.query()
.where(
'categorizeRefId',
oldMainUncategorizedTransaction.categorizeRefId,
)
.where(
'categorizeRefType',
oldMainUncategorizedTransaction.categorizeRefType,
)
// Exclude the main transaction.
.whereNot('id', uncategorizedTransactionId);
const oldUncategorizedTransactions = [
oldMainUncategorizedTransaction,
...associatedUncategorizedTransactions,
];
const oldUncategoirzedTransactionsIds = oldUncategorizedTransactions.map(
(t) => t.id,
);
// Updates the transaction under UOW.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onTransactionUncategorizing` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionUncategorizing,
{
uncategorizedTransactionId,
oldUncategorizedTransactions,
trx,
} as ICashflowTransactionUncategorizingPayload,
);
// Removes the ref relation with the related transaction.
await this.uncategorizedBankTransactionModel
.query(trx)
.whereIn('id', oldUncategoirzedTransactionsIds)
.patch({
categorized: false,
categorizeRefId: null,
categorizeRefType: null,
});
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel
.query(trx)
.whereIn('id', oldUncategoirzedTransactionsIds);
// Triggers `onTransactionUncategorized` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionUncategorized,
{
uncategorizedTransactionId,
oldMainUncategorizedTransaction,
uncategorizedTransactions,
oldUncategorizedTransactions,
trx,
} as ICashflowTransactionUncategorizedPayload,
);
return oldUncategoirzedTransactionsIds;
});
}
}

View File

@@ -1,32 +0,0 @@
import { castArray } from 'lodash';
import { PromisePool } from '@supercharge/promise-pool';
import { Injectable } from '@nestjs/common';
import { UncategorizeCashflowTransactionService } from './UncategorizeCashflowTransaction.service';
@Injectable()
export class UncategorizeCashflowTransactionsBulk {
constructor(
private readonly uncategorizeTransactionService: UncategorizeCashflowTransactionService
) {}
/**
* Uncategorize the given bank transactions in bulk.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
*/
public async uncategorizeBulk(
uncategorizedTransactionId: number | Array<number>
) {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
const result = await PromisePool.withConcurrency(MIGRATION_CONCURRENCY)
.for(uncategorizedTransactionIds)
.process(async (_uncategorizedTransactionId: number, index, pool) => {
await this.uncategorizeTransactionService.uncategorize(
_uncategorizedTransactionId
);
});
}
}
const MIGRATION_CONCURRENCY = 1;

View File

@@ -1,99 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import * as yup from 'yup';
import uniqid from 'uniqid';
import { Importable } from '../../Import/Importable';
import { CreateUncategorizedTransactionService } from './CreateUncategorizedTransaction.service';
import { ImportableContext } from '../../Import/interfaces';
import { BankTransactionsSampleData } from '../../BankingTransactions/constants';
import { Account } from '@/modules/Accounts/models/Account.model';
import { CreateUncategorizedTransactionDTO } from '../types/BankingCategorize.types';
@Injectable()
export class UncategorizedTransactionsImportable extends Importable {
constructor(
private readonly createUncategorizedTransaction: CreateUncategorizedTransactionService,
@Inject(Account.name)
private readonly accountModel: typeof Account,
) {
super();
}
/**
* Passing the sheet DTO to create uncategorized transaction.
* @param {CreateUncategorizedTransactionDTO,} createDTO
* @param {Knex.Transaction} trx
*/
public async importable(
createDTO: CreateUncategorizedTransactionDTO,
trx?: Knex.Transaction,
) {
return this.createUncategorizedTransaction.create(createDTO, trx);
}
/**
* Transformes the DTO before validating and importing.
* @param {CreateUncategorizedTransactionDTO} createDTO
* @param {ImportableContext} context
* @returns {CreateUncategorizedTransactionDTO}
*/
public transform(
createDTO: CreateUncategorizedTransactionDTO,
context?: ImportableContext,
): CreateUncategorizedTransactionDTO {
return {
...createDTO,
accountId: context.import.paramsParsed.accountId,
batch: context.import.paramsParsed.batch,
};
}
/**
* Sample data used to download sample sheet.
* @returns {Record<string, any>[]}
*/
public sampleData(): Record<string, any>[] {
return BankTransactionsSampleData;
}
// ------------------
// # Params
// ------------------
/**
* Params validation schema.
* @returns {ValidationSchema[]}
*/
public paramsValidationSchema() {
return yup.object().shape({
accountId: yup.number().required(),
});
}
/**
* Validates the params existance asyncly.
* @param {number} tenantId -
* @param {Record<string, any>} params -
*/
public async validateParams(params: Record<string, any>): Promise<void> {
if (params.accountId) {
await this.accountModel
.query()
.findById(params.accountId)
.throwIfNotFound({});
}
}
/**
* Transforms the import params before storing them.
* @param {Record<string, any>} parmas
*/
public transformParams(parmas: Record<string, any>) {
const batch = uniqid();
return {
...parmas,
batch,
};
}
}

View File

@@ -1,41 +0,0 @@
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { BankingMatchingApplication } from './BankingMatchingApplication';
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
@Controller('banking/matching')
export class BankingMatchingController {
constructor(
private readonly bankingMatchingApplication: BankingMatchingApplication
) {}
@Get('matched/transactions')
async getMatchedTransactions(
@Query('uncategorizedTransactionIds') uncategorizedTransactionIds: number[],
@Query() filter: GetMatchedTransactionsFilter
) {
return this.bankingMatchingApplication.getMatchedTransactions(
uncategorizedTransactionIds,
filter
);
}
@Post('/match/:uncategorizedTransactionId')
async matchTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number | number[],
@Body() matchedTransactions: IMatchTransactionDTO[]
) {
return this.bankingMatchingApplication.matchTransaction(
uncategorizedTransactionId,
matchedTransactions
);
}
@Post('/unmatch/:uncategorizedTransactionId')
async unmatchMatchedTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number
) {
return this.bankingMatchingApplication.unmatchMatchedTransaction(
uncategorizedTransactionId
);
}
}

View File

@@ -1,64 +0,0 @@
import moment from 'moment';
import * as R from 'ramda';
import { isEmpty, sumBy } from 'lodash';
import { ERRORS, MatchedTransactionPOJO } from './types';
import { ServiceError } from '../Items/ServiceError';
export const sortClosestMatchTransactions = (
amount: number,
date: Date,
matches: MatchedTransactionPOJO[]
) => {
return R.sortWith([
// Sort by amount difference (closest to uncategorized transaction amount first)
R.ascend((match: MatchedTransactionPOJO) =>
Math.abs(match.amount - amount)
),
// Sort by date difference (closest to uncategorized transaction date first)
R.ascend((match: MatchedTransactionPOJO) =>
Math.abs(moment(match.date).diff(moment(date), 'days'))
),
])(matches);
};
export const sumMatchTranasctions = (transactions: Array<any>) => {
return transactions.reduce(
(total, item) =>
total +
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
0
);
};
export const sumUncategorizedTransactions = (
uncategorizedTransactions: Array<any>
) => {
return sumBy(uncategorizedTransactions, 'amount');
};
export const validateUncategorizedTransactionsNotMatched = (
uncategorizedTransactions: any
) => {
const matchedTransactions = uncategorizedTransactions.filter(
(trans) => !isEmpty(trans.matchedBankTransactions)
);
//
if (matchedTransactions.length > 0) {
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_MATCHED, '', {
matchedTransactionsIds: matchedTransactions?.map((m) => m.id),
});
}
};
export const validateUncategorizedTransactionsExcluded = (
uncategorizedTransactions: any
) => {
const excludedTransactions = uncategorizedTransactions.filter(
(trans) => trans.excluded
);
if (excludedTransactions.length > 0) {
throw new ServiceError(ERRORS.CANNOT_MATCH_EXCLUDED_TRANSACTION, '', {
excludedTransactionsIds: excludedTransactions.map((e) => e.id),
});
}
};

View File

@@ -1,148 +0,0 @@
import { castArray } from 'lodash';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { PromisePool } from '@supercharge/promise-pool';
import {
ERRORS,
IBankTransactionMatchedEventPayload,
IBankTransactionMatchingEventPayload,
IMatchTransactionDTO,
} from '../types';
import { MatchTransactionsTypes } from './MatchTransactionsTypes';
import {
sumMatchTranasctions,
sumUncategorizedTransactions,
validateUncategorizedTransactionsExcluded,
validateUncategorizedTransactionsNotMatched,
} from '../_utils';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ServiceError } from '@/modules/Items/ServiceError';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { events } from '@/common/events/events';
@Injectable()
export class MatchBankTransactions {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
private readonly matchedBankTransactions: MatchTransactionsTypes,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Validates the match bank transactions DTO.
* @param {number} uncategorizedTransactionId - Uncategorized transaction id.
* @param {IMatchTransactionsDTO} matchTransactionsDTO - Match transactions DTO.
* @returns {Promise<void>}
*/
async validate(
uncategorizedTransactionId: number | Array<number>,
matchedTransactions: Array<IMatchTransactionDTO>,
) {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
// Validates the uncategorized transaction existance.
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel
.query()
.whereIn('id', uncategorizedTransactionIds)
.withGraphFetched('matchedBankTransactions')
.throwIfNotFound();
// Validates the uncategorized transaction is not already matched.
validateUncategorizedTransactionsNotMatched(uncategorizedTransactions);
// Validate the uncategorized transaction is not excluded.
validateUncategorizedTransactionsExcluded(uncategorizedTransactions);
// Validates the given matched transaction.
const validateMatchedTransaction = async (matchedTransaction) => {
const getMatchedTransactionsService =
this.matchedBankTransactions.registry.get(
matchedTransaction.referenceType,
);
if (!getMatchedTransactionsService) {
throw new ServiceError(
ERRORS.RESOURCE_TYPE_MATCHING_TRANSACTION_INVALID,
);
}
const foundMatchedTransaction =
await getMatchedTransactionsService.getMatchedTransaction(
matchedTransaction.referenceId,
);
if (!foundMatchedTransaction) {
throw new ServiceError(ERRORS.RESOURCE_ID_MATCHING_TRANSACTION_INVALID);
}
return foundMatchedTransaction;
};
// Matches the given transactions under promise pool concurrency controlling.
const validatationResult = await PromisePool.withConcurrency(10)
.for(matchedTransactions)
.process(validateMatchedTransaction);
if (validatationResult.errors?.length > 0) {
const error = validatationResult.errors.map((er) => er.raw)[0];
throw new ServiceError(error);
}
// Calculate the total given matching transactions.
const totalMatchedTranasctions = sumMatchTranasctions(
validatationResult.results,
);
const totalUncategorizedTransactions = sumUncategorizedTransactions(
uncategorizedTransactions,
);
// Validates the total given matching transcations whether is not equal
// uncategorized transaction amount.
if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
}
}
/**
* Matches the given uncategorized transaction to the given references.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @returns {Promise<void>}
*/
public async matchTransaction(
uncategorizedTransactionId: number | Array<number>,
matchedTransactions: Array<IMatchTransactionDTO>,
): Promise<void> {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
// Validates the given matching transactions DTO.
await this.validate(uncategorizedTransactionIds, matchedTransactions);
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers the event `onBankTransactionMatching`.
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
uncategorizedTransactionIds,
matchedTransactions,
trx,
} as IBankTransactionMatchingEventPayload);
// Matches the given transactions under promise pool concurrency controlling.
await PromisePool.withConcurrency(10)
.for(matchedTransactions)
.process(async (matchedTransaction) => {
const getMatchedTransactionsService =
this.matchedBankTransactions.registry.get(
matchedTransaction.referenceType,
);
await getMatchedTransactionsService.createMatchedTransaction(
uncategorizedTransactionIds,
matchedTransaction,
trx,
);
});
// Triggers the event `onBankTransactionMatched`.
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
uncategorizedTransactionIds,
matchedTransactions,
trx,
} as IBankTransactionMatchedEventPayload);
});
}
}

View File

@@ -1,34 +0,0 @@
import { Knex } from 'knex';
import { ERRORS } from '../types';
import { Inject, Injectable } from '@nestjs/common';
import { ServiceError } from '@/modules/Items/ServiceError';
import { MatchedBankTransaction } from '../models/MatchedBankTransaction';
@Injectable()
export class ValidateTransactionMatched {
constructor(
@Inject(MatchedBankTransaction.name)
private readonly matchedBankTransactionModel: typeof MatchedBankTransaction,
) {}
/**
* Validate the given transaction whether is matched with bank transactions.
* @param {string} referenceType - Transaction reference type.
* @param {number} referenceId - Transaction reference id.
* @returns {Promise<void>}
*/
public async validateTransactionNoMatchLinking(
referenceType: string,
referenceId: number,
trx?: Knex.Transaction
) {
const foundMatchedTransaction =
await this.matchedBankTransactionModel.query(trx).findOne({
referenceType,
referenceId,
});
if (foundMatchedTransaction) {
throw new ServiceError(ERRORS.CANNOT_DELETE_TRANSACTION_MATCHED);
}
}
}

View File

@@ -1,64 +0,0 @@
import {
IBankTransactionMatchedEventPayload,
IBankTransactionUnmatchedEventPayload,
} from '../types';
import PromisePool from '@supercharge/promise-pool';
import { OnEvent } from '@nestjs/event-emitter';
import { Account } from '@/modules/Accounts/models/Account.model';
import { Inject, Injectable } from '@nestjs/common';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { events } from '@/common/events/events';
@Injectable()
export class DecrementUncategorizedTransactionOnMatchingSubscriber {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Validates the cashflow transaction whether matched with bank transaction on deleting.
* @param {IManualJournalDeletingPayload}
*/
@OnEvent(events.bankMatch.onMatched)
public async decrementUnCategorizedTransactionsOnMatching({
uncategorizedTransactionIds,
trx,
}: IBankTransactionMatchedEventPayload) {
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel.query().whereIn(
'id',
uncategorizedTransactionIds
);
await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions)
.process(async (transaction) => {
await this.accountModel
.query(trx)
.findById(transaction.accountId)
.decrement('uncategorizedTransactions', 1);
});
}
/**
* Validates the cashflow transaction whether matched with bank transaction on deleting.
* @param {IManualJournalDeletingPayload}
*/
@OnEvent(events.bankMatch.onUnmatched)
public async incrementUnCategorizedTransactionsOnUnmatching({
uncategorizedTransactionId,
trx,
}: IBankTransactionUnmatchedEventPayload) {
const transaction =
await this.uncategorizedBankTransactionModel.query().findById(
uncategorizedTransactionId
);
await this.accountModel
.query(trx)
.findById(transaction.accountId)
.increment('uncategorizedTransactions', 1);
}
}

View File

@@ -1,77 +0,0 @@
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionCashflowTransformer } from './GetMatchedTransactionCashflowTransformer';
import { GetMatchedTransactionsFilter } from '../types';
import { BankTransaction } from '@/modules/BankingTransactions/models/BankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class GetMatchedTransactionsByCashflow extends GetMatchedTransactionsByType {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(BankTransaction.name)
private readonly bankTransactionModel: typeof BankTransaction,
) {
super();
}
/**
* Retrieve the matched transactions of cash flow.
* @param {number} tenantId
* @param {GetMatchedTransactionsFilter} filter
* @returns
*/
async getMatchedTransactions(
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>,
) {
const transactions = await this.bankTransactionModel
.query()
.onBuild((q) => {
// Not matched to bank transaction.
q.withGraphJoined('matchedBankTransaction');
q.whereNull('matchedBankTransaction.id');
// Not categorized.
q.modify('notCategorized');
// Published.
q.modify('published');
if (filter.fromDate) {
q.where('date', '>=', filter.fromDate);
}
if (filter.toDate) {
q.where('date', '<=', filter.toDate);
}
q.orderBy('date', 'DESC');
});
return this.transformer.transform(
transactions,
new GetMatchedTransactionCashflowTransformer(),
);
}
/**
* Retrieves the matched transaction of cash flow.
* @param {number} tenantId
* @param {number} transactionId
* @returns
*/
async getMatchedTransaction(transactionId: number) {
const transactions = await this.bankTransactionModel
.query()
.findById(transactionId)
.withGraphJoined('matchedBankTransaction')
.whereNull('matchedBankTransaction.id')
.modify('notCategorized')
.modify('published')
.throwIfNotFound();
return this.transformer.transform(
transactions,
new GetMatchedTransactionCashflowTransformer(),
);
}
}

View File

@@ -1,74 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from '../types';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionExpensesTransformer } from './GetMatchedTransactionExpensesTransformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { Expense } from '@/modules/Expenses/models/Expense.model';
@Injectable()
export class GetMatchedTransactionsByExpenses extends GetMatchedTransactionsByType {
constructor(
protected readonly transformer: TransformerInjectable,
@Inject(Expense.name)
protected readonly expenseModel: typeof Expense,
) {
super();
}
/**
* Retrieves the matched transactions of expenses.
* @param {number} tenantId
* @param {GetMatchedTransactionsFilter} filter
* @returns
*/
async getMatchedTransactions(filter: GetMatchedTransactionsFilter) {
// Retrieve the expense matches.
const expenses = await this.expenseModel.query().onBuild((query) => {
// Filter out the not matched to bank transactions.
query.withGraphJoined('matchedBankTransaction');
query.whereNull('matchedBankTransaction.id');
// Filter the published onyl
query.modify('filterByPublished');
if (filter.fromDate) {
query.where('paymentDate', '>=', filter.fromDate);
}
if (filter.toDate) {
query.where('paymentDate', '<=', filter.toDate);
}
if (filter.minAmount) {
query.where('totalAmount', '>=', filter.minAmount);
}
if (filter.maxAmount) {
query.where('totalAmount', '<=', filter.maxAmount);
}
query.orderBy('paymentDate', 'DESC');
});
return this.transformer.transform(
expenses,
new GetMatchedTransactionExpensesTransformer(),
);
}
/**
* Retrieves the given matched expense transaction.
* @param {number} tenantId
* @param {number} transactionId
* @returns {GetMatchedTransactionExpensesTransformer-}
*/
public async getMatchedTransaction(
transactionId: number,
): Promise<MatchedTransactionPOJO> {
const expense = await this.expenseModel
.query()
.findById(transactionId)
.throwIfNotFound();
return this.transformer.transform(
expense,
new GetMatchedTransactionExpensesTransformer(),
);
}
}

View File

@@ -1,132 +0,0 @@
import { Knex } from 'knex';
import { first } from 'lodash';
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
import {
GetMatchedTransactionsFilter,
IMatchTransactionDTO,
MatchedTransactionPOJO,
MatchedTransactionsPOJO,
} from '../types';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { CreatePaymentReceivedService } from '@/modules/PaymentReceived/commands/CreatePaymentReceived.serivce';
import { Inject, Injectable } from '@nestjs/common';
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { IPaymentReceivedCreateDTO } from '@/modules/PaymentReceived/types/PaymentReceived.types';
@Injectable()
export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByType {
constructor(
private readonly transformer: TransformerInjectable,
private readonly createPaymentReceivedService: CreatePaymentReceivedService,
@Inject(SaleInvoice.name)
private readonly saleInvoiceModel: typeof SaleInvoice,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {
super();
}
/**
* Retrieves the matched transactions.
* @param {GetMatchedTransactionsFilter} filter -
* @returns {Promise<MatchedTransactionsPOJO>}
*/
public async getMatchedTransactions(
filter: GetMatchedTransactionsFilter
): Promise<MatchedTransactionsPOJO> {
// Retrieve the invoices that not matched, unpaid.
const invoices = await this.saleInvoiceModel.query().onBuild((q) => {
q.withGraphJoined('matchedBankTransaction');
q.whereNull('matchedBankTransaction.id');
q.modify('unpaid');
q.modify('published');
if (filter.fromDate) {
q.where('invoiceDate', '>=', filter.fromDate);
}
if (filter.toDate) {
q.where('invoiceDate', '<=', filter.toDate);
}
q.orderBy('invoiceDate', 'DESC');
});
return this.transformer.transform(
invoices,
new GetMatchedTransactionInvoicesTransformer()
);
}
/**
* Retrieves the matched transaction.
* @param {number} tenantId
* @param {number} transactionId
* @returns {Promise<MatchedTransactionPOJO>}
*/
public async getMatchedTransaction(
transactionId: number
): Promise<MatchedTransactionPOJO> {
const invoice = await this.saleInvoiceModel.query().findById(transactionId);
return this.transformer.transform(
invoice,
new GetMatchedTransactionInvoicesTransformer()
);
}
/**
* Creates the common matched transaction.
* @param {number} tenantId
* @param {Array<number>} uncategorizedTransactionIds
* @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx
*/
public async createMatchedTransaction(
uncategorizedTransactionIds: Array<number>,
matchTransactionDTO: IMatchTransactionDTO,
trx?: Knex.Transaction
) {
await super.createMatchedTransaction(
uncategorizedTransactionIds,
matchTransactionDTO,
trx
);
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
const uncategorizedTransaction =
await this.uncategorizedBankTransactionModel.query(trx)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
const invoice = await SaleInvoice.query(trx)
.findById(matchTransactionDTO.referenceId)
.throwIfNotFound();
const createPaymentReceivedDTO: IPaymentReceivedCreateDTO = {
customerId: invoice.customerId,
paymentDate: uncategorizedTransaction.date,
amount: invoice.dueAmount,
depositAccountId: uncategorizedTransaction.accountId,
entries: [
{
index: 1,
invoiceId: invoice.id,
paymentAmount: invoice.dueAmount,
},
],
branchId: invoice.branchId,
};
// Create a payment received associated to the matched invoice.
const paymentReceived = await this.createPaymentReceivedService.createPaymentReceived(
createPaymentReceivedDTO,
trx
);
// Link the create payment received with matched invoice transaction.
await super.createMatchedTransaction(uncategorizedTransactionIds, {
referenceType: 'PaymentReceive',
referenceId: paymentReceived.id,
}, trx)
}
}

View File

@@ -1,75 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetMatchedTransactionManualJournalsTransformer } from './GetMatchedTransactionManualJournalsTransformer';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { GetMatchedTransactionsFilter } from '../types';
import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
@Injectable()
export class GetMatchedTransactionsByManualJournals extends GetMatchedTransactionsByType {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(ManualJournal.name)
private readonly manualJournalModel: typeof ManualJournal,
) {
super();
}
/**
* Retrieve the matched transactions of manual journals.
* @param {number} tenantId
* @param {GetMatchedTransactionsFilter} filter
* @returns
*/
async getMatchedTransactions(
filter: Omit<GetMatchedTransactionsFilter, 'transactionType'>,
) {
// @todo: get the account id from the filter
const accountId = 1000;
const manualJournals = await this.manualJournalModel.query().onBuild((query) => {
query.withGraphJoined('matchedBankTransaction');
query.whereNull('matchedBankTransaction.id');
query.withGraphJoined('entries');
query.where('entries.accountId', accountId);
query.modify('filterByPublished');
if (filter.fromDate) {
query.where('date', '>=', filter.fromDate);
}
if (filter.toDate) {
query.where('date', '<=', filter.toDate);
}
if (filter.minAmount) {
query.where('amount', '>=', filter.minAmount);
}
if (filter.maxAmount) {
query.where('amount', '<=', filter.maxAmount);
}
});
return this.transformer.transform(
manualJournals,
new GetMatchedTransactionManualJournalsTransformer(),
);
}
/**
* Retrieves the matched transaction of manual journals.
* @param {number} tenantId
* @param {number} transactionId
* @returns
*/
public async getMatchedTransaction(transactionId: number) {
const manualJournal = await this.manualJournalModel.query()
.findById(transactionId)
.whereNotExists(ManualJournal.relatedQuery('matchedBankTransaction'))
.throwIfNotFound();
return this.transformer.transform(
manualJournal,
new GetMatchedTransactionManualJournalsTransformer(),
);
}
}

View File

@@ -1,66 +0,0 @@
import { Knex } from 'knex';
import {
GetMatchedTransactionsFilter,
IMatchTransactionDTO,
MatchedTransactionPOJO,
MatchedTransactionsPOJO,
} from '../types';
import PromisePool from '@supercharge/promise-pool';
import { MatchedBankTransaction } from '../models/MatchedBankTransaction';
import { Inject } from '@nestjs/common';
export abstract class GetMatchedTransactionsByType {
@Inject(MatchedBankTransaction.name)
private readonly matchedBankTransactionModel: typeof MatchedBankTransaction;
/**
* Retrieves the matched transactions.
* @param {number} tenantId -
* @param {GetMatchedTransactionsFilter} filter -
* @returns {Promise<MatchedTransactionsPOJO>}
*/
public async getMatchedTransactions(
filter: GetMatchedTransactionsFilter
): Promise<MatchedTransactionsPOJO> {
throw new Error(
'The `getMatchedTransactions` method is not defined for the transaction type.'
);
}
/**
* Retrieves the matched transaction details.
* @param {number} tenantId -
* @param {number} transactionId -
* @returns {Promise<MatchedTransactionPOJO>}
*/
public async getMatchedTransaction(
transactionId: number
): Promise<MatchedTransactionPOJO> {
throw new Error(
'The `getMatchedTransaction` method is not defined for the transaction type.'
);
}
/**
* Creates the common matched transaction.
* @param {number} tenantId
* @param {Array<number>} uncategorizedTransactionIds
* @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx
*/
public async createMatchedTransaction(
uncategorizedTransactionIds: Array<number>,
matchTransactionDTO: IMatchTransactionDTO,
trx?: Knex.Transaction
) {
await PromisePool.withConcurrency(2)
.for(uncategorizedTransactionIds)
.process(async (uncategorizedTransactionId) => {
await this.matchedBankTransactionModel.query(trx).insert({
uncategorizedTransactionId,
referenceType: matchTransactionDTO.referenceType,
referenceId: matchTransactionDTO.referenceId,
});
});
}
}

View File

@@ -1,44 +0,0 @@
import { Module } from '@nestjs/common';
import { PlaidUpdateTransactionsOnItemCreatedSubscriber } from './subscribers/PlaidUpdateTransactionsOnItemCreatedSubscriber';
import { PlaidUpdateTransactions } from './command/PlaidUpdateTransactions';
import { PlaidSyncDb } from './command/PlaidSyncDB';
import { PlaidWebooks } from './command/PlaidWebhooks';
import { PlaidLinkTokenService } from './queries/GetPlaidLinkToken.service';
import { PlaidApplication } from './PlaidApplication';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { PlaidItem } from './models/PlaidItem';
import { PlaidModule } from '../Plaid/Plaid.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { BankingCategorizeModule } from '../BankingCategorize/BankingCategorize.module';
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
import { PlaidItemService } from './command/PlaidItem';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
import { SystemPlaidItem } from './models/SystemPlaidItem';
const models = [
RegisterTenancyModel(PlaidItem),
InjectSystemModel(SystemPlaidItem),
];
@Module({
imports: [
PlaidModule,
AccountsModule,
BankingCategorizeModule,
BankingTransactionsModule,
],
providers: [
...models,
PlaidItemService,
PlaidUpdateTransactions,
PlaidSyncDb,
PlaidWebooks,
PlaidLinkTokenService,
PlaidApplication,
PlaidUpdateTransactionsOnItemCreatedSubscriber,
TenancyContext,
],
exports: [...models],
})
export class BankingPlaidModule {}

View File

@@ -1,50 +0,0 @@
import { PlaidLinkTokenService } from './queries/GetPlaidLinkToken.service';
import { PlaidItemService } from './command/PlaidItem';
import { PlaidWebooks } from './command/PlaidWebhooks';
import { Injectable } from '@nestjs/common';
import { PlaidItemDTO } from './types/BankingPlaid.types';
@Injectable()
export class PlaidApplication {
constructor(
private readonly getLinkTokenService: PlaidLinkTokenService,
private readonly plaidItemService: PlaidItemService,
private readonly plaidWebhooks: PlaidWebooks,
) {}
/**
* Retrieves the Plaid link token.
* @returns {Promise<string>}
*/
public getLinkToken() {
return this.getLinkTokenService.getLinkToken();
}
/**
* Exchanges the Plaid access token.
* @param {PlaidItemDTO} itemDTO
* @returns
*/
public exchangeToken(itemDTO: PlaidItemDTO): Promise<void> {
return this.plaidItemService.item(itemDTO);
}
/**
* Listens to Plaid webhooks
* @param {string} plaidItemId - Plaid item id.
* @param {string} webhookType - Webhook type.
* @param {string} webhookCode - Webhook code.
* @returns {Promise<void>}
*/
public webhooks(
plaidItemId: string,
webhookType: string,
webhookCode: string,
): Promise<void> {
return this.plaidWebhooks.webhooks(
plaidItemId,
webhookType,
webhookCode,
);
}
}

View File

@@ -1,32 +0,0 @@
// import { Request, Response, NextFunction } from 'express';
// import { SystemPlaidItem, Tenant } from '@/system/models';
// import tenantDependencyInjection from '@/api/middleware/TenantDependencyInjection';
// export const PlaidWebhookTenantBootMiddleware = async (
// req: Request,
// res: Response,
// next: NextFunction
// ) => {
// const { item_id: plaidItemId } = req.body;
// const plaidItem = await SystemPlaidItem.query().findOne({ plaidItemId });
// const notFoundOrganization = () => {
// return res.boom.unauthorized('Organization identication not found.', {
// errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }],
// });
// };
// // In case the given organization not found.
// if (!plaidItem) {
// return notFoundOrganization();
// }
// const tenant = await Tenant.query()
// .findById(plaidItem.tenantId)
// .withGraphFetched('metadata');
// // When the given organization id not found on the system storage.
// if (!tenant) {
// return notFoundOrganization();
// }
// tenantDependencyInjection(req, tenant);
// next();
// };

View File

@@ -1,63 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { PlaidItem } from '../models/PlaidItem';
import { PlaidApi } from 'plaid';
import { PLAID_CLIENT } from '../../Plaid/Plaid.module';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { SystemPlaidItem } from '../models/SystemPlaidItem';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { IPlaidItemCreatedEventPayload, PlaidItemDTO } from '../types/BankingPlaid.types';
@Injectable()
export class PlaidItemService {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly tenancyContext: TenancyContext,
@Inject(SystemPlaidItem.name)
private readonly systemPlaidItemModel: typeof SystemPlaidItem,
@Inject(PlaidItem.name)
private readonly plaidItemModel: typeof PlaidItem,
@Inject(PLAID_CLIENT)
private readonly plaidClient: PlaidApi,
) {}
/**
* Exchanges the public token to get access token and item id and then creates
* a new Plaid item.
* @param {PlaidItemDTO} itemDTO - Plaid item data transfer object.
* @returns {Promise<void>}
*/
public async item(itemDTO: PlaidItemDTO): Promise<void> {
const { publicToken, institutionId } = itemDTO;
const tenant = await this.tenancyContext.getTenant();
const tenantId = tenant.id;
// Exchange the public token for a private access token and store with the item.
const response = await this.plaidClient.itemPublicTokenExchange({
public_token: publicToken,
});
const plaidAccessToken = response.data.access_token;
const plaidItemId = response.data.item_id;
// Store the Plaid item metadata on tenant scope.
const plaidItem = await this.plaidItemModel.query().insertAndFetch({
tenantId,
plaidAccessToken,
plaidItemId,
plaidInstitutionId: institutionId,
});
// Stores the Plaid item id on system scope.
await this.systemPlaidItemModel.query().insert({ tenantId, plaidItemId });
// Triggers `onPlaidItemCreated` event.
await this.eventEmitter.emitAsync(events.plaid.onItemCreated, {
plaidAccessToken,
plaidItemId,
plaidInstitutionId: institutionId,
} as IPlaidItemCreatedEventPayload);
}
}

View File

@@ -1,244 +0,0 @@
import * as R from 'ramda';
import bluebird from 'bluebird';
import { entries, groupBy } from 'lodash';
import {
AccountBase as PlaidAccountBase,
Item as PlaidItem,
Institution as PlaidInstitution,
Transaction as PlaidTransaction,
} from 'plaid';
import {
transformPlaidAccountToCreateAccount,
transformPlaidTrxsToCashflowCreate,
} from '../utils';
import { Knex } from 'knex';
import uniqid from 'uniqid';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { RemovePendingUncategorizedTransaction } from '../../BankingTransactions/commands/RemovePendingUncategorizedTransaction.service';
import { CreateAccountService } from '../../Accounts/CreateAccount.service';
import { Account } from '../../Accounts/models/Account.model';
import { events } from '@/common/events/events';
import { PlaidItem as PlaidItemModel } from '../models/PlaidItem';
import { IAccountCreateDTO } from '@/interfaces/Account';
import { IPlaidTransactionsSyncedEventPayload } from '../types/BankingPlaid.types';
import { UncategorizedBankTransaction } from '../../BankingTransactions/models/UncategorizedBankTransaction';
import { Inject, Injectable } from '@nestjs/common';
import { CreateUncategorizedTransactionService } from '@/modules/BankingCategorize/commands/CreateUncategorizedTransaction.service';
const CONCURRENCY_ASYNC = 10;
@Injectable()
export class PlaidSyncDb {
constructor(
private readonly createAccountService: CreateAccountService,
private readonly createUncategorizedTransaction: CreateUncategorizedTransactionService,
private readonly removePendingTransaction: RemovePendingUncategorizedTransaction,
private readonly eventPublisher: EventEmitter2,
@Inject(Account.name)
private readonly accountModel: typeof Account,
@Inject(PlaidItemModel.name)
private readonly plaidItemModel: typeof PlaidItemModel,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Syncs the Plaid bank account.
* @param {number} tenantId
* @param {IAccountCreateDTO} createBankAccountDTO
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public async syncBankAccount(
createBankAccountDTO: IAccountCreateDTO,
trx?: Knex.Transaction,
) {
const plaidAccount = await this.accountModel
.query(trx)
.findOne('plaidAccountId', createBankAccountDTO.plaidAccountId);
// Can't continue if the Plaid account is already created.
if (plaidAccount) {
return;
}
await this.createAccountService.createAccount(createBankAccountDTO, trx, {
ignoreUniqueName: true,
});
}
/**
* Syncs the plaid accounts to the system accounts.
* @param {number} tenantId Tenant ID.
* @param {PlaidAccount[]} plaidAccounts
* @returns {Promise<void>}
*/
public async syncBankAccounts(
plaidAccounts: PlaidAccountBase[],
institution: PlaidInstitution,
item: PlaidItem,
trx?: Knex.Transaction,
): Promise<void> {
const transformToPlaidAccounts = R.curry(
transformPlaidAccountToCreateAccount,
)(item, institution);
const accountCreateDTOs = R.map(transformToPlaidAccounts)(plaidAccounts);
await bluebird.map(
accountCreateDTOs,
(createAccountDTO: any) => this.syncBankAccount(createAccountDTO, trx),
{ concurrency: CONCURRENCY_ASYNC },
);
}
/**
* Synsc the Plaid transactions to the system GL entries.
* @param {number} tenantId - Tenant ID.
* @param {number} plaidAccountId - Plaid account ID.
* @param {PlaidTransaction[]} plaidTranasctions - Plaid transactions
* @return {Promise<void>}
*/
public async syncAccountTranactions(
plaidAccountId: number,
plaidTranasctions: PlaidTransaction[],
trx?: Knex.Transaction,
): Promise<void> {
const batch = uniqid();
const cashflowAccount = await this.accountModel
.query(trx)
.findOne({ plaidAccountId })
.throwIfNotFound();
// Transformes the Plaid transactions to cashflow create DTOs.
const transformTransaction = R.curry(transformPlaidTrxsToCashflowCreate)(
cashflowAccount.id,
);
const uncategorizedTransDTOs =
R.map(transformTransaction)(plaidTranasctions);
// Creating account transaction queue.
await bluebird.map(
uncategorizedTransDTOs,
(uncategoriedDTO) =>
this.createUncategorizedTransaction.create(
{ ...uncategoriedDTO, batch },
trx,
),
{ concurrency: 1 },
);
// Triggers `onPlaidTransactionsSynced` event.
await this.eventPublisher.emitAsync(events.plaid.onTransactionsSynced, {
plaidAccountId,
batch,
} as IPlaidTransactionsSyncedEventPayload);
}
/**
* Syncs the accounts transactions in paraller under controlled concurrency.
* @param {number} tenantId
* @param {PlaidTransaction[]} plaidTransactions
* @return {Promise<void>}
*/
public async syncAccountsTransactions(
plaidAccountsTransactions: PlaidTransaction[],
trx?: Knex.Transaction,
): Promise<void> {
const groupedTrnsxByAccountId = entries(
groupBy(plaidAccountsTransactions, 'account_id'),
);
await bluebird.map(
groupedTrnsxByAccountId,
([plaidAccountId, plaidTransactions]: [number, PlaidTransaction[]]) => {
return this.syncAccountTranactions(
plaidAccountId,
plaidTransactions,
trx,
);
},
{ concurrency: CONCURRENCY_ASYNC },
);
}
/**
* Syncs the removed Plaid transactions ids from the cashflow system transactions.
* @param {string[]} plaidTransactionsIds - Plaid Transactions IDs.
*/
public async syncRemoveTransactions(
plaidTransactionsIds: string[],
trx?: Knex.Transaction,
) {
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel
.query(trx)
.whereIn('plaidTransactionId', plaidTransactionsIds);
const uncategorizedTransactionsIds = uncategorizedTransactions.map(
(trans) => trans.id,
);
await bluebird.map(
uncategorizedTransactionsIds,
(uncategorizedTransactionId: number) =>
this.removePendingTransaction.removePendingTransaction(
uncategorizedTransactionId,
trx,
),
{ concurrency: CONCURRENCY_ASYNC },
);
}
/**
* Syncs the Plaid item last transaction cursor.
* @param {number} tenantId - Tenant ID.
* @param {string} itemId - Plaid item ID.
* @param {string} lastCursor - Last transaction cursor.
* @return {Promise<void>}
*/
public async syncTransactionsCursor(
plaidItemId: string,
lastCursor: string,
trx?: Knex.Transaction,
): Promise<void> {
await this.plaidItemModel
.query(trx)
.findOne({ plaidItemId })
.patch({ lastCursor });
}
/**
* Updates the last feeds updated at of the given Plaid accounts ids.
* @param {number} tenantId
* @param {string[]} plaidAccountIds
* @return {Promise<void>}
*/
public async updateLastFeedsUpdatedAt(
plaidAccountIds: string[],
trx?: Knex.Transaction,
): Promise<void> {
await this.accountModel
.query(trx)
.whereIn('plaid_account_id', plaidAccountIds)
.patch({
lastFeedsUpdatedAt: new Date(),
});
}
/**
* Updates the accounts feed active status of the given Plaid accounts ids.
* @param {number} tenantId
* @param {number[]} plaidAccountIds
* @param {boolean} isFeedsActive
* @returns {Promise<void>}
*/
public async updateAccountsFeedsActive(
plaidAccountIds: string[],
isFeedsActive: boolean = true,
trx?: Knex.Transaction,
): Promise<void> {
await this.accountModel
.query(trx)
.whereIn('plaid_account_id', plaidAccountIds)
.patch({
isFeedsActive,
});
}
}

View File

@@ -1,150 +0,0 @@
import { Knex } from 'knex';
import { PlaidSyncDb } from './PlaidSyncDB';
import { PlaidFetchedTransactionsUpdates } from '../types/BankingPlaid.types';
import { PlaidItem } from '../models/PlaidItem';
import { Inject, Injectable } from '@nestjs/common';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import {
CountryCode,
PlaidApi,
Transaction as PlaidTransaction,
RemovedTransaction,
} from 'plaid';
import { PLAID_CLIENT } from '@/modules/Plaid/Plaid.module';
@Injectable()
export class PlaidUpdateTransactions {
constructor(
private readonly plaidSync: PlaidSyncDb,
private readonly uow: UnitOfWork,
@Inject(PlaidItem.name)
private readonly plaidItemModel: typeof PlaidItem,
@Inject(PLAID_CLIENT)
private readonly plaidClient: PlaidApi,
) {}
/**
* Handles sync the Plaid item to Bigcaptial under UOW.
* @param {number} tenantId - Tenant id.
* @param {number} plaidItemId - Plaid item id.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactions(plaidItemId: string) {
return this.uow.withTransaction((trx: Knex.Transaction) => {
return this.updateTransactionsWork(plaidItemId, trx);
});
}
/**
* Handles the fetching and storing the following:
* - New, modified, or removed transactions.
* - New bank accounts.
* - Last accounts feeds updated at.
* - Turn on the accounts feed flag.
* @param {number} tenantId - Tenant ID.
* @param {string} plaidItemId - The Plaid ID for the item.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactionsWork(
plaidItemId: string,
trx?: Knex.Transaction,
): Promise<{
addedCount: number;
modifiedCount: number;
removedCount: number;
}> {
// Fetch new transactions from plaid api.
const { added, modified, removed, cursor, accessToken } =
await this.fetchTransactionUpdates(plaidItemId);
const request = { access_token: accessToken };
const {
data: { accounts, item },
} = await this.plaidClient.accountsGet(request);
const plaidAccountsIds = accounts.map((a) => a.account_id);
const {
data: { institution },
} = await this.plaidClient.institutionsGetById({
institution_id: item.institution_id,
country_codes: [CountryCode.Us, CountryCode.Gb],
});
// Sync bank accounts.
await this.plaidSync.syncBankAccounts(accounts, institution, item, trx);
// Sync removed transactions.
await this.plaidSync.syncRemoveTransactions(
removed?.map((r) => r.transaction_id),
trx,
);
// Sync bank account transactions.
await this.plaidSync.syncAccountsTransactions(added.concat(modified), trx);
// Sync transactions cursor.
await this.plaidSync.syncTransactionsCursor(plaidItemId, cursor, trx);
// Update the last feeds updated at of the updated accounts.
await this.plaidSync.updateLastFeedsUpdatedAt(plaidAccountsIds, trx);
// Turn on the accounts feeds flag.
await this.plaidSync.updateAccountsFeedsActive(plaidAccountsIds, true, trx);
return {
addedCount: added.length,
modifiedCount: modified.length,
removedCount: removed.length,
};
}
/**
* Fetches transactions from the `Plaid API` for a given item.
* @param {number} tenantId - Tenant ID.
* @param {string} plaidItemId - The Plaid ID for the item.
* @returns {Promise<PlaidFetchedTransactionsUpdates>}
*/
private async fetchTransactionUpdates(
plaidItemId: string,
): Promise<PlaidFetchedTransactionsUpdates> {
// the transactions endpoint is paginated, so we may need to hit it multiple times to
// retrieve all available transactions.
const plaidItem = await this.plaidItemModel
.query()
.findOne('plaidItemId', plaidItemId);
if (!plaidItem) {
throw new Error('The given Plaid item id is not found.');
}
const { plaidAccessToken, lastCursor } = plaidItem;
let cursor = lastCursor;
// New transaction updates since "cursor"
let added: PlaidTransaction[] = [];
let modified: PlaidTransaction[] = [];
// Removed transaction ids
let removed: RemovedTransaction[] = [];
let hasMore = true;
const batchSize = 100;
try {
// Iterate through each page of new transaction updates for item
/* eslint-disable no-await-in-loop */
while (hasMore) {
const request = {
access_token: plaidAccessToken,
cursor: cursor,
count: batchSize,
};
const response = await this.plaidClient.transactionsSync(request);
const data = response.data;
// Add this page of results
added = added.concat(data.added);
modified = modified.concat(data.modified);
removed = removed.concat(data.removed);
hasMore = data.has_more;
// Update cursor to the next cursor
cursor = data.next_cursor;
}
} catch (err) {
console.error(`Error fetching transactions: ${err.message}`);
cursor = lastCursor;
}
return { added, modified, removed, cursor, accessToken: plaidAccessToken };
}
}

View File

@@ -1,152 +0,0 @@
import { PlaidItem } from '../models/PlaidItem';
import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
import { Inject, Injectable } from '@nestjs/common';
@Injectable()
export class PlaidWebooks {
constructor(
private readonly updateTransactionsService: PlaidUpdateTransactions,
@Inject(PlaidItem.name)
private readonly plaidItemModel: typeof PlaidItem,
) {}
/**
* Listens to Plaid webhooks
* @param {string} webhookType - Webhook type.
* @param {string} plaidItemId - Plaid item Id.
* @param {string} webhookCode - webhook code.
*/
public async webhooks(
plaidItemId: string,
webhookType: string,
webhookCode: string,
): Promise<void> {
const _webhookType = webhookType.toLowerCase();
// There are five types of webhooks: AUTH, TRANSACTIONS, ITEM, INCOME, and ASSETS.
// @TODO implement handling for remaining webhook types.
const webhookHandlerMap = {
transactions: this.handleTransactionsWebooks.bind(this),
item: this.itemsHandler.bind(this),
};
const webhookHandler =
webhookHandlerMap[_webhookType] || this.unhandledWebhook;
await webhookHandler(plaidItemId, webhookCode);
}
/**
* Handles all unhandled/not yet implemented webhook events.
* @param {string} webhookType - Webhook type.
* @param {string} webhookCode - Webhook code.
* @param {string} plaidItemId - Plaid item id.
*/
private async unhandledWebhook(
webhookType: string,
webhookCode: string,
plaidItemId: string,
): Promise<void> {
console.log(
`UNHANDLED ${webhookType} WEBHOOK: ${webhookCode}: Plaid item id ${plaidItemId}: unhandled webhook type received.`,
);
}
/**
* Logs to console and emits to socket
* @param {string} additionalInfo - Additional info.
* @param {string} webhookCode - Webhook code.
* @param {string} plaidItemId - Plaid item id.
*/
private serverLogAndEmitSocket(
additionalInfo: string,
webhookCode: string,
plaidItemId: string,
): void {
console.log(
`PLAID WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}`,
);
}
/**
* Handles all transaction webhook events. The transaction webhook notifies
* you that a single item has new transactions available.
* @param {string} plaidItemId - Plaid item id.
* @param {string} webhookCode - Webhook code.
* @returns {Promise<void>}
*/
public async handleTransactionsWebooks(
tenantId: number,
plaidItemId: string,
webhookCode: string,
): Promise<void> {
const plaidItem = await this.plaidItemModel
.query()
.findOne({ plaidItemId })
.throwIfNotFound();
switch (webhookCode) {
case 'SYNC_UPDATES_AVAILABLE': {
if (plaidItem.isPaused) {
this.serverLogAndEmitSocket(
'Plaid item syncing is paused.',
webhookCode,
plaidItemId,
);
return;
}
// Fired when new transactions data becomes available.
const { addedCount, modifiedCount, removedCount } =
await this.updateTransactionsService.updateTransactions(plaidItemId);
this.serverLogAndEmitSocket(
`Transactions: ${addedCount} added, ${modifiedCount} modified, ${removedCount} removed`,
webhookCode,
plaidItemId,
);
break;
}
case 'DEFAULT_UPDATE':
case 'INITIAL_UPDATE':
case 'HISTORICAL_UPDATE':
/* ignore - not needed if using sync endpoint + webhook */
break;
default:
this.serverLogAndEmitSocket(
`unhandled webhook type received.`,
webhookCode,
plaidItemId,
);
}
}
/**
* Handles all Item webhook events.
* @param {number} tenantId - Tenant ID
* @param {string} webhookCode - The webhook code
* @param {string} plaidItemId - The Plaid ID for the item
* @returns {Promise<void>}
*/
public async itemsHandler(
plaidItemId: string,
webhookCode: string,
): Promise<void> {
switch (webhookCode) {
case 'WEBHOOK_UPDATE_ACKNOWLEDGED':
this.serverLogAndEmitSocket('is updated', webhookCode, plaidItemId);
break;
case 'ERROR': {
break;
}
case 'PENDING_EXPIRATION': {
break;
}
default:
this.serverLogAndEmitSocket(
'unhandled webhook type received.',
webhookCode,
plaidItemId,
);
}
}
}

View File

@@ -1,43 +0,0 @@
// import Container, { Service } from 'typedi';
// import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
// import { IPlaidItemCreatedEventPayload } from '@/interfaces';
// @Service()
// export class PlaidFetchTransactionsJob {
// /**
// * Constructor method.
// */
// constructor(agenda) {
// agenda.define(
// 'plaid-update-account-transactions',
// { priority: 'high', concurrency: 2 },
// this.handler
// );
// }
// /**
// * Triggers the function.
// */
// private handler = async (job, done: Function) => {
// const { tenantId, plaidItemId } = job.attrs
// .data as IPlaidItemCreatedEventPayload;
// const plaidFetchTransactionsService = Container.get(
// PlaidUpdateTransactions
// );
// const io = Container.get('socket');
// try {
// await plaidFetchTransactionsService.updateTransactions(
// tenantId,
// plaidItemId
// );
// // Notify the frontend to reflect the new transactions changes.
// io.emit('NEW_TRANSACTIONS_DATA', { plaidItemId });
// done();
// } catch (error) {
// console.log(error);
// done(error);
// }
// };
// }

View File

@@ -1,49 +0,0 @@
import { BaseModel } from '@/models/Model';
import { Model } from 'objection';
export class SystemPlaidItem extends BaseModel {
tenantId: number;
plaidItemId: string;
/**
* Table name.
*/
static get tableName() {
return 'plaid_items';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const Tenant = require('system/models/Tenant');
return {
/**
* System user may belongs to tenant model.
*/
tenant: {
relation: Model.BelongsToOneRelation,
modelClass: Tenant.default,
join: {
from: 'users.tenantId',
to: 'tenants.id',
},
},
};
}
}

View File

@@ -1,22 +0,0 @@
import { events } from '@/common/events/events';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { IPlaidItemCreatedEventPayload } from '../types/BankingPlaid.types';
@Injectable()
export class PlaidUpdateTransactionsOnItemCreatedSubscriber {
/**
* Updates the Plaid item transactions
* @param {IPlaidItemCreatedEventPayload} payload - Event payload.
*/
@OnEvent(events.plaid.onItemCreated)
public async handleUpdateTransactionsOnItemCreated({
tenantId,
plaidItemId,
plaidAccessToken,
plaidInstitutionId,
}: IPlaidItemCreatedEventPayload) {
const payload = { tenantId, plaidItemId };
// await this.agenda.now('plaid-update-account-transactions', payload);
};
}

View File

@@ -1,32 +0,0 @@
import { Knex } from "knex";
import { RemovedTransaction, Transaction } from "plaid";
export interface IPlaidTransactionsSyncedEventPayload {
// tenantId: number;
plaidAccountId: number;
batch: string;
trx?: Knex.Transaction
}
export interface PlaidItemDTO {
publicToken: string;
institutionId: string;
}
export interface PlaidFetchedTransactionsUpdates {
added: Transaction[];
modified: Transaction[];
removed: RemovedTransaction[];
accessToken: string;
cursor: string;
}
export interface IPlaidItemCreatedEventPayload {
tenantId: number;
plaidAccessToken: string;
plaidItemId: string;
plaidInstitutionId: string;
}

View File

@@ -1,29 +0,0 @@
import { forwardRef, Module } from '@nestjs/common';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { RecognizedBankTransaction } from './models/RecognizedBankTransaction';
import { GetAutofillCategorizeTransactionService } from './queries/GetAutofillCategorizeTransaction.service';
import { RevertRecognizedTransactionsService } from './commands/RevertRecognizedTransactions.service';
import { RecognizeTranasctionsService } from './commands/RecognizeTranasctions.service';
import { TriggerRecognizedTransactionsSubscriber } from './events/TriggerRecognizedTransactions';
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
import { BankRulesModule } from '../BankRules/BankRules.module';
const models = [RegisterTenancyModel(RecognizedBankTransaction)];
@Module({
imports: [BankingTransactionsModule, forwardRef(() => BankRulesModule)],
providers: [
...models,
GetAutofillCategorizeTransactionService,
RevertRecognizedTransactionsService,
RecognizeTranasctionsService,
TriggerRecognizedTransactionsSubscriber,
],
exports: [
...models,
GetAutofillCategorizeTransactionService,
RevertRecognizedTransactionsService,
RecognizeTranasctionsService,
],
})
export class BankingTransactionsRegonizeModule {}

View File

@@ -1,9 +0,0 @@
export interface RevertRecognizedTransactionsCriteria {
batch?: string;
accountId?: number;
}
export interface RecognizeTransactionsCriteria {
batch?: string;
accountId?: number;
}

View File

@@ -1,70 +0,0 @@
import { castArray } from 'lodash';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { RevertRecognizedTransactionsCriteria } from '../_types';
import { RecognizedBankTransaction } from '../models/RecognizedBankTransaction';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
@Injectable()
export class RevertRecognizedTransactionsService {
constructor(
private readonly uow: UnitOfWork,
@Inject(RecognizedBankTransaction.name)
private readonly recognizedBankTransactionModel: typeof RecognizedBankTransaction,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Revert and unlinks the recognized transactions based on the given bank rule
* and transactions criteria..
* @param {number|Array<number>} bankRuleId - Bank rule id.
* @param {RevertRecognizedTransactionsCriteria} transactionsCriteria -
* @param {Knex.Transaction} trx - Knex transaction.
* @returns {Promise<void>}
*/
public async revertRecognizedTransactions(
ruleId?: number | Array<number>,
transactionsCriteria?: RevertRecognizedTransactionsCriteria,
trx?: Knex.Transaction,
): Promise<void> {
const rulesIds = castArray(ruleId);
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Retrieves all the recognized transactions of the banbk rule.
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel.query(trx).onBuild((q) => {
q.withGraphJoined('recognizedTransaction');
q.whereNotNull('recognizedTransaction.id');
if (rulesIds.length > 0) {
q.whereIn('recognizedTransaction.bankRuleId', rulesIds);
}
if (transactionsCriteria?.accountId) {
q.where('accountId', transactionsCriteria.accountId);
}
if (transactionsCriteria?.batch) {
q.where('batch', transactionsCriteria.batch);
}
});
const uncategorizedTransactionIds = uncategorizedTransactions.map(
(r) => r.id,
);
// Unlink the recongized transactions out of uncategorized transactions.
await this.uncategorizedBankTransactionModel
.query(trx)
.whereIn('id', uncategorizedTransactionIds)
.patch({
recognizedTransactionId: null,
});
// Delete the recognized bank transactions that assocaited to bank rule.
await this.recognizedBankTransactionModel
.query(trx)
.whereIn('uncategorizedTransactionId', uncategorizedTransactionIds)
.delete();
}, trx);
}
}

View File

@@ -1,85 +0,0 @@
import { isEqual, omit } from 'lodash';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { IBankRuleEventCreatedPayload, IBankRuleEventDeletedPayload, IBankRuleEventEditedPayload } from '@/modules/BankRules/types';
@Injectable()
export class TriggerRecognizedTransactionsSubscriber {
/**
* Triggers the recognize uncategorized transactions job on rule created.
* @param {IBankRuleEventCreatedPayload} payload -
*/
@OnEvent(events.bankRules.onCreated)
private async recognizedTransactionsOnRuleCreated({
bankRule,
}: IBankRuleEventCreatedPayload) {
const payload = { ruleId: bankRule.id };
// await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}
/**
* Triggers the recognize uncategorized transactions job on rule edited.
* @param {IBankRuleEventEditedPayload} payload -
*/
@OnEvent(events.bankRules.onEdited)
private async recognizedTransactionsOnRuleEdited({
editRuleDTO,
oldBankRule,
bankRule,
}: IBankRuleEventEditedPayload) {
const payload = { ruleId: bankRule.id };
// Cannot continue if the new and old bank rule values are the same,
// after excluding `createdAt` and `updatedAt` dates.
if (
isEqual(
omit(bankRule, ['createdAt', 'updatedAt']),
omit(oldBankRule, ['createdAt', 'updatedAt'])
)
) {
return;
}
// await this.agenda.now(
// 'rerecognize-uncategorized-transactions-job',
// payload
// );
}
/**
* Triggers the recognize uncategorized transactions job on rule deleted.
* @param {IBankRuleEventDeletedPayload} payload -
*/
@OnEvent(events.bankRules.onDeleted)
private async recognizedTransactionsOnRuleDeleted({
ruleId,
}: IBankRuleEventDeletedPayload) {
const payload = { ruleId };
// await this.agenda.now(
// 'revert-recognized-uncategorized-transactions-job',
// payload
// );
}
/**
* Triggers the recognize bank transactions once the imported file commit.
* @param {IImportFileCommitedEventPayload} payload -
*/
@OnEvent(events.import.onImportCommitted)
private async triggerRecognizeTransactionsOnImportCommitted({
importId,
// @ts-ignore
}: IImportFileCommitedEventPayload) {
// const importFile = await Import.query().findOne({ importId });
// const batch = importFile.paramsParsed.batch;
// const payload = { transactionsCriteria: { batch } };
// // Cannot continue if the imported resource is not bank account transactions.
// if (importFile.resource !== 'UncategorizedCashflowTransaction') return;
// await this.agenda.now('recognize-uncategorized-transactions-job', payload);
}
}

View File

@@ -1,36 +0,0 @@
// import Container, { Service } from 'typedi';
// import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
// @Service()
// export class RegonizeTransactionsJob {
// /**
// * Constructor method.
// */
// constructor(agenda) {
// agenda.define(
// 'recognize-uncategorized-transactions-job',
// { priority: 'high', concurrency: 2 },
// this.handler
// );
// }
// /**
// * Triggers sending invoice mail.
// */
// private handler = async (job, done: Function) => {
// const { tenantId, ruleId, transactionsCriteria } = job.attrs.data;
// const regonizeTransactions = Container.get(RecognizeTranasctionsService);
// try {
// await regonizeTransactions.recognizeTransactions(
// tenantId,
// ruleId,
// transactionsCriteria
// );
// done();
// } catch (error) {
// console.log(error);
// done(error);
// }
// };
// }

View File

@@ -1,49 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import { BankingTransactionsApplication } from './BankingTransactionsApplication.service';
import {
IBankAccountsFilter,
ICashflowNewCommandDTO,
} from './types/BankingTransactions.types';
import { PublicRoute } from '../Auth/Jwt.guard';
@Controller('banking/transactions')
@PublicRoute()
export class BankingTransactionsController {
constructor(
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
) {}
@Get('')
async getBankAccounts(@Query() filterDTO: IBankAccountsFilter) {
return this.bankingTransactionsApplication.getBankAccounts(filterDTO);
}
@Post()
async createTransaction(@Body() transactionDTO: ICashflowNewCommandDTO) {
return this.bankingTransactionsApplication.createTransaction(
transactionDTO,
);
}
@Delete(':id')
async deleteTransaction(@Param('id') transactionId: string) {
return this.bankingTransactionsApplication.deleteTransaction(
Number(transactionId),
);
}
@Get(':id')
async getTransaction(@Param('id') transactionId: string) {
return this.bankingTransactionsApplication.getTransaction(
Number(transactionId),
);
}
}

View File

@@ -1,53 +0,0 @@
import { Module } from '@nestjs/common';
import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module';
import { UncategorizedBankTransaction } from './models/UncategorizedBankTransaction';
import { BankTransactionLine } from './models/BankTransactionLine';
import { BankTransaction } from './models/BankTransaction';
import { BankTransactionAutoIncrement } from './commands/BankTransactionAutoIncrement.service';
import BankingTransactionGLEntriesSubscriber from './subscribers/CashflowTransactionSubscriber';
import { DecrementUncategorizedTransactionOnCategorizeSubscriber } from './subscribers/DecrementUncategorizedTransactionOnCategorize';
import { DeleteCashflowTransactionOnUncategorizeSubscriber } from './subscribers/DeleteCashflowTransactionOnUncategorize';
import { PreventDeleteTransactionOnDeleteSubscriber } from './subscribers/PreventDeleteTransactionsOnDelete';
import { ValidateDeleteBankAccountTransactions } from './commands/ValidateDeleteBankAccountTransactions.service';
import { BankTransactionGLEntriesService } from './commands/BankTransactionGLEntries';
import { BankingTransactionsApplication } from './BankingTransactionsApplication.service';
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
import { LedgerModule } from '../Ledger/Ledger.module';
import { DeleteCashflowTransaction } from './commands/DeleteCashflowTransaction.service';
import { CreateBankTransactionService } from './commands/CreateBankTransaction.service';
import { GetBankTransactionService } from './queries/GetBankTransaction.service';
import { CommandBankTransactionValidator } from './commands/CommandCasflowValidator.service';
import { BranchTransactionDTOTransformer } from '../Branches/integrations/BranchTransactionDTOTransform';
import { BranchesModule } from '../Branches/Branches.module';
import { RemovePendingUncategorizedTransaction } from './commands/RemovePendingUncategorizedTransaction.service';
import { BankingTransactionsController } from './BankingTransactions.controller';
const models = [
RegisterTenancyModel(UncategorizedBankTransaction),
RegisterTenancyModel(BankTransaction),
RegisterTenancyModel(BankTransactionLine),
];
@Module({
imports: [AutoIncrementOrdersModule, LedgerModule, BranchesModule],
controllers: [BankingTransactionsController],
providers: [
BankTransactionAutoIncrement,
BankTransactionGLEntriesService,
ValidateDeleteBankAccountTransactions,
BankingTransactionGLEntriesSubscriber,
DecrementUncategorizedTransactionOnCategorizeSubscriber,
DeleteCashflowTransactionOnUncategorizeSubscriber,
PreventDeleteTransactionOnDeleteSubscriber,
BankingTransactionsApplication,
DeleteCashflowTransaction,
CreateBankTransactionService,
GetBankTransactionService,
CommandBankTransactionValidator,
BranchTransactionDTOTransformer,
RemovePendingUncategorizedTransaction,
...models,
],
exports: [...models, RemovePendingUncategorizedTransaction],
})
export class BankingTransactionsModule {}

View File

@@ -1,56 +0,0 @@
import { Knex } from 'knex';
import { DeleteCashflowTransaction } from './commands/DeleteCashflowTransaction.service';
import { CreateBankTransactionService } from './commands/CreateBankTransaction.service';
import { GetBankTransactionService } from './queries/GetBankTransaction.service';
import { IBankAccountsFilter, ICashflowNewCommandDTO } from './types/BankingTransactions.types';
import { Injectable } from '@nestjs/common';
import { GetBankAccountsService } from './queries/GetBankAccounts.service';
@Injectable()
export class BankingTransactionsApplication {
constructor(
private readonly createTransactionService: CreateBankTransactionService,
private readonly deleteTransactionService: DeleteCashflowTransaction,
private readonly getCashflowTransactionService: GetBankTransactionService,
private readonly getBankAccountsService: GetBankAccountsService,
) {}
/**
* Creates a new cashflow transaction.
* @param {ICashflowNewCommandDTO} transactionDTO
* @returns
*/
public createTransaction(transactionDTO: ICashflowNewCommandDTO) {
return this.createTransactionService.newCashflowTransaction(transactionDTO);
}
/**
* Deletes the given cashflow transaction.
* @param {number} cashflowTransactionId - Cashflow transaction id.
* @returns {Promise<{ oldCashflowTransaction: ICashflowTransaction }>}
*/
public deleteTransaction(cashflowTransactionId: number) {
return this.deleteTransactionService.deleteCashflowTransaction(
cashflowTransactionId,
);
}
/**
* Retrieves specific cashflow transaction.
* @param {number} cashflowTransactionId
* @returns
*/
public getTransaction(cashflowTransactionId: number) {
return this.getCashflowTransactionService.getBankTransaction(
cashflowTransactionId,
);
}
/**
* Retrieves the cashflow accounts.
* @param {IBankAccountsFilter} filterDTO
*/
public getBankAccounts(filterDTO: IBankAccountsFilter) {
return this.getBankAccountsService.getBankAccounts(filterDTO);
}
}

View File

@@ -1,48 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetPendingBankAccountTransactionTransformer } from './GetPendingBankAccountTransactionTransformer';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
@Injectable()
export class GetPendingBankAccountTransactions {
constructor(
private readonly transformerService: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction
) {}
/**
* Retrieves the given bank accounts pending transaction.
* @param {GetPendingTransactionsQuery} filter - Pending transactions query.
*/
async getPendingTransactions(filter?: GetPendingTransactionsQuery) {
const _filter = {
page: 1,
pageSize: 20,
...filter,
};
const { results, pagination } =
await this.uncategorizedBankTransactionModel.query()
.onBuild((q) => {
q.modify('pending');
if (_filter?.accountId) {
q.where('accountId', _filter.accountId);
}
})
.pagination(_filter.page - 1, _filter.pageSize);
const data = await this.transformerService.transform(
results,
new GetPendingBankAccountTransactionTransformer()
);
return { data, pagination };
}
}
interface GetPendingTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
}

View File

@@ -1,37 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
@Injectable()
export class GetRecognizedTransactionService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction
) {}
/**
* Retrieves the recognized transaction of the given uncategorized transaction.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
*/
public async getRecognizedTransaction(
uncategorizedTransactionId: number
) {
const uncategorizedTransaction =
await this.uncategorizedBankTransactionModel.query()
.findById(uncategorizedTransactionId)
.withGraphFetched('matchedBankTransactions')
.withGraphFetched('recognizedTransaction.assignAccount')
.withGraphFetched('recognizedTransaction.bankRule')
.withGraphFetched('account')
.throwIfNotFound();
return this.transformer.transform(
uncategorizedTransaction,
new GetRecognizedTransactionTransformer()
);
}
}

View File

@@ -1,67 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { GetRecognizedTransactionTransformer } from './GetRecognizedTransactionTransformer';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { IGetRecognizedTransactionsQuery } from '../types/BankingTransactions.types';
@Injectable()
export class GetRecognizedTransactionsService {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Retrieves the recognized transactions of the given account.
* @param {number} tenantId
* @param {IGetRecognizedTransactionsQuery} filter -
*/
async getRecognizedTranactions(filter?: IGetRecognizedTransactionsQuery) {
const _query = {
page: 1,
pageSize: 20,
...filter,
};
const { results, pagination } =
await this.uncategorizedBankTransactionModel.query()
.onBuild((q) => {
q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphFetched('recognizedTransaction.bankRule');
q.whereNotNull('recognizedTransactionId');
// Exclude the excluded transactions.
q.modify('notExcluded');
// Exclude the pending transactions.
q.modify('notPending');
if (_query.accountId) {
q.where('accountId', _query.accountId);
}
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
if (_query.accountId) {
q.where('accountId', _query.accountId);
}
})
.pagination(_query.page - 1, _query.pageSize);
const data = await this.transformer.transform(
results,
new GetRecognizedTransactionTransformer(),
);
return { data, pagination };
}
}

View File

@@ -1,73 +0,0 @@
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
import { Inject, Injectable } from '@nestjs/common';
import { UncategorizedTransactionTransformer } from '../../BankingCategorize/commands/UncategorizedTransaction.transformer';
import { IGetUncategorizedTransactionsQuery } from '../types/BankingTransactions.types';
@Injectable()
export class GetUncategorizedTransactions {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: typeof UncategorizedBankTransaction,
) {}
/**
* Retrieves the uncategorized cashflow transactions.
* @param {number} tenantId - Tenant id.
* @param {number} accountId - Account Id.
*/
public async getTransactions(
accountId: number,
query: IGetUncategorizedTransactionsQuery
) {
// Parsed query with default values.
const _query = {
page: 1,
pageSize: 20,
...query,
};
const { results, pagination } =
await this.uncategorizedBankTransactionModel.query()
.onBuild((q) => {
q.where('accountId', accountId);
q.where('categorized', false);
q.modify('notExcluded');
q.modify('notPending');
q.withGraphFetched('account');
q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphJoined('matchedBankTransactions');
q.whereNull('matchedBankTransactions.id');
q.orderBy('date', 'DESC');
if (_query.minDate) {
q.modify('fromDate', _query.minDate);
}
if (_query.maxDate) {
q.modify('toDate', _query.maxDate);
}
if (_query.minAmount) {
q.modify('minAmount', _query.minAmount);
}
if (_query.maxAmount) {
q.modify('maxAmount', _query.maxAmount);
}
})
.pagination(_query.page - 1, _query.pageSize);
const data = await this.transformer.transform(
results,
new UncategorizedTransactionTransformer()
);
return {
data,
pagination,
};
}
}

View File

@@ -1,60 +0,0 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { BankTransactionAutoIncrement } from '../commands/BankTransactionAutoIncrement.service';
import { BankTransactionGLEntriesService } from '../commands/BankTransactionGLEntries';
import { events } from '@/common/events/events';
import { ICommandCashflowCreatedPayload, ICommandCashflowDeletedPayload } from '../types/BankingTransactions.types';
@Injectable()
export default class BankingTransactionGLEntriesSubscriber {
/**
* @param {BankTransactionGLEntriesService} bankTransactionGLEntries - Bank transaction GL entries service.
* @param {BankTransactionAutoIncrement} cashflowTransactionAutoIncrement - Cashflow transaction auto increment service.
*/
constructor(
private readonly bankTransactionGLEntries: BankTransactionGLEntriesService,
private readonly cashflowTransactionAutoIncrement: BankTransactionAutoIncrement,
) {}
/**
* Writes the journal entries once the cashflow transaction create.
* @param {ICommandCashflowCreatedPayload} payload -
*/
@OnEvent(events.cashflow.onTransactionCreated)
public async writeJournalEntriesOnceTransactionCreated({
cashflowTransaction,
trx,
}: ICommandCashflowCreatedPayload) {
// Can't write GL entries if the transaction not published yet.
if (!cashflowTransaction.isPublished) return;
await this.bankTransactionGLEntries.writeJournalEntries(
cashflowTransaction.id,
trx,
);
}
/**
* Increment the cashflow transaction number once the transaction created.
* @param {ICommandCashflowCreatedPayload} payload -
*/
@OnEvent(events.cashflow.onTransactionCreated)
public async incrementTransactionNumberOnceTransactionCreated({}: ICommandCashflowCreatedPayload) {
this.cashflowTransactionAutoIncrement.incrementNextTransactionNumber();
}
/**
* Deletes the GL entries once the cashflow transaction deleted.
* @param {ICommandCashflowDeletedPayload} payload -
*/
@OnEvent(events.cashflow.onTransactionDeleted)
public async revertGLEntriesOnceTransactionDeleted({
cashflowTransactionId,
trx,
}: ICommandCashflowDeletedPayload) {
await this.bankTransactionGLEntries.revertJournalEntries(
cashflowTransactionId,
trx,
);
};
}

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