mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-13 03:10:31 +00:00
Compare commits
105 Commits
migrate-se
...
refactor-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1130975efd | ||
|
|
fa180b3ac5 | ||
|
|
90d6bea9b9 | ||
|
|
4366bf478a | ||
|
|
0a57b6e20e | ||
|
|
9a685ffe5d | ||
|
|
51988dba3b | ||
|
|
f87bd341e9 | ||
|
|
5595478e19 | ||
|
|
7247b52fe5 | ||
|
|
deadd5ac80 | ||
|
|
66a2261e50 | ||
|
|
c51347d3ec | ||
|
|
b7a3c42074 | ||
|
|
83c9392b74 | ||
|
|
24bf3dd06d | ||
|
|
2b3f98d8fe | ||
|
|
4e64a9eadb | ||
|
|
0823bfc4e9 | ||
|
|
99fe5a6b0d | ||
|
|
ce058b9416 | ||
|
|
4de1ef71ca | ||
|
|
ecb80b2cf2 | ||
|
|
aef208b9d8 | ||
|
|
c096135d9f | ||
|
|
0c9d961272 | ||
|
|
c10cad4256 | ||
|
|
9ebd967fe7 | ||
|
|
a42143a996 | ||
|
|
7506c2f37f | ||
|
|
3c8b7c92fe | ||
|
|
f78d6efe27 | ||
|
|
401b3dc111 | ||
|
|
c9d752d102 | ||
|
|
4f6ad2b293 | ||
|
|
1d53063e09 | ||
|
|
51de3631fc | ||
|
|
b9755ff01c | ||
|
|
5bfff51093 | ||
|
|
1bcee9293c | ||
|
|
c953c48c39 | ||
|
|
ab49113d5a | ||
|
|
d851e5b646 | ||
|
|
e8f1fedf35 | ||
|
|
04c25bd31a | ||
|
|
6287f8b6e3 | ||
|
|
4febc4e502 | ||
|
|
443fbdd89e | ||
|
|
55fcc908ef | ||
|
|
f068218a16 | ||
|
|
842a862b87 | ||
|
|
1ed77dd5ed | ||
|
|
e47ca98171 | ||
|
|
503d0016ea | ||
|
|
0a2ac4ee56 | ||
|
|
8eb23d3a6f | ||
|
|
18017d25d5 | ||
|
|
f11b09cd87 | ||
|
|
ed81d4c1e0 | ||
|
|
88f66f1c1c | ||
|
|
ab717b96ac | ||
|
|
caff6ce47c | ||
|
|
682be715ae | ||
|
|
85946d3161 | ||
|
|
173610d0fa | ||
|
|
f20f07a42f | ||
|
|
6251831741 | ||
|
|
1cfddf2b4d | ||
|
|
6461a2318f | ||
|
|
92d98ce1d3 | ||
|
|
ef22b9ddaf | ||
|
|
4c42515613 | ||
|
|
2eb56e5850 | ||
|
|
136cc907bb | ||
|
|
fd65ee9428 | ||
|
|
08de50e2b1 | ||
|
|
197d173db9 | ||
|
|
cf496909a5 | ||
|
|
67ae7ad037 | ||
|
|
40b7daa2e3 | ||
|
|
b7d0b6c24a | ||
|
|
95bb4fc8e3 | ||
|
|
5c0bb52b59 | ||
|
|
36851d3209 | ||
|
|
9eee0b384d | ||
|
|
9539003cac | ||
|
|
2017539032 | ||
|
|
c4692d1716 | ||
|
|
7b81d0c8e5 | ||
|
|
9a5110aa38 | ||
|
|
2e1c57438c | ||
|
|
b46f2a91c3 | ||
|
|
8e36aab529 | ||
|
|
9eec60ea22 | ||
|
|
6550e88af3 | ||
|
|
dfc5674088 | ||
|
|
6dd854178d | ||
|
|
520d053b36 | ||
|
|
108d286f62 | ||
|
|
271c46ea3b | ||
|
|
7bcd578c11 | ||
|
|
936800600b | ||
|
|
e7e7a95aa1 | ||
|
|
081fdebee0 | ||
|
|
4ab20ac76a |
@@ -1,3 +1,6 @@
|
||||
# App
|
||||
APP_JWT_SECRET=123123
|
||||
|
||||
# Mail
|
||||
MAIL_HOST=
|
||||
MAIL_USERNAME=
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,4 +6,5 @@ node_modules/
|
||||
# Production env file
|
||||
.env
|
||||
|
||||
test-results/
|
||||
test-results/
|
||||
.qodo
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -41,6 +41,8 @@ services:
|
||||
context: ./docker/redis
|
||||
expose:
|
||||
- "6379"
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis:/data
|
||||
deploy:
|
||||
|
||||
@@ -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
24
launch.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
56
packages/server-nest/.gitignore
vendored
56
packages/server-nest/.gitignore
vendored
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
## @bigcapitalhq/server
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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,
|
||||
}));
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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} }})"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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);
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
// }
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
// };
|
||||
// }
|
||||
@@ -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
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.',
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface IAuthSignedInEventPayload {}
|
||||
|
||||
export interface IAuthSigningInEventPayload {}
|
||||
|
||||
export interface IAuthSignInPOJO {}
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
export class AuthApplication {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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 {}
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
// };
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface RevertRecognizedTransactionsCriteria {
|
||||
batch?: string;
|
||||
accountId?: number;
|
||||
}
|
||||
|
||||
export interface RecognizeTransactionsCriteria {
|
||||
batch?: string;
|
||||
accountId?: number;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user