Compare commits

..

159 Commits

Author SHA1 Message Date
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
728b4cacd9 feat: wip send invoice mail preview 2024-10-24 20:48:16 +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
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
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
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
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
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
9517b4e279 feat: wip public payment page 2024-09-14 22:10:27 +02:00
Ahmed Bouhuolia
162b92ce84 feat: wip Stripe connect integration 2024-09-09 14:18:04 +02:00
Ahmed Bouhuolia
a183666df6 feat: Onboard accounts to Stripe Connect 2024-09-08 11:42:26 +02:00
423 changed files with 22903 additions and 3869 deletions

View File

@@ -92,4 +92,11 @@ S3_BUCKET=
# PostHog
POSTHOG_API_KEY=
POSTHOG_HOST=
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,6 +2,60 @@
All notable changes to Bigcapital server-side will be in this file.
# [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

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>

View File

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

View File

@@ -4,11 +4,11 @@
"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\"",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\"",
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"",
"test:e2e": "playwright test",
"prepare": "husky install"
},
@@ -29,5 +29,8 @@
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"dependencies": {
"tsup": "^8.3.0"
}
}

View File

@@ -90,11 +90,7 @@ RUN chown node:node /
RUN npm install -g pnpm
# Copy application dependency manifests to the container image.
COPY ./package*.json ./
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
COPY ./lerna.json ./lerna.json
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./packages/server/package*.json ./packages/server/
COPY --chown=node:node ./ ./
# Install application dependencies
RUN apk update
@@ -109,6 +105,6 @@ RUN pnpm install
COPY --chown=node:node ./packages/server ./packages/server
# # Creates a "dist" folder with the production build
RUN npm run build:server --skip-nx-cache
RUN pnpm run build:server --skip-nx-cache
CMD [ "node", "./packages/server/build/index.js" ]

View File

@@ -22,6 +22,8 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.576.0",
"@aws-sdk/s3-request-presigner": "^3.583.0",
"@bigcapital/utils": "*",
"@bigcapital/email-components": "*",
"@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
@@ -109,11 +111,13 @@
"rtl-detect": "^1.0.4",
"socket.io": "^4.7.4",
"source-map-loader": "^4.0.1",
"stripe": "^16.10.0",
"tmp-promise": "^3.0.3",
"ts-transformer-keys": "^0.4.2",
"tsyringe": "^4.3.0",
"typedi": "^0.8.0",
"uniqid": "^5.2.0",
"uuid": "^10.0.0",
"winston": "^3.2.1",
"xlsx": "^0.18.5",
"yup": "^0.28.1"

View File

@@ -10,21 +10,37 @@ block head
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title {
font-size: 60px;
font-size: 30px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap {
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-terms-list {
display: flex;
@@ -48,8 +64,8 @@ block head
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: center;
align-items: center;
-webkit-box-align: flex-start;
align-items: flex-start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
@@ -92,7 +108,14 @@ block head
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item .item__description{
color: #5f6b7c;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
@@ -127,54 +150,60 @@ block head
color: #666;
}
.#{prefix}-statement__value {
/* Styles for statement value */
white-space: pre-line;
}
block content
div(class=`${prefix}-root`)
div(class=`${prefix}-big-title`) Credit Note
if showCompanyLogo
div(class=`${prefix}-logo-wrap`)
img(src=companyLogo alt=`Company Logo`)
div(class=`${prefix}-terms-list`)
if showCreditNoteNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}:
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr}
//- Header (includes big title, details and logo)
div(class=`${prefix}-header`)
//- Header details (includes big title and details)
div(class=`${prefix}-header-details`)
div(class=`${prefix}-big-title`) Credit Note
if showCreditNoteDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}:
div(class=`${prefix}-terms-item__value`) #{creditNoteDate}
div(class=`${prefix}-terms-list`)
if showCreditNoteNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}:
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr}
if showCreditNoteDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}:
div(class=`${prefix}-terms-item__value`) #{creditNoteDate}
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt=`Company Logo`)
div(class=`${prefix}-address-section`)
if showBilledFromAddress
div(class=`${prefix}-address`)
strong #{companyName}
each address in billedFromAddress
div #{address}
if showBilledToAddress
div(class=`${prefix}-address`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
each address in billedToAddress
div #{address}
div !{customerAddress}
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header`) #{'Item'}
th(class=`${prefix}-table__header`) #{'Description'}
th(class=`${prefix}-table__header`) #{'Rate'}
th(class=`${prefix}-table__header`) #{'Total'}
th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{'Item'}
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{'Quantity'}
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{'Rate'}
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{'Total'}
tbody
each line in lines
tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell`) #{line.item}
td(class=`${prefix}-table__cell`) #{line.description}
td(class=`${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell--right`) #{line.total}
td(class=`${prefix}-table__cell ${prefix}-table__cell--item ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--quantity ${prefix}-table__cell--right`) #{line.quantity}
td(class=`${prefix}-table__cell ${prefix}-table__cell--rate ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--total ${prefix}-table__cell--right`) #{line.total}
div(class=`${prefix}-totals`)
if showSubtotal
@@ -187,12 +216,12 @@ block content
div(class=`${prefix}-totals__item-amount`) #{totalLabel}:
div(class=`${prefix}-totals__item-label`) #{total}
if showCustomerNote
if showCustomerNote && customerNote
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{customerNoteLabel}:
div(class=`${prefix}-statement__value`) #{customerNote}
if showTermsConditions
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}:
div(class=`${prefix}-statement__value`) #{termsConditions}

View File

@@ -10,21 +10,37 @@ block head
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header {
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title {
font-size: 60px;
font-size: 30px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap {
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-terms {
display: flex;
@@ -47,9 +63,7 @@ block head
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: start;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
@@ -82,6 +96,9 @@ block head
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__header--item{
width: 50%;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
@@ -95,6 +112,14 @@ block head
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item .item__description{
color: #5f6b7c;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
@@ -129,60 +154,70 @@ block head
color: #666;
}
.#{prefix}-statement__value {
white-space: pre-line;
}
block content
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
h1(class=`${prefix}-big-title`) Estimate
if showCompanyLogo
div(class=`${prefix}-logo-wrap`)
img(alt="", src=companyLogo)
//- Header (invluces big title, details and logo)
div(class=`${prefix}-header`)
//- Terms List
div(class=`${prefix}-terms`)
if showEstimateNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel}
div(class=`${prefix}-terms-item__value`) #{estimateNumebr}
if showEstimateDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{estimateDateLabel}
div(class=`${prefix}-terms-item__value`) #{estimateDate}
if showExpirationDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{expirationDateLabel}
div(class=`${prefix}-terms-item__value`) #{expirationDate}
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
h1(class=`${prefix}-big-title`) Estimate
//- Terms List
div(class=`${prefix}-terms`)
if showEstimateNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel}
div(class=`${prefix}-terms-item__value`) #{estimateNumebr}
if showEstimateDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{estimateDateLabel}
div(class=`${prefix}-terms-item__value`) #{estimateDate}
if showExpirationDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{expirationDateLabel}
div(class=`${prefix}-terms-item__value`) #{expirationDate}
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
//- Addresses (Group section)
div(class=`${prefix}-addresses`)
if showBilledFromAddress
div(class=`${prefix}-address`)
strong #{companyName}
each item in billedFromAddress
div(class=`${prefix}-address__item`) #{item}
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showBilledToAddress
div(class=`${prefix}-address`)
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
each item in billedToAddress
div(class=`${prefix}-address__item`) #{item}
div !{customerAddress}
//- Table section (Line items)
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header`) Item
th(class=`${prefix}-table__header`) Description
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody
each line in lines
tr
td(class=`${prefix}-table__cell`) #{line.item}
td(class=`${prefix}-table__cell`) #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--quantity ${prefix}-table__cell--right`) #{line.quantity}
td(class=`${prefix}-table__cell ${prefix}-table__cell--rate ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--total ${prefix}-table__cell--right`) #{line.total}
//- Totals section
div(class=`${prefix}-totals`)
@@ -196,12 +231,12 @@ block content
div(class=`${prefix}-totals__item-amount`) #{total}
//- Statements section
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
div(class=`${prefix}-statement__value`) #{termsConditions}
if showCustomerNote && customerNote
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{customerNoteLabel}
div(class=`${prefix}-statement__value`) #{customerNote}
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}
div(class=`${prefix}-statement__value`) #{termsConditions}

View File

@@ -10,21 +10,37 @@ block head
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title {
font-size: 60px;
font-size: 30px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap {
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-details {
display: flex;
@@ -48,9 +64,7 @@ block head
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: start;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
@@ -88,6 +102,9 @@ block head
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__header--item {
width: 50%;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
@@ -101,6 +118,14 @@ block head
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item__description {
color: #5f6b7c;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
@@ -135,63 +160,68 @@ block head
color: #666;
}
.#{prefix}-paragraph__value {
/* Styles for values within the paragraph section */
white-space: pre-line;
}
block content
//- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Invoice
if showCompanyLogo
div(class=`${prefix}-logo-wrap`)
img(alt="", src=companyLogo)
//- Header (includes big title, details and logo )
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Invoice
//- Invoice details
div(class=`${prefix}-details`)
if showInvoiceNumber
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
div(class=`${prefix}-detail__value`) #{invoiceNumber}
//- Invoice details
div(class=`${prefix}-details`)
if showInvoiceNumber
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
div(class=`${prefix}-detail__value`) #{invoiceNumber}
if showDateIssue
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dateIssueLabel}
div(class=`${prefix}-detail__value`) #{dateIssue}
if showDateIssue
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dateIssueLabel}
div(class=`${prefix}-detail__value`) #{dateIssue}
if showDueDate
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dueDateLabel}
div(class=`${prefix}-detail__value`) #{dueDate}
if showDueDate
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dueDateLabel}
div(class=`${prefix}-detail__value`) #{dueDate}
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
//- Address section
div(class=`${prefix}-address-root`)
if showBilledFromAddress
if showCompanyAddress
div(class=`${prefix}-address-from`)
strong #{companyName}
each item in billedFromAddres
div(class=`${prefix}-address-from__item`) #{item}
div !{companyAddress}
if showBillingToAddress
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
each item in billedToAddress
div(class=`${prefix}-address-to__item`) #{item}
div !{customerAddress}
//- Invoice table
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header`) #{lineItemLabel}
th(class=`${prefix}-table__header`) #{lineDescriptionLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineRateLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--right`) #{lineTotalLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{lineItemLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{lineQuantityLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{lineRateLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{lineTotalLabel}
tbody
each line in lines
tr
td(class=`${prefix}-table__cell`) #{line.item}
td(class=`${prefix}-table__cell`) #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}

View File

@@ -10,22 +10,38 @@ block head
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title{
font-size: 60px;
font-size: 30px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap{
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-terms-list{
display: flex;
@@ -46,9 +62,7 @@ block head
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: start;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
@@ -122,35 +136,35 @@ block head
}
block content
div(class=`${prefix}-root`)
div(class=`${prefix}-big-title`) Payment
//- Header (includes big title, details and logo )
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
div(class=`${prefix}-big-title`) Payment
div(class=`${prefix}-terms-list`)
if showPaymentReceivedNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr}
if showCompanyLogo
div(class=`${prefix}-logo-wrap`)
img(src=companyLogo alt="Company Logo")
div(class=`${prefix}-terms-list`)
if showPaymentReceivedNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr}
if showPaymentReceivedDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
if showPaymentReceivedDate
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate}
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt="Company Logo")
div(class=`${prefix}-addresses`)
if showBilledFromAddress
div(class=`${prefix}-address`)
strong(class=`${prefix}-address__item`) #{companyName}
each addressLine in billedFromAddress
div(class=`${prefix}-address__item`) #{addressLine}
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showBillingToAddress
div(class=`${prefix}-address`)
strong(class=`${prefix}-address__item`) #{billedToLabel}
each addressLine in billedToAddress
div(class=`${prefix}-address__item`) #{addressLine}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
table(class=`${prefix}-table`)
thead

View File

@@ -10,19 +10,33 @@ block head
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-logo-wrap {
height: 120px;
width: 120px;
position: absolute;
right: 26px;
top: 26px;
overflow: hidden;
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
align-items: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-big-title {
font-size: 60px;
font-size: 30px;
margin: 0;
line-height: 1;
margin-bottom: 25px;
font-weight: 500;
color: #333;
}
@@ -46,8 +60,8 @@ block head
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: center;
align-items: center;
-webkit-box-align: flex-start;
align-items: flex-start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
@@ -78,6 +92,9 @@ block head
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__header--item{
width: 50%;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
@@ -91,6 +108,14 @@ block head
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item .item__description{
color: #5f6b7c;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
@@ -119,59 +144,67 @@ block head
margin-bottom: 20px;
}
.#{prefix}-statement__label {}
.#{prefix}-statement__value {}
.#{prefix}-statement__value {
white-space: pre-line;
}
block content
//- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Receipt
if showCompanyLogo
div(class=`${prefix}-logo-wrap`)
img(src=companyLogo alt=`Company Logo`)
//- Header (includes big title, details and logo )
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Receipt
//- Terms List
div(class=`${prefix}-terms-list`)
if showReceiptNumber
div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__label`)= receiptNumberLabel
span(class=`${prefix}-terms-item__value`)= receiptNumber
if showReceiptDate
div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__label`)= receiptDateLabel
span(class=`${prefix}-terms-item__value`)= receiptDate
//- Terms List
div(class=`${prefix}-terms-list`)
if showReceiptNumber
div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__label`)= receiptNumberLabel
span(class=`${prefix}-terms-item__value`)= receiptNumber
if showReceiptDate
div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__label`)= receiptDateLabel
span(class=`${prefix}-terms-item__value`)= receiptDate
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(src=companyLogoUri alt=`Company Logo`)
//- Address Section
div(class=`${prefix}-address-section`)
if showBilledFromAddress
div(class=`${prefix}-address`)
strong= companyName
each addressLine in billedFromAddress
div= addressLine
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showBilledToAddress
div(class=`${prefix}-address`)
strong= billedToLabel
each addressLine in billedToAddress
div= addressLine
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
//- Table Section
table(class=`${prefix}-table`)
thead(class=`${prefix}-table__header`)
tr
th(class=`${prefix}-table__header`) Item
th(class=`${prefix}-table__header`) Description
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Total
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody
each line in lines
tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell`)= line.item
td(class=`${prefix}-table__cell`)= line.description
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.rate
td(class=`${prefix}-table__cell${prefix}-table__cell--right`)= line.total
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
//- Totals Section
div(class=`${prefix}-totals`)
@@ -186,13 +219,13 @@ block content
span(class=`${prefix}-totals__line__amount`)= total
//- Customer Note Section
if showCustomerNote
if showCustomerNote && customerNote
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`)= customerNoteLabel
div(class=`${prefix}-statement__value`)= customerNote
//- Terms & Conditions Section
if showTermsConditions
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`)= termsConditionsLabel
div(class=`${prefix}-statement__value`)= termsConditions

View File

@@ -18,7 +18,7 @@ import BaseController from '@/api/controllers/BaseController';
@Service()
export default class OrganizationController extends BaseController {
@Inject()
organizationService: OrganizationService;
private organizationService: OrganizationService;
/**
* Router constructor.
@@ -56,10 +56,10 @@ export default class OrganizationController extends BaseController {
}
/**
* Organization setup schema.
* @return {ValidationChain[]}
* Build organization validation schema.
* @returns {ValidationChain[]}
*/
private get commonOrganizationValidationSchema(): ValidationChain[] {
private get buildOrganizationValidationSchema(): ValidationChain[] {
return [
check('name').exists().trim(),
check('industry').optional({ nullable: true }).isString().trim(),
@@ -72,21 +72,34 @@ export default class OrganizationController extends BaseController {
];
}
/**
* Build organization validation schema.
* @returns {ValidationChain[]}
*/
private get buildOrganizationValidationSchema(): ValidationChain[] {
return [...this.commonOrganizationValidationSchema];
}
/**
* Update organization validation schema.
* @returns {ValidationChain[]}
*/
private get updateOrganizationValidationSchema(): ValidationChain[] {
return [
...this.commonOrganizationValidationSchema,
// # Profile
check('name').optional().trim(),
check('industry').optional({ nullable: true }).isString().trim(),
check('location').optional().isString().isISO31661Alpha2(),
check('base_currency').optional().isISO4217(),
check('timezone').optional().isIn(moment.tz.names()),
check('fiscal_year').optional().isIn(MONTHS),
check('language').optional().isString().isIn(ACCEPTED_LOCALES),
check('date_format').optional().isIn(DATE_FORMATS),
// # Address
check('address.address_1').optional().isString().trim(),
check('address.address_2').optional().isString().trim(),
check('address.postal_code').optional().isString().trim(),
check('address.city').optional().isString().trim(),
check('address.state_province').optional().isString().trim(),
check('address.phone').optional().isString().trim(),
// # Branding
check('primary_color').optional({ nullable: true }).isHexColor().trim(),
check('logo_key').optional({ nullable: true }).isString().trim(),
check('tax_number').optional({ nullable: true }).isString().trim(),
];
}
@@ -156,7 +169,7 @@ export default class OrganizationController extends BaseController {
next: NextFunction
) {
const { tenantId } = req;
const tenantDTO = this.matchedBodyData(req);
const tenantDTO = this.matchedBodyData(req, { includeOptionals: false });
try {
await this.organizationService.updateOrganization(tenantId, tenantDTO);

View File

@@ -0,0 +1,179 @@
import { Service, Inject } from 'typedi';
import { Request, Response, Router, NextFunction } from 'express';
import { body, param } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { PaymentServicesApplication } from '@/services/PaymentServices/PaymentServicesApplication';
@Service()
export class PaymentServicesController extends BaseController {
@Inject()
private paymentServicesApp: PaymentServicesApplication;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
asyncMiddleware(this.getPaymentServicesSpecificInvoice.bind(this))
);
router.get('/state', this.getPaymentMethodsState.bind(this));
router.get('/:paymentServiceId', this.getPaymentService.bind(this));
router.post(
'/:paymentMethodId',
[
param('paymentMethodId').exists(),
body('name').optional().isString(),
body('options.bank_account_id').optional().isNumeric(),
body('options.clearing_account_id').optional().isNumeric(),
],
this.validationResult,
asyncMiddleware(this.updatePaymentMethod.bind(this))
);
router.delete(
'/:paymentMethodId',
[param('paymentMethodId').exists()],
this.validationResult,
this.deletePaymentMethod.bind(this)
);
return router;
}
/**
* Retrieve accounts types list.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @return {Promise<Response | void>}
*/
private async getPaymentServicesSpecificInvoice(
req: Request<{ invoiceId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const paymentServices =
await this.paymentServicesApp.getPaymentServicesForInvoice(tenantId);
return res.status(200).send({ paymentServices });
} catch (error) {
next(error);
}
}
/**
* Retrieves a specific payment service.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next - Next function.
* @return {Promise<Response | void>}
*/
private async getPaymentService(
req: Request<{ paymentServiceId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { paymentServiceId } = req.params;
try {
const paymentService = await this.paymentServicesApp.getPaymentService(
tenantId,
paymentServiceId
);
return res.status(200).send({ data: paymentService });
} catch (error) {
next(error);
}
}
/**
* Edits the given payment method settings.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @return {Promise<Response | void>}
*/
private async updatePaymentMethod(
req: Request<{ paymentMethodId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { paymentMethodId } = req.params;
const updatePaymentMethodDTO = this.matchedBodyData(req);
try {
await this.paymentServicesApp.editPaymentMethod(
tenantId,
paymentMethodId,
updatePaymentMethodDTO
);
return res.status(200).send({
id: paymentMethodId,
message: 'The given payment method has been updated.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieves the payment state providing state.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next - Next function.
* @return {Promise<Response | void>}
*/
private async getPaymentMethodsState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const paymentMethodsState =
await this.paymentServicesApp.getPaymentMethodsState(tenantId);
return res.status(200).send({ data: paymentMethodsState });
} catch (error) {
next(error);
}
}
/**
* Deletes the given payment method.
* @param {Request<{ paymentMethodId: number }>} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next - Next function.
* @return {Promise<Response | void>}
*/
private async deletePaymentMethod(
req: Request<{ paymentMethodId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { paymentMethodId } = req.params;
try {
await this.paymentServicesApp.deletePaymentMethod(
tenantId,
paymentMethodId
);
return res.status(204).send({
id: paymentMethodId,
message: 'The payment method has been deleted.',
});
} catch (error) {
next(error);
}
}
}

View File

@@ -31,6 +31,7 @@ export class PdfTemplatesController extends BaseController {
this.validationResult,
this.editPdfTemplate.bind(this)
);
router.get('/state', this.getOrganizationBrandingState.bind(this));
router.get(
'/',
[query('resource').optional()],
@@ -175,4 +176,20 @@ export class PdfTemplatesController extends BaseController {
next(error);
}
}
async getOrganizationBrandingState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const data =
await this.pdfTemplateApplication.getPdfTemplateBrandingState(tenantId);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
}

View File

@@ -27,6 +27,7 @@ import GetCreditNoteAssociatedAppliedInvoices from '@/services/CreditNotes/GetCr
import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction';
import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { GetCreditNoteState } from '@/services/CreditNotes/GetCreditNoteState';
/**
* Credit notes controller.
* @service
@@ -81,6 +82,9 @@ export default class PaymentReceivesController extends BaseController {
@Inject()
creditNotePdf: GetCreditNotePdf;
@Inject()
getCreditNoteStateService: GetCreditNoteState;
/**
* Router constructor.
*/
@@ -105,6 +109,12 @@ export default class PaymentReceivesController extends BaseController {
this.asyncMiddleware(this.newCreditNote),
this.handleServiceErrors
);
router.get(
'/state',
CheckPolicies(CreditNoteAction.View, AbilitySubject.CreditNote),
this.asyncMiddleware(this.getCreditNoteState.bind(this)),
this.handleServiceErrors
);
// Get specific credit note.
router.get(
'/:id',
@@ -461,13 +471,14 @@ export default class PaymentReceivesController extends BaseController {
ACCEPT_TYPE.APPLICATION_PDF,
]);
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.creditNotePdf.getCreditNotePdf(
const [pdfContent, filename] = await this.creditNotePdf.getCreditNotePdf(
tenantId,
creditNoteId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
} else {
@@ -736,6 +747,23 @@ export default class PaymentReceivesController extends BaseController {
}
};
private getCreditNoteState = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
try {
const data = await this.getCreditNoteStateService.getCreditNoteState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param {Error} error

View File

@@ -95,6 +95,12 @@ export default class PaymentReceivesController extends BaseController {
asyncMiddleware(this.getPaymentReceiveInvoices.bind(this)),
this.handleServiceErrors
);
router.get(
'/state',
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
this.getPaymentReceivedState.bind(this),
this.handleServiceErrors
);
router.get(
'/:id',
CheckPolicies(PaymentReceiveAction.View, AbilitySubject.PaymentReceive),
@@ -391,6 +397,29 @@ export default class PaymentReceivesController extends BaseController {
}
}
/**
*
* @async
* @param {Request} req -
* @param {Response} res -
*/
private async getPaymentReceivedState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const data = await this.paymentReceiveApplication.getPaymentReceivedState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/**
* Retrieve the given payment receive details.
* @async
@@ -444,7 +473,7 @@ export default class PaymentReceivesController extends BaseController {
]);
// Response in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent =
const [pdfContent, filename] =
await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId,
paymentReceiveId
@@ -452,6 +481,7 @@ export default class PaymentReceivesController extends BaseController {
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
// Response in json format.

View File

@@ -51,7 +51,7 @@ export default class SalesEstimatesController extends BaseController {
router.post(
'/:id/approve',
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
[this.validateSpecificEstimateSchema],
[...this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.approveSaleEstimate.bind(this)),
this.handleServiceErrors
@@ -59,7 +59,7 @@ export default class SalesEstimatesController extends BaseController {
router.post(
'/:id/reject',
CheckPolicies(SaleEstimateAction.Edit, AbilitySubject.SaleEstimate),
[this.validateSpecificEstimateSchema],
[...this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.rejectSaleEstimate.bind(this)),
this.handleServiceErrors
@@ -105,6 +105,12 @@ export default class SalesEstimatesController extends BaseController {
asyncMiddleware(this.deleteEstimate.bind(this)),
this.handleServiceErrors
);
router.get(
'/state',
CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate),
this.getSaleEstimateState.bind(this),
this.handleServiceErrors
);
router.get(
'/:id',
CheckPolicies(SaleEstimateAction.View, AbilitySubject.SaleEstimate),
@@ -392,13 +398,15 @@ export default class SalesEstimatesController extends BaseController {
]);
// Retrieves estimate in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf(
tenantId,
estimateId
);
const [pdfContent, filename] =
await this.saleEstimatesApplication.getSaleEstimatePdf(
tenantId,
estimateId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
// Retrieves estimates in json format.
@@ -546,6 +554,23 @@ export default class SalesEstimatesController extends BaseController {
}
};
private getSaleEstimateState = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
try {
const data = await this.saleEstimatesApplication.getSaleEstimateState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param {Error} error

View File

@@ -130,6 +130,12 @@ export default class SaleInvoicesController extends BaseController {
this.asyncMiddleware(this.getInvoicePaymentTransactions),
this.handleServiceErrors
);
router.get(
'/state',
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
asyncMiddleware(this.getSaleInvoiceState.bind(this)),
this.handleServiceErrors
);
router.get(
'/:id',
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
@@ -138,6 +144,7 @@ export default class SaleInvoicesController extends BaseController {
asyncMiddleware(this.getSaleInvoice.bind(this)),
this.handleServiceErrors
);
router.get(
'/',
CheckPolicies(SaleInvoiceAction.View, AbilitySubject.SaleInvoice),
@@ -172,10 +179,21 @@ export default class SaleInvoicesController extends BaseController {
'/:id/mail',
[
...this.specificSaleInvoiceValidation,
body('subject').isString().optional(),
body('subject').isString().optional({ nullable: true }),
body('message').isString().optional({ nullable: true }),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('to').isArray().exists(),
body('to.*').isString().isEmail().optional(),
body('cc').isArray().optional({ nullable: true }),
body('cc.*').isString().isEmail().optional(),
body('bcc').isArray().optional({ nullable: true }),
body('bcc.*').isString().isEmail().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
@@ -183,7 +201,7 @@ export default class SaleInvoicesController extends BaseController {
this.handleServiceErrors
);
router.get(
'/:id/mail',
'/:id/mail/state',
[...this.specificSaleInvoiceValidation],
this.validationResult,
asyncMiddleware(this.getSaleInvoiceMail.bind(this)),
@@ -258,6 +276,11 @@ export default class SaleInvoicesController extends BaseController {
// Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// Payment methods.
check('payment_methods').optional({ nullable: true }).isArray(),
check('payment_methods.*.payment_integration_id').exists().toInt(),
check('payment_methods.*.enable').exists().isBoolean(),
];
}
@@ -429,13 +452,15 @@ export default class SaleInvoicesController extends BaseController {
]);
// Retrieves invoice in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoiceId
);
const [pdfContent, filename] =
await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoiceId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
// Retrieves invoice in json format.
@@ -448,6 +473,24 @@ export default class SaleInvoicesController extends BaseController {
return res.status(200).send({ saleInvoice });
}
}
private async getSaleInvoiceState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const data = await this.saleInvoiceApplication.getSaleInvoiceState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/**
* Retrieve paginated sales invoices with custom view metadata.
* @param {Request} req
@@ -746,7 +789,7 @@ export default class SaleInvoicesController extends BaseController {
}
/**
* Retrieves the default mail options of the given sale invoice.
* Retrieves the mail state of the given sale invoice.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
@@ -760,7 +803,7 @@ export default class SaleInvoicesController extends BaseController {
const { id: invoiceId } = req.params;
try {
const data = await this.saleInvoiceApplication.getSaleInvoiceMail(
const data = await this.saleInvoiceApplication.getSaleInvoiceMailState(
tenantId,
invoiceId
);

View File

@@ -108,6 +108,12 @@ export default class SalesReceiptsController extends BaseController {
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse
);
router.get(
'/state',
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
asyncMiddleware(this.getSaleReceiptState.bind(this)),
this.handleServiceErrors
);
router.get(
'/:id',
CheckPolicies(SaleReceiptAction.View, AbilitySubject.SaleReceipt),
@@ -350,13 +356,15 @@ export default class SalesReceiptsController extends BaseController {
]);
// Retrieves receipt in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf(
tenantId,
saleReceiptId
);
const [pdfContent, filename] =
await this.saleReceiptsApplication.getSaleReceiptPdf(
tenantId,
saleReceiptId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
'Content-Disposition': `attachment; filename="${filename}"`,
});
res.send(pdfContent);
// Retrieves receipt in json format.
@@ -369,6 +377,30 @@ export default class SalesReceiptsController extends BaseController {
}
}
/**
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async getSaleReceiptState(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
// Retrieves receipt in pdf format.
try {
const data = await this.saleReceiptsApplication.getSaleReceiptState(
tenantId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/**
* Sale receipt notification via SMS.
* @param {Request} req

View File

@@ -0,0 +1,117 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { param } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { PaymentLinksApplication } from '@/services/PaymentLinks/PaymentLinksApplication';
@Service()
export class PublicSharableLinkController extends BaseController {
@Inject()
private paymentLinkApp: PaymentLinksApplication;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/:paymentLinkId/invoice',
[param('paymentLinkId').exists()],
this.validationResult,
this.getPaymentLinkPublicMeta.bind(this),
this.validationResult
);
router.get(
'/:paymentLinkId/invoice/pdf',
[param('paymentLinkId').exists()],
this.validationResult,
this.getPaymentLinkInvoicePdf.bind(this),
this.validationResult
);
router.post(
'/:paymentLinkId/stripe_checkout_session',
[param('paymentLinkId').exists()],
this.validationResult,
this.createInvoicePaymentLinkCheckoutSession.bind(this)
);
return router;
}
/**
* Retrieves the payment link public meta.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
*/
public async getPaymentLinkPublicMeta(
req: Request<{ paymentLinkId: string }>,
res: Response,
next: NextFunction
) {
const { paymentLinkId } = req.params;
try {
const data = await this.paymentLinkApp.getInvoicePaymentLink(
paymentLinkId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/**
* Creates a Stripe checkout session for the given payment link id.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
public async createInvoicePaymentLinkCheckoutSession(
req: Request<{ paymentLinkId: string }>,
res: Response,
next: NextFunction
) {
const { paymentLinkId } = req.params;
try {
const session =
await this.paymentLinkApp.createInvoicePaymentCheckoutSession(
paymentLinkId
);
return res.status(200).send(session);
} catch (error) {
next(error);
}
}
/**
* Retrieves the sale invoice pdf of the given payment link.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async getPaymentLinkInvoicePdf(
req: Request<{ paymentLinkId: string }>,
res: Response,
next: NextFunction
) {
const { paymentLinkId } = req.params;
try {
const pdfContent = await this.paymentLinkApp.getPaymentLinkInvoicePdf(
paymentLinkId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { body } from 'express-validator';
import { AbilitySubject, PaymentReceiveAction } from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { GenerateShareLink } from '@/services/Sales/Invoices/GenerateeInvoicePaymentLink';
@Service()
export class ShareLinkController extends BaseController {
@Inject()
private generateShareLinkService: GenerateShareLink;
/**
* Router constructor.
*/
router() {
const router = Router();
router.post(
'/payment-links/generate',
CheckPolicies(PaymentReceiveAction.Edit, AbilitySubject.PaymentReceive),
[
body('transaction_type').exists(),
body('transaction_id').exists().isNumeric().toInt(),
body('publicity').optional(),
body('expiry_date').optional({ nullable: true }),
],
this.validationResult,
asyncMiddleware(this.generateShareLink.bind(this))
);
return router;
}
/**
* Generates sharable link for the given transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async generateShareLink(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { transactionType, transactionId, publicity, expiryDate } =
this.matchedBodyData(req);
try {
const link = await this.generateShareLinkService.generatePaymentLink(
tenantId,
transactionId,
transactionType,
publicity,
expiryDate
);
res.status(200).json({ link });
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,122 @@
import { NextFunction, Request, Response, Router } from 'express';
import { body } from 'express-validator';
import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import { StripePaymentApplication } from '@/services/StripePayment/StripePaymentApplication';
import BaseController from '../BaseController';
@Service()
export class StripeIntegrationController extends BaseController {
@Inject()
private stripePaymentApp: StripePaymentApplication;
public router() {
const router = Router();
router.get('/link', this.getStripeConnectLink.bind(this));
router.post(
'/callback',
[body('code').exists()],
this.validationResult,
this.exchangeOAuth.bind(this)
);
router.post('/account', asyncMiddleware(this.createAccount.bind(this)));
router.post(
'/account_link',
[body('stripe_account_id').exists()],
this.validationResult,
asyncMiddleware(this.createAccountLink.bind(this))
);
return router;
}
/**
* Retrieves Stripe OAuth2 connect link.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
public async getStripeConnectLink(
req: Request,
res: Response,
next: NextFunction
) {
try {
const authorizationUri = this.stripePaymentApp.getStripeConnectLink();
return res.status(200).send({ url: authorizationUri });
} catch (error) {
next(error);
}
}
/**
* Exchanges the given Stripe authorization code to Stripe user id and access token.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<void>}
*/
public async exchangeOAuth(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { code } = this.matchedBodyData(req);
try {
await this.stripePaymentApp.exchangeStripeOAuthToken(tenantId, code);
return res.status(200).send({});
} catch (error) {
next(error);
}
}
/**
* Creates a new Stripe account.
* @param {Request} req - The Express request object.
* @param {Response} res - The Express response object.
* @param {NextFunction} next - The Express next middleware function.
* @returns {Promise<void>}
*/
public async createAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
const accountId = await this.stripePaymentApp.createStripeAccount(
tenantId
);
return res.status(201).json({
accountId,
message: 'The Stripe account has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Creates a new Stripe account session.
* @param {Request} req - The Express request object.
* @param {Response} res - The Express response object.
* @param {NextFunction} next - The Express next middleware function.
* @returns {Promise<void>}
*/
public async createAccountLink(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { stripeAccountId } = this.matchedBodyData(req);
try {
const clientSecret = await this.stripePaymentApp.createAccountLink(
tenantId,
stripeAccountId
);
return res.status(200).json({ clientSecret });
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,83 @@
import { NextFunction, Request, Response, Router } from 'express';
import { Inject, Service } from 'typedi';
import bodyParser from 'body-parser';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { StripeWebhookEventPayload } from '@/interfaces/StripePayment';
import { StripePaymentService } from '@/services/StripePayment/StripePaymentService';
import events from '@/subscribers/events';
import config from '@/config';
@Service()
export class StripeWebhooksController {
@Inject()
private stripePaymentService: StripePaymentService;
@Inject()
private eventPublisher: EventPublisher;
public router() {
const router = Router();
router.post(
'/stripe',
bodyParser.raw({ type: 'application/json' }),
this.handleWebhook.bind(this)
);
return router;
}
/**
* Handles incoming Stripe webhook events.
* Verifies the webhook signature, processes the event based on its type,
* and triggers appropriate actions or events in the system.
*
* @param {Request} req - The Express request object containing the webhook payload.
* @param {Response} res - The Express response object.
* @param {NextFunction} next - The Express next middleware function.
*/
private async handleWebhook(req: Request, res: Response, next: NextFunction) {
try {
let event = req.body;
const sig = req.headers['stripe-signature'];
// Verify webhook signature and extract the event.
// See https://stripe.com/docs/webhooks#verify-events for more information.
try {
event = this.stripePaymentService.stripe.webhooks.constructEvent(
req.rawBody,
sig,
config.stripePayment.webhooksSecret
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event based on its type
switch (event.type) {
case 'checkout.session.completed':
// Triggers `onStripeCheckoutSessionCompleted` event.
this.eventPublisher.emitAsync(
events.stripeWebhooks.onCheckoutSessionCompleted,
{
event,
} as StripeWebhookEventPayload
);
break;
case 'account.updated':
this.eventPublisher.emitAsync(
events.stripeWebhooks.onAccountUpdated,
{
event,
} as StripeWebhookEventPayload
);
break;
// Add more cases as needed
default:
console.log(`Unhandled event type ${event.type}`);
}
res.status(200).json({ received: true });
} catch (error) {
next(error);
}
}
}

View File

@@ -1,9 +1,10 @@
import { NextFunction, Router, Request, Response } from 'express';
import { Inject, Service } from 'typedi';
import Container, { Inject, Service } from 'typedi';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import BaseController from '../BaseController';
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
import { StripeWebhooksController } from '../StripeIntegration/StripeWebhooksController';
@Service()
export class Webhooks extends BaseController {
@@ -24,6 +25,8 @@ export class Webhooks extends BaseController {
router.post('/lemon', this.lemonWebhooks.bind(this));
router.use(Container.get(StripeWebhooksController).router());
return router;
}

View File

@@ -64,7 +64,11 @@ import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
import { AttachmentsController } from './controllers/Attachments/AttachmentsController';
import { OneClickDemoController } from './controllers/OneClickDemo/OneClickDemoController';
import { StripeIntegrationController } from './controllers/StripeIntegration/StripeIntegrationController';
import { ShareLinkController } from './controllers/ShareLink/ShareLinkController';
import { PublicSharableLinkController } from './controllers/ShareLink/PublicSharableLinkController';
import { PdfTemplatesController } from './controllers/PdfTemplates/PdfTemplatesController';
import { PaymentServicesController } from './controllers/PaymentServices/PaymentServicesController';
export default () => {
const app = Router();
@@ -83,6 +87,10 @@ export default () => {
app.use('/account', Container.get(Account).router());
app.use('/webhooks', Container.get(Webhooks).router());
app.use('/demo', Container.get(OneClickDemoController).router());
app.use(
'/payment-links',
Container.get(PublicSharableLinkController).router()
);
// - Dashboard routes.
// ---------------------------
@@ -148,14 +156,22 @@ export default () => {
dashboard.use('/import', Container.get(ImportController).router());
dashboard.use('/export', Container.get(ExportController).router());
dashboard.use('/attachments', Container.get(AttachmentsController).router());
dashboard.use(
'/stripe_integration',
Container.get(StripeIntegrationController).router()
);
dashboard.use(
'/pdf-templates',
Container.get(PdfTemplatesController).router()
);
dashboard.use(
'/payment-services',
Container.get(PaymentServicesController).router()
);
dashboard.use('/', Container.get(ProjectTasksController).router());
dashboard.use('/', Container.get(ProjectTimesController).router());
dashboard.use('/', Container.get(WarehousesItemController).router());
dashboard.use('/', Container.get(ShareLinkController).router());
dashboard.use('/dashboard', Container.get(DashboardController).router());
dashboard.use('/', Container.get(Miscellaneous).router());

View File

@@ -50,7 +50,8 @@ export const injectI18nUtils = (req) => {
export const initalizeTenantServices = async (tenantId: number) => {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
.withGraphFetched('metadata')
.throwIfNotFound();
const tenantServices = Container.get(TenancyService);
const tenantsManager = Container.get(TenantsManagerService);

View File

@@ -259,6 +259,17 @@ module.exports = {
*/
posthog: {
apiKey: process.env.POSTHOG_API_KEY,
host: process.env.POSTHOG_HOST
}
host: process.env.POSTHOG_HOST,
},
/**
* Stripe Payment Integration.
*/
stripePayment: {
secretKey: process.env.STRIPE_PAYMENT_SECRET_KEY || '',
publishableKey: process.env.STRIPE_PAYMENT_PUBLISHABLE_KEY || '',
clientId: process.env.STRIPE_PAYMENT_CLIENT_ID || '',
redirectTo: process.env.STRIPE_PAYMENT_REDIRECT_URL || '',
webhooksSecret: process.env.STRIPE_PAYMENT_WEBHOOKS_SECRET || '',
},
};

View File

@@ -2,14 +2,25 @@ export const SALE_INVOICE_CREATED = 'Sale invoice created';
export const SALE_INVOICE_EDITED = 'Sale invoice edited';
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
export const SALE_INVOICE_VIEWED = 'Sale invoice viewed';
export const SALE_INVOICE_PDF_VIEWED = 'Sale invoice PDF viewed';
export const SALE_INVOICE_MAIL_SENT = 'Sale invoice mail sent';
export const SALE_INVOICE_MAIL_REMINDER_SENT =
'Sale invoice reminder mail sent';
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted';
export const SALE_ESTIMATE_PDF_VIEWED = 'Sale estimate PDF viewed';
export const PAYMENT_RECEIVED_CREATED = 'Payment received created';
export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted';
export const PAYMENT_RECEIVED_PDF_VIEWED = 'Payment received PDF viewed';
export const SALE_RECEIPT_PDF_VIEWED = 'Sale credit PDF viewed';
export const CREDIT_NOTE_PDF_VIEWED = 'Credit note PDF viewed';
export const BILL_CREATED = 'Bill created';
export const BILL_EDITED = 'Bill edited';
@@ -26,10 +37,12 @@ export const EXPENSE_DELETED = 'Expense deleted';
export const ACCOUNT_CREATED = 'Account created';
export const ACCOUNT_EDITED = 'Account Edited';
export const ACCOUNT_DELETED = 'Account deleted';
export const ACCOUNT_VIEWED = 'Account viewed';
export const ITEM_EVENT_CREATED = 'Item created';
export const ITEM_EVENT_EDITED = 'Item edited';
export const ITEM_EVENT_DELETED = 'Item deleted';
export const ITEM_EVENT_VIEWED = 'Item viewed';
export const AUTH_SIGNED_UP = 'Auth Signed-up';
export const AUTH_RESET_PASSWORD = 'Auth reset password';
@@ -69,6 +82,19 @@ export const BANK_RULE_CREATED = 'Bank rule created';
export const BANK_RULE_EDITED = 'Bank rule edited';
export const BANK_RULE_DELETED = 'Bank rule deleted';
export const PDF_TEMPLATE_CREATED = 'PDF template created';
export const PDF_TEMPLATE_EDITED = 'PDF template edited';
export const PDF_TEMPLATE_DELETED = 'PDF template deleted';
export const PDF_TEMPLATE_ASSIGNED_DEFAULT = 'PDF template assigned as default';
export const PAYMENT_METHOD_EDITED = 'Payment method edited';
export const PAYMENT_METHOD_DELETED = 'Payment method deleted';
export const INVOICE_PAYMENT_LINK_GENERATED = 'Invoice payment link generated';
export const STRIPE_INTEGRAION_CONNECTED =
'Stripe integration oauth2 connected';
// # Event Groups
export const ACCOUNT_GROUP = 'Account';
export const ITEM_GROUP = 'Item';

View File

@@ -129,6 +129,7 @@ export const ACCOUNT_TYPES = [
normal: ACCOUNT_NORMAL.CREDIT,
rootType: ACCOUNT_ROOT_TYPE.LIABILITY,
parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY,
multiCurrency: true,
balanceSheet: true,
incomeSheet: false,
},

View File

@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.table('payment_receives', (table) => {
table.string('stripe_pintent_id').nullable();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.table('payment_receives', (table) => {
table.dropColumn('stripe_pintent_id');
});
};

View File

@@ -0,0 +1,25 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable('payment_integrations', (table) => {
table.increments('id');
table.string('service');
table.string('name');
table.string('slug');
table.boolean('payment_enabled').defaultTo(false);
table.boolean('payout_enabled').defaultTo(false);
table.string('account_id');
table.json('options');
table.timestamps();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('payment_integrations');
};

View File

@@ -0,0 +1,27 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.createTable('transactions_payment_methods', (table) => {
table.increments('id');
table.integer('reference_id').unsigned();
table.string('reference_type');
table
.integer('payment_integration_id')
.unsigned()
.index()
.references('id')
.inTable('payment_integrations');
table.boolean('enable').defaultTo(false);
table.json('options').nullable();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.dropTableIfExists('transactions_payment_methods');
};

View File

@@ -31,6 +31,17 @@ export const PrepardExpenses = {
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 default [
{
name: 'Bank Account',

View File

@@ -262,16 +262,24 @@ export type ICreditNoteGLCommonEntry = Pick<
>;
export interface CreditNotePdfTemplateAttributes {
// # Primary color
primaryColor: string;
secondaryColor: string;
// # Company logo
showCompanyLogo: boolean;
companyLogo: string;
// # Company name
companyName: string;
billedToAddress: string[];
billedFromAddress: string[];
showBilledToAddress: boolean;
showBilledFromAddress: boolean;
// # Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// # Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
total: string;
@@ -306,3 +314,7 @@ export interface CreditNotePdfTemplateAttributes {
showCreditNoteDate: boolean;
creditNoteDateLabel: string;
}
export interface ICreditNoteState {
defaultTemplateId: number;
}

View File

@@ -30,18 +30,14 @@ export interface AddressItem {
}
export interface CommonMailOptions {
toAddresses: AddressItem[];
fromAddresses: AddressItem[];
from: string;
to: string | string[];
from: Array<string>;
subject: string;
body: string;
message: string;
to: Array<string>;
cc?: Array<string>;
bcc?: Array<string>;
data?: Record<string, any>;
}
export interface CommonMailOptionsDTO {
to?: string | string[];
from?: string;
subject?: string;
body?: string;
export interface CommonMailOptionsDTO extends Partial<CommonMailOptions> {
}

View File

@@ -207,10 +207,13 @@ export interface PaymentReceivedPdfTemplateAttributes {
companyLogo: string;
companyName: string;
billedToAddress: string[];
billedFromAddress: string[];
showBilledFromAddress: boolean;
showBillingToAddress: boolean;
// Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
total: string;
@@ -235,3 +238,8 @@ export interface PaymentReceivedPdfTemplateAttributes {
showPaymentReceivedDate: boolean;
paymentReceivedDateLabel: string;
}
export interface IPaymentReceivedState {
defaultTemplateId: number;
}

View File

@@ -144,3 +144,6 @@ export interface ISaleEstimateMailPresendEvent {
messageOptions: SaleEstimateMailOptionsDTO;
}
export interface ISaleEstimateState {
defaultTemplateId: number;
}

View File

@@ -5,6 +5,34 @@ import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { AttachmentLinkDTO } from './Attachments';
export interface PaymentIntegrationTransactionLink {
id: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
}
export interface PaymentIntegrationTransactionLinkEventPayload {
tenantId: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
saleInvoiceId: number;
trx?: Knex.Transaction;
}
export interface PaymentIntegrationTransactionLinkDeleteEventPayload {
tenantId: number;
enable: true;
paymentIntegrationId: number;
referenceType: string;
referenceId: number;
oldSaleInvoiceId: number;
trx?: Knex.Transaction;
}
export interface ISaleInvoice {
id: number;
amount: number;
@@ -50,6 +78,8 @@ export interface ISaleInvoice {
invoiceMessage: string;
pdfTemplateId?: number;
paymentMethods?: Array<PaymentIntegrationTransactionLink>;
}
export interface ISaleInvoiceDTO {
@@ -136,7 +166,13 @@ export interface ISaleInvoiceEditingPayload {
export interface ISaleInvoiceDeletePayload {
tenantId: number;
saleInvoice: ISaleInvoice;
oldSaleInvoice: ISaleInvoice;
saleInvoiceId: number;
}
export interface ISaleInvoiceDeletingPayload {
tenantId: number;
oldSaleInvoice: ISaleInvoice;
saleInvoiceId: number;
trx: Knex.Transaction;
}
@@ -198,7 +234,32 @@ export enum SaleInvoiceAction {
}
export interface SaleInvoiceMailOptions extends CommonMailOptions {
attachInvoice: boolean;
attachInvoice?: boolean;
formatArgs?: Record<string, any>;
}
export interface SaleInvoiceMailState extends SaleInvoiceMailOptions {
invoiceNo: string;
invoiceDate: string;
invoiceDateFormatted: string;
dueDate: string;
dueDateFormatted: string;
total: number;
totalFormatted: string;
subtotal: number;
subtotalFormatted: number;
companyName: string;
companyLogoUri: string;
customerName: string;
// # Invoice entries
entries?: Array<{ label: string; total: string; quantity: string | number }>;
}
export interface SendInvoiceMailDTO extends CommonMailOptionsDTO {
@@ -215,6 +276,7 @@ export interface ISaleInvoiceMailSend {
tenantId: number;
saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO;
formattedMessageOptions: SaleInvoiceMailOptions;
}
export interface ISaleInvoiceMailSent {
@@ -223,7 +285,6 @@ export interface ISaleInvoiceMailSent {
messageOptions: SendInvoiceMailDTO;
}
// Invoice Pdf Document
export interface InvoicePdfLine {
item: string;
@@ -241,9 +302,9 @@ export interface InvoicePdfTax {
export interface InvoicePdfTemplateAttributes {
primaryColor: string;
secondaryColor: string;
companyName: string;
showCompanyLogo: boolean;
companyLogo: string;
@@ -259,8 +320,13 @@ export interface InvoicePdfTemplateAttributes {
invoiceNumber: string;
showInvoiceNumber: boolean;
showBillingToAddress: boolean;
showBilledFromAddress: boolean;
// Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
lineItemLabel: string;
@@ -298,7 +364,8 @@ export interface InvoicePdfTemplateAttributes {
statementLabel: string;
showStatement: boolean;
statement: string;
}
billedToAddress: string[];
billedFromAddres: string[];
}
export interface ISaleInvocieState {
defaultTemplateId: number;
}

View File

@@ -163,11 +163,13 @@ export interface ISaleReceiptBrandingTemplateAttributes {
companyLogo: string;
companyName: string;
// Address
billedToAddress: string[];
billedFromAddress: string[];
showBilledFromAddress: boolean;
showBilledToAddress: boolean;
// Customer Address
showCustomerAddress: boolean;
customerAddress: string;
// Company address
showCompanyAddress: boolean;
companyAddress: string;
billedToLabel: string;
// Total
@@ -209,3 +211,8 @@ export interface ISaleReceiptBrandingTemplateAttributes {
showReceiptDate: boolean;
receiptDateLabel: string;
}
export interface ISaleReceiptState {
defaultTemplateId: number;
}

View File

@@ -18,14 +18,26 @@ export interface IOrganizationBuildDTO {
dateFormat?: string;
}
interface OrganizationAddressDTO {
address1: string;
address2: string;
postalCode: string;
city: string;
stateProvince: string;
phone: string;
}
export interface IOrganizationUpdateDTO {
name: string;
location: string;
baseCurrency: string;
timezone: string;
fiscalYear: string;
industry: string;
taxNumber: string;
location?: string;
baseCurrency?: string;
timezone?: string;
fiscalYear?: string;
industry?: string;
taxNumber?: string;
primaryColor?: string;
logoKey?: string;
address?: OrganizationAddressDTO;
}
export interface IOrganizationBuildEventPayload {
@@ -36,4 +48,4 @@ export interface IOrganizationBuildEventPayload {
export interface IOrganizationBuiltEventPayload {
tenantId: number;
}
}

View File

@@ -0,0 +1,20 @@
export interface StripePaymentLinkCreatedEventPayload {
tenantId: number;
paymentLinkId: string;
saleInvoiceId: number;
stripeIntegrationId: number;
}
export interface StripeCheckoutSessionCompletedEventPayload {
event: any;
}
export interface StripeInvoiceCheckoutSessionPOJO {
sessionId: string;
publishableKey: string;
redirectTo: string;
}
export interface StripeWebhookEventPayload {
event: any;
}

View File

@@ -117,8 +117,10 @@ import { DisconnectPlaidItemOnAccountDeleted } from '@/services/Banking/BankAcco
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
import { InvoicePaymentIntegrationSubscriber } from '@/services/Sales/Invoices/subscribers/InvoicePaymentIntegrationSubscriber';
import { StripeWebhooksSubscriber } from '@/services/StripePayment/events/StripeWebhooksSubscriber';
import { SeedStripeAccountsOnOAuthGrantedSubscriber } from '@/services/StripePayment/events/SeedStripeAccounts';
export default () => {
return new EventPublisher();
@@ -252,7 +254,6 @@ export const susbcribers = () => {
// Subscription
SubscribeFreeOnSignupCommunity,
SendVerfiyMailOnSignUp,
TriggerInvalidateCacheOnSubscriptionChange,
// Attachments
AttachmentsOnSaleInvoiceCreated,
@@ -291,6 +292,11 @@ export const susbcribers = () => {
// Demo Account
SeedInitialDemoAccountDataOnOrgBuild,
// Stripe Payment
InvoicePaymentIntegrationSubscriber,
StripeWebhooksSubscriber,
SeedStripeAccountsOnOAuthGrantedSubscriber,
...EventsTrackerListeners
];
};

View File

@@ -69,6 +69,8 @@ import { BankRuleCondition } from '@/models/BankRuleCondition';
import { RecognizedBankTransaction } from '@/models/RecognizedBankTransaction';
import { MatchedBankTransaction } from '@/models/MatchedBankTransaction';
import { PdfTemplate } from '@/models/PdfTemplate';
import { PaymentIntegration } from '@/models/PaymentIntegration';
import { TransactionPaymentServiceEntry } from '@/models/TransactionPaymentServiceEntry';
export default (knex) => {
const models = {
@@ -140,7 +142,9 @@ export default (knex) => {
BankRuleCondition,
RecognizedBankTransaction,
MatchedBankTransaction,
PdfTemplate
PdfTemplate,
PaymentIntegration,
TransactionPaymentServiceEntry,
};
return mapValues(models, (model) => model.bindKnex(knex));
};

View File

@@ -294,7 +294,7 @@ export default {
name: 'item.field.note',
fieldType: 'text',
},
category: {
categoryId: {
name: 'item.field.category',
fieldType: 'relation',
relationModel: 'ItemCategory',

View File

@@ -0,0 +1,61 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export class PaymentIntegration extends Model {
paymentEnabled!: boolean;
payoutEnabled!: boolean;
static get tableName() {
return 'payment_integrations';
}
static get idColumn() {
return 'id';
}
static get virtualAttributes() {
return ['fullEnabled'];
}
static get jsonAttributes() {
return ['options'];
}
get fullEnabled() {
return this.paymentEnabled && this.payoutEnabled;
}
static get modifiers() {
return {
/**
* Query to filter enabled payment and payout.
*/
fullEnabled(query) {
query.where('paymentEnabled', true).andWhere('payoutEnabled', true);
},
};
}
static get jsonSchema() {
return {
type: 'object',
required: ['name', 'service'],
properties: {
id: { type: 'integer' },
service: { type: 'string' },
paymentEnabled: { type: 'boolean' },
payoutEnabled: { type: 'boolean' },
accountId: { type: 'string' },
options: {
type: 'object',
properties: {
bankAccountId: { type: 'number' },
clearingAccountId: { type: 'number' },
},
},
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' },
},
};
}
}

View File

@@ -29,6 +29,20 @@ export class PdfTemplate extends TenantModel {
};
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the due invoices.
*/
default(query) {
query.where('default', true);
},
};
}
/**
* Virtual attributes.
*/

View File

@@ -413,6 +413,10 @@ export default class SaleInvoice extends mixin(TenantModel, [
const TaxRateTransaction = require('models/TaxRateTransaction');
const Document = require('models/Document');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
const {
TransactionPaymentServiceEntry,
} = require('models/TransactionPaymentServiceEntry');
const { PdfTemplate } = require('models/PdfTemplate');
return {
/**
@@ -509,7 +513,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
join: {
from: 'sales_invoices.warehouseId',
to: 'warehouses.id',
}
},
},
/**
@@ -566,12 +570,42 @@ export default class SaleInvoice extends mixin(TenantModel, [
modelClass: MatchedBankTransaction,
join: {
from: 'sales_invoices.id',
to: "matched_bank_transactions.referenceId",
to: 'matched_bank_transactions.referenceId',
},
filter(query) {
query.where('reference_type', 'SaleInvoice');
},
},
/**
* Sale invoice may belongs to payment methods entries.
*/
paymentMethods: {
relation: Model.HasManyRelation,
modelClass: TransactionPaymentServiceEntry,
join: {
from: 'sales_invoices.id',
to: 'transactions_payment_methods.referenceId',
},
beforeInsert: (model) => {
model.referenceType = 'SaleInvoice';
},
filter: (query) => {
query.where('reference_type', 'SaleInvoice');
},
},
/**
* Sale invoice may belongs to pdf branding template.
*/
pdfTemplate: {
relation: Model.BelongsToOneRelation,
modelClass: PdfTemplate,
join: {
from: 'sales_invoices.pdfTemplateId',
to: 'pdf_templates.id',
}
},
};
}

View File

@@ -0,0 +1,46 @@
import TenantModel from 'models/TenantModel';
export class TransactionPaymentServiceEntry extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'transactions_payment_methods';
}
/**
* Json schema of the model.
*/
static get jsonSchema() {
return {
type: 'object',
required: ['paymentIntegrationId'],
properties: {
id: { type: 'integer' },
referenceId: { type: 'integer' },
referenceType: { type: 'string' },
paymentIntegrationId: { type: 'integer' },
enable: { type: 'boolean' },
options: { type: 'object' },
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { PaymentIntegration } = require('./PaymentIntegration');
return {
paymentIntegration: {
relation: TenantModel.BelongsToOneRelation,
modelClass: PaymentIntegration,
join: {
from: 'transactions_payment_methods.paymentIntegrationId',
to: 'payment_integrations.id',
},
},
};
}
}

View File

@@ -11,6 +11,12 @@ export default class UncategorizedCashflowTransaction extends mixin(
) {
id!: number;
date!: Date | string;
/**
* Transaction amount.
* Negative represents to spending and positive to deposit/card charge.
* @param {number}
*/
amount!: number;
categorized!: boolean;
accountId!: number;

View File

@@ -4,6 +4,7 @@ import { IAccount } from '@/interfaces';
import { Knex } from 'knex';
import {
PrepardExpenses,
StripeClearingAccount,
TaxPayableAccount,
UnearnedRevenueAccount,
} from '@/database/seeds/data/accounts';
@@ -247,4 +248,37 @@ export default class AccountRepository extends TenantRepository {
}
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
) {
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({
tenantId: this.tenantId,
});
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;
}
}

View File

@@ -14,6 +14,8 @@ export class AccountTransformer extends Transformer {
*/
public includeAttributes = (): string[] => {
return [
'accountTypeLabel',
'accountNormalFormatted',
'formattedAmount',
'flattenName',
'bankBalanceFormatted',
@@ -84,6 +86,22 @@ export class AccountTransformer extends Transformer {
return account.plaidItem?.isPaused || false;
};
/**
* Retrieves formatted account type label.
* @returns {string}
*/
protected accountTypeLabel = (account: any): string => {
return this.context.i18n.__(account.accountTypeLabel);
};
/**
* Retrieves formatted account normal.
* @returns {string}
*/
protected accountNormalFormatted = (account: any): string => {
return this.context.i18n.__(account.accountNormalFormatted);
};
/**
* Transformes the accounts collection to flat or nested array.
* @param {IAccount[]}

View File

@@ -3,6 +3,8 @@ import I18nService from '@/services/I18n/I18nService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { AccountTransformer } from './AccountTransform';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class GetAccount {
@@ -15,6 +17,9 @@ export class GetAccount {
@Inject()
private transformer: TransformerInjectable;
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve the given account details.
* @param {number} tenantId
@@ -39,10 +44,13 @@ export class GetAccount {
new AccountTransformer(),
{ accountsGraph }
);
return this.i18nService.i18nApply(
[['accountTypeLabel'], ['accountNormalFormatted']],
transformed,
tenantId
);
const eventPayload = {
tenantId,
accountId,
};
// Triggers `onAccountViewed` event.
await this.eventPublisher.emitAsync(events.accounts.onViewed, eventPayload);
return transformed;
};
}

View File

@@ -1,17 +1,16 @@
import { NextFunction, Request, Response } from 'express';
import multer from 'multer';
import type { Multer } from 'multer';
import multerS3 from 'multer-s3';
import { s3 } from '@/lib/S3/S3';
import { Service } from 'typedi';
import config from '@/config';
import { NextFunction, Request, Response } from 'express';
@Service()
export class AttachmentUploadPipeline {
/**
* Middleware to ensure that S3 configuration is properly set before proceeding.
* This function checks if the necessary S3 configuration keys are present and throws an error if any are missing.
*
* @param req The HTTP request object.
* @param res The HTTP response object.
* @param next The callback to pass control to the next middleware function.
@@ -49,6 +48,11 @@ export class AttachmentUploadPipeline {
key: function (req, file, cb) {
cb(null, Date.now().toString());
},
acl: function(req, file, cb) {
// Conditionally set file to public or private based on isPublic flag
const aclValue = true ? 'public-read' : 'private';
cb(null, aclValue); // Set ACL based on the isPublic flag
}
}),
});
}

View File

@@ -3,7 +3,7 @@ import { isEmpty } from 'lodash';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceCreatingPaylaod,
ISaleInvoiceDeletePayload,
ISaleInvoiceDeletingPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
@@ -146,13 +146,13 @@ export class AttachmentsOnSaleInvoiceCreated {
*/
private async handleUnlinkAttachmentsOnInvoiceDeleted({
tenantId,
saleInvoice,
oldSaleInvoice,
trx,
}: ISaleInvoiceDeletePayload) {
}: ISaleInvoiceDeletingPayload) {
await this.unlinkAttachmentService.unlinkAllModelKeys(
tenantId,
'SaleInvoice',
saleInvoice.id,
oldSaleInvoice.id,
trx
);
}

View File

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

View File

@@ -4,11 +4,27 @@ import {
Institution as PlaidInstitution,
AccountBase as PlaidAccount,
TransactionBase as PlaidTransactionBase,
AccountType as PlaidAccountType,
} from 'plaid';
import {
CreateUncategorizedTransactionDTO,
IAccountCreateDTO,
} from '@/interfaces';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
/**
* Retrieves the system account type from the given Plaid account type.
* @param {PlaidAccountType} plaidAccountType
* @returns {string}
*/
const getAccountTypeFromPlaidAccountType = (
plaidAccountType: PlaidAccountType
) => {
if (plaidAccountType === PlaidAccountType.Credit) {
return ACCOUNT_TYPE.CREDIT_CARD;
}
return ACCOUNT_TYPE.BANK;
};
/**
* Transformes the Plaid account to create cashflow account DTO.
@@ -28,7 +44,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
code: '',
description: plaidAccount.official_name,
currencyCode: plaidAccount.balances.iso_currency_code,
accountType: 'cash',
accountType: getAccountTypeFromPlaidAccountType(plaidAccount.type),
active: true,
bankBalance: plaidAccount.balances.current,
accountMask: plaidAccount.mask,

View File

@@ -1,14 +1,7 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ILedgerEntry,
ICashflowTransaction,
AccountNormal,
} from '../../interfaces';
import {
transformCashflowTransactionType,
getCashflowAccountTransactionsTypes,
} from './utils';
import { ILedgerEntry, ICashflowTransaction } from '../../interfaces';
import { transformCashflowTransactionType } from './utils';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import Ledger from '@/services/Accounting/Ledger';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@@ -70,7 +63,7 @@ export default class CashflowTransactionJournalEntries {
debit: cashflowTransaction.isCashDebit
? cashflowTransaction.localAmount
: 0,
accountNormal: AccountNormal.DEBIT,
accountNormal: cashflowTransaction?.cashflowAccount?.accountNormal,
index: 1,
};
};
@@ -143,6 +136,7 @@ export default class CashflowTransactionJournalEntries {
// Retrieves the cashflow transactions with associated entries.
const transaction = await CashflowTransaction.query(trx)
.findById(cashflowTransactionId)
.withGraphFetched('cashflowAccount')
.withGraphFetched('creditAccount');
// Retrieves the cashflow transaction ledger.

View File

@@ -4,6 +4,7 @@ import { CashflowAccountTransformer } from './CashflowAccountTransformer';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
@Service()
export default class GetCashflowAccountsService {
@@ -41,14 +42,20 @@ export default class GetCashflowAccountsService {
const accounts = await CashflowAccount.query().onBuild((builder) => {
dynamicList.buildQuery()(builder);
builder.whereIn('account_type', ['bank', 'cash']);
builder.whereIn('account_type', [
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.CREDIT_CARD,
]);
builder.modify('inactiveMode', filter.inactiveMode);
});
// Retrieves the transformed accounts.
return this.transformer.transform(
const transformed = await this.transformer.transform(
tenantId,
accounts,
new CashflowAccountTransformer()
);
return transformed;
}
}

View File

@@ -12,7 +12,7 @@ export class GetCashflowTransactionService {
private tenancy: HasTenancyService;
@Inject()
private transfromer: TransformerInjectable;
private transformer: TransformerInjectable;
/**
* Retrieve the given cashflow transaction.
@@ -37,7 +37,7 @@ export class GetCashflowTransactionService {
this.throwErrorCashflowTranscationNotFound(cashflowTransaction);
// Transformes the cashflow transaction model to POJO.
return this.transfromer.transform(
return this.transformer.transform(
tenantId,
cashflowTransaction,
new CashflowTransactionTransformer()

View File

@@ -1,26 +1,44 @@
import { Inject } from "typedi";
import { GetPdfTemplate } from "../PdfTemplate/GetPdfTemplate";
import { defaultCreditNoteBrandingAttributes } from "./constants";
import { mergePdfTemplateWithDefaultAttributes } from "../Sales/Invoices/utils";
import { Inject } from 'typedi';
import { GetPdfTemplate } from '../PdfTemplate/GetPdfTemplate';
import { defaultCreditNoteBrandingAttributes } from './constants';
import { mergePdfTemplateWithDefaultAttributes } from '../Sales/Invoices/utils';
import { GetOrganizationBrandingAttributes } from '../PdfTemplate/GetOrganizationBrandingAttributes';
export class CreditNoteBrandingTemplate {
@Inject()
private getPdfTemplateService: GetPdfTemplate;
@Inject()
private getOrgBrandingAttributes: GetOrganizationBrandingAttributes;
/**
* Retrieves the credit note branding template.
* @param {number} tenantId
* @param {number} templateId
* @param {number} tenantId
* @param {number} templateId
* @returns {}
*/
public async getCreditNoteBrandingTemplate(tenantId: number, templateId: number) {
public async getCreditNoteBrandingTemplate(
tenantId: number,
templateId: number
) {
const template = await this.getPdfTemplateService.getPdfTemplate(
tenantId,
templateId
);
// Retrieves the organization branding attributes.
const commonOrgBrandingAttrs =
await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(
tenantId
);
// Merges the default branding attributes with common organization branding attrs.
const organizationBrandingAttrs = {
...defaultCreditNoteBrandingAttributes,
...commonOrgBrandingAttrs,
};
const attributes = mergePdfTemplateWithDefaultAttributes(
template.attributes,
defaultCreditNoteBrandingAttributes
organizationBrandingAttrs
);
return {
...template,

View File

@@ -6,6 +6,8 @@ import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate';
import { CreditNotePdfTemplateAttributes } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService';
import { transformCreditNoteToPdfTemplate } from './utils';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class GetCreditNotePdf {
@@ -24,24 +26,59 @@ export default class GetCreditNotePdf {
@Inject()
private creditNoteBrandingTemplate: CreditNoteBrandingTemplate;
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieves sale invoice pdf content.
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
* @returns {Promise<[Buffer, string]>}
*/
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
public async getCreditNotePdf(
tenantId: number,
creditNoteId: number
): Promise<[Buffer, string]> {
const brandingAttributes = await this.getCreditNoteBrandingAttributes(
tenantId,
creditNoteId
);
console.log(brandingAttributes, 'brandingAttributes');
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/credit-note-standard',
brandingAttributes
);
return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent);
const filename = await this.getCreditNoteFilename(tenantId, creditNoteId);
const document = await this.chromiumlyTenancy.convertHtmlContent(
tenantId,
htmlContent
);
const eventPayload = { tenantId, creditNoteId };
// Triggers the `onCreditNotePdfViewed` event.
await this.eventPublisher.emitAsync(
events.creditNote.onPdfViewed,
eventPayload
);
return [document, filename];
}
/**
* Retrieves the filename pdf document of the given credit note.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<string>}
*/
public async getCreditNoteFilename(
tenantId: number,
creditNoteId: number
): Promise<string> {
const { CreditNote } = this.tenancy.models(tenantId);
const creditNote = await CreditNote.query().findById(creditNoteId);
return `Credit-${creditNote.creditNoteNumber}`;
}
/**

View File

@@ -0,0 +1,26 @@
import { Inject, Service } from 'typedi';
import { ICreditNoteState } from '@/interfaces';
import HasTenancyService from '../Tenancy/TenancyService';
@Service()
export class GetCreditNoteState {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the create/edit initial state of the payment received.
* @param {Number} saleInvoiceId -
* @return {Promise<ISaleInvoice>}
*/
public async getCreditNoteState(tenantId: number): Promise<ICreditNoteState> {
const { PdfTemplate } = this.tenancy.models(tenantId);
const defaultPdfTemplate = await PdfTemplate.query()
.findOne({ resource: 'CreditNote' })
.modify('default');
return {
defaultTemplateId: defaultPdfTemplate?.id,
};
}
}

View File

@@ -68,30 +68,25 @@ export const DEFAULT_VIEWS = [
];
export const defaultCreditNoteBrandingAttributes = {
// # Colors
primaryColor: '',
secondaryColor: '',
// # Company logo
showCompanyLogo: true,
companyLogo: '',
companyLogoKey: '',
companyLogoUri: '',
// # Company name
companyName: 'Bigcapital Technology, Inc.',
// Address
billedToAddress: [
'Bigcapital Technology, Inc.',
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
billedFromAddress: [
'131 Continental Dr Suite 305 Newark,',
'Delaware 19713',
'United States',
'+1 762-339-5634',
'ahmed@bigcapital.app',
],
showBilledToAddress: true,
showBilledFromAddress: true,
// # Customer address
showCustomerAddress: true,
customerAddress: '',
// # Company address
showCompanyAddress: true,
companyAddress: '',
billedToLabel: 'Billed To',
// Total

View File

@@ -1,4 +1,5 @@
import { CreditNotePdfTemplateAttributes, ICreditNote } from '@/interfaces';
import { contactAddressTextFormat } from '@/utils/address-text-format';
export const transformCreditNoteToPdfTemplate = (
creditNote: ICreditNote
@@ -19,5 +20,6 @@ export const transformCreditNoteToPdfTemplate = (
})),
customerNote: creditNote.note,
termsConditions: creditNote.termsConditions,
customerAddress: contactAddressTextFormat(creditNote.customer),
};
};

View File

@@ -11,6 +11,7 @@ import {
ACCOUNT_CREATED,
ACCOUNT_EDITED,
ACCOUNT_DELETED,
ACCOUNT_VIEWED,
} from '@/constants/event-tracker';
@Service()
@@ -31,6 +32,7 @@ export class AccountEventsTracker extends EventSubscriber {
events.accounts.onDeleted,
this.handleTrackDeletedAccountEvent
);
bus.subscribe(events.accounts.onViewed, this.handleTrackAccountViewedEvent);
}
private handleTrackAccountCreatedEvent = ({
@@ -62,4 +64,12 @@ export class AccountEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleTrackAccountViewedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_VIEWED,
properties: {},
});
};
}

View File

@@ -11,6 +11,7 @@ import {
ITEM_EVENT_CREATED,
ITEM_EVENT_EDITED,
ITEM_EVENT_DELETED,
ITEM_EVENT_VIEWED,
} from '@/constants/event-tracker';
@Service()
@@ -25,6 +26,7 @@ export class ItemEventsTracker extends EventSubscriber {
bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent);
bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent);
bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent);
bus.subscribe(events.item.onViewed, this.handleTrackViewedItemEvent);
}
private handleTrackItemCreatedEvent = ({
@@ -56,4 +58,14 @@ export class ItemEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleTrackViewedItemEvent = ({
tenantId,
}: IItemEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_VIEWED,
properties: {},
});
};
}

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { PosthogService } from '../PostHog';
import { INVOICE_PAYMENT_LINK_GENERATED } from '@/constants/event-tracker';
import events from '@/subscribers/events';
@Service()
export class PaymentLinkEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onPublicLinkGenerated,
this.handleTrackInvoicePublicLinkGeneratedEvent
);
}
public handleTrackInvoicePublicLinkGeneratedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: INVOICE_PAYMENT_LINK_GENERATED,
properties: {},
});
};
}

View File

@@ -0,0 +1,44 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
PAYMENT_METHOD_EDITED,
PAYMENT_METHOD_DELETED,
} from '@/constants/event-tracker';
@Service()
export class PaymentMethodEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.paymentMethod.onEdited,
this.handleTrackPaymentMethodEditedEvent
);
bus.subscribe(
events.paymentMethod.onDeleted,
this.handleTrackPaymentMethodDeletedEvent
);
}
private handleTrackPaymentMethodEditedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_METHOD_EDITED,
properties: {},
});
};
private handleTrackPaymentMethodDeletedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_METHOD_DELETED,
properties: {},
});
};
}

View File

@@ -11,6 +11,7 @@ import {
PAYMENT_RECEIVED_CREATED,
PAYMENT_RECEIVED_EDITED,
PAYMENT_RECEIVED_DELETED,
PAYMENT_RECEIVED_PDF_VIEWED,
} from '@/constants/event-tracker';
@Service()
@@ -34,6 +35,10 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
events.paymentReceive.onDeleted,
this.handleTrackDeletedPaymentReceivedEvent
);
bus.subscribe(
events.paymentReceive.onPdfViewed,
this.handleTrackPdfViewedPaymentReceivedEvent
);
}
private handleTrackPaymentReceivedCreatedEvent = ({
@@ -65,4 +70,14 @@ export class PaymentReceivedEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleTrackPdfViewedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_PDF_VIEWED,
properties: {},
});
};
}

View File

@@ -0,0 +1,67 @@
import { Inject, Service } from 'typedi';
import {
PDF_TEMPLATE_CREATED,
PDF_TEMPLATE_EDITED,
PDF_TEMPLATE_DELETED,
PDF_TEMPLATE_ASSIGNED_DEFAULT,
} from '@/constants/event-tracker';
import { PosthogService } from '../PostHog';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class PdfTemplateEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
public attach(bus) {
bus.subscribe(
events.pdfTemplate.onCreated,
this.handleTrackPdfTemplateCreatedEvent
);
bus.subscribe(
events.pdfTemplate.onEdited,
this.handleTrackEditedPdfTemplateEvent
);
bus.subscribe(
events.pdfTemplate.onDeleted,
this.handleTrackDeletedPdfTemplateEvent
);
bus.subscribe(
events.pdfTemplate.onAssignedDefault,
this.handleTrackAssignedAsDefaultPdfTemplateEvent
);
}
private handleTrackPdfTemplateCreatedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PDF_TEMPLATE_CREATED,
properties: {},
});
};
private handleTrackEditedPdfTemplateEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PDF_TEMPLATE_EDITED,
properties: {},
});
};
private handleTrackDeletedPdfTemplateEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PDF_TEMPLATE_DELETED,
properties: {},
});
};
private handleTrackAssignedAsDefaultPdfTemplateEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PDF_TEMPLATE_ASSIGNED_DEFAULT,
properties: {},
});
};
}

View File

@@ -11,6 +11,7 @@ import {
SALE_ESTIMATE_CREATED,
SALE_ESTIMATE_EDITED,
SALE_ESTIMATE_DELETED,
SALE_ESTIMATE_PDF_VIEWED,
} from '@/constants/event-tracker';
@Service()
@@ -34,6 +35,10 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
events.saleEstimate.onDeleted,
this.handleTrackDeletedEstimateEvent
);
bus.subscribe(
events.saleEstimate.onPdfViewed,
this.handleTrackPdfViewedEstimateEvent
);
}
private handleTrackEstimateCreatedEvent = ({
@@ -65,4 +70,14 @@ export class SaleEstimateEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleTrackPdfViewedEstimateEvent = ({
tenantId,
}: ISaleEstimateDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_PDF_VIEWED,
properties: {},
});
};
}

View File

@@ -10,6 +10,9 @@ import {
SALE_INVOICE_CREATED,
SALE_INVOICE_DELETED,
SALE_INVOICE_EDITED,
SALE_INVOICE_MAIL_SENT,
SALE_INVOICE_PDF_VIEWED,
SALE_INVOICE_VIEWED,
} from '@/constants/event-tracker';
@Service()
@@ -33,6 +36,18 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
events.saleInvoice.onDeleted,
this.handleTrackDeletedInvoiceEvent
);
bus.subscribe(
events.saleInvoice.onViewed,
this.handleTrackViewedInvoiceEvent
);
bus.subscribe(
events.saleInvoice.onPdfViewed,
this.handleTrackPdfViewedInvoiceEvent
);
bus.subscribe(
events.saleInvoice.onMailSent,
this.handleTrackMailSentInvoiceEvent
);
}
private handleTrackInvoiceCreatedEvent = ({
@@ -64,4 +79,28 @@ export class SaleInvoiceEventsTracker extends EventSubscriber {
properties: {},
});
};
private handleTrackViewedInvoiceEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_VIEWED,
properties: {},
});
};
private handleTrackPdfViewedInvoiceEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_PDF_VIEWED,
properties: {},
});
};
private handleTrackMailSentInvoiceEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_MAIL_SENT,
properties: {},
});
};
}

View File

@@ -0,0 +1,32 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { ISaleInvoiceCreatedPayload } from '@/interfaces';
import { PosthogService } from '../PostHog';
import { STRIPE_INTEGRAION_CONNECTED } from '@/constants/event-tracker';
import events from '@/subscribers/events';
@Service()
export class StripeIntegrationEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.stripeIntegration.onOAuthCodeGranted,
this.handleTrackOAuthCodeGrantedTrackEvent
);
}
private handleTrackOAuthCodeGrantedTrackEvent = ({
tenantId,
}: ISaleInvoiceCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: STRIPE_INTEGRAION_CONNECTED,
properties: {},
});
};
}

View File

@@ -12,6 +12,10 @@ import { CustomerEventsTracker } from './CustomerEventsTracker';
import { VendorEventsTracker } from './VendorEventsTracker';
import { ManualJournalEventsTracker } from './ManualJournalEventsTracker';
import { BankRuleEventsTracker } from './BankRuleEventsTracker';
import { PdfTemplateEventsTracker } from './PdfTemplateEventsTracker';
import { PaymentMethodEventsTracker } from './PaymentMethodEventsTracker';
import { PaymentLinkEventsTracker } from './PaymentLinkEventsTracker';
import { StripeIntegrationEventsTracker } from './StripeIntegrationEventsTracker';
export const EventsTrackerListeners = [
SaleInvoiceEventsTracker,
@@ -28,4 +32,8 @@ export const EventsTrackerListeners = [
VendorEventsTracker,
ManualJournalEventsTracker,
BankRuleEventsTracker,
PdfTemplateEventsTracker,
PaymentMethodEventsTracker,
PaymentLinkEventsTracker,
StripeIntegrationEventsTracker,
];

View File

@@ -299,7 +299,7 @@ export const valueParser =
// Parses the enumeration value.
} else if (field.fieldType === 'enumeration') {
const option = get(field, 'options', []).find(
(option) => option.label === value
(option) => option.label?.toLowerCase() === value?.toLowerCase()
);
_value = get(option, 'key');
// Parses the numeric value.
@@ -433,8 +433,8 @@ export const getMapToPath = (to: string, group = '') =>
group ? `${group}.${to}` : to;
export const getImportsStoragePath = () => {
return path.join(global.__storage_dir, `/imports`);
}
return path.join(global.__storage_dir, `/imports`);
};
/**
* Deletes the imported file from the storage and database.

View File

@@ -3,6 +3,8 @@ import { IItem } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import ItemTransformer from './ItemTransformer';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Inject()
export class GetItem {
@@ -12,6 +14,9 @@ export class GetItem {
@Inject()
private transformer: TransformerInjectable;
@Inject()
private eventPublisher: EventPublisher;
/**
* Retrieve the item details of the given id with associated details.
* @param {number} tenantId
@@ -31,6 +36,16 @@ export class GetItem {
.withGraphFetched('purchaseTaxRate')
.throwIfNotFound();
return this.transformer.transform(tenantId, item, new ItemTransformer());
const transformed = await this.transformer.transform(
tenantId,
item,
new ItemTransformer()
);
const eventPayload = { tenantId, itemId };
// Triggers the `onItemViewed` event.
await this.eventPublisher.emitAsync(events.item.onViewed, eventPayload);
return transformed;
}
}

View File

@@ -4,6 +4,7 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
import { MailTenancy } from '@/services/MailTenancy/MailTenancy';
import { formatSmsMessage } from '@/utils';
import { Tenant } from '@/system/models';
import { castArray } from 'lodash';
@Service()
export class ContactMailNotification {
@@ -14,76 +15,54 @@ export class ContactMailNotification {
private tenancy: HasTenancyService;
/**
* Parses the default message options.
* @param {number} tenantId -
* @param {number} invoiceId -
* @param {string} subject -
* @param {string} body -
* @returns {Promise<SaleInvoiceMailOptions>}
* Gets the default mail address of the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Contact id.
* @returns {Promise<Pick<CommonMailOptions, 'to' | 'from'>>}
*/
public async getDefaultMailOptions(
tenantId: number,
contactId: number,
subject: string = '',
body: string = ''
): Promise<CommonMailOptions> {
customerId: number
): Promise<Pick<CommonMailOptions, 'to' | 'from'>> {
const { Customer } = this.tenancy.models(tenantId);
const contact = await Customer.query()
.findById(contactId)
const customer = await Customer.query()
.findById(customerId)
.throwIfNotFound();
const toAddresses = contact.contactAddresses;
const toAddresses = customer.contactAddresses;
const fromAddresses = await this.mailTenancy.senders(tenantId);
const toAddress = toAddresses.find((a) => a.primary);
const fromAddress = fromAddresses.find((a) => a.primary);
const to = toAddress?.mail || '';
const from = fromAddress?.mail || '';
const to = toAddress?.mail ? castArray(toAddress?.mail) : [];
const from = fromAddress?.mail ? castArray(fromAddress?.mail) : [];
return {
subject,
body,
to,
from,
fromAddresses,
toAddresses,
};
return { to, from };
}
/**
* Retrieves the mail options of the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Invoice id.
* @param {string} defaultSubject - Default subject text.
* @param {string} defaultBody - Default body text.
* @returns {Promise<CommonMailOptions>}
*/
public async getMailOptions(
public async parseMailOptions(
tenantId: number,
contactId: number,
defaultSubject?: string,
defaultBody?: string,
formatterData?: Record<string, any>
mailOptions: CommonMailOptions,
formatterArgs?: Record<string, any>
): Promise<CommonMailOptions> {
const mailOpts = await this.getDefaultMailOptions(
tenantId,
contactId,
defaultSubject,
defaultBody
);
const commonFormatArgs = await this.getCommonFormatArgs(tenantId);
const formatArgs = {
...commonFormatArgs,
...formatterData,
...formatterArgs,
};
const subject = formatSmsMessage(mailOpts.subject, formatArgs);
const body = formatSmsMessage(mailOpts.body, formatArgs);
const subjectFormatted = formatSmsMessage(mailOptions?.subject, formatArgs);
const messageFormatted = formatSmsMessage(mailOptions?.message, formatArgs);
return {
...mailOpts,
subject,
body,
...mailOptions,
subject: subjectFormatted,
message: messageFormatted,
};
}
@@ -100,7 +79,7 @@ export class ContactMailNotification {
.withGraphFetched('metadata');
return {
CompanyName: organization.metadata.name,
['Company Name']: organization.metadata.name,
};
}
}

View File

@@ -1,33 +1,46 @@
import { isEmpty } from 'lodash';
import { castArray, isEmpty } from 'lodash';
import { ServiceError } from '@/exceptions';
import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces';
import { CommonMailOptions } from '@/interfaces';
import { ERRORS } from './constants';
/**
* Merges the mail options with incoming options.
* @param {Partial<SaleInvoiceMailOptions>} mailOptions
* @param {Partial<SendInvoiceMailDTO>} overridedOptions
* @throws {ServiceError}
*/
export function parseAndValidateMailOptions(
mailOptions: Partial<CommonMailOptions>,
overridedOptions: Partial<CommonMailOptionsDTO>
) {
export function parseMailOptions(
mailOptions: CommonMailOptions,
overridedOptions: Partial<CommonMailOptions>
): CommonMailOptions {
const mergedMessageOptions = {
...mailOptions,
...overridedOptions,
};
if (isEmpty(mergedMessageOptions.from)) {
const parsedMessageOptions = {
...mergedMessageOptions,
from: mergedMessageOptions?.from
? castArray(mergedMessageOptions?.from)
: [],
to: mergedMessageOptions?.to ? castArray(mergedMessageOptions?.to) : [],
cc: mergedMessageOptions?.cc ? castArray(mergedMessageOptions?.cc) : [],
bcc: mergedMessageOptions?.bcc ? castArray(mergedMessageOptions?.bcc) : [],
};
return parsedMessageOptions;
}
export function validateRequiredMailOptions(
mailOptions: Partial<CommonMailOptions>
) {
if (isEmpty(mailOptions.from)) {
throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.to)) {
if (isEmpty(mailOptions.to)) {
throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.subject)) {
if (isEmpty(mailOptions.subject)) {
throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.body)) {
if (isEmpty(mailOptions.message)) {
throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND);
}
return mergedMessageOptions;
}

View File

@@ -93,7 +93,7 @@ export default class OrganizationService {
// Triggers the organization built event.
await this.eventPublisher.emitAsync(events.organization.built, {
tenantId: tenant.id,
} as IOrganizationBuiltEventPayload)
} as IOrganizationBuiltEventPayload);
}
/**
@@ -190,11 +190,13 @@ export default class OrganizationService {
this.throwIfTenantNotExists(tenant);
// Validate organization transactions before mutate base currency.
await this.validateMutateBaseCurrency(
tenant,
organizationDTO.baseCurrency,
tenant.metadata?.baseCurrency
);
if (organizationDTO.baseCurrency) {
await this.validateMutateBaseCurrency(
tenant,
organizationDTO.baseCurrency,
tenant.metadata?.baseCurrency
);
}
await tenant.saveMetadata(organizationDTO);
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {

View File

@@ -13,16 +13,13 @@ import TenantsManagerService from '@/services/Tenancy/TenantsManager';
@Service()
export default class OrganizationUpgrade {
@Inject()
tenancy: HasTenancyService;
private organizationService: OrganizationService;
@Inject()
organizationService: OrganizationService;
@Inject()
tenantsManager: TenantsManagerService;
private tenantsManager: TenantsManagerService;
@Inject('agenda')
agenda: any;
private agenda: any;
/**
* Upgrades the given organization database.
@@ -102,4 +99,4 @@ export default class OrganizationUpgrade {
throw new ServiceError(ERRORS.TENANT_UPGRADE_IS_RUNNING);
}
}
}
}

View File

@@ -0,0 +1,102 @@
import { Inject, Service } from 'typedi';
import { StripePaymentService } from '../StripePayment/StripePaymentService';
import HasTenancyService from '../Tenancy/TenancyService';
import { ISaleInvoice } from '@/interfaces';
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
import { PaymentLink } from '@/system/models';
import { initializeTenantSettings } from '@/api/middleware/SettingsMiddleware';
import config from '@/config';
const origin = 'http://localhost';
@Service()
export class CreateInvoiceCheckoutSession {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private stripePaymentService: StripePaymentService;
/**
* Creates a new Stripe checkout session from the given sale invoice.
* @param {number} tenantId
* @param {number} saleInvoiceId - Sale invoice id.
* @returns {Promise<StripeInvoiceCheckoutSessionPOJO>}
*/
async createInvoiceCheckoutSession(
publicPaymentLinkId: string
): Promise<StripeInvoiceCheckoutSessionPOJO> {
// Retrieves the payment link from the given id.
const paymentLink = await PaymentLink.query()
.findOne('linkId', publicPaymentLinkId)
.where('resourceType', 'SaleInvoice')
.throwIfNotFound();
const tenantId = paymentLink.tenantId;
await initializeTenantSettings(tenantId);
const { SaleInvoice } = this.tenancy.models(tenantId);
// Retrieves the invoice from associated payment link.
const invoice = await SaleInvoice.query()
.findById(paymentLink.resourceId)
.withGraphFetched('paymentMethods')
.throwIfNotFound();
// It will be only one Stripe payment method associated to the invoice.
const stripePaymentMethod = invoice.paymentMethods?.find(
(method) => method.paymentIntegration?.service === 'Stripe'
);
const stripeAccountId = stripePaymentMethod?.paymentIntegration?.accountId;
const paymentIntegrationId = stripePaymentMethod?.paymentIntegration?.id;
// Creates checkout session for the given invoice.
const session = await this.createCheckoutSession(invoice, stripeAccountId, {
tenantId,
paymentLinkId: paymentLink.id,
});
return {
sessionId: session.id,
publishableKey: config.stripePayment.publishableKey,
redirectTo: session.url,
};
}
/**
* Creates a new Stripe checkout session for the given sale invoice.
* @param {ISaleInvoice} invoice - The sale invoice for which the checkout session is created.
* @param {string} stripeAccountId - The Stripe account ID associated with the payment method.
* @returns {Promise<any>} - The created Stripe checkout session.
*/
private createCheckoutSession(
invoice: ISaleInvoice,
stripeAccountId: string,
metadata?: Record<string, any>
) {
return this.stripePaymentService.stripe.checkout.sessions.create(
{
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: invoice.currencyCode,
product_data: {
name: invoice.invoiceNo,
},
unit_amount: invoice.total * 100, // Amount in cents
},
quantity: 1,
},
],
mode: 'payment',
success_url: `${origin}/success`,
cancel_url: `${origin}/cancel`,
metadata: {
saleInvoiceId: invoice.id,
resource: 'SaleInvoice',
...metadata,
},
},
{ stripeAccount: stripeAccountId }
);
}
}

View File

@@ -0,0 +1,57 @@
import moment from 'moment';
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { PaymentLink } from '@/system/models';
import { GetInvoicePaymentLinkMetaTransformer } from '../Sales/Invoices/GetInvoicePaymentLinkTransformer';
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
@Service()
export class GetInvoicePaymentLinkMetadata {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieves the invoice sharable link meta of the link id.
* @param {number}
* @param {string} linkId
*/
async getInvoicePaymentLinkMeta(linkId: string) {
const paymentLink = await PaymentLink.query()
.findOne('linkId', linkId)
.where('resourceType', 'SaleInvoice')
.throwIfNotFound();
// Validate the expiry at date.
if (paymentLink.expiryAt) {
const currentDate = moment();
const expiryDate = moment(paymentLink.expiryAt);
if (expiryDate.isBefore(currentDate)) {
throw new ServiceError('PAYMENT_LINK_EXPIRED');
}
}
const tenantId = paymentLink.tenantId;
await initalizeTenantServices(tenantId);
const { SaleInvoice } = this.tenancy.models(tenantId);
const invoice = await SaleInvoice.query()
.findById(paymentLink.resourceId)
.withGraphFetched('entries.item')
.withGraphFetched('customer')
.withGraphFetched('taxes.taxRate')
.withGraphFetched('paymentMethods.paymentIntegration')
.throwIfNotFound();
return this.transformer.transform(
tenantId,
invoice,
new GetInvoicePaymentLinkMetaTransformer()
);
}
}

View File

@@ -0,0 +1,33 @@
import { Inject, Service } from 'typedi';
import { initalizeTenantServices } from '@/api/middleware/TenantDependencyInjection';
import { SaleInvoicePdf } from '../Sales/Invoices/SaleInvoicePdf';
import { PaymentLink } from '@/system/models';
@Service()
export class GetPaymentLinkInvoicePdf {
@Inject()
private getSaleInvoicePdfService: SaleInvoicePdf;
/**
* Retrieves the sale invoice PDF of the given payment link id.
* @param {number} tenantId
* @param {number} paymentLinkId
* @returns {Promise<Buffer>}
*/
async getPaymentLinkInvoicePdf(paymentLinkId: string): Promise<Buffer> {
const paymentLink = await PaymentLink.query()
.findOne('linkId', paymentLinkId)
.where('resourceType', 'SaleInvoice')
.throwIfNotFound();
const tenantId = paymentLink.tenantId;
await initalizeTenantServices(tenantId);
const saleInvoiceId = paymentLink.resourceId;
return this.getSaleInvoicePdfService.saleInvoicePdf(
tenantId,
saleInvoiceId
);
}
}

View File

@@ -0,0 +1,53 @@
import { Inject, Service } from 'typedi';
import { GetInvoicePaymentLinkMetadata } from './GetInvoicePaymentLinkMetadata';
import { CreateInvoiceCheckoutSession } from './CreateInvoiceCheckoutSession';
import { StripeInvoiceCheckoutSessionPOJO } from '@/interfaces/StripePayment';
import { GetPaymentLinkInvoicePdf } from './GetPaymentLinkInvoicePdf';
@Service()
export class PaymentLinksApplication {
@Inject()
private getInvoicePaymentLinkMetadataService: GetInvoicePaymentLinkMetadata;
@Inject()
private createInvoiceCheckoutSessionService: CreateInvoiceCheckoutSession;
@Inject()
private getPaymentLinkInvoicePdfService: GetPaymentLinkInvoicePdf;
/**
* Retrieves the invoice payment link.
* @param {string} paymentLinkId
* @returns {}
*/
public getInvoicePaymentLink(paymentLinkId: string) {
return this.getInvoicePaymentLinkMetadataService.getInvoicePaymentLinkMeta(
paymentLinkId
);
}
/**
* Create the invoice payment checkout session from the given payment link id.
* @param {string} paymentLinkId - Payment link id.
* @returns {Promise<StripeInvoiceCheckoutSessionPOJO>}
*/
public createInvoicePaymentCheckoutSession(
paymentLinkId: string
): Promise<StripeInvoiceCheckoutSessionPOJO> {
return this.createInvoiceCheckoutSessionService.createInvoiceCheckoutSession(
paymentLinkId
);
}
/**
* Retrieves the sale invoice pdf of the given payment link id.
* @param {number} tenantId
* @param {number} paymentLinkId
* @returns {Promise<Buffer> }
*/
public getPaymentLinkInvoicePdf(paymentLinkId: string): Promise<Buffer> {
return this.getPaymentLinkInvoicePdfService.getPaymentLinkInvoicePdf(
paymentLinkId
);
}
}

View File

@@ -0,0 +1,54 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class DeletePaymentMethodService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Deletes the given payment integration.
* @param {number} tenantId
* @param {number} paymentIntegrationId
* @returns {Promise<void>}
*/
public async deletePaymentMethod(
tenantId: number,
paymentIntegrationId: number
): Promise<void> {
const { PaymentIntegration, TransactionPaymentServiceEntry } =
this.tenancy.models(tenantId);
const paymentIntegration = await PaymentIntegration.query()
.findById(paymentIntegrationId)
.throwIfNotFound();
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Delete payment methods links.
await TransactionPaymentServiceEntry.query(trx)
.where('paymentIntegrationId', paymentIntegrationId)
.delete();
// Delete the payment integration.
await PaymentIntegration.query(trx)
.findById(paymentIntegrationId)
.delete();
// Triggers `onPaymentMethodDeleted` event.
await this.eventPublisher.emitAsync(events.paymentMethod.onDeleted, {
tenantId,
paymentIntegrationId,
});
});
}
}

View File

@@ -0,0 +1,60 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import UnitOfWork from '../UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { EditPaymentMethodDTO } from './types';
import events from '@/subscribers/events';
@Service()
export class EditPaymentMethodService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private uow: UnitOfWork;
@Inject()
private eventPublisher: EventPublisher;
/**
* Edits the given payment method.
* @param {number} tenantId
* @param {number} paymentIntegrationId
* @param {EditPaymentMethodDTO} editPaymentMethodDTO
* @returns {Promise<void>}
*/
async editPaymentMethod(
tenantId: number,
paymentIntegrationId: number,
editPaymentMethodDTO: EditPaymentMethodDTO
): Promise<void> {
const { PaymentIntegration } = this.tenancy.models(tenantId);
const paymentMethod = await PaymentIntegration.query()
.findById(paymentIntegrationId)
.throwIfNotFound();
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPaymentMethodEditing` event.
await this.eventPublisher.emitAsync(events.paymentMethod.onEditing, {
tenantId,
paymentIntegrationId,
editPaymentMethodDTO,
trx,
});
await PaymentIntegration.query(trx)
.findById(paymentIntegrationId)
.patch({
...editPaymentMethodDTO,
});
// Triggers `onPaymentMethodEdited` event.
await this.eventPublisher.emitAsync(events.paymentMethod.onEdited, {
tenantId,
paymentIntegrationId,
editPaymentMethodDTO,
trx,
});
});
}
}

View File

@@ -0,0 +1,62 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { GetPaymentMethodsPOJO } from './types';
import config from '@/config';
import { isStripePaymentConfigured } from './utils';
import { GetStripeAuthorizationLinkService } from '../StripePayment/GetStripeAuthorizationLink';
@Service()
export class GetPaymentMethodsStateService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private getStripeAuthorizationLinkService: GetStripeAuthorizationLinkService;
/**
* Retrieves the payment state provising state.
* @param {number} tenantId
* @returns {Promise<GetPaymentMethodsPOJO>}
*/
public async getPaymentMethodsState(
tenantId: number
): Promise<GetPaymentMethodsPOJO> {
const { PaymentIntegration } = this.tenancy.models(tenantId);
const stripePayment = await PaymentIntegration.query()
.orderBy('createdAt', 'ASC')
.findOne({
service: 'Stripe',
});
const isStripeAccountCreated = !!stripePayment;
const isStripePaymentEnabled = stripePayment?.paymentEnabled;
const isStripePayoutEnabled = stripePayment?.payoutEnabled;
const isStripeEnabled = stripePayment?.fullEnabled;
const stripePaymentMethodId = stripePayment?.id || null;
const stripeAccountId = stripePayment?.accountId || null;
const stripePublishableKey = config.stripePayment.publishableKey;
const stripeCurrencies = ['USD', 'EUR'];
const stripeRedirectUrl = 'https://your-stripe-redirect-url.com';
const isStripeServerConfigured = isStripePaymentConfigured();
const stripeAuthLink =
this.getStripeAuthorizationLinkService.getStripeAuthLink();
const paymentMethodPOJO: GetPaymentMethodsPOJO = {
stripe: {
isStripeAccountCreated,
isStripePaymentEnabled,
isStripePayoutEnabled,
isStripeEnabled,
isStripeServerConfigured,
stripeAccountId,
stripePaymentMethodId,
stripePublishableKey,
stripeCurrencies,
stripeAuthLink,
stripeRedirectUrl,
},
};
return paymentMethodPOJO;
}
}

View File

@@ -0,0 +1,27 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { GetPaymentMethodsPOJO } from './types';
@Service()
export class GetPaymentMethodService {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the payment state provising state.
* @param {number} tenantId
* @returns {Promise<GetPaymentMethodsPOJO>}
*/
public async getPaymentMethod(
tenantId: number,
paymentServiceId: number
): Promise<GetPaymentMethodsPOJO> {
const { PaymentIntegration } = this.tenancy.models(tenantId);
const stripePayment = await PaymentIntegration.query()
.findById(paymentServiceId)
.throwIfNotFound();
return stripePayment;
}
}

View File

@@ -0,0 +1,33 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { GetPaymentServicesSpecificInvoiceTransformer } from './GetPaymentServicesSpecificInvoiceTransformer';
@Service()
export class GetPaymentServicesSpecificInvoice {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transform: TransformerInjectable;
/**
* Retrieves the payment services of the given invoice.
* @param {number} tenantId
* @param {number} invoiceId
* @returns
*/
async getPaymentServicesInvoice(tenantId: number) {
const { PaymentIntegration } = this.tenancy.models(tenantId);
const paymentGateways = await PaymentIntegration.query()
.modify('fullEnabled')
.orderBy('name', 'ASC');
return this.transform.transform(
tenantId,
paymentGateways,
new GetPaymentServicesSpecificInvoiceTransformer()
);
}
}

View File

@@ -0,0 +1,19 @@
import { Transformer } from '@/lib/Transformer/Transformer';
export class GetPaymentServicesSpecificInvoiceTransformer extends Transformer {
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['accountId'];
};
public includeAttributes = (): string[] => {
return ['serviceFormatted'];
};
public serviceFormatted(method) {
return 'Stripe';
}
}

View File

@@ -0,0 +1,95 @@
import { Service, Inject } from 'typedi';
import { GetPaymentServicesSpecificInvoice } from './GetPaymentServicesSpecificInvoice';
import { DeletePaymentMethodService } from './DeletePaymentMethodService';
import { EditPaymentMethodService } from './EditPaymentMethodService';
import { EditPaymentMethodDTO, GetPaymentMethodsPOJO } from './types';
import { GetPaymentMethodsStateService } from './GetPaymentMethodsState';
import { GetPaymentMethodService } from './GetPaymentService';
@Service()
export class PaymentServicesApplication {
@Inject()
private getPaymentServicesSpecificInvoice: GetPaymentServicesSpecificInvoice;
@Inject()
private deletePaymentMethodService: DeletePaymentMethodService;
@Inject()
private editPaymentMethodService: EditPaymentMethodService;
@Inject()
private getPaymentMethodsStateService: GetPaymentMethodsStateService;
@Inject()
private getPaymentMethodService: GetPaymentMethodService;
/**
* Retrieves the payment services for a specific invoice.
* @param {number} tenantId - The ID of the tenant.
* @param {number} invoiceId - The ID of the invoice.
* @returns {Promise<any>} The payment services for the specified invoice.
*/
public async getPaymentServicesForInvoice(tenantId: number): Promise<any> {
return this.getPaymentServicesSpecificInvoice.getPaymentServicesInvoice(
tenantId
);
}
/**
* Retrieves specific payment service details.
* @param {number} tenantId - Tennat id.
* @param {number} paymentServiceId - Payment service id.
*/
public async getPaymentService(tenantId: number, paymentServiceId: number) {
return this.getPaymentMethodService.getPaymentMethod(
tenantId,
paymentServiceId
);
}
/**
* Deletes the given payment method.
* @param {number} tenantId
* @param {number} paymentIntegrationId
* @returns {Promise<void>}
*/
public async deletePaymentMethod(
tenantId: number,
paymentIntegrationId: number
): Promise<void> {
return this.deletePaymentMethodService.deletePaymentMethod(
tenantId,
paymentIntegrationId
);
}
/**
* Edits the given payment method.
* @param {number} tenantId
* @param {number} paymentIntegrationId
* @param {EditPaymentMethodDTO} editPaymentMethodDTO
* @returns {Promise<void>}
*/
public async editPaymentMethod(
tenantId: number,
paymentIntegrationId: number,
editPaymentMethodDTO: EditPaymentMethodDTO
): Promise<void> {
return this.editPaymentMethodService.editPaymentMethod(
tenantId,
paymentIntegrationId,
editPaymentMethodDTO
);
}
/**
* Retrieves the payment state providing state.
* @param {number} tenantId
* @returns {Promise<GetPaymentMethodsPOJO>}
*/
public async getPaymentMethodsState(
tenantId: number
): Promise<GetPaymentMethodsPOJO> {
return this.getPaymentMethodsStateService.getPaymentMethodsState(tenantId);
}
}

View File

@@ -0,0 +1,33 @@
export interface EditPaymentMethodDTO {
name?: string;
options?: {
bankAccountId?: number; // bank account.
clearningAccountId?: number; // current liability.
showVisa?: boolean;
showMasterCard?: boolean;
showDiscover?: boolean;
showAmer?: boolean;
showJcb?: boolean;
showDiners?: boolean;
};
}
export interface GetPaymentMethodsPOJO {
stripe: {
isStripeAccountCreated: boolean;
isStripePaymentEnabled: boolean;
isStripePayoutEnabled: boolean;
isStripeEnabled: boolean;
isStripeServerConfigured: boolean;
stripeAccountId: string | null;
stripePaymentMethodId: number | null;
stripePublishableKey: string | null;
stripeAuthLink: string;
stripeCurrencies: Array<string>;
stripeRedirectUrl: string | null;
};
}

View File

@@ -0,0 +1,9 @@
import config from '@/config';
export const isStripePaymentConfigured = () => {
return (
config.stripePayment.secretKey &&
config.stripePayment.publishableKey &&
config.stripePayment.webhooksSecret
);
};

View File

@@ -1,7 +1,6 @@
import * as R from 'ramda';
import HasTenancyService from '../Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash';
import { isNil } from 'lodash';
import HasTenancyService from '../Tenancy/TenancyService';
@Service()
export class BrandingTemplateDTOTransformer {
@@ -22,11 +21,12 @@ export class BrandingTemplateDTOTransformer {
const { PdfTemplate } = this.tenancy.models(tenantId);
const attributeName = 'pdfTemplateId';
const defaultTemplate = await PdfTemplate.query().findOne({
resource,
default: true,
});
if (!defaultTemplate || !isEmpty(object[attributeName])) {
const defaultTemplate = await PdfTemplate.query()
.modify('default')
.findOne({ resource });
// If the default template is not found OR the given object has no defined template id.
if (!defaultTemplate || !isNil(object[attributeName])) {
return object;
}
return {

View File

@@ -0,0 +1,31 @@
import { Service } from 'typedi';
import { TenantMetadata } from '@/system/models';
import { CommonOrganizationBrandingAttributes } from './types';
@Service()
export class GetOrganizationBrandingAttributes {
/**
* Retrieves the given organization branding attributes initial state.
* @param {number} tenantId
* @returns {Promise<CommonOrganizationBrandingAttributes>}
*/
async getOrganizationBrandingAttributes(
tenantId: number
): Promise<CommonOrganizationBrandingAttributes> {
const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
const companyName = tenantMetadata?.name;
const primaryColor = tenantMetadata?.primaryColor;
const companyLogoKey = tenantMetadata?.logoKey;
const companyLogoUri = tenantMetadata?.logoUri;
const companyAddress = tenantMetadata?.addressTextFormatted;
return {
companyName,
companyAddress,
companyLogoUri,
companyLogoKey,
primaryColor,
};
}
}

View File

@@ -0,0 +1,15 @@
import { Inject, Service } from 'typedi';
import { GetOrganizationBrandingAttributes } from './GetOrganizationBrandingAttributes';
@Service()
export class GetPdfTemplateBrandingState {
@Inject()
private getOrgBrandingAttributes: GetOrganizationBrandingAttributes;
getBrandingState(tenantId: number) {
const brandingAttributes =
this.getOrgBrandingAttributes.getOrganizationBrandingAttributes(tenantId);
return brandingAttributes;
}
}

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