Compare commits

...

463 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
a10b58a7c6 refactor: mail services to nestjs 2025-01-13 16:06:55 +02:00
Ahmed Bouhuolia
72818759a5 refactor: import resource module to nestjs 2025-01-13 10:15:57 +02:00
Ahmed Bouhuolia
270b421a6c refactor: dynamic list to nestjs 2025-01-12 18:22:48 +02:00
Ahmed Bouhuolia
ddaea20d16 fix: e2e test cases 2025-01-11 18:03:59 +02:00
Ahmed Bouhuolia
7e82080cb7 refactor: settings to nestjs 2025-01-11 11:02:57 +02:00
Ahmed Bouhuolia
3bf5f4be86 refactor: events tracker to nestjs 2025-01-08 17:26:11 +02:00
Ahmed Bouhuolia
6f870ea1e1 refactor: save settings service 2025-01-08 17:17:01 +02:00
Ahmed Bouhuolia
ee284196eb refactor: inventory adjustments e2e test cases 2025-01-08 15:43:43 +02:00
Ahmed Bouhuolia
52362a43ab refactor: events tracker to nestjs 2025-01-08 11:59:55 +02:00
Ahmed Bouhuolia
fdfb766587 refactor: inventory adjustments to GL 2025-01-07 23:00:21 +02:00
Ahmed Bouhuolia
1773df1858 refactor: inventory adjustments to nestjs 2025-01-07 22:17:23 +02:00
Ahmed Bouhuolia
abf92ac83f refactor: settings module to Nestjs 2025-01-07 20:43:31 +02:00
Ahmed Bouhuolia
385d84d654 refactor: bank rules e2e test cases 2025-01-06 18:10:24 +02:00
Ahmed Bouhuolia
2bf58d9cb4 refactor: banking modules to nestjs 2025-01-06 11:45:58 +02:00
Ahmed Bouhuolia
ba176394c8 refactor: banking modules to nestjs 2025-01-05 23:06:33 +02:00
Ahmed Bouhuolia
1869ba216f refactor: banking services to Nestjs 2025-01-05 16:26:23 +02:00
Ahmed Bouhuolia
b72f85b394 refactor: e2e 2025-01-01 14:39:25 +02:00
Ahmed Bouhuolia
8bacf3a001 refactor: migrate to nestjs 2025-01-01 13:39:31 +02:00
Ahmed Bouhuolia
505c4b28a5 refactor: migrate ledger writer to nestjs 2025-01-01 12:11:58 +02:00
Ahmed Bouhuolia
3ad34ba56f refactor: migrate ledger subscribers to nestjs 2024-12-31 23:51:24 +02:00
Ahmed Bouhuolia
a819d6c1ba refactor: GL entries 2024-12-31 14:57:24 +02:00
Ahmed Bouhuolia
1b15261adb feat: add attachments support to Expense model and transformer 2024-12-30 22:55:56 +02:00
Ahmed Bouhuolia
4938db704e refactor: expense GL to Nestjs 2024-12-30 22:08:50 +02:00
Ahmed Bouhuolia
3191076762 refactor: e2e tests for payments received 2024-12-30 20:59:19 +02:00
Ahmed Bouhuolia
b046edf337 refactor: e2e tests for payment received 2024-12-30 20:58:56 +02:00
Ahmed Bouhuolia
515a984714 refactor: migrate to Nestjs 2024-12-30 15:54:53 +02:00
Ahmed Bouhuolia
77bbf6828d refactor: credit notes and vendor credits to Nestjs 2024-12-29 22:55:42 +02:00
Ahmed Bouhuolia
caf235e2b5 refactor: migrate credit note and vendor credit services to nestjs 2024-12-29 18:37:33 +02:00
Ahmed Bouhuolia
9f9b75cd31 Merge branch 'develop' into migrate-server-nestjs 2024-12-29 11:14:15 +02:00
Ahmed Bouhuolia
736cedd63d feat: migrate manual journal to nestjs 2024-12-26 20:57:21 +02:00
Ahmed Bouhuolia
cd84872a61 feat: wip migrate to nestjs 2024-12-26 15:40:29 +02:00
Ahmed Bouhuolia
a6932d76f3 refactor: wip to nestjs 2024-12-25 00:43:55 +02:00
Ahmed Bouhuolia
336171081e refactor: sale estimates to nestjs 2024-12-22 14:16:01 +02:00
Ahmed Bouhuolia
8a12caf48d refactor: warehouses to nestjs 2024-12-21 15:07:01 +02:00
Ahmed Bouhuolia
cb8fd68d46 refactor: branches and warehouses to nestjs 2024-12-21 00:10:09 +02:00
Ahmed Bouhuolia
dc52f784b6 refactor: pdf templates to nestjs 2024-12-20 14:53:31 +02:00
Ahmed Bouhuolia
330192c042 refactor: tax rates to nestjs 2024-12-20 12:24:50 +02:00
Ahmed Bouhuolia
1f32a7c59a refactor: migrate item categories to nestjs 2024-12-20 10:40:35 +02:00
Ahmed Bouhuolia
83dfaa00fd refactor: migrate item categories module to nestjs 2024-12-19 19:06:03 +02:00
Ahmed Bouhuolia
93bf6d9d3d refactor: wip migrate ot nestjs 2024-12-19 12:48:24 +02:00
Ahmed Bouhuolia
bfff56c470 refactor: accounts module to Nestjs 2024-12-16 16:45:56 +02:00
Ahmed Bouhuolia
87e9cd64e8 refactor: items services to Nestjs 2024-12-15 15:23:46 +02:00
Ahmed Bouhuolia
0a112c5655 refactor: items services to Nestjs 2024-12-15 13:04:41 +02:00
Ahmed Bouhuolia
70211980aa feat: replace all src/ imports to @ alias 2024-12-15 00:50:10 +02:00
Ahmed Bouhuolia
2ba31148ca feat: initialize the server e2e tests 2024-12-15 00:49:10 +02:00
Ahmed Bouhuolia
1906d9f3f5 Merge pull request #766 from bigcapitalhq/discount-line-level
fix: Line-level discount
2024-12-12 15:39:19 +02:00
Ahmed Bouhuolia
d640dc1f40 feat: add totalExcludingTax property and update GL entry calculations 2024-12-12 12:49:52 +02:00
Ahmed Bouhuolia
8cd1b36a02 feat: item-level discount 2024-12-11 15:05:50 +02:00
Ahmed Bouhuolia
5a8d9cc7e8 feat: wip line-level discount 2024-12-11 12:37:15 +02:00
Ahmed Bouhuolia
6323e2ffec fix: line-level discount 2024-12-11 11:44:10 +02:00
Ahmed Bouhuolia
7af2e7ccbc chore: update CHANGELOG 2024-12-09 12:23:46 +02:00
Ahmed Bouhuolia
baf4c691d6 Merge pull request #763 from bigcapitalhq/fix-discount-gl-entries
fix: discount transactions GL entries
2024-12-09 11:08:19 +02:00
Ahmed Bouhuolia
c633fa8522 feat: add vendor credit discount and adjustment GL entries 2024-12-09 11:06:42 +02:00
Ahmed Bouhuolia
1d54947764 fix: correct debit and credit calculations for local adjustments in BillGLEntries 2024-12-09 00:44:50 +02:00
Ahmed Bouhuolia
477da0e7c0 feat: add local adjustment and discount properties to SaleInvoice and SaleReceipt interfaces. 2024-12-09 00:19:22 +02:00
Ahmed Bouhuolia
b9963aa241 feat: filter ledger blank entries 2024-12-09 00:01:42 +02:00
Ahmed Bouhuolia
994c441bb8 feat: add local discount and adjustment calculations to financial models and transformers
- Introduced `discountAmountLocal` and `adjustmentLocal` properties across Bill, CreditNote, SaleInvoice, SaleReceipt, and VendorCredit models to calculate amounts in local currency.
- Updated transformers for CreditNote, PurchaseInvoice, and VendorCredit to include formatted representations of local discount and adjustment amounts.
- Enhanced GL entry services to handle discount and adjustment entries for SaleReceipt and CreditNote, ensuring accurate ledger entries.
- Improved overall consistency in handling financial calculations across various models and services.
2024-12-08 18:11:03 +02:00
Ahmed Bouhuolia
0a5115fc20 feat: add paid amount attr and formatting to SaleReceipt transformer 2024-12-08 17:26:52 +02:00
Ahmed Bouhuolia
11d7a40326 fix: display adjustment in minues 2024-12-08 14:47:03 +02:00
Ahmed Bouhuolia
46719ef361 fix: discount transactions GL entries 2024-12-08 14:20:11 +02:00
Ahmed Bouhuolia
14ae978bde Merge pull request #762 from bigcapitalhq/fix-discount-adjustment-bugs
fix: discount & adjustment sale transactions bugs
2024-12-05 14:48:11 +02:00
Ahmed Bouhuolia
beec09788e fix: discount & adjustment sale transactions bugs 2024-12-05 14:47:11 +02:00
Ahmed Bouhuolia
391dc77071 Merge pull request #761 from bigcapitalhq/fix-total-lines-style
fix: total lines style
2024-12-04 15:26:25 +02:00
Ahmed Bouhuolia
38f2004e56 fix: total lines style 2024-12-04 15:24:58 +02:00
Ahmed Bouhuolia
5a5eac246b chore: update CHANGELOG.md 2024-12-04 13:36:25 +02:00
Ahmed Bouhuolia
a7bafd4f62 Merge pull request #760 from bigcapitalhq/fix-formatted-hooks
fix: update financial forms to use new formatted amount utilities and…
2024-12-04 13:24:23 +02:00
Ahmed Bouhuolia
a25ab39647 refactor: replace journal total calculations with new formatted amount hooks 2024-12-04 13:23:40 +02:00
Ahmed Bouhuolia
7dd09e2903 fix: discount and adjustment fields 2024-12-04 12:18:20 +02:00
Ahmed Bouhuolia
6ab461a212 feat: enhance discount and adjustment formatting 2024-12-04 12:00:22 +02:00
Ahmed Bouhuolia
fabc88c81a feat: add adjustment total in estimates, invoices, and receipts pdf templates 2024-12-03 23:38:27 +02:00
Ahmed Bouhuolia
3a19518440 fix: update financial forms to use new formatted amount utilities and add adjustment fields 2024-12-03 17:53:37 +02:00
Ahmed Bouhuolia
56b5e3469e Merge pull request #758 from bigcapitalhq/add-discount-to-transactions
feat: Add discount to transactions
2024-12-03 14:25:39 +02:00
Ahmed Bouhuolia
542763ddf5 feat: enhance discount and adjustment validation in Bills and Vendor Credit controllers 2024-12-03 14:22:49 +02:00
Ahmed Bouhuolia
1010d97a92 fix: discount and adjustment fields across financial forms 2024-12-03 13:54:26 +02:00
Ahmed Bouhuolia
d5dacaa988 feat: add discount and adjustment fields to email templates. 2024-12-03 13:20:19 +02:00
Ahmed Bouhuolia
154ade9647 feat: stylw tweaks of discount and adjustment in estimates, invoices, and receipts 2024-12-02 18:57:42 +02:00
Ahmed Bouhuolia
5b75fa9286 feat: link discount to mail receipts 2024-12-02 18:45:16 +02:00
Ahmed Bouhuolia
05cf94940e refactor: implementing new formatted amount hooks 2024-12-02 15:32:39 +02:00
Ahmed Bouhuolia
03b0d2519b refactor: update estimate and receipt forms to use new subtotal and total formatting utilities 2024-12-01 18:19:09 +02:00
Ahmed Bouhuolia
000c3e40e1 feat: enhance discount handling in financial forms
- Implemented discount and adjustment fields in Bill, Credit Note, Estimate, Invoice, and Receipt forms.
- Created new components for displaying discount and adjustment totals, improving clarity in financial documents.
- Updated utility functions to format discount and adjustment amounts consistently across various forms.
- Enhanced user experience by integrating discount functionality into the form context, allowing for better data management and display.

This update improves the overall functionality and user experience related to discounts in financial transactions.
2024-12-01 17:59:01 +02:00
Ahmed Bouhuolia
ffb06f5194 feat: add discount fields in sale and purchase forms 2024-11-30 18:02:50 +02:00
Ahmed Bouhuolia
73ab92e693 feat: implement discount display in various detail drawers
- Added discount amount and percentage display in Bill, Credit Note, Estimate, Invoice, Receipt, and Vendor Credit detail tables.
- Updated models to include discount-related attributes for better data handling.
- Enhanced user interface to show discount information when applicable, improving clarity in financial documents.
2024-11-30 16:01:29 +02:00
Ahmed Bouhuolia
dd1392cdc8 feat: add discount functionality to sales and purchase transactions
- Introduced discount_type and discount fields in Bills and SalesReceipts controllers.
- Updated database migrations to include discount and discount_type in estimates and credit notes tables.
- Enhanced SaleReceipt and SaleEstimate models to support discount attributes.
- Implemented formatting for discount amounts in transformers and PDF templates.
- Updated email templates to display discount information.

This commit enhances the handling of discounts across various transaction types, improving the overall functionality and user experience.
2024-11-30 14:46:56 +02:00
Ahmed Bouhuolia
17b3bbe1d8 feat: discount vendor credit 2024-11-28 18:17:47 +02:00
Ahmed Bouhuolia
e02ad1e795 feat: discount formatted attributes of sale transactions 2024-11-28 17:59:09 +02:00
Ahmed Bouhuolia
df8391201f feat: discount sale and purchase transactions 2024-11-28 11:14:16 +02:00
Ahmed Bouhuolia
aa4aaeb612 feat: add discount to sale and purchase transactions 2024-11-28 10:12:48 +02:00
Ahmed Bouhuolia
09b98664c5 Merge pull request #757 from bigcapitalhq/estimate-mail-preview
feat: estimate, receipt, credit note mail preview
2024-11-26 13:27:09 +02:00
Ahmed Bouhuolia
be46147d9a chore: add newrelic log to gitignore 2024-11-26 13:26:21 +02:00
Ahmed Bouhuolia
1fe7d58c8c chore: doc comments 2024-11-26 13:25:56 +02:00
Ahmed Bouhuolia
4f57782be4 feat: change default mail template messages 2024-11-26 13:15:15 +02:00
Ahmed Bouhuolia
7b5f0d3930 feat: receipt mail preview 2024-11-26 11:36:08 +02:00
Ahmed Bouhuolia
831fb9180c chore: add newrelic logs file tp gitignore 2024-11-25 16:25:49 +02:00
Ahmed Bouhuolia
2594e37dc7 feat: wip mail receipt preview 2024-11-25 16:22:40 +02:00
Ahmed Bouhuolia
ca44d6346d feat: wip preview mail receipt 2024-11-25 16:18:29 +02:00
Ahmed Bouhuolia
df41de7239 feat: payment received mail receipt 2024-11-25 11:51:13 +02:00
Ahmed Bouhuolia
459bf4cd55 feat: mail receipt preview 2024-11-24 15:40:12 +02:00
Ahmed Bouhuolia
3537a05ea2 feat: payment received mail preview 2024-11-24 13:19:26 +02:00
Ahmed Bouhuolia
da47418f17 feat: mail receipt preview 2024-11-23 20:36:46 +02:00
Ahmed Bouhuolia
d5b0546301 feat: wip send mail preview 2024-11-23 10:19:26 +02:00
Ahmed Bouhuolia
b6f3c0145f feat: payment received mail preview 2024-11-21 14:32:28 +02:00
Ahmed Bouhuolia
c5c85bdfbe feat: wip estimate send mail 2024-11-21 11:29:52 +02:00
Ahmed Bouhuolia
63a95df534 feat: payment received mail receipt preview 2024-11-20 13:03:17 +02:00
Ahmed Bouhuolia
6103f1e4c7 feat: receipt send mail preview 2024-11-20 09:42:55 +02:00
Ahmed Bouhuolia
3e591beb03 feat: estimate mail receipt 2024-11-19 22:46:58 +02:00
Ahmed Bouhuolia
c6db54175f feat: add ssr email templates rendering 2024-11-19 17:14:13 +02:00
Ahmed Bouhuolia
f7bf24acb3 feat: email templates 2024-11-19 14:21:10 +02:00
Ahmed Bouhuolia
ddffe630ff feat: email templates 2024-11-19 14:00:25 +02:00
Ahmed Bouhuolia
2c54092591 feat: wip email templates 2024-11-19 11:56:52 +02:00
Ahmed Bouhuolia
7e65f3f642 feat: estimate send mail drawer 2024-11-19 00:38:45 +02:00
Ahmed Bouhuolia
7df6aa4110 feat: wip receipt mail preview 2024-11-18 15:52:13 +02:00
Ahmed Bouhuolia
7df316aa56 feat: wip send estimate mail preview 2024-11-18 15:15:03 +02:00
Ahmed Bouhuolia
53ab40a075 feat: estimate, receipt, credit note mail preview 2024-11-17 15:45:55 +02:00
Ahmed Bouhuolia
d115ebde12 Merge pull request #755 from bigcapitalhq/fix-upload-attachments
hotbug: upload attachments
2024-11-16 21:23:37 +02:00
Ahmed Bouhuolia
0e99ac88d3 hotbug: upload attachments 2024-11-16 21:23:12 +02:00
Ahmed Bouhuolia
05f4b49b58 wip 2024-11-16 11:55:27 +02:00
Ahmed Bouhuolia
5d6f901d33 feat: allow quantity of entries accept decimal value (#753) 2024-11-13 18:35:57 +02:00
Ahmed Bouhuolia
908bbb9fa6 Merge pull request #754 from bigcapitalhq/adjust-decimal-manual-entry
fix: make manual entries adjust decimal credit/debit amounts
2024-11-13 18:16:37 +02:00
Ahmed Bouhuolia
6c1870be8f fix: make manual entries adjust decimal credit/debit amounts 2024-11-13 18:15:13 +02:00
Ahmed Bouhuolia
19080a67ab feat: wip migrate server to nestjs 2024-11-12 23:08:51 +02:00
Ahmed Bouhuolia
f5834c72c6 Merge pull request #751 from bigcapitalhq/fix-attach-branding-template-to-payment-page
fix: attach branding template attrs to payment page
2024-11-11 20:26:31 +02:00
Ahmed Bouhuolia
7ee3392d3e fix: attach branding template attrs to payment page 2024-11-11 20:25:42 +02:00
Ahmed Bouhuolia
c58822fd6c Merge pull request #750 from bigcapitalhq/fix-download-invoice-payment-page
fix: download invoice document on payment page
2024-11-11 19:02:28 +02:00
Ahmed Bouhuolia
ba8091d697 fix: download invoice document on payment page 2024-11-11 19:01:43 +02:00
Ahmed Bouhuolia
d668d60ed5 fix: remove the vite types from pdf-templates package 2024-11-10 11:49:32 +02:00
Ahmed Bouhuolia
a34b7a2106 fix: remove vite from pdf-templates packakge 2024-11-10 11:47:09 +02:00
Ahmed Bouhuolia
6f12127095 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-11-10 11:39:38 +02:00
Ahmed Bouhuolia
b4e5bbf376 fix: invoice payment email storybook 2024-11-10 11:39:03 +02:00
Ahmed Bouhuolia
a11530d190 Merge pull request #748 from bigcapitalhq/fix-monorepo-dependecies
fix: monorepo dependencies scope
2024-11-10 11:35:56 +02:00
Ahmed Bouhuolia
43d4425da5 fix: monorepo dependecoes scope 2024-11-10 11:35:16 +02:00
Ahmed Bouhuolia
4c1909cb73 Merge pull request #747 from bigcapitalhq/fix-template-company-logo
fix: company logo of the template
2024-11-09 23:45:49 +02:00
Ahmed Bouhuolia
a7b6b7a03e fix: company logo of the template 2024-11-09 23:44:40 +02:00
Ahmed Bouhuolia
1f46275bde Merge pull request #746 from bigcapitalhq/fix-mail-services
fix: mail services
2024-11-09 22:25:09 +02:00
Ahmed Bouhuolia
aa7e5d4ae9 fix: mail services 2024-11-09 22:23:52 +02:00
Ahmed Bouhuolia
bb482df3ce Merge pull request #530 from angelosorno/develop 2024-11-08 19:03:10 +02:00
Ahmed Bouhuolia
f878786646 Merge pull request #671 from Crims-on/Crims-on-sv-translation 2024-11-08 19:02:22 +02:00
Ahmed Bouhuolia
652851a1a9 Merge pull request #745 from ibutiti/big-265-fix-forgot-password-text
big-265: Fix forgot password text
2024-11-07 12:35:28 +02:00
Allan Ibutiti
850f4956cb big-265: fixed forgot password text 2024-11-07 12:13:59 +03:00
Ahmed Bouhuolia
94223b6ebf Merge pull request #744 from bigcapitalhq/fix-due-invoice-server-invoice
fix: due invoice server invoice
2024-11-06 17:25:24 +02:00
Ahmed Bouhuolia
e9d34e19ad fix: due invoice server invoice 2024-11-06 17:24:42 +02:00
Ahmed Bouhuolia
107532fe26 Merge pull request #742 from bigcapitalhq/fix-send-invoice-addresses
fix: send invoice receipt addresses
2024-11-06 14:05:22 +02:00
Ahmed Bouhuolia
c32aff82ee fix: send invoice receipt addresses 2024-11-06 14:04:29 +02:00
Ahmed Bouhuolia
de8a867d33 Merge pull request #741 from bigcapitalhq/fix-ssr-invoice-template
fix: style SSR invoice paper template
2024-11-06 11:51:48 +02:00
Ahmed Bouhuolia
17a8aba23f fix: style ssr invoice paper template 2024-11-06 11:51:04 +02:00
Ahmed Bouhuolia
04b601626b Merge pull request #740 from bigcapitalhq/fix-invoice-preview
feat: getting invoice preview on send mail view
2024-11-05 22:31:40 +02:00
Ahmed Bouhuolia
802775c118 feat: getting invoice preview on send mail view 2024-11-05 22:30:54 +02:00
Ahmed Bouhuolia
b6baa80134 Merge pull request #735 from bigcapitalhq/add-pdf-templates-package
feat: add shared package to pdf templates to render in the server and…
2024-11-05 17:20:12 +02:00
Ahmed Bouhuolia
b2d0f2ed3c Merge branch 'develop' into add-pdf-templates-package 2024-11-05 17:19:50 +02:00
Ahmed Bouhuolia
d23f33bae4 feat: add style to SSR invoice paper template 2024-11-05 17:09:47 +02:00
Ahmed Bouhuolia
22ea557337 feat: wip invoice paper template server-side 2024-11-05 13:33:22 +02:00
Ahmed Bouhuolia
b3ebbb429c Merge pull request #739 from bigcapitalhq/clean-up-invoice-mail-receipt-preview-component
fix: clean up ivnoice mail receipt preview component
2024-11-04 16:22:01 +02:00
Ahmed Bouhuolia
51218797af fix: clean up ivnoice mail receipt preview component 2024-11-04 16:21:13 +02:00
Ahmed Bouhuolia
2d18a6573e Merge pull request #738 from bigcapitalhq/fix-ts-typing-invoice-send-mail-fields
fix: typing invoice send mail fields
2024-11-04 14:19:16 +02:00
Ahmed Bouhuolia
2646ad5bc4 fix: typing invoice send mail fields 2024-11-04 14:18:47 +02:00
Ahmed Bouhuolia
51aec8d8b3 feat: render server-side invoice pdf template using React server 2024-11-04 12:55:12 +02:00
Ahmed Bouhuolia
638bd95d6f Merge pull request #737 from bigcapitalhq/change-default-invoice-mail-message
fix: change default invoice mail message
2024-11-03 20:58:53 +02:00
Ahmed Bouhuolia
f2fcc3a649 fix: change default invoice mail message 2024-11-03 20:56:22 +02:00
Ahmed Bouhuolia
48795748d8 Merge pull request #736 from bigcapitalhq/fix-company-logo-mail-receipt
fix: company logo does not show up in mail receipt preview
2024-11-03 20:23:56 +02:00
Ahmed Bouhuolia
6ba54a994a fix: company logo does not show up in mail receipt preview 2024-11-03 20:22:59 +02:00
Ahmed Bouhuolia
6687db4085 feat: add shared package to pdf templates to render in the server and client side 2024-11-03 17:31:17 +02:00
Ahmed Bouhuolia
ba1d9b3f28 Merge pull request #734 from bigcapitalhq/hook-up-cc-bcc-to-mail-sender
fix: hook up cc and bcc fields to mail sender
2024-11-02 19:33:58 +02:00
Ahmed Bouhuolia
51905825fd fix: hook up cc and bcc fields to mail sender 2024-11-02 19:33:29 +02:00
Ahmed Bouhuolia
bd5e33855a Merge pull request #733 from bigcapitalhq/fix-send-invoice-drawer-layout
fix: send invoice drawer layout
2024-11-02 17:11:06 +02:00
Ahmed Bouhuolia
f7fbc0e31c fix: send invoice drawer layout 2024-11-02 17:10:32 +02:00
Ahmed Bouhuolia
cb06fa342c Merge pull request #732 from bigcapitalhq/attach-payment-link-invoice
fix: attach payment link in sending invoice mail receipt
2024-11-02 16:02:53 +02:00
Ahmed Bouhuolia
581229053a fix: attach payment link in sending invoice mail receipt 2024-11-02 16:02:17 +02:00
Ahmed Bouhuolia
209da69b8f Merge pull request #731 from bigcapitalhq/refactor-mail-services
refactor: notification mail services
2024-11-02 15:00:54 +02:00
Ahmed Bouhuolia
d09aebcebb refactor: notification mail services 2024-11-02 14:59:57 +02:00
Ahmed Bouhuolia
0cc80bc179 Merge pull request #730 from bigcapitalhq/change-send-mail-button-invoice
fix: change the send mail button on invoice drawer
2024-11-02 11:51:34 +02:00
Ahmed Bouhuolia
79dcc592bc fix: change the send mail button on invoice drawer 2024-11-02 11:51:10 +02:00
Ahmed Bouhuolia
687111851a Merge pull request #723 from bigcapitalhq/invoice-mail-receipt
feat: wip Invoice mail receipt preview
2024-10-31 12:43:44 +02:00
Ahmed Bouhuolia
dbbaa387bd feat: send invoice receipt preview 2024-10-31 12:40:48 +02:00
Ahmed Bouhuolia
470bfd32f7 feat: wip send invoice receipt preview 2024-10-30 14:22:54 +02:00
Ahmed Bouhuolia
5fddd080fd feat: wip send invoice mail receipt 2024-10-30 13:10:56 +02:00
Ahmed Bouhuolia
e10c530b4b feat: wip preview invoice payment mail 2024-10-29 21:14:46 +02:00
Ahmed Bouhuolia
12189f018d feat: wip send invoice mail payment template 2024-10-28 18:33:16 +02:00
Ahmed Bouhuolia
0111b0e6ff feat: hook up the request to the send mail form 2024-10-28 12:00:17 +02:00
Ahmed Bouhuolia
0930b0428d fix: tsconfig email-components package 2024-10-28 00:28:52 +02:00
Ahmed Bouhuolia
289f40014e feat: invoice payment email template 2024-10-27 17:20:48 +02:00
Ahmed Bouhuolia
01cc0568f9 feat: wip invoice payment email template 2024-10-27 15:09:08 +02:00
Ahmed Bouhuolia
42ee8ed9fa feat: initialize email-components vite package 2024-10-27 10:16:04 +02:00
Ahmed Bouhuolia
1dae65cb74 feat: Style the send email right buttons 2024-10-26 19:01:37 +02:00
Ahmed Bouhuolia
ce40d67ea2 feat: wip send invoice preview 2024-10-26 18:39:36 +02:00
Ahmed Bouhuolia
26088a71ee Merge pull request #721 from bigcapitalhq/track-move-events
feat: track more services events
2024-10-26 12:40:30 +02:00
Ahmed Bouhuolia
cadf6b81a0 feat: Track reports view events 2024-10-26 12:39:48 +02:00
Ahmed Bouhuolia
728b4cacd9 feat: wip send invoice mail preview 2024-10-24 20:48:16 +02:00
allcontributors[bot]
b4d3ac2f96 docs: add nklmantey as a contributor for bug (#725)
* 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-10-24 16:51:21 +02:00
Ahmed Bouhuolia
bc8e440814 Merge pull request #722 from nklmantey/BIG-257-not-balanced-if-decimal
Fix: Credit and debit totals not balancing when decimal values are used
2024-10-24 16:49:52 +02:00
Ahmed Bouhuolia
4c0f9a0aef fix: using lodash utils for decimal rounding 2024-10-24 16:47:29 +02:00
Ahmed Bouhuolia
c321d90575 feat: send invoice mail receipt drawer 2024-10-23 16:30:39 +02:00
Ahmed Bouhuolia
03e6372f14 feat: style the invoice payment preview 2024-10-22 14:36:53 +02:00
Ahmed Bouhuolia
c0481f67ad feat: wip invoice mail receipt preview 2024-10-22 14:00:36 +02:00
Ahmed Bouhuolia
b7f316d25a feat: wip invoice mail receipt preview 2024-10-22 11:59:15 +02:00
Ahmed Bouhuolia
dffd818396 feat: Invoice mail receipt preview 2024-10-21 15:42:12 +02:00
Nana Kofi Larbi Mantey
bbc19df6b4 adds CREDIT_DEBIT_NOT_EQUAL error 2024-10-20 19:04:10 +00:00
Nana Kofi Larbi Mantey
c8c2786893 refactors getTotal function loginc 2024-10-20 17:45:44 +00:00
Nana Kofi Larbi Mantey
d79f26f1b5 refactors backend validator for credit-debit equals 2024-10-20 17:44:39 +00:00
Ahmed Bouhuolia
32ba6f9a6c feat: track more services events 2024-10-19 23:47:14 +02:00
Ahmed Bouhuolia
ccbb399685 Merge pull request #720 from bigcapitalhq/add-estimate-customer-note
fix Customer note does not appear in pdf document
2024-10-19 16:28:33 +02:00
Ahmed Bouhuolia
f381d433ec fix Customer note does not appear in pdf document 2024-10-19 16:28:01 +02:00
Ahmed Bouhuolia
2ee49f7496 Merge pull request #719 from bigcapitalhq/track-pdf-documents-views-events
feat: Track events of pdf documents views
2024-10-19 13:40:18 +02:00
Ahmed Bouhuolia
bb299aa595 feat: Track events of pdf documents views 2024-10-19 13:38:28 +02:00
Ahmed Bouhuolia
a66867463e Merge pull request #718 from bigcapitalhq/invoice-number-filename-document
feat: Invoice number in downloaded pdf document
2024-10-19 13:16:48 +02:00
Ahmed Bouhuolia
de50b89e5c feat: Invoice number in downloaded pdf document 2024-10-19 13:16:06 +02:00
Ahmed Bouhuolia
c4ee143354 Merge pull request #717 from bigcapitalhq/preline-statements
ffeat: Pre-line invoice statements
2024-10-19 11:00:31 +02:00
Ahmed Bouhuolia
a2227016e5 feat: Pre-line invoice statements 2024-10-19 10:59:46 +02:00
Ahmed Bouhuolia
4d6f65b179 Merge pull request #716 from bigcapitalhq/add-quantity-column-pdf-templates
feat: Add quantity column to pdf templates
2024-10-17 16:01:02 +02:00
Ahmed Bouhuolia
758ebbe261 feat: Add qty column to server-side pdf templates 2024-10-17 16:00:19 +02:00
Ahmed Bouhuolia
279890e922 feat: Add qty column to preview pdf templates: 2024-10-17 15:58:19 +02:00
Ahmed Bouhuolia
44fae36b82 Merge pull request #715 from bigcapitalhq/sync-account-norma-cashflow
fix: Sync account normal of cashflow GL entries
2024-10-16 20:12:51 +02:00
Ahmed Bouhuolia
fc2fac80af fix: Sync account normal of cashflow GL entries 2024-10-16 20:12:25 +02:00
Ahmed Bouhuolia
5ad9a9654b Merge pull request #714 from bigcapitalhq/sync-plaid-credit-card-account-type
fix: Sync Plaid credit card account type
2024-10-16 19:47:13 +02:00
Ahmed Bouhuolia
8a4034cc5d fix: Sync Plaid credit card account type 2024-10-16 19:46:39 +02:00
Ahmed Bouhuolia
5649657bf0 Merge pull request #713 from bigcapitalhq/i18napply
chore: Move i18nApply localization to the account transformer
2024-10-15 19:27:43 +02:00
Ahmed Bouhuolia
c929a7cb27 chore: Move i18nApply localization to the account transformer 2024-10-15 19:27:15 +02:00
Ahmed Bouhuolia
eeedb789a9 Merge pull request #711 from bigcapitalhq/fix-parse-non-lowercase-import
fix: Parse the uppercase values in importing
2024-10-14 20:02:31 +02:00
Ahmed Bouhuolia
321af8c271 fix: Parse the uppercase values in importing 2024-10-14 20:01:52 +02:00
Ahmed Bouhuolia
fd4d86e797 Merge pull request #710 from bigcapitalhq/fix-import-category-on-items
fix: Import category column of item resource
2024-10-14 19:49:54 +02:00
Ahmed Bouhuolia
49988e27a2 fix: Import category column of item resource 2024-10-14 19:49:32 +02:00
Ahmed Bouhuolia
8c94ee5982 Dump CHANGELOG 2024-10-14 13:58:57 +02:00
Ahmed Bouhuolia
2e73a34fef Merge pull request #709 from bigcapitalhq/track-viewed-events
feat: Track account, invoice and item viewed events
2024-10-14 12:16:40 +02:00
Ahmed Bouhuolia
ea7f987fe3 feat: Track account, invoice and item viewed events 2024-10-14 12:15:21 +02:00
Ahmed Bouhuolia
d55503f0c7 Update README.md 2024-10-13 23:10:43 +02:00
Ahmed Bouhuolia
f59b8166b6 Merge pull request #708 from bigcapitalhq/add-customize-templates-btn-to-edit-forms
feat: Add customize templates button to edit forms
2024-10-13 21:15:08 +02:00
Ahmed Bouhuolia
2c735d7edf feat: Add customize templates button to edit forms 2024-10-13 21:14:18 +02:00
Ahmed Bouhuolia
5b5ab9fe1e Merge pull request #707 from bigcapitalhq/refactor-date-field
refactor: invoice, estimate, receipt, credit note and payment received date input fields
2024-10-13 18:03:36 +02:00
Ahmed Bouhuolia
28ac9b2d90 refactor: invoice, estimate, receipt, credit note and payment received date input fields 2024-10-13 18:01:43 +02:00
Ahmed Bouhuolia
3300a6a499 Merge pull request #705 from bigcapitalhq/fix-invoice-form-layout
fix: Invoice form layout
2024-10-13 17:23:54 +02:00
Ahmed Bouhuolia
152a22baa0 fix: Remove unused scss files 2024-10-13 17:22:14 +02:00
Ahmed Bouhuolia
e873198238 feat: typeing AppIntlProvider 2024-10-13 16:51:04 +02:00
Ahmed Bouhuolia
68a0db91ee feat: form header fields 2024-10-13 13:56:13 +02:00
Ahmed Bouhuolia
ddea7be24a feat: Add css utilities to Box, Stack and Group components 2024-10-13 01:06:17 +02:00
Ahmed Bouhuolia
b7b86bb0c5 fix: Invoice form layout 2024-10-12 20:49:56 +02:00
Ahmed Bouhuolia
817ef906dc Merge pull request #701 from bigcapitalhq/fix-disable-tabs-customize
fix: Disable tabs of the pdf customization if the first field not filed up
2024-10-12 12:52:01 +02:00
Ahmed Bouhuolia
863c7693fa fix: Disable tabs of the pdf customization if the first field not filled up 2024-10-10 16:41:21 +02:00
Ahmed Bouhuolia
cf78255220 Merge pull request #700 from bigcapitalhq/fix-company-logo-dimenstion-pdf-template
fix: Set max width/height to company logo of pdf templates
2024-10-08 10:12:03 +02:00
Ahmed Bouhuolia
f9aa6abdd7 fix: Set max width/height to company logo of pdf templates 2024-10-08 10:11:40 +02:00
Ahmed Bouhuolia
0a5e40a0d8 Merge pull request #699 from bigcapitalhq/fix-remove-logo-pdf-template
fix: Delete company logo from the PDF template
2024-10-08 08:21:03 +02:00
Ahmed Bouhuolia
4aa445fe35 fix: Delete company logo from the pdf template 2024-10-08 08:20:35 +02:00
Ahmed Bouhuolia
1a1095c99b Merge pull request #698 from bigcapitalhq/fix-estimate-initial-value-template
fix: Estimate customize values
2024-10-07 17:21:35 +02:00
Ahmed Bouhuolia
d92b46aa7b fix: Estimate customize values 2024-10-07 17:20:45 +02:00
Ahmed Bouhuolia
682d40cbf8 Merge pull request #697 from bigcapitalhq/fix-pdf-branding-templates-request-data
fix: Pdf branding templates request data
2024-10-07 16:11:09 +02:00
Ahmed Bouhuolia
b62f3b3fa6 chore: remove commented line 2024-10-07 16:10:59 +02:00
Ahmed Bouhuolia
e76d3b15ce fix: Pdf branding template initial values 2024-10-07 16:08:25 +02:00
Ahmed Bouhuolia
9edfb83221 fix: Pdf branding templates request data 2024-10-07 16:03:56 +02:00
Ahmed Bouhuolia
bbdfe00c7a Merge pull request #696 from bigcapitalhq/fix-changing-pdf-template
fix: Changing the pdf template of the invoice
2024-10-07 09:51:17 +02:00
Ahmed Bouhuolia
e3942551cd fix: Changing the pdf template of the invoice 2024-10-07 09:50:46 +02:00
Ahmed Bouhuolia
a0c1a21983 Merge pull request #695 from bigcapitalhq/feat-change-document-title-of-payment-page
feat: Change the document title of the payment page
2024-10-06 22:21:53 +02:00
Ahmed Bouhuolia
3358ce58bc feat: Change the document title of the payment page 2024-10-06 22:21:12 +02:00
Ahmed Bouhuolia
3cd54653a8 Merge pull request #694 from bigcapitalhq/lerna-shared
feat: Add shared packages to Docker container
2024-10-06 17:26:39 +02:00
Ahmed Bouhuolia
6cad929738 feat: Lerna shared 2024-10-06 17:20:28 +02:00
Ahmed Bouhuolia
184648040c Merge pull request #693 from bigcapitalhq/fix-display-country-name
fix: Display country name
2024-10-06 13:19:17 +02:00
Ahmed Bouhuolia
df9d277e66 fix: Display country name 2024-10-06 13:18:31 +02:00
Ahmed Bouhuolia
75ec315de2 Merge pull request #689 from bigcapitalhq/download-payment-link-invoice-pdf
feat: Download invoice pdf of the payment link
2024-10-05 21:48:07 +02:00
Ahmed Bouhuolia
c89b2367e6 fix: Download invoice pdf of the payment link page 2024-10-05 21:46:48 +02:00
Ahmed Bouhuolia
bca5b3481c Merge pull request #691 from bigcapitalhq/pdf-templates-layout
fix: Pdf templates layout
2024-10-05 21:26:37 +02:00
Ahmed Bouhuolia
59996e7a40 feat: re-layout server-side pdf template 2024-10-05 21:24:07 +02:00
Ahmed Bouhuolia
af5726c48c fix: Pdf templates layout 2024-10-05 19:01:34 +02:00
Ahmed Bouhuolia
90f08c5d51 Merge pull request #690 from bigcapitalhq/fix-remove-empty-lines-from-address
fix: Remove empty lines from address formats
2024-10-05 16:09:03 +02:00
Ahmed Bouhuolia
a0a9f4a768 fix: Remove empty lines from address formats 2024-10-05 16:08:09 +02:00
Ahmed Bouhuolia
2649f1c326 feat: Download invoice pdf of the payment link 2024-10-05 13:56:25 +02:00
Ahmed Bouhuolia
c5ff1e4d4a Merge pull request #688 from bigcapitalhq/fix-pdf-template-addresses-controlling
fix: pdf template addresses controlling
2024-10-03 17:13:07 +02:00
Ahmed Bouhuolia
c74c8e896a fix: pdf template addresses controlling 2024-10-03 17:12:12 +02:00
Ahmed Bouhuolia
55fdc47ff0 Merge pull request #687 from bigcapitalhq/assign-default-pdf-template
feat: Assign default PDF template automatically
2024-10-03 17:02:17 +02:00
Ahmed Bouhuolia
126eb221d0 feat: invalidate invoice state once change default template 2024-10-03 17:01:35 +02:00
Ahmed Bouhuolia
3c7e22be43 feat: Assign default pdf template automatically 2024-10-03 16:36:44 +02:00
Ahmed Bouhuolia
b23112bc92 feat: Assign default PDF template automatically 2024-10-02 18:18:57 +02:00
Ahmed Bouhuolia
cbc60b3c73 Merge pull request #684 from bigcapitalhq/getting-uploaded-object-uri
fix: Getting uploaded object uri
2024-10-01 15:09:53 +02:00
Ahmed Bouhuolia
6caa1311fd fix: Getting uploaded object uri 2024-10-01 15:09:21 +02:00
Ahmed Bouhuolia
cd0bbd11c3 chore: change CHANGELOG.md 2024-10-01 12:56:33 +02:00
Ahmed Bouhuolia
2a944f8507 feat: Add Stripe payment env variables examples 2024-10-01 12:53:27 +02:00
Ahmed Bouhuolia
8a2754d9ce Merge pull request #683 from bigcapitalhq/feat-hook-up-customer-address
feat: Hook up customer/company address to invoice preview of payment page
2024-10-01 09:49:14 +02:00
Ahmed Bouhuolia
ace75f2dfa feat: Hook up customer/company address to invoice preview of payment page 2024-10-01 09:48:07 +02:00
Ahmed Bouhuolia
7ceb785c1b Merge pull request #682 from bigcapitalhq/listen-stripe-integration-events
feat: Listen to Stripe integration events
2024-09-30 23:13:14 +02:00
Ahmed Bouhuolia
904a52f5a1 feat: listen to Stripe integration events 2024-09-30 23:12:42 +02:00
Ahmed Bouhuolia
04fe65b176 fix: payment link events tracker 2024-09-30 18:09:57 +02:00
Ahmed Bouhuolia
7ac6e0d349 Merge pull request #681 from bigcapitalhq/fix-pdf-template-customize-content
fix: Branding customize content
2024-09-30 14:52:36 +02:00
Ahmed Bouhuolia
4ec3586173 fix: branding customize content 2024-09-30 14:51:03 +02:00
Ahmed Bouhuolia
4b6ab7035e Merge pull request #680 from bigcapitalhq/add-posthog-events-tracking-to-pdf-templates
feat: Track pdf templates Posthog events
2024-09-30 12:44:27 +02:00
Ahmed Bouhuolia
3fe7babe00 feat: Track pdf templates Posthog events 2024-09-30 12:43:51 +02:00
Ahmed Bouhuolia
f21570982e Merge pull request #679 from bigcapitalhq/fix-listen-to-stripe-session-completed
fix: Listen to Stripe session completed event
2024-09-30 11:49:48 +02:00
Ahmed Bouhuolia
ad8fe52b84 fix: Listen to Stripe session completed event 2024-09-30 11:49:19 +02:00
Ahmed Bouhuolia
15ce6ac710 Merge pull request #678 from bigcapitalhq/pdf-templates-company-customer-address
feat: Pdf templates customer/company addresses
2024-09-30 11:21:14 +02:00
Ahmed Bouhuolia
783387dce6 fix: pdf templates server-side rendered 2024-09-30 11:15:05 +02:00
Ahmed Bouhuolia
863c7ad99f feat: Hook up customer/company address to pdf templates 2024-09-29 22:59:14 +02:00
Ahmed Bouhuolia
776b69475c feat: PDF templates company/customer address 2024-09-29 19:31:00 +02:00
Ahmed Bouhuolia
6b6027a588 feat: Pdf templates customer/company addresses 2024-09-29 18:04:56 +02:00
Ahmed Bouhuolia
d465ee15bd Merge pull request #677 from bigcapitalhq/preferences-company-branding
feat: Company branding preferences
2024-09-29 13:44:29 +02:00
Ahmed Bouhuolia
be2049ca6e feat: Pdf template address 2024-09-29 13:43:09 +02:00
Ahmed Bouhuolia
9b63c176cd feat: Hook up company address to payment page 2024-09-28 20:19:05 +02:00
Ahmed Bouhuolia
e506a7ba35 feat: Hook orgnization name and logo to payment page 2024-09-28 19:20:01 +02:00
Ahmed Bouhuolia
2191ad0d40 feat: hook up preferences branding form 2024-09-28 18:44:08 +02:00
Ahmed Bouhuolia
ca162206a3 feat: Organization address and branding patch endpoint 2024-09-28 17:43:47 +02:00
Ahmed Bouhuolia
c5d7a2bfd8 feat: Company branding preferences 2024-09-28 14:47:59 +02:00
Ahmed Bouhuolia
b9506424d1 Merge pull request #675 from bigcapitalhq/hook-up-company-logo-to-pdf-templates
feat: Hook up company logo to server-side pdf templates
2024-09-26 18:33:45 +02:00
Ahmed Bouhuolia
46a145ae58 feat: Hook up company logo to server-side pdf templates 2024-09-26 18:33:21 +02:00
Ahmed Bouhuolia
e4044ef563 Merge pull request #674 from bigcapitalhq/clean-up-payment-links-endpoints
feat: Clean up payment links endpoints
2024-09-25 19:38:28 +02:00
Ahmed Bouhuolia
1cc71eb368 feat: Clean up payment links endpoints 2024-09-25 19:37:40 +02:00
Ahmed Bouhuolia
323b95de7b Merge pull request #673 from bigcapitalhq/fix-invoice-customize-bugs
fix: Invoice customize bugs
2024-09-25 15:21:12 +02:00
Ahmed Bouhuolia
b0658be041 fix: Invoice customize bugs 2024-09-25 15:20:24 +02:00
Ahmed Bouhuolia
b222d56148 Merge pull request #672 from bigcapitalhq/fix-invoice-brand-customize
fix: Invoice pdf customize
2024-09-25 12:22:07 +02:00
Ahmed Bouhuolia
946872204b fix: payment page 2024-09-25 12:21:26 +02:00
Ahmed Bouhuolia
2f9adfd908 fix: Invoice pdf customize 2024-09-25 11:04:17 +02:00
Ahmed Bouhuolia
1c8e19378f Merge pull request #670 from bigcapitalhq/upload-company-logo
feat: Upload company logo to invoice templates
2024-09-24 20:31:12 +02:00
Ahmed Bouhuolia
37fd4a1fdb feat: Uploading company logo 2024-09-24 20:28:19 +02:00
Ahmed Bouhuolia
7aed3d9c8c Merge pull request #668 from bigcapitalhq/stripe-integrate
feat: Onboard accounts to Stripe Connect
2024-09-24 14:12:39 +02:00
Ahmed Bouhuolia
b125e3e58b feat: Stripe connect using OAuth 2024-09-24 14:10:53 +02:00
Crims-on
65788e344a Create authentication.tsx 2024-09-23 18:07:47 +02:00
Crims-on
abc242d117 Create locale.tsx 2024-09-23 18:07:14 +02:00
Crims-on
6dd4968327 deepl translation 2024-09-23 17:59:28 +02:00
Ahmed Bouhuolia
70bba4a6ed fix: Stripe integration content 2024-09-23 17:34:27 +02:00
Ahmed Bouhuolia
1570995021 feat: Add Stripe pre-setup dialog 2024-09-23 14:44:07 +02:00
Ahmed Bouhuolia
8109236e72 fix: Stripe payment integration 2024-09-23 13:21:54 +02:00
Ahmed Bouhuolia
9ba651decb feat: Delete Stripe pamyent connection 2024-09-22 21:57:46 +02:00
Ahmed Bouhuolia
eb5fdbf4ee feat: Control the payment method from invoice form 2024-09-22 21:23:02 +02:00
Ahmed Bouhuolia
9827a84857 feat: Hook up edit Stripe settings form 2024-09-22 17:25:27 +02:00
Ahmed Bouhuolia
3308133736 feat: Edit Stripe payment settings 2024-09-22 14:55:48 +02:00
Ahmed Bouhuolia
3129c76c30 feat: Delete Stripe payment method 2024-09-22 14:30:47 +02:00
Ahmed Bouhuolia
c0a4c965f0 feat: Edit stripe payment integation drawer 2024-09-22 12:52:59 +02:00
Ahmed Bouhuolia
d16c57b63b feat: Upload company logo to invoice templates 2024-09-22 00:01:12 +02:00
Ahmed Bouhuolia
e04f5d26a3 feat: listen to stripe account updated webhook 2024-09-21 23:59:54 +02:00
Ahmed Bouhuolia
ad74007d58 fix: Style of paper template address 2024-09-21 20:04:23 +02:00
Ahmed Bouhuolia
6271d6c268 chore: remove newrelic logs file 2024-09-21 19:13:20 +02:00
Ahmed Bouhuolia
7756b5b304 feat: Stripe payment integration 2024-09-21 16:50:22 +02:00
Ahmed Bouhuolia
8de8695b25 feat: clean up the style of public payment page. 2024-09-21 09:53:00 +02:00
Ahmed Bouhuolia
11c56c75a4 feat: clean up the stripe payment integration 2024-09-21 09:18:39 +02:00
Ahmed Bouhuolia
f5a1d68c52 feat: Stripe payment checkout session 2024-09-19 22:24:07 +02:00
Ahmed Bouhuolia
0ae7a25c27 feat: Map the invoice preview data 2024-09-19 14:32:14 +02:00
Ahmed Bouhuolia
16eaacd4bc feat: Payment invoice preview drawer 2024-09-19 12:45:06 +02:00
Ahmed Bouhuolia
809973730f feat: style tweaks in the public payment page 2024-09-19 11:28:40 +02:00
Ahmed Bouhuolia
2ebb4595a8 feat: Emit Stripe webhooks to events in the system 2024-09-19 10:25:13 +02:00
Ahmed Bouhuolia
77f628509c fix: make the base url of payment link configurable 2024-09-18 23:53:46 +02:00
Ahmed Bouhuolia
d2cd32a735 feat: inactive associated Stripe payment link on invoice deleting 2024-09-18 23:41:59 +02:00
Ahmed Bouhuolia
4665f529e6 feat: integrate Stripe payment to invoices 2024-09-18 19:24:01 +02:00
Ahmed Bouhuolia
df706d2573 feat: payment methods preferences page 2024-09-18 11:19:59 +02:00
Ahmed Bouhuolia
5270e99de8 feat: select payment methods dialog 2024-09-18 10:43:21 +02:00
Ahmed Bouhuolia
eb48f66f6e Merge branch 'develop' into stripe-integrate 2024-09-17 19:26:13 +02:00
Ahmed Bouhuolia
5e7cff0eb7 Merge pull request #667 from bigcapitalhq/invoice-customize
feat: customize pdf templates
2024-09-17 19:21:26 +02:00
Ahmed Bouhuolia
34e781b4a2 fix: typo in invoice customize drawer 2024-09-17 19:18:22 +02:00
Ahmed Bouhuolia
5f40d50852 fix: pdf template customization 2024-09-17 18:19:28 +02:00
Ahmed Bouhuolia
bb0d91a9cb fix: pdf templates 2024-09-17 17:46:56 +02:00
Ahmed Bouhuolia
2c790427fa feat: rendering pdf templates on the server-side 2024-09-17 13:53:57 +02:00
Ahmed Bouhuolia
4f59b27d70 feat: hook up branding templates to invoices 2024-09-16 20:02:17 +02:00
Ahmed Bouhuolia
94c08f0b9e chore: clean pdf templates code 2024-09-15 22:55:39 +02:00
Ahmed Bouhuolia
ef4beaa564 feat: seed initial standard branding templates 2024-09-15 22:01:11 +02:00
Ahmed Bouhuolia
2b42215381 feat: add loading state to generate payment link dialog 2024-09-15 21:08:41 +02:00
Ahmed Bouhuolia
18d6ec7b59 feat: style the generate payment link dialog 2024-09-15 21:03:36 +02:00
Ahmed Bouhuolia
430cf19533 feat: Link transations with payment methods 2024-09-15 19:42:43 +02:00
Ahmed Bouhuolia
542e61dbfc feat: sharable payment link dialog 2024-09-15 19:28:43 +02:00
Ahmed Bouhuolia
8566422ce3 fix: Add mising address in branding templates customize 2024-09-14 22:52:37 +02:00
Ahmed Bouhuolia
9517b4e279 feat: wip public payment page 2024-09-14 22:10:27 +02:00
Ahmed Bouhuolia
70551bee30 feat: the element customize submit button 2024-09-14 20:18:03 +02:00
Ahmed Bouhuolia
d690c6a3fe feat: optimize branding templates customiing 2024-09-14 19:32:16 +02:00
Ahmed Bouhuolia
28319c2cdc feat: cannot delete a predefined branding template 2024-09-14 16:26:34 +02:00
Ahmed Bouhuolia
df0f73f338 feat: mark specific template as default 2024-09-14 16:19:06 +02:00
Ahmed Bouhuolia
411ac55986 feat: templates customize 2024-09-12 17:49:00 +02:00
Ahmed Bouhuolia
12226d469a feat: pdf template customize 2024-09-12 16:50:44 +02:00
Ahmed Bouhuolia
632c4629de feat: hook up the invice customize api 2024-09-12 14:16:07 +02:00
Ahmed Bouhuolia
a7df23cebc feat: branding templates table 2024-09-11 21:16:21 +02:00
Ahmed Bouhuolia
ef74e250f1 feat: link pdf template to sales transactions 2024-09-11 16:49:44 +02:00
Ahmed Bouhuolia
c0769662bd feat(server): add pdf template crud endpoints 2024-09-11 14:54:13 +02:00
Ahmed Bouhuolia
5b6270a184 feat: invoice pdf customize 2024-09-10 23:32:34 +02:00
Ahmed Bouhuolia
4541d28b68 chore: dump CHANGELOG 2024-09-10 22:25:16 +02:00
Ahmed Bouhuolia
716dec799a feat: paper template customize 2024-09-10 21:54:37 +02:00
Ahmed Bouhuolia
77a1e35ff4 feat: Paper template reusable 2024-09-10 18:37:38 +02:00
Ahmed Bouhuolia
317adfa0de feat: wip estimate, receipt, payment received customize 2024-09-10 17:06:17 +02:00
Ahmed Bouhuolia
f0dfc3d1b0 feat: invoice customize paper preview 2024-09-10 13:29:25 +02:00
Ahmed Bouhuolia
67904f52af feat: add more customize drawers 2024-09-09 21:31:14 +02:00
Ahmed Bouhuolia
f644ed6708 feat: element customize component 2024-09-09 21:07:22 +02:00
Ahmed Bouhuolia
dc18bde6be feat: wip invoice customizer 2024-09-09 19:40:23 +02:00
Ahmed Bouhuolia
132c1dfdbe feat: craft the paper template style 2024-09-09 16:24:09 +02:00
Ahmed Bouhuolia
162b92ce84 feat: wip Stripe connect integration 2024-09-09 14:18:04 +02:00
Ahmed Bouhuolia
9247745ab0 feat: wip invoice customize 2024-09-08 21:01:54 +02:00
Ahmed Bouhuolia
c5c0342c7b feat: wip styling invoice customize 2024-09-08 20:13:27 +02:00
Ahmed Bouhuolia
f5e9485a12 feat: wip invoice customize 2024-09-08 17:34:19 +02:00
Ahmed Bouhuolia
a183666df6 feat: Onboard accounts to Stripe Connect 2024-09-08 11:42:26 +02:00
Ahmed Bouhuolia
e6bad27771 feat: wip invoice customizer 2024-09-07 21:39:05 +02:00
Ahmed Bouhuolia
6d24474162 Merge pull request #663 from bigcapitalhq/fix-uncategorize-bank-transaction
fix: Un-categorize bank transactions
2024-09-07 13:27:11 +02:00
Ahmed Bouhuolia
5962b990c4 fix: Uncategorize bank transactions 2024-09-07 13:26:02 +02:00
Ahmed Bouhuolia
9f21f649f0 Merge pull request #662 from bigcapitalhq/refactor-expense-gl
refactor: The expense G/L writer
2024-09-04 21:10:51 +02:00
Ahmed Bouhuolia
d3d2112b8a refactor: The expense G/L writer 2024-09-04 21:10:13 +02:00
Ahmed Bouhuolia
3795322a65 Merge pull request #659 from bigcapitalhq/format-money-table-columns
feat: Tabular number of all money columns
2024-09-04 18:48:38 +02:00
Ahmed Bouhuolia
fe5cd5a8ea feat: Mark more columns as money columns 2024-09-04 18:46:42 +02:00
Ahmed Bouhuolia
c032a5db16 Merge pull request #661 from bigcapitalhq/fix-payment-made-full-amount
fix: Payment made filling the form full amount field
2024-09-04 17:35:54 +02:00
Ahmed Bouhuolia
3fcb6fefde fix: Payment made filling the form full amount field 2024-09-04 17:34:19 +02:00
Ahmed Bouhuolia
16f5cb713d Merge pull request #660 from bigcapitalhq/fix-cast-array-rule-ids
fix: Array cast of recognize function rule ids
2024-09-04 16:49:32 +02:00
Ahmed Bouhuolia
f7a7925028 fix: Array cast of recognize function rule ids 2024-09-04 16:48:34 +02:00
Ahmed Bouhuolia
66fb0c9fa3 feat: Tabular number of all money columns 2024-09-04 14:58:31 +02:00
Ahmed Bouhuolia
85acc85f17 Merge pull request #655 from bigcapitalhq/ui-tweaks
feat: Datatable UI improvements
2024-09-04 14:02:43 +02:00
Ahmed Bouhuolia
c76ce09191 feat: optimize the style of bank account transactions tables 2024-09-04 13:59:13 +02:00
Ahmed Bouhuolia
e3532098b2 Merge branch 'develop' into ui-tweaks 2024-09-04 09:53:55 +02:00
Ahmed Bouhuolia
b6783eb22e Merge pull request #658 from bigcapitalhq/banking-layout-breaking
feat: Bank pages layout breaking
2024-09-04 09:52:35 +02:00
Ahmed Bouhuolia
4ef00ab122 feat: Bank pages layout breaking 2024-09-04 09:51:35 +02:00
Ahmed Bouhuolia
3dd827417a fix: remove duplicated event types 2024-09-03 17:31:00 +02:00
Ahmed Bouhuolia
cbacd02aa2 Merge pull request #656 from bigcapitalhq/add-help-dropdown-menu
feat: Add help dropdown menu
2024-09-03 17:28:12 +02:00
Ahmed Bouhuolia
3b7e0fb78a Merge pull request #657 from bigcapitalhq/suspense-lazy-banking-pages
fix: Suspense the lazy loaded components in banking pages
2024-09-03 17:26:02 +02:00
Ahmed Bouhuolia
9add716395 fix: Suspense the lazy loaded components in banking pages 2024-09-03 17:24:11 +02:00
Ahmed Bouhuolia
083ea28a1f feat: Add help dropdown menu 2024-09-03 16:41:20 +02:00
Ahmed Bouhuolia
1b51742c36 feat: Datatable UI improvements 2024-09-03 16:39:13 +02:00
Ahmed Bouhuolia
0c6f23e770 Merge pull request #654 from bigcapitalhq/expense-credit-card
fix: Expense cannot accept credit card as payment account
2024-09-03 12:23:03 +02:00
Ahmed Bouhuolia
37a8ca4e97 fix: Expense cannot accept credit card as payment account 2024-09-03 12:22:07 +02:00
Ahmed Bouhuolia
795303c3a8 Merge pull request #653 from bigcapitalhq/tracking-more-events
feat: Tracking more Posthog events
2024-09-03 11:28:27 +02:00
Ahmed Bouhuolia
63ba3f0898 Merge branch 'develop' into tracking-more-events 2024-09-03 11:28:05 +02:00
Ahmed Bouhuolia
62594efa00 feat: Tracking more Posthog events 2024-09-03 11:26:24 +02:00
Ahmed Bouhuolia
9ac0dcbdc3 Merge pull request #651 from bigcapitalhq/bank-transactions-events
feat: Track banking service events
2024-09-02 17:13:49 +02:00
Ahmed Bouhuolia
df588dc4ce Merge branch 'develop' into bank-transactions-events 2024-09-02 17:13:43 +02:00
Ahmed Bouhuolia
d54f14a87a feat: Track banking service events 2024-09-02 17:12:37 +02:00
Ahmed Bouhuolia
a4d4be54c1 Merge pull request #650 from bigcapitalhq/cover-more-tracking-events
feat: Cover more tracking events.
2024-09-02 15:20:11 +02:00
Ahmed Bouhuolia
ddd17e74b5 feat: Cover more tracking events. 2024-09-02 15:19:01 +02:00
Ahmed Bouhuolia
81c0761fbe Merge pull request #649 from bigcapitalhq/import-multi-branches-expenses
fix: Integrate multiple branches with expense resource
2024-09-02 15:02:53 +02:00
Ahmed Bouhuolia
0812e3087e fix: Integrate multiple branches with expense resource 2024-09-02 15:02:02 +02:00
Ahmed Bouhuolia
0dd05493b2 Merge pull request #645 from bigcapitalhq/multi-branches-warehoues-in-importing
feat: integrate multiple branches and warehouses to resource importing
2024-09-02 14:46:08 +02:00
Ahmed Bouhuolia
bfb3909d26 feat: integrate multiple branches and warehouses with import 2024-09-02 14:42:05 +02:00
Ahmed Bouhuolia
266902026e Merge pull request #648 from bigcapitalhq/fix-bank-transactions-infinity-scrolling
fix: Bank transactions infinity scrolling
2024-09-02 10:41:54 +02:00
Ahmed Bouhuolia
791c4a4e9e fix: Bank transactions infinity scrolling 2024-09-02 10:41:16 +02:00
Ahmed Bouhuolia
2cfa9123b8 fix: Syntax error 2024-09-01 23:32:54 +02:00
Ahmed Bouhuolia
bb9614aafb Merge pull request #646 from bigcapitalhq/events-tracking
feat(server): Events tracking using Posthog
2024-09-01 23:06:38 +02:00
Ahmed Bouhuolia
302564a56e fix: remove the un-used file 2024-09-01 23:02:41 +02:00
Ahmed Bouhuolia
dcfc231d4d feat(server): Events tracking using Posthog 2024-09-01 23:01:25 +02:00
Ahmed Bouhuolia
64080ed678 Merge pull request #644 from bigcapitalhq/set-default-index-entries
fix: Set default index to transaction entries
2024-09-01 15:10:40 +02:00
Ahmed Bouhuolia
263935d91e Merge pull request #643 from bigcapitalhq/import-bugs
fix: Import bugs
2024-09-01 15:02:31 +02:00
Ahmed Bouhuolia
7f5ffb8da1 fix: Add the missing columns to the payment received and made models 2024-09-01 15:00:51 +02:00
Ahmed Bouhuolia
f07d25edbe fix: Set default index to transaction entries 2024-09-01 13:32:47 +02:00
Ahmed Bouhuolia
7c07d6b5ff fix: The unimported functions 2024-09-01 11:16:50 +02:00
Ahmed Bouhuolia
e433f4ad68 fix: Item resource columns labels 2024-09-01 11:07:55 +02:00
Ahmed Bouhuolia
a79b9caff6 Merge pull request #641 from bigcapitalhq/fix-getting-sheet-columns
fix: Getting the sheet columns in import sheet
2024-08-30 17:56:53 +02:00
Ahmed Bouhuolia
2227cead66 Merge pull request #624 from bigcapitalhq/subscription-middleware
fix: Subscription middleware
2024-08-30 17:56:02 +02:00
Ahmed Bouhuolia
410c4ea3e2 fix: Subscription active detarminer 2024-08-30 17:52:53 +02:00
Ahmed Bouhuolia
f92acbcbe0 fix: Getting the sheet columns in import sheet 2024-08-30 17:03:16 +02:00
Ahmed Bouhuolia
c986585cd9 Merge pull request #640 from bigcapitalhq/fix-typo-one-click-demo
fix: Typo one-click demo page
2024-08-30 00:15:29 +02:00
Ahmed Bouhuolia
250f0a30ef fix: Typo one-click demo page 2024-08-30 00:14:59 +02:00
Ahmed Bouhuolia
84b5e1adc1 Merge pull request #639 from bigcapitalhq/add-customer-type-to-customers
fix: Add customer type to customers resource
2024-08-29 22:50:20 +02:00
Ahmed Bouhuolia
2ab28370db fix: Add customer type to customers resource 2024-08-29 22:49:49 +02:00
Ahmed Bouhuolia
a88a525326 Merge pull request #638 from bigcapitalhq/use-standard-date-format-export
fix: use standard ISO 8601 format for exported data
2024-08-29 22:40:59 +02:00
Ahmed Bouhuolia
fce8e2c5a4 fix: use standard ISO 8601 format for exported data 2024-08-29 22:39:51 +02:00
Ahmed Bouhuolia
095608266c Merge pull request #632 from bigcapitalhq/split-lazy-loading
feat: Optimize loading perf. by splitting big chunks and lazy loading them
2024-08-29 21:27:40 +02:00
Ahmed Bouhuolia
0ec8aaa330 fix: delete unwanted files 2024-08-29 21:27:20 +02:00
Ahmed Bouhuolia
9fcb3ef77d feat: Add fallback spinner to authentication lazy-loaded pages 2024-08-29 21:19:36 +02:00
Ahmed Bouhuolia
dc61c57daf fix: Add spinner to preferences lazy loaded pages 2024-08-29 21:03:27 +02:00
Ahmed Bouhuolia
af284f3f6d feat: split the preferences pages 2024-08-29 20:49:08 +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
c9fe6d9b37 feat: Optimize loading perf. by spliting big chunks and lazy loading them 2024-08-26 22:51:40 +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
ee2d8d3065 fix: subscription middleare 2024-08-25 12:42:42 +02:00
Ahmed Bouhuolia
959ef7a691 Merge branch 'listen-payment-webhooks' into develop 2024-08-24 21:52:04 +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
angelosorno
d805703c08 feat: Added Spanish language to the App 2024-07-16 14:56:05 -05:00
2073 changed files with 138411 additions and 7880 deletions

View File

@@ -159,6 +159,15 @@
"contributions": [
"code"
]
},
{
"login": "nklmantey",
"name": "Mantey",
"avatar_url": "https://avatars.githubusercontent.com/u/90279429?v=4",
"profile": "https://nklmantey.com/",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

View File

@@ -89,3 +89,14 @@ S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_ENDPOINT=
S3_BUCKET=
# PostHog
POSTHOG_API_KEY=
POSTHOG_HOST=
# Stripe Payment
STRIPE_PAYMENT_SECRET_KEY=
STRIPE_PAYMENT_PUBLISHABLE_KEY=
STRIPE_PAYMENT_CLIENT_ID=
STRIPE_PAYMENT_WEBHOOKS_SECRET=
STRIPE_PAYMENT_REDIRECT_URL=

View File

@@ -2,7 +2,191 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.19.4] - 18-08-2024
# [0.22.0]
* feat: estimate, receipt, credit note mail preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/757
* feat: Add discount to transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/758
* fix: update financial forms to use new formatted amount utilities and… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/760
* fix: total lines style by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/761
* fix: discount & adjustment sale transactions bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/762
* fix: discount transactions GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/763
# [0.21.2]
* hotbug: upload attachments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/755
# [0.21.1]
* fix: download invoice document on payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/750
* fix: attach branding template attrs to payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/751
* fix: make manual entries adjust decimal credit/debit amounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/754
* feat: allow quantity of entries accept decimal value by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/753
# [0.21.0]
* fix: Credit and debit totals not balancing when decimal values are used by @nklmantey in https://github.com/bigcapitalhq/bigcapital/pull/722
* docs: add nklmantey as a contributor for bug by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/725
* feat: track more services events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/721
* feat: Invoice mail receipt preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/723
* fix: change the send mail button on invoice drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/730
* refactor: notification mail services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/731
* fix: attach payment link in sending invoice mail receipt by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/732
* fix: send invoice drawer layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/733
* fix: hook up cc and bcc fields to mail sender by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/734
* fix: company logo does not show up in mail receipt preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/736
* fix: change default invoice mail message by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/737
* fix: typing invoice send mail fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/738
* fix: clean up ivnoice mail receipt preview component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/739
* feat: add shared package to pdf templates to render in the server and… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/735
* feat: getting invoice preview on send mail view by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/740
* fix: style SSR invoice paper template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/741
* fix: send invoice receipt addresses by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/742
* fix: due invoice server invoice by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/744
* fix: `BIG-265` forgot password text by @ibutiti in https://github.com/bigcapitalhq/bigcapital/pull/745
* Crims on sv translation by @Crims-on in https://github.com/bigcapitalhq/bigcapital/pull/671
* feat: Added Spanish language to the App 🇪🇸 by @angelosorno in https://github.com/bigcapitalhq/bigcapital/pull/530
* fix: mail services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/746
* fix: company logo of the template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/747
* fix: monorepo dependencies scope by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/748
# [0.20.6]
* fix: Import category column of item resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/710
* fix: Parse the uppercase values in importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/711
* chore: Move i18nApply localization to the account transformer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/713
* fix: Sync Plaid credit card account type by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/714
* fix: Sync account normal of cashflow GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/715
* feat: Add quantity column to pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/716
* feat: Pre-line invoice statements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/717
* feat: Invoice number in downloaded pdf document by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/718
* feat: Track events of pdf documents views by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/719
* fix: Customer note does not appear in pdf document by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/720
# [0.20.5]
* fix: Disable tabs of the pdf customization if the first field not filed up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/701
* fix: Invoice form layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/705
* refactor: Invoice, estimate, receipt, credit note and payment received date input fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/707
* feat: Add customize templates button to edit forms by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/708
* feat: Track account, invoice and item viewed events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/709
# [0.20.4]
* fix: Delete company logo from the PDF template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/699
* fix: Set max width/height to company logo of pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/700
# [0.20.3]
* feat: Assign default PDF template automatically by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/687
* fix: pdf template addresses controlling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/688
* fix: Remove empty lines from address formats by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/690
* fix: Pdf templates layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/691
* feat: Download invoice pdf of the payment link by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/689
* fix: Display country name by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/693
* feat: Add shared packages to Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/694
# [0.20.2]
* feat: Assign default PDF template automatically by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/687
* fix: pdf template addresses controlling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/688
* fix: Remove empty lines from address formats by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/690
* fix: Pdf templates layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/691
* feat: Download invoice pdf of the payment link by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/689
* fix: Display country name by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/693
* feat: Add shared packages to Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/694
# [0.20.1]
* fix: Getting uploaded object uri by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/684
# [0.20.0]
* feat: Customize pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/667
* feat: Onboard accounts to Stripe Connect by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/668
* feat: Upload company logo to invoice templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/670
* fix: Invoice pdf customize by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/672
* fix: Invoice customize bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/673
* feat: Clean up payment links endpoints by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/674
* feat: Hook up company logo to server-side pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/675
* feat: Company branding preferences by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/677
* feat: Pdf templates customer/company addresses by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/678
* fix: Listen to Stripe session completed event by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/679
* feat: Track pdf templates Posthog events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/680
* fix: Branding customize content by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/681
* feat: Listen to Stripe integration events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/682
* feat: Hook up customer/company address to invoice preview of payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/683
# [0.19.17]
* fix: Un-categorize bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/663
# [0.19.16]
* feat: Tracking more Posthog events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/653
* fix: Expense cannot accept credit card as payment account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/654
* fix: Suspense the lazy loaded components in banking pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/657
* feat: Add help dropdown menu by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/656
* feat: Bank pages layout breaking by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/658
* feat: Datatable UI improvements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/655
* fix: Array cast of recognize function rule ids by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/660
* fix: Payment made filling the form full amount field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/661
* feat: Tabular number of all money columns by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/659
* refactor: The expense G/L writer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/662
## [0.19.15] -
* fix: Bank transactions infinity scrolling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/648
* feat: Integrate multiple branches and warehouses to resource importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/645
* fix: Integrate multiple branches with expense resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/649
* feat: Cover more tracking events. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/650
* feat: Track banking service events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/651
## [0.19.14]
* fix: Import bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/643
* fix: Set default index to transaction entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/644
* feat(server): Events tracking using Posthog by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/646
## [0.19.13]
* fix: Subscription middleware by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/624
* fix: Getting the sheet columns in import sheet by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/641
## [0.19.12]
* fix: Typo one-click demo page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/640
## [0.19.11]
* fix: Avoid running the cost job in import preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/635
* fix: Debounce scheduling calculating items cost by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/634
* fix: Expand the resources export page size limitation by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/636
* feat: Optimize loading perf. by splitting big chunks and lazy loading them by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/632
* fix: Use standard ISO 8601 format for exported data by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/638
* fix: Add customer type to customers resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/639
## [0.19.10]
* fix: Add subscription plans offer text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/629
## [0.19.9]
* fix: Make webapp package env variables dynamic by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/628
## [v0.19.8]
* fix: Cannot import items income and cost accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/617
* fix: Some bank account details hidden by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/618
* feat(ee): One-click demo account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/616
* feat: change banking service language by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/619
* feat(banking): Filter uncategorized bank transactions by date by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/590
* Fix: Syntax error caused error by @wolone in https://github.com/bigcapitalhq/bigcapital/pull/622
* fix: Listen to payment webhooks by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/623
* fix: Add prefix J-00001 to manual journals increments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/625
* fix: Disable sms service until Twilo integration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/626
* fix: Style tweaks in onboarding page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/627
## [0.19.5] - 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
@@ -23,6 +207,15 @@ All notable changes to Bigcapital server-side will be in this file.
* 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.19.4]
* feat: Import and export tax rates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/591
* feat: Un-categorize bank transactions in bulk by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/587
* feat: Pending bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/589
* fix: Update `dev` Script in `package.json` to Use `cross-env` by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/588
* fix: Should not load branches on reconcile matching form if the branches not enabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/592
* fix: Rounding the total amount the pending and matched transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/593
## [v0.18.0] - 10-08-2024
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511

View File

@@ -12,6 +12,9 @@
<a href="https://github.com/bigcapitalhq/bigcapital/commits/develop">
<img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" />
</a>
<a href="https://hub.docker.com/u/bigcapitalhq">
<img src="https://img.shields.io/docker/pulls/bigcapitalhq/webapp" />
</a>
<a href="https://discord.com/invite/c8nPBJafeb">
<img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" />
</a>
@@ -130,6 +133,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<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>
<td align="center" valign="top" width="14.28%"><a href="https://nklmantey.com/"><img src="https://avatars.githubusercontent.com/u/90279429?v=4?s=100" width="100px;" alt="Mantey"/><br /><sub><b>Mantey</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Anklmantey" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>

View File

@@ -3,6 +3,7 @@
"version": "independent",
"npmClient": "pnpm",
"packages": [
"packages/*"
"packages/*",
"shared/*"
]
}

View File

@@ -4,12 +4,14 @@
"scripts": {
"dev": "lerna run dev",
"build": "lerna run build",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\"",
"build:server": "lerna run build --scope \"@bigcapital/server\"",
"serve:server": "lerna run serve --scope \"@bigcapital/server\"",
"test:e2e": "playwright test",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\"",
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"",
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"server2:start": "lerna run start:dev --scope \"@bigcapital/server2\"",
"test:watch": "lerna run test:watch",
"test:e2e": "lerna run test:e2e",
"prepare": "husky install"
},
"devDependencies": {
@@ -29,5 +31,8 @@
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"dependencies": {
"tsup": "^8.3.0"
}
}

View File

@@ -0,0 +1,102 @@
# Mail
MAIL_HOST=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_PORT=
MAIL_SECURE=
MAIL_FROM_NAME=
MAIL_FROM_ADDRESS=
# Database
DB_HOST=localhost
DB_USER=bigcapital
DB_PASSWORD=bigcapital
DB_ROOT_PASSWORD=root
DB_CHARSET=utf8
# System database
SYSTEM_DB_NAME=bigcapital_system
# SYSTEM_DB_USER=
# SYSTEM_DB_PASSWORD=
# SYSTEM_DB_NAME=
# SYSTEM_DB_CHARSET=
# Tenant databases
TENANT_DB_NAME_PERFIX=bigcapital_tenant_
# TENANT_DB_HOST=
# TENANT_DB_USER=
# TENANT_DB_PASSWORD=
# TENANT_DB_CHARSET=
# Application
BASE_URL=http://example.com
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
# Jobs MongoDB
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
# App proxy
PUBLIC_PROXY_PORT=80
PUBLIC_PROXY_SSL_PORT=443
# Agendash
AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123
# Sign-up restrictions
SIGNUP_DISABLED=false
SIGNUP_ALLOWED_DOMAINS=
SIGNUP_ALLOWED_EMAILS=
# Sign-up Email Confirmation
SIGNUP_EMAIL_CONFIRMATION=false
# API rate limit (points,duration,block duration).
API_RATE_LIMIT=120,60,600
# Gotenberg API for PDF printing - (production).
GOTENBERG_URL=http://gotenberg:3000
GOTENBERG_DOCS_URL=http://server:3000/public/
# Gotenberg API - (development)
# GOTENBERG_URL=http://localhost:9000
# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/
# Exchange Rate Service
EXCHANGE_RATE_SERVICE=open-exchange-rate
# Open Exchange Rate
OPEN_EXCHANGE_RATE_APP_ID=
# The Plaid environment to use ('sandbox' or 'development').
# https://plaid.com/docs/#api-host
PLAID_ENV=sandbox
# Your Plaid keys, which can be found in the Plaid Dashboard.
# https://dashboard.plaid.com/account/keys
PLAID_CLIENT_ID=
PLAID_SECRET=
PLAID_LINK_WEBHOOK=
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
LEMONSQUEEZY_API_KEY=
LEMONSQUEEZY_STORE_ID=
LEMONSQUEEZY_WEBHOOK_SECRET=
# S3 documents and attachments
S3_REGION=US
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_ENDPOINT=
S3_BUCKET=
# PostHog
POSTHOG_API_KEY=
POSTHOG_HOST=
# Stripe Payment
STRIPE_PAYMENT_SECRET_KEY=
STRIPE_PAYMENT_PUBLISHABLE_KEY=
STRIPE_PAYMENT_CLIENT_ID=
STRIPE_PAYMENT_WEBHOOKS_SECRET=
STRIPE_PAYMENT_REDIRECT_URL=

View File

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

56
packages/server-nest/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"assets": [
{ "include": "i18n/**/*", "watchAssets": true }
]
}
}

View File

@@ -0,0 +1,127 @@
{
"name": "@bigcapital/server2",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json --watchAll"
},
"dependencies": {
"@bigcapital/email-components": "*",
"@bigcapital/pdf-templates": "*",
"@bigcapital/utils": "*",
"@nestjs/bull": "^10.2.1",
"@nestjs/bullmq": "^10.2.1",
"@nestjs/cache-manager": "^2.2.2",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^10.0.0",
"@nestjs/event-emitter": "^2.0.4",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.4.2",
"@nestjs/throttler": "^6.2.1",
"@supercharge/promise-pool": "^3.2.0",
"@types/nodemailer": "^6.4.17",
"@types/passport-local": "^1.0.38",
"@types/ramda": "^0.30.2",
"accounting": "^0.4.1",
"async": "^3.2.0",
"axios": "^1.6.0",
"bluebird": "^3.7.2",
"bull": "^4.16.3",
"bullmq": "^5.21.1",
"cache-manager": "^6.1.1",
"cache-manager-redis-store": "^3.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"express-validator": "^7.2.0",
"form-data": "^4.0.0",
"fp-ts": "^2.16.9",
"is-my-json-valid": "^2.20.5",
"js-money": "^0.6.3",
"knex": "^3.1.0",
"lamda": "^0.4.1",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"mysql": "^2.18.1",
"mysql2": "^3.11.3",
"nestjs-cls": "^4.4.1",
"nestjs-i18n": "^10.4.9",
"nodemailer": "^6.3.0",
"object-hash": "^2.0.3",
"objection": "^3.1.5",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"plaid": "^10.3.0",
"pluralize": "^8.0.0",
"posthog-node": "^4.3.2",
"pug": "^3.0.2",
"ramda": "^0.30.1",
"redis": "^4.7.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"serialize-interceptor": "^1.1.7",
"strategy": "^1.1.1",
"uniqid": "^5.2.0",
"uuid": "^10.0.0",
"yup": "^0.28.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@types/yup": "^0.29.13",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('gotenberg', () => ({
url: process.env.GOTENBERG_URL,
docsUrl: process.env.GOTENBERG_DOCS_URL,
}));

View File

@@ -0,0 +1,21 @@
import systemDatabase from './system-database';
import tenantDatabase from './tenant-database';
import signup from './signup';
import gotenberg from './gotenberg';
import plaid from './plaid';
import lemonsqueezy from './lemonsqueezy';
import s3 from './s3';
import openExchange from './open-exchange';
import posthog from './posthog';
export const config = [
systemDatabase,
tenantDatabase,
signup,
gotenberg,
plaid,
lemonsqueezy,
s3,
openExchange,
posthog,
];

View File

@@ -0,0 +1,7 @@
import { registerAs } from '@nestjs/config';
export default registerAs('lemonsqueezy', () => ({
apiKey: process.env.LEMONSQUEEZY_API_KEY,
storeId: process.env.LEMONSQUEEZY_STORE_ID,
webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
}));

View File

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

View File

@@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
export default registerAs('plaid', () => ({
env: process.env.PLAID_ENV || 'sandbox',
clientId: process.env.PLAID_CLIENT_ID,
secret: process.env.PLAID_SECRET,
linkWebhook: process.env.PLAID_LINK_WEBHOOK,
}));

View File

@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('posthog', () => ({
apiKey: process.env.POSTHOG_API_KEY,
host: process.env.POSTHOG_HOST || 'https://us.i.posthog.com',
}));

View File

@@ -0,0 +1,9 @@
import { registerAs } from '@nestjs/config';
export default registerAs('s3', () => ({
region: process.env.S3_REGION || 'US',
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET,
}));

View File

@@ -0,0 +1,12 @@
import { registerAs } from '@nestjs/config';
export default registerAs('signup', () => ({
disabled: process.env.SIGNUP_DISABLED === 'true',
allowedDomains: process.env.SIGNUP_ALLOWED_DOMAINS
? process.env.SIGNUP_ALLOWED_DOMAINS.split(',')
: [],
allowedEmails: process.env.SIGNUP_ALLOWED_EMAILS
? process.env.SIGNUP_ALLOWED_EMAILS.split(',')
: [],
emailConfirmation: process.env.SIGNUP_EMAIL_CONFIRMATION === 'true',
}));

View File

@@ -0,0 +1,9 @@
import { registerAs } from '@nestjs/config';
export default registerAs('stripePayment', () => ({
secretKey: process.env.STRIPE_PAYMENT_SECRET_KEY,
publishableKey: process.env.STRIPE_PAYMENT_PUBLISHABLE_KEY,
clientId: process.env.STRIPE_PAYMENT_CLIENT_ID,
webhooksSecret: process.env.STRIPE_PAYMENT_WEBHOOKS_SECRET,
redirectUrl: process.env.STRIPE_PAYMENT_REDIRECT_URL,
}));

View File

@@ -0,0 +1,10 @@
import { registerAs } from '@nestjs/config';
export default registerAs('systemDatabase', () => ({
client: 'mysql',
host: process.env.SYSTEM_DB_HOST || process.env.DB_HOST,
port: process.env.SYSTEM_DB_PORT || process.env.DB_PORT || 5432,
user: process.env.SYSTEM_DB_USER || process.env.DB_USER,
password: process.env.SYSTEM_DB_PASSWORD || process.env.DB_PASSWORD,
databaseName: process.env.SYSTEM_DB_NAME || process.env.DB_NAME,
}));

View File

@@ -0,0 +1,9 @@
import { registerAs } from '@nestjs/config';
export default registerAs('tenantDatabase', () => ({
client: 'mysql',
host: process.env.TENANT_DB_HOST || process.env.DB_HOST,
port: process.env.TENANT_DB_PORT || process.env.DB_PORT || 5432,
user: process.env.TENANT_DB_USER || process.env.DB_USER,
password: process.env.TENANT_DB_PASSWORD || process.env.DB_PASSWORD,
}));

View File

@@ -0,0 +1,756 @@
export const events = {
/**
* Authentication service.
*/
auth: {
signIn: 'onSignIn',
signingIn: 'onSigningIn',
signUp: 'onSignUp',
signingUp: 'onSigningUp',
signUpConfirming: 'signUpConfirming',
signUpConfirmed: 'signUpConfirmed',
sendingResetPassword: 'onSendingResetPassword',
sendResetPassword: 'onSendResetPassword',
resetPassword: 'onResetPassword',
resetingPassword: 'onResetingPassword',
},
/**
* Invite users service.
*/
inviteUser: {
acceptInvite: 'onUserAcceptInvite',
sendInvite: 'onUserSendInvite',
resendInvite: 'onUserInviteResend',
checkInvite: 'onUserCheckInvite',
sendInviteTenantSynced: 'onUserSendInviteTenantSynced',
},
/**
* Organization managment service.
*/
organization: {
build: 'onOrganizationBuild',
built: 'onOrganizationBuilt',
seeded: 'onOrganizationSeeded',
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
},
/**
* Organization subscription.
*/
subscription: {
onSubscriptionCancel: 'onSubscriptionCancel',
onSubscriptionCancelled: 'onSubscriptionCancelled',
onSubscriptionResume: 'onSubscriptionResume',
onSubscriptionResumed: 'onSubscriptionResumed',
onSubscriptionPlanChange: 'onSubscriptionPlanChange',
onSubscriptionPlanChanged: 'onSubscriptionPlanChanged',
onSubscriptionSubscribed: 'onSubscriptionSubscribed',
onSubscriptionPaymentSucceed: 'onSubscriptionPaymentSucceed',
onSubscriptionPaymentFailed: 'onSubscriptionPaymentFailed',
},
/**
* Tenants managment service.
*/
tenantManager: {
databaseCreated: 'onDatabaseCreated',
tenantMigrated: 'onTenantMigrated',
tenantSeeded: 'onTenantSeeded',
},
/**
* Accounts service.
*/
accounts: {
onViewed: 'onAccountViewed',
onListViewed: 'onAccountsListViewed',
onCreating: 'onAccountCreating',
onCreated: 'onAccountCreated',
onEditing: 'onAccountEditing',
onEdited: 'onAccountEdited',
onDelete: 'onAccountDelete',
onDeleted: 'onAccountDeleted',
onBulkDeleted: 'onBulkDeleted',
onBulkActivated: 'onAccountBulkActivated',
onActivated: 'onAccountActivated',
},
/**
* Manual journals service.
*/
manualJournals: {
onCreating: 'onManualJournalCreating',
onCreated: 'onManualJournalCreated',
onEditing: 'onManualJournalEditing',
onEdited: 'onManualJournalEdited',
onDeleting: 'onManualJournalDeleting',
onDeleted: 'onManualJournalDeleted',
onPublished: 'onManualJournalPublished',
onPublishing: 'onManualJournalPublishing',
},
/**
* Expenses service.
*/
expenses: {
onCreating: 'onExpenseCreating',
onCreated: 'onExpenseCreated',
onEditing: 'onExpenseEditing',
onEdited: 'onExpenseEdited',
onDeleting: 'onExpenseDeleting',
onDeleted: 'onExpenseDeleted',
onPublishing: 'onExpensePublishing',
onPublished: 'onExpensePublished',
},
/**
* Sales invoices service.
*/
saleInvoice: {
onViewed: 'onSaleInvoiceItemViewed',
onListViewed: 'onSaleInvoiceListViewed',
onPdfViewed: 'onSaleInvoicePdfViewed',
onCreate: 'onSaleInvoiceCreate',
onCreating: 'onSaleInvoiceCreating',
onCreated: 'onSaleInvoiceCreated',
onEdit: 'onSaleInvoiceEdit',
onEditing: 'onSaleInvoiceEditing',
onEdited: 'onSaleInvoiceEdited',
onDelete: 'onSaleInvoiceDelete',
onDeleting: 'onSaleInvoiceDeleting',
onDeleted: 'onSaleInvoiceDeleted',
onDelivering: 'onSaleInvoiceDelivering',
onDeliver: 'onSaleInvoiceDeliver',
onDelivered: 'onSaleInvoiceDelivered',
onPublish: 'onSaleInvoicePublish',
onPublished: 'onSaleInvoicePublished',
onWriteoff: 'onSaleInvoiceWriteoff',
onWrittenoff: 'onSaleInvoiceWrittenoff',
onWrittenoffCancel: 'onSaleInvoiceWrittenoffCancel',
onWrittenoffCanceled: 'onSaleInvoiceWrittenoffCanceled',
onNotifySms: 'onSaleInvoiceNotifySms',
onNotifiedSms: 'onSaleInvoiceNotifiedSms',
onNotifyMail: 'onSaleInvoiceNotifyMail',
onNotifyReminderMail: 'onSaleInvoiceNotifyReminderMail',
onPreMailSend: 'onSaleInvoicePreMailSend',
onMailSend: 'onSaleInvoiceMailSend',
onMailSent: 'onSaleInvoiceMailSent',
onMailReminderSend: 'onSaleInvoiceMailReminderSend',
onMailReminderSent: 'onSaleInvoiceMailReminderSent',
onPublicLinkGenerating: 'onPublicSharableLinkGenerating',
onPublicLinkGenerated: 'onPublicSharableLinkGenerated',
},
/**
* Sales estimates service.
*/
saleEstimate: {
onPdfViewed: 'onSaleEstimatePdfViewed',
onCreating: 'onSaleEstimateCreating',
onCreated: 'onSaleEstimateCreated',
onEditing: 'onSaleEstimateEditing',
onEdited: 'onSaleEstimateEdited',
onDeleting: 'onSaleEstimatedDeleting',
onDeleted: 'onSaleEstimatedDeleted',
onPublishing: 'onSaleEstimatedPublishing',
onPublished: 'onSaleEstimatedPublished',
onNotifySms: 'onSaleEstimateNotifySms',
onNotifiedSms: 'onSaleEstimateNotifiedSms',
onDelivering: 'onSaleEstimateDelivering',
onDelivered: 'onSaleEstimateDelivered',
onConvertedToInvoice: 'onSaleEstimateConvertedToInvoice',
onApproving: 'onSaleEstimateApproving',
onApproved: 'onSaleEstimateApproved',
onRejecting: 'onSaleEstimateRejecting',
onRejected: 'onSaleEstimateRejected',
onNotifyMail: 'onSaleEstimateNotifyMail',
onPreMailSend: 'onSaleEstimatePreMailSend',
onMailSend: 'onSaleEstimateMailSend',
onMailSent: 'onSaleEstimateMailSend',
onViewed: 'onSaleEstimateViewed',
},
/**
* Sales receipts service.
*/
saleReceipt: {
onPdfViewed: 'onSaleReceiptPdfViewed',
onCreating: 'onSaleReceiptsCreating',
onCreated: 'onSaleReceiptsCreated',
onEditing: 'onSaleReceiptsEditing',
onEdited: 'onSaleReceiptsEdited',
onDeleting: 'onSaleReceiptsDeleting',
onDeleted: 'onSaleReceiptsDeleted',
onPublishing: 'onSaleReceiptPublishing',
onPublished: 'onSaleReceiptPublished',
onClosed: 'onSaleReceiptClosed',
onClosing: 'onSaleReceiptClosing',
onNotifySms: 'onSaleReceiptNotifySms',
onNotifiedSms: 'onSaleReceiptNotifiedSms',
onPreMailSend: 'onSaleReceiptPreMailSend',
onMailSend: 'onSaleReceiptMailSend',
onMailSent: 'onSaleReceiptMailSent',
},
/**
* Payment receipts service.
*/
paymentReceive: {
onPdfViewed: 'onPaymentReceivedPdfViewed',
onCreated: 'onPaymentReceiveCreated',
onCreating: 'onPaymentReceiveCreating',
onEditing: 'onPaymentReceiveEditing',
onEdited: 'onPaymentReceiveEdited',
onDeleting: 'onPaymentReceiveDeleting',
onDeleted: 'onPaymentReceiveDeleted',
onPublishing: 'onPaymentReceivePublishing',
onPublished: 'onPaymentReceivePublished',
onNotifySms: 'onPaymentReceiveNotifySms',
onNotifiedSms: 'onPaymentReceiveNotifiedSms',
onPreMailSend: 'onPaymentReceivePreMailSend',
onMailSend: 'onPaymentReceiveMailSend',
onMailSent: 'onPaymentReceiveMailSent',
},
/**
* Bills service.
*/
bill: {
onCreating: 'onBillCreating',
onCreated: 'onBillCreated',
onEditing: 'onBillEditing',
onEdited: 'onBillEdited',
onDeleting: 'onBillDeleting',
onDeleted: 'onBillDeleted',
onPublishing: 'onBillPublishing',
onPublished: 'onBillPublished',
onOpening: 'onBillOpening',
onOpened: 'onBillOpened',
},
/**
* Bill payments service.
*/
billPayment: {
onCreating: 'onBillPaymentCreating',
onCreated: 'onBillPaymentCreated',
onEditing: 'onBillPaymentEditing',
onEdited: 'onBillPaymentEdited',
onDeleted: 'onBillPaymentDeleted',
onDeleting: 'onBillPaymentDeleting',
onPublishing: 'onBillPaymentPublishing',
onPublished: 'onBillPaymentPublished',
},
/**
* Customers services.
*/
customers: {
onCreating: 'onCustomerCreating',
onCreated: 'onCustomerCreated',
onEdited: 'onCustomerEdited',
onEditing: 'onCustomerEditing',
onDeleted: 'onCustomerDeleted',
onDeleting: 'onCustomerDeleting',
onBulkDeleted: 'onBulkDeleted',
onOpeningBalanceChanging: 'onCustomerOpeningBalanceChanging',
onOpeningBalanceChanged: 'onCustomerOpeingBalanceChanged',
onActivating: 'onCustomerActivating',
onActivated: 'onCustomerActivated',
},
/**
* Vendors services.
*/
vendors: {
onCreated: 'onVendorCreated',
onCreating: 'onVendorCreating',
onEdited: 'onVendorEdited',
onEditing: 'onVendorEditing',
onDeleted: 'onVendorDeleted',
onDeleting: 'onVendorDeleting',
onOpeningBalanceChanging: 'onVendorOpeingBalanceChanging',
onOpeningBalanceChanged: 'onVendorOpeingBalanceChanged',
onActivating: 'onVendorActivating',
onActivated: 'onVendorActivated',
},
/**
* Items service.
*/
item: {
onViewed: 'onItemViewed',
onCreated: 'onItemCreated',
onCreating: 'onItemCreating',
onEditing: 'onItemEditing',
onEdited: 'onItemEdited',
onDeleted: 'onItemDeleted',
onDeleting: 'onItemDeleting',
onActivating: 'onItemActivating',
onActivated: 'onItemActivated',
onInactivating: 'onInactivating',
onInactivated: 'onItemInactivated',
},
/**
* Item category service.
*/
itemCategory: {
onCreated: 'onItemCategoryCreated',
onEdited: 'onItemCategoryEdited',
onDeleted: 'onItemCategoryDeleted',
onBulkDeleted: 'onItemCategoryBulkDeleted',
},
/**
* Inventory service.
*/
inventory: {
onInventoryTransactionsCreated: 'onInventoryTransactionsCreated',
onInventoryTransactionsDeleted: 'onInventoryTransactionsDeleted',
onComputeItemCostJobScheduled: 'onComputeItemCostJobScheduled',
onComputeItemCostJobStarted: 'onComputeItemCostJobStarted',
onComputeItemCostJobCompleted: 'onComputeItemCostJobCompleted',
onInventoryCostEntriesWritten: 'onInventoryCostEntriesWritten',
onCostLotsGLEntriesBeforeWrite: 'onInventoryCostLotsGLEntriesBeforeWrite',
onCostLotsGLEntriesWrite: 'onInventoryCostLotsGLEntriesWrite',
},
/**
* Inventory adjustment service.
*/
inventoryAdjustment: {
onQuickCreating: 'onInventoryAdjustmentCreating',
onQuickCreated: 'onInventoryAdjustmentQuickCreated',
onCreated: 'onInventoryAdjustmentCreated',
onDeleting: 'onInventoryAdjustmentDeleting',
onDeleted: 'onInventoryAdjustmentDeleted',
onPublishing: 'onInventoryAdjustmentPublishing',
onPublished: 'onInventoryAdjustmentPublished',
},
/**
* Bill landed cost.
*/
billLandedCost: {
onCreate: 'onBillLandedCostCreate',
onCreated: 'onBillLandedCostCreated',
onDelete: 'onBillLandedCostDelete',
onDeleted: 'onBillLandedCostDeleted',
},
cashflow: {
onOwnerContributionCreate: 'onCashflowOwnerContributionCreate',
onOwnerContributionCreated: 'onCashflowOwnerContributionCreated',
onOtherIncomeCreate: 'onCashflowOtherIncomeCreate',
onOtherIncomeCreated: 'onCashflowOtherIncomeCreated',
onTransactionCreating: 'onCashflowTransactionCreating',
onTransactionCreated: 'onCashflowTransactionCreated',
onTransactionDeleting: 'onCashflowTransactionDeleting',
onTransactionDeleted: 'onCashflowTransactionDeleted',
onTransactionCategorizing: 'onTransactionCategorizing',
onTransactionCategorized: 'onCashflowTransactionCategorized',
onTransactionUncategorizedCreating: 'onTransactionUncategorizedCreating',
onTransactionUncategorizedCreated: 'onTransactionUncategorizedCreated',
onTransactionUncategorizing: 'onTransactionUncategorizing',
onTransactionUncategorized: 'onTransactionUncategorized',
onTransactionCategorizingAsExpense: 'onTransactionCategorizingAsExpense',
onTransactionCategorizedAsExpense: 'onTransactionCategorizedAsExpense',
},
/**
* Roles service events.
*/
roles: {
onCreate: 'onRoleCreate',
onCreated: 'onRoleCreated',
onEdit: 'onRoleEdit',
onEdited: 'onRoleEdited',
onDelete: 'onRoleDelete',
onDeleted: 'onRoleDeleted',
},
tenantUser: {
onEdited: 'onTenantUserEdited',
onDeleted: 'onTenantUserDeleted',
onActivated: 'onTenantUserActivated',
onInactivated: 'onTenantUserInactivated',
},
/**
* Credit note service.
*/
creditNote: {
onPdfViewed: 'onCreditNotePdfViewed',
onCreate: 'onCreditNoteCreate',
onCreating: 'onCreditNoteCreating',
onCreated: 'onCreditNoteCreated',
onEditing: 'onCreditNoteEditing',
onEdit: 'onCreditNoteEdit',
onEdited: 'onCreditNoteEdited',
onDelete: 'onCreditNoteDelete',
onDeleting: 'onCreditNoteDeleting',
onDeleted: 'onCreditNoteDeleted',
onOpen: 'onCreditNoteOpen',
onOpening: 'onCreditNoteOpening',
onOpened: 'onCreditNoteOpened',
onRefundCreate: 'onCreditNoteRefundCreate',
onRefundCreating: 'onCreditNoteRefundCreating',
onRefundCreated: 'onCreditNoteRefundCreated',
onRefundDelete: 'onCreditNoteRefundDelete',
onRefundDeleting: 'onCreditNoteRefundDeleting',
onRefundDeleted: 'onCreditNoteRefundDeleted',
onApplyToInvoicesCreated: 'onCreditNoteApplyToInvoiceCreated',
onApplyToInvoicesCreate: 'onCreditNoteApplyToInvoiceCreate',
onApplyToInvoicesDeleted: 'onCreditNoteApplyToInvoiceDeleted',
},
/**
* Vendor credit service.
*/
vendorCredit: {
onCreate: 'onVendorCreditCreate',
onCreating: 'onVendorCreditCreating',
onCreated: 'onVendorCreditCreated',
onEdit: 'onVendorCreditEdit',
onEditing: 'onVendorCreditEditing',
onEdited: 'onVendorCreditEdited',
onDelete: 'onVendorCreditDelete',
onDeleting: 'onVendorCreditDeleting',
onDeleted: 'onVendorCreditDeleted',
onOpen: 'onVendorCreditOpen',
onOpened: 'onVendorCreditOpened',
onRefundCreating: 'onVendorCreditRefundCreating',
onRefundCreate: 'onVendorCreditRefundCreate',
onRefundCreated: 'onVendorCreditRefundCreated',
onRefundDelete: 'onVendorCreditRefundDelete',
onRefundDeleting: 'onVendorCreditRefundDeleting',
onRefundDeleted: 'onVendorCreditRefundDeleted',
onApplyToInvoicesCreated: 'onVendorCreditApplyToInvoiceCreated',
onApplyToInvoicesCreate: 'onVendorCreditApplyToInvoiceCreate',
onApplyToInvoicesDeleted: 'onVendorCreditApplyToInvoiceDeleted',
},
transactionsLocking: {
locked: 'onTransactionLockingLocked',
lockCanceled: 'onTransactionLockingLockCanceled',
partialUnlocked: 'onTransactionLockingPartialUnlocked',
partialUnlockCanceled: 'onTransactionLockingPartialUnlockCanceled',
},
warehouse: {
onCreate: 'onWarehouseCreate',
onCreated: 'onWarehouseCreated',
onEdit: 'onWarehouseEdit',
onEdited: 'onWarehouseEdited',
onDelete: 'onWarehouseDelete',
onDeleted: 'onWarehouseDeleted',
onActivate: 'onWarehouseActivate',
onActivated: 'onWarehouseActivated',
onMarkPrimary: 'onWarehouseMarkPrimary',
onMarkedPrimary: 'onWarehouseMarkedPrimary',
},
warehouseTransfer: {
onCreate: 'onWarehouseTransferCreate',
onCreated: 'onWarehouseTransferCreated',
onEdit: 'onWarehouseTransferEdit',
onEdited: 'onWarehouseTransferEdited',
onDelete: 'onWarehouseTransferDelete',
onDeleted: 'onWarehouseTransferDeleted',
onInitiate: 'onWarehouseTransferInitiate',
onInitiated: 'onWarehouseTransferInitated',
onTransfer: 'onWarehouseTransferInitiate',
onTransferred: 'onWarehouseTransferTransferred',
},
/**
* Branches.
*/
branch: {
onActivate: 'onBranchActivate',
onActivated: 'onBranchActivated',
onMarkPrimary: 'onBranchMarkPrimary',
onMarkedPrimary: 'onBranchMarkedPrimary',
},
/**
* Projects.
*/
project: {
onCreate: 'onProjectCreate',
onCreating: 'onProjectCreating',
onCreated: 'onProjectCreated',
onEdit: 'onEditProject',
onEditing: 'onEditingProject',
onEdited: 'onEditedProject',
onEditStatus: 'onEditStatusProject',
onEditingStatus: 'onEditingStatusProject',
onEditedStatus: 'onEditedStatusProject',
onDelete: 'onDeleteProject',
onDeleting: 'onDeletingProject',
onDeleted: 'onDeletedProject',
},
/**
* Project Tasks.
*/
projectTask: {
onCreate: 'onProjectTaskCreate',
onCreating: 'onProjectTaskCreating',
onCreated: 'onProjectTaskCreated',
onEdit: 'onProjectTaskEdit',
onEditing: 'onProjectTaskEditing',
onEdited: 'onProjectTaskEdited',
onDelete: 'onProjectTaskDelete',
onDeleting: 'onProjectTaskDeleting',
onDeleted: 'onProjectTaskDeleted',
},
/**
* Project Times.
*/
projectTime: {
onCreate: 'onProjectTimeCreate',
onCreating: 'onProjectTimeCreating',
onCreated: 'onProjectTimeCreated',
onEdit: 'onProjectTimeEdit',
onEditing: 'onProjectTimeEditing',
onEdited: 'onProjectTimeEdited',
onDelete: 'onProjectTimeDelete',
onDeleting: 'onProjectTimeDeleting',
onDeleted: 'onProjectTimeDeleted',
},
taxRates: {
onCreating: 'onTaxRateCreating',
onCreated: 'onTaxRateCreated',
onEditing: 'onTaxRateEditing',
onEdited: 'onTaxRateEdited',
onDeleting: 'onTaxRateDeleting',
onDeleted: 'onTaxRateDeleted',
onActivating: 'onTaxRateActivating',
onActivated: 'onTaxRateActivated',
onInactivating: 'onTaxRateInactivating',
onInactivated: 'onTaxRateInactivated',
},
plaid: {
onItemCreated: 'onPlaidItemCreated',
onTransactionsSynced: 'onPlaidTransactionsSynced',
},
// Bank rules.
bankRules: {
onCreating: 'onBankRuleCreating',
onCreated: 'onBankRuleCreated',
onEditing: 'onBankRuleEditing',
onEdited: 'onBankRuleEdited',
onDeleting: 'onBankRuleDeleting',
onDeleted: 'onBankRuleDeleted',
},
// Bank matching.
bankMatch: {
onMatching: 'onBankTransactionMatching',
onMatched: 'onBankTransactionMatched',
onUnmatching: 'onBankTransactionUnmathcing',
onUnmatched: 'onBankTransactionUnmathced',
},
bankTransactions: {
onExcluding: 'onBankTransactionExclude',
onExcluded: 'onBankTransactionExcluded',
onUnexcluding: 'onBankTransactionUnexcluding',
onUnexcluded: 'onBankTransactionUnexcluded',
onPendingRemoving: 'onBankTransactionPendingRemoving',
onPendingRemoved: 'onBankTransactionPendingRemoved',
},
bankAccount: {
onDisconnecting: 'onBankAccountDisconnecting',
onDisconnected: 'onBankAccountDisconnected',
},
// Import files.
import: {
onImportCommitted: 'onImportFileCommitted',
},
// Branding templates
pdfTemplate: {
onCreating: 'onPdfTemplateCreating',
onCreated: 'onPdfTemplateCreated',
onEditing: 'onPdfTemplateEditing',
onEdited: 'onPdfTemplatedEdited',
onDeleting: 'onPdfTemplateDeleting',
onDeleted: 'onPdfTemplateDeleted',
onAssignedDefault: 'onPdfTemplateAssignedDefault',
onAssigningDefault: 'onPdfTemplateAssigningDefault',
},
// Payment method.
paymentMethod: {
onEditing: 'onPaymentMethodEditing',
onEdited: 'onPaymentMethodEdited',
onDeleted: 'onPaymentMethodDeleted',
},
// Payment methods integrations
paymentIntegrationLink: {
onPaymentIntegrationLink: 'onPaymentIntegrationLink',
onPaymentIntegrationDeleteLink: 'onPaymentIntegrationDeleteLink',
},
// Stripe Payment Integration
stripeIntegration: {
onAccountCreated: 'onStripeIntegrationAccountCreated',
onAccountDeleted: 'onStripeIntegrationAccountDeleted',
onPaymentLinkCreated: 'onStripePaymentLinkCreated',
onPaymentLinkInactivated: 'onStripePaymentLinkInactivated',
onOAuthCodeGranted: 'onStripeOAuthCodeGranted',
},
// Stripe Payment Webhooks
stripeWebhooks: {
onCheckoutSessionCompleted: 'onStripeCheckoutSessionCompleted',
onAccountUpdated: 'onStripeAccountUpdated',
},
};

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,20 @@
import {
PipeTransform,
ArgumentMetadata,
BadRequestException,
} from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {}
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value);
return parsedValue;
} catch (error) {
console.log(error);
throw new BadRequestException(error.errors);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,230 @@
export const ACCOUNT_TYPE = {
CASH: 'cash',
BANK: 'bank',
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
INVENTORY: 'inventory',
OTHER_CURRENT_ASSET: 'other-current-asset',
FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'none-current-asset',
ACCOUNTS_PAYABLE: 'accounts-payable',
CREDIT_CARD: 'credit-card',
TAX_PAYABLE: 'tax-payable',
OTHER_CURRENT_LIABILITY: 'other-current-liability',
LOGN_TERM_LIABILITY: 'long-term-liability',
NON_CURRENT_LIABILITY: 'non-current-liability',
EQUITY: 'equity',
INCOME: 'income',
OTHER_INCOME: 'other-income',
COST_OF_GOODS_SOLD: 'cost-of-goods-sold',
EXPENSE: 'expense',
OTHER_EXPENSE: 'other-expense',
};
export const ACCOUNT_PARENT_TYPE = {
CURRENT_ASSET: 'current-asset',
FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'non-current-asset',
CURRENT_LIABILITY: 'current-liability',
LOGN_TERM_LIABILITY: 'long-term-liability',
NON_CURRENT_LIABILITY: 'non-current-liability',
EQUITY: 'equity',
EXPENSE: 'expense',
INCOME: 'income',
};
export const ACCOUNT_ROOT_TYPE = {
ASSET: 'asset',
LIABILITY: 'liability',
EQUITY: 'equity',
EXPENSE: 'expense',
INCOME: 'income',
};
export const ACCOUNT_NORMAL = {
CREDIT: 'credit',
DEBIT: 'debit',
};
export const ACCOUNT_TYPES = [
{
label: 'Cash',
key: ACCOUNT_TYPE.CASH,
normal: ACCOUNT_NORMAL.DEBIT,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
multiCurrency: true,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Bank',
key: ACCOUNT_TYPE.BANK,
normal: ACCOUNT_NORMAL.DEBIT,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
multiCurrency: true,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Accounts Receivable',
key: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Inventory',
key: ACCOUNT_TYPE.INVENTORY,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Other Current Asset',
key: ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Fixed Asset',
key: ACCOUNT_TYPE.FIXED_ASSET,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Non-Current Asset',
key: ACCOUNT_TYPE.NON_CURRENT_ASSET,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.ASSET,
parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Accounts Payable',
key: ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Credit Card',
key: ACCOUNT_TYPE.CREDIT_CARD,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
multiCurrency: true,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Tax Payable',
key: ACCOUNT_TYPE.TAX_PAYABLE,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Other Current Liability',
key: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Long Term Liability',
key: ACCOUNT_TYPE.LOGN_TERM_LIABILITY,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.LOGN_TERM_LIABILITY,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Non-Current Liability',
key: ACCOUNT_TYPE.NON_CURRENT_LIABILITY,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.NON_CURRENT_LIABILITY,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Equity',
key: ACCOUNT_TYPE.EQUITY,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.EQUITY,
parentType: ACCOUNT_PARENT_TYPE.EQUITY,
balanceSheet: true,
incomeSheet: false,
},
{
label: 'Income',
key: ACCOUNT_TYPE.INCOME,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.INCOME,
parentType: ACCOUNT_PARENT_TYPE.INCOME,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Other Income',
key: ACCOUNT_TYPE.OTHER_INCOME,
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.INCOME,
parentType: ACCOUNT_PARENT_TYPE.INCOME,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Cost of Goods Sold',
key: ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Expense',
key: ACCOUNT_TYPE.EXPENSE,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
balanceSheet: false,
incomeSheet: true,
},
{
label: 'Other Expense',
key: ACCOUNT_TYPE.OTHER_EXPENSE,
normal: ACCOUNT_NORMAL.DEBIT,
rootType: ACCOUNT_ROOT_TYPE.EXPENSE,
parentType: ACCOUNT_PARENT_TYPE.EXPENSE,
balanceSheet: false,
incomeSheet: true,
},
];
export const getAccountsSupportsMultiCurrency = () => {
return ACCOUNT_TYPES.filter((account) => account.multiCurrency);
};

View File

@@ -0,0 +1,7 @@
export const DATATYPES_LENGTH = {
STRING: 255,
TEXT: 65535,
INT_10: 4294967295,
DECIMAL_13_3: 9999999999.999,
DECIMAL_15_5: 999999999999.999,
};

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map((value) => (value === null ? '' : value)));
}
}

View File

@@ -0,0 +1,21 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class GlobalPrefixInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
request.url = `/api${request.url}`;
return next.handle().pipe(
map((data) => {
return data;
}),
);
}
}

View File

@@ -0,0 +1,21 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import { Observable } from 'rxjs';
@Injectable()
export class UserIpInterceptor implements NestInterceptor {
constructor(private readonly cls: ClsService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const userIp = request.connection.remoteAddress;
this.cls.set('ip', userIp);
return next.handle();
}
}

View File

@@ -0,0 +1,174 @@
import { Knex } from 'knex';
export interface IAccountDTO {
name: string;
code: string;
description: string;
accountType: string;
parentAccountId?: number;
active: boolean;
bankBalance?: number;
accountMask?: string;
}
export interface IAccountCreateDTO extends IAccountDTO {
currencyCode?: string;
plaidAccountId?: string;
plaidItemId?: string;
}
export interface IAccountEditDTO extends IAccountDTO {}
export interface IAccount {
id: number;
name: string;
slug: string;
code: string;
index: number;
description: string;
accountType: string;
parentAccountId: number;
active: boolean;
predefined: boolean;
amount: number;
currencyCode: string;
transactions?: any[];
type?: any[];
accountNormal: string;
accountParentType: string;
bankBalance: string;
plaidItemId: number | null;
lastFeedsUpdatedAt: Date;
}
export enum AccountNormal {
DEBIT = 'debit',
CREDIT = 'credit',
}
export interface IAccountsTransactionsFilter {
accountId?: number;
limit?: number;
}
export interface IAccountTransaction {
id?: number;
credit: number;
debit: number;
currencyCode: string;
exchangeRate: number;
accountId: number;
contactId?: number | null;
date: string | Date;
referenceType: string;
referenceTypeFormatted: string;
referenceId: number;
referenceNumber?: string;
transactionNumber?: string;
transactionType?: string;
note?: string;
index: number;
indexGroup?: number;
costable?: boolean;
userId?: number;
itemId?: number;
branchId?: number;
projectId?: number;
account?: IAccount;
taxRateId?: number;
taxRate?: number;
}
export interface IAccountResponse extends IAccount {}
export enum IAccountsStructureType {
Tree = 'tree',
Flat = 'flat',
}
export interface IAccountsFilter {
stringifiedFilterRoles?: string;
onlyInactive: boolean;
structure?: IAccountsStructureType;
}
export interface IAccountType {
label: string;
key: string;
normal: string;
rootType: string;
childType: string;
balanceSheet: boolean;
incomeSheet: boolean;
}
export interface IAccountsTypesService {
getAccountsTypes(tenantId: number): Promise<IAccountType>;
}
export interface IAccountEventCreatingPayload {
tenantId: number;
accountDTO: any;
trx: Knex.Transaction;
}
export interface IAccountEventCreatedPayload {
tenantId: number;
account: IAccount;
accountId: number;
trx: Knex.Transaction;
}
export interface IAccountEventEditedPayload {
tenantId: number;
account: IAccount;
oldAccount: IAccount;
trx: Knex.Transaction;
}
export interface IAccountEventDeletedPayload {
tenantId: number;
accountId: number;
oldAccount: IAccount;
trx: Knex.Transaction;
}
export interface IAccountEventDeletePayload {
trx: Knex.Transaction;
oldAccount: IAccount;
tenantId: number;
}
export interface IAccountEventActivatedPayload {
tenantId: number;
accountId: number;
trx: Knex.Transaction;
}
export enum AccountAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
TransactionsLocking = 'TransactionsLocking',
}
export enum TaxRateAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
}
export interface CreateAccountParams {
ignoreUniqueName: boolean;
}

View File

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

View File

@@ -0,0 +1,193 @@
export interface IModel {
name: string;
tableName: string;
fields: { [key: string]: any };
}
export interface IFilterMeta {
sortOrder: string;
sortBy: string;
}
export interface IPaginationMeta {
pageSize: number;
page: number;
}
export interface IModelMetaDefaultSort {
sortOrder: ISortOrder;
sortField: string;
}
export type IModelColumnType =
| 'text'
| 'number'
| 'enumeration'
| 'boolean'
| 'relation';
export type ISortOrder = 'DESC' | 'ASC';
export interface IModelMetaFieldCommon {
name: string;
column: string;
columnable?: boolean;
customQuery?: Function;
required?: boolean;
importHint?: string;
importableRelationLabel?: string;
order?: number;
unique?: number;
dataTransferObjectKey?: string;
}
export interface IModelMetaFieldText {
fieldType: 'text';
minLength?: number;
maxLength?: number;
}
export interface IModelMetaFieldBoolean {
fieldType: 'boolean';
}
export interface IModelMetaFieldNumber {
fieldType: 'number';
min?: number;
max?: number;
}
export interface IModelMetaFieldDate {
fieldType: 'date';
}
export interface IModelMetaFieldUrl {
fieldType: 'url';
}
export type IModelMetaField = IModelMetaFieldCommon &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
| IModelMetaCollectionField
);
export interface IModelMetaEnumerationOption {
key: string;
label: string;
}
export interface IModelMetaEnumerationField {
fieldType: 'enumeration';
options: IModelMetaEnumerationOption[];
}
export interface IModelMetaRelationFieldCommon {
fieldType: 'relation';
}
export interface IModelMetaRelationEnumerationField {
relationType: 'enumeration';
relationKey: string;
relationEntityLabel: string;
relationEntityKey: string;
}
export interface IModelMetaFieldWithFields {
fields: IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
);
}
interface IModelMetaCollectionObjectField extends IModelMetaFieldWithFields {
collectionOf: 'object';
}
export interface IModelMetaCollectionFieldCommon {
fieldType: 'collection';
collectionMinLength?: number;
collectionMaxLength?: number;
}
export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
IModelMetaCollectionObjectField;
export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
IModelMetaRelationEnumerationField;
interface IModelPrintMeta {
pageTitle: string;
}
export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
exportable?: boolean;
exportFlattenOn?: string;
importable?: boolean;
importAggregator?: string;
importAggregateOn?: string;
importAggregateBy?: string;
print?: IModelPrintMeta;
fields: Record<string, IModelMetaField>;
fields2: Record<string, IModelMetaField2>;
columns: Record<string, IModelMetaColumn>;
}
// ----
export interface IModelMetaFieldCommon2 {
name: string;
required?: boolean;
importHint?: string;
order?: number;
unique?: number;
features?: Array<any>;
}
export interface IModelMetaRelationField2 {
fieldType: 'relation';
relationModel: string;
importableRelationLabel: string | string[];
}
export type IModelMetaField2 = IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField2
| IModelMetaCollectionField
);
export interface ImodelMetaColumnMeta {
name: string;
accessor?: string;
exportable?: boolean;
}
interface IModelMetaColumnText {
type: 'text;';
}
interface IModelMetaColumnCollection {
type: 'collection';
collectionOf: 'object';
columns: { [key: string]: ImodelMetaColumnMeta & IModelMetaColumnText };
}
export type IModelMetaColumn = ImodelMetaColumnMeta &
(IModelMetaColumnText | IModelMetaColumnCollection);

View File

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

View File

@@ -0,0 +1,101 @@
import { get } from 'lodash';
import { ACCOUNT_TYPES } from '@/constants/accounts';
export class AccountTypesUtils {
/**
* Retrieve account types list.
*/
static getList() {
return ACCOUNT_TYPES;
}
/**
* Retrieve accounts types by the given root type.
* @param {string} rootType -
* @return {string}
*/
static getTypesByRootType(rootType: string) {
return ACCOUNT_TYPES.filter((type) => type.rootType === rootType);
}
/**
* Retrieve account type by the given account type key.
* @param {string} key
* @param {string} accessor
*/
static getType(key: string, accessor?: string) {
const type = ACCOUNT_TYPES.find((type) => type.key === key);
if (accessor) {
return get(type, accessor);
}
return type;
}
/**
* Retrieve accounts types by the parent account type.
* @param {string} parentType
*/
static getTypesByParentType(parentType: string) {
return ACCOUNT_TYPES.filter((type) => type.parentType === parentType);
}
/**
* Retrieve accounts types by the given account normal.
* @param {string} normal
*/
static getTypesByNormal(normal: string) {
return ACCOUNT_TYPES.filter((type) => type.normal === normal);
}
/**
* Detarmines whether the root type equals the account type.
* @param {string} key
* @param {string} rootType
*/
static isRootTypeEqualsKey(key: string, rootType: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
const isRootType = type.rootType === rootType;
return isType && isRootType;
});
}
/**
* Detarmines whether the parent account type equals the account type key.
* @param {string} key - Account type key.
* @param {string} parentType - Account parent type.
*/
static isParentTypeEqualsKey(key: string, parentType: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
const isParentType = type.parentType === parentType;
return isType && isParentType;
});
}
/**
* Detarmines whether account type has balance sheet.
* @param {string} key - Account type key.
*
*/
static isTypeBalanceSheet(key: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
return isType && type.balanceSheet;
});
}
/**
* Detarmines whether account type has profit/loss sheet.
* @param {string} key - Account type key.
*/
static isTypePLSheet(key: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
return isType && type.incomeSheet;
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,350 @@
// @ts-nocheck
/**
* A simple dependency graph
*/
/**
* Helper for creating a Topological Sort using Depth-First-Search on a set of edges.
*
* Detects cycles and throws an Error if one is detected (unless the "circular"
* parameter is "true" in which case it ignores them).
*
* @param edges The set of edges to DFS through
* @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges)
* @param result An array in which the results will be populated
* @param circular A boolean to allow circular dependencies
*/
function createDFS(edges, leavesOnly, result, circular) {
var visited = {};
return function (start) {
if (visited[start]) {
return;
}
var inCurrentPath = {};
var currentPath = [];
var todo = []; // used as a stack
todo.push({ node: start, processed: false });
while (todo.length > 0) {
var current = todo[todo.length - 1]; // peek at the todo stack
var processed = current.processed;
var node = current.node;
if (!processed) {
// Haven't visited edges yet (visiting phase)
if (visited[node]) {
todo.pop();
continue;
} else if (inCurrentPath[node]) {
// It's not a DAG
if (circular) {
todo.pop();
// If we're tolerating cycles, don't revisit the node
continue;
}
currentPath.push(node);
throw new DepGraphCycleError(currentPath);
}
inCurrentPath[node] = true;
currentPath.push(node);
var nodeEdges = edges[node];
// (push edges onto the todo stack in reverse order to be order-compatible with the old DFS implementation)
for (var i = nodeEdges.length - 1; i >= 0; i--) {
todo.push({ node: nodeEdges[i], processed: false });
}
current.processed = true;
} else {
// Have visited edges (stack unrolling phase)
todo.pop();
currentPath.pop();
inCurrentPath[node] = false;
visited[node] = true;
if (!leavesOnly || edges[node].length === 0) {
result.push(node);
}
}
}
};
}
/**
* Simple Dependency Graph
*/
var DepGraph = (DepGraph = function DepGraph(opts) {
this.nodes = {}; // Node -> Node/Data (treated like a Set)
this.outgoingEdges = {}; // Node -> [Dependency Node]
this.incomingEdges = {}; // Node -> [Dependant Node]
this.circular = opts && !!opts.circular; // Allows circular deps
});
DepGraph.fromArray = (
items,
options = { itemId: 'id', parentItemId: 'parent_id' }
) => {
const depGraph = new DepGraph();
items.forEach((item) => {
depGraph.addNode(item[options.itemId], item);
});
items.forEach((item) => {
if (item[options.parentItemId]) {
depGraph.addDependency(item[options.parentItemId], item[options.itemId]);
}
});
return depGraph;
};
DepGraph.prototype = {
/**
* The number of nodes in the graph.
*/
size: function () {
return Object.keys(this.nodes).length;
},
/**
* Add a node to the dependency graph. If a node already exists, this method will do nothing.
*/
addNode: function (node, data) {
if (!this.hasNode(node)) {
// Checking the arguments length allows the user to add a node with undefined data
if (arguments.length === 2) {
this.nodes[node] = data;
} else {
this.nodes[node] = node;
}
this.outgoingEdges[node] = [];
this.incomingEdges[node] = [];
}
},
/**
* Remove a node from the dependency graph. If a node does not exist, this method will do nothing.
*/
removeNode: function (node) {
if (this.hasNode(node)) {
delete this.nodes[node];
delete this.outgoingEdges[node];
delete this.incomingEdges[node];
[this.incomingEdges, this.outgoingEdges].forEach(function (edgeList) {
Object.keys(edgeList).forEach(function (key) {
var idx = edgeList[key].indexOf(node);
if (idx >= 0) {
edgeList[key].splice(idx, 1);
}
}, this);
});
}
},
/**
* Check if a node exists in the graph
*/
hasNode: function (node) {
return this.nodes.hasOwnProperty(node);
},
/**
* Get the data associated with a node name
*/
getNodeData: function (node) {
if (this.hasNode(node)) {
return this.nodes[node];
} else {
throw new Error('Node does not exist: ' + node);
}
},
/**
* Set the associated data for a given node name. If the node does not exist, this method will throw an error
*/
setNodeData: function (node, data) {
if (this.hasNode(node)) {
this.nodes[node] = data;
} else {
throw new Error('Node does not exist: ' + node);
}
},
/**
* Add a dependency between two nodes. If either of the nodes does not exist,
* an Error will be thrown.
*/
addDependency: function (from, to) {
if (!this.hasNode(from)) {
throw new Error('Node does not exist: ' + from);
}
if (!this.hasNode(to)) {
throw new Error('Node does not exist: ' + to);
}
if (this.outgoingEdges[from].indexOf(to) === -1) {
this.outgoingEdges[from].push(to);
}
if (this.incomingEdges[to].indexOf(from) === -1) {
this.incomingEdges[to].push(from);
}
return true;
},
/**
* Remove a dependency between two nodes.
*/
removeDependency: function (from, to) {
var idx;
if (this.hasNode(from)) {
idx = this.outgoingEdges[from].indexOf(to);
if (idx >= 0) {
this.outgoingEdges[from].splice(idx, 1);
}
}
if (this.hasNode(to)) {
idx = this.incomingEdges[to].indexOf(from);
if (idx >= 0) {
this.incomingEdges[to].splice(idx, 1);
}
}
},
/**
* Return a clone of the dependency graph. If any custom data is attached
* to the nodes, it will only be shallow copied.
*/
clone: function () {
var source = this;
var result = new DepGraph();
var keys = Object.keys(source.nodes);
keys.forEach(function (n) {
result.nodes[n] = source.nodes[n];
result.outgoingEdges[n] = source.outgoingEdges[n].slice(0);
result.incomingEdges[n] = source.incomingEdges[n].slice(0);
});
return result;
},
/**
* Get an array containing the nodes that the specified node depends on (transitively).
*
* Throws an Error if the graph has a cycle, or the specified node does not exist.
*
* If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned
* in the array.
*/
dependenciesOf: function (node, leavesOnly) {
if (this.hasNode(node)) {
var result = [];
var DFS = createDFS(
this.outgoingEdges,
leavesOnly,
result,
this.circular
);
DFS(node);
var idx = result.indexOf(node);
if (idx >= 0) {
result.splice(idx, 1);
}
return result;
} else {
throw new Error('Node does not exist: ' + node);
}
},
/**
* get an array containing the nodes that depend on the specified node (transitively).
*
* Throws an Error if the graph has a cycle, or the specified node does not exist.
*
* If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
*/
dependantsOf: function (node, leavesOnly) {
if (this.hasNode(node)) {
var result = [];
var DFS = createDFS(
this.incomingEdges,
leavesOnly,
result,
this.circular
);
DFS(node);
var idx = result.indexOf(node);
if (idx >= 0) {
result.splice(idx, 1);
}
return result;
} else {
throw new Error('Node does not exist: ' + node);
}
},
/**
* Construct the overall processing order for the dependency graph.
*
* Throws an Error if the graph has a cycle.
*
* If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned.
*/
overallOrder: function (leavesOnly) {
var self = this;
var result = [];
var keys = Object.keys(this.nodes);
if (keys.length === 0) {
return result; // Empty graph
} else {
if (!this.circular) {
// Look for cycles - we run the DFS starting at all the nodes in case there
// are several disconnected subgraphs inside this dependency graph.
var CycleDFS = createDFS(this.outgoingEdges, false, [], this.circular);
keys.forEach(function (n) {
CycleDFS(n);
});
}
var DFS = createDFS(
this.outgoingEdges,
leavesOnly,
result,
this.circular
);
// Find all potential starting points (nodes with nothing depending on them) an
// run a DFS starting at these points to get the order
keys
.filter(function (node) {
return self.incomingEdges[node].length === 0;
})
.forEach(function (n) {
DFS(n);
});
// If we're allowing cycles - we need to run the DFS against any remaining
// nodes that did not end up in the initial result (as they are part of a
// subgraph that does not have a clear starting point)
if (this.circular) {
keys
.filter(function (node) {
return result.indexOf(node) === -1;
})
.forEach(function (n) {
DFS(n);
});
}
return result;
}
},
mapNodes(mapper) {},
};
/**
* Cycle error, including the path of the cycle.
*/
var DepGraphCycleError = (exports.DepGraphCycleError = function (cyclePath) {
var message = 'Dependency Cycle Found: ' + cyclePath.join(' -> ');
var instance = new Error(message);
instance.cyclePath = cyclePath;
Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
if (Error.captureStackTrace) {
Error.captureStackTrace(instance, DepGraphCycleError);
}
return instance;
});
DepGraphCycleError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Object.setPrototypeOf(DepGraphCycleError, Error);
export default DepGraph;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ClsMiddleware } from 'nestjs-cls';
import './utils/moment-mysql';
import { AppModule } from './modules/App/App.module';
import { ServiceErrorFilter } from './common/filters/service-error.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('/api');
// create and mount the middleware manually here
app.use(new ClsMiddleware({}).use);
const config = new DocumentBuilder()
.setTitle('Bigcapital')
.setDescription('Financial accounting software')
.setVersion('1.0')
.addTag('cats')
.build();
const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('swagger', app, documentFactory);
app.useGlobalFilters(new ServiceErrorFilter());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@@ -0,0 +1,11 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// @ts-expect-error
req.test = 'test';
next();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,91 @@
import { Knex } from 'knex';
import { Account } from './models/Account.model';
// import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
export enum AccountNormal {
DEBIT = 'debit',
CREDIT = 'credit',
}
export interface IAccountsTransactionsFilter {
accountId?: number;
limit?: number;
}
export enum IAccountsStructureType {
Tree = 'tree',
Flat = 'flat',
}
// export interface IAccountsFilter extends IDynamicListFilterDTO {
// }
export interface IAccountsFilter {
onlyInactive: boolean;
structure?: IAccountsStructureType;
}
export interface IAccountType {
label: string;
key: string;
normal: string;
rootType: string;
childType: string;
balanceSheet: boolean;
incomeSheet: boolean;
}
export interface IAccountsTypesService {
getAccountsTypes(): Promise<IAccountType>;
}
export interface IAccountEventCreatingPayload {
accountDTO: any;
trx: Knex.Transaction;
}
export interface IAccountEventCreatedPayload {
account: Account;
accountId: number;
trx: Knex.Transaction;
}
export interface IAccountEventEditedPayload {
account: Account;
oldAccount: Account;
trx: Knex.Transaction;
}
export interface IAccountEventDeletedPayload {
accountId: number;
oldAccount: Account;
trx: Knex.Transaction;
}
export interface IAccountEventDeletePayload {
trx: Knex.Transaction;
oldAccount: Account;
}
export interface IAccountEventActivatedPayload {
accountId: number;
trx: Knex.Transaction;
}
export enum AccountAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
TransactionsLocking = 'TransactionsLocking',
}
export enum TaxRateAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
}
export interface CreateAccountParams {
ignoreUniqueName: boolean;
}
export interface IGetAccountTransactionPOJO {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,458 @@
/* eslint-disable global-require */
// import { mixin, Model } from 'objection';
import { castArray } from 'lodash';
import DependencyGraph from '@/libs/dependency-graph';
import {
ACCOUNT_TYPES,
getAccountsSupportsMultiCurrency,
} from '@/constants/accounts';
import { TenantModel } from '@/modules/System/models/TenantModel';
// import { SearchableModel } from '@/modules/Search/SearchableMdel';
// import { CustomViewBaseModel } from '@/modules/CustomViews/CustomViewBaseModel';
// import { ModelSettings } from '@/modules/Settings/ModelSettings';
import { AccountTypesUtils } from '@/libs/accounts-utils/AccountTypesUtils';
import { Model } from 'objection';
import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem';
// import AccountSettings from './Account.Settings';
// import { DEFAULT_VIEWS } from '@/modules/Accounts/constants';
// import { buildFilterQuery, buildSortColumnQuery } from '@/lib/ViewRolesBuilder';
// import { flatToNestedArray } from 'utils';
// @ts-expect-error
// export class Account extends mixin(TenantModel, [
// ModelSettings,
// CustomViewBaseModel,
// SearchableModel,
// ]) {
export class Account extends TenantModel {
public name!: string;
public slug!: string;
public code!: string;
public index!: number;
public accountType!: string;
public predefined!: boolean;
public currencyCode!: string;
public active!: boolean;
public bankBalance!: number;
public lastFeedsUpdatedAt!: string | null;
public amount!: number;
public plaidItemId!: number;
public plaidItem!: PlaidItem;
/**
* Table name.
*/
static get tableName() {
return 'accounts';
}
/**
* Timestamps columns.
*/
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'accountTypeLabel',
'accountParentType',
'accountRootType',
'accountNormal',
'accountNormalFormatted',
'isBalanceSheetAccount',
'isPLSheet',
];
}
/**
* Account normal.
*/
get accountNormal() {
return AccountTypesUtils.getType(this.accountType, 'normal');
}
get accountNormalFormatted() {
const paris = {
credit: 'Credit',
debit: 'Debit',
};
return paris[this.accountNormal] || '';
}
/**
* Retrieve account type label.
*/
get accountTypeLabel() {
return AccountTypesUtils.getType(this.accountType, 'label');
}
/**
* Retrieve account parent type.
*/
get accountParentType() {
return AccountTypesUtils.getType(this.accountType, 'parentType');
}
/**
* Retrieve account root type.
*/
get accountRootType() {
return AccountTypesUtils.getType(this.accountType, 'rootType');
}
/**
* Retrieve whether the account is balance sheet account.
*/
get isBalanceSheetAccount() {
return this.isBalanceSheet();
}
/**
* Retrieve whether the account is profit/loss sheet account.
*/
get isPLSheet() {
return this.isProfitLossSheet();
}
/**
* Allows to mark model as resourceable to viewable and filterable.
*/
static get resourceable() {
return true;
}
/**
* Model modifiers.
*/
static get modifiers() {
const TABLE_NAME = Account.tableName;
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('accounts.active', !active);
},
filterAccounts(query, accountIds) {
if (accountIds.length > 0) {
query.whereIn(`${TABLE_NAME}.id`, accountIds);
}
},
filterAccountTypes(query, typesIds) {
if (typesIds.length > 0) {
query.whereIn('account_types.account_type_id', typesIds);
}
},
viewRolesBuilder(query, conditionals, expression) {
// buildFilterQuery(Account.tableName, conditionals, expression)(query);
},
sortColumnBuilder(query, columnKey, direction) {
// buildSortColumnQuery(Account.tableName, columnKey, direction)(query);
},
/**
* Filter by root type.
*/
filterByRootType(query, rootType) {
const filterTypes = ACCOUNT_TYPES.filter(
(accountType) => accountType.rootType === rootType,
).map((accountType) => accountType.key);
query.whereIn('account_type', filterTypes);
},
/**
* Filter by account normal
*/
filterByAccountNormal(query, accountNormal) {
const filterTypes = ACCOUNT_TYPES.filter(
(accountType) => accountType.normal === accountNormal,
).map((accountType) => accountType.key);
query.whereIn('account_type', filterTypes);
},
/**
* Finds account by the given slug.
* @param {*} query
* @param {*} slug
*/
findBySlug(query, slug) {
query.where('slug', slug).first();
},
/**
*
* @param {*} query
* @param {*} baseCyrrency
*/
preventMutateBaseCurrency(query) {
const accountsTypes = getAccountsSupportsMultiCurrency();
const accountsTypesKeys = accountsTypes.map((type) => type.key);
query
.whereIn('accountType', accountsTypesKeys)
.where('seededAt', null)
.first();
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { AccountTransaction } = require('./AccountTransaction.model');
const { Item } = require('../../Items/models/Item');
// const InventoryAdjustment = require('models/InventoryAdjustment');
// const ManualJournalEntry = require('models/ManualJournalEntry');
// const Expense = require('models/Expense');
// const ExpenseEntry = require('models/ExpenseCategory');
// const ItemEntry = require('models/ItemEntry');
// const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
const { PlaidItem } = require('../../BankingPlaid/models/PlaidItem');
return {
/**
* Account model may has many transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'accounts.id',
to: 'accounts_transactions.accountId',
},
},
/**
* Account may has many items as cost account.
*/
itemsCostAccount: {
relation: Model.HasManyRelation,
modelClass: Item,
join: {
from: 'accounts.id',
to: 'items.costAccountId',
},
},
/**
* Account may has many items as sell account.
*/
itemsSellAccount: {
relation: Model.HasManyRelation,
modelClass: Item,
join: {
from: 'accounts.id',
to: 'items.sellAccountId',
},
},
// /**
// *
// */
// inventoryAdjustments: {
// relation: Model.HasManyRelation,
// modelClass: InventoryAdjustment.default,
// join: {
// from: 'accounts.id',
// to: 'inventory_adjustments.adjustmentAccountId',
// },
// },
// /**
// *
// */
// manualJournalEntries: {
// relation: Model.HasManyRelation,
// modelClass: ManualJournalEntry.default,
// join: {
// from: 'accounts.id',
// to: 'manual_journals_entries.accountId',
// },
// },
// /**
// *
// */
// expensePayments: {
// relation: Model.HasManyRelation,
// modelClass: Expense.default,
// join: {
// from: 'accounts.id',
// to: 'expenses_transactions.paymentAccountId',
// },
// },
// /**
// *
// */
// expenseEntries: {
// relation: Model.HasManyRelation,
// modelClass: ExpenseEntry.default,
// join: {
// from: 'accounts.id',
// to: 'expense_transaction_categories.expenseAccountId',
// },
// },
// /**
// *
// */
// entriesCostAccount: {
// relation: Model.HasManyRelation,
// modelClass: ItemEntry.default,
// join: {
// from: 'accounts.id',
// to: 'items_entries.costAccountId',
// },
// },
// /**
// *
// */
// entriesSellAccount: {
// relation: Model.HasManyRelation,
// modelClass: ItemEntry.default,
// join: {
// from: 'accounts.id',
// to: 'items_entries.sellAccountId',
// },
// },
// /**
// * Associated uncategorized transactions.
// */
// uncategorizedTransactions: {
// relation: Model.HasManyRelation,
// modelClass: UncategorizedTransaction.default,
// join: {
// from: 'accounts.id',
// to: 'uncategorized_cashflow_transactions.accountId',
// },
// filter: (query) => {
// query.where('categorized', false);
// },
// },
/**
* Account model may belongs to a Plaid item.
*/
plaidItem: {
relation: Model.BelongsToOneRelation,
modelClass: PlaidItem,
join: {
from: 'accounts.plaidItemId',
to: 'plaid_items.plaidItemId',
},
},
};
}
/**
* Detarmines whether the given type equals the account type.
* @param {string} accountType
* @return {boolean}
*/
isAccountType(accountType) {
const types = castArray(accountType);
return types.indexOf(this.accountType) !== -1;
}
/**
* Detarmines whether the given root type equals the account type.
* @param {string} rootType
* @return {boolean}
*/
isRootType(rootType) {
return AccountTypesUtils.isRootTypeEqualsKey(this.accountType, rootType);
}
/**
* Detarmine whether the given parent type equals the account type.
* @param {string} parentType
* @return {boolean}
*/
isParentType(parentType) {
return AccountTypesUtils.isParentTypeEqualsKey(
this.accountType,
parentType,
);
}
/**
* Detarmines whether the account is balance sheet account.
* @return {boolean}
*/
isBalanceSheet() {
return AccountTypesUtils.isTypeBalanceSheet(this.accountType);
}
/**
* Detarmines whether the account is profit/loss account.
* @return {boolean}
*/
isProfitLossSheet() {
return AccountTypesUtils.isTypePLSheet(this.accountType);
}
/**
* Detarmines whether the account is income statement account
* @return {boolean}
*/
isIncomeSheet() {
return this.isProfitLossSheet();
}
/**
* Converts flatten accounts list to nested array.
* @param {Array} accounts
* @param {Object} options
*/
static toNestedArray(accounts, options = { children: 'children' }) {
// return flatToNestedArray(accounts, {
// id: 'id',
// parentId: 'parentAccountId',
// });
}
/**
* Transformes the accounts list to depenedency graph structure.
* @param {IAccount[]} accounts
*/
static toDependencyGraph(accounts) {
return DependencyGraph.fromArray(accounts, {
itemId: 'id',
parentItemId: 'parentAccountId',
});
}
/**
* Model settings.
*/
// static get meta() {
// return AccountSettings;
// }
/**
* Retrieve the default custom views, roles and columns.
*/
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,101 @@
import { get } from 'lodash';
import { ACCOUNT_TYPES } from '../Accounts.constants';
export class AccountTypesUtils {
/**
* Retrieve account types list.
*/
static getList() {
return ACCOUNT_TYPES;
}
/**
* Retrieve accounts types by the given root type.
* @param {string} rootType -
* @return {string}
*/
static getTypesByRootType(rootType: string) {
return ACCOUNT_TYPES.filter((type) => type.rootType === rootType);
}
/**
* Retrieve account type by the given account type key.
* @param {string} key
* @param {string} accessor
*/
static getType(key: string, accessor?: string) {
const type = ACCOUNT_TYPES.find((type) => type.key === key);
if (accessor) {
return get(type, accessor);
}
return type;
}
/**
* Retrieve accounts types by the parent account type.
* @param {string} parentType
*/
static getTypesByParentType(parentType: string) {
return ACCOUNT_TYPES.filter((type) => type.parentType === parentType);
}
/**
* Retrieve accounts types by the given account normal.
* @param {string} normal
*/
static getTypesByNormal(normal: string) {
return ACCOUNT_TYPES.filter((type) => type.normal === normal);
}
/**
* Detarmines whether the root type equals the account type.
* @param {string} key
* @param {string} rootType
*/
static isRootTypeEqualsKey(key: string, rootType: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
const isRootType = type.rootType === rootType;
return isType && isRootType;
});
}
/**
* Detarmines whether the parent account type equals the account type key.
* @param {string} key - Account type key.
* @param {string} parentType - Account parent type.
*/
static isParentTypeEqualsKey(key: string, parentType: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
const isParentType = type.parentType === parentType;
return isType && isParentType;
});
}
/**
* Detarmines whether account type has balance sheet.
* @param {string} key - Account type key.
*
*/
static isTypeBalanceSheet(key: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
return isType && type.balanceSheet;
});
}
/**
* Detarmines whether account type has profit/loss sheet.
* @param {string} key - Account type key.
*/
static isTypePLSheet(key: string): boolean {
return ACCOUNT_TYPES.some((type) => {
const isType = type.key === key;
return isType && type.incomeSheet;
});
}
}

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './App.controller';
import { AppService } from './App.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './App.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@@ -0,0 +1,192 @@
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { join } from 'path';
import {
AcceptLanguageResolver,
CookieResolver,
HeaderResolver,
I18nModule,
QueryResolver,
} from 'nestjs-i18n';
import { BullModule } from '@nestjs/bullmq';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ClsModule } from 'nestjs-cls';
import { AppController } from './App.controller';
import { AppService } from './App.service';
import { ItemsModule } from '../Items/items.module';
import { config } from '../../common/config';
import { SystemDatabaseModule } from '../System/SystemDB/SystemDB.module';
import { SystemModelsModule } from '../System/SystemModels/SystemModels.module';
import { JwtStrategy } from '../Auth/Jwt.strategy';
import { jwtConstants } from '../Auth/Auth.constants';
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
import { TenancyModelsModule } from '../Tenancy/TenancyModels/Tenancy.module';
import { LoggerMiddleware } from '@/middleware/logger.middleware';
import { ExcludeNullInterceptor } from '@/interceptors/ExcludeNull.interceptor';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { JwtAuthGuard } from '../Auth/Jwt.guard';
import { UserIpInterceptor } from '@/interceptors/user-ip.interceptor';
import { TenancyGlobalMiddleware } from '../Tenancy/TenancyGlobal.middleware';
import { TransformerModule } from '../Transformer/Transformer.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { ExpensesModule } from '../Expenses/Expenses.module';
import { ItemCategoryModule } from '../ItemCategories/ItemCategory.module';
import { TaxRatesModule } from '../TaxRates/TaxRate.module';
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { BranchesModule } from '../Branches/Branches.module';
import { WarehousesModule } from '../Warehouses/Warehouses.module';
import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor';
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
import { CustomersModule } from '../Customers/Customers.module';
import { VendorsModule } from '../Vendors/Vendors.module';
import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module';
import { BillsModule } from '../Bills/Bills.module';
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
import { SaleReceiptsModule } from '../SaleReceipts/SaleReceipts.module';
import { ManualJournalsModule } from '../ManualJournals/ManualJournals.module';
import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
import { VendorCreditsModule } from '../VendorCredit/VendorCredits.module';
import { VendorCreditApplyBillsModule } from '../VendorCreditsApplyBills/VendorCreditApplyBills.module';
import { VendorCreditsRefundModule } from '../VendorCreditsRefund/VendorCreditsRefund.module';
import { CreditNoteRefundsModule } from '../CreditNoteRefunds/CreditNoteRefunds.module';
import { BillPaymentsModule } from '../BillPayments/BillPayments.module';
import { PaymentsReceivedModule } from '../PaymentReceived/PaymentsReceived.module';
import { LedgerModule } from '../Ledger/Ledger.module';
import { BankRulesModule } from '../BankRules/BankRules.module';
import { BankAccountsModule } from '../BankingAccounts/BankAccounts.module';
import { BankingTransactionsExcludeModule } from '../BankingTransactionsExclude/BankingTransactionsExclude.module';
import { BankingTransactionsRegonizeModule } from '../BankingTranasctionsRegonize/BankingTransactionsRegonize.module';
import { BankingMatchingModule } from '../BankingMatching/BankingMatching.module';
import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module';
import { TransactionsLockingModule } from '../TransactionsLocking/TransactionsLocking.module';
import { SettingsModule } from '../Settings/Settings.module';
import { InventoryAdjustmentsModule } from '../InventoryAdjutments/InventoryAdjustments.module';
import { PostHogModule } from '../EventsTracker/postHog.module';
import { EventTrackerModule } from '../EventsTracker/EventTracker.module';
import { MailModule } from '../Mail/Mail.module';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '.env',
load: config,
isGlobal: true,
}),
SystemDatabaseModule,
SystemModelsModule,
EventEmitterModule.forRoot(),
I18nModule.forRootAsync({
useFactory: () => ({
fallbackLanguage: 'en',
loaderOptions: {
path: join(__dirname, '/../../i18n/'),
watch: true,
},
}),
resolvers: [
new QueryResolver(),
new HeaderResolver(),
new CookieResolver(),
AcceptLanguageResolver,
],
}),
PassportModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
BullModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
connection: {
host: configService.get('QUEUE_HOST'),
port: configService.get('QUEUE_PORT'),
},
}),
inject: [ConfigService],
}),
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
setup: (cls, req: Request, res: Response) => {
cls.set('organizationId', req.headers['organization-id']);
cls.set('userId', 1);
},
},
}),
TenancyDatabaseModule,
TenancyModelsModule,
ChromiumlyTenancyModule,
TransformerModule,
MailModule,
ItemsModule,
ItemCategoryModule,
AccountsModule,
ExpensesModule,
TaxRatesModule,
PdfTemplatesModule,
BranchesModule,
WarehousesModule,
CustomersModule,
VendorsModule,
SaleInvoicesModule,
SaleEstimatesModule,
SaleReceiptsModule,
BillsModule,
ManualJournalsModule,
CreditNotesModule,
VendorCreditsModule,
VendorCreditApplyBillsModule,
VendorCreditsRefundModule,
CreditNoteRefundsModule,
BillPaymentsModule,
PaymentsReceivedModule,
LedgerModule,
BankAccountsModule,
BankRulesModule,
BankingTransactionsModule,
BankingTransactionsExcludeModule,
BankingTransactionsRegonizeModule,
BankingMatchingModule,
TransactionsLockingModule,
SettingsModule,
InventoryAdjustmentsModule,
PostHogModule,
EventTrackerModule,
],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: SerializeInterceptor,
},
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: UserIpInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: ExcludeNullInterceptor,
},
AppService,
JwtStrategy,
],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
consumer
.apply(TenancyGlobalMiddleware)
.forRoutes({ path: '*', method: RequestMethod.ALL });
}
}

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AppService {
// configService: ConfigService;
constructor(
private configService: ConfigService,
private jwtService: JwtService,
) {}
getHello(): string {
console.log(this.configService.get('DATABASE_PORT'));
const payload = {};
const accessToken = this.jwtService.sign(payload);
console.log(accessToken);
return accessToken;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
export const jwtConstants = {
secret:
'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
};

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
import {
ExecutionContext,
Injectable,
Scope,
SetMetadata,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { ClsService } from 'nestjs-cls';
export const IS_PUBLIC_KEY = 'isPublic';
export const PublicRoute = () => SetMetadata(IS_PUBLIC_KEY, true);
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private reflector: Reflector,
private readonly cls: ClsService,
) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

View File

@@ -0,0 +1,19 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './Auth.constants';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}

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