Compare commits

...

104 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
17deeb18e3 Merge pull request #965 from bigcapitalhq/fix/ahmedbouhuolia/cashflow-transaction-type-consistency
fix: correct cash flow transaction type naming inconsistencies
2026-02-16 23:05:22 +02:00
Ahmed Bouhuolia
8416b45f4e fix: correct cash flow transaction type naming inconsistencies
- Fix typo ONWERS_DRAWING -> OWNERS_DRAWING in server constants
- Change OwnerDrawing -> owner_drawing for consistency in webapp
- Fix typo TRANSACRIONS_TYPE -> TRANSACTIONS_TYPE
- Fix typo OnwersDrawing -> OwnerDrawing
- Add missing Icon and FDateInput imports
- Add dark mode styling for BranchRowDivider

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-02-16 23:02:38 +02:00
Ahmed Bouhuolia
3cc5aab80e Merge pull request #963 from bigcapitalhq/fix/ahmedbouhuolia/mail-queue-cleanup
fix: correct queue name, add missing await, and clean up constants
2026-02-16 22:27:08 +02:00
Ahmed Bouhuolia
93711a7bf4 fix: correct queue name, add missing await, and clean up constants 2026-02-16 22:23:41 +02:00
Ahmed Bouhuolia
43066c4f1f Merge pull request #962 from bigcapitalhq/feature/ahmedbouhuolia/add-permission-guards-to-credit-controllers
fix(server): add permission guards to credit note and vendor credit controllers
2026-02-16 20:07:36 +02:00
Ahmed Bouhuolia
d5402b6a9b feat: add permission guards to credit note and vendor credit controllers
Add AuthorizationGuard and PermissionGuard to the following controllers:
- CreditNoteRefundsController
- CreditNotesApplyInvoiceController
- VendorCreditApplyBillsController
- VendorCreditsRefundController

Add @RequirePermission decorators with appropriate actions:
- View action for GET endpoints
- Edit action for POST/DELETE endpoints
- Refund action for refund-related operations

Also fixes AuthorizationGuard to use userId from clsService instead of
user.id from request for consistency with the abilities cache.
2026-02-16 20:04:48 +02:00
Ahmed Bouhuolia
174aec78ca fix(webapp): send mail preview style 2026-02-16 17:45:04 +02:00
Ahmed Bouhuolia
7162e948dd Merge pull request #961 from bigcapitalhq/fix/trial-balance-sheet-filtering
fix: correct trial balance sheet filtering logic
2026-02-16 15:34:33 +02:00
Ahmed Bouhuolia
8ad1be1d52 fix: correct trial balance sheet filtering logic
- Include child account transactions when filtering parent accounts
- Fix order of operations in accounts section mapping
- Ensure accounts with child transactions are not incorrectly filtered out

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:27:33 +02:00
Ahmed Bouhuolia
a96ced10db Merge pull request #958 from bigcapitalhq/premissions-guard
fix(server): permissions guard for read and write endpoints
2026-02-15 22:58:01 +02:00
Ahmed Bouhuolia
2d39e38578 fix(server): premissions guard for read and write endpoints 2026-02-15 22:55:10 +02:00
Ahmed Bouhuolia
af80afcf59 Merge pull request #955 from bigcapitalhq/fix/user-invite-email
fix: user invite email not sending and null variables
2026-02-14 00:34:08 +02:00
Ahmed Bouhuolia
66cb0521e5 fix: user invite email not sending and null variables
- Add missing BullModule queue registration and BullBoardModule to UsersModule
- Add invitingUser to event payloads to track who sent the invite
- Fix incorrect global variable in SendInviteUsersMailMessage (__views_dir -> __images_dirname)
- Use invitingUser as fromUser instead of invited user in email
- Update processors to use BullMQ pattern

Fixes issues:
1. Email not sending due to missing queue/processor registration
2. Null variables in email (firstName/lastName) because fromUser was the invited user
3. Image attachment failing due to wrong path
2026-02-14 00:31:28 +02:00
Ahmed Bouhuolia
9204b76346 Merge pull request #950 from bigcapitalhq/fix-tax-rates
fix(server): use `DrawerActionsBar` instead of `DashboardActionsBar` in drawer components
2026-02-12 23:21:28 +02:00
Ahmed Bouhuolia
36cbb1eef5 fix: use DrawerActionsBar instead of DashboardActionsBar in drawer components
Replace DashboardActionsBar with DrawerActionsBar in all drawer action bars
for consistency with the design system:
- ContactDetailActionsBar
- CustomerDetailsActionsBar
- VendorCreditDetailActionsBar
- TaxRateDetailsContentActionsBar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:19:16 +02:00
Ahmed Bouhuolia
441e27581b Merge pull request #949 from bigcapitalhq/fix-tax-rates
fix: tax rates API and UI improvements
2026-02-12 20:08:49 +02:00
Ahmed Bouhuolia
e0d9a56a29 fix: tax rates API and UI improvements
- Add @ToNumber() decorator to rate field for proper validation
- Fix getTaxRates to return { data: taxRates } response
- Fix useTaxRate URL typo and response handling
- Fix activate/inactivate endpoint methods and paths
- Apply TEXT_MUTED class to description and compound tax
- Add dark mode support for rate number display
2026-02-12 20:06:49 +02:00
Ahmed Bouhuolia
5a017104ce Merge pull request #948 from bigcapitalhq/fix/abouolia/rerecognize-transactions-on-rule-edit
fix: paper template scrollable area
2026-02-12 15:02:12 +02:00
Ahmed Bouhuolia
25ca620836 fix: add consistent Box wrapper to paper template forms in customize components 2026-02-12 14:59:55 +02:00
Ahmed Bouhuolia
5a3655e093 Merge pull request #944 from bigcapitalhq/fix/abouolia/rerecognize-transactions-on-rule-edit
fix(server): re-recognize transactions when bank rule is edited
2026-02-11 23:18:18 +02:00
Ahmed Bouhuolia
49c2777587 fix: re-recognize transactions when bank rule is edited (closes #809) 2026-02-11 23:15:20 +02:00
Ahmed Bouhuolia
a5680c08c2 Merge pull request #943 from bigcapitalhq/fix/abouolia/bank-rule-payee-field-validation
fix(server): add missing S3_FORCE_PATH_STYLE environment variable
2026-02-11 19:36:52 +02:00
Ahmed Bouhuolia
d909dad1bf fix: add missing S3_FORCE_PATH_STYLE environment variable
The S3 module was referencing config.forcePathStyle but the value
was never being read from the environment. This adds the missing
forcePathStyle configuration to the S3 config.

Closes #940

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 19:35:21 +02:00
Ahmed Bouhuolia
f32cc752ef Merge pull request #942 from bigcapitalhq/fix/abouolia/bank-rule-payee-field-validation
fix(server): allow 'payee' field in bank rule conditions validation
2026-02-11 19:13:35 +02:00
Ahmed Bouhuolia
a7f98201cc fix: allow 'payee' field in bank rule conditions validation
The BankRuleConditionDto validation only allowed 'description' and 'amount'
fields, but the frontend also sends 'payee' as a valid condition field.
This caused a 400 Bad Request error when creating rules with payee conditions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 19:11:58 +02:00
Ahmed Bouhuolia
a1d0fc3f0a Merge pull request #941 from bigcapitalhq/fix/ahmedbouhuolia/phone-validation-formatted-numbers
fix(webapp): allow formatted phone numbers in customer and vendor forms
2026-02-11 18:39:52 +02:00
Ahmed Bouhuolia
11575cfb96 fix(webapp): allow formatted phone numbers in customer and vendor forms 2026-02-11 18:37:39 +02:00
Ahmed Bouhuolia
a2160c0595 Merge pull request #939 from bigcapitalhq/fix/ahmedbouhuolia/docker-healthcheck-endpoint
fix(server): fix Docker healthcheck endpoint
2026-02-10 00:25:25 +02:00
Ahmed Bouhuolia
956a9b58dd fix(server): register SystemDatabaseController and add PublicRoute decorator
- Register SystemDatabaseController in SystemDatabaseModule to expose /api/system_db endpoint
- Add PublicRoute decorator to bypass authentication for healthcheck endpoint
- Update ping() method to return { status: 'ok' } with HTTP 200

This fixes the Docker healthcheck that was failing with 404 Not Found errors.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 00:22:40 +02:00
Ahmed Bouhuolia
acb701d618 Merge pull request #938 from bigcapitalhq/fix/universal-search-api-endpoints
refactor: update UniversalSearch components with TypeScript and TextStatus
2026-02-09 19:55:37 +02:00
Ahmed Bouhuolia
09ff72d302 fix: add TypeScript types to If component 2026-02-09 19:52:17 +02:00
Ahmed Bouhuolia
7375512fec refactor: update UniversalSearch components with TypeScript and TextStatus 2026-02-09 19:26:26 +02:00
Ahmed Bouhuolia
77e65389a4 Merge pull request #937 from bigcapitalhq/fix/universal-search-api-endpoints
fix: universal search API endpoint errors
2026-02-09 13:42:53 +02:00
Ahmed Bouhuolia
1972861c97 fix(server): add missing searchRoles to Item model
Add searchRoles static property to enable searching items by name and code.
This fixes the 500 Internal Server Error when searching items via
/api/items?search_keyword=...

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:40:34 +02:00
Ahmed Bouhuolia
c47acdee03 fix(webapp): correct API endpoint URLs for universal search
Update resource URL mappings to match backend NestJS controller routes:
- /sales/invoices -> /sale-invoices
- /sales/estimates -> /sale-estimates
- /sales/receipts -> /sale-receipts
- /purchases/bills -> /bills
- /sales/payment_receives -> /payments-received
- /purchases/bill_payments -> /bill-payments
- /sales/credit_notes -> /credit-notes
- /purchases/vendor-credit -> /vendor-credits

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-09 13:40:18 +02:00
Ahmed Bouhuolia
8689962bf3 Merge pull request #935 from bigcapitalhq/feat/abouolia/add-throttle-organization-build-job
feat: expand rate limiting of getting org build job endpoint
2026-02-09 13:23:49 +02:00
Ahmed Bouhuolia
3258159474 feat: add rate limiting to organization build job endpoint
Add @Throttle decorator to GET /build/:buildJobId endpoint to limit
to 300 requests per minute to prevent abuse.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-08 09:39:55 +02:00
Ahmed Bouhuolia
36bfa573ad 🐛 fix(manual-journal): fix race condition in form submission handlers
Fix the order of setSubmitPayload and submitForm calls in all six
button handlers to prevent race condition where submitForm reads
stale state before setSubmitPayload updates it.

Changes:
- handleSubmitPublishBtnClick
- handleSubmitPublishAndNewBtnClick
- handleSubmitPublishContinueEditingBtnClick
- handleSubmitDraftBtnClick
- handleSubmitDraftAndNewBtnClick
- handleSubmitDraftContinueEditingBtnClick

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-07 16:58:57 +02:00
Ahmed Bouhuolia
2c05785096 Merge pull request #934 from bigcapitalhq/fix/branches-activation-bills
fix(server): branches activation not marking bills and payments with primary branch
2026-02-05 16:06:39 +02:00
Ahmed Bouhuolia
6af4be9c6c fix(server): branches activation not marking bills and payments with primary branch
When activating the multi-branches feature, existing bills, vendor credits,
and bill payments were not being marked with the default primary branch.

Changes:
- Add missing @Inject decorators to BillActivateBranches, VendorCreditActivateBranches,
  and BillPaymentsActivateBranches services
- Create BillBranchesActivateSubscriber to listen to onActivated event
- Create VendorCreditBranchesActivateSubscriber to listen to onActivated event
- Register BillPaymentsActivateBranches and PaymentMadeActivateBranchesSubscriber
  in BranchesModule
- Add branch object to BillResponseDto for API responses
- Add branch to BillTransformer includeAttributes

Fixes: #935
2026-02-05 16:03:57 +02:00
Ahmed Bouhuolia
8def1d31d2 Merge pull request #933 from bigcapitalhq/20260205-151219-7770
feat(webapp): add blurry background to sticky data table cells
2026-02-05 15:29:47 +02:00
Ahmed Bouhuolia
afab02a053 feat(webapp): add blurry background to sticky data table cells
Add backdrop-filter blur effect to sticky column cells in financial reports
to prevent content from showing through during horizontal scrolling.
The effect only applies when rows are not hovered to preserve hover
background interactions.
2026-02-05 15:27:45 +02:00
Ahmed Bouhuolia
8e925c62f2 Merge pull request #932 from bigcapitalhq/20260205-151219-7770
fix(server): balance sheet query validation schema
2026-02-05 15:14:45 +02:00
Ahmed Bouhuolia
1b7d513adf fix(server): balance sheet query validation schema 2026-02-05 15:12:54 +02:00
Ahmed Bouhuolia
7d764fb390 Merge pull request #931 from bigcapitalhq/fix/item-error-handling
fix(items): correct error type handling and add swagger documentation
2026-02-04 21:44:45 +02:00
Ahmed Bouhuolia
c571f50a74 fix(items): correct error type handling and add swagger documentation
- Fix error type mismatch: change 'ITEM.NAME.ALREADY.EXISTS' to 'ITEM_NAME_EXISTS'
- Add ItemErrorType constant with UpperCamelCase keys for better maintainability
- Update all error checks to use the new ItemErrorType constant
- Add ItemErrorResponse.dto.ts with documented error types for swagger
- Add @ApiResponse decorators to document 400 validation errors in swagger
2026-02-04 21:42:39 +02:00
Ahmed Bouhuolia
6549026344 Merge pull request #930 from bigcapitalhq/fix/account-delete-error-handling
fix(webapp): account delete error handling response types
2026-02-04 21:31:38 +02:00
Ahmed Bouhuolia
0963394b04 fix(webapp): account delete error handling response types 2026-02-04 21:27:25 +02:00
Ahmed Bouhuolia
6cab0651fc Merge pull request #927 from bigcapitalhq/feature/20260202223150
fix(webapp): darkmode warehouses list page
2026-02-02 22:36:42 +02:00
Ahmed Bouhuolia
4af537d6dd fix(webapp): darkmode warehouses list page 2026-02-02 22:31:53 +02:00
Ahmed Bouhuolia
34db64612c Merge pull request #926 from bigcapitalhq/20260202-185120-9c84
fix(webapp): constrant not found row color
2026-02-02 18:53:48 +02:00
Ahmed Bouhuolia
10225bbfed fix(webapp): constrant not found row color 2026-02-02 18:51:52 +02:00
Ahmed Bouhuolia
c3a4fe6b37 Merge pull request #924 from bigcapitalhq/20260201-180532-f578
fix(webapp): normalize api path
2026-02-01 18:06:51 +02:00
Ahmed Bouhuolia
02be959461 fix(webapp): normalize api path 2026-02-01 18:05:51 +02:00
Ahmed Bouhuolia
d5bf56e333 Merge pull request #923 from bigcapitalhq/20260201-165255-f063
fix(server): copy .js migration files
2026-02-01 16:56:49 +02:00
Ahmed Bouhuolia
e3182c15b3 fix(server): copy .js migration files 2026-02-01 16:53:21 +02:00
Ahmed Bouhuolia
dfa63ece21 Merge pull request #921 from bigcapitalhq/20260131-145158-fd0c
fix(scripts): db migration dockerfile
2026-01-31 15:32:07 +02:00
Ahmed Bouhuolia
6e95bd7da1 fix(scripts): db migration dockerfile 2026-01-31 15:31:17 +02:00
Ahmed Bouhuolia
f51fffa5c7 Merge pull request #918 from bigcapitalhq/20260129-203653-75b0
feat(server): add bull ui board
2026-01-29 20:39:05 +02:00
Ahmed Bouhuolia
6193358cc3 feat(server): add bull ui board 2026-01-29 20:37:04 +02:00
Ahmed Bouhuolia
518abcd30d Merge pull request #917 from bigcapitalhq/20260128-195652-2287
fix: dockerfile build script
2026-01-28 23:42:24 +02:00
Ahmed Bouhuolia
7874b9f765 fix(ci): dockerfile build script 2026-01-28 23:40:32 +02:00
Ahmed Bouhuolia
02cc7e0c96 Merge pull request #916 from bigcapitalhq/20260128-181425-8b6a
fix(webapp): blueprintjs datetime version
2026-01-28 18:17:29 +02:00
Ahmed Bouhuolia
57cc513873 fix(webapp): blueprintjs datetime version 2026-01-28 18:14:44 +02:00
Ahmed Bouhuolia
f5bfdede30 Merge pull request #915 from bigcapitalhq/fix-vendor-customer-edit-opening-balance
fix(webapp): vendor/customer edit opening balance
2026-01-27 22:09:00 +02:00
Ahmed Bouhuolia
488556bb59 fix(webapp): vendor/customer edit opening balance 2026-01-27 22:06:57 +02:00
Ahmed Bouhuolia
0fc5a66e95 Merge pull request #914 from bigcapitalhq/fix-costable-inventory-transactions
fix(server): costable attr of inventory gl entries
2026-01-26 15:02:35 +02:00
Ahmed Bouhuolia
d9ae51027e fix(server): costable attr of inventory gl entries 2026-01-26 15:00:17 +02:00
Ahmed Bouhuolia
a92d6112d9 Merge pull request #913 from bigcapitalhq/feature/20260125222025
fix(server): sale receipt cost gl entries
2026-01-25 22:22:08 +02:00
Ahmed Bouhuolia
889b0cec4b fix(server): sale receipt cost gl entries 2026-01-25 22:20:28 +02:00
Ahmed Bouhuolia
1c4c41ebba Merge pull request #912 from bigcapitalhq/feature/20260125215941
fix(server): mark compute inventory cost flag
2026-01-25 22:02:13 +02:00
Ahmed Bouhuolia
421f0c26a7 fix(server): mark compute inventory cost flag 2026-01-25 21:59:44 +02:00
Ahmed Bouhuolia
f461cc221b Merge pull request #911 from bigcapitalhq/feature/20260125001703
fix(server): landed cost gl transactions
2026-01-25 00:19:07 +02:00
Ahmed Bouhuolia
acae75a912 fix(server): landed cost gl transactions 2026-01-25 00:17:14 +02:00
Ahmed Bouhuolia
b5a69971a9 Merge pull request #910 from bigcapitalhq/feature/20260123174320
fix(server): customer/vendor opening balance
2026-01-24 14:02:17 +02:00
Ahmed Bouhuolia
04d065b969 wip 2026-01-24 13:59:43 +02:00
Ahmed Bouhuolia
ca910ee489 fix(server): customer/vendor opening balance: 2026-01-23 17:43:22 +02:00
Ahmed Bouhuolia
e3cf6bf099 Merge pull request #908 from bigcapitalhq/feature/20260121133953
fix: bill response with entries
2026-01-21 13:40:53 +02:00
Ahmed Bouhuolia
6da7e8185c fix: bill response with entries 2026-01-21 13:39:56 +02:00
Ahmed Bouhuolia
785c49f2e6 Merge pull request #907 from bigcapitalhq/feature/20260121130702
hotbug(server): interceptors order
2026-01-21 13:08:18 +02:00
Ahmed Bouhuolia
d7331554ad hotbug(server): interceptors order 2026-01-21 13:07:03 +02:00
Ahmed Bouhuolia
78b1e9136a Merge pull request #897 from bigcapitalhq/more-e2e-test-cases
feat(server): more e2e test cases
2026-01-18 22:46:12 +02:00
Ahmed Bouhuolia
fea9bb5caa Merge remote-tracking branch 'refs/remotes/origin/more-e2e-test-cases' into more-e2e-test-cases 2026-01-18 22:44:17 +02:00
Ahmed Bouhuolia
db5caa138a wip 2026-01-18 22:43:54 +02:00
Ahmed Bouhuolia
bf821885c0 Merge branch 'develop' into more-e2e-test-cases 2026-01-18 15:01:49 +02:00
Ahmed Bouhuolia
5ce5d8b899 Merge pull request #906 from bigcapitalhq/move-app-filters
fix(server): move global filters, pipes, and interceptors to AppModule
2026-01-18 15:00:58 +02:00
Ahmed Bouhuolia
458093fca2 fix(server): move global filters, pipes, and interceptors to AppModule 2026-01-18 14:59:20 +02:00
Ahmed Bouhuolia
97e17848f8 Merge pull request #905 from bigcapitalhq/pagination-darkmode
fix(webapp): pagination darkmode
2026-01-17 23:35:36 +02:00
Ahmed Bouhuolia
3dfe884413 fix(webapp): pagination darkmode 2026-01-17 23:33:10 +02:00
Ahmed Bouhuolia
f26a59f0fb Merge pull request #904 from bigcapitalhq/fix-landed-cost-dialog
fix: landed cost dialog
2026-01-17 21:45:23 +02:00
Ahmed Bouhuolia
7ee161733f fix: landed cost dialog 2026-01-17 21:42:27 +02:00
Ahmed Bouhuolia
4efc0b3eb4 Merge pull request #903 from bigcapitalhq/fix-cancel-invoice-written-off
fix(webapp): cancel the written-off invoice
2026-01-16 19:10:26 +02:00
Ahmed Bouhuolia
532aa07e7f fix(webapp): cancel the written-off invoice 2026-01-16 19:08:07 +02:00
Ahmed Bouhuolia
abacb543c7 Merge pull request #902 from bigcapitalhq/fix-bank-transactions-unexclude2
fix(webapp): unexclude bank transactions
2026-01-16 18:54:36 +02:00
Ahmed Bouhuolia
769eaebc76 fix(webapp): unexclude bank transactions 2026-01-16 18:52:12 +02:00
Ahmed Bouhuolia
e0fb345a48 fix: improve banking transaction exclude/unexclude logic 2026-01-16 18:49:27 +02:00
Ahmed Bouhuolia
c21301061f wip 2026-01-16 00:23:16 +02:00
Ahmed Bouhuolia
2bbc154f18 wip 2026-01-15 22:04:51 +02:00
Ahmed Bouhuolia
3c1273becb wip 2026-01-12 01:04:28 +02:00
Ahmed Bouhuolia
16f1d57279 feat(server): more e2e test cases 2026-01-10 01:01:41 +02:00
Ahmed Bouhuolia
8726b4b3b0 Merge pull request #896 from bigcapitalhq/fix-server-build
fix(server): Dockerfile
2026-01-09 23:40:19 +02:00
Ahmed Bouhuolia
5ace03ea99 fix(server): Dockerfile 2026-01-09 23:38:52 +02:00
Ahmed Bouhuolia
5b6c473780 Merge pull request #895 from bigcapitalhq/fix-bank-accounts-filter
fix(server): bank accounts filter
2026-01-09 20:02:36 +02:00
Ahmed Bouhuolia
2186828516 fix(server): bank accounts filter 2026-01-09 20:00:44 +02:00
343 changed files with 6121 additions and 2458 deletions

93
.dockerignore Normal file
View File

@@ -0,0 +1,93 @@
# Dependencies
node_modules/
**/node_modules/
.pnpm-store/
# Build outputs
dist/
build/
**/dist/
**/build/
*.tsbuildinfo
# Development files
.git/
.gitignore
.vscode/
.idea/
*.swp
*.swo
*~
# Test files
test/
**/test/
**/*.spec.ts
**/*.test.ts
**/*.e2e-spec.ts
coverage/
.nyc_output/
test-results/
playwright-report/
# Documentation
*.md
!README.md
docs/
CHANGELOG.md
CONTRIBUTING.md
DISCLAIMER
LICENSE
# CI/CD
.github/
.gitpod.yml
# Environment files
.env
.env.*
!.env.example
# Logs
*.log
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# OS files
.DS_Store
Thumbs.db
*.pid
*.seed
*.pid.lock
# Docker files (don't copy Dockerfiles into themselves)
docker-compose*.yml
Dockerfile*
.dockerignore
# Misc
.cache/
.temp/
tmp/
*.tmp
.qodo/
e2e/
playwright.config.ts
# Source maps (not needed in production)
*.map
# TypeScript configs (not needed at runtime)
tsconfig*.json
!tsconfig.json
# Linting/formatting
.eslintrc*
.prettierrc*
.eslintcache
# Package manager locks (we copy them explicitly)
# pnpm-lock.yaml

View File

@@ -35,17 +35,10 @@ TENANT_DB_NAME_PERFIX=bigcapital_tenant_
BASE_URL=http://example.com BASE_URL=http://example.com
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
# Jobs MongoDB
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
# App proxy # App proxy
PUBLIC_PROXY_PORT=80 PUBLIC_PROXY_PORT=80
PUBLIC_PROXY_SSL_PORT=443 PUBLIC_PROXY_SSL_PORT=443
# Agendash
AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123
# Sign-up restrictions # Sign-up restrictions
SIGNUP_DISABLED=false SIGNUP_DISABLED=false
SIGNUP_ALLOWED_DOMAINS= SIGNUP_ALLOWED_DOMAINS=

View File

@@ -32,11 +32,9 @@ services:
- '3000' - '3000'
links: links:
- mysql - mysql
- mongo
- redis - redis
depends_on: depends_on:
- mysql - mysql
- mongo
- redis - redis
restart: on-failure restart: on-failure
networks: networks:
@@ -60,22 +58,21 @@ services:
# System database # System database
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME} - SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
# Redis
- REDIS_HOST=redis
- REDIS_PORT=6379
- QUEUE_HOST=redis
- QUEUE_PORT=6379
# Tenants databases # Tenants databases
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX} - TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
# Authentication # Authentication
- JWT_SECRET=${JWT_SECRET} - JWT_SECRET=${JWT_SECRET}
# MongoDB
- MONGODB_DATABASE_URL=mongodb://mongo/bigcapital
# Application # Application
- BASE_URL=${BASE_URL} - BASE_URL=${BASE_URL}
# Agendash
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
# Sign-up restrictions # Sign-up restrictions
- SIGNUP_DISABLED=${SIGNUP_DISABLED} - SIGNUP_DISABLED=${SIGNUP_DISABLED}
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS} - SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
@@ -166,17 +163,6 @@ services:
networks: networks:
- bigcapital_network - bigcapital_network
mongo:
container_name: bigcapital-mongo
restart: on-failure
build: ./docker/mongo
expose:
- '27017'
volumes:
- mongo:/var/lib/mongodb
networks:
- bigcapital_network
redis: redis:
container_name: bigcapital-redis container_name: bigcapital-redis
restart: on-failure restart: on-failure
@@ -202,10 +188,6 @@ volumes:
name: bigcapital_prod_mysql name: bigcapital_prod_mysql
driver: local driver: local
mongo:
name: bigcapital_prod_mongo
driver: local
redis: redis:
name: bigcapital_prod_redis name: bigcapital_prod_redis
driver: local driver: local

View File

@@ -24,25 +24,13 @@ services:
restart_policy: restart_policy:
condition: unless-stopped condition: unless-stopped
mongo:
build: ./docker/mongo
expose:
- '27017'
volumes:
- mongo:/var/lib/mongodb
ports:
- '27017:27017'
deploy:
restart_policy:
condition: unless-stopped
redis: redis:
build: build:
context: ./docker/redis context: ./docker/redis
expose: expose:
- "6379" - '6379'
ports: ports:
- "6379:6379" - '6379:6379'
volumes: volumes:
- redis:/data - redis:/data
deploy: deploy:
@@ -52,7 +40,7 @@ services:
gotenberg: gotenberg:
image: gotenberg/gotenberg:7 image: gotenberg/gotenberg:7
ports: ports:
- "9000:3000" - '9000:3000'
# Volumes # Volumes
volumes: volumes:
@@ -60,10 +48,6 @@ volumes:
name: bigcapital_dev_mysql name: bigcapital_dev_mysql
driver: local driver: local
mongo:
name: bigcapital_dev_mongo
driver: local
redis: redis:
name: bigcapital_dev_redis name: bigcapital_dev_redis
driver: local driver: local

View File

@@ -35,4 +35,4 @@ WORKDIR /app/packages/server
RUN git clone https://github.com/vishnubob/wait-for-it.git RUN git clone https://github.com/vishnubob/wait-for-it.git
# Once we listen the mysql port run the migration task. # Once we listen the mysql port run the migration task.
CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "node ./build/commands.js system:migrate:latest && node ./build/commands.js tenants:migrate:latest" CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "node dist/cli.js system:migrate:latest && node dist/cli.js tenants:migrate:latest"

View File

@@ -1 +0,0 @@
FROM mongo:5.0

View File

@@ -35,17 +35,10 @@ TENANT_DB_NAME_PERFIX=bigcapital_tenant_
BASE_URL=http://example.com BASE_URL=http://example.com
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
# Jobs MongoDB
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
# App proxy # App proxy
PUBLIC_PROXY_PORT=80 PUBLIC_PROXY_PORT=80
PUBLIC_PROXY_SSL_PORT=443 PUBLIC_PROXY_SSL_PORT=443
# Agendash
AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123
# Sign-up restrictions # Sign-up restrictions
SIGNUP_DISABLED=false SIGNUP_DISABLED=false
SIGNUP_ALLOWED_DOMAINS= SIGNUP_ALLOWED_DOMAINS=

102
packages/server/Dockerfile Normal file
View File

@@ -0,0 +1,102 @@
# Stage 1: Build
FROM node:18.16.0-alpine AS builder
WORKDIR /app
# Install pnpm
RUN npm install -g pnpm@8.10.2
# Install build dependencies
RUN apk add --no-cache python3 build-base chromium
# Set Python environment
ENV PYTHON=/usr/bin/python3
# Copy package files for dependency installation
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./
COPY --chown=node:node packages/server/package.json ./packages/server/
COPY --chown=node:node shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
COPY --chown=node:node shared/pdf-templates/package.json ./shared/pdf-templates/
COPY --chown=node:node shared/email-components/package.json ./shared/email-components/
# Install all dependencies (including devDependencies for build)
RUN pnpm install --frozen-lockfile
# Copy source code
COPY --chown=node:node ./packages/server ./packages/server
COPY --chown=node:node ./shared/bigcapital-utils ./shared/bigcapital-utils
COPY --chown=node:node ./shared/pdf-templates ./shared/pdf-templates
COPY --chown=node:node ./shared/email-components ./shared/email-components
# Build NestJS application
RUN pnpm run build:server --skip-nx-cache
# Stage 2: Production
FROM node:18.16.0-alpine AS production
WORKDIR /app
# Install pnpm for production
RUN npm install -g pnpm@8.10.2
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Install build dependencies for native modules (bcrypt, etc.)
RUN apk add --no-cache python3 build-base
# Set Python environment
ENV PYTHON=/usr/bin/python3
# Copy package files for production dependency installation
COPY --chown=nodejs:nodejs package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY --chown=nodejs:nodejs packages/server/package.json ./packages/server/
COPY --chown=nodejs:nodejs shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
COPY --chown=nodejs:nodejs shared/pdf-templates/package.json ./shared/pdf-templates/
COPY --chown=nodejs:nodejs shared/email-components/package.json ./shared/email-components/
# Copy .husky directory (needed for husky install command)
COPY --chown=nodejs:nodejs .husky ./.husky
# Install only production dependencies
# Install husky temporarily so prepare script can run, then remove it
RUN pnpm add -D -w husky && \
pnpm install --prod --frozen-lockfile && \
pnpm remove -w husky && \
# Remove build dependencies to reduce image size
apk del python3 build-base
# Copy built application from builder stage
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/dist ./packages/server/dist
# Copy static assets (i18n, public, static directories)
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/i18n ./packages/server/dist/i18n
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/public ./packages/server/public
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/static ./packages/server/static
# Copy database migration files (needed for running migrations)
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/database ./packages/server/src/database
# Copy built shared packages (dist folders and package.json for module resolution)
COPY --from=builder --chown=nodejs:nodejs /app/shared/bigcapital-utils/dist ./shared/bigcapital-utils/dist
COPY --from=builder --chown=nodejs:nodejs /app/shared/pdf-templates/dist ./shared/pdf-templates/dist
COPY --from=builder --chown=nodejs:nodejs /app/shared/email-components/dist ./shared/email-components/dist
# Set runtime environment variables (these should be provided at runtime via docker-compose or k8s)
ENV NODE_ENV=production
ENV NEW_RELIC_NO_CONFIG_FILE=true
ENV PORT=3000
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 3000
# Health check - uses /api/system_db ping endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/api/system_db', (r) => {process.exit(r.statusCode >= 200 && r.statusCode < 300 ? 0 : 1)}).on('error', () => process.exit(1))"
# Start the application
CMD [ "node", "packages/server/dist/main.js" ]

View File

@@ -2,10 +2,23 @@
"$schema": "https://json.schemastore.org/nest-cli", "$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src", "sourceRoot": "src",
"entryFile": "main",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true, "deleteOutDir": true,
"assets": [ "assets": [
{ "include": "i18n/**/*", "watchAssets": true } { "include": "i18n/**/*", "watchAssets": true },
{ "include": "database/**/*", "exclude": "**/*.ts", "watchAssets": true }
] ]
},
"projects": {
"cli": {
"type": "application",
"root": "src",
"entryFile": "cli",
"sourceRoot": "src",
"compilerOptions": {
"tsConfigPath": "tsconfig.json"
}
}
} }
} }

View File

@@ -40,6 +40,9 @@
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0", "@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@liaoliaots/nestjs-redis": "^10.0.0", "@liaoliaots/nestjs-redis": "^10.0.0",
"@nest-lab/throttler-storage-redis": "^1.1.0", "@nest-lab/throttler-storage-redis": "^1.1.0",
"@bull-board/api": "^5.22.0",
"@bull-board/express": "^5.22.0",
"@bull-board/nestjs": "^5.22.0",
"@nestjs/bull": "^10.2.1", "@nestjs/bull": "^10.2.1",
"@nestjs/bullmq": "^10.2.2", "@nestjs/bullmq": "^10.2.2",
"@nestjs/cache-manager": "^2.2.2", "@nestjs/cache-manager": "^2.2.2",

View File

@@ -0,0 +1,8 @@
import { registerAs } from '@nestjs/config';
import { parseBoolean } from '@/utils/parse-boolean';
export default registerAs('bullBoard', () => ({
enabled: parseBoolean<boolean>(process.env.BULL_BOARD_ENABLED, false),
username: process.env.BULL_BOARD_USERNAME,
password: process.env.BULL_BOARD_PASSWORD,
}));

View File

@@ -17,6 +17,9 @@ import loops from './loops';
import bankfeed from './bankfeed'; import bankfeed from './bankfeed';
import throttle from './throttle'; import throttle from './throttle';
import cloud from './cloud'; import cloud from './cloud';
import redis from './redis';
import queue from './queue';
import bullBoard from './bull-board';
export const config = [ export const config = [
app, app,
@@ -38,4 +41,7 @@ export const config = [
loops, loops,
bankfeed, bankfeed,
throttle, throttle,
redis,
queue,
bullBoard,
]; ];

View File

@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('queue', () => ({
host: process.env.QUEUE_HOST || 'localhost',
port: parseInt(process.env.QUEUE_PORT, 10) || 6379,
}));

View File

@@ -6,4 +6,5 @@ export default registerAs('s3', () => ({
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
endpoint: process.env.S3_ENDPOINT, endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET, bucket: process.env.S3_BUCKET,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
})); }));

View File

@@ -5,15 +5,18 @@ import {
NestInterceptor, NestInterceptor,
} from '@nestjs/common'; } from '@nestjs/common';
import { Observable, map } from 'rxjs'; import { Observable, map } from 'rxjs';
import { mapValues, mapValuesDeep } from '@/utils/deepdash'; import { mapValuesDeep } from '@/utils/deepdash';
@Injectable() @Injectable()
export class ToJsonInterceptor implements NestInterceptor { export class ToJsonInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe( return next.handle().pipe(
map((data) => { map((data) => {
if (data === null || data === undefined) {
return data;
}
return mapValuesDeep(data, (value) => { return mapValuesDeep(data, (value) => {
if (value && typeof value.toJSON === 'function') { if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
return value.toJSON(); return value.toJSON();
} }
return value; return value;

View File

@@ -22,6 +22,9 @@
"field.status.unpaid": "Unpaid", "field.status.unpaid": "Unpaid",
"field.status.opened": "Opened", "field.status.opened": "Opened",
"field.status.draft": "Draft", "field.status.draft": "Draft",
"field.created_at": "Created At" "field.created_at": "Created At",
"allocation_method": "Allocation Method",
"allocation_method.quantity": "Quantity",
"allocation_method.value": "Valuation"
} }

View File

@@ -4,10 +4,6 @@ import { ClsMiddleware } from 'nestjs-cls';
import * as path from 'path'; import * as path from 'path';
import './utils/moment-mysql'; import './utils/moment-mysql';
import { AppModule } from './modules/App/App.module'; import { AppModule } from './modules/App/App.module';
import { ServiceErrorFilter } from './common/filters/service-error.filter';
import { ModelHasRelationsFilter } from './common/filters/model-has-relations.filter';
import { ValidationPipe } from './common/pipes/ClassValidation.pipe';
import { ToJsonInterceptor } from './common/interceptors/to-json.interceptor';
import { NestExpressApplication } from '@nestjs/platform-express'; import { NestExpressApplication } from '@nestjs/platform-express';
global.__public_dirname = path.join(__dirname, '..', 'public'); global.__public_dirname = path.join(__dirname, '..', 'public');
@@ -25,11 +21,6 @@ async function bootstrap() {
// create and mount the middleware manually here // create and mount the middleware manually here
app.use(new ClsMiddleware({}).use); app.use(new ClsMiddleware({}).use);
app.useGlobalInterceptors(new ToJsonInterceptor());
// use the validation pipe globally
app.useGlobalPipes(new ValidationPipe());
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle('Bigcapital') .setTitle('Bigcapital')
.setDescription('Financial accounting software') .setDescription('Financial accounting software')
@@ -39,9 +30,6 @@ async function bootstrap() {
const documentFactory = () => SwaggerModule.createDocument(app, config); const documentFactory = () => SwaggerModule.createDocument(app, config);
SwaggerModule.setup('swagger', app, documentFactory); SwaggerModule.setup('swagger', app, documentFactory);
app.useGlobalFilters(new ServiceErrorFilter());
app.useGlobalFilters(new ModelHasRelationsFilter());
await app.listen(process.env.PORT ?? 3000); await app.listen(process.env.PORT ?? 3000);
} }
bootstrap(); bootstrap();

View File

@@ -0,0 +1,59 @@
import { Request, Response, NextFunction } from 'express';
/**
* Creates Express middleware for the Bull Board UI:
* - When disabled: responds with 404.
* - When enabled and username/password are set: enforces HTTP Basic Auth (401 if invalid).
* - When enabled and credentials are not set: allows access (no auth).
*/
export function createBullBoardAuthMiddleware(
enabled: boolean,
username: string | undefined,
password: string | undefined,
): (req: Request, res: Response, next: NextFunction) => void {
return (req: Request, res: Response, next: NextFunction) => {
if (!enabled) {
res.status(404).send('Not Found');
return;
}
if (!username || !password) {
return next();
}
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
res.status(401).send('Authentication required');
return;
}
const base64Credentials = authHeader.slice(6);
let decoded: string;
try {
decoded = Buffer.from(base64Credentials, 'base64').toString('utf8');
} catch {
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
res.status(401).send('Invalid credentials');
return;
}
const colonIndex = decoded.indexOf(':');
if (colonIndex === -1) {
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
res.status(401).send('Invalid credentials');
return;
}
const reqUsername = decoded.slice(0, colonIndex);
const reqPassword = decoded.slice(colonIndex + 1);
if (reqUsername !== username || reqPassword !== password) {
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
res.status(401).send('Invalid credentials');
return;
}
next();
};
}

View File

@@ -8,6 +8,8 @@ import {
Query, Query,
ParseIntPipe, ParseIntPipe,
Put, Put,
HttpCode,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { AccountsApplication } from './AccountsApplication.service'; import { AccountsApplication } from './AccountsApplication.service';
import { CreateAccountDTO } from './CreateAccount.dto'; import { CreateAccountDTO } from './CreateAccount.dto';
@@ -31,6 +33,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { AccountAction } from './Accounts.types';
@Controller('accounts') @Controller('accounts')
@ApiTags('Accounts') @ApiTags('Accounts')
@@ -39,10 +46,13 @@ import {
@ApiExtraModels(GetAccountTransactionResponseDto) @ApiExtraModels(GetAccountTransactionResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class AccountsController { export class AccountsController {
constructor(private readonly accountsApplication: AccountsApplication) { } constructor(private readonly accountsApplication: AccountsApplication) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.', 'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
@@ -64,6 +74,8 @@ export class AccountsController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' }) @ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -78,6 +90,7 @@ export class AccountsController {
} }
@Post() @Post()
@RequirePermission(AccountAction.CREATE, AbilitySubject.Account)
@ApiOperation({ summary: 'Create an account' }) @ApiOperation({ summary: 'Create an account' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -88,6 +101,7 @@ export class AccountsController {
} }
@Put(':id') @Put(':id')
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Edit the given account.' }) @ApiOperation({ summary: 'Edit the given account.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -108,6 +122,7 @@ export class AccountsController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Delete the given account.' }) @ApiOperation({ summary: 'Delete the given account.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -125,6 +140,8 @@ export class AccountsController {
} }
@Post(':id/activate') @Post(':id/activate')
@HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Activate the given account.' }) @ApiOperation({ summary: 'Activate the given account.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -142,6 +159,8 @@ export class AccountsController {
} }
@Post(':id/inactivate') @Post(':id/inactivate')
@HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Inactivate the given account.' }) @ApiOperation({ summary: 'Inactivate the given account.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -159,6 +178,7 @@ export class AccountsController {
} }
@Get('types') @Get('types')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account types.' }) @ApiOperation({ summary: 'Retrieves the account types.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -175,6 +195,7 @@ export class AccountsController {
} }
@Get('transactions') @Get('transactions')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account transactions.' }) @ApiOperation({ summary: 'Retrieves the account transactions.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -193,6 +214,7 @@ export class AccountsController {
} }
@Get(':id') @Get(':id')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account details.' }) @ApiOperation({ summary: 'Retrieves the account details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -211,6 +233,7 @@ export class AccountsController {
} }
@Get() @Get()
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the accounts.' }) @ApiOperation({ summary: 'Retrieves the accounts.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -18,7 +18,7 @@ export class DeleteAccount {
private eventEmitter: EventEmitter2, private eventEmitter: EventEmitter2,
private uow: UnitOfWork, private uow: UnitOfWork,
private validator: CommandAccountValidators, private validator: CommandAccountValidators,
) {} ) { }
/** /**
* Authorize account delete. * Authorize account delete.
@@ -57,7 +57,10 @@ export class DeleteAccount {
trx?: Knex.Transaction, trx?: Knex.Transaction,
): Promise<void> => { ): Promise<void> => {
// Retrieve account or not found service error. // Retrieve account or not found service error.
const oldAccount = await this.accountModel().query().findById(accountId); const oldAccount = await this.accountModel()
.query()
.findById(accountId)
.throwIfNotFound();
// Authorize before delete account. // Authorize before delete account.
await this.authorize(accountId, oldAccount); await this.authorize(accountId, oldAccount);

View File

@@ -33,6 +33,7 @@ export class AccountTransaction extends BaseModel {
public readonly userId!: number; public readonly userId!: number;
public readonly itemId!: number; public readonly itemId!: number;
public readonly projectId!: number; public readonly projectId!: number;
public readonly costable!: boolean;
public readonly account: Account; public readonly account: Account;
/** /**

View File

@@ -1,7 +1,7 @@
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter'; import { EventEmitterModule } from '@nestjs/event-emitter';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE, APP_FILTER } from '@nestjs/core';
import { join } from 'path'; import { join } from 'path';
import { ServeStaticModule } from '@nestjs/serve-static'; import { ServeStaticModule } from '@nestjs/serve-static';
import { RedisModule } from '@liaoliaots/nestjs-redis'; import { RedisModule } from '@liaoliaots/nestjs-redis';
@@ -12,6 +12,9 @@ import {
I18nModule, I18nModule,
QueryResolver, QueryResolver,
} from 'nestjs-i18n'; } from 'nestjs-i18n';
import { BullBoardModule } from '@bull-board/nestjs';
import { ExpressAdapter } from '@bull-board/express';
import { createBullBoardAuthMiddleware } from '@/middleware/bull-board-auth.middleware';
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from '@nestjs/bullmq';
import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleModule } from '@nestjs/schedule';
import { PassportModule } from '@nestjs/passport'; import { PassportModule } from '@nestjs/passport';
@@ -36,6 +39,10 @@ import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
import { BranchesModule } from '../Branches/Branches.module'; import { BranchesModule } from '../Branches/Branches.module';
import { WarehousesModule } from '../Warehouses/Warehouses.module'; import { WarehousesModule } from '../Warehouses/Warehouses.module';
import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor'; import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor';
import { ToJsonInterceptor } from '@/common/interceptors/to-json.interceptor';
import { ValidationPipe } from '@/common/pipes/ClassValidation.pipe';
import { ServiceErrorFilter } from '@/common/filters/service-error.filter';
import { ModelHasRelationsFilter } from '@/common/filters/model-has-relations.filter';
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module'; import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
import { CustomersModule } from '../Customers/Customers.module'; import { CustomersModule } from '../Customers/Customers.module';
import { VendorsModule } from '../Vendors/Vendors.module'; import { VendorsModule } from '../Vendors/Vendors.module';
@@ -133,12 +140,30 @@ import { AppThrottleModule } from './AppThrottle.module';
imports: [ConfigModule], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
connection: { connection: {
host: configService.get('QUEUE_HOST'), host: configService.get('queue.host'),
port: configService.get('QUEUE_PORT'), port: configService.get('queue.port'),
}, },
}), }),
inject: [ConfigService], inject: [ConfigService],
}), }),
BullBoardModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
const enabled = configService.get<boolean>('bullBoard.enabled');
const username = configService.get<string>('bullBoard.username');
const password = configService.get<string>('bullBoard.password');
return {
route: '/queues',
adapter: ExpressAdapter,
middleware: createBullBoardAuthMiddleware(
enabled,
username,
password,
),
};
},
inject: [ConfigService],
}),
ClsModule.forRoot({ ClsModule.forRoot({
global: true, global: true,
middleware: { middleware: {
@@ -154,8 +179,8 @@ import { AppThrottleModule } from './AppThrottle.module';
imports: [ConfigModule], imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({ useFactory: (configService: ConfigService) => ({
config: { config: {
host: configService.get('redis.host') || 'localhost', host: configService.get('redis.host'),
port: configService.get('redis.port') || 6379, port: configService.get('redis.port'),
}, },
}), }),
inject: [ConfigService], inject: [ConfigService],
@@ -234,6 +259,10 @@ import { AppThrottleModule } from './AppThrottle.module';
], ],
controllers: [AppController], controllers: [AppController],
providers: [ providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
{ {
provide: APP_GUARD, provide: APP_GUARD,
useClass: ThrottlerGuard, useClass: ThrottlerGuard,
@@ -242,6 +271,10 @@ import { AppThrottleModule } from './AppThrottle.module';
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,
useClass: SerializeInterceptor, useClass: SerializeInterceptor,
}, },
{
provide: APP_INTERCEPTOR,
useClass: ToJsonInterceptor,
},
{ {
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,
useClass: UserIpInterceptor, useClass: UserIpInterceptor,
@@ -250,6 +283,14 @@ import { AppThrottleModule } from './AppThrottle.module';
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,
useClass: ExcludeNullInterceptor, useClass: ExcludeNullInterceptor,
}, },
{
provide: APP_FILTER,
useClass: ServiceErrorFilter,
},
{
provide: APP_FILTER,
useClass: ModelHasRelationsFilter,
},
AppService, AppService,
], ],
}) })

View File

@@ -9,6 +9,27 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: (configService: ConfigService) => { useFactory: (configService: ConfigService) => {
// Use in-memory storage with very high limits for test environment
const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
if (isTest) {
return {
throttlers: [
{
name: 'default',
ttl: 60000,
limit: 1000000, // Effectively disable throttling in tests
},
{
name: 'auth',
ttl: 60000,
limit: 1000000, // Effectively disable throttling in tests
},
],
// No storage specified = uses in-memory storage
};
}
const host = configService.get<string>('redis.host') || 'localhost'; const host = configService.get<string>('redis.host') || 'localhost';
const port = Number(configService.get<number>('redis.port') || 6379); const port = Number(configService.get<number>('redis.port') || 6379);
const password = configService.get<string>('redis.password'); const password = configService.get<string>('redis.password');

View File

@@ -17,6 +17,8 @@ import { PassportModule } from '@nestjs/passport';
import { APP_GUARD } from '@nestjs/core'; import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './guards/jwt.guard'; import { JwtAuthGuard } from './guards/jwt.guard';
import { AuthMailSubscriber } from './subscribers/AuthMail.subscriber'; import { AuthMailSubscriber } from './subscribers/AuthMail.subscriber';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from '@nestjs/bullmq';
import { import {
SendResetPasswordMailQueue, SendResetPasswordMailQueue,
@@ -63,6 +65,14 @@ const models = [
TenancyModule, TenancyModule,
BullModule.registerQueue({ name: SendResetPasswordMailQueue }), BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }), BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
BullBoardModule.forFeature({
name: SendResetPasswordMailQueue,
adapter: BullMQAdapter,
}),
BullBoardModule.forFeature({
name: SendSignupVerificationMailQueue,
adapter: BullMQAdapter,
}),
], ],
exports: [...models], exports: [...models],
providers: [ providers: [
@@ -98,4 +108,4 @@ const models = [
AuthMailSubscriber, AuthMailSubscriber,
], ],
}) })
export class AuthModule { } export class AuthModule {}

View File

@@ -1,10 +1,6 @@
import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { import { SendResetPasswordMailQueue } from '../Auth.constants';
SendResetPasswordMailJob,
SendResetPasswordMailQueue,
} from '../Auth.constants';
import { Process } from '@nestjs/bull';
import { Job } from 'bullmq'; import { Job } from 'bullmq';
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice'; import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service'; import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
@@ -23,7 +19,6 @@ export class SendResetPasswordMailProcessor extends WorkerHost {
super(); super();
} }
@Process(SendResetPasswordMailJob)
async process(job: Job<SendResetPasswordMailJobPayload>) { async process(job: Job<SendResetPasswordMailJobPayload>) {
try { try {
await this.authMailMesssages.sendResetPasswordMail( await this.authMailMesssages.sendResetPasswordMail(

View File

@@ -1,11 +1,7 @@
import { Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { Job } from 'bullmq'; import { Job } from 'bullmq';
import { Process } from '@nestjs/bull';
import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { import { SendSignupVerificationMailQueue } from '../Auth.constants';
SendSignupVerificationMailJob,
SendSignupVerificationMailQueue,
} from '../Auth.constants';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service'; import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice'; import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
@@ -21,7 +17,6 @@ export class SendSignupVerificationMailProcessor extends WorkerHost {
super(); super();
} }
@Process(SendSignupVerificationMailJob)
async process(job: Job<SendSignupVerificationMailJobPayload>) { async process(job: Job<SendSignupVerificationMailJobPayload>) {
try { try {
await this.authMailMesssages.sendSignupVerificationMail( await this.authMailMesssages.sendSignupVerificationMail(

View File

@@ -16,7 +16,7 @@ import { ToNumber } from '@/common/decorators/Validators';
class BankRuleConditionDto { class BankRuleConditionDto {
@IsNotEmpty() @IsNotEmpty()
@IsIn(['description', 'amount']) @IsIn(['description', 'amount', 'payee'])
field: string; field: string;
@IsNotEmpty() @IsNotEmpty()

View File

@@ -1,16 +1,28 @@
import { Controller, Get, Param, Post, Query } from '@nestjs/common'; import { Controller, Get, Param, Post, Query } from '@nestjs/common';
import { BankAccountsApplication } from './BankAccountsApplication.service'; import { BankAccountsApplication } from './BankAccountsApplication.service';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ICashflowAccountsFilter } from './types/BankAccounts.types'; import { BankAccountsQueryDto } from './dtos/BankAccountsQuery.dto';
import { BankAccountResponseDto } from './dtos/BankAccountResponse.dto';
@Controller('banking/accounts') @Controller('banking/accounts')
@ApiTags('Bank Accounts') @ApiTags('Bank Accounts')
export class BankAccountsController { export class BankAccountsController {
constructor(private bankAccountsApplication: BankAccountsApplication) {} constructor(private bankAccountsApplication: BankAccountsApplication) { }
@Get() @Get()
@ApiOperation({ summary: 'Retrieve the bank accounts.' }) @ApiOperation({ summary: 'Retrieve the bank accounts.' })
getBankAccounts(@Query() filterDto: ICashflowAccountsFilter) { @ApiQuery({
name: 'query',
description: 'Query parameters for the bank accounts list.',
type: BankAccountsQueryDto,
required: false,
})
@ApiResponse({
status: 200,
description: 'List of bank accounts retrieved successfully.',
type: [BankAccountResponseDto],
})
getBankAccounts(@Query() filterDto: BankAccountsQueryDto) {
return this.bankAccountsApplication.getBankAccounts(filterDto); return this.bankAccountsApplication.getBankAccounts(filterDto);
} }

View File

@@ -6,6 +6,7 @@ import { PauseBankAccountFeeds } from './commands/PauseBankAccountFeeds.service'
import { GetBankAccountsService } from './queries/GetBankAccounts'; import { GetBankAccountsService } from './queries/GetBankAccounts';
import { ICashflowAccountsFilter } from './types/BankAccounts.types'; import { ICashflowAccountsFilter } from './types/BankAccounts.types';
import { GetBankAccountSummary } from './queries/GetBankAccountSummary'; import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
import { BankAccountsQueryDto } from './dtos/BankAccountsQuery.dto';
@Injectable() @Injectable()
export class BankAccountsApplication { export class BankAccountsApplication {
@@ -16,13 +17,13 @@ export class BankAccountsApplication {
private readonly refreshBankAccountService: RefreshBankAccountService, private readonly refreshBankAccountService: RefreshBankAccountService,
private readonly resumeBankAccountFeedsService: ResumeBankAccountFeedsService, private readonly resumeBankAccountFeedsService: ResumeBankAccountFeedsService,
private readonly pauseBankAccountFeedsService: PauseBankAccountFeeds, private readonly pauseBankAccountFeedsService: PauseBankAccountFeeds,
) {} ) { }
/** /**
* Retrieves the bank accounts. * Retrieves the bank accounts.
* @param {ICashflowAccountsFilter} filterDto - * @param {ICashflowAccountsFilter} filterDto -
*/ */
getBankAccounts(filterDto: ICashflowAccountsFilter) { getBankAccounts(filterDto: BankAccountsQueryDto) {
return this.getBankAccountsService.getCashflowAccounts(filterDto); return this.getBankAccountsService.getCashflowAccounts(filterDto);
} }

View File

@@ -0,0 +1,166 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* Bank Account Response DTO
* Based on AccountResponseDto but excludes fields that are filtered out by CashflowAccountTransformer:
* - predefined
* - index
* - accountTypeLabel
*/
export class BankAccountResponseDto {
@ApiProperty({
description: 'The unique identifier of the account',
example: 1,
})
id: number;
@ApiProperty({
description: 'The name of the account',
example: 'Cash Account',
})
name: string;
@ApiProperty({
description: 'The slug of the account',
example: 'cash-account',
})
slug: string;
@ApiProperty({
description: 'The code of the account',
example: '1001',
})
code: string;
@ApiProperty({
description: 'The type of the account',
example: 'bank',
})
accountType: string;
@ApiProperty({
description: 'The parent account ID',
example: null,
})
parentAccountId: number | null;
@ApiProperty({
description: 'The currency code of the account',
example: 'USD',
})
currencyCode: string;
@ApiProperty({
description: 'Whether the account is active',
example: true,
})
active: boolean;
@ApiProperty({
description: 'The bank balance of the account',
example: 5000.0,
})
bankBalance: number;
@ApiProperty({
description: 'The formatted bank balance',
example: '$5,000.00',
})
bankBalanceFormatted: string;
@ApiProperty({
description: 'The last feeds update timestamp',
example: '2024-03-20T10:30:00Z',
})
lastFeedsUpdatedAt: string | Date | null;
@ApiProperty({
description: 'The formatted last feeds update timestamp',
example: 'Mar 20, 2024 10:30 AM',
})
lastFeedsUpdatedAtFormatted: string;
@ApiProperty({
description: 'The last feeds updated from now (relative time)',
example: '2 hours ago',
})
lastFeedsUpdatedFromNow: string;
@ApiProperty({
description: 'The amount of the account',
example: 5000.0,
})
amount: number;
@ApiProperty({
description: 'The formatted amount',
example: '$5,000.00',
})
formattedAmount: string;
@ApiProperty({
description: 'The Plaid item ID',
example: 'plaid-item-123',
})
plaidItemId: string;
@ApiProperty({
description: 'The Plaid account ID',
example: 'plaid-account-456',
})
plaidAccountId: string | null;
@ApiProperty({
description: 'Whether the feeds are active',
example: true,
})
isFeedsActive: boolean;
@ApiProperty({
description: 'Whether the account is syncing owner',
example: true,
})
isSyncingOwner: boolean;
@ApiProperty({
description: 'Whether the feeds are paused',
example: false,
})
isFeedsPaused: boolean;
@ApiProperty({
description: 'The account normal',
example: 'debit',
})
accountNormal: string;
@ApiProperty({
description: 'The formatted account normal',
example: 'Debit',
})
accountNormalFormatted: string;
@ApiProperty({
description: 'The flatten name with all dependant accounts names',
example: 'Assets: Cash Account',
})
flattenName: string;
@ApiProperty({
description: 'The account level in the hierarchy',
example: 2,
})
accountLevel?: number;
@ApiProperty({
description: 'The creation timestamp',
example: '2024-03-20T10:00:00Z',
})
createdAt: Date;
@ApiProperty({
description: 'The update timestamp',
example: '2024-03-20T10:30:00Z',
})
updatedAt: Date;
}

View File

@@ -0,0 +1,107 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, Min } from 'class-validator';
import { ToNumber } from '@/common/decorators/Validators';
import { IFilterRole, ISortOrder } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
import { parseBoolean } from '@/utils/parse-boolean';
export class BankAccountsQueryDto {
@ApiPropertyOptional({
description: 'Custom view ID',
type: Number,
example: 1,
})
@IsOptional()
@ToNumber()
@IsInt()
customViewId?: number;
@ApiPropertyOptional({
description: 'Filter roles array',
type: Array,
isArray: true,
})
@IsArray()
@IsOptional()
filterRoles?: IFilterRole[];
@ApiPropertyOptional({
description: 'Column to sort by',
type: String,
example: 'created_at',
})
@IsOptional()
@IsString()
columnSortBy?: string;
@ApiPropertyOptional({
description: 'Sort order',
enum: ISortOrder,
example: ISortOrder.DESC,
})
@IsOptional()
@IsEnum(ISortOrder)
sortOrder?: string;
@ApiPropertyOptional({
description: 'Stringified filter roles',
type: String,
example: '{"fieldKey":"status","value":"active"}',
})
@IsOptional()
@IsString()
stringifiedFilterRoles?: string;
@ApiPropertyOptional({
description: 'Search keyword',
type: String,
example: 'bank account',
})
@IsOptional()
@IsString()
searchKeyword?: string;
@ApiPropertyOptional({
description: 'View slug',
type: String,
example: 'active-accounts',
})
@IsOptional()
@IsString()
viewSlug?: string;
@ApiPropertyOptional({
description: 'Page number',
type: Number,
example: 1,
minimum: 1,
})
@IsOptional()
@ToNumber()
@IsInt()
@Min(1)
page?: number;
@ApiPropertyOptional({
description: 'Page size',
type: Number,
example: 25,
minimum: 1,
})
@IsOptional()
@ToNumber()
@IsInt()
@Min(1)
pageSize?: number;
@ApiPropertyOptional({
description: 'Include inactive accounts',
type: Boolean,
example: false,
default: false,
})
@IsOptional()
@Transform(({ value }) => parseBoolean(value, false))
@IsBoolean()
inactiveMode?: boolean;
}

View File

@@ -6,6 +6,8 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ICashflowAccountsFilter } from '../types/BankAccounts.types'; import { ICashflowAccountsFilter } from '../types/BankAccounts.types';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
import { BankAccountsQueryDto } from '../dtos/BankAccountsQuery.dto';
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
@Injectable() @Injectable()
export class GetBankAccountsService { export class GetBankAccountsService {
@@ -15,14 +17,14 @@ export class GetBankAccountsService {
@Inject(Account.name) @Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>, private readonly accountModel: TenantModelProxy<typeof Account>,
) {} ) { }
/** /**
* Retrieve the cash flow accounts. * Retrieve the cash flow accounts.
* @param {ICashflowAccountsFilter} filterDTO - Filter DTO. * @param {ICashflowAccountsFilter} filterDTO - Filter DTO.
* @returns {ICashflowAccount[]} * @returns {ICashflowAccount[]}
*/ */
public async getCashflowAccounts(filterDTO: ICashflowAccountsFilter) { public async getCashflowAccounts(filterDTO: BankAccountsQueryDto) {
const _filterDto = { const _filterDto = {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
@@ -30,12 +32,14 @@ export class GetBankAccountsService {
...filterDTO, ...filterDTO,
}; };
// Parsees accounts list filter DTO. // Parsees accounts list filter DTO.
const filter = this.dynamicListService.parseStringifiedFilter(_filterDto); const filter = this.dynamicListService.parseStringifiedFilter<BankAccountsQueryDto>(
_filterDto,
);
// Dynamic list service. // Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList( const dynamicList = await this.dynamicListService.dynamicList(
this.accountModel(), this.accountModel(),
filter, filter as IDynamicListFilter,
); );
// Retrieve accounts model based on the given query. // Retrieve accounts model based on the given query.
const accounts = await this.accountModel() const accounts = await this.accountModel()

View File

@@ -1,5 +1,5 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { BankingMatchingApplication } from './BankingMatchingApplication'; import { BankingMatchingApplication } from './BankingMatchingApplication';
import { GetMatchedTransactionsFilter } from './types'; import { GetMatchedTransactionsFilter } from './types';
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto'; import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
@@ -34,7 +34,7 @@ export class BankingMatchingController {
); );
} }
@Post('/unmatch/:uncategorizedTransactionId') @Patch('/unmatch/:uncategorizedTransactionId')
@ApiOperation({ summary: 'Unmatch the given uncategorized transaction.' }) @ApiOperation({ summary: 'Unmatch the given uncategorized transaction.' })
async unmatchMatchedTransaction( async unmatchMatchedTransaction(
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number, @Param('uncategorizedTransactionId') uncategorizedTransactionId: number,

View File

@@ -1,3 +1,5 @@
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from '@nestjs/bullmq';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { SocketModule } from '../Socket/Socket.module'; import { SocketModule } from '../Socket/Socket.module';
@@ -33,6 +35,10 @@ const models = [RegisterTenancyModel(PlaidItem)];
BankingCategorizeModule, BankingCategorizeModule,
BankingTransactionsModule, BankingTransactionsModule,
BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }), BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }),
BullBoardModule.forFeature({
name: UpdateBankingPlaidTransitionsQueueJob,
adapter: BullMQAdapter,
}),
...models, ...models,
], ],
providers: [ providers: [
@@ -51,4 +57,4 @@ const models = [RegisterTenancyModel(PlaidItem)];
exports: [...models], exports: [...models],
controllers: [BankingPlaidController, BankingPlaidWebhooksController], controllers: [BankingPlaidController, BankingPlaidWebhooksController],
}) })
export class BankingPlaidModule { } export class BankingPlaidModule {}

View File

@@ -1,11 +1,9 @@
import { Process } from '@nestjs/bull';
import { UseCls } from 'nestjs-cls'; import { UseCls } from 'nestjs-cls';
import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { Job } from 'bullmq'; import { Job } from 'bullmq';
import { import {
PlaidFetchTransitonsEventPayload, PlaidFetchTransitonsEventPayload,
UpdateBankingPlaidTransitionsJob,
UpdateBankingPlaidTransitionsQueueJob, UpdateBankingPlaidTransitionsQueueJob,
} from '../types/BankingPlaid.types'; } from '../types/BankingPlaid.types';
import { PlaidUpdateTransactions } from '../command/PlaidUpdateTransactions'; import { PlaidUpdateTransactions } from '../command/PlaidUpdateTransactions';
@@ -28,7 +26,6 @@ export class PlaidFetchTransactionsProcessor extends WorkerHost {
/** /**
* Triggers the function. * Triggers the function.
*/ */
@Process(UpdateBankingPlaidTransitionsJob)
@UseCls() @UseCls()
async process(job: Job<PlaidFetchTransitonsEventPayload>) { async process(job: Job<PlaidFetchTransitonsEventPayload>) {
const { plaidItemId } = job.data; const { plaidItemId } = job.data;

View File

@@ -10,6 +10,8 @@ import { BankingRecognizedTransactionsController } from './BankingRecognizedTran
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application'; import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions'; import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service'; import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq'; import { BullModule } from '@nestjs/bullmq';
import { RecognizeUncategorizedTransactionsQueue } from './_types'; import { RecognizeUncategorizedTransactionsQueue } from './_types';
import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob'; import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob';
@@ -25,6 +27,10 @@ const models = [RegisterTenancyModel(RecognizedBankTransaction)];
BullModule.registerQueue({ BullModule.registerQueue({
name: RecognizeUncategorizedTransactionsQueue, name: RecognizeUncategorizedTransactionsQueue,
}), }),
BullBoardModule.forFeature({
name: RecognizeUncategorizedTransactionsQueue,
adapter: BullMQAdapter,
}),
...models, ...models,
], ],
providers: [ providers: [

View File

@@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob =
export const RecognizeUncategorizedTransactionsQueue = export const RecognizeUncategorizedTransactionsQueue =
'recognize-uncategorized-transactions-queue'; 'recognize-uncategorized-transactions-queue';
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload { export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
ruleId: number, ruleId: number,
transactionsCriteria: any; transactionsCriteria?: RecognizeTransactionsCriteria;
/**
* When true, first reverts recognized transactions before recognizing again.
* Used when a bank rule is edited to ensure transactions previously recognized
* by lower-priority rules are re-evaluated against the updated rule.
*/
shouldRevert?: boolean;
} }

View File

@@ -93,6 +93,10 @@ export class RecognizeTranasctionsService {
q.whereIn('id', rulesIds); q.whereIn('id', rulesIds);
} }
q.withGraphFetched('conditions'); q.withGraphFetched('conditions');
// Order by the 'order' field to ensure higher priority rules (lower order values)
// are matched first.
q.orderBy('order', 'asc');
}); });
const bankRulesByAccountId = transformToMapBy( const bankRulesByAccountId = transformToMapBy(

View File

@@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber {
const tenantPayload = await this.tenancyContect.getTenantJobPayload(); const tenantPayload = await this.tenancyContect.getTenantJobPayload();
const payload = { const payload = {
ruleId: bankRule.id, ruleId: bankRule.id,
shouldRevert: true,
...tenantPayload, ...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload; } as RecognizeUncategorizedTransactionsJobPayload;
// Re-recognize the transactions based on the new rules. // Re-recognize the transactions based on the new rules.
// Setting shouldRevert to true ensures that transactions previously recognized
// by this or lower-priority rules are re-evaluated against the updated rule.
await this.recognizeTransactionsQueue.add( await this.recognizeTransactionsQueue.add(
RecognizeUncategorizedTransactionsJob, RecognizeUncategorizedTransactionsJob,
payload, payload,

View File

@@ -3,11 +3,11 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls'; import { ClsService, UseCls } from 'nestjs-cls';
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service'; import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service';
import { import {
RecognizeUncategorizedTransactionsJobPayload, RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue, RecognizeUncategorizedTransactionsQueue,
} from '../_types'; } from '../_types';
import { Process } from '@nestjs/bull';
@Processor({ @Processor({
name: RecognizeUncategorizedTransactionsQueue, name: RecognizeUncategorizedTransactionsQueue,
@@ -16,10 +16,12 @@ import { Process } from '@nestjs/bull';
export class RegonizeTransactionsPrcessor extends WorkerHost { export class RegonizeTransactionsPrcessor extends WorkerHost {
/** /**
* @param {RecognizeTranasctionsService} recognizeTranasctionsService - * @param {RecognizeTranasctionsService} recognizeTranasctionsService -
* @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService -
* @param {ClsService} clsService - * @param {ClsService} clsService -
*/ */
constructor( constructor(
private readonly recognizeTranasctionsService: RecognizeTranasctionsService, private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
private readonly clsService: ClsService, private readonly clsService: ClsService,
) { ) {
super(); super();
@@ -28,15 +30,23 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
/** /**
* Triggers sending invoice mail. * Triggers sending invoice mail.
*/ */
@Process(RecognizeUncategorizedTransactionsQueue)
@UseCls() @UseCls()
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) { async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
const { ruleId, transactionsCriteria } = job.data; const { ruleId, transactionsCriteria, shouldRevert } = job.data;
this.clsService.set('organizationId', job.data.organizationId); this.clsService.set('organizationId', job.data.organizationId);
this.clsService.set('userId', job.data.userId); this.clsService.set('userId', job.data.userId);
try { try {
// If shouldRevert is true, first revert recognized transactions before re-recognizing.
// This is used when a bank rule is edited to ensure transactions previously recognized
// by lower-priority rules are re-evaluated against the updated rule.
if (shouldRevert) {
await this.revertRecognizedTransactionsService.revertRecognizedTransactions(
ruleId,
transactionsCriteria,
);
}
await this.recognizeTranasctionsService.recognizeTransactions( await this.recognizeTranasctionsService.recognizeTransactions(
ruleId, ruleId,
transactionsCriteria, transactionsCriteria,

View File

@@ -27,7 +27,7 @@ export enum CASHFLOW_DIRECTION {
} }
export enum CASHFLOW_TRANSACTION_TYPE { export enum CASHFLOW_TRANSACTION_TYPE {
ONWERS_DRAWING = 'OwnerDrawing', OWNERS_DRAWING = 'OwnerDrawing',
OWNER_CONTRIBUTION = 'OwnerContribution', OWNER_CONTRIBUTION = 'OwnerContribution',
OTHER_INCOME = 'OtherIncome', OTHER_INCOME = 'OtherIncome',
TRANSFER_FROM_ACCOUNT = 'TransferFromAccount', TRANSFER_FROM_ACCOUNT = 'TransferFromAccount',
@@ -36,7 +36,7 @@ export enum CASHFLOW_TRANSACTION_TYPE {
} }
export const CASHFLOW_TRANSACTION_TYPE_META = { export const CASHFLOW_TRANSACTION_TYPE_META = {
[`${CASHFLOW_TRANSACTION_TYPE.ONWERS_DRAWING}`]: { [`${CASHFLOW_TRANSACTION_TYPE.OWNERS_DRAWING}`]: {
type: 'OwnerDrawing', type: 'OwnerDrawing',
direction: CASHFLOW_DIRECTION.OUT, direction: CASHFLOW_DIRECTION.OUT,
creditType: [ACCOUNT_TYPE.EQUITY], creditType: [ACCOUNT_TYPE.EQUITY],

View File

@@ -4,8 +4,8 @@ import {
validateTransactionShouldBeExcluded, validateTransactionShouldBeExcluded,
} from './utils'; } from './utils';
import { import {
IBankTransactionExcludedEventPayload, IBankTransactionUnexcludedEventPayload,
IBankTransactionExcludingEventPayload, IBankTransactionUnexcludingEventPayload,
} from '../types/BankTransactionsExclude.types'; } from '../types/BankTransactionsExclude.types';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
@@ -24,7 +24,7 @@ export class UnexcludeBankTransactionService {
private readonly uncategorizedBankTransactionModel: TenantModelProxy< private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction typeof UncategorizedBankTransaction
>, >,
) {} ) { }
/** /**
* Marks the given bank transaction as excluded. * Marks the given bank transaction as excluded.
@@ -50,7 +50,8 @@ export class UnexcludeBankTransactionService {
return this.uow.withTransaction(async (trx: Knex.Transaction) => { return this.uow.withTransaction(async (trx: Knex.Transaction) => {
await this.eventEmitter.emitAsync(events.bankTransactions.onUnexcluding, { await this.eventEmitter.emitAsync(events.bankTransactions.onUnexcluding, {
uncategorizedTransactionId, uncategorizedTransactionId,
} as IBankTransactionExcludingEventPayload); trx,
} as IBankTransactionUnexcludingEventPayload);
await this.uncategorizedBankTransactionModel() await this.uncategorizedBankTransactionModel()
.query(trx) .query(trx)
@@ -61,7 +62,8 @@ export class UnexcludeBankTransactionService {
await this.eventEmitter.emitAsync(events.bankTransactions.onUnexcluded, { await this.eventEmitter.emitAsync(events.bankTransactions.onUnexcluded, {
uncategorizedTransactionId, uncategorizedTransactionId,
} as IBankTransactionExcludedEventPayload); trx,
} as IBankTransactionUnexcludedEventPayload);
}); });
} }
} }

View File

@@ -19,7 +19,7 @@ export class DecrementUncategorizedTransactionOnExclude {
private readonly uncategorizedBankTransaction: TenantModelProxy< private readonly uncategorizedBankTransaction: TenantModelProxy<
typeof UncategorizedBankTransaction typeof UncategorizedBankTransaction
>, >,
) {} ) { }
/** /**
* Validates the cashflow transaction whether matched with bank transaction on deleting. * Validates the cashflow transaction whether matched with bank transaction on deleting.
@@ -50,7 +50,7 @@ export class DecrementUncategorizedTransactionOnExclude {
trx, trx,
}: IBankTransactionUnexcludedEventPayload) { }: IBankTransactionUnexcludedEventPayload) {
const transaction = await this.uncategorizedBankTransaction() const transaction = await this.uncategorizedBankTransaction()
.query() .query(trx)
.findById(uncategorizedTransactionId); .findById(uncategorizedTransactionId);
// //
await this.account() await this.account()

View File

@@ -2,9 +2,11 @@ import { forwardRef, Module } from '@nestjs/common';
import { TransactionLandedCostEntriesService } from './TransactionLandedCostEntries.service'; import { TransactionLandedCostEntriesService } from './TransactionLandedCostEntries.service';
import { AllocateLandedCostService } from './commands/AllocateLandedCost.service'; import { AllocateLandedCostService } from './commands/AllocateLandedCost.service';
import { LandedCostGLEntriesSubscriber } from './commands/LandedCostGLEntries.subscriber'; import { LandedCostGLEntriesSubscriber } from './commands/LandedCostGLEntries.subscriber';
// import { LandedCostGLEntries } from './commands/LandedCostGLEntries.service'; import { LandedCostGLEntriesService } from './commands/LandedCostGLEntries.service';
import { LandedCostSyncCostTransactions } from './commands/LandedCostSyncCostTransactions.service'; import { LandedCostSyncCostTransactions } from './commands/LandedCostSyncCostTransactions.service';
import { LedgerModule } from '../Ledger/Ledger.module';
import { LandedCostSyncCostTransactionsSubscriber } from './commands/LandedCostSyncCostTransactions.subscriber'; import { LandedCostSyncCostTransactionsSubscriber } from './commands/LandedCostSyncCostTransactions.subscriber';
import { LandedCostInventoryTransactionsSubscriber } from './commands/LandedCostInventoryTransactions.subscriber';
import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service'; import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service';
import { BillAllocateLandedCostController } from './LandedCost.controller'; import { BillAllocateLandedCostController } from './LandedCost.controller';
import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service'; import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service';
@@ -16,12 +18,12 @@ import { ExpenseLandedCost } from './commands/ExpenseLandedCost.service';
import { BillLandedCost } from './commands/BillLandedCost.service'; import { BillLandedCost } from './commands/BillLandedCost.service';
@Module({ @Module({
imports: [forwardRef(() => InventoryCostModule)], imports: [forwardRef(() => InventoryCostModule), LedgerModule],
providers: [ providers: [
AllocateLandedCostService, AllocateLandedCostService,
TransactionLandedCostEntriesService, TransactionLandedCostEntriesService,
BillAllocatedLandedCostTransactions, BillAllocatedLandedCostTransactions,
LandedCostGLEntriesSubscriber, LandedCostGLEntriesService,
TransactionLandedCost, TransactionLandedCost,
BillLandedCost, BillLandedCost,
ExpenseLandedCost, ExpenseLandedCost,
@@ -29,6 +31,8 @@ import { BillLandedCost } from './commands/BillLandedCost.service';
RevertAllocatedLandedCost, RevertAllocatedLandedCost,
LandedCostInventoryTransactions, LandedCostInventoryTransactions,
LandedCostTranasctions, LandedCostTranasctions,
LandedCostGLEntriesSubscriber,
LandedCostInventoryTransactionsSubscriber,
LandedCostSyncCostTransactionsSubscriber, LandedCostSyncCostTransactionsSubscriber,
], ],
exports: [TransactionLandedCostEntriesService], exports: [TransactionLandedCostEntriesService],

View File

@@ -25,7 +25,7 @@ export class BillAllocateLandedCostController {
private billAllocatedCostTransactions: BillAllocatedLandedCostTransactions, private billAllocatedCostTransactions: BillAllocatedLandedCostTransactions,
private revertAllocatedLandedCost: RevertAllocatedLandedCost, private revertAllocatedLandedCost: RevertAllocatedLandedCost,
private landedCostTransactions: LandedCostTranasctions, private landedCostTransactions: LandedCostTranasctions,
) {} ) { }
@Get('/transactions') @Get('/transactions')
@ApiOperation({ summary: 'Get landed cost transactions' }) @ApiOperation({ summary: 'Get landed cost transactions' })

View File

@@ -23,7 +23,9 @@ export class AllocateLandedCostService extends BaseLandedCostService {
private readonly billModel: TenantModelProxy<typeof Bill>, private readonly billModel: TenantModelProxy<typeof Bill>,
@Inject(BillLandedCost.name) @Inject(BillLandedCost.name)
protected readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost> protected readonly billLandedCostModel: TenantModelProxy<
typeof BillLandedCost
>,
) { ) {
super(); super();
} }
@@ -54,7 +56,8 @@ export class AllocateLandedCostService extends BaseLandedCostService {
const amount = this.getAllocateItemsCostTotal(allocateCostDTO); const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
// Retrieve the purchase invoice or throw not found error. // Retrieve the purchase invoice or throw not found error.
const bill = await this.billModel().query() const bill = await this.billModel()
.query()
.findById(billId) .findById(billId)
.withGraphFetched('entries') .withGraphFetched('entries')
.throwIfNotFound(); .throwIfNotFound();
@@ -89,8 +92,9 @@ export class AllocateLandedCostService extends BaseLandedCostService {
// unit-of-work eniverment. // unit-of-work eniverment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => { return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Save the bill landed cost model. // Save the bill landed cost model.
const billLandedCost = const billLandedCost = await this.billLandedCostModel()
await BillLandedCost.query(trx).insertGraph(billLandedCostObj); .query(trx)
.insertGraph(billLandedCostObj);
// Triggers `onBillLandedCostCreated` event. // Triggers `onBillLandedCostCreated` event.
await this.eventPublisher.emitAsync(events.billLandedCost.onCreated, { await this.eventPublisher.emitAsync(events.billLandedCost.onCreated, {
bill, bill,
@@ -103,5 +107,5 @@ export class AllocateLandedCostService extends BaseLandedCostService {
return billLandedCost; return billLandedCost;
}); });
}; }
} }

View File

@@ -21,7 +21,7 @@ export class BillAllocatedLandedCostTransactions {
private readonly billLandedCostModel: TenantModelProxy< private readonly billLandedCostModel: TenantModelProxy<
typeof BillLandedCost typeof BillLandedCost
>, >,
) {} ) { }
/** /**
* Retrieve the bill associated landed cost transactions. * Retrieve the bill associated landed cost transactions.
@@ -77,6 +77,13 @@ export class BillAllocatedLandedCostTransactions {
transaction.fromTransactionType, transaction.fromTransactionType,
transaction, transaction,
); );
const allocationMethodFormattedKey = transaction.allocationMethodFormatted;
const allocationMethodFormatted = allocationMethodFormattedKey
? this.i18nService.t(allocationMethodFormattedKey, {
defaultValue: allocationMethodFormattedKey,
})
: '';
return { return {
formattedAmount: formatNumber(transaction.amount, { formattedAmount: formatNumber(transaction.amount, {
currencyCode: transaction.currencyCode, currencyCode: transaction.currencyCode,
@@ -84,12 +91,14 @@ export class BillAllocatedLandedCostTransactions {
...omit(transaction, [ ...omit(transaction, [
'allocatedFromBillEntry', 'allocatedFromBillEntry',
'allocatedFromExpenseEntry', 'allocatedFromExpenseEntry',
'allocationMethodFormatted',
]), ]),
name, name,
description, description,
formattedLocalAmount: formatNumber(transaction.localAmount, { formattedLocalAmount: formatNumber(transaction.localAmount, {
currencyCode: 'USD', currencyCode: 'USD',
}), }),
allocationMethodFormatted,
}; };
}; };

View File

@@ -1,236 +1,188 @@
// import * as R from 'ramda'; import { Knex } from 'knex';
// import { Knex } from 'knex'; import { Inject, Injectable } from '@nestjs/common';
// import { Inject, Injectable } from '@nestjs/common'; import * as moment from 'moment';
// import { BaseLandedCostService } from '../BaseLandedCost.service'; import { BaseLandedCostService } from '../BaseLandedCost.service';
// import { BillLandedCost } from '../models/BillLandedCost'; import { BillLandedCost } from '../models/BillLandedCost';
// import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { Bill } from '@/modules/Bills/models/Bill';
// import { Bill } from '@/modules/Bills/models/Bill'; import { BillLandedCostEntry } from '../models/BillLandedCostEntry';
// import { BillLandedCostEntry } from '../models/BillLandedCostEntry'; import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
// import { ILedger, ILedgerEntry } from '@/modules/Ledger/types/Ledger.types'; import { Ledger } from '@/modules/Ledger/Ledger';
// import { Ledger } from '@/modules/Ledger/Ledger'; import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
// import { AccountNormal } from '@/interfaces/Account'; import { AccountNormal } from '@/modules/Accounts/Accounts.types';
// import { ILandedCostTransactionEntry } from '../types/BillLandedCosts.types'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
// @Injectable() @Injectable()
// export class LandedCostGLEntries extends BaseLandedCostService { export class LandedCostGLEntriesService extends BaseLandedCostService {
// constructor( constructor(
// private readonly journalService: JournalPosterService, private readonly ledgerStorage: LedgerStorageService,
// private readonly ledgerRepository: LedgerRepository,
// @Inject(BillLandedCost.name) @Inject(BillLandedCost.name)
// private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>, protected readonly billLandedCostModel: TenantModelProxy<
// ) { typeof BillLandedCost
// super(); >,
// } ) {
super();
}
// /** /**
// * Retrieves the landed cost GL common entry. * Retrieves the landed cost GL common entry.
// * @param {IBill} bill */
// * @param {IBillLandedCost} allocatedLandedCost private getLandedCostGLCommonEntry(
// * @returns bill: Bill,
// */ allocatedLandedCost: BillLandedCost,
// private getLandedCostGLCommonEntry = ( ) {
// bill: Bill, return {
// allocatedLandedCost: BillLandedCost date: moment(bill.billDate).format('YYYY-MM-DD'),
// ) => { currencyCode: allocatedLandedCost.currencyCode,
// return { exchangeRate: allocatedLandedCost.exchangeRate,
// date: bill.billDate,
// currencyCode: allocatedLandedCost.currencyCode,
// exchangeRate: allocatedLandedCost.exchangeRate,
// transactionType: 'LandedCost', transactionType: 'LandedCost',
// transactionId: allocatedLandedCost.id, transactionId: allocatedLandedCost.id,
// transactionNumber: bill.billNumber, transactionNumber: bill.billNumber,
// referenceNumber: bill.referenceNo, referenceNumber: bill.referenceNo,
// credit: 0, branchId: bill.branchId,
// debit: 0, projectId: bill.projectId,
// };
// };
// /** credit: 0,
// * Retrieves the landed cost GL inventory entry. debit: 0,
// * @param {IBill} bill };
// * @param {IBillLandedCost} allocatedLandedCost }
// * @param {IBillLandedCostEntry} allocatedEntry
// * @returns {ILedgerEntry}
// */
// private getLandedCostGLInventoryEntry = (
// bill: Bill,
// allocatedLandedCost: BillLandedCost,
// allocatedEntry: BillLandedCostEntry
// ): ILedgerEntry => {
// const commonEntry = this.getLandedCostGLCommonEntry(
// bill,
// allocatedLandedCost
// );
// return {
// ...commonEntry,
// debit: allocatedLandedCost.localAmount,
// accountId: allocatedEntry.itemEntry.item.inventoryAccountId,
// index: 1,
// accountNormal: AccountNormal.DEBIT,
// };
// };
// /** /**
// * Retrieves the landed cost GL cost entry. * Retrieves the landed cost GL inventory entry for an allocated item.
// * @param {IBill} bill */
// * @param {IBillLandedCost} allocatedLandedCost private getLandedCostGLInventoryEntry(
// * @param {ILandedCostTransactionEntry} fromTransactionEntry bill: Bill,
// * @returns {ILedgerEntry} allocatedLandedCost: BillLandedCost,
// */ allocatedEntry: BillLandedCostEntry,
// private getLandedCostGLCostEntry = ( index: number,
// bill: Bill, ): ILedgerEntry {
// allocatedLandedCost: BillLandedCost, const commonEntry = this.getLandedCostGLCommonEntry(
// fromTransactionEntry: ILandedCostTransactionEntry bill,
// ): ILedgerEntry => { allocatedLandedCost,
// const commonEntry = this.getLandedCostGLCommonEntry( );
// bill, const itemEntry = (
// allocatedLandedCost allocatedEntry as BillLandedCostEntry & {
// ); itemEntry?: {
// return { item?: { type?: string; inventoryAccountId?: number };
// ...commonEntry, costAccountId?: number;
// credit: allocatedLandedCost.localAmount, itemId?: number;
// accountId: fromTransactionEntry.costAccountId, };
// index: 2, }
// accountNormal: AccountNormal.CREDIT, ).itemEntry;
// }; const item = itemEntry?.item;
// }; const isInventory = item && ['inventory'].indexOf(item.type) !== -1;
const accountId = isInventory
? item?.inventoryAccountId
: itemEntry?.costAccountId;
// /** if (!accountId) {
// * Retrieve allocated landed cost entry GL entries. throw new Error(
// * @param {IBill} bill `Cannot determine GL account for landed cost allocate entry (entryId: ${allocatedEntry.entryId})`,
// * @param {IBillLandedCost} allocatedLandedCost );
// * @param {ILandedCostTransactionEntry} fromTransactionEntry }
// * @param {IBillLandedCostEntry} allocatedEntry
// * @returns {ILedgerEntry}
// */
// private getLandedCostGLAllocateEntry = R.curry(
// (
// bill: Bill,
// allocatedLandedCost: BillLandedCost,
// fromTransactionEntry: ILandedCostTransactionEntry,
// allocatedEntry: BillLandedCostEntry
// ): ILedgerEntry[] => {
// const inventoryEntry = this.getLandedCostGLInventoryEntry(
// bill,
// allocatedLandedCost,
// allocatedEntry
// );
// const costEntry = this.getLandedCostGLCostEntry(
// bill,
// allocatedLandedCost,
// fromTransactionEntry
// );
// return [inventoryEntry, costEntry];
// }
// );
// /** const localAmount =
// * Compose the landed cost GL entries. allocatedEntry.cost * (allocatedLandedCost.exchangeRate || 1);
// * @param {BillLandedCost} allocatedLandedCost
// * @param {Bill} bill
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @returns {ILedgerEntry[]}
// */
// public getLandedCostGLEntries = (
// allocatedLandedCost: BillLandedCost,
// bill: Bill,
// fromTransactionEntry: ILandedCostTransactionEntry
// ): ILedgerEntry[] => {
// const getEntry = this.getLandedCostGLAllocateEntry(
// bill,
// allocatedLandedCost,
// fromTransactionEntry
// );
// return allocatedLandedCost.allocateEntries.map(getEntry).flat();
// };
// /** return {
// * Retrieves the landed cost GL ledger. ...commonEntry,
// * @param {BillLandedCost} allocatedLandedCost debit: localAmount,
// * @param {Bill} bill accountId,
// * @param {ILandedCostTransactionEntry} fromTransactionEntry index: index + 1,
// * @returns {ILedger} indexGroup: 10,
// */ itemId: itemEntry?.itemId,
// public getLandedCostLedger = ( accountNormal: AccountNormal.DEBIT,
// allocatedLandedCost: BillLandedCost, };
// bill: Bill, }
// fromTransactionEntry: ILandedCostTransactionEntry
// ): ILedger => {
// const entries = this.getLandedCostGLEntries(
// allocatedLandedCost,
// bill,
// fromTransactionEntry
// );
// return new Ledger(entries);
// };
// /** /**
// * Writes landed cost GL entries to the storage layer. * Retrieves the landed cost GL cost entry (credit to cost account).
// * @param {number} tenantId - */
// */ private getLandedCostGLCostEntry(
// public writeLandedCostGLEntries = async ( bill: Bill,
// allocatedLandedCost: BillLandedCost, allocatedLandedCost: BillLandedCost,
// bill: Bill, ): ILedgerEntry {
// fromTransactionEntry: ILandedCostTransactionEntry, const commonEntry = this.getLandedCostGLCommonEntry(
// trx?: Knex.Transaction bill,
// ) => { allocatedLandedCost,
// const ledgerEntries = this.getLandedCostGLEntries( );
// allocatedLandedCost,
// bill,
// fromTransactionEntry
// );
// await this.ledgerRepository.saveLedgerEntries(ledgerEntries, trx);
// };
// /** return {
// * Generates and writes GL entries of the given landed cost. ...commonEntry,
// * @param {number} billLandedCostId credit: allocatedLandedCost.localAmount,
// * @param {Knex.Transaction} trx accountId: allocatedLandedCost.costAccountId,
// */ index: 1,
// public createLandedCostGLEntries = async ( indexGroup: 20,
// billLandedCostId: number, accountNormal: AccountNormal.CREDIT,
// trx?: Knex.Transaction };
// ) => { }
// // Retrieve the bill landed cost transacion with associated
// // allocated entries and items.
// const allocatedLandedCost = await this.billLandedCostModel().query(trx)
// .findById(billLandedCostId)
// .withGraphFetched('bill')
// .withGraphFetched('allocateEntries.itemEntry.item');
// // Retrieve the allocated from transactione entry. /**
// const transactionEntry = await this.getLandedCostEntry( * Composes the landed cost GL entries.
// allocatedLandedCost.fromTransactionType, */
// allocatedLandedCost.fromTransactionId, public getLandedCostGLEntries(
// allocatedLandedCost.fromTransactionEntryId allocatedLandedCost: BillLandedCost,
// ); bill: Bill,
// // Writes the given landed cost GL entries to the storage layer. ): ILedgerEntry[] {
// await this.writeLandedCostGLEntries( const inventoryEntries = allocatedLandedCost.allocateEntries.map(
// allocatedLandedCost, (allocatedEntry, index) =>
// allocatedLandedCost.bill, this.getLandedCostGLInventoryEntry(
// transactionEntry, bill,
// trx allocatedLandedCost,
// ); allocatedEntry,
// }; index,
),
);
const costEntry = this.getLandedCostGLCostEntry(bill, allocatedLandedCost);
// /** return [...inventoryEntries, costEntry];
// * Reverts GL entries of the given allocated landed cost transaction. }
// * @param {number} tenantId
// * @param {number} landedCostId /**
// * @param {Knex.Transaction} trx * Retrieves the landed cost GL ledger.
// */ */
// public revertLandedCostGLEntries = async ( public getLandedCostLedger(
// landedCostId: number, allocatedLandedCost: BillLandedCost,
// trx: Knex.Transaction bill: Bill,
// ) => { ): Ledger {
// await this.journalService.revertJournalTransactions( const entries = this.getLandedCostGLEntries(allocatedLandedCost, bill);
// landedCostId, return new Ledger(entries);
// 'LandedCost', }
// trx
// ); /**
// }; * Generates and writes GL entries of the given landed cost.
// } */
public createLandedCostGLEntries = async (
billLandedCostId: number,
trx?: Knex.Transaction,
) => {
const allocatedLandedCost = await this.billLandedCostModel()
.query(trx)
.findById(billLandedCostId)
.withGraphFetched('bill')
.withGraphFetched('allocateEntries.itemEntry.item');
if (!allocatedLandedCost?.bill) {
throw new Error('BillLandedCost or associated Bill not found');
}
const ledger = this.getLandedCostLedger(
allocatedLandedCost,
allocatedLandedCost.bill,
);
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Reverts GL entries of the given allocated landed cost transaction.
*/
public revertLandedCostGLEntries = async (
landedCostId: number,
trx?: Knex.Transaction,
) => {
await this.ledgerStorage.deleteByReference(landedCostId, 'LandedCost', trx);
};
}

View File

@@ -3,14 +3,15 @@ import {
IAllocatedLandedCostDeletedPayload, IAllocatedLandedCostDeletedPayload,
} from '../types/BillLandedCosts.types'; } from '../types/BillLandedCosts.types';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
// import { LandedCostGLEntries } from './LandedCostGLEntries.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
import { LandedCostGLEntriesService } from './LandedCostGLEntries.service';
@Injectable() @Injectable()
export class LandedCostGLEntriesSubscriber { export class LandedCostGLEntriesSubscriber {
constructor() // private readonly billLandedCostGLEntries: LandedCostGLEntries, constructor(
{} private readonly landedCostGLEntries: LandedCostGLEntriesService,
) {}
/** /**
* Writes GL entries once landed cost transaction created. * Writes GL entries once landed cost transaction created.
@@ -21,10 +22,10 @@ export class LandedCostGLEntriesSubscriber {
billLandedCost, billLandedCost,
trx, trx,
}: IAllocatedLandedCostCreatedPayload) { }: IAllocatedLandedCostCreatedPayload) {
// await this.billLandedCostGLEntries.createLandedCostGLEntries( await this.landedCostGLEntries.createLandedCostGLEntries(
// billLandedCost.id, billLandedCost.id,
// trx trx,
// ); );
} }
/** /**
@@ -32,13 +33,13 @@ export class LandedCostGLEntriesSubscriber {
* @param {IAllocatedLandedCostDeletedPayload} payload - * @param {IAllocatedLandedCostDeletedPayload} payload -
*/ */
@OnEvent(events.billLandedCost.onDeleted) @OnEvent(events.billLandedCost.onDeleted)
async revertGLEnteriesOnceLandedCostDeleted({ async revertGLEntriesOnceLandedCostDeleted({
oldBillLandedCost, oldBillLandedCost,
trx, trx,
}: IAllocatedLandedCostDeletedPayload) { }: IAllocatedLandedCostDeletedPayload) {
// await this.billLandedCostGLEntries.revertLandedCostGLEntries( await this.landedCostGLEntries.revertLandedCostGLEntries(
// oldBillLandedCost.id, oldBillLandedCost.id,
// trx trx,
// ); );
} }
} }

View File

@@ -14,7 +14,7 @@ import { LandedCostTransactionsQueryDto } from '../dtos/LandedCostTransactionsQu
@Injectable() @Injectable()
export class LandedCostTranasctions { export class LandedCostTranasctions {
constructor(private readonly transactionLandedCost: TransactionLandedCost) {} constructor(private readonly transactionLandedCost: TransactionLandedCost) { }
/** /**
* Retrieve the landed costs based on the given query. * Retrieve the landed costs based on the given query.
@@ -45,8 +45,8 @@ export class LandedCostTranasctions {
)(transactionType); )(transactionType);
return pipe( return pipe(
this.transformLandedCostTransactions,
R.map(transformLandedCost), R.map(transformLandedCost),
this.transformLandedCostTransactions,
)(transactions); )(transactions);
}; };
@@ -90,7 +90,7 @@ export class LandedCostTranasctions {
const entries = R.map< const entries = R.map<
ILandedCostTransactionEntry, ILandedCostTransactionEntry,
ILandedCostTransactionEntryDOJO ILandedCostTransactionEntryDOJO
>(transformLandedCostEntry)(transaction.entries); >(transformLandedCostEntry)(transaction.entries ?? []);
return { return {
...transaction, ...transaction,

View File

@@ -4,7 +4,6 @@ import {
IsOptional, IsOptional,
IsArray, IsArray,
ValidateNested, ValidateNested,
IsDecimal,
IsString, IsString,
IsNumber, IsNumber,
} from 'class-validator'; } from 'class-validator';
@@ -17,8 +16,9 @@ export class AllocateBillLandedCostItemDto {
@ToNumber() @ToNumber()
entryId: number; entryId: number;
@IsDecimal() @IsNumber()
cost: string; // Use string for IsDecimal, or use @IsNumber() if you want a number @ToNumber()
cost: number;
} }
export class AllocateBillLandedCostDto { export class AllocateBillLandedCostDto {

View File

@@ -60,8 +60,8 @@ export class BillLandedCost extends BaseModel {
const allocationMethod = lowerCase(this.allocationMethod); const allocationMethod = lowerCase(this.allocationMethod);
const keyLabelsPairs = { const keyLabelsPairs = {
value: 'allocation_method.value.label', value: 'bill.allocation_method.value',
quantity: 'allocation_method.quantity.label', quantity: 'bill.allocation_method.quantity',
}; };
return keyLabelsPairs[allocationMethod] || ''; return keyLabelsPairs[allocationMethod] || '';
} }

View File

@@ -7,6 +7,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { BillPaymentsApplication } from './BillPaymentsApplication.service'; import { BillPaymentsApplication } from './BillPaymentsApplication.service';
import { import {
@@ -26,12 +27,18 @@ import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto'; import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto';
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto'; import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { IPaymentMadeAction } from './types/BillPayments.types';
@Controller('bill-payments') @Controller('bill-payments')
@ApiTags('Bill Payments') @ApiTags('Bill Payments')
@ApiExtraModels(BillPaymentResponseDto) @ApiExtraModels(BillPaymentResponseDto)
@ApiExtraModels(PaginatedResponseDto) @ApiExtraModels(PaginatedResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BillPaymentsController { export class BillPaymentsController {
constructor( constructor(
private billPaymentsApplication: BillPaymentsApplication, private billPaymentsApplication: BillPaymentsApplication,
@@ -39,12 +46,14 @@ export class BillPaymentsController {
) {} ) {}
@Post() @Post()
@RequirePermission(IPaymentMadeAction.Create, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Create a new bill payment.' }) @ApiOperation({ summary: 'Create a new bill payment.' })
public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) { public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) {
return this.billPaymentsApplication.createBillPayment(billPaymentDTO); return this.billPaymentsApplication.createBillPayment(billPaymentDTO);
} }
@Delete(':billPaymentId') @Delete(':billPaymentId')
@RequirePermission(IPaymentMadeAction.Delete, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Delete the given bill payment.' }) @ApiOperation({ summary: 'Delete the given bill payment.' })
@ApiParam({ @ApiParam({
name: 'billPaymentId', name: 'billPaymentId',
@@ -59,6 +68,7 @@ export class BillPaymentsController {
} }
@Put(':billPaymentId') @Put(':billPaymentId')
@RequirePermission(IPaymentMadeAction.Edit, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Edit the given bill payment.' }) @ApiOperation({ summary: 'Edit the given bill payment.' })
@ApiParam({ @ApiParam({
name: 'billPaymentId', name: 'billPaymentId',
@@ -77,6 +87,7 @@ export class BillPaymentsController {
} }
@Get('/new-page/entries') @Get('/new-page/entries')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ @ApiOperation({
summary: summary:
'Retrieves the payable entries of the new page once vendor be selected.', 'Retrieves the payable entries of the new page once vendor be selected.',
@@ -95,6 +106,7 @@ export class BillPaymentsController {
} }
@Get(':billPaymentId/bills') @Get(':billPaymentId/bills')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' }) @ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' })
@ApiParam({ @ApiParam({
name: 'billPaymentId', name: 'billPaymentId',
@@ -107,6 +119,7 @@ export class BillPaymentsController {
} }
@Get('/:billPaymentId/edit-page') @Get('/:billPaymentId/edit-page')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ @ApiOperation({
summary: 'Retrieves the edit page of the given bill payment.', summary: 'Retrieves the edit page of the given bill payment.',
}) })
@@ -126,6 +139,7 @@ export class BillPaymentsController {
} }
@Get(':billPaymentId') @Get(':billPaymentId')
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bill payment details.' }) @ApiOperation({ summary: 'Retrieves the bill payment details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -145,6 +159,7 @@ export class BillPaymentsController {
} }
@Get() @Get()
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
@ApiOperation({ summary: 'Retrieves the bill payments list.' }) @ApiOperation({ summary: 'Retrieves the bill payments list.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -11,10 +11,13 @@ import {
Post, Post,
Body, Body,
Put, Put,
Patch,
Param, Param,
Delete, Delete,
Get, Get,
Query, Query,
HttpCode,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { BillsApplication } from './Bills.application'; import { BillsApplication } from './Bills.application';
import { IBillsFilter } from './Bills.types'; import { IBillsFilter } from './Bills.types';
@@ -26,6 +29,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { BillAction } from './Bills.types';
@Controller('bills') @Controller('bills')
@ApiTags('Bills') @ApiTags('Bills')
@@ -33,13 +41,16 @@ import {
@ApiExtraModels(PaginatedResponseDto) @ApiExtraModels(PaginatedResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BillsController { export class BillsController {
constructor(private billsApplication: BillsApplication) { } constructor(private billsApplication: BillsApplication) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ @ApiOperation({
summary: 'Validate which bills can be deleted and return the results.', summary: 'Validate which bills can be deleted and return the results.',
}) })
@HttpCode(200)
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: description:
@@ -55,7 +66,9 @@ export class BillsController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ summary: 'Deletes multiple bills.' }) @ApiOperation({ summary: 'Deletes multiple bills.' })
@HttpCode(200)
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: 'Bills deleted successfully', description: 'Bills deleted successfully',
@@ -69,12 +82,14 @@ export class BillsController {
} }
@Post() @Post()
@RequirePermission(BillAction.Create, AbilitySubject.Bill)
@ApiOperation({ summary: 'Create a new bill.' }) @ApiOperation({ summary: 'Create a new bill.' })
createBill(@Body() billDTO: CreateBillDto) { createBill(@Body() billDTO: CreateBillDto) {
return this.billsApplication.createBill(billDTO); return this.billsApplication.createBill(billDTO);
} }
@Put(':id') @Put(':id')
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
@ApiOperation({ summary: 'Edit the given bill.' }) @ApiOperation({ summary: 'Edit the given bill.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -87,6 +102,7 @@ export class BillsController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ summary: 'Delete the given bill.' }) @ApiOperation({ summary: 'Delete the given bill.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -99,6 +115,7 @@ export class BillsController {
} }
@Get() @Get()
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the bills.' }) @ApiOperation({ summary: 'Retrieves the bills.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -128,6 +145,7 @@ export class BillsController {
} }
@Get(':id/payment-transactions') @Get(':id/payment-transactions')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ @ApiOperation({
summary: 'Retrieve the specific bill associated payment transactions.', summary: 'Retrieve the specific bill associated payment transactions.',
}) })
@@ -142,6 +160,7 @@ export class BillsController {
} }
@Get(':id') @Get(':id')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the bill details.' }) @ApiOperation({ summary: 'Retrieves the bill details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -160,7 +179,8 @@ export class BillsController {
return this.billsApplication.getBill(billId); return this.billsApplication.getBill(billId);
} }
@Post(':id/open') @Patch(':id/open')
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
@ApiOperation({ summary: 'Open the given bill.' }) @ApiOperation({ summary: 'Open the given bill.' })
@ApiParam({ @ApiParam({
name: 'id', name: 'id',
@@ -173,6 +193,7 @@ export class BillsController {
} }
@Get('due') @Get('due')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the due bills.' }) @ApiOperation({ summary: 'Retrieves the due bills.' })
getDueBills(@Body('vendorId') vendorId?: number) { getDueBills(@Body('vendorId') vendorId?: number) {
return this.billsApplication.getDueBills(vendorId); return this.billsApplication.getDueBills(vendorId);

View File

@@ -1,6 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto'; import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto'; import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
import { BranchResponseDto } from '@/modules/Branches/dtos/BranchResponse.dto';
import { DiscountType } from '@/common/types/Discount'; import { DiscountType } from '@/common/types/Discount';
export class BillResponseDto { export class BillResponseDto {
@@ -89,6 +91,14 @@ export class BillResponseDto {
}) })
branchId?: number; branchId?: number;
@ApiProperty({
description: 'Branch details',
type: () => BranchResponseDto,
required: false,
})
@Type(() => BranchResponseDto)
branch?: BranchResponseDto;
@ApiProperty({ @ApiProperty({
description: 'The ID of the project', description: 'The ID of the project',
example: 301, example: 301,

View File

@@ -51,6 +51,7 @@ export class Bill extends TenantBaseModel {
public updatedAt: Date | null; public updatedAt: Date | null;
public entries?: ItemEntry[]; public entries?: ItemEntry[];
public attachments!: Document[];
public locatedLandedCosts?: BillLandedCost[]; public locatedLandedCosts?: BillLandedCost[];
/** /**
* Timestamps columns. * Timestamps columns.
@@ -633,7 +634,7 @@ export class Bill extends TenantBaseModel {
return this.query(trx) return this.query(trx)
.where('id', billId) .where('id', billId)
[changeMethod]('payment_amount', Math.abs(amount)); [changeMethod]('payment_amount', Math.abs(amount));
} }
/** /**

View File

@@ -1,5 +1,7 @@
import { Transformer } from '@/modules/Transformer/Transformer'; import { Transformer } from '@/modules/Transformer/Transformer';
import { Bill } from '../models/Bill'; import { Bill } from '../models/Bill';
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer';
export class BillTransformer extends Transformer { export class BillTransformer extends Transformer {
/** /**
@@ -28,6 +30,7 @@ export class BillTransformer extends Transformer {
'taxes', 'taxes',
'entries', 'entries',
'attachments', 'attachments',
'branch',
]; ];
}; };
@@ -231,20 +234,18 @@ export class BillTransformer extends Transformer {
/** /**
* Retrieves the entries of the bill. * Retrieves the entries of the bill.
* @param {Bill} credit * @param {Bill} credit
* @returns {}
*/ */
// protected entries = (bill: Bill) => { protected entries = (bill: Bill) => {
// return this.item(bill.entries, new ItemEntryTransformer(), { return this.item(bill.entries, new ItemEntryTransformer(), {
// currencyCode: bill.currencyCode, currencyCode: bill.currencyCode,
// }); });
// }; };
/** /**
* Retrieves the bill attachments. * Retrieves the bill attachments.
* @param {ISaleInvoice} invoice * @param {Bill} bill
* @returns
*/ */
// protected attachments = (bill: Bill) => { protected attachments = (bill: Bill) => {
// return this.item(bill.attachments, new AttachmentTransformer()); return this.item(bill.attachments, new AttachmentTransformer());
// }; };
} }

View File

@@ -31,6 +31,12 @@ import { ValidateBranchExistance } from './integrations/ValidateBranchExistance'
import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator'; import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator';
import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches'; import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches';
import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches'; import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches';
import { BillActivateBranches } from './integrations/Purchases/BillBranchesActivate';
import { VendorCreditActivateBranches } from './integrations/Purchases/VendorCreditBranchesActivate';
import { BillPaymentsActivateBranches } from './integrations/Purchases/PaymentMadeBranchesActivate';
import { BillBranchesActivateSubscriber } from './subscribers/Activate/BillBranchesActivateSubscriber';
import { VendorCreditBranchesActivateSubscriber } from './subscribers/Activate/VendorCreditBranchesActivateSubscriber';
import { PaymentMadeActivateBranchesSubscriber } from './subscribers/Activate/PaymentMadeBranchesActivateSubscriber';
import { FeaturesModule } from '../Features/Features.module'; import { FeaturesModule } from '../Features/Features.module';
@Module({ @Module({
@@ -66,7 +72,13 @@ import { FeaturesModule } from '../Features/Features.module';
ValidateBranchExistance, ValidateBranchExistance,
ManualJournalBranchesValidator, ManualJournalBranchesValidator,
CashflowTransactionsActivateBranches, CashflowTransactionsActivateBranches,
ExpensesActivateBranches ExpensesActivateBranches,
BillActivateBranches,
VendorCreditActivateBranches,
BillPaymentsActivateBranches,
BillBranchesActivateSubscriber,
VendorCreditBranchesActivateSubscriber,
PaymentMadeActivateBranchesSubscriber
], ],
exports: [ exports: [
BranchesSettingsService, BranchesSettingsService,

View File

@@ -1,11 +1,14 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Bill } from '@/modules/Bills/models/Bill'; import { Bill } from '@/modules/Bills/models/Bill';
@Injectable() @Injectable()
export class BillActivateBranches { export class BillActivateBranches {
constructor(private readonly billModel: TenantModelProxy<typeof Bill>) {} constructor(
@Inject(Bill.name)
private readonly billModel: TenantModelProxy<typeof Bill>,
) {}
/** /**
* Updates all bills transactions with the primary branch. * Updates all bills transactions with the primary branch.
@@ -17,7 +20,7 @@ export class BillActivateBranches {
primaryBranchId: number, primaryBranchId: number,
trx?: Knex.Transaction, trx?: Knex.Transaction,
) => { ) => {
// Updates the sale invoice with primary branch. // Updates the bills with primary branch.
await Bill.query(trx).update({ branchId: primaryBranchId }); await this.billModel().query(trx).update({ branchId: primaryBranchId });
}; };
} }

View File

@@ -1,11 +1,12 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { BillPayment } from '@/modules/BillPayments/models/BillPayment'; import { BillPayment } from '@/modules/BillPayments/models/BillPayment';
import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class BillPaymentsActivateBranches { export class BillPaymentsActivateBranches {
constructor( constructor(
@Inject(BillPayment.name)
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>, private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
) {} ) {}

View File

@@ -1,11 +1,12 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit'; import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
@Injectable() @Injectable()
export class VendorCreditActivateBranches { export class VendorCreditActivateBranches {
constructor( constructor(
@Inject(VendorCredit.name)
private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>, private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>,
) {} ) {}

View File

@@ -0,0 +1,28 @@
import { IBranchesActivatedPayload } from '../../Branches.types';
import { events } from '@/common/events/events';
import { Injectable } from '@nestjs/common';
import { BillActivateBranches } from '../../integrations/Purchases/BillBranchesActivate';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class BillBranchesActivateSubscriber {
constructor(
private readonly billActivateBranches: BillActivateBranches,
) { }
/**
* Updates bills transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
@OnEvent(events.branch.onActivated)
async updateBillsWithBranchOnActivated({
primaryBranch,
trx,
}: IBranchesActivatedPayload) {
await this.billActivateBranches.updateBillsWithBranch(
primaryBranch.id,
trx,
);
}
}

View File

@@ -0,0 +1,28 @@
import { IBranchesActivatedPayload } from '../../Branches.types';
import { events } from '@/common/events/events';
import { Injectable } from '@nestjs/common';
import { VendorCreditActivateBranches } from '../../integrations/Purchases/VendorCreditBranchesActivate';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class VendorCreditBranchesActivateSubscriber {
constructor(
private readonly vendorCreditActivateBranches: VendorCreditActivateBranches,
) { }
/**
* Updates vendor credits transactions with the primary branch once
* the multi-branches is activated.
* @param {IBranchesActivatedPayload}
*/
@OnEvent(events.branch.onActivated)
async updateVendorCreditsWithBranchOnActivated({
primaryBranch,
trx,
}: IBranchesActivatedPayload) {
await this.vendorCreditActivateBranches.updateVendorCreditsWithBranch(
primaryBranch.id,
trx,
);
}
}

View File

@@ -17,7 +17,7 @@ export class BillBranchValidateSubscriber {
* Validate branch existance on bill creating. * Validate branch existance on bill creating.
* @param {IBillCreatingPayload} payload * @param {IBillCreatingPayload} payload
*/ */
@OnEvent(events.bill.onCreating) @OnEvent(events.bill.onCreating, { suppressErrors: false })
async validateBranchExistanceOnBillCreating({ async validateBranchExistanceOnBillCreating({
billDTO, billDTO,
}: IBillCreatingPayload) { }: IBillCreatingPayload) {
@@ -30,7 +30,7 @@ export class BillBranchValidateSubscriber {
* Validate branch existance once bill editing. * Validate branch existance once bill editing.
* @param {IBillEditingPayload} payload * @param {IBillEditingPayload} payload
*/ */
@OnEvent(events.bill.onEditing) @OnEvent(events.bill.onEditing, { suppressErrors: false })
async validateBranchExistanceOnBillEditing({ billDTO }: IBillEditingPayload) { async validateBranchExistanceOnBillEditing({ billDTO }: IBillEditingPayload) {
await this.validateBranchExistance.validateTransactionBranchWhenActive( await this.validateBranchExistance.validateTransactionBranchWhenActive(
billDTO.branchId, billDTO.branchId,

View File

@@ -14,7 +14,7 @@ export class CashflowBranchDTOValidatorSubscriber {
* Validate branch existance once cashflow transaction creating. * Validate branch existance once cashflow transaction creating.
* @param {ICommandCashflowCreatingPayload} payload * @param {ICommandCashflowCreatingPayload} payload
*/ */
@OnEvent(events.cashflow.onTransactionCreating) @OnEvent(events.cashflow.onTransactionCreating, { suppressErrors: false })
async validateBranchExistanceOnCashflowTransactionCreating({ async validateBranchExistanceOnCashflowTransactionCreating({
newTransactionDTO, newTransactionDTO,
}: ICommandCashflowCreatingPayload) { }: ICommandCashflowCreatingPayload) {

View File

@@ -15,13 +15,13 @@ import {
export class ContactBranchValidateSubscriber { export class ContactBranchValidateSubscriber {
constructor( constructor(
private readonly validateBranchExistance: ValidateBranchExistance, private readonly validateBranchExistance: ValidateBranchExistance,
) { } ) {}
/** /**
* Validate branch existance on customer creating. * Validate branch existance on customer creating.
* @param {ICustomerEventCreatingPayload} payload * @param {ICustomerEventCreatingPayload} payload
*/ */
@OnEvent(events.customers.onCreating) @OnEvent(events.customers.onCreating, { suppressErrors: false })
async validateBranchExistanceOnCustomerCreating({ async validateBranchExistanceOnCustomerCreating({
customerDTO, customerDTO,
}: ICustomerEventCreatingPayload) { }: ICustomerEventCreatingPayload) {
@@ -37,7 +37,7 @@ export class ContactBranchValidateSubscriber {
* Validate branch existance once customer opening balance editing. * Validate branch existance once customer opening balance editing.
* @param {ICustomerOpeningBalanceEditingPayload} payload * @param {ICustomerOpeningBalanceEditingPayload} payload
*/ */
@OnEvent(events.customers.onOpeningBalanceChanging) @OnEvent(events.customers.onOpeningBalanceChanging, { suppressErrors: false })
async validateBranchExistanceOnCustomerOpeningBalanceEditing({ async validateBranchExistanceOnCustomerOpeningBalanceEditing({
openingBalanceEditDTO, openingBalanceEditDTO,
}: ICustomerOpeningBalanceEditingPayload) { }: ICustomerOpeningBalanceEditingPayload) {
@@ -52,7 +52,7 @@ export class ContactBranchValidateSubscriber {
* Validates the branch existance on vendor creating. * Validates the branch existance on vendor creating.
* @param {IVendorEventCreatingPayload} payload * @param {IVendorEventCreatingPayload} payload
*/ */
@OnEvent(events.vendors.onCreating) @OnEvent(events.vendors.onCreating, { suppressErrors: false })
async validateBranchExistanceonVendorCreating({ async validateBranchExistanceonVendorCreating({
vendorDTO, vendorDTO,
}: IVendorEventCreatingPayload) { }: IVendorEventCreatingPayload) {
@@ -68,7 +68,7 @@ export class ContactBranchValidateSubscriber {
* Validate branch existance once the vendor opening balance editing. * Validate branch existance once the vendor opening balance editing.
* @param {IVendorOpeningBalanceEditingPayload} payload * @param {IVendorOpeningBalanceEditingPayload} payload
*/ */
@OnEvent(events.vendors.onOpeningBalanceChanging) @OnEvent(events.vendors.onOpeningBalanceChanging, { suppressErrors: false })
async validateBranchExistanceOnVendorOpeningBalanceEditing({ async validateBranchExistanceOnVendorOpeningBalanceEditing({
openingBalanceEditDTO, openingBalanceEditDTO,
}: IVendorOpeningBalanceEditingPayload) { }: IVendorOpeningBalanceEditingPayload) {

View File

@@ -15,7 +15,7 @@ export class CreditNoteBranchValidateSubscriber {
* Validate branch existance on credit note creating. * Validate branch existance on credit note creating.
* @param {ICreditNoteCreatingPayload} payload * @param {ICreditNoteCreatingPayload} payload
*/ */
@OnEvent(events.creditNote.onCreating) @OnEvent(events.creditNote.onCreating, { suppressErrors: false })
async validateBranchExistanceOnCreditCreating({ async validateBranchExistanceOnCreditCreating({
creditNoteDTO, creditNoteDTO,
}: ICreditNoteCreatingPayload) { }: ICreditNoteCreatingPayload) {
@@ -28,7 +28,7 @@ export class CreditNoteBranchValidateSubscriber {
* Validate branch existance once credit note editing. * Validate branch existance once credit note editing.
* @param {ICreditNoteEditingPayload} payload * @param {ICreditNoteEditingPayload} payload
*/ */
@OnEvent(events.creditNote.onEditing) @OnEvent(events.creditNote.onEditing, { suppressErrors: false })
async validateBranchExistanceOnCreditEditing({ async validateBranchExistanceOnCreditEditing({
creditNoteEditDTO, creditNoteEditDTO,
}: ICreditNoteEditingPayload) { }: ICreditNoteEditingPayload) {

View File

@@ -14,7 +14,7 @@ export class CreditNoteRefundBranchValidateSubscriber {
* Validate branch existance on refund credit note creating. * Validate branch existance on refund credit note creating.
* @param {IRefundCreditNoteCreatingPayload} payload * @param {IRefundCreditNoteCreatingPayload} payload
*/ */
@OnEvent(events.creditNote.onRefundCreating) @OnEvent(events.creditNote.onRefundCreating, { suppressErrors: false })
async validateBranchExistanceOnCreditRefundCreating({ async validateBranchExistanceOnCreditRefundCreating({
newCreditNoteDTO, newCreditNoteDTO,
}: IRefundCreditNoteCreatingPayload) { }: IRefundCreditNoteCreatingPayload) {

View File

@@ -16,7 +16,7 @@ export class ExpenseBranchValidateSubscriber {
* Validate branch existance once expense transaction creating. * Validate branch existance once expense transaction creating.
* @param {IExpenseCreatingPayload} payload * @param {IExpenseCreatingPayload} payload
*/ */
@OnEvent(events.expenses.onCreating) @OnEvent(events.expenses.onCreating, { suppressErrors: false })
async validateBranchExistanceOnExpenseCreating({ async validateBranchExistanceOnExpenseCreating({
expenseDTO, expenseDTO,
}: IExpenseCreatingPayload) { }: IExpenseCreatingPayload) {
@@ -29,7 +29,7 @@ export class ExpenseBranchValidateSubscriber {
* Validate branch existance once expense transaction editing. * Validate branch existance once expense transaction editing.
* @param {IExpenseEventEditingPayload} payload * @param {IExpenseEventEditingPayload} payload
*/ */
@OnEvent(events.expenses.onEditing) @OnEvent(events.expenses.onEditing, { suppressErrors: false })
async validateBranchExistanceOnExpenseEditing({ async validateBranchExistanceOnExpenseEditing({
expenseDTO, expenseDTO,
}: IExpenseEventEditingPayload) { }: IExpenseEventEditingPayload) {

View File

@@ -14,7 +14,7 @@ export class InventoryAdjustmentBranchValidateSubscriber {
* Validate branch existance on inventory adjustment creating. * Validate branch existance on inventory adjustment creating.
* @param {IInventoryAdjustmentCreatingPayload} payload * @param {IInventoryAdjustmentCreatingPayload} payload
*/ */
@OnEvent(events.inventoryAdjustment.onQuickCreating) @OnEvent(events.inventoryAdjustment.onQuickCreating, { suppressErrors: false })
async validateBranchExistanceOnInventoryCreating({ async validateBranchExistanceOnInventoryCreating({
quickAdjustmentDTO, quickAdjustmentDTO,
}: IInventoryAdjustmentCreatingPayload) { }: IInventoryAdjustmentCreatingPayload) {

View File

@@ -17,7 +17,7 @@ export class InvoiceBranchValidateSubscriber {
* Validate branch existance on invoice creating. * Validate branch existance on invoice creating.
* @param {ISaleInvoiceCreatingPayload} payload * @param {ISaleInvoiceCreatingPayload} payload
*/ */
@OnEvent(events.saleInvoice.onCreating) @OnEvent(events.saleInvoice.onCreating, { suppressErrors: false })
async validateBranchExistanceOnInvoiceCreating({ async validateBranchExistanceOnInvoiceCreating({
saleInvoiceDTO, saleInvoiceDTO,
}: ISaleInvoiceCreatingPaylaod) { }: ISaleInvoiceCreatingPaylaod) {
@@ -30,7 +30,7 @@ export class InvoiceBranchValidateSubscriber {
* Validate branch existance once invoice editing. * Validate branch existance once invoice editing.
* @param {ISaleInvoiceEditingPayload} payload * @param {ISaleInvoiceEditingPayload} payload
*/ */
@OnEvent(events.saleInvoice.onEditing) @OnEvent(events.saleInvoice.onEditing, { suppressErrors: false })
async validateBranchExistanceOnInvoiceEditing({ async validateBranchExistanceOnInvoiceEditing({
saleInvoiceDTO, saleInvoiceDTO,
}: ISaleInvoiceEditingPayload) { }: ISaleInvoiceEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class PaymentMadeBranchValidateSubscriber {
* Validate branch existance on estimate creating. * Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload * @param {ISaleEstimateCreatedPayload} payload
*/ */
@OnEvent(events.billPayment.onCreating) @OnEvent(events.billPayment.onCreating, { suppressErrors: false })
async validateBranchExistanceOnPaymentCreating({ async validateBranchExistanceOnPaymentCreating({
billPaymentDTO, billPaymentDTO,
}: IBillPaymentCreatingPayload) { }: IBillPaymentCreatingPayload) {
@@ -30,7 +30,7 @@ export class PaymentMadeBranchValidateSubscriber {
* Validate branch existance once estimate editing. * Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload * @param {ISaleEstimateEditingPayload} payload
*/ */
@OnEvent(events.billPayment.onEditing) @OnEvent(events.billPayment.onEditing, { suppressErrors: false })
async validateBranchExistanceOnPaymentEditing({ async validateBranchExistanceOnPaymentEditing({
billPaymentDTO, billPaymentDTO,
}: IBillPaymentEditingPayload) { }: IBillPaymentEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class PaymentReceiveBranchValidateSubscriber {
* Validate branch existance on estimate creating. * Validate branch existance on estimate creating.
* @param {IPaymentReceivedCreatingPayload} payload * @param {IPaymentReceivedCreatingPayload} payload
*/ */
@OnEvent(events.paymentReceive.onCreating) @OnEvent(events.paymentReceive.onCreating, { suppressErrors: false })
async validateBranchExistanceOnPaymentCreating({ async validateBranchExistanceOnPaymentCreating({
paymentReceiveDTO, paymentReceiveDTO,
}: IPaymentReceivedCreatingPayload) { }: IPaymentReceivedCreatingPayload) {
@@ -30,7 +30,7 @@ export class PaymentReceiveBranchValidateSubscriber {
* Validate branch existance once estimate editing. * Validate branch existance once estimate editing.
* @param {IPaymentReceivedEditingPayload} payload * @param {IPaymentReceivedEditingPayload} payload
*/ */
@OnEvent(events.paymentReceive.onEditing) @OnEvent(events.paymentReceive.onEditing, { suppressErrors: false })
async validateBranchExistanceOnPaymentEditing({ async validateBranchExistanceOnPaymentEditing({
paymentReceiveDTO, paymentReceiveDTO,
}: IPaymentReceivedEditingPayload) { }: IPaymentReceivedEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class SaleEstimateBranchValidateSubscriber {
* Validate branch existance on estimate creating. * Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload * @param {ISaleEstimateCreatedPayload} payload
*/ */
@OnEvent(events.saleEstimate.onCreating) @OnEvent(events.saleEstimate.onCreating, { suppressErrors: false })
async validateBranchExistanceOnEstimateCreating({ async validateBranchExistanceOnEstimateCreating({
estimateDTO, estimateDTO,
}: ISaleEstimateCreatingPayload) { }: ISaleEstimateCreatingPayload) {
@@ -30,7 +30,7 @@ export class SaleEstimateBranchValidateSubscriber {
* Validate branch existance once estimate editing. * Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload * @param {ISaleEstimateEditingPayload} payload
*/ */
@OnEvent(events.saleEstimate.onEditing) @OnEvent(events.saleEstimate.onEditing, { suppressErrors: false })
async validateBranchExistanceOnEstimateEditing({ async validateBranchExistanceOnEstimateEditing({
estimateDTO, estimateDTO,
}: ISaleEstimateEditingPayload) { }: ISaleEstimateEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class SaleReceiptBranchValidateSubscriber {
* Validate branch existance on estimate creating. * Validate branch existance on estimate creating.
* @param {ISaleReceiptCreatingPayload} payload * @param {ISaleReceiptCreatingPayload} payload
*/ */
@OnEvent(events.saleReceipt.onCreating) @OnEvent(events.saleReceipt.onCreating, { suppressErrors: false })
async validateBranchExistanceOnInvoiceCreating({ async validateBranchExistanceOnInvoiceCreating({
saleReceiptDTO, saleReceiptDTO,
}: ISaleReceiptCreatingPayload) { }: ISaleReceiptCreatingPayload) {
@@ -30,7 +30,7 @@ export class SaleReceiptBranchValidateSubscriber {
* Validate branch existance once estimate editing. * Validate branch existance once estimate editing.
* @param {ISaleReceiptEditingPayload} payload * @param {ISaleReceiptEditingPayload} payload
*/ */
@OnEvent(events.saleReceipt.onEditing) @OnEvent(events.saleReceipt.onEditing, { suppressErrors: false })
async validateBranchExistanceOnInvoiceEditing({ async validateBranchExistanceOnInvoiceEditing({
saleReceiptDTO, saleReceiptDTO,
}: ISaleReceiptEditingPayload) { }: ISaleReceiptEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class VendorCreditBranchValidateSubscriber {
* Validate branch existance on estimate creating. * Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload * @param {ISaleEstimateCreatedPayload} payload
*/ */
@OnEvent(events.vendorCredit.onCreating) @OnEvent(events.vendorCredit.onCreating, { suppressErrors: false })
async validateBranchExistanceOnCreditCreating({ async validateBranchExistanceOnCreditCreating({
vendorCreditCreateDTO, vendorCreditCreateDTO,
}: IVendorCreditCreatingPayload) { }: IVendorCreditCreatingPayload) {
@@ -30,7 +30,7 @@ export class VendorCreditBranchValidateSubscriber {
* Validate branch existance once estimate editing. * Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload * @param {ISaleEstimateEditingPayload} payload
*/ */
@OnEvent(events.vendorCredit.onEditing) @OnEvent(events.vendorCredit.onEditing, { suppressErrors: false })
async validateBranchExistanceOnCreditEditing({ async validateBranchExistanceOnCreditEditing({
vendorCreditDTO, vendorCreditDTO,
}: IVendorCreditEditingPayload) { }: IVendorCreditEditingPayload) {

View File

@@ -14,7 +14,7 @@ export class VendorCreditRefundBranchValidateSubscriber {
* Validate branch existance on refund credit note creating. * Validate branch existance on refund credit note creating.
* @param {IRefundVendorCreditCreatingPayload} payload * @param {IRefundVendorCreditCreatingPayload} payload
*/ */
@OnEvent(events.vendorCredit.onRefundCreating) @OnEvent(events.vendorCredit.onRefundCreating, { suppressErrors: false })
async validateBranchExistanceOnCreditRefundCreating({ async validateBranchExistanceOnCreditRefundCreating({
refundVendorCreditDTO, refundVendorCreditDTO,
}: IRefundVendorCreditCreatingPayload) { }: IRefundVendorCreditCreatingPayload) {

View File

@@ -22,6 +22,7 @@ export abstract class BaseCommand extends CommandRunner {
}, },
migrations: { migrations: {
directory: this.configService.get('systemDatabase.migrationDir'), directory: this.configService.get('systemDatabase.migrationDir'),
loadExtensions: ['.js'],
}, },
seeds: { seeds: {
directory: this.configService.get('systemDatabase.seedsDir'), directory: this.configService.get('systemDatabase.seedsDir'),
@@ -43,6 +44,7 @@ export abstract class BaseCommand extends CommandRunner {
}, },
migrations: { migrations: {
directory: this.configService.get('tenantDatabase.migrationsDir') || './src/database/migrations', directory: this.configService.get('tenantDatabase.migrationsDir') || './src/database/migrations',
loadExtensions: ['.js'],
}, },
seeds: { seeds: {
directory: this.configService.get('tenantDatabase.seedsDir') || './src/database/seeds/core', directory: this.configService.get('tenantDatabase.seedsDir') || './src/database/seeds/core',

View File

@@ -3,7 +3,7 @@ import {
Get, Get,
Query, Query,
Param, Param,
Post, Patch,
ParseIntPipe, ParseIntPipe,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger';
@@ -27,7 +27,7 @@ export class ContactsController {
return this.getAutoCompleteService.autocompleteContacts(query); return this.getAutoCompleteService.autocompleteContacts(query);
} }
@Post(':id/activate') @Patch(':id/activate')
@ApiOperation({ summary: 'Activate a contact' }) @ApiOperation({ summary: 'Activate a contact' })
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' }) @ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
async activateContact(@Param('id', ParseIntPipe) contactId: number) { async activateContact(@Param('id', ParseIntPipe) contactId: number) {
@@ -35,7 +35,7 @@ export class ContactsController {
return { id: contactId, activated: true }; return { id: contactId, activated: true };
} }
@Post(':id/inactivate') @Patch(':id/inactivate')
@ApiOperation({ summary: 'Inactivate a contact' }) @ApiOperation({ summary: 'Inactivate a contact' })
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' }) @ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
async inactivateContact(@Param('id', ParseIntPipe) contactId: number) { async inactivateContact(@Param('id', ParseIntPipe) contactId: number) {

View File

@@ -1,20 +1,35 @@
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'; import {
Body,
Controller,
Delete,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { ICreditNoteRefundDTO } from '../CreditNotes/types/CreditNotes.types'; import { ICreditNoteRefundDTO } from '../CreditNotes/types/CreditNotes.types';
import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service'; import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service';
import { RefundCreditNote } from './models/RefundCreditNote'; import { RefundCreditNote } from './models/RefundCreditNote';
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto'; import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
@Controller('credit-notes') @Controller('credit-notes')
@ApiTags('Credit Note Refunds') @ApiTags('Credit Note Refunds')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNoteRefundsController { export class CreditNoteRefundsController {
constructor( constructor(
private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication, private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication,
) {} ) {}
@Get(':creditNoteId/refunds') @Get(':creditNoteId/refunds')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Retrieve the credit note graph.' }) @ApiOperation({ summary: 'Retrieve the credit note graph.' })
getCreditNoteRefunds(@Param('creditNoteId') creditNoteId: number) { getCreditNoteRefunds(@Param('creditNoteId') creditNoteId: number) {
return this.creditNotesRefundsApplication.getCreditNoteRefunds( return this.creditNotesRefundsApplication.getCreditNoteRefunds(
@@ -29,6 +44,7 @@ export class CreditNoteRefundsController {
* @returns {Promise<RefundCreditNote>} * @returns {Promise<RefundCreditNote>}
*/ */
@Post(':creditNoteId/refunds') @Post(':creditNoteId/refunds')
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Create a refund for the given credit note.' }) @ApiOperation({ summary: 'Create a refund for the given credit note.' })
createRefundCreditNote( createRefundCreditNote(
@Param('creditNoteId') creditNoteId: number, @Param('creditNoteId') creditNoteId: number,
@@ -46,6 +62,7 @@ export class CreditNoteRefundsController {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
@Delete('refunds/:refundCreditId') @Delete('refunds/:refundCreditId')
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete a refund for the given credit note.' }) @ApiOperation({ summary: 'Delete a refund for the given credit note.' })
deleteRefundCreditNote( deleteRefundCreditNote(
@Param('refundCreditId') refundCreditId: number, @Param('refundCreditId') refundCreditId: number,

View File

@@ -18,6 +18,7 @@ import {
Put, Put,
Query, Query,
Res, Res,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { CreditNoteApplication } from './CreditNoteApplication.service'; import { CreditNoteApplication } from './CreditNoteApplication.service';
import { ICreditNotesQueryDTO } from './types/CreditNotes.types'; import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
@@ -30,6 +31,11 @@ import {
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from './types/CreditNotes.types';
@Controller('credit-notes') @Controller('credit-notes')
@ApiTags('Credit Notes') @ApiTags('Credit Notes')
@@ -37,6 +43,7 @@ import { AcceptType } from '@/constants/accept-type';
@ApiExtraModels(PaginatedResponseDto) @ApiExtraModels(PaginatedResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto) @ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNotesController { export class CreditNotesController {
/** /**
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service. * @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
@@ -44,6 +51,7 @@ export class CreditNotesController {
constructor(private creditNoteApplication: CreditNoteApplication) { } constructor(private creditNoteApplication: CreditNoteApplication) { }
@Post() @Post()
@RequirePermission(CreditNoteAction.Create, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Create a new credit note' }) @ApiOperation({ summary: 'Create a new credit note' })
@ApiResponse({ status: 201, description: 'Credit note successfully created' }) @ApiResponse({ status: 201, description: 'Credit note successfully created' })
@ApiResponse({ status: 400, description: 'Invalid input data' }) @ApiResponse({ status: 400, description: 'Invalid input data' })
@@ -52,6 +60,7 @@ export class CreditNotesController {
} }
@Get('state') @Get('state')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get credit note state' }) @ApiOperation({ summary: 'Get credit note state' })
@ApiResponse({ status: 200, description: 'Returns the credit note state' }) @ApiResponse({ status: 200, description: 'Returns the credit note state' })
getCreditNoteState() { getCreditNoteState() {
@@ -59,6 +68,7 @@ export class CreditNotesController {
} }
@Get(':id') @Get(':id')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get a specific credit note by ID' }) @ApiOperation({ summary: 'Get a specific credit note by ID' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ @ApiResponse({
@@ -92,6 +102,7 @@ export class CreditNotesController {
} }
@Get() @Get()
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Get all credit notes' }) @ApiOperation({ summary: 'Get all credit notes' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -115,6 +126,7 @@ export class CreditNotesController {
} }
@Put(':id') @Put(':id')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Update a credit note' }) @ApiOperation({ summary: 'Update a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully updated' }) @ApiResponse({ status: 200, description: 'Credit note successfully updated' })
@@ -131,6 +143,7 @@ export class CreditNotesController {
} }
@Put(':id/open') @Put(':id/open')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Open a credit note' }) @ApiOperation({ summary: 'Open a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully opened' }) @ApiResponse({ status: 200, description: 'Credit note successfully opened' })
@@ -140,6 +153,7 @@ export class CreditNotesController {
} }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which credit notes can be deleted and returns the results.', 'Validates which credit notes can be deleted and returns the results.',
@@ -161,6 +175,7 @@ export class CreditNotesController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Deletes multiple credit notes.' }) @ApiOperation({ summary: 'Deletes multiple credit notes.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -173,6 +188,7 @@ export class CreditNotesController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Delete a credit note' }) @ApiOperation({ summary: 'Delete a credit note' })
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' }) @ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
@ApiResponse({ status: 200, description: 'Credit note successfully deleted' }) @ApiResponse({ status: 200, description: 'Credit note successfully deleted' })

View File

@@ -1,15 +1,31 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import {
Body,
Controller,
Get,
Param,
Post,
UseGuards,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service'; import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
@Controller('credit-notes') @Controller('credit-notes')
@ApiTags('Credit Notes Apply Invoice') @ApiTags('Credit Notes Apply Invoice')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CreditNotesApplyInvoiceController { export class CreditNotesApplyInvoiceController {
constructor( constructor(
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices, private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
) {} ) {}
@Get(':creditNoteId/applied-invoices') @Get(':creditNoteId/applied-invoices')
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Applied credit note to invoices' }) @ApiOperation({ summary: 'Applied credit note to invoices' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -24,6 +40,7 @@ export class CreditNotesApplyInvoiceController {
} }
@Post(':creditNoteId/apply-invoices') @Post(':creditNoteId/apply-invoices')
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
@ApiOperation({ summary: 'Apply credit note to invoices' }) @ApiOperation({ summary: 'Apply credit note to invoices' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -1,117 +1,103 @@
// import { Service, Inject } from 'typedi'; import { Injectable } from '@nestjs/common';
// import { AccountNormal, ICustomer, ILedgerEntry } from '@/interfaces'; import { AccountNormal } from '@/interfaces/Account';
// import Ledger from '@/services/Accounting/Ledger'; import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { Customer } from './models/Customer';
// @Service() @Injectable()
// export class CustomerGLEntries { export class CustomerGLEntries {
// /** /**
// * Retrieves the customer opening balance common entry attributes. * Retrieves the customer opening balance common entry attributes.
// * @param {ICustomer} customer */
// */ private getCustomerOpeningGLCommonEntry = (customer: Customer) => {
// private getCustomerOpeningGLCommonEntry = (customer: ICustomer) => { return {
// return { exchangeRate: customer.openingBalanceExchangeRate,
// exchangeRate: customer.openingBalanceExchangeRate, currencyCode: customer.currencyCode,
// currencyCode: customer.currencyCode,
// transactionType: 'CustomerOpeningBalance', transactionType: 'CustomerOpeningBalance',
// transactionId: customer.id, transactionId: customer.id,
// date: customer.openingBalanceAt, date: customer.openingBalanceAt,
// userId: customer.userId, contactId: customer.id,
// contactId: customer.id,
// credit: 0, credit: 0,
// debit: 0, debit: 0,
// branchId: customer.openingBalanceBranchId, branchId: customer.openingBalanceBranchId,
// }; };
// }; };
// /** /**
// * Retrieves the customer opening GL credit entry. * Retrieves the customer opening GL credit entry.
// * @param {number} ARAccountId */
// * @param {ICustomer} customer private getCustomerOpeningGLCreditEntry = (
// * @returns {ILedgerEntry} ARAccountId: number,
// */ customer: Customer
// private getCustomerOpeningGLCreditEntry = ( ): ILedgerEntry => {
// ARAccountId: number, const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
// customer: ICustomer
// ): ILedgerEntry => {
// const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
// return { return {
// ...commonEntry, ...commonEntry,
// credit: 0, credit: 0,
// debit: customer.localOpeningBalance, debit: customer.localOpeningBalance,
// accountId: ARAccountId, accountId: ARAccountId,
// accountNormal: AccountNormal.DEBIT, accountNormal: AccountNormal.DEBIT,
// index: 1, index: 1,
// }; };
// }; };
// /** /**
// * Retrieves the customer opening GL debit entry. * Retrieves the customer opening GL debit entry.
// * @param {number} incomeAccountId */
// * @param {ICustomer} customer private getCustomerOpeningGLDebitEntry = (
// * @returns {ILedgerEntry} incomeAccountId: number,
// */ customer: Customer
// private getCustomerOpeningGLDebitEntry = ( ): ILedgerEntry => {
// incomeAccountId: number, const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
// customer: ICustomer
// ): ILedgerEntry => {
// const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
// return { return {
// ...commonEntry, ...commonEntry,
// credit: customer.localOpeningBalance, credit: customer.localOpeningBalance,
// debit: 0, debit: 0,
// accountId: incomeAccountId, accountId: incomeAccountId,
// accountNormal: AccountNormal.CREDIT, accountNormal: AccountNormal.CREDIT,
// index: 2, index: 2,
// }; };
// }; };
// /** /**
// * Retrieves the customer opening GL entries. * Retrieves the customer opening GL entries.
// * @param {number} ARAccountId */
// * @param {number} incomeAccountId public getCustomerOpeningGLEntries = (
// * @param {ICustomer} customer ARAccountId: number,
// * @returns {ILedgerEntry[]} incomeAccountId: number,
// */ customer: Customer
// public getCustomerOpeningGLEntries = ( ) => {
// ARAccountId: number, const debitEntry = this.getCustomerOpeningGLDebitEntry(
// incomeAccountId: number, incomeAccountId,
// customer: ICustomer customer
// ) => { );
// const debitEntry = this.getCustomerOpeningGLDebitEntry( const creditEntry = this.getCustomerOpeningGLCreditEntry(
// incomeAccountId, ARAccountId,
// customer customer
// ); );
// const creditEntry = this.getCustomerOpeningGLCreditEntry( return [debitEntry, creditEntry];
// ARAccountId, };
// customer
// );
// return [debitEntry, creditEntry];
// };
// /** /**
// * Retrieves the customer opening balance ledger. * Retrieves the customer opening balance ledger.
// * @param {number} ARAccountId */
// * @param {number} incomeAccountId public getCustomerOpeningLedger = (
// * @param {ICustomer} customer ARAccountId: number,
// * @returns {ILedger} incomeAccountId: number,
// */ customer: Customer
// public getCustomerOpeningLedger = ( ) => {
// ARAccountId: number, const entries = this.getCustomerOpeningGLEntries(
// incomeAccountId: number, ARAccountId,
// customer: ICustomer incomeAccountId,
// ) => { customer
// const entries = this.getCustomerOpeningGLEntries( );
// ARAccountId, return new Ledger(entries);
// incomeAccountId, };
// customer }
// );
// return new Ledger(entries);
// };
// }

View File

@@ -1,90 +1,84 @@
// import { Knex } from 'knex'; import { Knex } from 'knex';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import { Inject, Injectable } from '@nestjs/common';
// import HasTenancyService from '@/services/Tenancy/TenancyService'; import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
// import { Service, Inject } from 'typedi'; import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
// import { CustomerGLEntries } from './CustomerGLEntries'; import { CustomerGLEntries } from './CustomerGLEntries';
import { Customer } from './models/Customer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Account } from '../Accounts/models/Account.model';
// @Service() @Injectable()
// export class CustomerGLEntriesStorage { export class CustomerGLEntriesStorage {
// @Inject() constructor(
// private tenancy: HasTenancyService; private readonly ledgerStorage: LedgerStorageService,
private readonly accountRepository: AccountRepository,
private readonly customerGLEntries: CustomerGLEntries,
// @Inject() @Inject(Account.name)
// private ledegrRepository: LedgerStorageService; private readonly accountModel: TenantModelProxy<typeof Account>,
// @Inject() @Inject(Customer.name)
// private customerGLEntries: CustomerGLEntries; private readonly customerModel: TenantModelProxy<typeof Customer>,
) { }
// /** /**
// * Customer opening balance journals. * Customer opening balance journals.
// * @param {number} tenantId */
// * @param {number} customerId public writeCustomerOpeningBalance = async (
// * @param {Knex.Transaction} trx customerId: number,
// */ trx?: Knex.Transaction,
// public writeCustomerOpeningBalance = async ( ) => {
// tenantId: number, const customer = await this.customerModel()
// customerId: number, .query(trx)
// trx?: Knex.Transaction .findById(customerId);
// ) => {
// const { Customer } = this.tenancy.models(tenantId);
// const { accountRepository } = this.tenancy.repositories(tenantId);
// const customer = await Customer.query(trx).findById(customerId); // Finds the income account.
const incomeAccount = await this.accountModel()
.query(trx)
.findOne({ slug: 'other-income' });
// // Finds the income account. // Find or create the A/R account.
// const incomeAccount = await accountRepository.findOne({ const ARAccount =
// slug: 'other-income', await this.accountRepository.findOrCreateAccountReceivable(
// }); customer.currencyCode,
// // Find or create the A/R account. {},
// const ARAccount = await accountRepository.findOrCreateAccountReceivable( trx,
// customer.currencyCode, );
// {}, // Retrieves the customer opening balance ledger.
// trx const ledger = this.customerGLEntries.getCustomerOpeningLedger(
// ); ARAccount.id,
// // Retrieves the customer opening balance ledger. incomeAccount.id,
// const ledger = this.customerGLEntries.getCustomerOpeningLedger( customer,
// ARAccount.id, );
// incomeAccount.id, // Commits the ledger entries to the storage.
// customer await this.ledgerStorage.commit(ledger, trx);
// ); };
// // Commits the ledger entries to the storage.
// await this.ledegrRepository.commit(tenantId, ledger, trx);
// };
// /** /**
// * Reverts the customer opening balance GL entries. * Reverts the customer opening balance GL entries.
// * @param {number} tenantId */
// * @param {number} customerId public revertCustomerOpeningBalance = async (
// * @param {Knex.Transaction} trx customerId: number,
// */ trx?: Knex.Transaction,
// public revertCustomerOpeningBalance = async ( ) => {
// tenantId: number, await this.ledgerStorage.deleteByReference(
// customerId: number, customerId,
// trx?: Knex.Transaction 'CustomerOpeningBalance',
// ) => { trx,
// await this.ledegrRepository.deleteByReference( );
// tenantId, };
// customerId,
// 'CustomerOpeningBalance',
// trx
// );
// };
// /** /**
// * Writes the customer opening balance GL entries. * Writes the customer opening balance GL entries.
// * @param {number} tenantId */
// * @param {number} customerId public rewriteCustomerOpeningBalance = async (
// * @param {Knex.Transaction} trx customerId: number,
// */ trx?: Knex.Transaction,
// public rewriteCustomerOpeningBalance = async ( ) => {
// tenantId: number, // Reverts the customer opening balance entries.
// customerId: number, await this.revertCustomerOpeningBalance(customerId, trx);
// trx?: Knex.Transaction
// ) => {
// // Reverts the customer opening balance entries.
// await this.revertCustomerOpeningBalance(tenantId, customerId, trx);
// // Write the customer opening balance entries. // Write the customer opening balance entries.
// await this.writeCustomerOpeningBalance(tenantId, customerId, trx); await this.writeCustomerOpeningBalance(customerId, trx);
// }; };
// } }

View File

@@ -7,12 +7,10 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { CustomersApplication } from './CustomersApplication.service'; import { CustomersApplication } from './CustomersApplication.service';
import { import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
ICustomerOpeningBalanceEditDTO,
ICustomersFilter,
} from './types/Customers.types';
import { import {
ApiOperation, ApiOperation,
ApiResponse, ApiResponse,
@@ -29,15 +27,22 @@ import {
ValidateBulkDeleteCustomersResponseDto, ValidateBulkDeleteCustomersResponseDto,
} from './dtos/BulkDeleteCustomers.dto'; } from './dtos/BulkDeleteCustomers.dto';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { CustomerAction } from './types/Customers.types';
@Controller('customers') @Controller('customers')
@ApiTags('Customers') @ApiTags('Customers')
@ApiExtraModels(CustomerResponseDto) @ApiExtraModels(CustomerResponseDto)
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CustomersController { export class CustomersController {
constructor(private customersApplication: CustomersApplication) { } constructor(private customersApplication: CustomersApplication) { }
@Get(':id') @Get(':id')
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
@ApiOperation({ summary: 'Retrieves the customer details.' }) @ApiOperation({ summary: 'Retrieves the customer details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -49,6 +54,7 @@ export class CustomersController {
} }
@Get() @Get()
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
@ApiOperation({ summary: 'Retrieves the customers paginated list.' }) @ApiOperation({ summary: 'Retrieves the customers paginated list.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -63,6 +69,7 @@ export class CustomersController {
} }
@Post() @Post()
@RequirePermission(CustomerAction.Create, AbilitySubject.Customer)
@ApiOperation({ summary: 'Create a new customer.' }) @ApiOperation({ summary: 'Create a new customer.' })
@ApiResponse({ @ApiResponse({
status: 201, status: 201,
@@ -74,6 +81,7 @@ export class CustomersController {
} }
@Put(':id') @Put(':id')
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
@ApiOperation({ summary: 'Edit the given customer.' }) @ApiOperation({ summary: 'Edit the given customer.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -88,6 +96,7 @@ export class CustomersController {
} }
@Delete(':id') @Delete(':id')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ summary: 'Delete the given customer.' }) @ApiOperation({ summary: 'Delete the given customer.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -98,6 +107,7 @@ export class CustomersController {
} }
@Put(':id/opening-balance') @Put(':id/opening-balance')
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
@ApiOperation({ summary: 'Edit the opening balance of the given customer.' }) @ApiOperation({ summary: 'Edit the opening balance of the given customer.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -106,7 +116,7 @@ export class CustomersController {
}) })
editOpeningBalance( editOpeningBalance(
@Param('id') customerId: number, @Param('id') customerId: number,
@Body() openingBalanceDTO: ICustomerOpeningBalanceEditDTO, @Body() openingBalanceDTO: CustomerOpeningBalanceEditDto,
) { ) {
return this.customersApplication.editOpeningBalance( return this.customersApplication.editOpeningBalance(
customerId, customerId,
@@ -115,6 +125,7 @@ export class CustomersController {
} }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ @ApiOperation({
summary: summary:
'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.', 'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.',
@@ -134,6 +145,7 @@ export class CustomersController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
@ApiOperation({ summary: 'Deletes multiple customers in bulk.' }) @ApiOperation({ summary: 'Deletes multiple customers in bulk.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -18,9 +18,19 @@ import { GetCustomers } from './queries/GetCustomers.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module'; import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { BulkDeleteCustomersService } from './BulkDeleteCustomers.service'; import { BulkDeleteCustomersService } from './BulkDeleteCustomers.service';
import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service'; import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service';
import { LedgerModule } from '../Ledger/Ledger.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { CustomerGLEntries } from './CustomerGLEntries';
import { CustomerGLEntriesStorage } from './CustomerGLEntriesStorage';
import { CustomerWriteGLOpeningBalanceSubscriber } from './subscribers/CustomerGLEntriesSubscriber';
@Module({ @Module({
imports: [TenancyDatabaseModule, DynamicListModule], imports: [
TenancyDatabaseModule,
DynamicListModule,
LedgerModule,
AccountsModule,
],
controllers: [CustomersController], controllers: [CustomersController],
providers: [ providers: [
ActivateCustomer, ActivateCustomer,
@@ -41,6 +51,9 @@ import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomer
GetCustomers, GetCustomers,
BulkDeleteCustomersService, BulkDeleteCustomersService,
ValidateBulkDeleteCustomersService, ValidateBulkDeleteCustomersService,
CustomerGLEntries,
CustomerGLEntriesStorage,
CustomerWriteGLOpeningBalanceSubscriber,
], ],
}) })
export class CustomersModule {} export class CustomersModule {}

View File

@@ -4,10 +4,7 @@ import { CreateCustomer } from './commands/CreateCustomer.service';
import { EditCustomer } from './commands/EditCustomer.service'; import { EditCustomer } from './commands/EditCustomer.service';
import { DeleteCustomer } from './commands/DeleteCustomer.service'; import { DeleteCustomer } from './commands/DeleteCustomer.service';
import { EditOpeningBalanceCustomer } from './commands/EditOpeningBalanceCustomer.service'; import { EditOpeningBalanceCustomer } from './commands/EditOpeningBalanceCustomer.service';
import { import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
ICustomerOpeningBalanceEditDTO,
ICustomersFilter,
} from './types/Customers.types';
import { CreateCustomerDto } from './dtos/CreateCustomer.dto'; import { CreateCustomerDto } from './dtos/CreateCustomer.dto';
import { EditCustomerDto } from './dtos/EditCustomer.dto'; import { EditCustomerDto } from './dtos/EditCustomer.dto';
import { GetCustomers } from './queries/GetCustomers.service'; import { GetCustomers } from './queries/GetCustomers.service';
@@ -18,12 +15,12 @@ import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomer
@Injectable() @Injectable()
export class CustomersApplication { export class CustomersApplication {
constructor( constructor(
private getCustomerService: GetCustomerService, private readonly getCustomerService: GetCustomerService,
private createCustomerService: CreateCustomer, private readonly createCustomerService: CreateCustomer,
private editCustomerService: EditCustomer, private readonly editCustomerService: EditCustomer,
private deleteCustomerService: DeleteCustomer, private readonly deleteCustomerService: DeleteCustomer,
private editOpeningBalanceService: EditOpeningBalanceCustomer, private readonly editOpeningBalanceService: EditOpeningBalanceCustomer,
private getCustomersService: GetCustomers, private readonly getCustomersService: GetCustomers,
private readonly bulkDeleteCustomersService: BulkDeleteCustomersService, private readonly bulkDeleteCustomersService: BulkDeleteCustomersService,
private readonly validateBulkDeleteCustomersService: ValidateBulkDeleteCustomersService, private readonly validateBulkDeleteCustomersService: ValidateBulkDeleteCustomersService,
) {} ) {}
@@ -72,7 +69,7 @@ export class CustomersApplication {
*/ */
public editOpeningBalance = ( public editOpeningBalance = (
customerId: number, customerId: number,
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO, openingBalanceEditDTO: CustomerOpeningBalanceEditDto,
) => { ) => {
return this.editOpeningBalanceService.changeOpeningBalance( return this.editOpeningBalanceService.changeOpeningBalance(
customerId, customerId,
@@ -82,7 +79,7 @@ export class CustomersApplication {
/** /**
* Retrieve customers paginated list. * Retrieve customers paginated list.
* @param {ICustomersFilter} filter - Cusotmers filter. * @param {GetCustomersQueryDto} filter - Cusotmers filter.
*/ */
public getCustomers = (filterDTO: GetCustomersQueryDto) => { public getCustomers = (filterDTO: GetCustomersQueryDto) => {
return this.getCustomersService.getCustomersList(filterDTO); return this.getCustomersService.getCustomersList(filterDTO);

View File

@@ -31,7 +31,7 @@ export class CreateCustomer {
/** /**
* Creates a new customer. * Creates a new customer.
* @param {ICustomerNewDTO} customerDTO * @param {CreateCustomerDto} customerDTO
* @return {Promise<ICustomer>} * @return {Promise<ICustomer>}
*/ */
public async createCustomer( public async createCustomer(

View File

@@ -1,10 +1,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import {
ICustomerOpeningBalanceEditDTO,
ICustomerOpeningBalanceEditedPayload, ICustomerOpeningBalanceEditedPayload,
ICustomerOpeningBalanceEditingPayload, ICustomerOpeningBalanceEditingPayload,
} from '../types/Customers.types'; } from '../types/Customers.types';
import { CustomerOpeningBalanceEditDto } from '../dtos/CustomerOpeningBalanceEdit.dto';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { Customer } from '../models/Customer'; import { Customer } from '../models/Customer';
@@ -29,11 +29,11 @@ export class EditOpeningBalanceCustomer {
/** /**
* Changes the opening balance of the given customer. * Changes the opening balance of the given customer.
* @param {number} customerId - Customer ID. * @param {number} customerId - Customer ID.
* @param {ICustomerOpeningBalanceEditDTO} openingBalanceEditDTO * @param {CustomerOpeningBalanceEditDto} openingBalanceEditDTO
*/ */
public async changeOpeningBalance( public async changeOpeningBalance(
customerId: number, customerId: number,
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO, openingBalanceEditDTO: CustomerOpeningBalanceEditDto,
): Promise<Customer> { ): Promise<Customer> {
// Retrieves the old customer or throw not found error. // Retrieves the old customer or throw not found error.
const oldCustomer = await this.customerModel() const oldCustomer = await this.customerModel()

View File

@@ -4,6 +4,7 @@ import {
IsNotEmpty, IsNotEmpty,
IsNumber, IsNumber,
IsString, IsString,
ValidateIf,
} from 'class-validator'; } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, ToNumber } from '@/common/decorators/Validators'; import { IsOptional, ToNumber } from '@/common/decorators/Validators';
@@ -40,10 +41,11 @@ export class CreateCustomerDto extends ContactAddressDto {
@ApiProperty({ @ApiProperty({
required: false, required: false,
description: 'Opening balance date', description: 'Opening balance date (required when openingBalance is provided)',
example: '2024-01-01', example: '2024-01-01',
}) })
@IsOptional() @ValidateIf((o) => o.openingBalance != null)
@IsNotEmpty({ message: 'openingBalanceAt is required when openingBalance is provided' })
@IsString() @IsString()
openingBalanceAt?: string; openingBalanceAt?: string;

View File

@@ -0,0 +1,44 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
export class CustomerOpeningBalanceEditDto {
@ApiProperty({
required: true,
description: 'Opening balance',
example: 5000.0,
})
@IsNumber()
@IsNotEmpty()
@ToNumber()
openingBalance: number;
@ApiProperty({
required: false,
description: 'Opening balance date',
example: '2024-01-01',
})
@IsOptional()
@IsString()
openingBalanceAt?: string;
@ApiProperty({
required: false,
description: 'Opening balance exchange rate',
example: 1.0,
})
@IsOptional()
@IsNumber()
@ToNumber()
openingBalanceExchangeRate?: number;
@ApiProperty({
required: false,
description: 'Opening balance branch ID',
example: 101,
})
@IsOptional()
@IsNumber()
@ToNumber()
openingBalanceBranchId?: number;
}

View File

@@ -1,91 +1,63 @@
// import { Service, Inject } from 'typedi'; import { Injectable } from '@nestjs/common';
// import { import { OnEvent } from '@nestjs/event-emitter';
// ICustomerEventCreatedPayload, import {
// ICustomerEventDeletedPayload, ICustomerEventCreatedPayload,
// ICustomerOpeningBalanceEditedPayload, ICustomerEventDeletedPayload,
// } from '@/interfaces'; ICustomerOpeningBalanceEditedPayload,
// import events from '@/subscribers/events'; } from '../types/Customers.types';
// import { CustomerGLEntriesStorage } from '../CustomerGLEntriesStorage'; import { events } from '@/common/events/events';
import { CustomerGLEntriesStorage } from '../CustomerGLEntriesStorage';
// @Service() @Injectable()
// export class CustomerWriteGLOpeningBalanceSubscriber { export class CustomerWriteGLOpeningBalanceSubscriber {
// @Inject() constructor(private readonly customerGLEntries: CustomerGLEntriesStorage) { }
// private customerGLEntries: CustomerGLEntriesStorage;
// /** /**
// * Attaches events with handlers. * Handles the writing opening balance journal entries once the customer created.
// */ */
// public attach(bus) { @OnEvent(events.customers.onCreated)
// bus.subscribe( public async handleWriteOpenBalanceEntries({
// events.customers.onCreated, customer,
// this.handleWriteOpenBalanceEntries trx,
// ); }: ICustomerEventCreatedPayload) {
// bus.subscribe( // Writes the customer opening balance journal entries.
// events.customers.onDeleted, if (customer.openingBalance) {
// this.handleRevertOpeningBalanceEntries await this.customerGLEntries.writeCustomerOpeningBalance(
// ); customer.id,
// bus.subscribe( trx,
// events.customers.onOpeningBalanceChanged, );
// this.handleRewriteOpeningEntriesOnChanged }
// ); }
// }
// /** /**
// * Handles the writing opening balance journal entries once the customer created. * Handles the deleting opening balance journal entries once the customer deleted.
// * @param {ICustomerEventCreatedPayload} payload - */
// */ @OnEvent(events.customers.onDeleted)
// private handleWriteOpenBalanceEntries = async ({ public async handleRevertOpeningBalanceEntries({
// tenantId, customerId,
// customer, trx,
// trx, }: ICustomerEventDeletedPayload) {
// }: ICustomerEventCreatedPayload) => { await this.customerGLEntries.revertCustomerOpeningBalance(customerId, trx);
// // Writes the customer opening balance journal entries. }
// if (customer.openingBalance) {
// await this.customerGLEntries.writeCustomerOpeningBalance(
// tenantId,
// customer.id,
// trx
// );
// }
// };
// /** /**
// * Handles the deleting opeing balance journal entrise once the customer deleted. * Handles the rewrite opening balance entries once opening balance changed.
// * @param {ICustomerEventDeletedPayload} payload - */
// */ @OnEvent(events.customers.onOpeningBalanceChanged)
// private handleRevertOpeningBalanceEntries = async ({ public async handleRewriteOpeningEntriesOnChanged({
// tenantId, customer,
// customerId, trx,
// trx, }: ICustomerOpeningBalanceEditedPayload) {
// }: ICustomerEventDeletedPayload) => { if (customer.openingBalance) {
// await this.customerGLEntries.revertCustomerOpeningBalance( await this.customerGLEntries.rewriteCustomerOpeningBalance(
// tenantId, customer.id,
// customerId, trx,
// trx );
// ); } else {
// }; await this.customerGLEntries.revertCustomerOpeningBalance(
customer.id,
// /** trx,
// * Handles the rewrite opening balance entries once opening balnace changed. );
// * @param {ICustomerOpeningBalanceEditedPayload} payload - }
// */ }
// private handleRewriteOpeningEntriesOnChanged = async ({ }
// tenantId,
// customer,
// trx,
// }: ICustomerOpeningBalanceEditedPayload) => {
// if (customer.openingBalance) {
// await this.customerGLEntries.rewriteCustomerOpeningBalance(
// tenantId,
// customer.id,
// trx
// );
// } else {
// await this.customerGLEntries.revertCustomerOpeningBalance(
// tenantId,
// customer.id,
// trx
// );
// }
// };
// }

View File

@@ -4,6 +4,7 @@ import { IContactAddressDTO } from '@/modules/Contacts/types/Contacts.types';
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types'; import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model'; import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { CreateCustomerDto } from '../dtos/CreateCustomer.dto'; import { CreateCustomerDto } from '../dtos/CreateCustomer.dto';
import { CustomerOpeningBalanceEditDto } from '../dtos/CustomerOpeningBalanceEdit.dto';
import { EditCustomerDto } from '../dtos/EditCustomer.dto'; import { EditCustomerDto } from '../dtos/EditCustomer.dto';
// Customer Interfaces. // Customer Interfaces.
@@ -113,23 +114,16 @@ export enum VendorAction {
View = 'View', View = 'View',
} }
export interface ICustomerOpeningBalanceEditDTO {
openingBalance: number;
openingBalanceAt: Date | string;
openingBalanceExchangeRate: number;
openingBalanceBranchId?: number;
}
export interface ICustomerOpeningBalanceEditingPayload { export interface ICustomerOpeningBalanceEditingPayload {
oldCustomer: Customer; oldCustomer: Customer;
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO; openingBalanceEditDTO: CustomerOpeningBalanceEditDto;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }
export interface ICustomerOpeningBalanceEditedPayload { export interface ICustomerOpeningBalanceEditedPayload {
customer: Customer; customer: Customer;
oldCustomer: Customer; oldCustomer: Customer;
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO; openingBalanceEditDTO: CustomerOpeningBalanceEditDto;
trx: Knex.Transaction; trx: Knex.Transaction;
} }

View File

@@ -16,7 +16,7 @@ export class DynamicListService {
private dynamicListSearch: DynamicListSearch, private dynamicListSearch: DynamicListSearch,
private dynamicListSortBy: DynamicListSortBy, private dynamicListSortBy: DynamicListSortBy,
private dynamicListView: DynamicListCustomView, private dynamicListView: DynamicListCustomView,
) {} ) { }
/** /**
* Parses filter DTO. * Parses filter DTO.
@@ -31,9 +31,9 @@ export class DynamicListService {
// Merges the default properties with filter object. // Merges the default properties with filter object.
...(model.defaultSort ...(model.defaultSort
? { ? {
sortOrder: model.defaultSort.sortOrder, sortOrder: model.defaultSort.sortOrder,
columnSortBy: model.defaultSort.sortOrder, columnSortBy: model.defaultSort.sortOrder,
} }
: {}), : {}),
...filterDTO, ...filterDTO,
}; };
@@ -93,7 +93,7 @@ export class DynamicListService {
* Parses stringified filter roles. * Parses stringified filter roles.
* @param {string} stringifiedFilterRoles - Stringified filter roles. * @param {string} stringifiedFilterRoles - Stringified filter roles.
*/ */
public parseStringifiedFilter<T extends IDynamicListFilter>( public parseStringifiedFilter<T extends { stringifiedFilterRoles?: string }>(
filterRoles: T, filterRoles: T,
): T { ): T {
return { return {

View File

@@ -7,6 +7,7 @@ import {
Post, Post,
Put, Put,
Query, Query,
UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { ExpensesApplication } from './ExpensesApplication.service'; import { ExpensesApplication } from './ExpensesApplication.service';
import { IExpensesFilter } from './Expenses.types'; import { IExpensesFilter } from './Expenses.types';
@@ -25,6 +26,11 @@ import {
BulkDeleteDto, BulkDeleteDto,
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
} from '@/common/dtos/BulkDelete.dto'; } from '@/common/dtos/BulkDelete.dto';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ExpenseAction } from './Expenses.types';
@Controller('expenses') @Controller('expenses')
@ApiTags('Expenses') @ApiTags('Expenses')
@@ -34,10 +40,12 @@ import {
ValidateBulkDeleteResponseDto, ValidateBulkDeleteResponseDto,
) )
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ExpensesController { export class ExpensesController {
constructor(private readonly expensesApplication: ExpensesApplication) { } constructor(private readonly expensesApplication: ExpensesApplication) { }
@Post('validate-bulk-delete') @Post('validate-bulk-delete')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({ @ApiOperation({
summary: 'Validate which expenses can be deleted and return the results.', summary: 'Validate which expenses can be deleted and return the results.',
}) })
@@ -58,6 +66,7 @@ export class ExpensesController {
} }
@Post('bulk-delete') @Post('bulk-delete')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({ summary: 'Deletes multiple expenses.' }) @ApiOperation({ summary: 'Deletes multiple expenses.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -76,6 +85,7 @@ export class ExpensesController {
* @param {IExpenseCreateDTO} expenseDTO * @param {IExpenseCreateDTO} expenseDTO
*/ */
@Post() @Post()
@RequirePermission(ExpenseAction.Create, AbilitySubject.Expense)
@ApiOperation({ summary: 'Create a new expense transaction.' }) @ApiOperation({ summary: 'Create a new expense transaction.' })
public createExpense(@Body() expenseDTO: CreateExpenseDto) { public createExpense(@Body() expenseDTO: CreateExpenseDto) {
return this.expensesApplication.createExpense(expenseDTO); return this.expensesApplication.createExpense(expenseDTO);
@@ -87,6 +97,7 @@ export class ExpensesController {
* @param {IExpenseEditDTO} expenseDTO * @param {IExpenseEditDTO} expenseDTO
*/ */
@Put(':id') @Put(':id')
@RequirePermission(ExpenseAction.Edit, AbilitySubject.Expense)
@ApiOperation({ summary: 'Edit the given expense transaction.' }) @ApiOperation({ summary: 'Edit the given expense transaction.' })
public editExpense( public editExpense(
@Param('id') expenseId: number, @Param('id') expenseId: number,
@@ -100,6 +111,7 @@ export class ExpensesController {
* @param {number} expenseId * @param {number} expenseId
*/ */
@Delete(':id') @Delete(':id')
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
@ApiOperation({ summary: 'Delete the given expense transaction.' }) @ApiOperation({ summary: 'Delete the given expense transaction.' })
public deleteExpense(@Param('id') expenseId: number) { public deleteExpense(@Param('id') expenseId: number) {
return this.expensesApplication.deleteExpense(expenseId); return this.expensesApplication.deleteExpense(expenseId);
@@ -110,6 +122,7 @@ export class ExpensesController {
* @param {number} expenseId * @param {number} expenseId
*/ */
@Post(':id/publish') @Post(':id/publish')
@RequirePermission(ExpenseAction.Edit, AbilitySubject.Expense)
@ApiOperation({ summary: 'Publish the given expense transaction.' }) @ApiOperation({ summary: 'Publish the given expense transaction.' })
public publishExpense(@Param('id') expenseId: number) { public publishExpense(@Param('id') expenseId: number) {
return this.expensesApplication.publishExpense(expenseId); return this.expensesApplication.publishExpense(expenseId);
@@ -119,6 +132,7 @@ export class ExpensesController {
* Get the expense transaction details. * Get the expense transaction details.
*/ */
@Get() @Get()
@RequirePermission(ExpenseAction.View, AbilitySubject.Expense)
@ApiOperation({ summary: 'Get the expense transactions.' }) @ApiOperation({ summary: 'Get the expense transactions.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -146,6 +160,7 @@ export class ExpensesController {
* @param {number} expenseId * @param {number} expenseId
*/ */
@Get(':id') @Get(':id')
@RequirePermission(ExpenseAction.View, AbilitySubject.Expense)
@ApiOperation({ summary: 'Get the expense transaction details.' }) @ApiOperation({ summary: 'Get the expense transaction details.' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,

View File

@@ -4,14 +4,15 @@ import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TableSheetPdf } from './TableSheetPdf'; import { TableSheetPdf } from './TableSheetPdf';
import { TemplateInjectableModule } from '@/modules/TemplateInjectable/TemplateInjectable.module'; import { TemplateInjectableModule } from '@/modules/TemplateInjectable/TemplateInjectable.module';
import { ChromiumlyTenancyModule } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.module'; import { ChromiumlyTenancyModule } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.module';
import { InventoryCostModule } from '@/modules/InventoryCost/InventoryCost.module';
@Module({ @Module({
imports: [TemplateInjectableModule, ChromiumlyTenancyModule], imports: [
providers: [ TemplateInjectableModule,
FinancialSheetMeta, ChromiumlyTenancyModule,
TenancyContext, InventoryCostModule,
TableSheetPdf,
], ],
providers: [FinancialSheetMeta, TenancyContext, TableSheetPdf],
exports: [FinancialSheetMeta, TableSheetPdf], exports: [FinancialSheetMeta, TableSheetPdf],
}) })
export class FinancialSheetCommonModule {} export class FinancialSheetCommonModule {}

View File

@@ -1,10 +1,14 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { IFinancialSheetCommonMeta } from '../types/Report.types'; import { IFinancialSheetCommonMeta } from '../types/Report.types';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { InventoryComputeCostService } from '@/modules/InventoryCost/commands/InventoryComputeCost.service';
@Injectable() @Injectable()
export class FinancialSheetMeta { export class FinancialSheetMeta {
constructor(private readonly tenancyContext: TenancyContext) {} constructor(
private readonly tenancyContext: TenancyContext,
private readonly inventoryComputeCostService: InventoryComputeCostService,
) {}
/** /**
* Retrieves the common meta data of the financial sheet. * Retrieves the common meta data of the financial sheet.
@@ -17,10 +21,8 @@ export class FinancialSheetMeta {
const baseCurrency = tenantMetadata.baseCurrency; const baseCurrency = tenantMetadata.baseCurrency;
const dateFormat = tenantMetadata.dateFormat; const dateFormat = tenantMetadata.dateFormat;
// const isCostComputeRunning = const isCostComputeRunning =
// this.inventoryService.isItemsCostComputeRunning(); await this.inventoryComputeCostService.isItemsCostComputeRunning();
const isCostComputeRunning = false;
return { return {
organizationName, organizationName,

View File

@@ -1,5 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { APAgingSummaryApplication } from './APAgingSummaryApplication'; import { APAgingSummaryApplication } from './APAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { import {
@@ -11,14 +11,21 @@ import {
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto'; import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
import { APAgingSummaryResponseExample } from './APAgingSummary.swagger'; import { APAgingSummaryResponseExample } from './APAgingSummary.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('reports/payable-aging-summary') @Controller('reports/payable-aging-summary')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class APAgingSummaryController { export class APAgingSummaryController {
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) {} constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) { }
@Get() @Get()
@RequirePermission(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get payable aging summary' }) @ApiOperation({ summary: 'Get payable aging summary' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -37,12 +44,13 @@ export class APAgingSummaryController {
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@Headers('accept') acceptHeader: string, @Headers('accept') acceptHeader: string,
) { ) {
const accept = acceptHeader || '';
// Retrieves the json table format. // Retrieves the json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) { if (accept.includes(AcceptType.ApplicationJsonTable)) {
return this.APAgingSummaryApp.table(filter); return this.APAgingSummaryApp.table(filter);
// Retrieves the csv format. // Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) { } else if (accept.includes(AcceptType.ApplicationCsv)) {
const csv = await this.APAgingSummaryApp.csv(filter); const csv = await this.APAgingSummaryApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
@@ -50,7 +58,7 @@ export class APAgingSummaryController {
res.send(csv); res.send(csv);
// Retrieves the xlsx format. // Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) { } else if (accept.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.APAgingSummaryApp.xlsx(filter); const buffer = await this.APAgingSummaryApp.xlsx(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx'); res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
@@ -60,7 +68,7 @@ export class APAgingSummaryController {
); );
res.send(buffer); res.send(buffer);
// Retrieves the pdf format. // Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) { } else if (accept.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.APAgingSummaryApp.pdf(filter); const pdfContent = await this.APAgingSummaryApp.pdf(filter);
res.set({ res.set({

View File

@@ -1,5 +1,4 @@
import { Controller, Get, Headers } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { Query, Res } from '@nestjs/common';
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication'; import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express'; import { Response } from 'express';
@@ -12,14 +11,21 @@ import {
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto'; import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
import { ARAgingSummaryResponseExample } from './ARAgingSummary.swagger'; import { ARAgingSummaryResponseExample } from './ARAgingSummary.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('reports/receivable-aging-summary') @Controller('reports/receivable-aging-summary')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ARAgingSummaryController { export class ARAgingSummaryController {
constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {} constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {}
@Get() @Get()
@RequirePermission(ReportsAction.READ_AR_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get receivable aging summary' }) @ApiOperation({ summary: 'Get receivable aging summary' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -38,8 +44,9 @@ export class ARAgingSummaryController {
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@Headers('accept') acceptHeader: string, @Headers('accept') acceptHeader: string,
) { ) {
const accept = acceptHeader || '';
// Retrieves the xlsx format. // Retrieves the xlsx format.
if (acceptHeader.includes(AcceptType.ApplicationXlsx)) { if (accept.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.ARAgingSummaryApp.xlsx(filter); const buffer = await this.ARAgingSummaryApp.xlsx(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx'); res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
@@ -49,11 +56,11 @@ export class ARAgingSummaryController {
); );
res.send(buffer); res.send(buffer);
// Retrieves the table format. // Retrieves the table format.
} else if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) { } else if (accept.includes(AcceptType.ApplicationJsonTable)) {
return this.ARAgingSummaryApp.table(filter); return this.ARAgingSummaryApp.table(filter);
// Retrieves the csv format. // Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) { } else if (accept.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.ARAgingSummaryApp.csv(filter); const buffer = await this.ARAgingSummaryApp.csv(filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
@@ -61,7 +68,7 @@ export class ARAgingSummaryController {
res.send(buffer); res.send(buffer);
// Retrieves the pdf format. // Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) { } else if (accept.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.ARAgingSummaryApp.pdf(filter); const pdfContent = await this.ARAgingSummaryApp.pdf(filter);
res.set({ res.set({

View File

@@ -1,5 +1,5 @@
import { Response } from 'express'; import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common'; import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { AcceptType } from '@/constants/accept-type'; import { AcceptType } from '@/constants/accept-type';
import { BalanceSheetApplication } from './BalanceSheetApplication'; import { BalanceSheetApplication } from './BalanceSheetApplication';
import { import {
@@ -11,10 +11,16 @@ import {
import { BalanceSheetQueryDto } from './BalanceSheet.dto'; import { BalanceSheetQueryDto } from './BalanceSheet.dto';
import { BalanceSheetResponseExample } from './BalanceSheet.swagger'; import { BalanceSheetResponseExample } from './BalanceSheet.swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders'; import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
import { AbilitySubject } from '@/modules/Roles/Roles.types';
import { ReportsAction } from '../../types/Report.types';
@Controller('/reports/balance-sheet') @Controller('/reports/balance-sheet')
@ApiTags('Reports') @ApiTags('Reports')
@ApiCommonHeaders() @ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BalanceSheetStatementController { export class BalanceSheetStatementController {
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {} constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
@@ -25,6 +31,7 @@ export class BalanceSheetStatementController {
* @param {string} acceptHeader - Accept header. * @param {string} acceptHeader - Accept header.
*/ */
@Get('') @Get('')
@RequirePermission(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report)
@ApiOperation({ summary: 'Get balance sheet statement' }) @ApiOperation({ summary: 'Get balance sheet statement' })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
@@ -43,13 +50,14 @@ export class BalanceSheetStatementController {
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@Headers('accept') acceptHeader: string, @Headers('accept') acceptHeader: string,
) { ) {
const accept = acceptHeader || '';
// Retrieves the json table format. // Retrieves the json table format.
if (acceptHeader.includes(AcceptType.ApplicationJsonTable)) { if (accept.includes(AcceptType.ApplicationJsonTable)) {
const table = await this.balanceSheetApp.table(query); const table = await this.balanceSheetApp.table(query);
return table; return table;
// Retrieves the csv format. // Retrieves the csv format.
} else if (acceptHeader.includes(AcceptType.ApplicationCsv)) { } else if (accept.includes(AcceptType.ApplicationCsv)) {
const buffer = await this.balanceSheetApp.csv(query); const buffer = await this.balanceSheetApp.csv(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
@@ -57,7 +65,7 @@ export class BalanceSheetStatementController {
res.send(buffer); res.send(buffer);
// Retrieves the xlsx format. // Retrieves the xlsx format.
} else if (acceptHeader.includes(AcceptType.ApplicationXlsx)) { } else if (accept.includes(AcceptType.ApplicationXlsx)) {
const buffer = await this.balanceSheetApp.xlsx(query); const buffer = await this.balanceSheetApp.xlsx(query);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx'); res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
@@ -67,7 +75,7 @@ export class BalanceSheetStatementController {
); );
res.send(buffer); res.send(buffer);
// Retrieves the pdf format. // Retrieves the pdf format.
} else if (acceptHeader.includes(AcceptType.ApplicationPdf)) { } else if (accept.includes(AcceptType.ApplicationPdf)) {
const pdfContent = await this.balanceSheetApp.pdf(query); const pdfContent = await this.balanceSheetApp.pdf(query);
res.set({ res.set({

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