Compare commits

..

73 Commits

Author SHA1 Message Date
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
214 changed files with 3313 additions and 2029 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

@@ -58,6 +58,12 @@ services:
# System database
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
# Redis
- REDIS_HOST=redis
- REDIS_PORT=6379
- QUEUE_HOST=redis
- QUEUE_PORT=6379
# Tenants databases
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}

View File

@@ -35,4 +35,4 @@ WORKDIR /app/packages/server
RUN git clone https://github.com/vishnubob/wait-for-it.git
# 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,100 +1,102 @@
FROM node:18.16.0-alpine as build
# Stage 1: Build
FROM node:18.16.0-alpine AS builder
USER root
ARG MAIL_HOST= \
MAIL_USERNAME= \
MAIL_PASSWORD= \
MAIL_PORT= \
MAIL_SECURE= \
MAIL_FROM_NAME= \
MAIL_FROM_ADDRESS= \
# Database
DB_HOST= \
DB_USER= \
DB_PASSWORD= \
DB_CHARSET= \
# System database.
SYSTEM_DB_NAME= \
SYSTEM_DB_PASSWORD= \
SYSTEM_DB_USER= \
SYSTEM_DB_HOST= \
SYSTEM_DB_CHARSET= \
# Tenant databases.
TENANT_DB_USER= \
TENANT_DB_PASSWORD= \
TENANT_DB_HOST= \
TENANT_DB_NAME_PERFIX= \
TENANT_DB_CHARSET= \
# Authentication
JWT_SECRET= \
# Application
BASE_URL= \
# Sign-up restriction
SIGNUP_DISABLED= \
SIGNUP_ALLOWED_DOMAINS= \
SIGNUP_ALLOWED_EMAILS=
ENV MAIL_HOST=$MAIL_HOST \
MAIL_USERNAME=$MAIL_USERNAME \
MAIL_PASSWORD=$MAIL_PASSWORD \
MAIL_PORT=$MAIL_PORT \
MAIL_SECURE=$MAIL_SECURE \
MAIL_FROM_NAME=$MAIL_FROM_NAME \
MAIL_FROM_ADDRESS=$MAIL_FROM_ADDRESS \
# Database
DB_HOST=$DB_HOST \
DB_USER=$DB_USER \
DB_PASSWORD=$DB_PASSWORD \
DB_CHARSET=$DB_CHARSET \
# System database.
SYSTEM_DB_HOST=$SYSTEM_DB_HOST \
SYSTEM_DB_USER=$SYSTEM_DB_USER \
SYSTEM_DB_PASSWORD=$SYSTEM_DB_PASSWORD \
SYSTEM_DB_NAME=$SYSTEM_DB_NAME \
SYSTEM_DB_CHARSET=$SYSTEM_DB_CHARSET \
# Tenant databases.
TENANT_DB_NAME_PERFIX=$TENANT_DB_NAME_PERFIX \
TENANT_DB_HOST=$TENANT_DB_HOST \
TENANT_DB_PASSWORD=$TENANT_DB_PASSWORD \
TENANT_DB_USER=$TENANT_DB_USER \
TENANT_DB_CHARSET=$TENANT_DB_CHARSET \
# Authentication
JWT_SECRET=$JWT_SECRET \
# Application
BASE_URL=$BASE_URL \
# Sign-up restriction
SIGNUP_DISABLED=$SIGNUP_DISABLED \
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
# New Relic config file.
ENV NEW_RELIC_NO_CONFIG_FILE=true
# Create app directory.
WORKDIR /app
RUN chown node:node /
# Install pnpm
RUN npm install -g pnpm@8.10.2
# Install pnpm
RUN npm install -g pnpm
# Install build dependencies
RUN apk add --no-cache python3 build-base chromium
# Copy application dependency manifests to the container image.
COPY --chown=node:node ./ ./
# Install application dependencies
RUN apk update
RUN apk add python3 build-base chromium
# Set PYHTON env
# Set Python environment
ENV PYTHON=/usr/bin/python3
# Install packages dependencies for production.
RUN pnpm install
# 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
# # Creates a "dist" folder with the production build
# Build NestJS application
RUN pnpm run build:server --skip-nx-cache
CMD [ "node", "./packages/server/build/index.js" ]
# 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",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"entryFile": "main",
"compilerOptions": {
"deleteOutDir": true,
"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",
"@liaoliaots/nestjs-redis": "^10.0.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/bullmq": "^10.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 throttle from './throttle';
import cloud from './cloud';
import redis from './redis';
import queue from './queue';
import bullBoard from './bull-board';
export const config = [
app,
@@ -38,4 +41,7 @@ export const config = [
loops,
bankfeed,
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,
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
}));

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

@@ -9,6 +9,7 @@ import {
ParseIntPipe,
Put,
HttpCode,
UseGuards,
} from '@nestjs/common';
import { AccountsApplication } from './AccountsApplication.service';
import { CreateAccountDTO } from './CreateAccount.dto';
@@ -32,6 +33,11 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} 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')
@ApiTags('Accounts')
@@ -40,11 +46,13 @@ import {
@ApiExtraModels(GetAccountTransactionResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class AccountsController {
constructor(private readonly accountsApplication: AccountsApplication) { }
@Post('validate-bulk-delete')
@HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({
summary:
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
@@ -67,6 +75,7 @@ export class AccountsController {
@Post('bulk-delete')
@HttpCode(200)
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
@ApiResponse({
status: 200,
@@ -81,6 +90,7 @@ export class AccountsController {
}
@Post()
@RequirePermission(AccountAction.CREATE, AbilitySubject.Account)
@ApiOperation({ summary: 'Create an account' })
@ApiResponse({
status: 200,
@@ -91,6 +101,7 @@ export class AccountsController {
}
@Put(':id')
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Edit the given account.' })
@ApiResponse({
status: 200,
@@ -111,6 +122,7 @@ export class AccountsController {
}
@Delete(':id')
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
@ApiOperation({ summary: 'Delete the given account.' })
@ApiResponse({
status: 200,
@@ -129,6 +141,7 @@ export class AccountsController {
@Post(':id/activate')
@HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Activate the given account.' })
@ApiResponse({
status: 200,
@@ -147,6 +160,7 @@ export class AccountsController {
@Post(':id/inactivate')
@HttpCode(200)
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
@ApiOperation({ summary: 'Inactivate the given account.' })
@ApiResponse({
status: 200,
@@ -164,6 +178,7 @@ export class AccountsController {
}
@Get('types')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account types.' })
@ApiResponse({
status: 200,
@@ -180,6 +195,7 @@ export class AccountsController {
}
@Get('transactions')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account transactions.' })
@ApiResponse({
status: 200,
@@ -198,6 +214,7 @@ export class AccountsController {
}
@Get(':id')
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the account details.' })
@ApiResponse({
status: 200,
@@ -216,6 +233,7 @@ export class AccountsController {
}
@Get()
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
@ApiOperation({ summary: 'Retrieves the accounts.' })
@ApiResponse({
status: 200,

View File

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

View File

@@ -12,6 +12,9 @@ import {
I18nModule,
QueryResolver,
} 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 { ScheduleModule } from '@nestjs/schedule';
import { PassportModule } from '@nestjs/passport';
@@ -137,12 +140,30 @@ import { AppThrottleModule } from './AppThrottle.module';
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
connection: {
host: configService.get('QUEUE_HOST'),
port: configService.get('QUEUE_PORT'),
host: configService.get('queue.host'),
port: configService.get('queue.port'),
},
}),
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({
global: true,
middleware: {
@@ -158,8 +179,8 @@ import { AppThrottleModule } from './AppThrottle.module';
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
config: {
host: configService.get('redis.host') || 'localhost',
port: configService.get('redis.port') || 6379,
host: configService.get('redis.host'),
port: configService.get('redis.port'),
},
}),
inject: [ConfigService],

View File

@@ -17,6 +17,8 @@ import { PassportModule } from '@nestjs/passport';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './guards/jwt.guard';
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 {
SendResetPasswordMailQueue,
@@ -63,6 +65,14 @@ const models = [
TenancyModule,
BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
BullBoardModule.forFeature({
name: SendResetPasswordMailQueue,
adapter: BullMQAdapter,
}),
BullBoardModule.forFeature({
name: SendSignupVerificationMailQueue,
adapter: BullMQAdapter,
}),
],
exports: [...models],
providers: [
@@ -98,4 +108,4 @@ const models = [
AuthMailSubscriber,
],
})
export class AuthModule { }
export class AuthModule {}

View File

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

View File

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

View File

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

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 { Module } from '@nestjs/common';
import { SocketModule } from '../Socket/Socket.module';
@@ -33,6 +35,10 @@ const models = [RegisterTenancyModel(PlaidItem)];
BankingCategorizeModule,
BankingTransactionsModule,
BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }),
BullBoardModule.forFeature({
name: UpdateBankingPlaidTransitionsQueueJob,
adapter: BullMQAdapter,
}),
...models,
],
providers: [
@@ -51,4 +57,4 @@ const models = [RegisterTenancyModel(PlaidItem)];
exports: [...models],
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 { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import {
PlaidFetchTransitonsEventPayload,
UpdateBankingPlaidTransitionsJob,
UpdateBankingPlaidTransitionsQueueJob,
} from '../types/BankingPlaid.types';
import { PlaidUpdateTransactions } from '../command/PlaidUpdateTransactions';
@@ -28,7 +26,6 @@ export class PlaidFetchTransactionsProcessor extends WorkerHost {
/**
* Triggers the function.
*/
@Process(UpdateBankingPlaidTransitionsJob)
@UseCls()
async process(job: Job<PlaidFetchTransitonsEventPayload>) {
const { plaidItemId } = job.data;

View File

@@ -10,6 +10,8 @@ import { BankingRecognizedTransactionsController } from './BankingRecognizedTran
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
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 { RecognizeUncategorizedTransactionsQueue } from './_types';
import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob';
@@ -25,6 +27,10 @@ const models = [RegisterTenancyModel(RecognizedBankTransaction)];
BullModule.registerQueue({
name: RecognizeUncategorizedTransactionsQueue,
}),
BullBoardModule.forFeature({
name: RecognizeUncategorizedTransactionsQueue,
adapter: BullMQAdapter,
}),
...models,
],
providers: [

View File

@@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob =
export const RecognizeUncategorizedTransactionsQueue =
'recognize-uncategorized-transactions-queue';
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
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.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(

View File

@@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber {
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
const payload = {
ruleId: bankRule.id,
shouldRevert: true,
...tenantPayload,
} as RecognizeUncategorizedTransactionsJobPayload;
// 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(
RecognizeUncategorizedTransactionsJob,
payload,

View File

@@ -3,11 +3,11 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service';
import {
RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue,
} from '../_types';
import { Process } from '@nestjs/bull';
@Processor({
name: RecognizeUncategorizedTransactionsQueue,
@@ -16,10 +16,12 @@ import { Process } from '@nestjs/bull';
export class RegonizeTransactionsPrcessor extends WorkerHost {
/**
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
* @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService -
* @param {ClsService} clsService -
*/
constructor(
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
private readonly clsService: ClsService,
) {
super();
@@ -28,15 +30,23 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
/**
* Triggers sending invoice mail.
*/
@Process(RecognizeUncategorizedTransactionsQueue)
@UseCls()
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('userId', job.data.userId);
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(
ruleId,
transactionsCriteria,

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ import {
Get,
Query,
HttpCode,
UseGuards,
} from '@nestjs/common';
import { BillsApplication } from './Bills.application';
import { IBillsFilter } from './Bills.types';
@@ -28,6 +29,11 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} 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')
@ApiTags('Bills')
@@ -35,10 +41,12 @@ import {
@ApiExtraModels(PaginatedResponseDto)
@ApiCommonHeaders()
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BillsController {
constructor(private billsApplication: BillsApplication) { }
@Post('validate-bulk-delete')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({
summary: 'Validate which bills can be deleted and return the results.',
})
@@ -58,6 +66,7 @@ export class BillsController {
}
@Post('bulk-delete')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ summary: 'Deletes multiple bills.' })
@HttpCode(200)
@ApiResponse({
@@ -73,12 +82,14 @@ export class BillsController {
}
@Post()
@RequirePermission(BillAction.Create, AbilitySubject.Bill)
@ApiOperation({ summary: 'Create a new bill.' })
createBill(@Body() billDTO: CreateBillDto) {
return this.billsApplication.createBill(billDTO);
}
@Put(':id')
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
@ApiOperation({ summary: 'Edit the given bill.' })
@ApiParam({
name: 'id',
@@ -91,6 +102,7 @@ export class BillsController {
}
@Delete(':id')
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
@ApiOperation({ summary: 'Delete the given bill.' })
@ApiParam({
name: 'id',
@@ -103,6 +115,7 @@ export class BillsController {
}
@Get()
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the bills.' })
@ApiResponse({
status: 200,
@@ -132,6 +145,7 @@ export class BillsController {
}
@Get(':id/payment-transactions')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({
summary: 'Retrieve the specific bill associated payment transactions.',
})
@@ -146,6 +160,7 @@ export class BillsController {
}
@Get(':id')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the bill details.' })
@ApiResponse({
status: 200,
@@ -165,6 +180,7 @@ export class BillsController {
}
@Patch(':id/open')
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
@ApiOperation({ summary: 'Open the given bill.' })
@ApiParam({
name: 'id',
@@ -177,6 +193,7 @@ export class BillsController {
}
@Get('due')
@RequirePermission(BillAction.View, AbilitySubject.Bill)
@ApiOperation({ summary: 'Retrieves the due bills.' })
getDueBills(@Body('vendorId') vendorId?: number) {
return this.billsApplication.getDueBills(vendorId);

View File

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

View File

@@ -30,6 +30,7 @@ export class BillTransformer extends Transformer {
'taxes',
'entries',
'attachments',
'branch',
];
};

View File

@@ -31,6 +31,12 @@ import { ValidateBranchExistance } from './integrations/ValidateBranchExistance'
import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator';
import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches';
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';
@Module({
@@ -66,7 +72,13 @@ import { FeaturesModule } from '../Features/Features.module';
ValidateBranchExistance,
ManualJournalBranchesValidator,
CashflowTransactionsActivateBranches,
ExpensesActivateBranches
ExpensesActivateBranches,
BillActivateBranches,
VendorCreditActivateBranches,
BillPaymentsActivateBranches,
BillBranchesActivateSubscriber,
VendorCreditBranchesActivateSubscriber,
PaymentMadeActivateBranchesSubscriber
],
exports: [
BranchesSettingsService,

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
import { Knex } from 'knex';
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
@Injectable()
export class VendorCreditActivateBranches {
constructor(
@Inject(VendorCredit.name)
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.
* @param {IBillCreatingPayload} payload
*/
@OnEvent(events.bill.onCreating)
@OnEvent(events.bill.onCreating, { suppressErrors: false })
async validateBranchExistanceOnBillCreating({
billDTO,
}: IBillCreatingPayload) {
@@ -30,7 +30,7 @@ export class BillBranchValidateSubscriber {
* Validate branch existance once bill editing.
* @param {IBillEditingPayload} payload
*/
@OnEvent(events.bill.onEditing)
@OnEvent(events.bill.onEditing, { suppressErrors: false })
async validateBranchExistanceOnBillEditing({ billDTO }: IBillEditingPayload) {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
billDTO.branchId,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,9 +18,19 @@ import { GetCustomers } from './queries/GetCustomers.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { BulkDeleteCustomersService } from './BulkDeleteCustomers.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({
imports: [TenancyDatabaseModule, DynamicListModule],
imports: [
TenancyDatabaseModule,
DynamicListModule,
LedgerModule,
AccountsModule,
],
controllers: [CustomersController],
providers: [
ActivateCustomer,
@@ -41,6 +51,9 @@ import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomer
GetCustomers,
BulkDeleteCustomersService,
ValidateBulkDeleteCustomersService,
CustomerGLEntries,
CustomerGLEntriesStorage,
CustomerWriteGLOpeningBalanceSubscriber,
],
})
export class CustomersModule {}

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import {
IsNotEmpty,
IsNumber,
IsString,
ValidateIf,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
@@ -40,10 +41,11 @@ export class CreateCustomerDto extends ContactAddressDto {
@ApiProperty({
required: false,
description: 'Opening balance date',
description: 'Opening balance date (required when openingBalance is provided)',
example: '2024-01-01',
})
@IsOptional()
@ValidateIf((o) => o.openingBalance != null)
@IsNotEmpty({ message: 'openingBalanceAt is required when openingBalance is provided' })
@IsString()
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 {
// ICustomerEventCreatedPayload,
// ICustomerEventDeletedPayload,
// ICustomerOpeningBalanceEditedPayload,
// } from '@/interfaces';
// import events from '@/subscribers/events';
// import { CustomerGLEntriesStorage } from '../CustomerGLEntriesStorage';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
ICustomerEventCreatedPayload,
ICustomerEventDeletedPayload,
ICustomerOpeningBalanceEditedPayload,
} from '../types/Customers.types';
import { events } from '@/common/events/events';
import { CustomerGLEntriesStorage } from '../CustomerGLEntriesStorage';
// @Service()
// export class CustomerWriteGLOpeningBalanceSubscriber {
// @Inject()
// private customerGLEntries: CustomerGLEntriesStorage;
@Injectable()
export class CustomerWriteGLOpeningBalanceSubscriber {
constructor(private readonly customerGLEntries: CustomerGLEntriesStorage) { }
// /**
// * Attaches events with handlers.
// */
// public attach(bus) {
// bus.subscribe(
// events.customers.onCreated,
// this.handleWriteOpenBalanceEntries
// );
// bus.subscribe(
// events.customers.onDeleted,
// this.handleRevertOpeningBalanceEntries
// );
// bus.subscribe(
// events.customers.onOpeningBalanceChanged,
// this.handleRewriteOpeningEntriesOnChanged
// );
// }
/**
* Handles the writing opening balance journal entries once the customer created.
*/
@OnEvent(events.customers.onCreated)
public async handleWriteOpenBalanceEntries({
customer,
trx,
}: ICustomerEventCreatedPayload) {
// Writes the customer opening balance journal entries.
if (customer.openingBalance) {
await this.customerGLEntries.writeCustomerOpeningBalance(
customer.id,
trx,
);
}
}
// /**
// * Handles the writing opening balance journal entries once the customer created.
// * @param {ICustomerEventCreatedPayload} payload -
// */
// private handleWriteOpenBalanceEntries = async ({
// tenantId,
// customer,
// trx,
// }: ICustomerEventCreatedPayload) => {
// // Writes the customer opening balance journal entries.
// if (customer.openingBalance) {
// await this.customerGLEntries.writeCustomerOpeningBalance(
// tenantId,
// customer.id,
// trx
// );
// }
// };
/**
* Handles the deleting opening balance journal entries once the customer deleted.
*/
@OnEvent(events.customers.onDeleted)
public async handleRevertOpeningBalanceEntries({
customerId,
trx,
}: ICustomerEventDeletedPayload) {
await this.customerGLEntries.revertCustomerOpeningBalance(customerId, trx);
}
// /**
// * Handles the deleting opeing balance journal entrise once the customer deleted.
// * @param {ICustomerEventDeletedPayload} payload -
// */
// private handleRevertOpeningBalanceEntries = async ({
// tenantId,
// customerId,
// trx,
// }: ICustomerEventDeletedPayload) => {
// await this.customerGLEntries.revertCustomerOpeningBalance(
// tenantId,
// customerId,
// 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
// );
// }
// };
// }
/**
* Handles the rewrite opening balance entries once opening balance changed.
*/
@OnEvent(events.customers.onOpeningBalanceChanged)
public async handleRewriteOpeningEntriesOnChanged({
customer,
trx,
}: ICustomerOpeningBalanceEditedPayload) {
if (customer.openingBalance) {
await this.customerGLEntries.rewriteCustomerOpeningBalance(
customer.id,
trx,
);
} else {
await this.customerGLEntries.revertCustomerOpeningBalance(
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 { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { CreateCustomerDto } from '../dtos/CreateCustomer.dto';
import { CustomerOpeningBalanceEditDto } from '../dtos/CustomerOpeningBalanceEdit.dto';
import { EditCustomerDto } from '../dtos/EditCustomer.dto';
// Customer Interfaces.
@@ -113,23 +114,16 @@ export enum VendorAction {
View = 'View',
}
export interface ICustomerOpeningBalanceEditDTO {
openingBalance: number;
openingBalanceAt: Date | string;
openingBalanceExchangeRate: number;
openingBalanceBranchId?: number;
}
export interface ICustomerOpeningBalanceEditingPayload {
oldCustomer: Customer;
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO;
openingBalanceEditDTO: CustomerOpeningBalanceEditDto;
trx?: Knex.Transaction;
}
export interface ICustomerOpeningBalanceEditedPayload {
customer: Customer;
oldCustomer: Customer;
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO;
openingBalanceEditDTO: CustomerOpeningBalanceEditDto;
trx: Knex.Transaction;
}

View File

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

View File

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

View File

@@ -1,10 +1,14 @@
import { Injectable } from '@nestjs/common';
import { IFinancialSheetCommonMeta } from '../types/Report.types';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { InventoryComputeCostService } from '@/modules/InventoryCost/commands/InventoryComputeCost.service';
@Injectable()
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.
@@ -17,10 +21,8 @@ export class FinancialSheetMeta {
const baseCurrency = tenantMetadata.baseCurrency;
const dateFormat = tenantMetadata.dateFormat;
// const isCostComputeRunning =
// this.inventoryService.isItemsCostComputeRunning();
const isCostComputeRunning = false;
const isCostComputeRunning =
await this.inventoryComputeCostService.isItemsCostComputeRunning();
return {
organizationName,

View File

@@ -1,5 +1,5 @@
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 { AcceptType } from '@/constants/accept-type';
import {
@@ -11,14 +11,21 @@ import {
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
import { APAgingSummaryResponseExample } from './APAgingSummary.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class APAgingSummaryController {
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) { }
@Get()
@RequirePermission(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get payable aging summary' })
@ApiResponse({
status: 200,

View File

@@ -1,5 +1,4 @@
import { Controller, Get, Headers } from '@nestjs/common';
import { Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
import { AcceptType } from '@/constants/accept-type';
import { Response } from 'express';
@@ -12,14 +11,21 @@ import {
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
import { ARAgingSummaryResponseExample } from './ARAgingSummary.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ARAgingSummaryController {
constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {}
@Get()
@RequirePermission(ReportsAction.READ_AR_AGING_SUMMARY, AbilitySubject.Report)
@ApiOperation({ summary: 'Get receivable aging summary' })
@ApiResponse({
status: 200,

View File

@@ -1,5 +1,5 @@
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 { BalanceSheetApplication } from './BalanceSheetApplication';
import {
@@ -11,10 +11,16 @@ import {
import { BalanceSheetQueryDto } from './BalanceSheet.dto';
import { BalanceSheetResponseExample } from './BalanceSheet.swagger';
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')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class BalanceSheetStatementController {
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
@@ -25,6 +31,7 @@ export class BalanceSheetStatementController {
* @param {string} acceptHeader - Accept header.
*/
@Get('')
@RequirePermission(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report)
@ApiOperation({ summary: 'Get balance sheet statement' })
@ApiResponse({
status: 200,

View File

@@ -24,14 +24,14 @@ export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
displayColumnsType: 'total' | 'date_periods' = 'total';
@ApiProperty({
enum: ['day', 'month', 'year'],
enum: ['day', 'month', 'year', 'quarter'],
default: 'year',
description: 'Time period for column display',
})
@IsString()
@IsOptional()
@IsEnum(['day', 'month', 'year'])
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
@IsEnum(['day', 'month', 'year', 'quarter'])
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@ApiProperty({
description: 'Start date for the balance sheet period',

View File

@@ -34,13 +34,13 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
@ApiProperty({
description: 'Display columns by time period',
required: false,
enum: ['day', 'month', 'year'],
enum: ['day', 'month', 'year', 'quarter'],
default: 'year',
})
@IsString()
@IsOptional()
@IsEnum(['day', 'month', 'year'])
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
@IsEnum(['day', 'month', 'year', 'quarter'])
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@ApiProperty({
description: 'Type of column display',

View File

@@ -1,5 +1,5 @@
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 { CashflowSheetApplication } from './CashflowSheetApplication';
import {
@@ -11,14 +11,21 @@ import {
import { CashFlowStatementQueryDto } from './CashFlowStatementQuery.dto';
import { CashflowStatementResponseExample } from './CashflowStatement.swagger';
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/cashflow-statement')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class CashflowController {
constructor(private readonly cashflowSheetApp: CashflowSheetApplication) { }
@Get()
@RequirePermission(ReportsAction.READ_CASHFLOW, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Cashflow statement report',

View File

@@ -5,22 +5,29 @@ import {
ApiTags,
} from '@nestjs/swagger';
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { GeneralLedgerApplication } from './GeneralLedgerApplication';
import { AcceptType } from '@/constants/accept-type';
import { GeneralLedgerQueryDto } from './GeneralLedgerQuery.dto';
import { GeneralLedgerResponseExample } from './GeneralLedger.swagger';
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/general-ledger')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class GeneralLedgerController {
constructor(
private readonly generalLedgerApplication: GeneralLedgerApplication,
) {}
@Get()
@RequirePermission(ReportsAction.READ_GENERAL_LEDGET, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'General ledger report',

View File

@@ -1,4 +1,4 @@
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { Response } from 'express';
import { AcceptType } from '@/constants/accept-type';
import { JournalSheetApplication } from './JournalSheetApplication';
@@ -11,14 +11,21 @@ import {
import { JournalSheetQueryDto } from './JournalSheetQuery.dto';
import { JournalSheetResponseExample } from './JournalSheet.swagger';
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/journal')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class JournalSheetController {
constructor(private readonly journalSheetApp: JournalSheetApplication) {}
@Get()
@RequirePermission(ReportsAction.READ_JOURNAL, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Journal report',

View File

@@ -1,5 +1,5 @@
import { Response } from 'express';
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
import { ProfitLossSheetApplication } from './ProfitLossSheetApplication';
import { AcceptType } from '@/constants/accept-type';
import {
@@ -11,10 +11,16 @@ import {
import { ProfitLossSheetQueryDto } from './ProfitLossSheetQuery.dto';
import { ProfitLossSheetResponseExample } from './ProfitLossSheet.swagger';
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/profit-loss-sheet')
@ApiTags('Reports')
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ProfitLossSheetController {
constructor(
private readonly profitLossSheetApp: ProfitLossSheetApplication,
@@ -27,6 +33,7 @@ export class ProfitLossSheetController {
* @param {string} acceptHeader
*/
@Get('/')
@RequirePermission(ReportsAction.READ_PROFIT_LOSS, AbilitySubject.Report)
@ApiResponse({
status: 200,
description: 'Profit & loss statement',

View File

@@ -64,10 +64,10 @@ export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
displayColumnsType: 'total' | 'date_periods';
@IsString()
@IsEnum(['day', 'month', 'year'])
@IsEnum(['day', 'month', 'year', 'quarter'])
@IsOptional()
@ApiProperty({ description: 'How to display columns' })
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@Transform(({ value }) => parseBoolean(value, false))
@IsBoolean()

View File

@@ -172,8 +172,11 @@ export class TrialBalanceSheet extends FinancialSheet {
private filterNoneTransactions = (
accountNode: ITrialBalanceAccount
): boolean => {
const accountLedger = this.repository.totalAccountsLedger.whereAccountId(
accountNode.id,
const depsAccountsIds =
this.repository.accountsDepGraph.dependenciesOf(accountNode.id);
const accountLedger = this.repository.totalAccountsLedger.whereAccountsIds(
[accountNode.id, ...depsAccountsIds]
);
return !accountLedger.isEmpty();
};
@@ -241,8 +244,8 @@ export class TrialBalanceSheet extends FinancialSheet {
*/
private accountsSection(accounts: ModelObject<Account>[]) {
return R.compose(
this.nestedAccountsNode,
this.accountsFilter,
this.nestedAccountsNode,
this.accountsMapper
)(accounts);
}
@@ -250,7 +253,6 @@ export class TrialBalanceSheet extends FinancialSheet {
/**
* Retrieve trial balance sheet statement data.
* Note: Retruns null in case there is no transactions between the given date periods.
*
* @return {ITrialBalanceSheetData}
*/
public reportData(): ITrialBalanceSheetData {

View File

@@ -14,6 +14,7 @@ import {
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { InventoryAdjustmentsApplicationService } from './InventoryAdjustmentsApplication.service';
import { IInventoryAdjustmentsFilter } from './types/InventoryAdjustments.types';
@@ -21,17 +22,24 @@ import { InventoryAdjustment } from './models/InventoryAdjustment';
import { CreateQuickInventoryAdjustmentDto } from './dtos/CreateQuickInventoryAdjustment.dto';
import { InventoryAdjustmentResponseDto } from './dtos/InventoryAdjustmentResponse.dto';
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 { InventoryAdjustmentAction } from './types/InventoryAdjustments.types';
@Controller('inventory-adjustments')
@ApiTags('Inventory Adjustments')
@ApiExtraModels(InventoryAdjustmentResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class InventoryAdjustmentsController {
constructor(
private readonly inventoryAdjustmentsApplicationService: InventoryAdjustmentsApplicationService,
) {}
@Post('quick')
@RequirePermission(InventoryAdjustmentAction.CREATE, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Create a quick inventory adjustment.' })
@ApiResponse({
status: 200,
@@ -46,6 +54,7 @@ export class InventoryAdjustmentsController {
}
@Delete(':id')
@RequirePermission(InventoryAdjustmentAction.DELETE, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Delete the given inventory adjustment.' })
@ApiResponse({
status: 200,
@@ -60,6 +69,7 @@ export class InventoryAdjustmentsController {
}
@Get()
@RequirePermission(InventoryAdjustmentAction.VIEW, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Retrieves the inventory adjustments.' })
@ApiResponse({
status: 200,
@@ -78,6 +88,7 @@ export class InventoryAdjustmentsController {
}
@Get(':id')
@RequirePermission(InventoryAdjustmentAction.VIEW, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Retrieves the inventory adjustment details.' })
@ApiResponse({
status: 200,
@@ -94,6 +105,7 @@ export class InventoryAdjustmentsController {
}
@Put(':id/publish')
@RequirePermission(InventoryAdjustmentAction.EDIT, AbilitySubject.InventoryAdjustment)
@ApiOperation({ summary: 'Publish the given inventory adjustment.' })
@ApiResponse({
status: 200,

View File

@@ -16,6 +16,8 @@ import {
ComputeItemCostQueue,
WriteInventoryTransactionsGLEntriesQueue,
} from './types/InventoryCost.types';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq';
import { InventoryAverageCostMethodService } from './commands/InventoryAverageCostMethod.service';
import { InventoryItemCostService } from './commands/InventoryCosts.service';
@@ -39,6 +41,14 @@ const models = [
BullModule.registerQueue({
name: WriteInventoryTransactionsGLEntriesQueue,
}),
BullBoardModule.forFeature({
name: ComputeItemCostQueue,
adapter: BullMQAdapter,
}),
BullBoardModule.forFeature({
name: WriteInventoryTransactionsGLEntriesQueue,
adapter: BullMQAdapter,
}),
forwardRef(() => SaleInvoicesModule),
ImportModule,
],
@@ -56,7 +66,7 @@ const models = [
InventoryItemCostService,
InventoryItemOpeningAvgCostService,
InventoryCostSubscriber,
GetItemsInventoryValuationListService
GetItemsInventoryValuationListService,
],
exports: [
...models,
@@ -64,6 +74,6 @@ const models = [
InventoryItemCostService,
InventoryComputeCostService,
],
controllers: [InventoryCostController]
controllers: [InventoryCostController],
})
export class InventoryCostModule {}

View File

@@ -7,11 +7,7 @@ import * as moment from 'moment';
import { TenantJobPayload } from '@/interfaces/Tenant';
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
import { events } from '@/common/events/events';
import {
ComputeItemCostQueue,
ComputeItemCostQueueJob,
} from '../types/InventoryCost.types';
import { Process } from '@nestjs/bull';
import { ComputeItemCostQueue } from '../types/InventoryCost.types';
interface ComputeItemCostJobPayload extends TenantJobPayload {
itemId: number;
@@ -39,7 +35,6 @@ export class ComputeItemCostProcessor extends WorkerHost {
* Process the compute item cost job.
* @param {Job<ComputeItemCostJobPayload>} job - The job to process
*/
@Process(ComputeItemCostQueueJob)
@UseCls()
async process(job: Job<ComputeItemCostJobPayload>) {
const { itemId, startingDate, organizationId, userId } = job.data;
@@ -68,6 +63,12 @@ export class ComputeItemCostProcessor extends WorkerHost {
} catch (error) {
console.error(`[error] Error computing item cost for item ${itemId}:`, error);
console.error('Error stack:', error instanceof Error ? error.stack : 'No stack trace');
// Reset cost_compute_running when job fails so it does not stay true indefinitely
try {
await this.inventoryComputeCostService.markItemsCostComputeRunning(false);
} catch (markError) {
console.error('[error] Failed to mark cost compute as not running:', markError);
}
throw error;
}
}

View File

@@ -1,8 +1,4 @@
import { Process } from '@nestjs/bull';
import {
WriteInventoryTransactionsGLEntriesQueue,
WriteInventoryTransactionsGLEntriesQueueJob,
} from '../types/InventoryCost.types';
import { WriteInventoryTransactionsGLEntriesQueue } from '../types/InventoryCost.types';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
@@ -15,6 +11,5 @@ export class WriteInventoryTransactionsGLEntriesProcessor extends WorkerHost {
super();
}
@Process(WriteInventoryTransactionsGLEntriesQueueJob)
async process() {}
}

View File

@@ -9,6 +9,7 @@ import {
Put,
Query,
HttpCode,
UseGuards,
} from '@nestjs/common';
import { TenantController } from '../Tenancy/Tenant.controller';
import { ItemsApplicationService } from './ItemsApplication.service';
@@ -34,6 +35,12 @@ import {
BulkDeleteItemsDto,
ValidateBulkDeleteItemsResponseDto,
} from './dtos/BulkDeleteItems.dto';
import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.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 { ItemAction } from '@/interfaces/Item';
@Controller('/items')
@ApiTags('Items')
@@ -45,13 +52,16 @@ import {
@ApiExtraModels(ItemEstimatesResponseDto)
@ApiExtraModels(ItemReceiptsResponseDto)
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
@ApiExtraModels(ItemApiErrorResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ItemsController extends TenantController {
constructor(private readonly itemsApplication: ItemsApplicationService) {
super();
}
@Get()
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({ summary: 'Retrieves the item list.' })
@ApiResponse({
status: 200,
@@ -142,11 +152,19 @@ export class ItemsController extends TenantController {
* @returns The updated item id.
*/
@Put(':id')
@RequirePermission(ItemAction.EDIT, AbilitySubject.Item)
@ApiOperation({ summary: 'Edit the given item (product or service).' })
@ApiResponse({
status: 200,
description: 'The item has been successfully updated.',
})
@ApiResponse({
status: 400,
description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, INVENTORY_ACCOUNT_CANNOT_MODIFIED, TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The item not found.' })
@ApiParam({
name: 'id',
@@ -165,6 +183,7 @@ export class ItemsController extends TenantController {
@Post('validate-bulk-delete')
@HttpCode(200)
@RequirePermission(ItemAction.DELETE, AbilitySubject.Item)
@ApiOperation({
summary:
'Validates which items can be deleted and returns counts of deletable and non-deletable items.',
@@ -185,6 +204,7 @@ export class ItemsController extends TenantController {
@Post('bulk-delete')
@HttpCode(200)
@RequirePermission(ItemAction.DELETE, AbilitySubject.Item)
@ApiOperation({ summary: 'Deletes multiple items in bulk.' })
@ApiResponse({
status: 200,
@@ -199,11 +219,19 @@ export class ItemsController extends TenantController {
}
@Post()
@RequirePermission(ItemAction.CREATE, AbilitySubject.Item)
@ApiOperation({ summary: 'Create a new item (product or service).' })
@ApiResponse({
status: 200,
description: 'The item has been successfully created.',
})
@ApiResponse({
status: 400,
description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, ITEM_CATEOGRY_NOT_FOUND, COST_ACCOUNT_NOT_COGS, SELL_ACCOUNT_NOT_INCOME, INVENTORY_ACCOUNT_NOT_INVENTORY, INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM, COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
// @UsePipes(new ZodValidationPipe(createItemSchema))
async createItem(
@Body() createItemDto: CreateItemDto,
@@ -214,11 +242,19 @@ export class ItemsController extends TenantController {
}
@Delete(':id')
@RequirePermission(ItemAction.DELETE, AbilitySubject.Item)
@ApiOperation({ summary: 'Delete the given item (product or service).' })
@ApiResponse({
status: 200,
description: 'The item has been successfully deleted.',
})
@ApiResponse({
status: 400,
description: 'Cannot delete item. Possible error types: ITEM_HAS_ASSOCIATED_TRANSACTINS, ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The item not found.' })
@ApiParam({
name: 'id',
@@ -232,6 +268,7 @@ export class ItemsController extends TenantController {
}
@Patch(':id/inactivate')
@RequirePermission(ItemAction.EDIT, AbilitySubject.Item)
@ApiOperation({ summary: 'Inactivate the given item (product or service).' })
@ApiResponse({
status: 200,
@@ -250,6 +287,7 @@ export class ItemsController extends TenantController {
}
@Patch(':id/activate')
@RequirePermission(ItemAction.EDIT, AbilitySubject.Item)
@ApiOperation({ summary: 'Activate the given item (product or service).' })
@ApiResponse({
status: 200,
@@ -268,6 +306,7 @@ export class ItemsController extends TenantController {
}
@Get(':id')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({ summary: 'Get the given item (product or service).' })
@ApiResponse({
status: 200,
@@ -289,6 +328,7 @@ export class ItemsController extends TenantController {
}
@Get(':id/invoices')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({
summary: 'Retrieves the item associated invoices transactions.',
})
@@ -314,6 +354,7 @@ export class ItemsController extends TenantController {
}
@Get(':id/bills')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({
summary: 'Retrieves the item associated bills transactions.',
})
@@ -339,6 +380,7 @@ export class ItemsController extends TenantController {
}
@Get(':id/estimates')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({
summary: 'Retrieves the item associated estimates transactions.',
})
@@ -364,6 +406,7 @@ export class ItemsController extends TenantController {
}
@Get(':id/receipts')
@RequirePermission(ItemAction.VIEW, AbilitySubject.Item)
@ApiOperation({
summary: 'Retrieves the item associated receipts transactions.',
})

View File

@@ -0,0 +1,112 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* Item API Error Types
* These error types are returned when item operations fail validation
*/
export enum ItemErrorType {
/** Item name already exists in the system */
ItemNameExists = 'ITEM_NAME_EXISTS',
/** Item category was not found */
ItemCategoryNotFound = 'ITEM_CATEOGRY_NOT_FOUND',
/** Cost account is not a Cost of Goods Sold account */
CostAccountNotCogs = 'COST_ACCOUNT_NOT_COGS',
/** Cost account was not found */
CostAccountNotFound = 'COST_ACCOUNT_NOT_FOUMD',
/** Sell account was not found */
SellAccountNotFound = 'SELL_ACCOUNT_NOT_FOUND',
/** Sell account is not an income account */
SellAccountNotIncome = 'SELL_ACCOUNT_NOT_INCOME',
/** Inventory account was not found */
InventoryAccountNotFound = 'INVENTORY_ACCOUNT_NOT_FOUND',
/** Account is not an inventory type account */
InventoryAccountNotInventory = 'INVENTORY_ACCOUNT_NOT_INVENTORY',
/** Multiple items have associated transactions */
ItemsHaveAssociatedTransactions = 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
/** Item has associated transactions (singular) */
ItemHasAssociatedTransactions = 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
/** Item has associated inventory adjustments */
ItemHasAssociatedInventoryAdjustment = 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
/** Cannot change item type to inventory */
ItemCannotChangeInventoryType = 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
/** Cannot change type when item has transactions */
TypeCannotChangeWithItemHasTransactions = 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
/** Inventory account cannot be modified */
InventoryAccountCannotModified = 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
/** Purchase tax rate was not found */
PurchaseTaxRateNotFound = 'PURCHASE_TAX_RATE_NOT_FOUND',
/** Sell tax rate was not found */
SellTaxRateNotFound = 'SELL_TAX_RATE_NOT_FOUND',
/** Income account is required for sellable items */
IncomeAccountRequiredWithSellableItem = 'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
/** Cost account is required for purchasable items */
CostAccountRequiredWithPurchasableItem = 'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
/** Item not found */
NotFound = 'NOT_FOUND',
/** Items not found */
ItemsNotFound = 'ITEMS_NOT_FOUND',
}
/**
* Item API Error Response
* Returned when an item operation fails
*/
export class ItemErrorResponseDto {
@ApiProperty({
description: 'HTTP status code',
example: 400,
})
statusCode: number;
@ApiProperty({
description: 'Error type identifier',
enum: ItemErrorType,
example: ItemErrorType.ItemNameExists,
})
type: ItemErrorType;
@ApiProperty({
description: 'Human-readable error message',
example: 'The item name is already exist.',
required: false,
nullable: true,
})
message: string | null;
@ApiProperty({
description: 'Additional error payload data',
required: false,
nullable: true,
})
payload: any;
}
/**
* Item API Error Response Wrapper
*/
export class ItemApiErrorResponseDto {
@ApiProperty({
description: 'Array of error details',
type: [ItemErrorResponseDto],
})
errors: ItemErrorResponseDto[];
}

View File

@@ -70,6 +70,16 @@ export class Item extends TenantBaseModel {
};
}
/**
* Model search roles.
*/
static get searchRoles() {
return [
{ condition: 'or', fieldKey: 'name', comparator: 'contains' },
{ condition: 'or', fieldKey: 'code', comparator: 'like' },
];
}
/**
* Relationship mapping.
*/

View File

@@ -35,7 +35,7 @@ export const transformLedgerEntryToTransaction = (
itemId: entry.itemId,
projectId: entry.projectId,
// costable: entry.costable,
costable: entry.costable,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,

View File

@@ -8,6 +8,7 @@ import {
Post,
Put,
Query,
UseGuards,
} from '@nestjs/common';
import { ManualJournalsApplication } from './ManualJournalsApplication.service';
import {
@@ -29,16 +30,23 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} 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 { ManualJournalAction } from './types/ManualJournals.types';
@Controller('manual-journals')
@ApiTags('Manual Journals')
@ApiExtraModels(ManualJournalResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class ManualJournalsController {
constructor(private manualJournalsApplication: ManualJournalsApplication) { }
@Post('validate-bulk-delete')
@RequirePermission(ManualJournalAction.Delete, AbilitySubject.ManualJournal)
@ApiOperation({
summary:
'Validate which manual journals can be deleted and return the results.',
@@ -60,6 +68,7 @@ export class ManualJournalsController {
}
@Post('bulk-delete')
@RequirePermission(ManualJournalAction.Delete, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Deletes multiple manual journals.' })
@ApiResponse({
status: 200,
@@ -75,6 +84,7 @@ export class ManualJournalsController {
}
@Post()
@RequirePermission(ManualJournalAction.Create, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Create a new manual journal.' })
@ApiResponse({
status: 201,
@@ -86,6 +96,7 @@ export class ManualJournalsController {
}
@Put(':id')
@RequirePermission(ManualJournalAction.Edit, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Edit the given manual journal.' })
@ApiResponse({
status: 200,
@@ -110,6 +121,7 @@ export class ManualJournalsController {
}
@Delete(':id')
@RequirePermission(ManualJournalAction.Delete, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Delete the given manual journal.' })
@ApiResponse({
status: 200,
@@ -127,6 +139,7 @@ export class ManualJournalsController {
}
@Patch(':id/publish')
@RequirePermission(ManualJournalAction.Edit, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Publish the given manual journal.' })
@ApiResponse({
status: 200,
@@ -147,6 +160,7 @@ export class ManualJournalsController {
}
@Get(':id')
@RequirePermission(ManualJournalAction.View, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Retrieves the manual journal details.' })
@ApiResponse({
status: 200,
@@ -167,6 +181,7 @@ export class ManualJournalsController {
}
@Get()
@RequirePermission(ManualJournalAction.View, AbilitySubject.ManualJournal)
@ApiOperation({ summary: 'Retrieves the manual journals paginated list.' })
@ApiResponse({
status: 200,

View File

@@ -17,6 +17,7 @@ import {
HttpCode,
Param,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { BuildOrganizationService } from './commands/BuildOrganization.service';
import {
BuildOrganizationDto,
@@ -50,7 +51,7 @@ export class OrganizationController {
private readonly updateOrganizationService: UpdateOrganizationService,
private readonly getBuildOrganizationJobService: GetBuildOrganizationBuildJob,
private readonly orgBaseCurrencyLockingService: OrganizationBaseCurrencyLocking,
) { }
) {}
@Post('build')
@HttpCode(200)
@@ -77,6 +78,7 @@ export class OrganizationController {
}
@Get('build/:buildJobId')
@Throttle({ default: { limit: 300, ttl: 60000 } }) // 300 req/min
@ApiParam({
name: 'buildJobId',
required: true,

View File

@@ -3,6 +3,8 @@ import { GetCurrentOrganizationService } from './queries/GetCurrentOrganization.
import { BuildOrganizationService } from './commands/BuildOrganization.service';
import { UpdateOrganizationService } from './commands/UpdateOrganization.service';
import { OrganizationController } from './Organization.controller';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq';
import { OrganizationBuildQueue } from './Organization.types';
import { OrganizationBuildProcessor } from './processors/OrganizationBuild.processor';
@@ -25,10 +27,14 @@ import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob
OrganizationBaseCurrencyLocking,
SyncSystemUserToTenantService,
SyncSystemUserToTenantSubscriber,
GetBuildOrganizationBuildJob
GetBuildOrganizationBuildJob,
],
imports: [
BullModule.registerQueue({ name: OrganizationBuildQueue }),
BullBoardModule.forFeature({
name: OrganizationBuildQueue,
adapter: BullMQAdapter,
}),
TenantDBManagerModule,
],
controllers: [OrganizationController],

View File

@@ -2,10 +2,8 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import { ClsService, UseCls } from 'nestjs-cls';
import { Process } from '@nestjs/bull';
import {
OrganizationBuildQueue,
OrganizationBuildQueueJob,
OrganizationBuildQueueJobPayload,
} from '../Organization.types';
import { BuildOrganizationService } from '../commands/BuildOrganization.service';
@@ -22,7 +20,6 @@ export class OrganizationBuildProcessor extends WorkerHost {
super();
}
@Process(OrganizationBuildQueueJob)
@UseCls()
async process(job: Job<OrganizationBuildQueueJobPayload>) {
console.log('Processing organization build job:', job.id);

View File

@@ -19,6 +19,7 @@ import {
Put,
Query,
Res,
UseGuards,
} from '@nestjs/common';
import { PaymentReceivesApplication } from './PaymentReceived.application';
import {
@@ -38,6 +39,11 @@ import {
BulkDeleteDto,
ValidateBulkDeleteResponseDto,
} 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 { PaymentReceiveAction } from './types/PaymentReceived.types';
@Controller('payments-received')
@ApiTags('Payments Received')
@@ -46,6 +52,7 @@ import {
@ApiExtraModels(PaymentReceivedStateResponseDto)
@ApiExtraModels(ValidateBulkDeleteResponseDto)
@ApiCommonHeaders()
@UseGuards(AuthorizationGuard, PermissionGuard)
export class PaymentReceivesController {
constructor(private paymentReceivesApplication: PaymentReceivesApplication) { }
@@ -94,6 +101,7 @@ export class PaymentReceivesController {
}
@Post()
@RequirePermission(PaymentReceiveAction.Create, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Create a new payment received.' })
public createPaymentReceived(
@Body() paymentReceiveDTO: CreatePaymentReceivedDto,
@@ -104,6 +112,7 @@ export class PaymentReceivesController {
}
@Put(':id')
@RequirePermission(PaymentReceiveAction.Edit, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Edit the given payment received.' })
public editPaymentReceive(
@Param('id', ParseIntPipe) paymentReceiveId: number,
@@ -116,6 +125,7 @@ export class PaymentReceivesController {
}
@Delete(':id')
@RequirePermission(PaymentReceiveAction.Delete, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Delete the given payment received.' })
public deletePaymentReceive(
@Param('id', ParseIntPipe) paymentReceiveId: number,
@@ -126,6 +136,7 @@ export class PaymentReceivesController {
}
@Get()
@RequirePermission(PaymentReceiveAction.View, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Retrieves the payment received list.' })
@ApiResponse({
status: 200,
@@ -151,6 +162,7 @@ export class PaymentReceivesController {
}
@Post('validate-bulk-delete')
@RequirePermission(PaymentReceiveAction.Delete, AbilitySubject.PaymentReceive)
@ApiOperation({
summary:
'Validates which payments received can be deleted and returns the results.',
@@ -172,6 +184,7 @@ export class PaymentReceivesController {
}
@Post('bulk-delete')
@RequirePermission(PaymentReceiveAction.Delete, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Deletes multiple payments received.' })
@ApiResponse({
status: 200,
@@ -187,6 +200,7 @@ export class PaymentReceivesController {
}
@Get('state')
@RequirePermission(PaymentReceiveAction.View, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Retrieves the payment received state.' })
@ApiResponse({
status: 200,
@@ -200,6 +214,7 @@ export class PaymentReceivesController {
}
@Get(':id/invoices')
@RequirePermission(PaymentReceiveAction.View, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Retrieves the payment received invoices.' })
@ApiResponse({
status: 200,
@@ -215,6 +230,7 @@ export class PaymentReceivesController {
}
@Get(':id')
@RequirePermission(PaymentReceiveAction.View, AbilitySubject.PaymentReceive)
@ApiOperation({ summary: 'Retrieves the payment received details.' })
@ApiResponse({
status: 200,

View File

@@ -1,5 +1,7 @@
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq';
import { PaymentReceivesController } from './PaymentsReceived.controller';
import { PaymentReceivesApplication } from './PaymentReceived.application';
import { CreatePaymentReceivedService } from './commands/CreatePaymentReceived.serivce';
@@ -95,6 +97,10 @@ import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePa
DynamicListModule,
MailModule,
BullModule.registerQueue({ name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE }),
BullBoardModule.forFeature({
name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
adapter: BullMQAdapter,
}),
],
})
export class PaymentsReceivedModule {}

View File

@@ -1,6 +1,6 @@
import { JOB_REF, Process, Processor } from '@nestjs/bull';
import { Job } from 'bull';
import { Inject, Scope } from '@nestjs/common';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Job } from 'bullmq';
import { Scope } from '@nestjs/common';
import { ClsService, UseCls } from 'nestjs-cls';
import {
SEND_PAYMENT_RECEIVED_MAIL_JOB,
@@ -13,20 +13,18 @@ import { SendPaymentReceivedMailPayload } from '../types/PaymentReceived.types';
name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
scope: Scope.REQUEST,
})
export class SendPaymentReceivedMailProcessor {
export class SendPaymentReceivedMailProcessor extends WorkerHost {
constructor(
private readonly sendPaymentReceivedMail: SendPaymentReceiveMailNotification,
private readonly clsService: ClsService,
) {
super();
}
@Inject(JOB_REF)
private readonly jobRef: Job<SendPaymentReceivedMailPayload>,
) { }
@Process(SEND_PAYMENT_RECEIVED_MAIL_JOB)
@UseCls()
async handleSendMail() {
async process(job: Job<SendPaymentReceivedMailPayload>) {
const { messageOptions, paymentReceivedId, organizationId, userId } =
this.jobRef.data;
job.data;
this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId);

View File

@@ -31,9 +31,10 @@ export class AuthorizationGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const { user } = request as any;
const userId = this.clsService.get('userId');
if (ABILITIES_CACHE.has(user.id)) {
(request as any).ability = ABILITIES_CACHE.get(user.id);
if (ABILITIES_CACHE.has(userId)) {
(request as any).ability = ABILITIES_CACHE.get(userId);
} else {
const ability = await this.getAbilityForUser();
(request as any).ability = ability;

View File

@@ -0,0 +1,67 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { REQUIRED_PERMISSION_KEY, RequiredPermission } from './RequirePermission.decorator';
import { AbilitySubject } from './Roles.types';
/**
* Guard that checks if the user has the required permission to access a route.
* Uses CASL ability instance attached to the request by AuthorizationGuard.
*/
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
/**
* Checks if the user has the required permission to access the route.
* @param context - The execution context
* @returns A boolean indicating if the user can access the route
* @throws ForbiddenException if the user doesn't have the required permission
*/
canActivate(context: ExecutionContext): boolean {
const requiredPermission = this.reflector.getAllAndOverride<RequiredPermission>(
REQUIRED_PERMISSION_KEY,
[context.getHandler(), context.getClass()],
);
// If no permission is required, allow access
if (!requiredPermission) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const ability = (request as any).ability;
// If no ability instance is attached to the request, deny access
if (!ability) {
throw new ForbiddenException('Ability instance not found. Ensure AuthorizationGuard is applied.');
}
const { ability: action, subject } = requiredPermission;
// Check if the user has the required permission using CASL ability
const hasPermission = ability.can(action, subject);
if (!hasPermission) {
throw new ForbiddenException(
`You do not have permission to ${action} ${subject}`,
);
}
return true;
}
/**
* Helper method to check if a subject value is a valid AbilitySubject.
* @param subject - The subject value to check
* @returns True if the subject is a valid AbilitySubject enum value
*/
private isValidSubject(subject: string): subject is AbilitySubject {
return Object.values(AbilitySubject).includes(subject as AbilitySubject);
}
}

View File

@@ -0,0 +1,29 @@
import { SetMetadata } from '@nestjs/common';
import { AbilitySubject } from './Roles.types';
export const REQUIRED_PERMISSION_KEY = 'requiredPermission';
export interface RequiredPermission {
ability: string;
subject: AbilitySubject | string;
}
/**
* Decorator to specify required ability and subject for a route handler or controller.
* @param ability - The ability/action required (e.g., 'Create', 'View', 'Edit', 'Delete')
* @param subject - The subject/entity the ability applies to (e.g., AbilitySubject.Item, AbilitySubject.SaleInvoice)
* @example
* ```typescript
* @RequirePermission('Create', AbilitySubject.Item)
* @Post()
* async createItem(@Body() dto: CreateItemDto) { ... }
*
* @RequirePermission('View', AbilitySubject.SaleInvoice)
* @Get(':id')
* async getInvoice(@Param('id') id: number) { ... }
* ```
*/
export const RequirePermission = (
ability: string,
subject: AbilitySubject | string,
) => SetMetadata(REQUIRED_PERMISSION_KEY, { ability, subject });

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