Compare commits

...

123 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
7e6f1efe30 feat: Add date format controll to import 2024-08-29 19:48:58 +02:00
Ahmed Bouhuolia
c43123db76 Merge pull request #636 from bigcapitalhq/expand-export-page-size
fix: Expand the resources export page size limitation
2024-08-29 14:23:48 +02:00
Ahmed Bouhuolia
ebbcab3926 fix: Expand the resources export page size limitation 2024-08-29 14:22:45 +02:00
Ahmed Bouhuolia
a235f573c0 Merge pull request #635 from bigcapitalhq/fix-avoid-cost-job-import-preview
fix: Avoid running the cost job in import preview
2024-08-29 10:09:13 +02:00
Ahmed Bouhuolia
84a0b8f495 fix: re-schedule the jobs have date from the current moment 2024-08-29 10:05:38 +02:00
Ahmed Bouhuolia
b87321c897 fix: Avoid running the cost job in import preview 2024-08-28 22:15:15 +02:00
Ahmed Bouhuolia
fc6ebfea5c Debounce scheduling calculating items cost 2024-08-28 21:25:47 +02:00
Ahmed Bouhuolia
161d60393a Merge pull request #629 from bigcapitalhq/details-subscription
fix: Add subscription plans offer text
2024-08-25 19:44:34 +02:00
Ahmed Bouhuolia
79413fa85e fix: Add subscription plans offer text 2024-08-25 19:43:54 +02:00
Ahmed Bouhuolia
58552c6c94 Merge pull request #628 from bigcapitalhq/fix-webapp-env-variables
fix: Make webapp package env variables dynamic
2024-08-25 18:21:55 +02:00
Ahmed Bouhuolia
2072e35cfa fix: Make webapp package env variables dynamic 2024-08-25 18:21:08 +02:00
Ahmed Bouhuolia
1eaac9d691 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-08-25 14:18:04 +02:00
Ahmed Bouhuolia
a916e8a0cb fix: syntax error 2024-08-25 14:17:23 +02:00
Ahmed Bouhuolia
a56f560036 Merge pull request #627 from bigcapitalhq/fix-style-tweeks-in-onboarding-page
fix: Style tweaks in onboarding page
2024-08-25 13:20:39 +02:00
Ahmed Bouhuolia
0fb886936c Merge pull request #626 from bigcapitalhq/disable-sms-service
fix: Disable sms service until Twilo integration
2024-08-25 13:20:14 +02:00
Ahmed Bouhuolia
670136916f fix: Style tweaks in onboarding page 2024-08-25 13:19:16 +02:00
Ahmed Bouhuolia
768297f137 fix: Disable sms service until Twilo integration 2024-08-25 13:07:07 +02:00
Ahmed Bouhuolia
ef505a0a62 Merge pull request #625 from bigcapitalhq/manual-journal-number-prefix
fix: Add prefix J-00001 to manual journals increments
2024-08-25 13:05:52 +02:00
Ahmed Bouhuolia
42d40620ec fix: add prefix J-00001 to manual journals increments 2024-08-25 13:05:28 +02:00
Ahmed Bouhuolia
959ef7a691 Merge branch 'listen-payment-webhooks' into develop 2024-08-24 21:52:04 +02:00
Ahmed Bouhuolia
60f03f534b fix(subscription): event TS types 2024-08-24 21:51:15 +02:00
Ahmed Bouhuolia
e44ebb700a Merge pull request #623 from bigcapitalhq/listen-payment-webhooks
fix: Listen to payment webhooks
2024-08-24 21:49:36 +02:00
Ahmed Bouhuolia
8e94c7a755 feat(subscription): invalidate subscription cache 2024-08-24 21:40:28 +02:00
Ahmed Bouhuolia
3a2ca36c07 Merge branch 'develop' into listen-payment-webhooks 2024-08-24 21:08:47 +02:00
Ahmed Bouhuolia
88ece74c8a Merge pull request #622 from wolone/develop
Fix: Syntax error caused error
2024-08-24 20:48:14 +02:00
Ahmed Bouhuolia
67a8610328 feat: cancel/resume LS subscriptions 2024-08-24 20:46:30 +02:00
Ahmed Bouhuolia
278d61ce61 fix: Listen to payment webhooks 2024-08-24 18:50:12 +02:00
wolone
e72d6ad6b8 Fix: Syntax error caused error 2024-08-24 23:33:53 +08:00
Ahmed Bouhuolia
bf66b31679 Merge pull request #590 from bigcapitalhq/filter-uncategorized-bank-transactions
feat(banking): Filter uncategorized bank transactions by date
2024-08-23 17:42:59 +02:00
Ahmed Bouhuolia
b4d426d2e8 feat: Filter uncategorized transactions by date 2024-08-23 17:40:05 +02:00
Ahmed Bouhuolia
7f7dd270e7 Merge pull request #619 from bigcapitalhq/change-banking-service-language
feat: change banking service language
2024-08-23 02:00:47 +02:00
Ahmed Bouhuolia
f6bad8fe30 fix: wip filter uncategorized transactions by date 2024-08-23 01:57:52 +02:00
Ahmed Bouhuolia
820b363f79 feat: change banking service language 2024-08-22 20:49:15 +02:00
Ahmed Bouhuolia
07740a51ef Merge pull request #616 from bigcapitalhq/one-click-demo-account
feat(ee): One-click demo account
2024-08-22 20:33:06 +02:00
Ahmed Bouhuolia
d15fb6fe19 chore: document http query 2024-08-22 20:32:48 +02:00
Ahmed Bouhuolia
5749ccec81 feat: seed more demo bank transactions 2024-08-22 19:50:31 +02:00
Ahmed Bouhuolia
4a99f6c0cf feat: one-click demo account 2024-08-22 19:21:23 +02:00
Ahmed Bouhuolia
59f480f9d5 feat: add more demo account seeders 2024-08-22 13:04:51 +02:00
Ahmed Bouhuolia
6cb9c919b5 Merge branch 'develop' into one-click-demo-account 2024-08-22 10:48:24 +02:00
Ahmed Bouhuolia
1062b65b5b feat: wip bank account transactions date filter 2024-08-22 01:03:03 +02:00
Ahmed Bouhuolia
bf3a70cabd Merge branch 'develop' into filter-uncategorized-bank-transactions 2024-08-22 00:12:37 +02:00
Ahmed Bouhuolia
f46cd28f87 Merge pull request #618 from bigcapitalhq/display-details-bank-account
fix: Some bank account details hidden
2024-08-21 21:22:15 +02:00
Ahmed Bouhuolia
dffcfe50aa fix: some bank account details hidden 2024-08-21 21:19:59 +02:00
Ahmed Bouhuolia
fac55efbc7 Merge pull request #617 from bigcapitalhq/fix-impoort-itemns
fix: Cannot import items income and cost accounts
2024-08-21 19:33:50 +02:00
Ahmed Bouhuolia
8b90ce5f6c fix: cannot import items income and cost accounts 2024-08-21 19:32:59 +02:00
Ahmed Bouhuolia
705b8da053 fix: protect the one-click demo accounts endpoints 2024-08-21 01:04:18 +02:00
Ahmed Bouhuolia
4a05ccc692 feat: add more seedders 2024-08-20 23:40:23 +02:00
Ahmed Bouhuolia
3200d65d90 feat: add demo account button on onboarding 2024-08-20 22:01:36 +02:00
Ahmed Bouhuolia
3f23038227 chore: comment 2024-08-20 18:49:39 +02:00
Ahmed Bouhuolia
408c807fc2 feat: import sheet files on initializing demo account 2024-08-20 18:30:21 +02:00
Ahmed Bouhuolia
d29079a8c5 fix: one-click demo account 2024-08-20 12:51:23 +02:00
Ahmed Bouhuolia
cca596b4a9 fix: one click demo 2024-08-19 21:21:39 +02:00
Ahmed Bouhuolia
b768f18294 chore: change subscription prices 2024-08-19 13:08:09 +02:00
Ahmed Bouhuolia
fed620505d feat: add one click endpoint 2024-08-19 12:10:38 +02:00
Ahmed Bouhuolia
a008aea3f3 feat(ee): one-click demo account 2024-08-19 12:08:58 +02:00
Ahmed Bouhuolia
25297bc191 chore: dump CHANGELOG 2024-08-18 20:56:10 +02:00
Ahmed Bouhuolia
1989887b25 Merge pull request #615 from bigcapitalhq/activate-account-from-drawer
feat: activate/inactivate account from drawer details
2024-08-18 20:24:07 +02:00
Ahmed Bouhuolia
e4fb126d39 feat: activate/inactivate account from drawer details 2024-08-18 20:23:47 +02:00
Ahmed Bouhuolia
5fcb2d9cc9 Merge pull request #614 from bigcapitalhq/delete-bank-account-with-uncategorized-transactions
fix: Delete bank account with uncategorized transactions
2024-08-18 19:55:15 +02:00
Ahmed Bouhuolia
06ea631732 fix: making pagination more readable 2024-08-18 19:38:15 +02:00
Ahmed Bouhuolia
2f21107a43 feat: delete uncategorized transactions before deleting bank account 2024-08-18 19:30:09 +02:00
Ahmed Bouhuolia
fb8118bea8 fix: Delete bank account with uncategorized transactions 2024-08-18 14:20:23 +02:00
Ahmed Bouhuolia
4ba1c0aa22 Merge pull request #612 from Champetaman/fix-manual-journal-date-expense-drawer
Fix: Correctly display Date, Published At, and Created At in ExpenseDrawerHeader
2024-08-18 11:20:25 +02:00
Ahmed Bouhuolia
169f115fa0 fix: remove the default value from date and createdAt because always required 2024-08-18 11:19:06 +02:00
Ahmed Bouhuolia
61ab2b78d9 Merge pull request #613 from bigcapitalhq/language-typos
fix: Language typos
2024-08-18 11:12:49 +02:00
Ahmed Bouhuolia
93732430fc fix: Language typos 2024-08-18 11:11:17 +02:00
Camilo Oviedo
0215206220 add: Created attribute formattedPublishedAt to display on Expense Drawer 2024-08-17 10:11:05 +10:00
Camilo Oviedo
3c8956fedf fix: Correctly display Date, Published At, and Created At fields 2024-08-17 10:09:44 +10:00
Ahmed Bouhuolia
4477ada1ad Merge pull request #611 from bigcapitalhq/fix-connection-lost
fix: Database connection lost error
2024-08-15 23:48:28 +02:00
Ahmed Bouhuolia
fde9ccc5ca fix: Database connection lost error 2024-08-15 23:47:21 +02:00
Ahmed Bouhuolia
bbbd96f159 Merge pull request #604 from bigcapitalhq/inconsistance-pagination-page-size
fix: inconsistance page size of paginated data tables
2024-08-14 22:14:03 +02:00
Ahmed Bouhuolia
9f4de8115f fix: initial page size to the data tables from store state 2024-08-14 22:12:21 +02:00
Ahmed Bouhuolia
d9f241a2f8 Merge branch 'develop' into inconsistance-pagination-page-size 2024-08-14 22:01:04 +02:00
Ahmed Bouhuolia
ee96dc68cc fix: Change Dropzone title and subtitle (#607) 2024-08-14 20:13:45 +02:00
Ahmed Bouhuolia
b12f090d13 fix: matching bank transactions should create associate payment transactions for bills and invoicese. (#606) 2024-08-14 19:23:15 +02:00
allcontributors[bot]
f6ce761a27 docs: add Champetaman as a contributor for code (#605)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-08-14 16:20:50 +02:00
Ahmed Bouhuolia
8c89e04f54 Merge pull request #603 from Champetaman/fix-import-drag-area-colors-blue
Fix: Enhance Dropzone visual feedback for dropzone
2024-08-14 16:17:44 +02:00
Ahmed Bouhuolia
f97b127a69 fix: style tweaks in dropzoen accept/reject modes 2024-08-14 16:12:13 +02:00
Ahmed Bouhuolia
5c1fa8f5cd fix: inconsistance page size of paginated data tables 2024-08-14 15:23:14 +02:00
Ahmed Bouhuolia
64c4d7b5a4 Merge pull request #599 from bigcapitalhq/rename-payment-receives-to-payment-received
fix: Typo payment receive messages
2024-08-14 14:54:54 +02:00
Ahmed Bouhuolia
ea2fad648b Merge branch 'develop' into rename-payment-receives-to-payment-received 2024-08-14 14:54:48 +02:00
Camilo Oviedo
9b8b51cb91 fix: Enhance visual feedback on file drag-and-drop 2024-08-14 16:29:38 +10:00
Camilo Oviedo
09b7e74d65 fix: Enhance visual feedback on file drag-and-drop 2024-08-14 16:21:48 +10:00
Ahmed Bouhuolia
7137e06d99 Merge pull request #602 from bigcapitalhq/remove-views-tabs-from-receipts-list
fix: Remove views tabs from receipts list
2024-08-14 00:06:14 +02:00
Ahmed Bouhuolia
fc29b765f7 fix: remove views tabs from receipts list 2024-08-14 00:04:11 +02:00
Ahmed Bouhuolia
2946475f89 Merge pull request #601 from bigcapitalhq/autofill-quick-customer-vendor
fix: Autofill the quick created customer/vendor
2024-08-13 23:59:26 +02:00
Ahmed Bouhuolia
d2193fdac0 fix: autofill the quick created customer/vendor 2024-08-13 23:55:53 +02:00
Ahmed Bouhuolia
ec2b7e332e Merge pull request #600 from bigcapitalhq/fix-typo-categories-list
fix: Typo categories list
2024-08-13 19:39:17 +02:00
Ahmed Bouhuolia
a3704df6dd fix: Typo categories list 2024-08-13 19:38:07 +02:00
Ahmed Bouhuolia
c3c784e52c Merge pull request #598 from bigcapitalhq/move-payment-mades-to-payments-made
fix: Typo payments made
2024-08-13 19:33:02 +02:00
Ahmed Bouhuolia
bbcf695a6c fix: Typo payments made 2024-08-13 19:10:09 +02:00
Ahmed Bouhuolia
038d4dd5a7 chore: renmame payment receive term to payment received 2024-08-13 15:15:07 +02:00
Ahmed Bouhuolia
961e4b99e8 fix: rename interfaces to PaymentReceived 2024-08-13 14:17:37 +02:00
Ahmed Bouhuolia
9991eebaaf fix(server): rename term to 2024-08-13 13:41:09 +02:00
Ahmed Bouhuolia
cd90fede54 Merge pull request #597 from bigcapitalhq/refresh-acccounts-bank-transactions
fix: Refresh accounts and account transactions.
2024-08-13 11:39:11 +02:00
Ahmed Bouhuolia
a2d28648bd fix: refresh accounts and account transactions. 2024-08-13 11:37:59 +02:00
Ahmed Bouhuolia
3097d05eda Merge pull request #596 from bigcapitalhq/transaction-type-description-general-ledger
fix: Transaction type and description do not show in general ledger.
2024-08-12 21:11:02 +02:00
Ahmed Bouhuolia
ff94d8d9b2 fix: Transaction type and description do not show in general ledger. 2024-08-12 21:08:02 +02:00
Ahmed Bouhuolia
79cc09fad9 Merge pull request #595 from bigcapitalhq/add-comparators-to-amount-bank-rule
feat: Add amount comparators to amount bank rule field
2024-08-12 20:17:20 +02:00
Ahmed Bouhuolia
c1b29c3f23 fix: add equal condition to number fields on bank rule 2024-08-12 20:16:18 +02:00
Ahmed Bouhuolia
cf4bb3007e feat: run re-recognizing bank transactions on edit bank rule 2024-08-12 20:07:01 +02:00
Ahmed Bouhuolia
193a86cf30 feat: add amount comparators to amount bank rule field 2024-08-12 17:53:57 +02:00
Ahmed Bouhuolia
7a81f14eb2 Merge pull request #594 from bigcapitalhq/multi-lines-transactions-statements
fix: Multi-lines transactions statements
2024-08-12 16:33:01 +02:00
Ahmed Bouhuolia
14d1f0bd1d fix: Multi-lines transactions statements 2024-08-12 16:31:36 +02:00
Ahmed Bouhuolia
82f8648c59 Merge pull request #593 from bigcapitalhq/fix-round-pending-matching
fix: Rounding the total amount the pending and matched transactions
2024-08-12 13:01:27 +02:00
Ahmed Bouhuolia
c928940d32 fix: rounding the total amount the pending and matched transactions 2024-08-12 13:01:00 +02:00
Ahmed Bouhuolia
0a78d56015 Merge pull request #592 from bigcapitalhq/matching-reconcile-branches
fix: Should not load branches on reconcile matching form if the branches not enabled
2024-08-12 11:29:48 +02:00
Ahmed Bouhuolia
1a5716873e fix: should not load branches on reconcile matching form if the branches not enabled 2024-08-12 11:28:34 +02:00
Ahmed Bouhuolia
01b7c86ab9 Merge pull request #588 from Champetaman/fix-dev-variable-setting-error
Update `dev` Script in `package.json` to Use `cross-env`
2024-08-12 10:55:45 +02:00
Ahmed Bouhuolia
0ca209b195 Merge pull request #589 from bigcapitalhq/bank-pending-transactions
feat: Pending bank transactions
2024-08-12 10:54:32 +02:00
Ahmed Bouhuolia
be6f6e3c73 fix: function description 2024-08-12 10:54:16 +02:00
Ahmed Bouhuolia
cb016be78c fix: avoid decrement/increment for pending bank transactions 2024-08-12 10:48:36 +02:00
Ahmed Bouhuolia
fc085f2328 Merge pull request #587 from bigcapitalhq/big-244-uncategorize-bank-transactions-in-bulk
feat: Uncategorize bank transactions in bulk
2024-08-12 10:11:55 +02:00
Ahmed Bouhuolia
9a34f3e283 fix: invalidate account cache on bulk uncategorizing 2024-08-12 10:11:40 +02:00
Ahmed Bouhuolia
7054e862d5 feat: pending transactions table 2024-08-11 22:51:58 +02:00
Ahmed Bouhuolia
faa81abee4 feat(banking): uncategorize bank transactions in bulk 2024-08-11 21:26:02 +02:00
Ahmed Bouhuolia
6d01f2a323 Merge pull request #591 from bigcapitalhq/import-export-tax-rates
feat: import and export tax rates
2024-08-11 19:51:50 +02:00
Ahmed Bouhuolia
72678bb936 feat: import and export tax rates 2024-08-11 19:51:16 +02:00
Ahmed Bouhuolia
df8b68fda6 feat(banking): Filter uncategorized bank transactions 2024-08-11 18:34:45 +02:00
Ahmed Bouhuolia
9ae5644af9 feat: Pending bank transactions 2024-08-11 16:14:13 +02:00
Camilo Oviedo
e8830c5911 Fix dev variable setting causing error on windows for craco start command 2024-08-11 22:14:54 +10:00
Camilo Oviedo
7699889bd6 Fix dev variable setting causing error on windows for craco start command 2024-08-11 21:57:50 +10:00
Ahmed Bouhuolia
35a061d188 feat: Uncategorize bank transactions in bulk 2024-08-11 13:02:38 +02:00
444 changed files with 6067 additions and 1695 deletions

View File

@@ -150,6 +150,15 @@
"contributions": [
"bug"
]
},
{
"login": "Champetaman",
"name": "Camilo Oviedo",
"avatar_url": "https://avatars.githubusercontent.com/u/64604272?v=4",
"profile": "https://www.camilooviedo.com/",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -2,6 +2,27 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.19.4] - 18-08-2024
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
* fix: Transaction type and description do not show in general ledger. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/596
* fix: Refresh accounts and account transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/597
* fix: Typo payments made by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/598
* fix: Typo categories list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/600
* fix: Autofill the quick created customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/601
* fix: Remove views tabs from receipts list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/602
* fix: Typo payment receive messages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/599
* fix: Enhance Dropzone visual of accept and reject modes by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/603
* fix: Matching bank transactions should create associate payment transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/606
* fix: Change Dropzone title and subtitle by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/607
* fix: Inconsistance page size of paginated data tables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/604
* fix: Database connection lost error by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/611
* fix: Language typos by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/613
* Fix: Correctly display Date, Published At, and Created At in ExpenseDrawerHeader by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/612
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
## [v0.18.0] - 10-08-2024
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511

View File

@@ -129,6 +129,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oleynikd"><img src="https://avatars.githubusercontent.com/u/3976868?v=4?s=100" width="100px;" alt="Denis"/><br /><sub><b>Denis</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aoleynikd" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.camilooviedo.com/"><img src="https://avatars.githubusercontent.com/u/64604272?v=4?s=100" width="100px;" alt="Camilo Oviedo"/><br /><sub><b>Camilo Oviedo</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Champetaman" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -37,6 +37,7 @@
"agendash": "^3.1.0",
"app-root-path": "^3.0.0",
"async": "^3.2.0",
"async-mutex": "^0.5.0",
"axios": "^1.6.0",
"babel-loader": "^9.1.2",
"bcryptjs": "^2.4.3",
@@ -69,9 +70,8 @@
"is-my-json-valid": "^2.20.5",
"js-money": "^0.6.3",
"jsonwebtoken": "^8.5.1",
"knex": "^0.95.15",
"knex": "^3.1.0",
"knex-cleaner": "^1.3.0",
"knex-db-manager": "^0.6.1",
"libphonenumber-js": "^1.9.6",
"lodash": "^4.17.15",
"lru-cache": "^6.0.0",

View File

@@ -1,9 +1,10 @@
import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express';
import { param, query } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
import { param } from 'express-validator';
import { GetPendingBankAccountTransactions } from '@/services/Cashflow/GetPendingBankAccountTransaction';
@Service()
export class BankAccountsController extends BaseController {
@@ -13,6 +14,9 @@ export class BankAccountsController extends BaseController {
@Inject()
private bankAccountsApp: BankAccountsApplication;
@Inject()
private getPendingTransactionsService: GetPendingBankAccountTransactions;
/**
* Router constructor.
*/
@@ -20,6 +24,16 @@ export class BankAccountsController extends BaseController {
const router = Router();
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
router.get(
'/pending_transactions',
[
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
],
this.validationResult,
this.getBankAccountsPendingTransactions.bind(this)
);
router.post(
'/:bankAccountId/disconnect',
this.disconnectBankAccount.bind(this)
@@ -27,17 +41,13 @@ export class BankAccountsController extends BaseController {
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
router.post(
'/:bankAccountId/pause_feeds',
[
param('bankAccountId').exists().isNumeric().toInt(),
],
[param('bankAccountId').exists().isNumeric().toInt()],
this.validationResult,
this.pauseBankAccountFeeds.bind(this)
);
router.post(
'/:bankAccountId/resume_feeds',
[
param('bankAccountId').exists().isNumeric().toInt(),
],
[param('bankAccountId').exists().isNumeric().toInt()],
this.validationResult,
this.resumeBankAccountFeeds.bind(this)
);
@@ -72,6 +82,32 @@ export class BankAccountsController extends BaseController {
}
}
/**
* Retrieves the bank account pending transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getBankAccountsPendingTransactions(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const query = this.matchedQueryData(req);
try {
const data =
await this.getPendingTransactionsService.getPendingTransactions(
tenantId,
query
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
/**
* Disonnect the given bank account.
* @param {Request} req
@@ -128,9 +164,9 @@ export class BankAccountsController extends BaseController {
/**
* Resumes the bank account feeds sync.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async resumeBankAccountFeeds(
@@ -155,9 +191,9 @@ export class BankAccountsController extends BaseController {
/**
* Pauses the bank account feeds sync.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async pauseBankAccountFeeds(

View File

@@ -33,7 +33,16 @@ export class BankingRulesController extends BaseController {
body('conditions.*.field').exists().isIn(['description', 'amount']),
body('conditions.*.comparator')
.exists()
.isIn(['equals', 'contains', 'not_contain'])
.isIn([
'equals',
'equal',
'contains',
'not_contain',
'bigger',
'bigger_or_equal',
'smaller',
'smaller_or_equal',
])
.default('contain')
.trim(),
body('conditions.*.value').exists().trim(),

View File

@@ -46,6 +46,10 @@ export class ExcludeBankTransactionsController extends BaseController {
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
],
this.validationResult,
this.getExcludedBankTransactions.bind(this)

View File

@@ -21,6 +21,10 @@ export class RecognizedTransactionsController extends BaseController {
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('account_id').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().isFloat(),
],
this.validationResult,
this.getRecognizedTransactions.bind(this)

View File

@@ -1,5 +1,5 @@
import { Service, Inject } from 'typedi';
import { ValidationChain, check, param, query } from 'express-validator';
import { ValidationChain, body, check, param, query } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import { omit } from 'lodash';
import BaseController from '../BaseController';
@@ -43,6 +43,16 @@ export default class NewCashflowTransactionController extends BaseController {
this.asyncMiddleware(this.newCashflowTransaction),
this.catchServiceErrors
);
router.post(
'/transactions/uncategorize/bulk',
[
body('ids').isArray({ min: 1 }),
body('ids.*').exists().isNumeric().toInt(),
],
this.validationResult,
this.uncategorizeBulkTransactions.bind(this),
this.catchServiceErrors
);
router.post(
'/transactions/:id/uncategorize',
this.revertCategorizedCashflowTransaction,
@@ -74,6 +84,10 @@ export default class NewCashflowTransactionController extends BaseController {
param('id').exists().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
];
}
@@ -184,6 +198,34 @@ export default class NewCashflowTransactionController extends BaseController {
}
};
/**
* Uncategorize the given transactions in bulk.
* @param {Request<{}>} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | null>}
*/
private uncategorizeBulkTransactions = async (
req: Request<{}>,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { ids: uncategorizedTransactionIds } = this.matchedBodyData(req);
try {
await this.cashflowApplication.uncategorizeTransactions(
tenantId,
uncategorizedTransactionIds
);
return res.status(200).send({
message: 'The given transactions have been uncategorized successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Categorize the cashflow transaction.
* @param {Request} req

View File

@@ -40,6 +40,7 @@ export class ImportController extends BaseController {
body('mapping.*.group').optional(),
body('mapping.*.from').exists(),
body('mapping.*.to').exists(),
body('mapping.*.dateFormat').optional({ nullable: true }),
],
this.validationResult,
this.asyncMiddleware(this.mapping.bind(this)),

View File

@@ -0,0 +1,87 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { body } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { OneClickDemoApplication } from '@/services/OneClickDemo/OneClickDemoApplication';
import config from '@/config';
@Service()
export class OneClickDemoController extends BaseController {
@Inject()
private oneClickDemoApp: OneClickDemoApplication;
/**
* Router constructor method.
*/
router() {
const router = Router();
// Protects the endpoints if the feature is not enabled.
const protectMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// Add your protection logic here
if (config.oneClickDemoAccounts) {
next();
} else {
res.status(403).send({ message: 'Forbidden' });
}
};
router.post(
'/one_click',
protectMiddleware,
asyncMiddleware(this.oneClickDemo.bind(this))
);
router.post(
'/one_click_signin',
[body('demo_id').exists()],
this.validationResult,
protectMiddleware,
asyncMiddleware(this.oneClickSignIn.bind(this))
);
return router;
}
/**
* One-click demo application.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private async oneClickDemo(req: Request, res: Response, next: NextFunction) {
try {
const data = await this.oneClickDemoApp.createOneClick();
return res.status(200).send({
data,
message: 'The one-click demo has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Sign-in to one-click demo account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async oneClickSignIn(
req: Request,
res: Response,
next: NextFunction
) {
const { demoId } = this.matchedBodyData(req);
try {
const data = await this.oneClickDemoApp.autoSignIn(demoId);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
}

View File

@@ -9,9 +9,9 @@ import {
} from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import PaymentReceivesPages from '@/services/Sales/PaymentReceives/PaymentReceivesPages';
import PaymentsReceivedPages from '@/services/Sales/PaymentReceived/PaymentsReceivedPages';
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceived/PaymentReceivedApplication';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ServiceError } from '@/exceptions';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@@ -22,7 +22,7 @@ export default class PaymentReceivesController extends BaseController {
private paymentReceiveApplication: PaymentReceivesApplication;
@Inject()
private PaymentReceivesPages: PaymentReceivesPages;
private PaymentsReceivedPages: PaymentsReceivedPages;
@Inject()
private dynamicListService: DynamicListingService;
@@ -229,7 +229,7 @@ export default class PaymentReceivesController extends BaseController {
try {
const storedPaymentReceive =
await this.paymentReceiveApplication.createPaymentReceive(
await this.paymentReceiveApplication.createPaymentReceived(
tenantId,
paymentReceive,
user
@@ -376,7 +376,7 @@ export default class PaymentReceivesController extends BaseController {
const { customerId } = this.matchedQueryData(req);
try {
const entries = await this.PaymentReceivesPages.getNewPageEntries(
const entries = await this.PaymentsReceivedPages.getNewPageEntries(
tenantId,
customerId
);
@@ -404,7 +404,7 @@ export default class PaymentReceivesController extends BaseController {
try {
const { paymentReceive, entries } =
await this.PaymentReceivesPages.getPaymentReceiveEditPage(
await this.PaymentsReceivedPages.getPaymentReceiveEditPage(
tenantId,
paymentReceiveId,
user

View File

@@ -113,7 +113,7 @@ export class SubscriptionController extends BaseController {
const { tenantId } = req;
try {
await this.subscriptionApp.cancelSubscription(tenantId, '455610');
await this.subscriptionApp.cancelSubscription(tenantId);
return res.status(200).send({
status: 200,

View File

@@ -35,7 +35,7 @@ export class Webhooks extends BaseController {
*/
public async lemonWebhooks(req: Request, res: Response, next: NextFunction) {
const data = req.body;
const signature = req.headers['x-signature'] ?? '';
const signature = req.headers['x-signature'] as string ?? '';
const rawBody = req.rawBody;
try {

View File

@@ -63,6 +63,7 @@ import { BankingController } from './controllers/Banking/BankingController';
import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
export default () => {
const app = Router();
@@ -80,6 +81,7 @@ export default () => {
app.use('/jobs', Container.get(Jobs).router());
app.use('/account', Container.get(Account).router());
app.use('/webhooks', Container.get(Webhooks).router());
app.use('/demo', Container.get(OneClickDemoController).router())
// - Dashboard routes.
// ---------------------------

View File

@@ -1,11 +1,21 @@
import { Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
import SettingsStore from '@/services/Settings/SettingsStore';
export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user;
const Logger = Container.get('logger');
const settings = await initializeTenantSettings(tenantId);
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
}
export const initializeTenantSettings = async (tenantId: number) => {
const tenantContainer = Container.of(`tenant-${tenantId}`);
if (tenantContainer && !tenantContainer.has('settings')) {
@@ -18,10 +28,5 @@ export default async (req: Request, res: Response, next: NextFunction) => {
await settings.load();
req.settings = settings;
res.on('finish', async () => {
await settings.save();
});
next();
return settings;
}

View File

@@ -4,6 +4,7 @@ import { Request } from 'express';
import TenancyService from '@/services/Tenancy/TenancyService';
import TenantsManagerService from '@/services/Tenancy/TenantsManager';
import rtlDetect from 'rtl-detect';
import { Tenant } from '@/system/models';
export default (req: Request, tenant: ITenant) => {
const { id: tenantId, organizationId } = tenant;
@@ -16,7 +17,7 @@ export default (req: Request, tenant: ITenant) => {
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils(req));
tenantContainer.set('i18n', injectI18nUtils());
const knexInstance = tenantServices.knex(tenantId);
const models = tenantServices.models(tenantId);
@@ -33,14 +34,35 @@ export default (req: Request, tenant: ITenant) => {
};
export const injectI18nUtils = (req) => {
const locale = req.getLocale();
const globalI18n = Container.get('i18n');
const locale = globalI18n.getLocale();
const direction = rtlDetect.getLangDir(locale);
return {
locale,
__: req.__,
__: globalI18n.__,
direction,
isRtl: direction === 'rtl',
isLtr: direction === 'ltr',
};
};
export const initalizeTenantServices = async (tenantId: number) => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const tenantServices = Container.get(TenancyService);
const tenantsManager = Container.get(TenantsManagerService);
// Initialize the knex instance.
tenantsManager.setupKnexInstance(tenant);
const tenantContainer = tenantServices.tenantContainer(tenantId);
tenantContainer.set('i18n', injectI18nUtils());
tenantServices.knex(tenantId);
tenantServices.models(tenantId);
tenantServices.repositories(tenantId);
tenantServices.cache(tenantId);
};

View File

@@ -32,7 +32,7 @@ module.exports = {
*/
tenant: {
db_client: process.env.TENANT_DB_CLIENT || process.env.DB_CLIENT || 'mysql',
db_name_prefix: process.env.TENANT_DB_NAME_PERFIX,
db_name_prefix: process.env.TENANT_DB_NAME_PERFIX || 'bigcapital_tenant_',
db_host: process.env.TENANT_DB_HOST || process.env.DB_HOST,
db_user: process.env.TENANT_DB_USER || process.env.DB_USER,
db_password: process.env.TENANT_DB_PASSWORD || process.env.DB_PASSWORD,
@@ -245,4 +245,12 @@ module.exports = {
loops: {
apiKey: process.env.LOOPS_API_KEY,
},
/**
* One-click demo accounts.
*/
oneClickDemoAccounts: {
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
},
};

View File

@@ -0,0 +1,13 @@
exports.up = function (knex) {
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
table.boolean('pending').defaultTo(false);
table.string('pending_plaid_transaction_id').nullable();
});
};
exports.down = function (knex) {
return knex.schema.table('uncategorized_cashflow_transactions', (table) => {
table.dropColumn('pending');
table.dropColumn('pending_plaid_transaction_id');
});
};

View File

@@ -15,6 +15,7 @@ export default class SeedSettings extends TenantSeeder {
// 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.

View File

@@ -7,7 +7,6 @@ export interface IRegisterDTO {
lastName: string;
email: string;
password: string;
organizationName: string;
}
export interface ILoginDTO {
@@ -77,6 +76,10 @@ export interface IAuthSendedResetPassword {
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
oneClickDemo: {
enable: boolean;
demoUrl: string;
};
}
export interface IAuthSignUpVerifingEventPayload {

View File

@@ -268,6 +268,8 @@ export interface CreateUncategorizedTransactionDTO {
description?: string;
referenceNo?: string | null;
plaidTransactionId?: string | null;
pending?: boolean;
pendingPlaidTransactionId?: string | null;
batch?: string;
}
@@ -283,3 +285,17 @@ export interface IUncategorizedTransactionCreatedEventPayload {
createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO;
trx: Knex.Transaction;
}
export interface IPendingTransactionRemovingEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
pendingTransaction: IUncategorizedCashflowTransaction;
trx?: Knex.Transaction;
}
export interface IPendingTransactionRemovedEventPayload {
tenantId: number;
uncategorizedTransactionId: number;
pendingTransaction: IUncategorizedCashflowTransaction;
trx?: Knex.Transaction;
}

View File

@@ -167,11 +167,18 @@ export interface CategorizeTransactionAsExpenseDTO {
export interface IGetUncategorizedTransactionsQuery {
page?: number;
pageSize?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}
export interface IGetRecognizedTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
}
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}

View File

@@ -30,7 +30,7 @@ export interface IGeneralLedgerSheetAccountTransaction {
currencyCode: string;
note?: string;
transactionType?: string;
transactionTypeFormatted: string;
transactionNumber: string;
referenceId?: number;

View File

@@ -3,6 +3,6 @@ import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
export interface IImportFileCommitedEventPayload {
tenantId: number;
importId: number;
importId: string;
meta: ImportFilePreviewPOJO;
}

View File

@@ -8,7 +8,7 @@ import { ILedgerEntry } from './Ledger';
import { ISaleInvoice } from './SaleInvoice';
import { AttachmentLinkDTO } from './Attachments';
export interface IPaymentReceive {
export interface IPaymentReceived {
id?: number;
customerId: number;
paymentDate: Date;
@@ -19,14 +19,14 @@ export interface IPaymentReceive {
depositAccountId: number;
paymentReceiveNo: string;
statement: string;
entries: IPaymentReceiveEntry[];
entries: IPaymentReceivedEntry[];
userId: number;
createdAt: Date;
updatedAt: Date;
localAmount?: number;
branchId?: number;
}
export interface IPaymentReceiveCreateDTO {
export interface IPaymentReceivedCreateDTO {
customerId: number;
paymentDate: Date;
amount: number;
@@ -35,13 +35,13 @@ export interface IPaymentReceiveCreateDTO {
depositAccountId: number;
paymentReceiveNo?: string;
statement: string;
entries: IPaymentReceiveEntryDTO[];
entries: IPaymentReceivedEntryDTO[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
}
export interface IPaymentReceiveEditDTO {
export interface IPaymentReceivedEditDTO {
customerId: number;
paymentDate: Date;
amount: number;
@@ -50,12 +50,12 @@ export interface IPaymentReceiveEditDTO {
depositAccountId: number;
paymentReceiveNo?: string;
statement: string;
entries: IPaymentReceiveEntryDTO[];
entries: IPaymentReceivedEntryDTO[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
}
export interface IPaymentReceiveEntry {
export interface IPaymentReceivedEntry {
id?: number;
paymentReceiveId: number;
invoiceId: number;
@@ -64,15 +64,15 @@ export interface IPaymentReceiveEntry {
invoice?: ISaleInvoice;
}
export interface IPaymentReceiveEntryDTO {
export interface IPaymentReceivedEntryDTO {
id?: number;
index: number;
paymentReceiveId: number;
paymentReceiveId?: number;
invoiceId: number;
paymentAmount: number;
}
export interface IPaymentReceivesFilter extends IDynamicListFilterDTO {
export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string;
}
@@ -88,65 +88,65 @@ export interface IPaymentReceivePageEntry {
date: Date | string;
}
export interface IPaymentReceiveEditPage {
paymentReceive: IPaymentReceive;
export interface IPaymentReceivedEditPage {
paymentReceive: IPaymentReceived;
entries: IPaymentReceivePageEntry[];
}
export interface IPaymentsReceiveService {
export interface IPaymentsReceivedService {
validateCustomerHasNoPayments(
tenantId: number,
customerId: number
): Promise<void>;
}
export interface IPaymentReceiveSmsDetails {
export interface IPaymentReceivedSmsDetails {
customerName: string;
customerPhoneNumber: string;
smsMessage: string;
}
export interface IPaymentReceiveCreatingPayload {
export interface IPaymentReceivedCreatingPayload {
tenantId: number;
paymentReceiveDTO: IPaymentReceiveCreateDTO;
paymentReceiveDTO: IPaymentReceivedCreateDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceiveCreatedPayload {
export interface IPaymentReceivedCreatedPayload {
tenantId: number;
paymentReceive: IPaymentReceive;
paymentReceive: IPaymentReceived;
paymentReceiveId: number;
authorizedUser: ISystemUser;
paymentReceiveDTO: IPaymentReceiveCreateDTO;
paymentReceiveDTO: IPaymentReceivedCreateDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceiveEditedPayload {
export interface IPaymentReceivedEditedPayload {
tenantId: number;
paymentReceiveId: number;
paymentReceive: IPaymentReceive;
oldPaymentReceive: IPaymentReceive;
paymentReceiveDTO: IPaymentReceiveEditDTO;
paymentReceive: IPaymentReceived;
oldPaymentReceive: IPaymentReceived;
paymentReceiveDTO: IPaymentReceivedEditDTO;
authorizedUser: ISystemUser;
trx: Knex.Transaction;
}
export interface IPaymentReceiveEditingPayload {
export interface IPaymentReceivedEditingPayload {
tenantId: number;
oldPaymentReceive: IPaymentReceive;
paymentReceiveDTO: IPaymentReceiveEditDTO;
oldPaymentReceive: IPaymentReceived;
paymentReceiveDTO: IPaymentReceivedEditDTO;
trx: Knex.Transaction;
}
export interface IPaymentReceiveDeletingPayload {
export interface IPaymentReceivedDeletingPayload {
tenantId: number;
oldPaymentReceive: IPaymentReceive;
oldPaymentReceive: IPaymentReceived;
trx: Knex.Transaction;
}
export interface IPaymentReceiveDeletedPayload {
export interface IPaymentReceivedDeletedPayload {
tenantId: number;
paymentReceiveId: number;
oldPaymentReceive: IPaymentReceive;
oldPaymentReceive: IPaymentReceived;
authorizedUser: ISystemUser;
trx: Knex.Transaction;
}

View File

@@ -33,3 +33,7 @@ export interface IOrganizationBuildEventPayload {
buildDTO: IOrganizationBuildDTO;
systemUser: ISystemUser;
}
export interface IOrganizationBuiltEventPayload {
tenantId: number;
}

View File

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

View File

@@ -51,5 +51,4 @@ export interface ISystemService {
cache();
repositories();
knex();
dbManager();
}

View File

@@ -75,6 +75,7 @@ export * from './Times';
export * from './ProjectProfitabilitySummary';
export * from './TaxRate';
export * from './Plaid';
export * from './Subscription';
export interface I18nService {
__: (input: string) => string;

View File

@@ -21,7 +21,7 @@ export default class ComputeItemCostJob {
agenda.define(
'compute-item-cost',
{ priority: 'high', concurrency: 1 },
{ priority: 'high', concurrency: 20 },
this.handler.bind(this)
);
this.agenda.on('start:compute-item-cost', this.onJobStart.bind(this));

View File

@@ -8,7 +8,7 @@ export default class OrganizationSetupJob {
constructor(agenda) {
agenda.define(
'organization-setup',
{ priority: 'high', concurrency: 1 },
{ priority: 'high', concurrency: 20 },
this.handler
);
}

View File

@@ -15,7 +15,7 @@ export default class WriteInvoicesJournalEntries {
agenda.define(
eventName,
{ priority: 'normal', concurrency: 1 },
{ priority: 'normal', concurrency: 20 },
this.handler.bind(this)
);
agenda.on(`complete:${eventName}`, this.onJobCompleted.bind(this));

View File

@@ -164,6 +164,10 @@ export class Transformer {
return date ? moment(date).format(this.dateFormat) : '';
}
protected formatDateFromNow(date){
return date ? moment(date).fromNow(true) : '';
}
/**
*
* @param number

View File

@@ -1,7 +0,0 @@
import knexManager from 'knex-db-manager';
import { systemKnexConfig, systemDbManager } from 'config/knexConfig';
export default () => knexManager.databaseManagerFactory({
knex: systemKnexConfig,
dbManager: systemDbManager,
});

View File

@@ -3,7 +3,6 @@ import LoggerInstance from '@/loaders/logger';
import agendaFactory from '@/loaders/agenda';
import SmsClientLoader from '@/loaders/smsClient';
import mailInstance from '@/loaders/mail';
import dbManagerFactory from '@/loaders/dbManager';
import i18n from '@/loaders/i18n';
import repositoriesLoader from '@/loaders/systemRepositories';
import Cache from '@/services/Cache';
@@ -16,7 +15,6 @@ export default ({ mongoConnection, knex }) => {
try {
const agendaInstance = agendaFactory({ mongoConnection });
const smsClientInstance = SmsClientLoader(config.easySMSGateway.api_key);
const dbManager = dbManagerFactory(knex);
const cacheInstance = new Cache();
Container.set('logger', LoggerInstance);
@@ -24,7 +22,6 @@ export default ({ mongoConnection, knex }) => {
Container.set('SMSClient', smsClientInstance);
Container.set('mail', mailInstance);
Container.set('dbManager', dbManager);
LoggerInstance.info(
'[DI] Database manager has been injected into container.'
);

View File

@@ -115,6 +115,9 @@ import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/E
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAccounts/events/DisconnectPlaidItemOnAccountDeleted';
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
export default () => {
return new EventPublisher();
@@ -245,8 +248,10 @@ export const susbcribers = () => {
DeleteCashflowTransactionOnUncategorize,
PreventDeleteTransactionOnDelete,
// Subscription
SubscribeFreeOnSignupCommunity,
SendVerfiyMailOnSignUp,
TriggerInvalidateCacheOnSubscriptionChange,
// Attachments
AttachmentsOnSaleInvoiceCreated,
@@ -277,8 +282,12 @@ export const susbcribers = () => {
// Plaid
RecognizeSyncedBankTranasctions,
DisconnectPlaidItemOnAccountDeleted,
DeleteUncategorizedTransactionsOnAccountDeleting,
// Loops
LoopsEventsSubscriber
LoopsEventsSubscriber,
// Demo Account
SeedInitialDemoAccountDataOnOrgBuild,
];
};

View File

@@ -34,4 +34,4 @@
// import 'services/Sales/SaleInvoiceWriteoffSubscriber';
// import 'subscribers/SaleInvoices/SendSmsNotificationToCustomer';
// import 'subscribers/SaleReceipt/SendNotificationToCustomer';
// import 'services/Sales/PaymentReceives/PaymentReceiveSmsSubscriber';
// import 'services/Sales/PaymentReceived/PaymentReceiveSmsSubscriber';

View File

@@ -2,6 +2,7 @@ import { I18n } from 'i18n';
export default () => new I18n({
locales: ['en', 'ar'],
defaultLocale: 'en',
register: global,
directory: global.__locales_dir,
updateFiles: false,

View File

@@ -9,7 +9,7 @@ import { SendSaleInvoiceMailJob } from '@/services/Sales/Invoices/SendSaleInvoic
import { SendSaleInvoiceReminderMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailReminderJob';
import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEstimateMailJob';
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
import { PaymentReceivedMailNotificationJob } from '@/services/Sales/PaymentReceived/PaymentReceivedMailNotificationJob';
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
@@ -28,7 +28,7 @@ export default ({ agenda }: { agenda: Agenda }) => {
new SendSaleInvoiceReminderMailJob(agenda);
new SendSaleEstimateMailJob(agenda);
new SaleReceiptMailNotificationJob(agenda);
new PaymentReceiveMailNotificationJob(agenda);
new PaymentReceivedMailNotificationJob(agenda);
new PlaidFetchTransactionsJob(agenda);
new ImportDeleteExpiredFilesJobs(agenda);
new SendVerifyMailJob(agenda);

View File

@@ -3,7 +3,7 @@ import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import BillPaymentSettings from './BillPayment.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceives/constants';
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants';
import ModelSearchable from './ModelSearchable';
export default class BillPayment extends mixin(TenantModel, [

View File

@@ -257,25 +257,25 @@ export default {
name: 'item.field.sell_price',
fieldType: 'number',
},
cost_price: {
costPrice: {
name: 'item.field.cost_price',
fieldType: 'number',
},
costAccount: {
costAccountId: {
name: 'item.field.cost_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
sellAccount: {
sellAccountId: {
name: 'item.field.sell_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
inventoryAccount: {
inventoryAccountId: {
name: 'item.field.inventory_account',
fieldType: 'relation',
relationModel: 'Account',

View File

@@ -1,4 +1,5 @@
import { Model } from 'objection';
import { castArray, omit, pick } from 'lodash';
import { isEmpty } from 'lodash';
import { ServiceError } from '@/exceptions';
@@ -16,7 +17,15 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
});
}
queryAndThrowIfHasRelations = ({ type, message }) => {
queryAndThrowIfHasRelations = ({
type,
message,
excludeRelations = [],
includedRelations = [],
}) => {
const _excludeRelations = castArray(excludeRelations);
const _includedRelations = castArray(includedRelations);
const model = this.modelClass();
const modelRelations = Object.keys(model.relationMappings).filter(
(relation) =>
@@ -25,9 +34,20 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
) !== -1
);
const relations = model.secureDeleteRelations || modelRelations;
const filteredByIncluded = relations.filter((r) =>
_includedRelations.includes(r)
);
const filteredByExcluded = relations.filter(
(r) => !excludeRelations.includes(r)
);
const filteredRelations = !isEmpty(_includedRelations)
? filteredByIncluded
: !isEmpty(_excludeRelations)
? filteredByExcluded
: relations;
this.runAfter((model, query) => {
const nonEmptyRelations = relations.filter(
const nonEmptyRelations = filteredRelations.filter(
(relation) => !isEmpty(model[relation])
);
if (nonEmptyRelations.length > 0) {
@@ -36,7 +56,7 @@ export default class PaginationQueryBuilder extends Model.QueryBuilder {
return model;
});
return this.onBuild((query) => {
relations.forEach((relation) => {
filteredRelations.forEach((relation) => {
query.withGraphFetched(`${relation}(selectId)`).modifiers({
selectId(builder) {
builder.select('id');

View File

@@ -3,7 +3,7 @@ import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import PaymentReceiveSettings from './PaymentReceive.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceives/constants';
import { DEFAULT_VIEWS } from '@/services/Sales/PaymentReceived/constants';
import ModelSearchable from './ModelSearchable';
export default class PaymentReceive extends mixin(TenantModel, [

View File

@@ -0,0 +1,69 @@
export default {
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
exportable: true,
importable: true,
print: {
pageTitle: 'Tax Rates',
},
columns: {
name: {
name: 'Tax Rate Name',
type: 'text',
accessor: 'name',
},
code: {
name: 'Code',
type: 'text',
accessor: 'code',
},
rate: {
name: 'Rate',
type: 'text',
},
description: {
name: 'Description',
type: 'text',
},
isNonRecoverable: {
name: 'Is Non Recoverable',
type: 'boolean',
},
active: {
name: 'Active',
type: 'boolean',
},
},
field: {},
fields2: {
name: {
name: 'Tax name',
fieldType: 'name',
required: true,
},
code: {
name: 'Code',
fieldType: 'code',
required: true,
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
isNonRecoverable: {
name: 'Is Non Recoverable',
fieldType: 'boolean',
},
active: {
name: 'Active',
fieldType: 'boolean',
},
},
};

View File

@@ -2,8 +2,13 @@ import { mixin, Model, raw } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSearchable from './ModelSearchable';
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
import TaxRateMeta from './TaxRate.settings';
import ModelSetting from './ModelSetting';
export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
export default class TaxRate extends mixin(TenantModel, [
ModelSetting,
ModelSearchable,
]) {
/**
* Table name
*/
@@ -25,6 +30,13 @@ export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
return ['createdAt', 'updatedAt'];
}
/**
* Retrieves the tax rate meta.
*/
static get meta() {
return TaxRateMeta;
}
/**
* Virtual attributes.
*/

View File

@@ -1,9 +1,8 @@
/* eslint-disable global-require */
import * as R from 'ramda';
import { Model, ModelOptions, QueryContext, mixin } from 'objection';
import moment from 'moment';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSettings from './ModelSetting';
import Account from './Account';
import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta';
export default class UncategorizedCashflowTransaction extends mixin(
@@ -21,6 +20,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
plaidTransactionId!: string;
recognizedTransactionId!: number;
excludedAt: Date;
pending: boolean;
/**
* Table name.
@@ -46,7 +46,8 @@ export default class UncategorizedCashflowTransaction extends mixin(
'isDepositTransaction',
'isWithdrawalTransaction',
'isRecognized',
'isExcluded'
'isExcluded',
'isPending',
];
}
@@ -99,6 +100,14 @@ export default class UncategorizedCashflowTransaction extends mixin(
return !!this.excludedAt;
}
/**
* Detarmines whether the transaction is pending.
* @returns {boolean}
*/
public get isPending(): boolean {
return !!this.pending;
}
/**
* Model modifiers.
*/
@@ -143,6 +152,42 @@ export default class UncategorizedCashflowTransaction extends mixin(
query.whereNull('categorizeRefType');
query.whereNull('categorizeRefId');
},
/**
* Filters the not pending transactions.
*/
notPending(query) {
query.where('pending', false);
},
/**
* Filters the pending transactions.
*/
pending(query) {
query.where('pending', true);
},
minAmount(query, minAmount) {
query.where('amount', '>=', minAmount);
},
maxAmount(query, maxAmount) {
query.where('amount', '<=', maxAmount);
},
toDate(query, toDate) {
const dateFormat = 'YYYY-MM-DD';
const _toDate = moment(toDate).endOf('day').format(dateFormat);
query.where('date', '<=', _toDate);
},
fromDate(query, fromDate) {
const dateFormat = 'YYYY-MM-DD';
const _fromDate = moment(fromDate).startOf('day').format(dateFormat);
query.where('date', '>=', _fromDate);
},
};
}

View File

@@ -249,6 +249,7 @@ export default class Ledger implements ILedger {
transactionId: entry.referenceId,
transactionType: entry.referenceType,
transactionSubType: entry.transactionType,
transactionNumber: entry.transactionNumber,
referenceNumber: entry.referenceNumber,
@@ -262,6 +263,8 @@ export default class Ledger implements ILedger {
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,
note: entry.note,
};
}

View File

@@ -43,8 +43,8 @@ export class AccountsApplication {
/**
* Creates a new account.
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @param {number} tenantId
* @param {IAccountCreateDTO} accountDTO
* @returns {Promise<IAccount>}
*/
public createAccount = (
@@ -108,8 +108,8 @@ export class AccountsApplication {
/**
* Retrieves the account details.
* @param {number} tenantId
* @param {number} accountId
* @param {number} tenantId
* @param {number} accountId
* @returns {Promise<IAccount>}
*/
public getAccount = (tenantId: number, accountId: number) => {

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { AccountsApplication } from './AccountsApplication';
import { Exportable } from '../Export/Exportable';
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class AccountsExportable extends Exportable {
@@ -20,7 +21,7 @@ export class AccountsExportable extends Exportable {
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
pageSize: 12000,
pageSize: EXPORT_SIZE_LIMIT,
page: 1,
} as IAccountsFilter;

View File

@@ -73,6 +73,7 @@ export class DeleteAccount {
.throwIfNotFound()
.queryAndThrowIfHasRelations({
type: ERRORS.ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS,
excludeRelations: ['uncategorizedTransactions', 'plaidItem']
});
// Authorize before delete account.
await this.authorize(tenantId, accountId, oldAccount);

View File

@@ -1,10 +1,10 @@
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash';
import {
IPaymentReceiveCreatedPayload,
IPaymentReceiveCreatingPayload,
IPaymentReceiveDeletingPayload,
IPaymentReceiveEditedPayload,
IPaymentReceivedCreatedPayload,
IPaymentReceivedCreatingPayload,
IPaymentReceivedDeletingPayload,
IPaymentReceivedEditedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { LinkAttachment } from '../LinkAttachment';
@@ -50,13 +50,13 @@ export class AttachmentsOnPaymentsReceived {
/**
* Validates the attachment keys on creating payment.
* @param {IPaymentReceiveCreatingPayload}
* @param {IPaymentReceivedCreatingPayload}
* @returns {Promise<void>}
*/
private async validateAttachmentsOnPaymentCreate({
paymentReceiveDTO,
tenantId,
}: IPaymentReceiveCreatingPayload): Promise<void> {
}: IPaymentReceivedCreatingPayload): Promise<void> {
if (isEmpty(paymentReceiveDTO.attachments)) {
return;
}
@@ -67,7 +67,7 @@ export class AttachmentsOnPaymentsReceived {
/**
* Handles linking the attachments of the created payment.
* @param {IPaymentReceiveCreatedPayload}
* @param {IPaymentReceivedCreatedPayload}
* @returns {Promise<void>}
*/
private async handleAttachmentsOnPaymentCreated({
@@ -75,7 +75,7 @@ export class AttachmentsOnPaymentsReceived {
paymentReceiveDTO,
paymentReceive,
trx,
}: IPaymentReceiveCreatedPayload): Promise<void> {
}: IPaymentReceivedCreatedPayload): Promise<void> {
if (isEmpty(paymentReceiveDTO.attachments)) return;
const keys = paymentReceiveDTO.attachments?.map(
@@ -92,14 +92,14 @@ export class AttachmentsOnPaymentsReceived {
/**
* Handles unlinking all the unpresented keys of the edited payment.
* @param {IPaymentReceiveEditedPayload}
* @param {IPaymentReceivedEditedPayload}
*/
private async handleUnlinkUnpresentedKeysOnPaymentEdited({
tenantId,
paymentReceiveDTO,
oldPaymentReceive,
trx,
}: IPaymentReceiveEditedPayload) {
}: IPaymentReceivedEditedPayload) {
const keys = paymentReceiveDTO.attachments?.map(
(attachment) => attachment.key
);
@@ -114,7 +114,7 @@ export class AttachmentsOnPaymentsReceived {
/**
* Handles linking all the presented keys of the edited payment.
* @param {IPaymentReceiveEditedPayload}
* @param {IPaymentReceivedEditedPayload}
* @returns {Promise<void>}
*/
private async handleLinkPresentedKeysOnPaymentEdited({
@@ -122,7 +122,7 @@ export class AttachmentsOnPaymentsReceived {
paymentReceiveDTO,
oldPaymentReceive,
trx,
}: IPaymentReceiveEditedPayload) {
}: IPaymentReceivedEditedPayload) {
if (isEmpty(paymentReceiveDTO.attachments)) return;
const keys = paymentReceiveDTO.attachments?.map(
@@ -146,7 +146,7 @@ export class AttachmentsOnPaymentsReceived {
tenantId,
oldPaymentReceive,
trx,
}: IPaymentReceiveDeletingPayload) {
}: IPaymentReceivedDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'PaymentReceive',

View File

@@ -11,6 +11,10 @@ export class GetAuthMeta {
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return {
signupDisabled: config.signupRestrictions.disabled,
oneClickDemo: {
enable: config.oneClickDemoAccounts.enable,
demoUrl: config.oneClickDemoAccounts.demoUrl,
},
};
}
}

View File

@@ -52,6 +52,9 @@ export class GetBankAccountSummary {
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();
@@ -65,16 +68,32 @@ export class GetBankAccountSummary {
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 UncategorizedCashflowTransaction.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 UncategorizedCashflowTransaction.query().onBuild((q) => {
q.where('accountId', bankAccountId);
q.modify('pending');
// Count the results.
q.count('uncategorized_cashflow_transactions.id as total');
q.first();
@@ -83,14 +102,15 @@ export class GetBankAccountSummary {
const totalUncategorizedTransactions =
uncategorizedTranasctionsCount?.total || 0;
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
const totalPendingTransactions = pendingTransactionsCount?.total || 0;
return {
name: bankAccount.name,
totalUncategorizedTransactions,
totalRecognizedTransactions,
totalExcludedTransactions,
totalPendingTransactions,
};
}
}

View File

@@ -0,0 +1,78 @@
import { Inject, Service } from 'typedi';
import { initialize } from 'objection';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import events from '@/subscribers/events';
import { IAccountEventDeletePayload } from '@/interfaces';
import { DeleteBankRulesService } from '../../Rules/DeleteBankRules';
import { RevertRecognizedTransactions } from '../../RegonizeTranasctions/RevertRecognizedTransactions';
@Service()
export class DeleteUncategorizedTransactionsOnAccountDeleting {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private deleteBankRules: DeleteBankRulesService;
@Inject()
private revertRecognizedTransactins: RevertRecognizedTransactions;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.accounts.onDelete,
this.handleDeleteBankRulesOnAccountDeleting.bind(this)
);
}
/**
* Handles revert the recognized transactions and delete all the bank rules
* associated to the deleted bank account.
* @param {IAccountEventDeletePayload}
*/
private async handleDeleteBankRulesOnAccountDeleting({
tenantId,
oldAccount,
trx,
}: IAccountEventDeletePayload) {
const knex = this.tenancy.knex(tenantId);
const {
BankRule,
UncategorizedCashflowTransaction,
MatchedBankTransaction,
RecognizedBankTransaction,
} = this.tenancy.models(tenantId);
const foundAssociatedRules = await BankRule.query(trx).where(
'applyIfAccountId',
oldAccount.id
);
const foundAssociatedRulesIds = foundAssociatedRules.map((rule) => rule.id);
await initialize(knex, [
UncategorizedCashflowTransaction,
RecognizedBankTransaction,
MatchedBankTransaction,
]);
// Revert the recognized transactions of the given bank rules.
await this.revertRecognizedTransactins.revertRecognizedTransactions(
tenantId,
foundAssociatedRulesIds,
null,
trx
);
// Delete the associated uncategorized transactions.
await UncategorizedCashflowTransaction.query(trx)
.where('accountId', oldAccount.id)
.delete();
// Delete the given bank rules.
await this.deleteBankRules.deleteBankRules(
tenantId,
foundAssociatedRulesIds,
trx
);
}
}

View File

@@ -51,6 +51,7 @@ export class DisconnectPlaidItemOnAccountDeleted {
.findOne('plaidItemId', oldAccount.plaidItemId)
.delete();
// Remove Plaid item once the transaction resolve.
if (oldPlaidItem) {
const plaidInstance = PlaidClientWrapper.getClient();

View File

@@ -1,5 +1,6 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import moment from 'moment';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ExcludedBankTransactionsQuery } from './_types';
import { UncategorizedTransactionTransformer } from '@/services/Cashflow/UncategorizedTransactionTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@@ -39,6 +40,18 @@ export class GetExcludedBankTransactionsService {
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);

View File

@@ -4,6 +4,10 @@ export interface ExcludedBankTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
minDate?: Date;
maxDate?: Date;
minAmount?: number;
maxAmount?: number;
}
export interface IBankTransactionUnexcludingEventPayload {

View File

@@ -64,7 +64,7 @@ export class GetMatchedTransactions {
.whereIn('id', uncategorizedTransactionIds)
.throwIfNotFound();
const totalPending = Math.abs(sumBy(uncategorizedTransactions, 'amount'));
const totalPending = sumBy(uncategorizedTransactions, 'amount');
const filtered = filter.transactionType
? this.registered.filter((item) => item.type === filter.transactionType)

View File

@@ -1,18 +1,25 @@
import { Inject, Service } from 'typedi';
import { initialize } from 'objection';
import { Knex } from 'knex';
import { first } from 'lodash';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetMatchedTransactionBillsTransformer } from './GetMatchedTransactionBillsTransformer';
import { GetMatchedTransactionsFilter, MatchedTransactionPOJO } from './types';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import {
GetMatchedTransactionsFilter,
IMatchTransactionDTO,
MatchedTransactionPOJO,
} from './types';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { CreateBillPayment } from '@/services/Purchases/BillPayments/CreateBillPayment';
import { IBillPaymentDTO } from '@/interfaces';
@Service()
export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType {
@Inject()
private tenancy: HasTenancyService;
private transformer: TransformerInjectable;
@Inject()
private transformer: TransformerInjectable;
private createPaymentMadeService: CreateBillPayment;
/**
* Retrieves the matched transactions.
@@ -71,4 +78,62 @@ export class GetMatchedTransactionsByBills extends GetMatchedTransactionsByType
new GetMatchedTransactionBillsTransformer()
);
}
/**
* Creates the common matched transaction.
* @param {number} tenantId
* @param {Array<number>} uncategorizedTransactionIds
* @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx
*/
public async createMatchedTransaction(
tenantId: number,
uncategorizedTransactionIds: Array<number>,
matchTransactionDTO: IMatchTransactionDTO,
trx?: Knex.Transaction
): Promise<void> {
await super.createMatchedTransaction(
tenantId,
uncategorizedTransactionIds,
matchTransactionDTO,
trx
);
const { Bill, UncategorizedCashflowTransaction, MatchedBankTransaction } =
this.tenancy.models(tenantId);
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
const bill = await Bill.query(trx)
.findById(matchTransactionDTO.referenceId)
.throwIfNotFound();
const createPaymentMadeDTO: IBillPaymentDTO = {
vendorId: bill.vendorId,
paymentAccountId: uncategorizedTransaction.accountId,
paymentDate: uncategorizedTransaction.date,
exchangeRate: 1,
entries: [
{
paymentAmount: bill.dueAmount,
billId: bill.id,
},
],
branchId: bill.branchId,
};
// Create a new bill payment associated to the matched bill.
const billPayment = await this.createPaymentMadeService.createBillPayment(
tenantId,
createPaymentMadeDTO,
trx
);
// Link the create bill payment with matched transaction.
await super.createMatchedTransaction(tenantId, uncategorizedTransactionIds, {
referenceType: 'BillPayment',
referenceId: billPayment.id,
}, trx);
}
}

View File

@@ -1,22 +1,26 @@
import { Inject, Service } from 'typedi';
import { initialize } from 'objection';
import { Knex } from 'knex';
import { first } from 'lodash';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetMatchedTransactionInvoicesTransformer } from './GetMatchedTransactionInvoicesTransformer';
import {
GetMatchedTransactionsFilter,
IMatchTransactionDTO,
MatchedTransactionPOJO,
MatchedTransactionsPOJO,
} from './types';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GetMatchedTransactionsByType } from './GetMatchedTransactionsByType';
import { CreatePaymentReceived } from '@/services/Sales/PaymentReceived/CreatePaymentReceived';
import { IPaymentReceivedCreateDTO } from '@/interfaces';
@Service()
export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByType {
@Inject()
protected tenancy: HasTenancyService;
protected transformer: TransformerInjectable;
@Inject()
protected transformer: TransformerInjectable;
protected createPaymentReceivedService: CreatePaymentReceived;
/**
* Retrieves the matched transactions.
@@ -78,4 +82,64 @@ export class GetMatchedTransactionsByInvoices extends GetMatchedTransactionsByTy
new GetMatchedTransactionInvoicesTransformer()
);
}
/**
* Creates the common matched transaction.
* @param {number} tenantId
* @param {Array<number>} uncategorizedTransactionIds
* @param {IMatchTransactionDTO} matchTransactionDTO
* @param {Knex.Transaction} trx
*/
public async createMatchedTransaction(
tenantId: number,
uncategorizedTransactionIds: Array<number>,
matchTransactionDTO: IMatchTransactionDTO,
trx?: Knex.Transaction
) {
await super.createMatchedTransaction(
tenantId,
uncategorizedTransactionIds,
matchTransactionDTO,
trx
);
const { SaleInvoice, UncategorizedCashflowTransaction, MatchedBankTransaction } =
this.tenancy.models(tenantId);
const uncategorizedTransactionId = first(uncategorizedTransactionIds);
const uncategorizedTransaction =
await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
const invoice = await SaleInvoice.query(trx)
.findById(matchTransactionDTO.referenceId)
.throwIfNotFound();
const createPaymentReceivedDTO: IPaymentReceivedCreateDTO = {
customerId: invoice.customerId,
paymentDate: uncategorizedTransaction.date,
amount: invoice.dueAmount,
depositAccountId: uncategorizedTransaction.accountId,
entries: [
{
index: 1,
invoiceId: invoice.id,
paymentAmount: invoice.dueAmount,
},
],
branchId: invoice.branchId,
};
// Create a payment received associated to the matched invoice.
const paymentReceived = await this.createPaymentReceivedService.createPaymentReceived(
tenantId,
createPaymentReceivedDTO,
{},
trx
);
// Link the create payment received with matched invoice transaction.
await super.createMatchedTransaction(tenantId, uncategorizedTransactionIds, {
referenceType: 'PaymentReceive',
referenceId: paymentReceived.id,
}, trx)
}
}

View File

@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi';
import {
IBillPaymentEventDeletedPayload,
IPaymentReceiveDeletedPayload,
IPaymentReceivedDeletedPayload,
} from '@/interfaces';
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
import events from '@/subscribers/events';
@@ -23,7 +23,7 @@ export class ValidateMatchingOnPaymentMadeDelete {
/**
* Validates the payment made transaction whether matched with bank transaction on deleting.
* @param {IPaymentReceiveDeletedPayload}
* @param {IPaymentReceivedDeletedPayload}
*/
public async validateMatchingOnPaymentMadeDeleting({
tenantId,

View File

@@ -1,5 +1,5 @@
import { Inject, Service } from 'typedi';
import { IPaymentReceiveDeletedPayload } from '@/interfaces';
import { IPaymentReceivedDeletedPayload } from '@/interfaces';
import { ValidateTransactionMatched } from '../ValidateTransactionsMatched';
import events from '@/subscribers/events';
@@ -20,13 +20,13 @@ export class ValidateMatchingOnPaymentReceivedDelete {
/**
* Validates the payment received transaction whether matched with bank transaction on deleting.
* @param {IPaymentReceiveDeletedPayload}
* @param {IPaymentReceivedDeletedPayload}
*/
public async validateMatchingOnPaymentReceivedDeleting({
tenantId,
oldPaymentReceive,
trx,
}: IPaymentReceiveDeletedPayload) {
}: IPaymentReceivedDeletedPayload) {
await this.validateNoMatchingLinkedService.validateTransactionNoMatchLinking(
tenantId,
'PaymentReceive',

View File

@@ -25,6 +25,7 @@ import { Knex } from 'knex';
import uniqid from 'uniqid';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { RemovePendingUncategorizedTransaction } from '@/services/Cashflow/RemovePendingUncategorizedTransaction';
const CONCURRENCY_ASYNC = 10;
@@ -40,7 +41,7 @@ export class PlaidSyncDb {
private cashflowApp: CashflowApplication;
@Inject()
private deleteCashflowTransactionService: DeleteCashflowTransaction;
private removePendingTransaction: RemovePendingUncategorizedTransaction;
@Inject()
private eventPublisher: EventPublisher;
@@ -185,21 +186,22 @@ export class PlaidSyncDb {
plaidTransactionsIds: string[],
trx?: Knex.Transaction
) {
const { CashflowTransaction } = this.tenancy.models(tenantId);
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
'plaidTransactionId',
plaidTransactionsIds
);
const cashflowTransactionsIds = cashflowTransactions.map(
const uncategorizedTransactions =
await UncategorizedCashflowTransaction.query(trx).whereIn(
'plaidTransactionId',
plaidTransactionsIds
);
const uncategorizedTransactionsIds = uncategorizedTransactions.map(
(trans) => trans.id
);
await bluebird.map(
cashflowTransactionsIds,
(transactionId: number) =>
this.deleteCashflowTransactionService.deleteCashflowTransaction(
uncategorizedTransactionsIds,
(uncategorizedTransactionId: number) =>
this.removePendingTransaction.removePendingTransaction(
tenantId,
transactionId,
uncategorizedTransactionId,
trx
),
{ concurrency: CONCURRENCY_ASYNC }

View File

@@ -1,10 +1,10 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
import { PlaidSyncDb } from './PlaidSyncDB';
import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
import UnitOfWork from '@/services/UnitOfWork';
import { Knex } from 'knex';
@Service()
export class PlaidUpdateTransactions {
@@ -19,9 +19,9 @@ export class PlaidUpdateTransactions {
/**
* Handles sync the Plaid item to Bigcaptial under UOW.
* @param {number} tenantId
* @param {number} plaidItemId
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
* @param {number} tenantId - Tenant id.
* @param {number} plaidItemId - Plaid item id.
* @returns {Promise<{ addedCount: number; modifiedCount: number; removedCount: number; }>}
*/
public async updateTransactions(tenantId: number, plaidItemId: string) {
return this.uow.withTransaction(tenantId, (trx: Knex.Transaction) => {
@@ -73,6 +73,12 @@ export class PlaidUpdateTransactions {
item,
trx
);
// Sync removed transactions.
await this.plaidSync.syncRemoveTransactions(
tenantId,
removed?.map((r) => r.transaction_id),
trx
);
// Sync bank account transactions.
await this.plaidSync.syncAccountsTransactions(
tenantId,

View File

@@ -3,11 +3,11 @@ import {
Item as PlaidItem,
Institution as PlaidInstitution,
AccountBase as PlaidAccount,
TransactionBase as PlaidTransactionBase,
} from 'plaid';
import {
CreateUncategorizedTransactionDTO,
IAccountCreateDTO,
PlaidTransaction,
} from '@/interfaces';
/**
@@ -48,7 +48,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
export const transformPlaidTrxsToCashflowCreate = R.curry(
(
cashflowAccountId: number,
plaidTranasction: PlaidTransaction
plaidTranasction: PlaidTransactionBase
): CreateUncategorizedTransactionDTO => {
return {
date: plaidTranasction.date,
@@ -64,6 +64,8 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
accountId: cashflowAccountId,
referenceNo: plaidTranasction.payment_meta?.reference_number,
plaidTransactionId: plaidTranasction.transaction_id,
pending: plaidTranasction.pending,
pendingPlaidTransactionId: plaidTranasction.pending_transaction_id,
};
}
);

View File

@@ -33,17 +33,29 @@ const matchNumberCondition = (
transaction: UncategorizedCashflowTransaction,
condition: IBankRuleCondition
) => {
const conditionValue = parseFloat(condition.value);
const transactionAmount =
condition.field === 'amount'
? Math.abs(transaction[condition.field])
: (transaction[condition.field] as unknown as number);
switch (condition.comparator) {
case BankRuleConditionComparator.Equals:
return transaction[condition.field] === condition.value;
case BankRuleConditionComparator.Contains:
return transaction[condition.field]
?.toString()
.includes(condition.value.toString());
case BankRuleConditionComparator.NotContain:
return !transaction[condition.field]
?.toString()
.includes(condition.value.toString());
case BankRuleConditionComparator.Equal:
return transactionAmount === conditionValue;
case BankRuleConditionComparator.BiggerOrEqual:
return transactionAmount >= conditionValue;
case BankRuleConditionComparator.Bigger:
return transactionAmount > conditionValue;
case BankRuleConditionComparator.Smaller:
return transactionAmount < conditionValue;
case BankRuleConditionComparator.SmallerOrEqual:
return transactionAmount <= conditionValue;
default:
return false;
}
@@ -53,18 +65,19 @@ const matchTextCondition = (
transaction: UncategorizedCashflowTransaction,
condition: IBankRuleCondition
): boolean => {
const transactionValue = transaction[condition.field] as string;
switch (condition.comparator) {
case BankRuleConditionComparator.Equals:
return transaction[condition.field] === condition.value;
case BankRuleConditionComparator.Equal:
return transactionValue === condition.value;
case BankRuleConditionComparator.Contains:
const fieldValue = lowerCase(transaction[condition.field]);
const fieldValue = lowerCase(transactionValue);
const conditionValue = lowerCase(condition.value);
return fieldValue.includes(conditionValue);
case BankRuleConditionComparator.NotContain:
return !transaction[condition.field]?.includes(
condition.value.toString()
);
return !transactionValue?.includes(condition.value.toString());
default:
return false;
}
@@ -101,8 +114,8 @@ const determineFieldType = (field: string): string => {
case 'amount':
return 'number';
case 'description':
return 'text';
case 'payee':
default:
return 'unknown';
return 'text';
}
};
};

View File

@@ -1,4 +1,5 @@
import { Inject, Service } from 'typedi';
import { isEqual, omit } from 'lodash';
import events from '@/subscribers/events';
import {
IBankRuleEventCreatedPayload,
@@ -55,10 +56,22 @@ export class TriggerRecognizedTransactions {
private async recognizedTransactionsOnRuleEdited({
tenantId,
editRuleDTO,
oldBankRule,
bankRule,
ruleId,
}: IBankRuleEventEditedPayload) {
const payload = { tenantId, ruleId };
// 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
@@ -87,12 +100,14 @@ export class TriggerRecognizedTransactions {
private async triggerRecognizeTransactionsOnImportCommitted({
tenantId,
importId,
meta,
}: IImportFileCommitedEventPayload) {
const importFile = await Import.query().findOne({ importId });
const batch = importFile.paramsParsed.batch;
const payload = { tenantId, 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

@@ -26,31 +26,39 @@ export class DeleteBankRuleSerivce {
* @param {number} ruleId
* @returns {Promise<void>}
*/
public async deleteBankRule(tenantId: number, ruleId: number): Promise<void> {
public async deleteBankRule(
tenantId: number,
ruleId: number,
trx?: Knex.Transaction
): Promise<void> {
const { BankRule, BankRuleCondition } = this.tenancy.models(tenantId);
const oldBankRule = await BankRule.query()
.findById(ruleId)
.throwIfNotFound();
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBankRuleDeleting` event.
await this.eventPublisher.emitAsync(events.bankRules.onDeleting, {
tenantId,
oldBankRule,
ruleId,
trx,
} as IBankRuleEventDeletingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onBankRuleDeleting` event.
await this.eventPublisher.emitAsync(events.bankRules.onDeleting, {
tenantId,
oldBankRule,
ruleId,
trx,
} as IBankRuleEventDeletingPayload);
await BankRuleCondition.query(trx).where('ruleId', ruleId).delete();
await BankRule.query(trx).findById(ruleId).delete();
await BankRuleCondition.query(trx).where('ruleId', ruleId).delete()
await BankRule.query(trx).findById(ruleId).delete();
// Triggers `onBankRuleDeleted` event.
await await this.eventPublisher.emitAsync(events.bankRules.onDeleted, {
tenantId,
ruleId,
trx,
} as IBankRuleEventDeletedPayload);
});
// Triggers `onBankRuleDeleted` event.
await await this.eventPublisher.emitAsync(events.bankRules.onDeleted, {
tenantId,
ruleId,
trx,
} as IBankRuleEventDeletedPayload);
},
trx
);
}
}

View File

@@ -0,0 +1,34 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool';
import { castArray, uniq } from 'lodash';
import { DeleteBankRuleSerivce } from './DeleteBankRule';
@Service()
export class DeleteBankRulesService {
@Inject()
private deleteBankRuleService: DeleteBankRuleSerivce;
/**
* Delete bank rules.
* @param {number} tenantId
* @param {number | Array<number>} bankRuleId
*/
async deleteBankRules(
tenantId: number,
bankRuleId: number | Array<number>,
trx?: Knex.Transaction
) {
const bankRulesIds = uniq(castArray(bankRuleId));
const results = await PromisePool.withConcurrency(1)
.for(bankRulesIds)
.process(async (bankRuleId: number) => {
await this.deleteBankRuleService.deleteBankRule(
tenantId,
bankRuleId,
trx
);
});
}
}

View File

@@ -47,6 +47,7 @@ export class EditBankRuleService {
const oldBankRule = await BankRule.query()
.findById(ruleId)
.withGraphFetched('conditions')
.throwIfNotFound();
const tranformDTO = this.transformDTO(editRuleDTO);
@@ -64,15 +65,15 @@ export class EditBankRuleService {
} as IBankRuleEventEditingPayload);
// Updates the given bank rule.
await BankRule.query(trx).upsertGraphAndFetch({
const bankRule = await BankRule.query(trx).upsertGraphAndFetch({
...tranformDTO,
id: ruleId,
});
// Triggers `onBankRuleEdited` event.
await this.eventPublisher.emitAsync(events.bankRules.onEdited, {
tenantId,
oldBankRule,
bankRule,
ruleId,
editRuleDTO,
trx,

View File

@@ -1,15 +1,20 @@
import { Knex } from 'knex';
export enum BankRuleConditionField {
Amount = 'Amount',
Description = 'Description',
Payee = 'Payee',
Amount = 'amount',
Description = 'description',
Payee = 'payee',
}
export enum BankRuleConditionComparator {
Contains = 'contains',
Equals = 'equals',
Equal = 'equal',
NotContain = 'not_contain',
Bigger = 'bigger',
BiggerOrEqual = 'bigger_or_equal',
Smaller = 'smaller',
SmallerOrEqual = 'smaller_or_equal',
}
export interface IBankRuleCondition {
@@ -56,7 +61,15 @@ export enum BankRuleAssignCategory {
export interface IBankRuleConditionDTO {
id?: number;
field: string;
comparator: string;
comparator:
| 'contains'
| 'equals'
| 'not_contains'
| 'equal'
| 'bigger'
| 'bigger_or_equal'
| 'smaller'
| 'smaller_or_equal';
value: number;
}
@@ -99,6 +112,8 @@ export interface IBankRuleEventEditingPayload {
export interface IBankRuleEventEditedPayload {
tenantId: number;
ruleId: number;
oldBankRule: IBankRule;
bankRule: IBankRule;
editRuleDTO: IEditBankRuleDTO;
trx?: Knex.Transaction;
}

View File

@@ -1,8 +1,8 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
IPaymentReceiveCreatingPayload,
IPaymentReceiveEditingPayload,
IPaymentReceivedCreatingPayload,
IPaymentReceivedEditingPayload,
} from '@/interfaces';
import { ValidateBranchExistance } from '../../Integrations/ValidateBranchExistance';
@@ -28,12 +28,12 @@ export class PaymentReceiveBranchValidateSubscriber {
/**
* Validate branch existance on estimate creating.
* @param {IPaymentReceiveCreatingPayload} payload
* @param {IPaymentReceivedCreatingPayload} payload
*/
private validateBranchExistanceOnPaymentCreating = async ({
tenantId,
paymentReceiveDTO,
}: IPaymentReceiveCreatingPayload) => {
}: IPaymentReceivedCreatingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
paymentReceiveDTO.branchId
@@ -42,12 +42,12 @@ export class PaymentReceiveBranchValidateSubscriber {
/**
* Validate branch existance once estimate editing.
* @param {IPaymentReceiveEditingPayload} payload
* @param {IPaymentReceivedEditingPayload} payload
*/
private validateBranchExistanceOnPaymentEditing = async ({
paymentReceiveDTO,
tenantId,
}: IPaymentReceiveEditingPayload) => {
}: IPaymentReceivedEditingPayload) => {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
tenantId,
paymentReceiveDTO.branchId

View File

@@ -8,7 +8,12 @@ export class CashflowAccountTransformer extends Transformer {
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return ['formattedAmount'];
return [
'formattedAmount',
'lastFeedsUpdatedAt',
'lastFeedsUpdatedAtFormatted',
'lastFeedsUpdatedFromNow',
];
};
/**
@@ -29,7 +34,7 @@ export class CashflowAccountTransformer extends Transformer {
/**
* Retrieve formatted account amount.
* @param {IAccount} invoice
* @param {IAccount} invoice
* @returns {string}
*/
protected formattedAmount = (account: IAccount): string => {
@@ -37,4 +42,22 @@ export class CashflowAccountTransformer extends Transformer {
currencyCode: account.currencyCode,
});
};
/**
* Retrieves the last feeds update at formatted date.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedAtFormatted(account: IAccount): string {
return this.formatDate(account.lastFeedsUpdatedAt);
}
/**
* Retrieves the last feeds updated from now.
* @param {IAccount} account
* @returns {string}
*/
protected lastFeedsUpdatedFromNow(account: IAccount): string {
return this.formatDateFromNow(account.lastFeedsUpdatedAt);
}
}

View File

@@ -21,6 +21,7 @@ import GetCashflowAccountsService from './GetCashflowAccountsService';
import { GetCashflowTransactionService } from './GetCashflowTransactionsService';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
import { GetRecognizedTransactionService } from './GetRecognizedTransaction';
import { UncategorizeCashflowTransactionsBulk } from './UncategorizeCashflowTransactionsBulk';
@Service()
export class CashflowApplication {
@@ -39,6 +40,9 @@ export class CashflowApplication {
@Inject()
private uncategorizeTransactionService: UncategorizeCashflowTransaction;
@Inject()
private uncategorizeTransasctionsService: UncategorizeCashflowTransactionsBulk;
@Inject()
private categorizeTransactionService: CategorizeCashflowTransaction;
@@ -155,6 +159,22 @@ export class CashflowApplication {
);
}
/**
* Uncategorize the given transactions in bulk.
* @param {number} tenantId
* @param {number | Array<number>} transactionId
* @returns
*/
public uncategorizeTransactions(
tenantId: number,
transactionId: number | Array<number>
) {
return this.uncategorizeTransasctionsService.uncategorizeBulk(
tenantId,
transactionId
);
}
/**
* Categorize the given cashflow transaction.
* @param {number} tenantId
@@ -241,9 +261,9 @@ export class CashflowApplication {
/**
* Retrieves the recognized transaction of the given uncategorized transaction.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @returns
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
* @returns
*/
public getRecognizedTransaction(
tenantId: number,

View File

@@ -0,0 +1,53 @@
import { Inject } from 'typedi';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '../Tenancy/TenancyService';
import { GetPendingBankAccountTransactionTransformer } from './GetPendingBankAccountTransactionTransformer';
export class GetPendingBankAccountTransactions {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the given bank accounts pending transaction.
* @param {number} tenantId - Tenant id.
* @param {GetPendingTransactionsQuery} filter - Pending transactions query.
*/
async getPendingTransactions(
tenantId: number,
filter?: GetPendingTransactionsQuery
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const _filter = {
page: 1,
pageSize: 20,
...filter,
};
const { results, pagination } =
await UncategorizedCashflowTransaction.query()
.onBuild((q) => {
q.modify('pending');
if (_filter?.accountId) {
q.where('accountId', _filter.accountId);
}
})
.pagination(_filter.page - 1, _filter.pageSize);
const data = await this.transformer.transform(
tenantId,
results,
new GetPendingBankAccountTransactionTransformer()
);
return { data, pagination };
}
}
interface GetPendingTransactionsQuery {
page?: number;
pageSize?: number;
accountId?: number;
}

View File

@@ -0,0 +1,73 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from '@/utils';
export class GetPendingBankAccountTransactionTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {string[]}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedDate',
'formattedDepositAmount',
'formattedWithdrawalAmount',
];
};
/**
* Exclude all attributes.
* @returns {Array<string>}
*/
public excludeAttributes = (): string[] => {
return [];
};
/**
* Formattes the transaction date.
* @param transaction
* @returns {string}
*/
public formattedDate(transaction) {
return this.formatDate(transaction.date);
}
/**
* Formatted amount.
* @param transaction
* @returns {string}
*/
public formattedAmount(transaction) {
return formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode,
});
}
/**
* Formatted deposit amount.
* @param transaction
* @returns {string}
*/
protected formattedDepositAmount(transaction) {
if (transaction.isDepositTransaction) {
return formatNumber(transaction.deposit, {
currencyCode: transaction.currencyCode,
});
}
return '';
}
/**
* Formatted withdrawal amount.
* @param transaction
* @returns {string}
*/
protected formattedWithdrawalAmount(transaction) {
if (transaction.isWithdrawalTransaction) {
return formatNumber(transaction.withdrawal, {
currencyCode: transaction.currencyCode,
});
}
return '';
}
}

View File

@@ -23,7 +23,7 @@ export class GetRecognizedTransactionsService {
) {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const _filter = {
const _query = {
page: 1,
pageSize: 20,
...filter,
@@ -34,13 +34,33 @@ export class GetRecognizedTransactionsService {
q.withGraphFetched('recognizedTransaction.assignAccount');
q.withGraphFetched('recognizedTransaction.bankRule');
q.whereNotNull('recognizedTransactionId');
// Exclude the excluded transactions.
q.modify('notExcluded');
if (_filter.accountId) {
q.where('accountId', _filter.accountId);
// 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(_filter.page - 1, _filter.pageSize);
.pagination(_query.page - 1, _query.pageSize);
const data = await this.transformer.transform(
tenantId,

View File

@@ -51,7 +51,9 @@ export class GetUncategorizedTransactions {
.onBuild((q) => {
q.where('accountId', accountId);
q.where('categorized', false);
q.modify('notExcluded');
q.modify('notPending');
q.withGraphFetched('account');
q.withGraphFetched('recognizedTransaction.assignAccount');
@@ -60,6 +62,19 @@ export class GetUncategorizedTransactions {
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);

View File

@@ -0,0 +1,72 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants';
import {
IPendingTransactionRemovedEventPayload,
IPendingTransactionRemovingEventPayload,
} from '@/interfaces';
@Service()
export class RemovePendingUncategorizedTransaction {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* REmoves the pending uncategorized transaction.
* @param {number} tenantId -
* @param {number} uncategorizedTransactionId -
* @param {Knex.Transaction} trx -
* @returns {Promise<void>}
*/
public async removePendingTransaction(
tenantId: number,
uncategorizedTransactionId: number,
trx?: Knex.Transaction
): Promise<void> {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const pendingTransaction = await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.throwIfNotFound();
if (!pendingTransaction.isPending) {
throw new ServiceError(ERRORS.TRANSACTION_NOT_PENDING);
}
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
await this.eventPublisher.emitAsync(
events.bankTransactions.onPendingRemoving,
{
tenantId,
uncategorizedTransactionId,
pendingTransaction,
trx,
} as IPendingTransactionRemovingEventPayload
);
// Removes the pending uncategorized transaction.
await UncategorizedCashflowTransaction.query(trx)
.findById(uncategorizedTransactionId)
.delete();
await this.eventPublisher.emitAsync(
events.bankTransactions.onPendingRemoved,
{
tenantId,
uncategorizedTransactionId,
pendingTransaction,
trx,
} as IPendingTransactionRemovedEventPayload
);
});
}
}

View File

@@ -0,0 +1,37 @@
import PromisePool from '@supercharge/promise-pool';
import { castArray } from 'lodash';
import { Service, Inject } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
@Service()
export class UncategorizeCashflowTransactionsBulk {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uncategorizeTransaction: UncategorizeCashflowTransaction;
/**
* Uncategorize the given bank transactions in bulk.
* @param {number} tenantId
* @param {number} uncategorizedTransactionId
*/
public async uncategorizeBulk(
tenantId: number,
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.uncategorizeTransaction.uncategorize(
tenantId,
_uncategorizedTransactionId
);
});
}
}
const MIGRATION_CONCURRENCY = 1;

View File

@@ -87,7 +87,7 @@ export class UncategorizedTransactionsImportable extends Importable {
}
/**
* Transformes the import params before storing them.
* Transforms the import params before storing them.
* @param {Record<string, any>} parmas
*/
public transformParams(parmas: Record<string, any>) {

View File

@@ -15,10 +15,10 @@ export const ERRORS = {
'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'
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION:
'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION',
TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED',
TRANSACTION_NOT_PENDING: 'TRANSACTION_NOT_PENDING',
};
export enum CASHFLOW_DIRECTION {

View File

@@ -1,11 +1,11 @@
import { Inject, Service } from 'typedi';
import PromisePool from '@supercharge/promise-pool';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import {
ICashflowTransactionCategorizedPayload,
ICashflowTransactionUncategorizedPayload,
} from '@/interfaces';
import PromisePool from '@supercharge/promise-pool';
@Service()
export class DecrementUncategorizedTransactionOnCategorize {
@@ -36,13 +36,17 @@ export class DecrementUncategorizedTransactionOnCategorize {
public async decrementUnCategorizedTransactionsOnCategorized({
tenantId,
uncategorizedTransactions,
trx
trx,
}: ICashflowTransactionCategorizedPayload) {
const { Account } = this.tenancy.models(tenantId);
await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions)
.process(async (uncategorizedTransaction) => {
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) {
return;
}
await Account.query(trx)
.findById(uncategorizedTransaction.accountId)
.decrement('uncategorizedTransactions', 1);
@@ -56,13 +60,17 @@ export class DecrementUncategorizedTransactionOnCategorize {
public async incrementUnCategorizedTransactionsOnUncategorized({
tenantId,
uncategorizedTransactions,
trx
trx,
}: ICashflowTransactionUncategorizedPayload) {
const { Account } = this.tenancy.models(tenantId);
await PromisePool.withConcurrency(1)
.for(uncategorizedTransactions)
.process(async (uncategorizedTransaction) => {
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) {
return;
}
await Account.query(trx)
.findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1);
@@ -82,6 +90,9 @@ export class DecrementUncategorizedTransactionOnCategorize {
if (!uncategorizedTransaction.accountId) return;
// Cannot continue if the transaction is still pending.
if (uncategorizedTransaction.isPending) return;
await Account.query(trx)
.findById(uncategorizedTransaction.accountId)
.increment('uncategorizedTransactions', 1);

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { CustomersApplication } from './CustomersApplication';
import { Exportable } from '@/services/Export/Exportable';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class CustomersExportable extends Exportable {
@@ -17,9 +18,9 @@ export class CustomersExportable extends Exportable {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter;
return this.customersApplication

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { VendorsApplication } from './VendorsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
@Service()
export class VendorsExportable extends Exportable {
@@ -17,9 +18,9 @@ export class VendorsExportable extends Exportable {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
page: 1,
pageSize: EXPORT_SIZE_LIMIT,
} as IItemsFilter;
return this.vendorsApplication

View File

@@ -9,7 +9,7 @@ import {
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import UnitOfWork from '@/services/UnitOfWork';
import { PaymentReceiveValidators } from '../Sales/PaymentReceives/PaymentReceiveValidators';
import { PaymentReceivedValidators } from '../Sales/PaymentReceived/PaymentReceivedValidators';
import BaseCreditNotes from './CreditNotes';
import {
IApplyCreditToInvoicesDTO,
@@ -26,7 +26,7 @@ export default class CreditNoteApplyToInvoices extends BaseCreditNotes {
private tenancy: HasTenancyService;
@Inject()
private paymentReceiveValidators: PaymentReceiveValidators;
private paymentReceiveValidators: PaymentReceivedValidators;
@Inject()
private uow: UnitOfWork;

View File

@@ -16,6 +16,7 @@ export class ExpenseTransfromer extends Transformer {
'formattedAllocatedCostAmount',
'formattedDate',
'formattedCreatedAt',
'formattedPublishedAt',
'categories',
'attachments',
];
@@ -91,4 +92,13 @@ export class ExpenseTransfromer extends Transformer {
protected attachments = (expense: IExpense) => {
return this.item(expense.attachments, new AttachmentTransformer());
};
/**
* Retrieve formatted published at date.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedPublishedAt = (expense: IExpense): string => {
return this.formatDate(expense.publishedAt);
}
}

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IExpensesFilter } from '@/interfaces';
import { ExpensesApplication } from './ExpensesApplication';
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
@Service()
export class ExpensesExportable extends Exportable {
@@ -19,7 +20,7 @@ export class ExpensesExportable extends Exportable {
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
pageSize: EXPORT_SIZE_LIMIT,
} as IExpensesFilter;
return this.expensesApplication

View File

@@ -9,12 +9,13 @@ import { SaleInvoicesExportable } from '../Sales/Invoices/SaleInvoicesExportable
import { SaleEstimatesExportable } from '../Sales/Estimates/SaleEstimatesExportable';
import { SaleReceiptsExportable } from '../Sales/Receipts/SaleReceiptsExportable';
import { BillsExportable } from '../Purchases/Bills/BillsExportable';
import { PaymentsReceivedExportable } from '../Sales/PaymentReceives/PaymentsReceivedExportable';
import { PaymentsReceivedExportable } from '../Sales/PaymentReceived/PaymentsReceivedExportable';
import { BillPaymentExportable } from '../Purchases/BillPayments/BillPaymentExportable';
import { ManualJournalsExportable } from '../ManualJournals/ManualJournalExportable';
import { CreditNotesExportable } from '../CreditNotes/CreditNotesExportable';
import { VendorCreditsExportable } from '../Purchases/VendorCredits/VendorCreditsExportable';
import { ItemCategoriesExportable } from '../ItemCategories/ItemCategoriesExportable';
import { TaxRatesExportable } from '../TaxRates/TaxRatesExportable';
@Service()
export class ExportableResources {
@@ -46,6 +47,7 @@ export class ExportableResources {
{ resource: 'ManualJournal', exportable: ManualJournalsExportable },
{ resource: 'CreditNote', exportable: CreditNotesExportable },
{ resource: 'VendorCredit', exportable: VendorCreditsExportable },
{ resource: 'TaxRate', exportable: TaxRatesExportable },
];
/**

View File

@@ -0,0 +1 @@
export const EXPORT_SIZE_LIMIT = 9999999;

View File

@@ -15,6 +15,7 @@ import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { flatToNestedArray } from '@/utils';
import Ledger from '@/services/Accounting/Ledger';
import { calculateRunningBalance } from './_utils';
import { getTransactionTypeLabel } from '@/utils/transactions-types';
/**
* General ledger sheet.
@@ -90,11 +91,13 @@ export default class GeneralLedgerSheet extends R.compose(
date: entry.date,
dateFormatted: moment(entry.date).format('YYYY MMM DD'),
transactionNumber: entry.transactionNumber,
referenceType: entry.referenceType,
referenceId: entry.referenceId,
referenceTypeFormatted: this.i18n.__(entry.referenceTypeFormatted),
referenceType: entry.transactionType,
referenceId: entry.transactionId,
transactionNumber: entry.transactionNumber,
transactionTypeFormatted: this.i18n.__(
getTransactionTypeLabel(entry.transactionType, entry.transactionSubType)
),
contactName: get(contact, 'displayName'),
contactType: get(contact, 'contactService'),

View File

@@ -67,7 +67,7 @@ export class GeneralLedgerTable extends R.compose(
return [
{ key: 'date', accessor: 'dateFormatted' },
{ key: 'account_name', accessor: 'account.name' },
{ key: 'reference_type', accessor: 'referenceTypeFormatted' },
{ key: 'reference_type', accessor: 'transactionTypeFormatted' },
{ key: 'reference_number', accessor: 'transactionNumber' },
{ key: 'description', accessor: 'note' },
{ key: 'credit', accessor: 'formattedCredit' },

View File

@@ -0,0 +1,104 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ImportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), callback);
}
/**
* Runs a callback function in preview mode within the AsyncLocalStorage context.
* @param callback The function to be executed in preview mode.
* @returns The result of the callback function.
*/
public runPreview<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportPreview();
return callback();
});
}
/**
* Runs a callback function in commit mode within the AsyncLocalStorage context.
* @param {() => T} callback - The function to be executed in commit mode.
* @returns {T} The result of the callback function.
*/
public runCommit<T>(callback: () => T): T {
return this.run(() => {
this.markAsImport();
this.markAsImportCommit();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an import operation.
* @param flag Boolean flag to set or unset the import status. Defaults to true.
*/
public markAsImport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImport', flag);
}
/**
* Marks the current context as an import commit operation.
* @param flag Boolean flag to set or unset the import commit status. Defaults to true.
*/
public markAsImportCommit(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportCommit', flag);
}
/**
* Marks the current context as an import preview operation.
* @param {boolean} flag - Boolean flag to set or unset the import preview status. Defaults to true.
*/
public markAsImportPreview(flag: boolean = true): void {
const store = this.getStore();
store?.set('isImportPreview', flag);
}
/**
* Checks if the current context is an import operation.
* @returns {boolean} True if the context is an import operation, false otherwise.
*/
public isImport(): boolean {
return !!this.getStore()?.get('isImport');
}
/**
* Checks if the current context is an import commit operation.
* @returns {boolean} True if the context is an import commit operation, false otherwise.
*/
public isImportCommit(): boolean {
return !!this.getStore()?.get('isImportCommit');
}
/**
* Checks if the current context is an import preview operation.
* @returns {boolean} True if the context is an import preview operation, false otherwise.
*/
public isImportPreview(): boolean {
return !!this.getStore()?.get('isImportPreview');
}
}

View File

@@ -12,7 +12,11 @@ import {
ImportableContext,
} from './interfaces';
import { ServiceError } from '@/exceptions';
import { getUniqueImportableValue, trimObject } from './_utils';
import {
convertMappingsToObject,
getUniqueImportableValue,
trimObject,
} from './_utils';
import { ImportableResources } from './ImportableResources';
import ResourceService from '../Resource/ResourceService';
import { Import } from '@/system/models';
@@ -43,7 +47,6 @@ export class ImportFileCommon {
return XLSX.utils.sheet_to_json(worksheet, {});
}
/**
* Imports the given parsed data to the resource storage through registered importable service.
* @param {number} tenantId -
@@ -82,11 +85,14 @@ export class ImportFileCommon {
rowNumber,
uniqueValue,
};
const mappingSettings = convertMappingsToObject(importFile.columnsParsed);
try {
// Validate the DTO object before passing it to the service layer.
await this.importFileValidator.validateData(
resourceFields,
transformedDTO
transformedDTO,
mappingSettings
);
try {
// Run the importable function and listen to the errors.

View File

@@ -1,5 +1,9 @@
import { Service } from 'typedi';
import { ImportInsertError, ResourceMetaFieldsMap } from './interfaces';
import {
ImportInsertError,
ImportMappingAttr,
ResourceMetaFieldsMap,
} from './interfaces';
import { ERRORS, convertFieldsToYupValidation } from './_utils';
import { IModelMeta } from '@/interfaces';
import { ServiceError } from '@/exceptions';
@@ -24,9 +28,13 @@ export class ImportFileDataValidator {
*/
public async validateData(
importableFields: ResourceMetaFieldsMap,
data: Record<string, any>
data: Record<string, any>,
mappingSettings: Record<string, ImportMappingAttr> = {}
): Promise<void | ImportInsertError[]> {
const YupSchema = convertFieldsToYupValidation(importableFields);
const YupSchema = convertFieldsToYupValidation(
importableFields,
mappingSettings
);
const _data = { ...data };
try {

View File

@@ -23,7 +23,7 @@ export class ImportFileMapping {
*/
public async mapping(
tenantId: number,
importId: number,
importId: string,
maps: ImportMappingAttr[]
): Promise<ImportFileMapPOJO> {
const importFile = await Import.query()

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { ImportFilePreviewPOJO } from './interfaces';
import { ImportFileProcess } from './ImportFileProcess';
import { ImportAls } from './ImportALS';
@Service()
export class ImportFilePreview {
@@ -11,15 +12,33 @@ export class ImportFilePreview {
@Inject()
private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
/**
* Preview the imported file results before commiting the transactions.
* @param {number} tenantId -
* @param {string} importId -
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runPreview<Promise<ImportFilePreviewPOJO>>(() =>
this.previewAlsRun(tenantId, importId)
);
}
/**
* Preview the imported file results before commiting the transactions.
* @param {number} tenantId
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async preview(
public async previewAlsRun(
tenantId: number,
importId: number
importId: string
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

View File

@@ -37,7 +37,7 @@ export class ImportFileProcess {
*/
public async import(
tenantId: number,
importId: number,
importId: string,
trx?: Knex.Transaction
): Promise<ImportFilePreviewPOJO> {
const importFile = await Import.query()

View File

@@ -5,6 +5,7 @@ import { ImportFileProcess } from './ImportFileProcess';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
import { ImportAls } from './ImportALS';
@Service()
export class ImportFileProcessCommit {
@@ -14,18 +15,36 @@ export class ImportFileProcessCommit {
@Inject()
private importFile: ImportFileProcess;
@Inject()
private importAls: ImportAls;
@Inject()
private eventPublisher: EventPublisher;
/**
* Commits the imported file under ALS.
* @param {number} tenantId
* @param {string} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public commit(
tenantId: number,
importId: string
): Promise<ImportFilePreviewPOJO> {
return this.importAls.runCommit<Promise<ImportFilePreviewPOJO>>(() =>
this.commitAlsRun(tenantId, importId)
);
}
/**
* Commits the imported file.
* @param {number} tenantId
* @param {number} importId
* @returns {Promise<ImportFilePreviewPOJO>}
*/
public async commit(
public async commitAlsRun(
tenantId: number,
importId: number
importId: string
): Promise<ImportFilePreviewPOJO> {
const knex = this.tenancy.knex(tenantId);
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });

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