Compare commits

...

74 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
adb1bea374 feat: use the same Authorization header for jwt and api key 2025-07-02 08:30:53 +02:00
Ahmed Bouhuolia
5d96357042 feat: clean up items controller 2025-07-01 23:48:56 +02:00
Ahmed Bouhuolia
9457b3cda1 feat: api keys 2025-07-01 23:45:38 +02:00
Ahmed Bouhuolia
84cb7693c8 feat: api keys 2025-07-01 23:05:58 +02:00
Ahmed Bouhuolia
9f6e9e85a5 feat(server): endpoints swagger docs 2025-06-30 16:30:55 +02:00
Ahmed Bouhuolia
83e698acf3 fix:create customer/vendor 2025-06-29 16:55:02 +02:00
Ahmed Bouhuolia
fa5c3bd955 feat: deleteIfNoRelations 2025-06-28 22:35:29 +02:00
Ahmed Bouhuolia
0ca98c7ae4 fix: cycle dependecy 2025-06-27 02:18:01 +02:00
Ahmed Bouhuolia
0c0e1dc22e fix: invoice generate sharable link 2025-06-27 01:59:46 +02:00
Ahmed Bouhuolia
e7178a6575 fix: adjust contact balance 2025-06-26 17:04:46 +02:00
Ahmed Bouhuolia
6a39e9d71f feat: endpoints swagger document 2025-06-22 23:46:39 +02:00
Ahmed Bouhuolia
9aa1ed93ca feat: update endpoint swagger docs 2025-06-22 20:58:53 +02:00
Ahmed Bouhuolia
b8c9919799 fox: journal sheet 2025-06-21 21:10:05 +02:00
Ahmed Bouhuolia
e5701140e1 feat: swagger doc 2025-06-21 20:55:32 +02:00
Ahmed Bouhuolia
91976842a7 fix: AR/AP aging report 2025-06-21 20:15:42 +02:00
Ahmed Bouhuolia
4d52059dba feat: swagger document endpoints 2025-06-19 21:04:54 +02:00
Ahmed Bouhuolia
26c1f118c1 feat: more response docs 2025-06-19 00:49:43 +02:00
Ahmed Bouhuolia
437bcb8854 feat: models default views 2025-06-17 20:53:13 +02:00
Ahmed Bouhuolia
f624cf7ae6 feat: document more endpoints 2025-06-16 23:40:12 +02:00
Ahmed Bouhuolia
e057b4e2f0 feat: add swagger docs 2025-06-16 15:53:00 +02:00
Ahmed Bouhuolia
c4668d7d22 feat: add swagger docs for responses 2025-06-16 13:50:30 +02:00
Ahmed Bouhuolia
88ef60ef28 fix: delete inventory adjustment gl entries 2025-06-15 17:51:44 +02:00
Ahmed Bouhuolia
bbf9ef9bc2 fix: formatted transaction type 2025-06-15 15:22:19 +02:00
Ahmed Bouhuolia
bcae2dae03 feat: change the controllers tags 2025-06-13 01:57:53 +02:00
Ahmed Bouhuolia
ff93168d72 refactor(nestjs): landed cost 2025-06-11 14:04:37 +02:00
Ahmed Bouhuolia
1130975efd refactor(nestjs): landed cost 2025-06-10 17:08:32 +02:00
Ahmed Bouhuolia
fa180b3ac5 refactor: gl entries 2025-06-10 12:29:46 +02:00
Ahmed Bouhuolia
90d6bea9b9 fix: mail state 2025-06-09 15:37:20 +02:00
Ahmed Bouhuolia
4366bf478a refactor: mail templates 2025-06-08 16:49:03 +02:00
Ahmed Bouhuolia
0a57b6e20e fix: cashflow statement localization 2025-06-06 20:40:56 +02:00
Ahmed Bouhuolia
9a685ffe5d refactor: financial reports query dtos 2025-06-06 00:11:51 +02:00
Ahmed Bouhuolia
51988dba3b refactor(nestjs): bank transactions matching 2025-06-05 14:41:26 +02:00
Ahmed Bouhuolia
f87bd341e9 refactor(nestjs): banking modules 2025-06-03 21:42:09 +02:00
Ahmed Bouhuolia
5595478e19 refactor(nestjs): banking module 2025-06-02 21:32:53 +02:00
Ahmed Bouhuolia
7247b52fe5 refactor(nestjs): banking module 2025-06-02 15:41:41 +02:00
Ahmed Bouhuolia
deadd5ac80 refactor(nestjs): plaid banking syncing 2025-06-01 18:38:44 +02:00
Ahmed Bouhuolia
66a2261e50 refactor(nestjs): wip 2025-05-28 21:32:48 +02:00
Ahmed Bouhuolia
c51347d3ec refactor(nestjs): wip import module 2025-05-28 17:01:46 +02:00
Ahmed Bouhuolia
b7a3c42074 refactor(nestjs): wip 2025-05-27 15:42:27 +02:00
Ahmed Bouhuolia
83c9392b74 refactor(nestjs): wip dtos validation schema 2025-05-26 17:04:53 +02:00
Ahmed Bouhuolia
24bf3dd06d refactor(nestjs): validation schema dtos 2025-05-25 23:39:54 +02:00
Ahmed Bouhuolia
2b3f98d8fe refactor(nestjs): hook the new endpoints 2025-05-22 19:55:55 +02:00
Ahmed Bouhuolia
4e64a9eadb refactor(nestjs): pdf templates 2025-05-22 13:36:10 +02:00
Ahmed Bouhuolia
0823bfc4e9 refactor(nestjs): contacts module 2025-05-20 23:55:39 +02:00
Ahmed Bouhuolia
99fe5a6b0d refactor(nestjs): Implement users module 2025-05-20 17:55:58 +02:00
Ahmed Bouhuolia
ce058b9416 refactor(nestjs): currencies module 2025-05-17 12:14:02 +02:00
Ahmed Bouhuolia
4de1ef71ca refactor(nestjs): hook up new endpoints 2025-05-16 01:41:11 +02:00
Ahmed Bouhuolia
ecb80b2cf2 refactor(nestjs): hook up the client with new endpoints 2025-05-14 21:45:13 +02:00
Ahmed Bouhuolia
aef208b9d8 refactor(nestjs): resource meta endpoint 2025-05-12 15:44:39 +02:00
Ahmed Bouhuolia
c096135d9f fix: the reports endpoint url 2025-05-11 23:53:59 +02:00
Ahmed Bouhuolia
0c9d961272 fix: financial reports i18n 2025-05-11 17:26:55 +02:00
Ahmed Bouhuolia
c10cad4256 fix: financial reports responses 2025-05-11 15:06:03 +02:00
Ahmed Bouhuolia
9ebd967fe7 fix: return wrong response 2025-05-11 00:40:43 +02:00
Ahmed Bouhuolia
a42143a996 fix: retrieve the build org job state 2025-05-10 22:33:54 +02:00
Ahmed Bouhuolia
7506c2f37f refactor(nestjs): replace the reports endpoints 2025-05-09 18:55:16 +02:00
Ahmed Bouhuolia
3c8b7c92fe feat(nestjs): resend the auth confirmation message 2025-05-08 19:01:43 +02:00
Ahmed Bouhuolia
f78d6efe27 refactor(nestjs): hook up auth endpoints 2025-05-08 18:10:02 +02:00
Ahmed Bouhuolia
401b3dc111 refactor(nestjs): add sale receipts retrieval and metadata configuration 2025-05-04 19:42:35 +02:00
Ahmed Bouhuolia
c9d752d102 refactor(nestjs): list resources 2025-05-04 11:19:34 +02:00
Ahmed Bouhuolia
4f6ad2b293 feat: apply credit note to invoice module 2025-05-04 01:32:08 +02:00
Ahmed Bouhuolia
1d53063e09 refactor(nestjs): add importable service to other modules 2025-04-12 19:26:15 +02:00
Ahmed Bouhuolia
51de3631fc Merge pull request #807 from bigcapitalhq/import-module
refactor(nestjs): Import module
2025-04-12 13:40:11 +02:00
Ahmed Bouhuolia
b9755ff01c refactor(nestjs): import module 2025-04-12 13:39:17 +02:00
Ahmed Bouhuolia
5bfff51093 refactor(nestjs): import module 2025-04-12 08:38:29 +02:00
Ahmed Bouhuolia
1bcee9293c Merge pull request #806 from bigcapitalhq/refactor-export-module
refactor(nestjs): export module
2025-04-10 23:35:27 +02:00
Ahmed Bouhuolia
c953c48c39 refactor(nestjs): export module 2025-04-10 23:34:42 +02:00
Ahmed Bouhuolia
ab49113d5a refactor(nestjs): export and import module 2025-04-09 18:35:17 +02:00
Ahmed Bouhuolia
d851e5b646 refactor(nestjs): import module 2025-04-09 10:39:08 +02:00
Ahmed Bouhuolia
e8f1fedf35 refactor(nestjs): exportable modules 2025-04-08 22:44:24 +02:00
Ahmed Bouhuolia
04c25bd31a refactor(nestjs): export module 2025-04-08 16:19:35 +02:00
Ahmed Bouhuolia
6287f8b6e3 refactor(nestjs): fix the failed e2e test cases 2025-04-07 22:50:11 +02:00
Ahmed Bouhuolia
4febc4e502 refactor(nestjs): transaction locking 2025-04-07 13:35:02 +02:00
Ahmed Bouhuolia
443fbdd89e feat: update the README.md file 2025-04-07 11:51:49 +02:00
Ahmed Bouhuolia
55fcc908ef feat(nestjs): migrate to NestJS 2025-04-07 11:51:24 +02:00
4386 changed files with 54103 additions and 222476 deletions

View File

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

View File

@@ -1,25 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

View File

@@ -1,56 +0,0 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View File

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

View File

@@ -1,151 +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": {
"@aws-sdk/client-s3": "^3.576.0",
"@aws-sdk/s3-request-presigner": "^3.583.0",
"@bigcapital/email-components": "*",
"@bigcapital/pdf-templates": "*",
"@bigcapital/utils": "*",
"@casl/ability": "^5.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@liaoliaots/nestjs-redis": "^10.0.0",
"@types/multer": "^1.4.11",
"@nestjs/bull": "^10.2.1",
"@nestjs/bullmq": "^10.2.2",
"@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": "^11.0.5",
"@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",
"async-mutex": "^0.5.0",
"axios": "^1.6.0",
"bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3",
"bluebird": "^3.7.2",
"bull": "^4.16.3",
"bullmq": "^5.25.6",
"cache-manager": "^6.1.1",
"cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"deepdash": "^5.3.9",
"express-validator": "^7.2.0",
"form-data": "^4.0.0",
"fp-ts": "^2.16.9",
"ioredis": "^5.6.0",
"is-my-json-valid": "^2.20.5",
"js-money": "^0.6.3",
"knex": "^3.1.0",
"lamda": "^0.4.1",
"lodash": "^4.17.21",
"lru-cache": "^6.0.0",
"mathjs": "^9.4.0",
"mime-types": "^2.1.35",
"moment": "^2.30.1",
"moment-range": "^4.0.2",
"moment-timezone": "^0.5.43",
"mysql": "^2.18.1",
"mysql2": "^3.11.3",
"multer": "1.4.5-lts.1",
"multer-s3": "^3.0.1",
"nestjs-cls": "^5.2.0",
"nestjs-i18n": "^10.4.9",
"nestjs-redis": "^1.3.3",
"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",
"remeda": "^2.19.2",
"rxjs": "^7.8.1",
"serialize-interceptor": "^1.1.7",
"strategy": "^1.1.1",
"stripe": "^16.10.0",
"uniqid": "^5.2.0",
"uuid": "^10.0.0",
"xlsx": "^0.18.5",
"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/mathjs": "^6.0.12",
"@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",
"mustache": "^3.0.3",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@@ -1,33 +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';
import stripePayment from './stripe-payment';
import signupConfirmation from './signup-confirmation';
import signupRestrictions from './signup-restrictions';
import jwt from './jwt';
import mail from './mail';
import loops from './loops';
export const config = [
systemDatabase,
tenantDatabase,
signup,
gotenberg,
plaid,
lemonsqueezy,
s3,
openExchange,
posthog,
stripePayment,
signupConfirmation,
signupRestrictions,
jwt,
mail,
loops
];

View File

@@ -1,775 +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: {
onViewed: 'onSaleEstimateViewed',
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: 'onSaleEstimateMailSent',
},
/**
* 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',
},
// Reports
reports: {
onBalanceSheetViewed: 'onBalanceSheetViewed',
onTrialBalanceSheetView: 'onTrialBalanceSheetViewed',
onProfitLossSheetViewed: 'onProfitLossSheetViewed',
onCashflowStatementViewed: 'onCashflowStatementViewed',
onGeneralLedgerViewed: 'onGeneralLedgerViewed',
onJournalViewed: 'onJounralViewed',
onReceivableAgingViewed: 'onReceivableAgingViewed',
onPayableAgingViewed: 'onPayableAgingViewed',
onCustomerBalanceSummaryViewed: 'onInventoryValuationViewed',
onVendorBalanceSummaryViewed: 'onVendorBalanceSummaryViewed',
onInventoryValuationViewed: 'onCustomerBalanceSummaryViewed',
onCustomerTransactionsViewed: 'onCustomerTransactionsViewed',
onVendorTransactionsViewed: 'onVendorTransactionsViewed',
onSalesByItemViewed: 'onSalesByItemViewed',
onPurchasesByItemViewed: 'onPurchasesByItemViewed',
},
};

View File

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

View File

@@ -1,7 +0,0 @@
exports.up = (knex) => {
return knex.schema.table('accounts', (table) => {
table.date('seeded_at').after('currency_code').nullable();
});
};
exports.down = (knex) => {};

View File

@@ -1,93 +0,0 @@
exports.up = (knex) => {
return knex.schema
.createTable('projects', (table) => {
table.increments('id').comment('Auto-generated id');
table.string('name');
table.integer('contact_id').unsigned();
table.date('deadline');
table.decimal('cost_estimate');
table.string('status');
table.timestamps();
})
.createTable('tasks', (table) => {
table.increments('id').comment('Auto-generated id');
table.string('name');
table.string('charge_type');
table.decimal('rate');
table.decimal('estimate_hours').unsigned();
table.decimal('actual_hours').unsigned();
table.decimal('invoiced_hours').unsigned().default(0);
table
.integer('project_id')
.unsigned()
.references('id')
.inTable('projects');
table.timestamps();
})
.createTable('times', (table) => {
table.increments('id').comment('Auto-generated id');
table.integer('duration').unsigned();
table.string('description');
table.date('date');
table.integer('taskId').unsigned().references('id').inTable('tasks');
table
.integer('project_id')
.unsigned()
.references('id')
.inTable('projects');
table.timestamps();
})
.table('accounts_transactions', (table) => {
table
.integer('projectId')
.unsigned()
.references('id')
.inTable('projects');
})
.table('manual_journals_entries', (table) => {
table
.integer('projectId')
.unsigned()
.references('id')
.inTable('projects');
})
.table('bills', (table) => {
table
.integer('projectId')
.unsigned()
.references('id')
.inTable('projects');
table.decimal('invoiced_amount').unsigned().defaultTo(0);
})
.table('items_entries', (table) => {
table
.integer('projectId')
.unsigned()
.references('id')
.inTable('projects');
table.integer('project_ref_id').unsigned();
table.string('project_ref_type');
table.decimal('project_ref_invoiced_amount').unsigned().defaultTo(0);
})
.table('sales_invoices', (table) => {
table
.integer('projectId')
.unsigned()
.references('id')
.inTable('projects');
})
.table('expenses_transactions', (table) => {
table
.integer('projectId')
.unsigned()
.references('id')
.inTable('projects');
table.decimal('invoiced_amount').unsigned().defaultTo(0);
});
};
exports.down = (knex) => {
return knex.schema.dropTable('tasks');
};

View File

@@ -1,7 +0,0 @@
exports.up = (knex) => {
return knex.schema.table('expense_transaction_categories', (table) => {
table.integer('projectId').unsigned().references('id').inTable('projects');
});
};
exports.down = (knex) => {};

View File

@@ -1,21 +0,0 @@
import { TenantSeeder } from '@/libs/migration-seed/TenantSeeder';
import { AccountsData } from '../data/accounts';
export default class SeedAccounts extends TenantSeeder {
/**
* Seeds initial accounts to the organization.
*/
up(knex) {
const data = AccountsData.map((account) => ({
...account,
name: this.i18n.t(account.name),
description: account.description ? this.i18n.t(account.description) : '',
currencyCode: this.tenant.metadata.baseCurrency,
seededAt: new Date(),
}));
return knex('accounts').then(async () => {
// Inserts seed entries.
return knex('accounts').insert(data);
});
}
}

View File

@@ -1,53 +0,0 @@
import { TenantSeeder } from '@/libs/migration-seed/TenantSeeder';
export default class SeedSettings extends TenantSeeder {
/**
*
* @returns
*/
up() {
const settings = [
// Orgnization settings.
{ group: 'organization', key: 'accounting_basis', value: 'accrual' },
// Accounts settings.
{ group: 'accounts', key: 'account_code_unique', value: true },
// Manual journals settings.
{ group: 'manual_journals', key: 'next_number', value: '00001' },
{ group: 'manual_journals', key: 'number_prefix', value: 'J-' },
{ group: 'manual_journals', key: 'auto_increment', value: true },
// Sale invoices settings.
{ group: 'sales_invoices', key: 'next_number', value: '00001' },
{ group: 'sales_invoices', key: 'number_prefix', value: 'INV-' },
{ group: 'sales_invoices', key: 'auto_increment', value: true },
// Sale receipts settings.
{ group: 'sales_receipts', key: 'next_number', value: '00001' },
{ group: 'sales_receipts', key: 'number_prefix', value: 'REC-' },
{ group: 'sales_receipts', key: 'auto_increment', value: true },
// Sale estimates settings.
{ group: 'sales_estimates', key: 'next_number', value: '00001' },
{ group: 'sales_estimates', key: 'number_prefix', value: 'EST-' },
{ group: 'sales_estimates', key: 'auto_increment', value: true },
// Payment receives settings.
{ group: 'payment_receives', key: 'number_prefix', value: 'PAY-' },
{ group: 'payment_receives', key: 'next_number', value: '00001' },
{ group: 'payment_receives', key: 'auto_increment', value: true },
// Cashflow settings.
{ group: 'cashflow', key: 'number_prefix', value: 'CF-' },
{ group: 'cashflow', key: 'next_number', value: '00001' },
{ group: 'cashflow', key: 'auto_increment', value: true },
// warehouse transfers settings.
{ group: 'warehouse_transfers', key: 'next_number', value: '00001' },
{ group: 'warehouse_transfers', key: 'number_prefix', value: 'WT-' },
{ group: 'warehouse_transfers', key: 'auto_increment', value: true },
];
return this.knex('settings').insert(settings);
}
}

View File

@@ -1,34 +0,0 @@
import { TenantSeeder } from '@/libs/migration-seed/TenantSeeder';
export default class SeedSettings extends TenantSeeder {
/**
*
* @param knex
* @returns
*/
async up(knex) {
const costAccount = await knex('accounts')
.where('slug', 'cost-of-goods-sold')
.first();
const sellAccount = await knex('accounts')
.where('slug', 'sales-of-product-income')
.first();
const inventoryAccount = await knex('accounts')
.where('slug', 'inventory-asset')
.first();
const settings = [
// Items settings.
{ group: 'items', key: 'preferred_sell_account', value: sellAccount?.id },
{ group: 'items', key: 'preferred_cost_account', value: costAccount?.id },
{
group: 'items',
key: 'preferred_inventory_account',
value: inventoryAccount?.id,
},
];
return knex('settings').insert(settings);
}
}

View File

@@ -1,28 +0,0 @@
import { TenantSeeder } from '@/libs/migration-seed/TenantSeeder';
export default class SeedRolesAndPermissions extends TenantSeeder {
/**
* Seeds roles and associated permissiojns.
* @param knex
* @returns
*/
// eslint-disable-next-line class-methods-use-this
async up(knex) {
return knex('roles').insert([
{
id: 1,
name: 'role.admin.name',
predefined: true,
slug: 'admin',
description: 'role.admin.desc',
},
{
id: 2,
name: 'role.staff.name',
predefined: true,
slug: 'staff',
description: 'role.staff.desc',
},
]);
}
}

View File

@@ -1,49 +0,0 @@
import { TenantSeeder } from '@/libs/migration-seed/TenantSeeder';
export default class SeedRolesAndPermissions extends TenantSeeder {
/**
* Seeds roles and associated permissiojns.
* @param knex
* @returns
*/
// eslint-disable-next-line class-methods-use-this
async up(knex) {
return knex('role_permissions').insert([
// Assign sale invoice permissions to staff role.
{ roleId: 2, subject: 'SaleInvoice', ability: 'create' },
{ roleId: 2, subject: 'SaleInvoice', ability: 'delete' },
{ roleId: 2, subject: 'SaleInvoice', ability: 'view' },
{ roleId: 2, subject: 'SaleInvoice', ability: 'edit' },
// Assign sale estimate permissions to staff role.
{ roleId: 2, subject: 'SaleEstimate', ability: 'create' },
{ roleId: 2, subject: 'SaleEstimate', ability: 'delete' },
{ roleId: 2, subject: 'SaleEstimate', ability: 'view' },
{ roleId: 2, subject: 'SaleEstimate', ability: 'edit' },
// Assign sale receipt permissions to staff role.
{ roleId: 2, subject: 'SaleReceipt', ability: 'create' },
{ roleId: 2, subject: 'SaleReceipt', ability: 'delete' },
{ roleId: 2, subject: 'SaleReceipt', ability: 'view' },
{ roleId: 2, subject: 'SaleReceipt', ability: 'edit' },
// Assign payment receive permissions to staff role.
{ roleId: 2, subject: 'PaymentReceive', ability: 'create' },
{ roleId: 2, subject: 'PaymentReceive', ability: 'delete' },
{ roleId: 2, subject: 'PaymentReceive', ability: 'view' },
{ roleId: 2, subject: 'PaymentReceive', ability: 'edit' },
// Assign bill permissions to staff role.
{ roleId: 2, subject: 'Bill', ability: 'create' },
{ roleId: 2, subject: 'Bill', ability: 'delete' },
{ roleId: 2, subject: 'Bill', ability: 'view' },
{ roleId: 2, subject: 'Bill', ability: 'edit' },
// Assign payment made permissions to staff role.
{ roleId: 2, subject: 'PaymentMade', ability: 'create' },
{ roleId: 2, subject: 'PaymentMade', ability: 'delete' },
{ roleId: 2, subject: 'PaymentMade', ability: 'view' },
{ roleId: 2, subject: 'PaymentMade', ability: 'edit' },
]);
}
}

View File

@@ -1,22 +0,0 @@
import { TenantSeeder } from '@/libs/migration-seed/TenantSeeder';
export default class SeedCustomerVendorCreditSettings extends TenantSeeder {
/**
*
* @returns
*/
up() {
const settings = [
// Credit note.
{ group: 'credit_note', key: 'number_prefix', value: 'CN-' },
{ group: 'credit_note', key: 'next_number', value: '00001' },
{ group: 'credit_note', key: 'auto_increment', value: true },
// Vendor credit.
{ group: 'vendor_credit', key: 'number_prefix', value: 'VC-' },
{ group: 'vendor_credit', key: 'next_number', value: '00001' },
{ group: 'vendor_credit', key: 'auto_increment', value: true },
];
return this.knex('settings').insert(settings);
}
}

View File

@@ -1,14 +0,0 @@
import { TenantSeeder } from '@/libs/migration-seed/TenantSeeder';
import { InitialTaxRates } from '../data/TaxRates';
export default class SeedTaxRates extends TenantSeeder {
/**
* Seeds initial tax rates to the organization.
*/
up(knex) {
return knex('tax_rates').then(async () => {
// Inserts seed entries.
return knex('tax_rates').insert(InitialTaxRates);
});
}
}

View File

@@ -1,16 +0,0 @@
import { TenantSeeder } from '@/libs/migration-seed/TenantSeeder';
import { InitialTaxRates } from '../data/TaxRates';
export default class UpdateTaxPayableAccount extends TenantSeeder {
/**
* Seeds initial tax rates to the organization.
*/
up(knex) {
return knex('accounts').then(async () => {
// Inserts seed entries.
return knex('accounts').where('slug', 'tax-payable').update({
account_type: 'tax-payable',
});
});
}
}

View File

@@ -1 +0,0 @@
// .gitkeep

View File

@@ -1,30 +0,0 @@
export const InitialTaxRates = [
{
name: 'Tax Exempt',
code: 'TAX-EXEMPT',
description: 'Exempts goods or services from taxes.',
rate: 0,
active: 1,
},
{
name: 'Tax on Purchases',
code: 'TAX-PURCHASES',
description: 'Fee added to the cost when you buy items.',
rate: 0,
active: 1,
},
{
name: 'Tax on Sales',
code: 'TAX-SALES',
description: 'Fee added to the cost when you sell items.',
rate: 0,
active: 1,
},
{
name: 'Sales Tax on Imports',
code: 'TAX-IMPORTS',
description: 'Fee added to the cost when you sale to another country.',
rate: 0,
active: 1,
},
];

View File

@@ -1,21 +0,0 @@
{
"HELLO": "Hello",
"PRODUCT": {
"NEW": "New Product: {name}"
},
"ENGLISH": "English",
"ARRAY": ["ONE", "TWO", "THREE"],
"cat": "Cat",
"cat_name": "Cat: {name}",
"set-up-password": {
"heading": "Hello, {username}",
"title": "Forgot password",
"followLink": "Please follow the link to set up your password"
},
"day_interval": {
"one": "Every day",
"other": "Every {count} days",
"zero": "Never"
},
"nested": "We go shopping: $t(test.day_interval, {{\"count\": {count} }})"
}

View File

@@ -1,170 +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 {
accountDTO: any;
trx: Knex.Transaction;
}
export interface IAccountEventCreatedPayload {
account: IAccount;
accountId: number;
trx: Knex.Transaction;
}
export interface IAccountEventEditedPayload {
account: IAccount;
oldAccount: IAccount;
trx: Knex.Transaction;
}
export interface IAccountEventDeletedPayload {
accountId: number;
oldAccount: IAccount;
trx: Knex.Transaction;
}
export interface IAccountEventDeletePayload {
trx: Knex.Transaction;
oldAccount: IAccount;
tenantId: number;
}
export interface IAccountEventActivatedPayload {
tenantId: number;
accountId: number;
trx: Knex.Transaction;
}
export enum AccountAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
TransactionsLocking = 'TransactionsLocking',
}
export enum TaxRateAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
}
export interface CreateAccountParams {
ignoreUniqueName: boolean;
}

View File

@@ -1,165 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { Knex } from 'knex';
import { Item } from '@/modules/Items/models/Item';
// import { AbilitySubject } from '@/interfaces';
// import { IFilterRole } from '@/interfaces/DynamicFilter';
export interface IItem {
id: number;
name: string;
type: string;
code: string;
sellable: boolean;
purchasable: boolean;
costPrice: number;
sellPrice: number;
currencyCode: string;
costAccountId: number;
sellAccountId: number;
inventoryAccountId: number;
sellDescription: string;
purchaseDescription: string;
sellTaxRateId: number;
purchaseTaxRateId: number;
quantityOnHand: number;
note: string;
active: boolean;
categoryId: number;
userId: number;
createdAt: Date;
updatedAt: Date;
}
export class IItemDTO {
@ApiProperty()
name: string;
@ApiProperty()
type: string;
@ApiProperty()
code: string;
@ApiProperty()
sellable: boolean;
@ApiProperty()
purchasable: boolean;
@ApiProperty()
costPrice: number;
@ApiProperty()
sellPrice: number;
@ApiProperty()
currencyCode: string;
@ApiProperty()
costAccountId: number;
@ApiProperty()
sellAccountId: number;
@ApiProperty()
inventoryAccountId: number;
@ApiProperty()
sellDescription: string;
@ApiProperty()
purchaseDescription: string;
@ApiProperty()
sellTaxRateId: number;
@ApiProperty()
purchaseTaxRateId: number;
@ApiProperty()
quantityOnHand: number;
@ApiProperty()
note: string;
@ApiProperty()
active: boolean;
@ApiProperty()
categoryId: number;
}
export interface IItemCreateDTO extends IItemDTO {}
export interface IItemEditDTO extends IItemDTO {}
// export interface IItemsService {
// getItem(tenantId: number, itemId: number): Promise<IItem>;
// deleteItem(tenantId: number, itemId: number): Promise<void>;
// editItem(tenantId: number, itemId: number, itemDTO: IItemDTO): Promise<IItem>;
// newItem(tenantId: number, itemDTO: IItemDTO): Promise<IItem>;
// itemsList(
// tenantId: number,
// itemsFilter: IItemsFilter,
// ): Promise<{ items: IItem[] }>;
// }
// export interface IItemsFilter extends IDynamicListFilterDTO {
// stringifiedFilterRoles?: string;
// page: number;
// pageSize: number;
// inactiveMode: boolean;
// viewSlug?: string;
// }
// export interface IItemsAutoCompleteFilter {
// limit: number;
// keyword: string;
// filterRoles?: IFilterRole[];
// columnSortBy: string;
// sortOrder: string;
// }
export interface IItemEventCreatedPayload {
// tenantId: number;
item: Item;
itemId: number;
trx: Knex.Transaction;
}
export interface IItemEventEditedPayload {
item: Item;
oldItem: Item;
itemId: number;
trx: Knex.Transaction;
}
export interface IItemEventDeletingPayload {
// tenantId: number;
trx: Knex.Transaction;
oldItem: Item;
}
export interface IItemEventDeletedPayload {
// tenantId: number;
itemId: number;
oldItem: Item;
trx: Knex.Transaction;
}
export enum ItemAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
}
// export type ItemAbility = [ItemAction, AbilitySubject.Item];

View File

@@ -1,195 +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;
filterCustomQuery?: Function;
sortCustomQuery?: Function;
}
export interface IModelMetaFieldText {
fieldType: 'text';
minLength?: number;
maxLength?: number;
}
export interface IModelMetaFieldBoolean {
fieldType: 'boolean';
}
export interface IModelMetaFieldNumber {
fieldType: 'number';
min?: number;
max?: number;
}
export interface IModelMetaFieldDate {
fieldType: 'date';
}
export interface IModelMetaFieldUrl {
fieldType: 'url';
}
export type IModelMetaField = IModelMetaFieldCommon &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
| IModelMetaCollectionField
);
export interface IModelMetaEnumerationOption {
key: string;
label: string;
}
export interface IModelMetaEnumerationField {
fieldType: 'enumeration';
options: IModelMetaEnumerationOption[];
}
export interface IModelMetaRelationFieldCommon {
fieldType: 'relation';
}
export interface IModelMetaRelationEnumerationField {
relationType: 'enumeration';
relationKey: string;
relationEntityLabel: string;
relationEntityKey: string;
}
export interface IModelMetaFieldWithFields {
fields: IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
);
}
interface IModelMetaCollectionObjectField extends IModelMetaFieldWithFields {
collectionOf: 'object';
}
export interface IModelMetaCollectionFieldCommon {
fieldType: 'collection';
collectionMinLength?: number;
collectionMaxLength?: number;
}
export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
IModelMetaCollectionObjectField;
export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
IModelMetaRelationEnumerationField;
interface IModelPrintMeta {
pageTitle: string;
}
export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
exportable?: boolean;
exportFlattenOn?: string;
importable?: boolean;
importAggregator?: string;
importAggregateOn?: string;
importAggregateBy?: string;
print?: IModelPrintMeta;
fields: Record<string, IModelMetaField>;
fields2: Record<string, IModelMetaField2>;
columns: Record<string, IModelMetaColumn>;
}
// ----
export interface IModelMetaFieldCommon2 {
name: string;
required?: boolean;
importHint?: string;
order?: number;
unique?: number;
features?: Array<any>;
}
export interface IModelMetaRelationField2 {
fieldType: 'relation';
relationModel: string;
importableRelationLabel: string | string[];
}
export type IModelMetaField2 = IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField2
| IModelMetaCollectionField
);
export interface ImodelMetaColumnMeta {
name: string;
accessor?: string;
exportable?: boolean;
}
interface IModelMetaColumnText {
type: 'text;';
}
interface IModelMetaColumnCollection {
type: 'collection';
collectionOf: 'object';
columns: { [key: string]: ImodelMetaColumnMeta & IModelMetaColumnText };
}
export type IModelMetaColumn = ImodelMetaColumnMeta &
(IModelMetaColumnText | IModelMetaColumnCollection);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
import { AccountsApplication } from './AccountsApplication.service';
import { Exportable } from '../Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
import { IAccountsFilter, IAccountsStructureType } from './Accounts.types';
export class AccountsExportable extends Exportable {
constructor(private readonly accountsApplication: AccountsApplication) {
super();
}
/**
* Retrieves the accounts data to exportable sheet.
*/
public exportable(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(parsedQuery)
.then((output) => output.accounts);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,131 +0,0 @@
import bluebird from 'bluebird';
import { difference } from 'lodash';
import {
validateLinkModelEntryExists,
validateLinkModelExists,
} from './_utils';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { DocumentLinkModel } from './models/DocumentLink.model';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { DocumentModel } from './models/Document.model';
import { getAttachableModelsMap } from './decorators/InjectAttachable.decorator';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class UnlinkAttachment {
constructor(
private moduleRef: ModuleRef,
@Inject(DocumentModel.name)
private readonly documentModel: TenantModelProxy<typeof DocumentModel>,
@Inject(DocumentLinkModel.name)
private readonly documentLinkModel: TenantModelProxy<
typeof DocumentLinkModel
>,
) {}
/**
* Unlink the attachments from the model entry.
* @param {string} filekey - File key.
* @param {string} modelRef - Model reference.
* @param {number} modelId - Model id.
*/
async unlink(
filekey: string,
modelRef: string,
modelId: number,
trx?: Knex.Transaction,
): Promise<void> {
const attachmentsAttachableModels = getAttachableModelsMap();
const attachableModel = attachmentsAttachableModels.get(modelRef);
validateLinkModelExists(attachableModel);
const LinkModel = this.moduleRef.get(modelRef, { strict: false });
const foundLinkModel = await LinkModel.query(trx).findById(modelId);
validateLinkModelEntryExists(foundLinkModel);
const document = await this.documentModel().query(trx).findOne('key', filekey);
// Delete the document link.
await this.documentLinkModel().query(trx)
.where('modelRef', modelRef)
.where('modelId', modelId)
.where('documentId', document.id)
.delete();
}
/**
* Bulk unlink the attachments from the model entry.
* @param {string} fieldkey
* @param {string} modelRef
* @param {number} modelId
* @returns {Promise<void>}
*/
async bulkUnlink(
filekeys: string[],
modelRef: string,
modelId: number,
trx?: Knex.Transaction,
): Promise<void> {
await bluebird.each(filekeys, (fieldKey: string) => {
try {
this.unlink(fieldKey, modelRef, modelId, trx);
} catch {
// Ignore catching exceptions on bulk action.
}
});
}
/**
* Unlink all the unpresented keys of the given model type and id.
* @param {number} tenantId
* @param {string[]} presentedKeys
* @param {string} modelRef
* @param {number} modelId
* @param {Knex.Transaction} trx
*/
async unlinkUnpresentedKeys(
presentedKeys: string[],
modelRef: string,
modelId: number,
trx?: Knex.Transaction,
): Promise<void> {
const modelLinks = await this.documentLinkModel()
.query(trx)
.where('modelRef', modelRef)
.where('modelId', modelId)
.withGraphFetched('document');
const modelLinkKeys = modelLinks.map((link) => link.document.key);
const unpresentedKeys = difference(modelLinkKeys, presentedKeys);
await this.bulkUnlink(unpresentedKeys, modelRef, modelId, trx);
}
/**
* Unlink all attachments of the given model type and id.
* @param {string} modelRef
* @param {number} modelId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
async unlinkAllModelKeys(
modelRef: string,
modelId: number,
trx?: Knex.Transaction,
): Promise<void> {
// Get all the keys of the modelRef and modelId.
const modelLinks = await this.documentLinkModel()
.query(trx)
.where('modelRef', modelRef)
.where('modelId', modelId)
.withGraphFetched('document');
const modelLinkKeys = modelLinks.map((link) => link.document.key);
await this.bulkUnlink(modelLinkKeys, modelRef, modelId, trx);
}
}

View File

@@ -1,10 +0,0 @@
import path from 'path';
// import config from '@/config';
export const getUploadedObjectUri = (objectKey: string) => {
return '';
// return new URL(
// path.join(config.s3.bucket, objectKey),
// config.s3.endpoint
// ).toString();
};

View File

@@ -1,75 +0,0 @@
import { Module } from '@nestjs/common';
import { AuthController } from './Auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './strategies/Jwt.strategy';
import { AuthenticationApplication } from './AuthApplication.sevice';
import { AuthSendResetPasswordService } from './commands/AuthSendResetPassword.service';
import { AuthResetPasswordService } from './commands/AuthResetPassword.service';
import { AuthSignupConfirmResendService } from './commands/AuthSignupConfirmResend.service';
import { AuthSignupConfirmService } from './commands/AuthSignupConfirm.service';
import { AuthSignupService } from './commands/AuthSignup.service';
import { AuthSigninService } from './commands/AuthSignin.service';
import { PasswordReset } from './models/PasswordReset';
import { TenantDBManagerModule } from '../TenantDBManager/TenantDBManager.module';
import { AuthenticationMailMesssages } from './AuthMailMessages.esrvice';
import { LocalStrategy } from './strategies/Local.strategy';
import { PassportModule } from '@nestjs/passport';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './guards/jwt.guard';
import { AuthMailSubscriber } from './Subscribers/AuthMail.subscriber';
import { BullModule } from '@nestjs/bullmq';
import {
SendResetPasswordMailQueue,
SendSignupVerificationMailQueue,
} from './Auth.constants';
import { SendResetPasswordMailProcessor } from './processors/SendResetPasswordMail.processor';
import { SendSignupVerificationMailProcessor } from './processors/SendSignupVerificationMail.processor';
import { MailModule } from '../Mail/Mail.module';
import { ConfigService } from '@nestjs/config';
import { InjectSystemModel } from '../System/SystemModels/SystemModels.module';
import { GetAuthMetaService } from './queries/GetAuthMeta.service';
const models = [InjectSystemModel(PasswordReset)];
@Module({
controllers: [AuthController],
imports: [
MailModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get('jwt.secret'),
signOptions: { expiresIn: '1d', algorithm: 'HS384' },
verifyOptions: { algorithms: ['HS384'] },
}),
}),
TenantDBManagerModule,
BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
],
exports: [...models],
providers: [
...models,
LocalStrategy,
JwtStrategy,
AuthenticationApplication,
AuthSendResetPasswordService,
AuthResetPasswordService,
AuthSignupConfirmResendService,
AuthSignupConfirmService,
AuthSignupService,
AuthSigninService,
AuthenticationMailMesssages,
SendResetPasswordMailProcessor,
SendSignupVerificationMailProcessor,
GetAuthMetaService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
AuthMailSubscriber,
],
})
export class AuthModule {}

View File

@@ -1,10 +0,0 @@
import * as bcrypt from 'bcrypt';
export const hashPassword = (password: string): Promise<string> =>
new Promise((resolve) => {
bcrypt.genSalt(10, (error, salt) => {
bcrypt.hash(password, salt, (err, hash: string) => {
resolve(hash);
});
});
});

View File

@@ -1,5 +0,0 @@
export class AuthSignupConfirmResendService {
signUpConfirmResend(userId: number) {
return;
}
}

View File

@@ -1,11 +0,0 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class AuthSigninDto {
@IsNotEmpty()
@IsString()
password: string;
@IsNotEmpty()
@IsString()
email: string;
}

View File

@@ -1,20 +0,0 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
export class AuthSignupDto {
@IsNotEmpty()
@IsString()
firstName: string;
@IsNotEmpty()
@IsString()
lastName: string;
@IsNotEmpty()
@IsString()
@IsEmail()
email: string;
@IsNotEmpty()
@IsString()
password: string;
}

View File

@@ -1,55 +0,0 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { BankRulesApplication } from './BankRulesApplication';
import { BankRule } from './models/BankRule';
import { CreateBankRuleDto } from './dtos/BankRule.dto';
import { EditBankRuleDto } from './dtos/BankRule.dto';
@Controller('banking/rules')
@ApiTags('bank-rules')
export class BankRulesController {
constructor(private readonly bankRulesApplication: BankRulesApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new bank rule.' })
async createBankRule(
@Body() createRuleDTO: CreateBankRuleDto,
): Promise<BankRule> {
return this.bankRulesApplication.createBankRule(createRuleDTO);
}
@Put(':id')
@ApiOperation({ summary: 'Edit the given bank rule.' })
async editBankRule(
@Param('id') ruleId: number,
@Body() editRuleDTO: EditBankRuleDto,
): Promise<void> {
return this.bankRulesApplication.editBankRule(ruleId, editRuleDTO);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete the given bank rule.' })
async deleteBankRule(@Param('id') ruleId: number): Promise<void> {
return this.bankRulesApplication.deleteBankRule(ruleId);
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the bank rule details.' })
async getBankRule(@Param('id') ruleId: number): Promise<any> {
return this.bankRulesApplication.getBankRule(ruleId);
}
@Get()
@ApiOperation({ summary: 'Retrieves the bank rules.' })
async getBankRules(): Promise<any> {
return this.bankRulesApplication.getBankRules();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,114 +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';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@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: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
) {}
/**
* Categorize the given cashflow transaction.
* @param {ICategorizeCashflowTransactioDTO} categorizeDTO - Categorize DTO.
*/
public async categorize(
uncategorizedTransactionId: number | Array<number>,
categorizeDTO: ICategorizeCashflowTransactioDTO,
) {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
// Retrieves the uncategorized transaction or throw an error.
const oldUncategorizedTransactions =
await this.uncategorizedBankTransactionModel()
.query()
.whereIn('id', uncategorizedTransactionIds)
.throwIfNotFound();
// Validate cannot categorize excluded transaction.
validateUncategorizedTransactionsNotExcluded(oldUncategorizedTransactions);
// Validates the transaction shouldn't be categorized before.
this.commandValidators.validateTransactionsShouldNotCategorized(
oldUncategorizedTransactions,
);
// Validate the uncateogirzed transaction if it's deposit the transaction direction
// should `IN` and the same thing if it's withdrawal the direction should be OUT.
this.commandValidators.validateUncategorizeTransactionType(
oldUncategorizedTransactions,
categorizeDTO.transactionType,
);
// Edits the cashflow transaction under UOW env.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onTransactionCategorizing` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorizing,
{
// tenantId,
oldUncategorizedTransactions,
trx,
} as ICashflowTransactionUncategorizingPayload,
);
// Transformes the categorize DTO to the cashflow transaction.
const cashflowTransactionDTO = transformCategorizeTransToCashflow(
oldUncategorizedTransactions,
categorizeDTO,
);
// Creates a new cashflow transaction.
const cashflowTransaction =
await this.createBankTransaction.newCashflowTransaction(
cashflowTransactionDTO,
);
// Updates the uncategorized transaction as categorized.
await this.uncategorizedBankTransactionModel()
.query(trx)
.whereIn('id', uncategorizedTransactionIds)
.patch({
categorized: true,
categorizeRefType: 'CashflowTransaction',
categorizeRefId: cashflowTransaction.id,
});
// Fetch the new updated uncategorized transactions.
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel()
.query(trx)
.whereIn('id', uncategorizedTransactionIds);
// Triggers `onCashflowTransactionCategorized` event.
await this.eventPublisher.emitAsync(
events.cashflow.onTransactionCategorized,
{
cashflowTransaction,
uncategorizedTransactions,
oldUncategorizedTransactions,
categorizeDTO,
trx,
} as ICashflowTransactionCategorizedPayload,
);
});
}
}

View File

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

View File

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

View File

@@ -1,47 +0,0 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { BankingMatchingApplication } from './BankingMatchingApplication';
import { GetMatchedTransactionsFilter, IMatchTransactionDTO } from './types';
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
@Controller('banking/matching')
@ApiTags('banking-transactions-matching')
export class BankingMatchingController {
constructor(
private readonly bankingMatchingApplication: BankingMatchingApplication
) {}
@Get('matched/transactions')
@ApiOperation({ summary: 'Retrieves the matched transactions.' })
async getMatchedTransactions(
@Query('uncategorizedTransactionIds') uncategorizedTransactionIds: number[],
@Query() filter: GetMatchedTransactionsFilter
) {
return this.bankingMatchingApplication.getMatchedTransactions(
uncategorizedTransactionIds,
filter
);
}
@Post('/match/:uncategorizedTransactionId')
@ApiOperation({ summary: 'Match the given uncategorized transaction.' })
async matchTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number | number[],
@Body() matchedTransactions: MatchBankTransactionDto
) {
return this.bankingMatchingApplication.matchTransaction(
uncategorizedTransactionId,
matchedTransactions
);
}
@Post('/unmatch/:uncategorizedTransactionId')
@ApiOperation({ summary: 'Unmatch the given uncategorized transaction.' })
async unmatchMatchedTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number
) {
return this.bankingMatchingApplication.unmatchMatchedTransaction(
uncategorizedTransactionId
);
}
}

View File

@@ -1,153 +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';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { MatchBankTransactionDto } from '../dtos/MatchBankTransaction.dto';
@Injectable()
export class MatchBankTransactions {
constructor(
private readonly uow: UnitOfWork,
private readonly eventPublisher: EventEmitter2,
private readonly matchedBankTransactions: MatchTransactionsTypes,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
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>,
matchedTransactionsDto: MatchBankTransactionDto,
): Promise<void> {
const uncategorizedTransactionIds = castArray(uncategorizedTransactionId);
const matchedTransactions = matchedTransactionsDto.entries;
// Validates the given matching transactions DTO.
await this.validate(uncategorizedTransactionIds, matchedTransactions);
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers the event `onBankTransactionMatching`.
await this.eventPublisher.emitAsync(events.bankMatch.onMatching, {
uncategorizedTransactionIds,
matchedTransactions,
trx,
} as IBankTransactionMatchingEventPayload);
// Matches the given transactions under promise pool concurrency controlling.
await PromisePool.withConcurrency(10)
.for(matchedTransactions)
.process(async (matchedTransaction) => {
const getMatchedTransactionsService =
this.matchedBankTransactions.registry.get(
matchedTransaction.referenceType,
);
await getMatchedTransactionsService.createMatchedTransaction(
uncategorizedTransactionIds,
matchedTransaction,
trx,
);
});
// Triggers the event `onBankTransactionMatched`.
await this.eventPublisher.emitAsync(events.bankMatch.onMatched, {
uncategorizedTransactionIds,
matchedTransactions,
trx,
} as IBankTransactionMatchedEventPayload);
});
}
}

View File

@@ -1,67 +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';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class DecrementUncategorizedTransactionOnMatchingSubscriber {
constructor(
@Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
) {}
/**
* Validates the cashflow transaction whether matched with bank transaction on deleting.
* @param {IManualJournalDeletingPayload}
*/
@OnEvent(events.bankMatch.onMatched)
public async decrementUnCategorizedTransactionsOnMatching({
uncategorizedTransactionIds,
trx,
}: IBankTransactionMatchedEventPayload) {
const uncategorizedTransactions =
await this.uncategorizedBankTransactionModel()
.query()
.whereIn('id', uncategorizedTransactionIds);
await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions)
.process(async (transaction) => {
await this.accountModel()
.query(trx)
.findById(transaction.accountId)
.decrement('uncategorizedTransactions', 1);
});
}
/**
* Validates the cashflow transaction whether matched with bank transaction on deleting.
* @param {IManualJournalDeletingPayload}
*/
@OnEvent(events.bankMatch.onUnmatched)
public async incrementUnCategorizedTransactionsOnUnmatching({
uncategorizedTransactionId,
trx,
}: IBankTransactionUnmatchedEventPayload) {
const transaction = await this.uncategorizedBankTransactionModel()
.query()
.findById(uncategorizedTransactionId);
await this.accountModel()
.query(trx)
.findById(transaction.accountId)
.increment('uncategorizedTransactions', 1);
}
}

View File

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

View File

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

View File

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

View File

@@ -1,42 +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)];
@Module({
imports: [
PlaidModule,
AccountsModule,
BankingCategorizeModule,
BankingTransactionsModule,
...models,
],
providers: [
InjectSystemModel(SystemPlaidItem),
PlaidItemService,
PlaidUpdateTransactions,
PlaidSyncDb,
PlaidWebooks,
PlaidLinkTokenService,
PlaidApplication,
PlaidUpdateTransactionsOnItemCreatedSubscriber,
TenancyContext,
],
exports: [...models],
})
export class BankingPlaidModule {}

View File

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

View File

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

View File

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

View File

@@ -1,240 +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';
import { TenantModelProxy } from '../../System/models/TenantBaseModel';
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: TenantModelProxy<typeof Account>,
@Inject(PlaidItemModel.name)
private readonly plaidItemModel: TenantModelProxy<typeof PlaidItemModel>,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
) {}
/**
* Syncs the Plaid bank account.
* @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 {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} 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 {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 {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 {string[]} plaidAccountIds - Plaid accounts ids.
* @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[]} plaidAccountIds - Plaid accounts ids.
* @param {boolean} isFeedsActive - Feeds active status.
* @returns {Promise<void>}
*/
public async updateAccountsFeedsActive(
plaidAccountIds: string[],
isFeedsActive: boolean = true,
trx?: Knex.Transaction,
): Promise<void> {
await this.accountModel()
.query(trx)
.whereIn('plaid_account_id', plaidAccountIds)
.patch({
isFeedsActive,
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +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),
...models,
],
providers: [
GetAutofillCategorizeTransactionService,
RevertRecognizedTransactionsService,
RecognizeTranasctionsService,
TriggerRecognizedTransactionsSubscriber,
],
exports: [
...models,
GetAutofillCategorizeTransactionService,
RevertRecognizedTransactionsService,
RecognizeTranasctionsService,
],
})
export class BankingTransactionsRegonizeModule {}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,63 +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 { 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';
import { GetBankAccountsService } from './queries/GetBankAccounts.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { BankAccount } from './models/BankAccount';
import { LedgerModule } from '../Ledger/Ledger.module';
const models = [
RegisterTenancyModel(UncategorizedBankTransaction),
RegisterTenancyModel(BankTransaction),
RegisterTenancyModel(BankTransactionLine),
RegisterTenancyModel(BankAccount),
];
@Module({
imports: [
AutoIncrementOrdersModule,
LedgerModule,
BranchesModule,
DynamicListModule,
...models,
],
controllers: [BankingTransactionsController],
providers: [
BankTransactionAutoIncrement,
BankTransactionGLEntriesService,
ValidateDeleteBankAccountTransactions,
BankingTransactionGLEntriesSubscriber,
DecrementUncategorizedTransactionOnCategorizeSubscriber,
DeleteCashflowTransactionOnUncategorizeSubscriber,
PreventDeleteTransactionOnDeleteSubscriber,
BankingTransactionsApplication,
DeleteCashflowTransaction,
CreateBankTransactionService,
GetBankTransactionService,
GetBankAccountsService,
CommandBankTransactionValidator,
BranchTransactionDTOTransformer,
RemovePendingUncategorizedTransaction,
],
exports: [...models, RemovePendingUncategorizedTransaction],
})
export class BankingTransactionsModule {}

View File

@@ -1,58 +0,0 @@
import { Injectable } from '@nestjs/common';
import { DeleteCashflowTransaction } from './commands/DeleteCashflowTransaction.service';
import { CreateBankTransactionService } from './commands/CreateBankTransaction.service';
import { GetBankTransactionService } from './queries/GetBankTransaction.service';
import {
IBankAccountsFilter,
} from './types/BankingTransactions.types';
import { GetBankAccountsService } from './queries/GetBankAccounts.service';
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
@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: CreateBankTransactionDto) {
return this.createTransactionService.newCashflowTransaction(transactionDTO);
}
/**
* Deletes the given cashflow transaction.
* @param {number} cashflowTransactionId - Cashflow transaction id.
* @returns {Promise<{ oldCashflowTransaction: ICashflowTransaction }>}
*/
public deleteTransaction(cashflowTransactionId: number) {
return this.deleteTransactionService.deleteCashflowTransaction(
cashflowTransactionId,
);
}
/**
* Retrieves specific cashflow transaction.
* @param {number} cashflowTransactionId
* @returns
*/
public getTransaction(cashflowTransactionId: number) {
return this.getCashflowTransactionService.getBankTransaction(
cashflowTransactionId,
);
}
/**
* Retrieves the cashflow accounts.
* @param {IBankAccountsFilter} filterDTO
*/
public getBankAccounts(filterDTO: IBankAccountsFilter) {
return this.getBankAccountsService.getBankAccounts(filterDTO);
}
}

View File

@@ -1,149 +0,0 @@
import { ACCOUNT_TYPE } from "@/constants/accounts";
export const ERRORS = {
CASHFLOW_TRANSACTION_TYPE_INVALID: 'CASHFLOW_TRANSACTION_TYPE_INVALID',
CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE: 'CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE',
CASHFLOW_TRANSACTION_NOT_FOUND: 'CASHFLOW_TRANSACTION_NOT_FOUND',
CASHFLOW_ACCOUNTS_IDS_NOT_FOUND: 'CASHFLOW_ACCOUNTS_IDS_NOT_FOUND',
CREDIT_ACCOUNTS_IDS_NOT_FOUND: 'CREDIT_ACCOUNTS_IDS_NOT_FOUND',
CREDIT_ACCOUNTS_HAS_INVALID_TYPE: 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE',
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED',
UNCATEGORIZED_TRANSACTION_TYPE_INVALID:
'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED:
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION:
'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION',
TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED',
TRANSACTION_NOT_PENDING: 'TRANSACTION_NOT_PENDING',
};
export enum CASHFLOW_DIRECTION {
IN = 'In',
OUT = 'Out',
}
export enum CASHFLOW_TRANSACTION_TYPE {
ONWERS_DRAWING = 'OwnerDrawing',
OWNER_CONTRIBUTION = 'OwnerContribution',
OTHER_INCOME = 'OtherIncome',
TRANSFER_FROM_ACCOUNT = 'TransferFromAccount',
TRANSFER_TO_ACCOUNT = 'TransferToAccount',
OTHER_EXPENSE = 'OtherExpense',
}
export const CASHFLOW_TRANSACTION_TYPE_META = {
[`${CASHFLOW_TRANSACTION_TYPE.ONWERS_DRAWING}`]: {
type: 'OwnerDrawing',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [ACCOUNT_TYPE.EQUITY],
},
[`${CASHFLOW_TRANSACTION_TYPE.OWNER_CONTRIBUTION}`]: {
type: 'OwnerContribution',
direction: CASHFLOW_DIRECTION.IN,
creditType: [ACCOUNT_TYPE.EQUITY],
},
[`${CASHFLOW_TRANSACTION_TYPE.OTHER_INCOME}`]: {
type: 'OtherIncome',
direction: CASHFLOW_DIRECTION.IN,
creditType: [ACCOUNT_TYPE.INCOME, ACCOUNT_TYPE.OTHER_INCOME],
},
[`${CASHFLOW_TRANSACTION_TYPE.TRANSFER_FROM_ACCOUNT}`]: {
type: 'TransferFromAccount',
direction: CASHFLOW_DIRECTION.IN,
creditType: [
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CREDIT_CARD,
],
},
[`${CASHFLOW_TRANSACTION_TYPE.TRANSFER_TO_ACCOUNT}`]: {
type: 'TransferToAccount',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CREDIT_CARD,
],
},
[`${CASHFLOW_TRANSACTION_TYPE.OTHER_EXPENSE}`]: {
type: 'OtherExpense',
direction: CASHFLOW_DIRECTION.OUT,
creditType: [
ACCOUNT_TYPE.EXPENSE,
ACCOUNT_TYPE.OTHER_EXPENSE,
ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
],
},
};
export interface ICashflowTransactionTypeMeta {
type: string;
direction: CASHFLOW_DIRECTION;
creditType: string[];
}
export const BankTransactionsSampleData = [
{
Amount: '6,410.19',
Date: '2024-03-26',
Payee: 'MacGyver and Sons',
'Reference No.': 'REF-1',
Description: 'Commodi quo labore.',
},
{
Amount: '8,914.17',
Date: '2024-01-05',
Payee: 'Eichmann - Bergnaum',
'Reference No.': 'REF-1',
Description: 'Quia enim et.',
},
{
Amount: '6,200.88',
Date: '2024-02-17',
Payee: 'Luettgen, Mraz and Legros',
'Reference No.': 'REF-1',
Description: 'Occaecati consequuntur cum impedit illo.',
},
];
export const CashflowTransactionTypes = {
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
};
export const TransactionTypes = {
SaleInvoice: 'Sale invoice',
SaleReceipt: 'Sale receipt',
PaymentReceive: 'Payment received',
Bill: 'Bill',
BillPayment: 'Payment made',
VendorOpeningBalance: 'Vendor opening balance',
CustomerOpeningBalance: 'Customer opening balance',
InventoryAdjustment: 'Inventory adjustment',
ManualJournal: 'Manual journal',
Journal: 'Manual journal',
Expense: 'Expense',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
InvoiceWriteOff: 'Invoice write-off',
CreditNote: 'transaction_type.credit_note',
VendorCredit: 'transaction_type.vendor_credit',
RefundCreditNote: 'transaction_type.refund_credit_note',
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
LandedCost: 'transaction_type.landed_cost',
CashflowTransaction: CashflowTransactionTypes,
};

View File

@@ -1,58 +0,0 @@
import {
IsBoolean,
IsDate,
IsNumber,
IsOptional,
IsString,
} from 'class-validator';
export class CreateBankTransactionDto {
@IsDate()
date: Date;
@IsString()
transactionNumber: string;
@IsString()
referenceNo: string;
@IsString()
transactionType: string;
@IsString()
description: string;
@IsNumber()
amount: number;
@IsNumber()
exchangeRate: number;
@IsString()
currencyCode: string;
@IsNumber()
creditAccountId: number;
@IsNumber()
cashflowAccountId: number;
@IsBoolean()
publish: boolean;
@IsOptional()
@IsNumber()
branchId?: number;
@IsOptional()
@IsString()
plaidTransactionId?: string;
@IsOptional()
@IsString()
plaidAccountId?: string;
@IsOptional()
@IsNumber()
uncategorizedTransactionId?: number;
}

View File

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

View File

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

View File

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

View File

@@ -1,60 +0,0 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { BankTransactionAutoIncrement } from '../commands/BankTransactionAutoIncrement.service';
import { BankTransactionGLEntriesService } from '../commands/BankTransactionGLEntries';
import { events } from '@/common/events/events';
import { ICommandCashflowCreatedPayload, ICommandCashflowDeletedPayload } from '../types/BankingTransactions.types';
@Injectable()
export 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,
);
};
}

View File

@@ -1,88 +0,0 @@
import PromisePool from '@supercharge/promise-pool';
import {
ICashflowTransactionCategorizedPayload,
ICashflowTransactionUncategorizedPayload,
} from '../types/BankingTransactions.types';
import { OnEvent } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { Account } from '@/modules/Accounts/models/Account.model';
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
@Injectable()
export class DecrementUncategorizedTransactionOnCategorizeSubscriber {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
) {}
/**
* Decrement the uncategoirzed transactions on the account once categorizing.
* @param {ICashflowTransactionCategorizedPayload}
*/
@OnEvent(events.cashflow.onTransactionCategorized)
public async decrementUnCategorizedTransactionsOnCategorized({
uncategorizedTransactions,
trx,
}: ICashflowTransactionCategorizedPayload) {
await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions)
.process(
async (uncategorizedTransaction: UncategorizedBankTransaction) => {
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) {
return;
}
await this.accountModel
.query(trx)
.findById(uncategorizedTransaction.accountId)
.decrement('uncategorizedTransactions', 1);
},
);
}
/**
* Increment the uncategorized transaction on the given account on uncategorizing.
* @param {IManualJournalDeletingPayload}
*/
@OnEvent(events.cashflow.onTransactionUncategorized)
public async incrementUnCategorizedTransactionsOnUncategorized({
uncategorizedTransactions,
trx,
}: ICashflowTransactionUncategorizedPayload) {
await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions)
.process(
async (uncategorizedTransaction: UncategorizedBankTransaction) => {
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) {
return;
}
await this.accountModel
.query(trx)
.findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1);
},
);
}
/**
* Increments uncategorized transactions count once creating a new transaction.
* @param {ICommandCashflowCreatedPayload} payload -
*/
@OnEvent(events.cashflow.onTransactionUncategorizedCreated)
public async incrementUncategoirzedTransactionsOnCreated({
uncategorizedTransaction,
trx,
}: any) {
if (!uncategorizedTransaction.accountId) return;
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) return;
await this.accountModel
.query(trx)
.findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1);
}
}

View File

@@ -1,60 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import { ExcludeBankTransactionsApplication } from './ExcludeBankTransactionsApplication';
import { ExcludedBankTransactionsQuery } from './types/BankTransactionsExclude.types';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
@Controller('banking/transactions')
@ApiTags('banking-transactions')
export class BankingTransactionsExcludeController {
constructor(
private readonly excludeBankTransactionsApplication: ExcludeBankTransactionsApplication,
) {}
@Get()
@ApiOperation({ summary: 'Retrieves the excluded bank transactions.' })
public getExcludedBankTransactions(
@Query() query: ExcludedBankTransactionsQuery,
) {
return this.excludeBankTransactionsApplication.getExcludedBankTransactions(
query,
);
}
@Post(':id/exclude')
@ApiOperation({ summary: 'Exclude the given bank transaction.' })
public excludeBankTransaction(@Param('id') id: string) {
return this.excludeBankTransactionsApplication.excludeBankTransaction(
Number(id),
);
}
@Delete(':id/exclude')
@ApiOperation({ summary: 'Unexclude the given bank transaction.' })
public unexcludeBankTransaction(@Param('id') id: string) {
return this.excludeBankTransactionsApplication.unexcludeBankTransaction(
Number(id),
);
}
@Post('bulk/exclude')
@ApiOperation({ summary: 'Exclude the given bank transactions.' })
public excludeBankTransactions(@Body('ids') ids: number[]) {
return this.excludeBankTransactionsApplication.excludeBankTransactions(ids);
}
@Delete('bulk/exclude')
@ApiOperation({ summary: 'Unexclude the given bank transactions.' })
public unexcludeBankTransactions(@Body('ids') ids: number[]) {
return this.excludeBankTransactionsApplication.unexcludeBankTransactions(
ids,
);
}
}

View File

@@ -1,67 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { ExcludedBankTransactionsQuery } from '../types/BankTransactionsExclude.types';
import { UncategorizedTransactionTransformer } from '@/modules/BankingCategorize/commands/UncategorizedTransaction.transformer';
import { UncategorizedBankTransaction } from '@/modules/BankingTransactions/models/UncategorizedBankTransaction';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
@Injectable()
export class GetExcludedBankTransactionsService {
/**
* @param {TransformerInjectable} transformer
* @param {TenantModelProxy<typeof UncategorizedBankTransaction>} uncategorizedBankTransaction
*/
constructor(
private readonly transformer: TransformerInjectable,
@Inject(UncategorizedBankTransaction.name)
private readonly uncategorizedBankTransaction: TenantModelProxy<
typeof UncategorizedBankTransaction
>,
) {}
/**
* Retrieves the excluded uncategorized bank transactions.
* @param {ExcludedBankTransactionsQuery} filter
* @returns
*/
public async getExcludedBankTransactions(
filter: ExcludedBankTransactionsQuery,
) {
// Parsed query with default values.
const _query = {
page: 1,
pageSize: 20,
...filter,
};
const { results, pagination } = await this.uncategorizedBankTransaction()
.query()
.onBuild((q) => {
q.modify('excluded');
q.orderBy('date', 'DESC');
if (_query.accountId) {
q.where('account_id', _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);
}
})
.pagination(_query.page - 1, _query.pageSize);
const data = await this.transformer.transform(
results,
new UncategorizedTransactionTransformer(),
);
return { data, pagination };
}
}

View File

@@ -1,8 +0,0 @@
import { Module } from '@nestjs/common';
import { TransactionLandedCostEntriesService } from './TransactionLandedCostEntries.service';
@Module({
providers: [TransactionLandedCostEntriesService],
exports: [TransactionLandedCostEntriesService],
})
export class BillLandedCostsModule {}

View File

@@ -1,107 +0,0 @@
import { Model } from 'objection';
import { lowerCase } from 'lodash';
// import TenantModel from 'models/TenantModel';
import { BaseModel } from '@/models/Model';
export class BillLandedCost extends BaseModel {
amount!: number;
fromTransactionId!: number;
fromTransactionType!: string;
fromTransactionEntryId!: number;
allocationMethod!: string;
costAccountId!: number;
description!: string;
billId!: number;
exchangeRate!: number;
/**
* Table name
*/
static get tableName() {
return 'bill_located_costs';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount', 'allocationMethodFormatted'];
}
/**
* Retrieves the cost local amount.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Allocation method formatted.
*/
get allocationMethodFormatted() {
const allocationMethod = lowerCase(this.allocationMethod);
const keyLabelsPairs = {
value: 'allocation_method.value.label',
quantity: 'allocation_method.quantity.label',
};
return keyLabelsPairs[allocationMethod] || '';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { BillLandedCostEntry } = require('./BillLandedCostEntry');
const { Bill } = require('../../Bills/models/Bill');
const {
ItemEntry,
} = require('../../TransactionItemEntry/models/ItemEntry');
const {
ExpenseCategory,
} = require('../../Expenses/models/ExpenseCategory.model');
return {
bill: {
relation: Model.BelongsToOneRelation,
modelClass: Bill,
join: {
from: 'bill_located_costs.billId',
to: 'bills.id',
},
},
allocateEntries: {
relation: Model.HasManyRelation,
modelClass: BillLandedCostEntry,
join: {
from: 'bill_located_costs.id',
to: 'bill_located_cost_entries.billLocatedCostId',
},
},
allocatedFromBillEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ItemEntry,
join: {
from: 'bill_located_costs.fromTransactionEntryId',
to: 'items_entries.id',
},
},
allocatedFromExpenseEntry: {
relation: Model.BelongsToOneRelation,
modelClass: ExpenseCategory,
join: {
from: 'bill_located_costs.fromTransactionEntryId',
to: 'expense_transaction_categories.id',
},
},
};
}
}

View File

@@ -1,83 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { BillPaymentsApplication } from './BillPaymentsApplication.service';
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import {
CreateBillPaymentDto,
EditBillPaymentDto,
} from './dtos/BillPayment.dto';
@Controller('bill-payments')
@ApiTags('bill-payments')
export class BillPaymentsController {
constructor(private billPaymentsApplication: BillPaymentsApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new bill payment.' })
public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) {
return this.billPaymentsApplication.createBillPayment(billPaymentDTO);
}
@Delete(':billPaymentId')
@ApiOperation({ summary: 'Delete the given bill payment.' })
@ApiParam({
name: 'billPaymentId',
required: true,
type: Number,
description: 'The bill payment id',
})
public deleteBillPayment(@Param('billPaymentId') billPaymentId: string) {
return this.billPaymentsApplication.deleteBillPayment(
Number(billPaymentId),
);
}
@Put(':billPaymentId')
@ApiOperation({ summary: 'Edit the given bill payment.' })
@ApiParam({
name: 'billPaymentId',
required: true,
type: Number,
description: 'The bill payment id',
})
public editBillPayment(
@Param('billPaymentId') billPaymentId: string,
@Body() billPaymentDTO: EditBillPaymentDto,
) {
return this.billPaymentsApplication.editBillPayment(
Number(billPaymentId),
billPaymentDTO,
);
}
@Get(':billPaymentId')
@ApiOperation({ summary: 'Retrieves the bill payment details.' })
@ApiParam({
name: 'billPaymentId',
required: true,
type: Number,
description: 'The bill payment id',
})
public getBillPayment(@Param('billPaymentId') billPaymentId: string) {
return this.billPaymentsApplication.getBillPayment(Number(billPaymentId));
}
@Get(':billPaymentId/bills')
@ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' })
@ApiParam({
name: 'billPaymentId',
required: true,
type: Number,
description: 'The bill payment id',
})
public getPaymentBills(@Param('billPaymentId') billPaymentId: string) {
return this.billPaymentsApplication.getPaymentBills(Number(billPaymentId));
}
}

View File

@@ -1,41 +0,0 @@
import { Module } from '@nestjs/common';
import { BillPaymentsApplication } from './BillPaymentsApplication.service';
import { CreateBillPaymentService } from './commands/CreateBillPayment.service';
import { EditBillPayment } from './commands/EditBillPayment.service';
import { GetBillPayment } from './queries/GetBillPayment.service';
import { DeleteBillPayment } from './commands/DeleteBillPayment.service';
import { BillPaymentBillSync } from './commands/BillPaymentBillSync.service';
import { GetPaymentBills } from './queries/GetPaymentBills.service';
import { BillPaymentValidators } from './commands/BillPaymentValidators.service';
import { CommandBillPaymentDTOTransformer } from './commands/CommandBillPaymentDTOTransformer.service';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { BranchTransactionDTOTransformer } from '../Branches/integrations/BranchTransactionDTOTransform';
import { BranchesSettingsService } from '../Branches/BranchesSettings';
import { BillPaymentsController } from './BillPayments.controller';
import { BillPaymentGLEntries } from './commands/BillPaymentGLEntries';
import { BillPaymentGLEntriesSubscriber } from './subscribers/BillPaymentGLEntriesSubscriber';
import { LedgerModule } from '../Ledger/Ledger.module';
import { AccountsModule } from '../Accounts/Accounts.module';
@Module({
imports: [LedgerModule, AccountsModule],
providers: [
BillPaymentsApplication,
CreateBillPaymentService,
EditBillPayment,
GetBillPayment,
DeleteBillPayment,
BillPaymentBillSync,
GetPaymentBills,
BillPaymentValidators,
CommandBillPaymentDTOTransformer,
BranchTransactionDTOTransformer,
BranchesSettingsService,
TenancyContext,
BillPaymentGLEntries,
BillPaymentGLEntriesSubscriber,
],
exports: [BillPaymentValidators, CreateBillPaymentService],
controllers: [BillPaymentsController],
})
export class BillPaymentsModule {}

View File

@@ -1,75 +0,0 @@
// import { Inject, Service } from 'typedi';
// import * as R from 'ramda';
// import {
// IBillPayment,
// IBillPaymentsFilter,
// IPaginationMeta,
// IFilterMeta,
// } from '@/interfaces';
// import { BillPaymentTransformer } from './queries/BillPaymentTransformer';
// import DynamicListingService from '@/services/DynamicListing/DynamicListService';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
// @Service()
// export class GetBillPayments {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private dynamicListService: DynamicListingService;
// @Inject()
// private transformer: TransformerInjectable;
// /**
// * Retrieve bill payment paginted and filterable list.
// * @param {number} tenantId
// * @param {IBillPaymentsFilter} billPaymentsFilter
// */
// public async getBillPayments(
// tenantId: number,
// filterDTO: IBillPaymentsFilter
// ): Promise<{
// billPayments: IBillPayment[];
// pagination: IPaginationMeta;
// filterMeta: IFilterMeta;
// }> {
// const { BillPayment } = this.tenancy.models(tenantId);
// // Parses filter DTO.
// const filter = this.parseListFilterDTO(filterDTO);
// // Dynamic list service.
// const dynamicList = await this.dynamicListService.dynamicList(
// tenantId,
// BillPayment,
// filter
// );
// const { results, pagination } = await BillPayment.query()
// .onBuild((builder) => {
// builder.withGraphFetched('vendor');
// builder.withGraphFetched('paymentAccount');
// dynamicList.buildQuery()(builder);
// filter?.filterQuery && filter?.filterQuery(builder);
// })
// .pagination(filter.page - 1, filter.pageSize);
// // Transformes the bill payments models to POJO.
// const billPayments = await this.transformer.transform(
// tenantId,
// results,
// new BillPaymentTransformer()
// );
// return {
// billPayments,
// pagination,
// filterMeta: dynamicList.getResponseMeta(),
// };
// }
// private parseListFilterDTO(filterDTO) {
// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
// }
// }

View File

@@ -1,34 +0,0 @@
// import { Inject, Service } from 'typedi';
// import { Exportable } from '@/services/Export/Exportable';
// import { BillPaymentsApplication } from './BillPaymentsApplication';
// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
// @Service()
// export class BillPaymentExportable extends Exportable {
// @Inject()
// private billPaymentsApplication: BillPaymentsApplication;
// /**
// * Retrieves the accounts data to exportable sheet.
// * @param {number} tenantId
// * @returns
// */
// public exportable(tenantId: number, query: any) {
// const filterQuery = (builder) => {
// builder.withGraphFetched('entries.bill');
// builder.withGraphFetched('branch');
// };
// const parsedQuery = {
// sortOrder: 'desc',
// columnSortBy: 'created_at',
// ...query,
// page: 1,
// pageSize: EXPORT_SIZE_LIMIT,
// filterQuery
// } as any;
// return this.billPaymentsApplication
// .getBillPayments(tenantId, parsedQuery)
// .then((output) => output.billPayments);
// }
// }

View File

@@ -1,45 +0,0 @@
// import { Inject, Service } from 'typedi';
// import { Knex } from 'knex';
// import { IBillPaymentDTO } from '@/interfaces';
// import { CreateBillPayment } from './CreateBillPayment';
// import { Importable } from '@/services/Import/Importable';
// import { BillsPaymentsSampleData } from './constants';
// @Service()
// export class BillPaymentsImportable extends Importable {
// @Inject()
// private createBillPaymentService: CreateBillPayment;
// /**
// * Importing to account service.
// * @param {number} tenantId
// * @param {IAccountCreateDTO} createAccountDTO
// * @returns
// */
// public importable(
// tenantId: number,
// billPaymentDTO: IBillPaymentDTO,
// trx?: Knex.Transaction
// ) {
// return this.createBillPaymentService.createBillPayment(
// tenantId,
// billPaymentDTO,
// 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 BillsPaymentsSampleData;
// }
// }

View File

@@ -1,50 +0,0 @@
export const ERRORS = {
BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND',
PAYMENT_MADE_NOT_FOUND: 'PAYMENT_MADE_NOT_FOUND',
BILL_PAYMENT_NUMBER_NOT_UNQIUE: 'BILL_PAYMENT_NUMBER_NOT_UNQIUE',
PAYMENT_ACCOUNT_NOT_FOUND: 'PAYMENT_ACCOUNT_NOT_FOUND',
PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE:
'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND',
INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT',
PAYMENT_NUMBER_SHOULD_NOT_MODIFY: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY',
BILLS_NOT_OPENED_YET: 'BILLS_NOT_OPENED_YET',
VENDOR_HAS_PAYMENTS: 'VENDOR_HAS_PAYMENTS',
WITHDRAWAL_ACCOUNT_CURRENCY_INVALID: 'WITHDRAWAL_ACCOUNT_CURRENCY_INVALID',
};
export const DEFAULT_VIEWS = [];
export const BillsPaymentsSampleData = [
{
'Payment Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10001',
'Reference No.': 'REF-1',
'Payment Account': 'Petty Cash',
Statement: 'Vel et dolorem architecto veniam.',
'Bill No': 'B-120',
'Payment Amount': 100,
},
{
'Payment Date': '2024-03-02',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10002',
'Reference No.': 'REF-2',
'Payment Account': 'Petty Cash',
Statement: 'Id est molestias.',
'Bill No': 'B-121',
'Payment Amount': 100,
},
{
'Payment Date': '2024-03-03',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10003',
'Reference No.': 'REF-3',
'Payment Account': 'Petty Cash',
Statement: 'Quam cupiditate at nihil dicta dignissimos non fugit illo.',
'Bill No': 'B-122',
'Payment Amount': 100,
},
];

View File

@@ -1,185 +0,0 @@
import { Model, mixin } from 'objection';
// import TenantModel from 'models/TenantModel';
// import ModelSetting from './ModelSetting';
// import BillPaymentSettings from './BillPayment.Settings';
// import CustomViewBaseModel from './CustomViewBaseModel';
// import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants';
// import ModelSearchable from './ModelSearchable';
import { BaseModel } from '@/models/Model';
import { BillPaymentEntry } from './BillPaymentEntry';
import { Vendor } from '@/modules/Vendors/models/Vendor';
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
export class BillPayment extends BaseModel{
vendorId: number;
amount: number;
currencyCode: string;
paymentAccountId: number;
paymentNumber: string;
paymentDate: string;
paymentMethod: string;
reference: string;
userId: number;
statement: string;
exchangeRate: number;
createdAt?: Date;
updatedAt?: Date;
branchId?: number;
entries?: BillPaymentEntry[];
vendor?: Vendor;
attachments?: Document[];
/**
* Table name
*/
static get tableName() {
return 'bills_payments';
}
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['localAmount'];
}
/**
* Payment amount in local currency.
* @returns {number}
*/
get localAmount() {
return this.amount * this.exchangeRate;
}
/**
* Model settings.
*/
// static get meta() {
// return BillPaymentSettings;
// }
/**
* Relationship mapping.
*/
// static get relationMappings() {
// const BillPaymentEntry = require('models/BillPaymentEntry');
// const AccountTransaction = require('models/AccountTransaction');
// const Vendor = require('models/Vendor');
// const Account = require('models/Account');
// const Branch = require('models/Branch');
// const Document = require('models/Document');
// return {
// entries: {
// relation: Model.HasManyRelation,
// modelClass: BillPaymentEntry.default,
// join: {
// from: 'bills_payments.id',
// to: 'bills_payments_entries.billPaymentId',
// },
// filter: (query) => {
// query.orderBy('index', 'ASC');
// },
// },
// vendor: {
// relation: Model.BelongsToOneRelation,
// modelClass: Vendor.default,
// join: {
// from: 'bills_payments.vendorId',
// to: 'contacts.id',
// },
// filter(query) {
// query.where('contact_service', 'vendor');
// },
// },
// paymentAccount: {
// relation: Model.BelongsToOneRelation,
// modelClass: Account.default,
// join: {
// from: 'bills_payments.paymentAccountId',
// to: 'accounts.id',
// },
// },
// transactions: {
// relation: Model.HasManyRelation,
// modelClass: AccountTransaction.default,
// join: {
// from: 'bills_payments.id',
// to: 'accounts_transactions.referenceId',
// },
// filter(builder) {
// builder.where('reference_type', 'BillPayment');
// },
// },
// /**
// * Bill payment may belongs to branch.
// */
// branch: {
// relation: Model.BelongsToOneRelation,
// modelClass: Branch.default,
// join: {
// from: 'bills_payments.branchId',
// to: 'branches.id',
// },
// },
// /**
// * Bill payment may has many attached attachments.
// */
// attachments: {
// relation: Model.ManyToManyRelation,
// modelClass: Document.default,
// join: {
// from: 'bills_payments.id',
// through: {
// from: 'document_links.modelId',
// to: 'document_links.documentId',
// },
// to: 'documents.id',
// },
// filter(query) {
// query.where('model_ref', 'BillPayment');
// },
// },
// };
// }
/**
* Retrieve the default custom views, roles and columns.
*/
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'payment_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -1,60 +0,0 @@
import {
IBillPaymentEventCreatedPayload,
IBillPaymentEventDeletedPayload,
IBillPaymentEventEditedPayload,
} from '../types/BillPayments.types';
import { BillPaymentGLEntries } from '../commands/BillPaymentGLEntries';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
@Injectable()
export class BillPaymentGLEntriesSubscriber {
constructor(
private readonly billPaymentGLEntries: BillPaymentGLEntries,
) {}
/**
* Handle bill payment writing journal entries once created.
*/
@OnEvent(events.billPayment.onCreated)
private async handleWriteJournalEntries({
billPayment,
trx,
}: IBillPaymentEventCreatedPayload) {
// Records the journal transactions after bills payment
// and change diff account balance.
await this.billPaymentGLEntries.writePaymentGLEntries(
billPayment.id,
trx
);
};
/**
* Handle bill payment re-writing journal entries once the payment transaction be edited.
*/
@OnEvent(events.billPayment.onEdited)
private async handleRewriteJournalEntriesOncePaymentEdited({
billPayment,
trx,
}: IBillPaymentEventEditedPayload) {
await this.billPaymentGLEntries.rewritePaymentGLEntries(
billPayment.id,
trx
);
};
/**
* Reverts journal entries once bill payment deleted.
*/
@OnEvent(events.billPayment.onDeleted)
private async handleRevertJournalEntries({
billPaymentId,
trx,
}: IBillPaymentEventDeletedPayload) {
await this.billPaymentGLEntries.revertPaymentGLEntries(
billPaymentId,
trx
);
};
}

View File

@@ -1,92 +0,0 @@
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
import {
Controller,
Post,
Body,
Put,
Param,
Delete,
Get,
Query,
} from '@nestjs/common';
import { BillsApplication } from './Bills.application';
import { IBillsFilter } from './Bills.types';
import { CreateBillDto, EditBillDto } from './dtos/Bill.dto';
@Controller('bills')
@ApiTags('bills')
export class BillsController {
constructor(private billsApplication: BillsApplication) {}
@Post()
@ApiOperation({ summary: 'Create a new bill.' })
createBill(@Body() billDTO: CreateBillDto) {
return this.billsApplication.createBill(billDTO);
}
@Put(':id')
@ApiOperation({ summary: 'Edit the given bill.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The bill id',
})
editBill(@Param('id') billId: number, @Body() billDTO: EditBillDto) {
return this.billsApplication.editBill(billId, billDTO);
}
@Delete(':id')
@ApiOperation({ summary: 'Delete the given bill.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The bill id',
})
deleteBill(@Param('id') billId: number) {
return this.billsApplication.deleteBill(billId);
}
@Get()
@ApiOperation({ summary: 'Retrieves the bills.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The bill id',
})
getBills(@Query() filterDTO: IBillsFilter) {
return this.billsApplication.getBills(filterDTO);
}
@Get(':id')
@ApiOperation({ summary: 'Retrieves the bill details.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The bill id',
})
getBill(@Param('id') billId: number) {
return this.billsApplication.getBill(billId);
}
@Post(':id/open')
@ApiOperation({ summary: 'Open the given bill.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The bill id',
})
openBill(@Param('id') billId: number) {
return this.billsApplication.openBill(billId);
}
@Get('due')
@ApiOperation({ summary: 'Retrieves the due bills.' })
getDueBills(@Body('vendorId') vendorId?: number) {
return this.billsApplication.getDueBills(vendorId);
}
}

View File

@@ -1,37 +0,0 @@
// import { Inject, Service } from 'typedi';
// import { Knex } from 'knex';
// import { IBillsFilter } from '@/interfaces';
// import { Exportable } from '@/services/Export/Exportable';
// import { BillsApplication } from '../Bills.application';
// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
// import Objection from 'objection';
// @Service()
// export class BillsExportable extends Exportable {
// @Inject()
// private billsApplication: BillsApplication;
// /**
// * Retrieves the accounts data to exportable sheet.
// * @param {number} tenantId
// * @returns
// */
// public exportable(tenantId: number, query: IBillsFilter) {
// const filterQuery = (query) => {
// query.withGraphFetched('branch');
// query.withGraphFetched('warehouse');
// };
// const parsedQuery = {
// sortOrder: 'desc',
// columnSortBy: 'created_at',
// ...query,
// page: 1,
// pageSize: EXPORT_SIZE_LIMIT,
// filterQuery,
// } as IBillsFilter;
// return this.billsApplication
// .getBills(tenantId, parsedQuery)
// .then((output) => output.bills);
// }
// }

View File

@@ -1,46 +0,0 @@
// import { Inject, Service } from 'typedi';
// import { Knex } from 'knex';
// import { Importable } from '@/services/Import/Importable';
// import { CreateBill } from './CreateBill.service';
// import { IBillDTO } from '@/interfaces';
// import { BillsSampleData } from '../Bills.constants';
// @Service()
// export class BillsImportable extends Importable {
// @Inject()
// private createBillService: CreateBill;
// /**
// * Importing to account service.
// * @param {number} tenantId
// * @param {IAccountCreateDTO} createAccountDTO
// * @returns
// */
// public importable(
// tenantId: number,
// createAccountDTO: IBillDTO,
// trx?: Knex.Transaction
// ) {
// return this.createBillService.createBill(
// 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 BillsSampleData;
// }
// }

View File

@@ -1,107 +0,0 @@
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsInt,
IsNumber,
IsOptional,
IsPositive,
IsString,
Min,
MinLength,
ValidateNested,
} from 'class-validator';
enum DiscountType {
Percentage = 'percentage',
Amount = 'amount',
}
export class BillEntryDto extends ItemEntryDto {
@IsOptional()
@IsBoolean()
landedCost?: boolean;
}
class AttachmentDto {
@IsString()
key: string;
}
export class CommandBillDto {
@IsString()
billNumber: string;
@IsOptional()
@IsString()
referenceNo?: string;
@IsDate()
@Type(() => Date)
billDate: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
dueDate?: Date;
@IsInt()
vendorId: number;
@IsOptional()
@IsNumber()
@IsPositive()
exchangeRate?: number;
@IsOptional()
@IsInt()
warehouseId?: number;
@IsOptional()
@IsInt()
branchId?: number;
@IsOptional()
@IsInt()
projectId?: number;
@IsOptional()
@IsString()
note?: string;
@IsBoolean()
open: boolean = false;
@IsBoolean()
isInclusiveTax: boolean = false;
@IsArray()
@ValidateNested({ each: true })
@Type(() => BillEntryDto)
@ArrayMinSize(1)
entries: BillEntryDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AttachmentDto)
attachments?: AttachmentDto[];
@IsEnum(DiscountType)
discountType: DiscountType = DiscountType.Amount;
@IsOptional()
@IsNumber()
discount?: number;
@IsOptional()
@IsNumber()
adjustment?: number;
}
export class CreateBillDto extends CommandBillDto {}
export class EditBillDto extends CommandBillDto {}

View File

@@ -1,659 +0,0 @@
import { Model, raw, mixin } from 'objection';
import { castArray, difference, defaultTo } from 'lodash';
import * as moment from 'moment';
import * as R from 'ramda';
// import TenantModel from 'models/TenantModel';
// import BillSettings from './Bill.Settings';
// import ModelSetting from './ModelSetting';
// import CustomViewBaseModel from './CustomViewBaseModel';
// import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants';
// import ModelSearchable from './ModelSearchable';
import { BaseModel, PaginationQueryBuilderType } from '@/models/Model';
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost';
import { DiscountType } from '@/common/types/Discount';
import type { Knex, QueryBuilder } from 'knex';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
export class Bill extends TenantBaseModel {
public amount: number;
public paymentAmount: number;
public landedCostAmount: number;
public allocatedCostAmount: number;
public isInclusiveTax: boolean;
public taxAmountWithheld: number;
public exchangeRate: number;
public vendorId: number;
public billNumber: string;
public billDate: Date;
public dueDate: Date;
public referenceNo: string;
public status: string;
public note: string;
public currencyCode: string;
public creditedAmount: number;
public invLotNumber: string;
public invoicedAmount: number;
public openedAt: Date | string;
public userId: number;
public discountType: DiscountType;
public discount: number;
public adjustment: number;
public branchId: number;
public warehouseId: number;
public projectId: number;
public createdAt: Date;
public updatedAt: Date | null;
public entries?: ItemEntry[];
public locatedLandedCosts?: BillLandedCost[];
/**
* Timestamps columns.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'balance',
'dueAmount',
'isOpen',
'isPartiallyPaid',
'isFullyPaid',
'isPaid',
'remainingDays',
'overdueDays',
'isOverdue',
'unallocatedCostAmount',
'localAmount',
'localAllocatedCostAmount',
'billableAmount',
'amountLocal',
'discountAmount',
'discountAmountLocal',
'discountPercentage',
'adjustmentLocal',
'subtotal',
'subtotalLocal',
'subtotalExludingTax',
'taxAmountWithheldLocal',
'total',
'totalLocal',
];
}
/**
* Invoice amount in base currency.
* @returns {number}
*/
get amountLocal(): number {
return this.amount * this.exchangeRate;
}
/**
* Subtotal. (Tax inclusive) if the tax inclusive is enabled.
* @returns {number}
*/
get subtotal(): number {
return this.amount;
}
/**
* Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled.
* @returns {number}
*/
get subtotalLocal(): number {
return this.amountLocal;
}
/**
* Sale invoice amount excluding tax.
* @returns {number}
*/
get subtotalExcludingTax(): number {
return this.isInclusiveTax
? this.subtotal - this.taxAmountWithheld
: this.subtotal;
}
/**
* Tax amount withheld in base currency.
* @returns {number}
*/
get taxAmountWithheldLocal(): number {
return this.taxAmountWithheld * this.exchangeRate;
}
/**
* Discount amount.
* @returns {number}
*/
get discountAmount(): number {
return this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
}
/**
* Discount amount in local currency.
* @returns {number | null}
*/
get discountAmountLocal(): number | null {
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
}
/**
/**
* Discount percentage.
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
* Adjustment amount in local currency.
* @returns {number | null}
*/
get adjustmentLocal(): number | null {
return this.adjustment ? this.adjustment * this.exchangeRate : null;
}
/**
* Invoice total. (Tax included)
* @returns {number}
*/
get total(): number {
const adjustmentAmount = defaultTo(this.adjustment, 0);
return R.compose(
R.add(adjustmentAmount),
R.subtract(R.__, this.discountAmount),
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld)),
)(this.subtotal);
}
/**
* Invoice total in local currency. (Tax included)
* @returns {number}
*/
get totalLocal(): number {
return this.total * this.exchangeRate;
}
/**
* Invoice amount in organization base currency.
* @deprecated
* @returns {number}
*/
get localAmount(): number {
return this.amountLocal;
}
/**
* Retrieves the local allocated cost amount.
* @returns {number}
*/
get localAllocatedCostAmount(): number {
return this.allocatedCostAmount * this.exchangeRate;
}
/**
* Retrieves the local landed cost amount.
* @returns {number}
*/
get localLandedCostAmount(): number {
return this.landedCostAmount * this.exchangeRate;
}
/**
* Retrieves the local unallocated cost amount.
* @returns {number}
*/
get localUnallocatedCostAmount(): number {
return this.unallocatedCostAmount * this.exchangeRate;
}
/**
* Retrieve the balance of bill.
* @return {number}
*/
get balance(): number {
return this.paymentAmount + this.creditedAmount;
}
/**
* Due amount of the given.
* @return {number}
*/
get dueAmount(): number {
return Math.max(this.total - this.balance, 0);
}
/**
* Detarmine whether the bill is open.
* @return {boolean}
*/
get isOpen(): boolean {
return !!this.openedAt;
}
/**
* Deetarmine whether the bill paid partially.
* @return {boolean}
*/
get isPartiallyPaid(): boolean {
return this.dueAmount !== this.total && this.dueAmount > 0;
}
/**
* Deetarmine whether the bill paid fully.
* @return {boolean}
*/
get isFullyPaid(): boolean {
return this.dueAmount === 0;
}
/**
* Detarmines whether the bill paid fully or partially.
* @return {boolean}
*/
get isPaid(): boolean {
return this.isPartiallyPaid || this.isFullyPaid;
}
/**
* Retrieve the remaining days in number
* @return {number|null}
*/
get remainingDays(): number | null {
const currentMoment = moment();
const dueDateMoment = moment(this.dueDate);
return Math.max(dueDateMoment.diff(currentMoment, 'days'), 0);
}
/**
* Retrieve the overdue days in number.
* @return {number|null}
*/
get overdueDays(): number | null {
const currentMoment = moment();
const dueDateMoment = moment(this.dueDate);
return Math.max(currentMoment.diff(dueDateMoment, 'days'), 0);
}
/**
* Detarmines the due date is over.
* @return {boolean}
*/
get isOverdue(): boolean {
return this.overdueDays > 0;
}
/**
* Retrieve the unallocated cost amount.
* @return {number}
*/
get unallocatedCostAmount(): number {
return Math.max(this.landedCostAmount - this.allocatedCostAmount, 0);
}
/**
* Retrieves the calculated amount which have not been invoiced.
*/
get billableAmount(): number {
return Math.max(this.total - this.invoicedAmount, 0);
}
/**
* Table name
*/
static get tableName() {
return 'bills';
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the bills in draft status.
*/
draft(query) {
query.where('opened_at', null);
},
/**
* Filters the opened bills.
*/
published(query) {
query.whereNot('openedAt', null);
},
/**
* Filters the opened bills.
*/
opened(query) {
query.whereNot('opened_at', null);
},
/**
* Filters the unpaid bills.
*/
unpaid(query) {
query.where('payment_amount', 0);
},
/**
* Filters the due bills.
*/
dueBills(query) {
query.where(
raw(`COALESCE(AMOUNT, 0) -
COALESCE(PAYMENT_AMOUNT, 0) -
COALESCE(CREDITED_AMOUNT, 0) > 0
`),
);
},
/**
* Filters the overdue bills.
*/
overdue(query) {
query.where('due_date', '<', moment().format('YYYY-MM-DD'));
},
/**
* Filters the not overdue invoices.
*/
notOverdue(query, asDate = moment().format('YYYY-MM-DD')) {
query.where('due_date', '>=', asDate);
},
/**
* Filters the partially paid bills.
*/
partiallyPaid(query) {
query.whereNot('payment_amount', 0);
query.whereNot(raw('`PAYMENT_AMOUNT` = `AMOUNT`'));
},
/**
* Filters the paid bills.
*/
paid(query) {
query.where(raw('`PAYMENT_AMOUNT` = `AMOUNT`'));
},
/**
* Filters the bills from the given date.
*/
fromDate(query, fromDate) {
query.where('bill_date', '<=', fromDate);
},
/**
* Sort the bills by full-payment bills.
*/
sortByStatus(query, order) {
query.orderByRaw(`PAYMENT_AMOUNT = AMOUNT ${order}`);
},
/**
* Status filter.
*/
statusFilter(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
default:
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
/**
* Filters by branches.
*/
filterByBranches(query, branchesIds) {
const formattedBranchesIds = castArray(branchesIds);
query.whereIn('branchId', formattedBranchesIds);
},
dueBillsFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
query.modify('dueBills');
query.modify('notOverdue');
query.modify('fromDate', asDate);
},
overdueBillsFromDate(query, asDate = moment().format('YYYY-MM-DD')) {
query.modify('dueBills');
query.modify('overdue', asDate);
query.modify('fromDate', asDate);
},
/**
*
*/
billable(query) {
query.where(raw('AMOUNT > INVOICED_AMOUNT'));
},
};
}
/**
* Bill model settings.
*/
// static get meta() {
// return BillSettings;
// }
/**
* Relationship mapping.
*/
static get relationMappings() {
const { Vendor } = require('../../Vendors/models/Vendor');
const {
ItemEntry,
} = require('../../TransactionItemEntry/models/ItemEntry');
const {
BillLandedCost,
} = require('../../BillLandedCosts/models/BillLandedCost');
const { Branch } = require('../../Branches/models/Branch.model');
const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
const { TaxRateModel } = require('../../TaxRates/models/TaxRate.model');
const {
TaxRateTransaction,
} = require('../../TaxRates/models/TaxRateTransaction.model');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
return {
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Vendor,
join: {
from: 'bills.vendorId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'vendor');
},
},
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry,
join: {
from: 'bills.id',
to: 'items_entries.referenceId',
},
filter(builder) {
builder.where('reference_type', 'Bill');
builder.orderBy('index', 'ASC');
},
},
locatedLandedCosts: {
relation: Model.HasManyRelation,
modelClass: BillLandedCost,
join: {
from: 'bills.id',
to: 'bill_located_costs.billId',
},
},
/**
* Bill may belongs to associated branch.
*/
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch,
join: {
from: 'bills.branchId',
to: 'branches.id',
},
},
/**
* Bill may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse,
join: {
from: 'bills.warehouseId',
to: 'warehouses.id',
},
},
/**
* Bill may has associated tax rate transactions.
*/
taxes: {
relation: Model.HasManyRelation,
modelClass: TaxRateTransaction,
join: {
from: 'bills.id',
to: 'tax_rate_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'Bill');
},
},
/**
* Bill may has many attached attachments.
*/
attachments: {
relation: Model.ManyToManyRelation,
modelClass: Document,
join: {
from: 'bills.id',
through: {
from: 'document_links.modelId',
to: 'document_links.documentId',
},
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'Bill');
},
},
/**
* Bill may belongs to matched bank transaction.
*/
// matchedBankTransaction: {
// relation: Model.HasManyRelation,
// modelClass: MatchedBankTransaction,
// join: {
// from: 'bills.id',
// to: 'matched_bank_transactions.referenceId',
// },
// filter(query) {
// query.where('reference_type', 'Bill');
// },
// },
};
}
/**
* Retrieve the not found bills ids as array that associated to the given vendor.
* @param {Array} billsIds
* @param {number} vendorId -
* @return {Array}
*/
static async getNotFoundBills(billsIds, vendorId) {
const storedBills = await this.query().onBuild((builder) => {
builder.whereIn('id', billsIds);
if (vendorId) {
builder.where('vendor_id', vendorId);
}
});
const storedBillsIds = storedBills.map((t) => t.id);
const notFoundBillsIds = difference(billsIds, storedBillsIds);
return notFoundBillsIds;
}
static changePaymentAmount(
billId: number,
amount: number,
trx: Knex.Transaction,
) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.query(trx)
.where('id', billId)
[changeMethod]('payment_amount', Math.abs(amount));
}
/**
* Retrieve the default custom views, roles and columns.
*/
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'bill_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -1,32 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { BillPaymentEntry } from '@/modules/BillPayments/models/BillPaymentEntry';
import { BillPaymentTransactionTransformer } from '@/modules/BillPayments/queries/BillPaymentTransactionTransformer';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
@Injectable()
export class GetBillPayments {
constructor(
@Inject(BillPaymentEntry.name)
private billPaymentEntryModel: typeof BillPaymentEntry,
private transformer: TransformerInjectable,
) {}
/**
* Retrieve the specific bill associated payment transactions.
* @param {number} billId
* @returns {}
*/
public getBillPayments = async (billId: number) => {
const billsEntries = await this.billPaymentEntryModel
.query()
.where('billId', billId)
.withGraphJoined('payment.paymentAccount')
.withGraphJoined('bill')
.orderBy('payment:paymentDate', 'ASC');
return this.transformer.transform(
billsEntries,
new BillPaymentTransactionTransformer(),
);
};
}

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