Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3a4fe6b37 | ||
|
|
02be959461 | ||
|
|
d5bf56e333 | ||
|
|
e3182c15b3 | ||
|
|
dfa63ece21 | ||
|
|
6e95bd7da1 | ||
|
|
f51fffa5c7 | ||
|
|
6193358cc3 | ||
|
|
518abcd30d | ||
|
|
7874b9f765 | ||
|
|
02cc7e0c96 | ||
|
|
57cc513873 | ||
|
|
f5bfdede30 | ||
|
|
488556bb59 | ||
|
|
0fc5a66e95 | ||
|
|
d9ae51027e | ||
|
|
a92d6112d9 | ||
|
|
889b0cec4b | ||
|
|
1c4c41ebba | ||
|
|
421f0c26a7 | ||
|
|
f461cc221b | ||
|
|
acae75a912 | ||
|
|
b5a69971a9 | ||
|
|
04d065b969 | ||
|
|
ca910ee489 | ||
|
|
e3cf6bf099 | ||
|
|
6da7e8185c | ||
|
|
785c49f2e6 | ||
|
|
d7331554ad | ||
|
|
78b1e9136a | ||
|
|
fea9bb5caa | ||
|
|
db5caa138a | ||
|
|
bf821885c0 | ||
|
|
5ce5d8b899 | ||
|
|
458093fca2 | ||
|
|
97e17848f8 | ||
|
|
3dfe884413 | ||
|
|
f26a59f0fb | ||
|
|
7ee161733f | ||
|
|
4efc0b3eb4 | ||
|
|
532aa07e7f | ||
|
|
abacb543c7 | ||
|
|
769eaebc76 | ||
|
|
e0fb345a48 | ||
|
|
c21301061f | ||
|
|
2bbc154f18 | ||
|
|
3c1273becb | ||
|
|
16f1d57279 | ||
|
|
8726b4b3b0 | ||
|
|
5ace03ea99 | ||
|
|
5b6c473780 | ||
|
|
2186828516 | ||
|
|
3f2ab6e8f0 | ||
|
|
f0fae7d148 | ||
|
|
e063597a80 | ||
|
|
9b3f6b22d1 | ||
|
|
0475ce136a | ||
|
|
987ad992a4 | ||
|
|
ee92c2815b | ||
|
|
5767f1f603 | ||
|
|
885d8014c2 | ||
|
|
3ffab896ed | ||
|
|
92a5086f1f | ||
|
|
1bf9038ddc | ||
|
|
2736b76ced | ||
|
|
9e921b074f | ||
|
|
0f377e19f3 | ||
|
|
5d872798ff | ||
|
|
0ef78a19fe | ||
|
|
70b0a4833c | ||
|
|
ead4fc9b97 | ||
|
|
a91a7c612f | ||
|
|
339289be9f | ||
|
|
350d229e98 | ||
|
|
8152a16fd5 | ||
|
|
00aad6e35c | ||
|
|
30d8fdb4c0 | ||
|
|
872fc661ce | ||
|
|
054cd1fae4 | ||
|
|
7cb169bce9 | ||
|
|
f2663c4af3 | ||
|
|
6fea7779da | ||
|
|
c00af18327 | ||
|
|
37f0f4e227 | ||
|
|
8662c5899e | ||
|
|
a9a7cd8617 | ||
|
|
e50fc3b523 | ||
|
|
b294a72a26 | ||
|
|
62ae49941b | ||
|
|
31f5cbf335 | ||
|
|
b22328cff9 | ||
|
|
58f609353c | ||
|
|
8a2a8eed3b | ||
|
|
636d206b0e | ||
|
|
63922c391a | ||
|
|
6ecfe1ff12 | ||
|
|
17651e0768 | ||
|
|
151b623771 | ||
|
|
2d4459c2f9 | ||
|
|
3cbdc3ec96 | ||
|
|
3cfb5cdde8 | ||
|
|
736f2c4109 | ||
|
|
2e21437056 | ||
|
|
340b78d968 | ||
|
|
d006362be2 | ||
|
|
bc21dcb37e | ||
|
|
578b0deb3e | ||
|
|
c3dc26a1e4 | ||
|
|
32d74b0413 | ||
|
|
71b1206f8a | ||
|
|
cb1bcaae77 |
@@ -168,6 +168,16 @@
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Daniel15",
|
||||
"name": "Daniel Lo Nigro",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/91933?v=4",
|
||||
"profile": "https://d.sb/",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
93
.dockerignore
Normal file
93
.dockerignore
Normal 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
|
||||
@@ -35,17 +35,10 @@ TENANT_DB_NAME_PERFIX=bigcapital_tenant_
|
||||
BASE_URL=http://example.com
|
||||
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
|
||||
|
||||
# Jobs MongoDB
|
||||
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
|
||||
|
||||
# App proxy
|
||||
PUBLIC_PROXY_PORT=80
|
||||
PUBLIC_PROXY_SSL_PORT=443
|
||||
|
||||
# Agendash
|
||||
AGENDASH_AUTH_USER=agendash
|
||||
AGENDASH_AUTH_PASSWORD=123123
|
||||
|
||||
# Sign-up restrictions
|
||||
SIGNUP_DISABLED=false
|
||||
SIGNUP_ALLOWED_DOMAINS=
|
||||
|
||||
@@ -135,6 +135,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.camilooviedo.com/"><img src="https://avatars.githubusercontent.com/u/64604272?v=4?s=100" width="100px;" alt="Camilo Oviedo"/><br /><sub><b>Camilo Oviedo</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Champetaman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nklmantey.com/"><img src="https://avatars.githubusercontent.com/u/90279429?v=4?s=100" width="100px;" alt="Mantey"/><br /><sub><b>Mantey</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Anklmantey" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://d.sb/"><img src="https://avatars.githubusercontent.com/u/91933?v=4?s=100" width="100px;" alt="Daniel Lo Nigro"/><br /><sub><b>Daniel Lo Nigro</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3ADaniel15" title="Bug reports">🐛</a> <a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Daniel15" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -32,11 +32,9 @@ services:
|
||||
- '3000'
|
||||
links:
|
||||
- mysql
|
||||
- mongo
|
||||
- redis
|
||||
depends_on:
|
||||
- mysql
|
||||
- mongo
|
||||
- redis
|
||||
restart: on-failure
|
||||
networks:
|
||||
@@ -60,22 +58,21 @@ 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}
|
||||
|
||||
# Authentication
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
|
||||
# MongoDB
|
||||
- MONGODB_DATABASE_URL=mongodb://mongo/bigcapital
|
||||
|
||||
# Application
|
||||
- BASE_URL=${BASE_URL}
|
||||
|
||||
# Agendash
|
||||
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
|
||||
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
|
||||
|
||||
# Sign-up restrictions
|
||||
- SIGNUP_DISABLED=${SIGNUP_DISABLED}
|
||||
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
|
||||
@@ -122,7 +119,7 @@ services:
|
||||
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
|
||||
- S3_ENDPOINT=${S3_ENDPOINT}
|
||||
- S3_BUCKET=${S3_BUCKET}
|
||||
|
||||
|
||||
# Stripe
|
||||
- STRIPE_PAYMENT_SECRET_KEY=${STRIPE_PAYMENT_SECRET_KEY}
|
||||
- STRIPE_PAYMENT_PUBLISHABLE_KEY=${STRIPE_PAYMENT_PUBLISHABLE_KEY}
|
||||
@@ -166,17 +163,6 @@ services:
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
mongo:
|
||||
container_name: bigcapital-mongo
|
||||
restart: on-failure
|
||||
build: ./docker/mongo
|
||||
expose:
|
||||
- '27017'
|
||||
volumes:
|
||||
- mongo:/var/lib/mongodb
|
||||
networks:
|
||||
- bigcapital_network
|
||||
|
||||
redis:
|
||||
container_name: bigcapital-redis
|
||||
restart: on-failure
|
||||
@@ -202,10 +188,6 @@ volumes:
|
||||
name: bigcapital_prod_mysql
|
||||
driver: local
|
||||
|
||||
mongo:
|
||||
name: bigcapital_prod_mongo
|
||||
driver: local
|
||||
|
||||
redis:
|
||||
name: bigcapital_prod_redis
|
||||
driver: local
|
||||
|
||||
@@ -24,25 +24,13 @@ services:
|
||||
restart_policy:
|
||||
condition: unless-stopped
|
||||
|
||||
mongo:
|
||||
build: ./docker/mongo
|
||||
expose:
|
||||
- '27017'
|
||||
volumes:
|
||||
- mongo:/var/lib/mongodb
|
||||
ports:
|
||||
- '27017:27017'
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: unless-stopped
|
||||
|
||||
redis:
|
||||
build:
|
||||
context: ./docker/redis
|
||||
expose:
|
||||
- "6379"
|
||||
- '6379'
|
||||
ports:
|
||||
- "6379:6379"
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- redis:/data
|
||||
deploy:
|
||||
@@ -52,7 +40,7 @@ services:
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:7
|
||||
ports:
|
||||
- "9000:3000"
|
||||
- '9000:3000'
|
||||
|
||||
# Volumes
|
||||
volumes:
|
||||
@@ -60,10 +48,6 @@ volumes:
|
||||
name: bigcapital_dev_mysql
|
||||
driver: local
|
||||
|
||||
mongo:
|
||||
name: bigcapital_dev_mongo
|
||||
driver: local
|
||||
|
||||
redis:
|
||||
name: bigcapital_dev_redis
|
||||
driver: local
|
||||
driver: local
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
FROM mongo:5.0
|
||||
@@ -35,17 +35,10 @@ TENANT_DB_NAME_PERFIX=bigcapital_tenant_
|
||||
BASE_URL=http://example.com
|
||||
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
|
||||
|
||||
# Jobs MongoDB
|
||||
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
|
||||
|
||||
# App proxy
|
||||
PUBLIC_PROXY_PORT=80
|
||||
PUBLIC_PROXY_SSL_PORT=443
|
||||
|
||||
# Agendash
|
||||
AGENDASH_AUTH_USER=agendash
|
||||
AGENDASH_AUTH_PASSWORD=123123
|
||||
|
||||
# Sign-up restrictions
|
||||
SIGNUP_DISABLED=false
|
||||
SIGNUP_ALLOWED_DOMAINS=
|
||||
|
||||
102
packages/server/Dockerfile
Normal file
102
packages/server/Dockerfile
Normal file
@@ -0,0 +1,102 @@
|
||||
# Stage 1: Build
|
||||
FROM node:18.16.0-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm@8.10.2
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache python3 build-base chromium
|
||||
|
||||
# Set Python environment
|
||||
ENV PYTHON=/usr/bin/python3
|
||||
|
||||
# Copy package files for dependency installation
|
||||
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./
|
||||
COPY --chown=node:node packages/server/package.json ./packages/server/
|
||||
COPY --chown=node:node shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
|
||||
COPY --chown=node:node shared/pdf-templates/package.json ./shared/pdf-templates/
|
||||
COPY --chown=node:node shared/email-components/package.json ./shared/email-components/
|
||||
|
||||
# Install all dependencies (including devDependencies for build)
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code
|
||||
COPY --chown=node:node ./packages/server ./packages/server
|
||||
COPY --chown=node:node ./shared/bigcapital-utils ./shared/bigcapital-utils
|
||||
COPY --chown=node:node ./shared/pdf-templates ./shared/pdf-templates
|
||||
COPY --chown=node:node ./shared/email-components ./shared/email-components
|
||||
|
||||
# Build NestJS application
|
||||
RUN pnpm run build:server --skip-nx-cache
|
||||
|
||||
# Stage 2: Production
|
||||
FROM node:18.16.0-alpine AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm for production
|
||||
RUN npm install -g pnpm@8.10.2
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
adduser -S nodejs -u 1001
|
||||
|
||||
# Install build dependencies for native modules (bcrypt, etc.)
|
||||
RUN apk add --no-cache python3 build-base
|
||||
|
||||
# Set Python environment
|
||||
ENV PYTHON=/usr/bin/python3
|
||||
|
||||
# Copy package files for production dependency installation
|
||||
COPY --chown=nodejs:nodejs package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY --chown=nodejs:nodejs packages/server/package.json ./packages/server/
|
||||
COPY --chown=nodejs:nodejs shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
|
||||
COPY --chown=nodejs:nodejs shared/pdf-templates/package.json ./shared/pdf-templates/
|
||||
COPY --chown=nodejs:nodejs shared/email-components/package.json ./shared/email-components/
|
||||
|
||||
# Copy .husky directory (needed for husky install command)
|
||||
COPY --chown=nodejs:nodejs .husky ./.husky
|
||||
|
||||
# Install only production dependencies
|
||||
# Install husky temporarily so prepare script can run, then remove it
|
||||
RUN pnpm add -D -w husky && \
|
||||
pnpm install --prod --frozen-lockfile && \
|
||||
pnpm remove -w husky && \
|
||||
# Remove build dependencies to reduce image size
|
||||
apk del python3 build-base
|
||||
|
||||
# Copy built application from builder stage
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/dist ./packages/server/dist
|
||||
|
||||
# Copy static assets (i18n, public, static directories)
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/i18n ./packages/server/dist/i18n
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/public ./packages/server/public
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/static ./packages/server/static
|
||||
|
||||
# Copy database migration files (needed for running migrations)
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/database ./packages/server/src/database
|
||||
|
||||
# Copy built shared packages (dist folders and package.json for module resolution)
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared/bigcapital-utils/dist ./shared/bigcapital-utils/dist
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared/pdf-templates/dist ./shared/pdf-templates/dist
|
||||
COPY --from=builder --chown=nodejs:nodejs /app/shared/email-components/dist ./shared/email-components/dist
|
||||
|
||||
# Set runtime environment variables (these should be provided at runtime via docker-compose or k8s)
|
||||
ENV NODE_ENV=production
|
||||
ENV NEW_RELIC_NO_CONFIG_FILE=true
|
||||
ENV PORT=3000
|
||||
|
||||
# Switch to non-root user
|
||||
USER nodejs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check - uses /api/system_db ping endpoint
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000/api/system_db', (r) => {process.exit(r.statusCode >= 200 && r.statusCode < 300 ? 0 : 1)}).on('error', () => process.exit(1))"
|
||||
|
||||
# Start the application
|
||||
CMD [ "node", "packages/server/dist/main.js" ]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -167,6 +170,9 @@
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
"testEnvironment": "node",
|
||||
"moduleNameMapper": {
|
||||
"^@/(.*)$": "<rootDir>/$1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
packages/server/src/common/config/app.ts
Normal file
5
packages/server/src/common/config/app.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('app', () => ({
|
||||
baseUrl: process.env.BASE_URL,
|
||||
}));
|
||||
8
packages/server/src/common/config/bull-board.ts
Normal file
8
packages/server/src/common/config/bull-board.ts
Normal 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,
|
||||
}));
|
||||
@@ -1,3 +1,4 @@
|
||||
import app from './app';
|
||||
import systemDatabase from './system-database';
|
||||
import tenantDatabase from './tenant-database';
|
||||
import signup from './signup';
|
||||
@@ -16,8 +17,12 @@ 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,
|
||||
systemDatabase,
|
||||
cloud,
|
||||
tenantDatabase,
|
||||
@@ -36,4 +41,7 @@ export const config = [
|
||||
loops,
|
||||
bankfeed,
|
||||
throttle,
|
||||
redis,
|
||||
queue,
|
||||
bullBoard,
|
||||
];
|
||||
|
||||
6
packages/server/src/common/config/queue.ts
Normal file
6
packages/server/src/common/config/queue.ts
Normal 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,
|
||||
}));
|
||||
@@ -7,53 +7,46 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { type Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { mapKeysDeep } from '@/utils/deepdash';
|
||||
|
||||
export function camelToSnake<T = any>(value: T) {
|
||||
export function camelToSnake<T = any>(value: T): T {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(camelToSnake);
|
||||
}
|
||||
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => [
|
||||
key
|
||||
.split(/(?=[A-Z])/)
|
||||
.join('_')
|
||||
.toLowerCase(),
|
||||
camelToSnake(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
return mapKeysDeep(
|
||||
value,
|
||||
(_value: string, key: any, parent: any, context: any) => {
|
||||
if (Array.isArray(parent)) {
|
||||
// tell mapKeysDeep to skip mapping inside this branch
|
||||
context.skipChildren = true;
|
||||
return key;
|
||||
}
|
||||
return key
|
||||
.split(/(?=[A-Z])/)
|
||||
.join('_')
|
||||
.toLowerCase();
|
||||
},
|
||||
) as T;
|
||||
}
|
||||
|
||||
export function snakeToCamel<T = any>(value: T) {
|
||||
export function snakeToCamel<T = any>(value: T): T {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(snakeToCamel);
|
||||
}
|
||||
|
||||
const impl = (str: string) => {
|
||||
const converted = str.replace(/([-_]\w)/g, (group) =>
|
||||
group[1].toUpperCase(),
|
||||
);
|
||||
return converted[0].toLowerCase() + converted.slice(1);
|
||||
};
|
||||
|
||||
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, value]) => [
|
||||
impl(key),
|
||||
snakeToCamel(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
return value;
|
||||
return mapKeysDeep(
|
||||
value,
|
||||
(_value: string, key: any, parent: any, context: any) => {
|
||||
if (Array.isArray(parent)) {
|
||||
// tell mapKeysDeep to skip mapping inside this branch
|
||||
context.skipChildren = true;
|
||||
return key;
|
||||
}
|
||||
const converted = key.replace(/([-_]\w)/g, (group) =>
|
||||
group[1].toUpperCase(),
|
||||
);
|
||||
return converted[0].toLowerCase() + converted.slice(1);
|
||||
},
|
||||
) as T;
|
||||
}
|
||||
|
||||
export const DEFAULT_STRATEGY = {
|
||||
@@ -63,7 +56,7 @@ export const DEFAULT_STRATEGY = {
|
||||
|
||||
@Injectable()
|
||||
export class SerializeInterceptor implements NestInterceptor<any, any> {
|
||||
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) {}
|
||||
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) { }
|
||||
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
|
||||
@@ -5,15 +5,18 @@ import {
|
||||
NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { mapValues, mapValuesDeep } from '@/utils/deepdash';
|
||||
import { mapValuesDeep } from '@/utils/deepdash';
|
||||
|
||||
@Injectable()
|
||||
export class ToJsonInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
if (data === null || data === undefined) {
|
||||
return data;
|
||||
}
|
||||
return mapValuesDeep(data, (value) => {
|
||||
if (value && typeof value.toJSON === 'function') {
|
||||
if (value !== null && value !== undefined && typeof value.toJSON === 'function') {
|
||||
return value.toJSON();
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
// import { getTransactionsLockingSettingsSchema } from '@/api/controllers/TransactionsLocking/utils';
|
||||
import { chain, mapKeys } from 'lodash';
|
||||
|
||||
const getTransactionsLockingSettingsSchema = (modules: string[]) => {
|
||||
const moduleSchema = {
|
||||
active: { type: 'boolean' },
|
||||
lock_to_date: { type: 'date' },
|
||||
unlock_from_date: { type: 'date' },
|
||||
unlock_to_date: { type: 'date' },
|
||||
lock_reason: { type: 'string' },
|
||||
unlock_reason: { type: 'string' },
|
||||
};
|
||||
return chain(modules)
|
||||
.map((module: string) => {
|
||||
return mapKeys(moduleSchema, (value, key: string) => `${module}.${key}`);
|
||||
})
|
||||
.flattenDeep()
|
||||
.reduce((result, value) => {
|
||||
return {
|
||||
...result,
|
||||
...value,
|
||||
};
|
||||
}, {})
|
||||
.value();
|
||||
};
|
||||
|
||||
export const SettingsOptions = {
|
||||
organization: {
|
||||
@@ -223,12 +246,12 @@ export const SettingsOptions = {
|
||||
'locking-type': {
|
||||
type: 'string',
|
||||
},
|
||||
// ...getTransactionsLockingSettingsSchema([
|
||||
// 'all',
|
||||
// 'sales',
|
||||
// 'purchases',
|
||||
// 'financial',
|
||||
// ]),
|
||||
...getTransactionsLockingSettingsSchema([
|
||||
'all',
|
||||
'sales',
|
||||
'purchases',
|
||||
'financial',
|
||||
]),
|
||||
},
|
||||
features: {
|
||||
'multi-warehouses': {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"field.description": "Description",
|
||||
"field.slug": "Account slug",
|
||||
"field.code": "Account code",
|
||||
"field.code_hint": "Unique number to identify the account.",
|
||||
"field.root_type": "Root type",
|
||||
"field.normal": "Account normal",
|
||||
"field.normal.credit": "Credit",
|
||||
@@ -13,5 +14,6 @@
|
||||
"field.balance": "Balance",
|
||||
"field.bank_balance": "Bank Balance",
|
||||
"field.parent_account": "Parent Account",
|
||||
"field.created_at": "Created at"
|
||||
"field.created_at": "Created at",
|
||||
"field.account_hint": "Matches the account name or code."
|
||||
}
|
||||
|
||||
30
packages/server/src/i18n/en/bill.json
Normal file
30
packages/server/src/i18n/en/bill.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"field.vendor": "Vendor",
|
||||
"field.bill_number": "Bill No.",
|
||||
"field.bill_date": "Date",
|
||||
"field.due_date": "Due Date",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.note": "Note",
|
||||
"field.open": "Open",
|
||||
"field.entries": "Entries",
|
||||
"field.item": "Item",
|
||||
"field.item_hint": "Matches the item name or code.",
|
||||
"field.rate": "Rate",
|
||||
"field.quantity": "Quantity",
|
||||
"field.description": "Line Description",
|
||||
"field.amount": "Amount",
|
||||
"field.payment_amount": "Payment Amount",
|
||||
"field.status": "Status",
|
||||
"field.status.paid": "Paid",
|
||||
"field.status.partially-paid": "Partially Paid",
|
||||
"field.status.overdue": "Overdue",
|
||||
"field.status.unpaid": "Unpaid",
|
||||
"field.status.opened": "Opened",
|
||||
"field.status.draft": "Draft",
|
||||
"field.created_at": "Created At",
|
||||
"allocation_method": "Allocation Method",
|
||||
"allocation_method.quantity": "Quantity",
|
||||
"allocation_method.value": "Valuation"
|
||||
}
|
||||
|
||||
15
packages/server/src/i18n/en/bill_payment.json
Normal file
15
packages/server/src/i18n/en/bill_payment.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"field.vendor": "Vendor",
|
||||
"field.payment_date": "Payment Date",
|
||||
"field.payment_number": "Payment No.",
|
||||
"field.payment_account": "Payment Account",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.note": "Note",
|
||||
"field.reference": "Reference",
|
||||
"field.entries": "Entries",
|
||||
"field.entries.bill": "Bill",
|
||||
"field.entries.payment_amount": "Payment Amount",
|
||||
"field.payment_number_hint": "The payment number should be unique.",
|
||||
"field.bill_hint": "Matches the bill number."
|
||||
}
|
||||
|
||||
16
packages/server/src/i18n/en/credit_note.json
Normal file
16
packages/server/src/i18n/en/credit_note.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"field.customer": "Customer",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.credit_note_date": "Credit Note Date",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.note": "Note",
|
||||
"field.terms_conditions": "Terms & Conditions",
|
||||
"field.credit_note_number": "Credit Note Number",
|
||||
"field.open": "Open",
|
||||
"field.entries": "Entries",
|
||||
"field.item": "Item",
|
||||
"field.rate": "Rate",
|
||||
"field.quantity": "Quantity",
|
||||
"field.description": "Description"
|
||||
}
|
||||
|
||||
5
packages/server/src/i18n/en/customer.json
Normal file
5
packages/server/src/i18n/en/customer.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"type.business": "Business",
|
||||
"type.individual": "Individual"
|
||||
}
|
||||
|
||||
21
packages/server/src/i18n/en/estimate.json
Normal file
21
packages/server/src/i18n/en/estimate.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"field.customer": "Customer",
|
||||
"field.estimate_date": "Estimate Date",
|
||||
"field.expiration_date": "Expiration Date",
|
||||
"field.estimate_number": "Estimate No.",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.currency": "Currency",
|
||||
"field.note": "Note",
|
||||
"field.terms_conditions": "Terms & Conditions",
|
||||
"field.delivered": "Delivered",
|
||||
"field.entries": "Entries",
|
||||
"field.amount": "Amount",
|
||||
"field.status": "Status",
|
||||
"field.status.draft": "Draft",
|
||||
"field.status.delivered": "Delivered",
|
||||
"field.status.rejected": "Rejected",
|
||||
"field.status.approved": "Approved",
|
||||
"field.created_at": "Created At"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"field.due_amount": "Due amount",
|
||||
"field.delivered": "Delivered",
|
||||
"field.item_name": "Item Name",
|
||||
"field.item_hint": "Matches the item name or code.",
|
||||
"field.rate": "Rate",
|
||||
"field.quantity": "Quantity",
|
||||
"field.description": "Description",
|
||||
@@ -38,5 +39,7 @@
|
||||
"field.status.draft": "Draft",
|
||||
"field.created_at": "Created at",
|
||||
"field.currency": "Currency",
|
||||
"field.entries": "Entries"
|
||||
"field.entries": "Entries",
|
||||
"field.branch": "Branch",
|
||||
"field.warehouse": "Warehouse"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"field.quantity_on_hand": "Quantity on Hand",
|
||||
"field.note": "Note",
|
||||
"field.category": "Category",
|
||||
"field.category_hint": "Matches the category name.",
|
||||
"field.active": "Active",
|
||||
"field.created_at": "Created At"
|
||||
}
|
||||
|
||||
17
packages/server/src/i18n/en/payment_receive.json
Normal file
17
packages/server/src/i18n/en/payment_receive.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"field.customer": "Customer",
|
||||
"field.payment_date": "Payment Date",
|
||||
"field.amount": "Amount",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.deposit_account": "Deposit Account",
|
||||
"field.payment_receive_no": "Payment No.",
|
||||
"field.statement": "Statement",
|
||||
"field.entries": "Entries",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.invoice": "Invoice",
|
||||
"field.entries.payment_amount": "Payment Amount",
|
||||
"field.created_at": "Created At",
|
||||
"field.payment_no_hint": "The payment number should be unique.",
|
||||
"field.invoice_hint": "Matches the invoice number."
|
||||
}
|
||||
|
||||
@@ -10,5 +10,21 @@
|
||||
"paper.receipt_amount": "Receipt amount",
|
||||
"paper.total": "Total",
|
||||
"paper.balance_due": "Balance Due",
|
||||
"paper.payment_amount": "Payment Amount"
|
||||
"paper.payment_amount": "Payment Amount",
|
||||
|
||||
"field.receipt_date": "Receipt Date",
|
||||
"field.customer": "Customer",
|
||||
"field.deposit_account": "Deposit Account",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.receipt_number": "Receipt Number",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.closed": "Closed",
|
||||
"field.entries": "Entries",
|
||||
"field.statement": "Statement",
|
||||
"field.receipt_message": "Receipt Message",
|
||||
"field.amount": "Amount",
|
||||
"field.status": "Status",
|
||||
"field.status.draft": "Draft",
|
||||
"field.status.closed": "Closed",
|
||||
"field.created_at": "Created At"
|
||||
}
|
||||
@@ -2,5 +2,18 @@
|
||||
"view.draft": "Draft",
|
||||
"view.published": "Published",
|
||||
"view.open": "Open",
|
||||
"view.closed": "Closed"
|
||||
"view.closed": "Closed",
|
||||
|
||||
"field.vendor": "Vendor",
|
||||
"field.vendor_credit_number": "Vendor Credit No.",
|
||||
"field.vendor_credit_date": "Vendor Credit Date",
|
||||
"field.reference_no": "Reference No.",
|
||||
"field.exchange_rate": "Exchange Rate",
|
||||
"field.note": "Note",
|
||||
"field.open": "Open",
|
||||
"field.entries": "Entries",
|
||||
"field.item": "Item",
|
||||
"field.rate": "Rate",
|
||||
"field.quantity": "Quantity",
|
||||
"field.description": "Description"
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import { ClsMiddleware } from 'nestjs-cls';
|
||||
import * as path from 'path';
|
||||
import './utils/moment-mysql';
|
||||
import { AppModule } from './modules/App/App.module';
|
||||
import { ServiceErrorFilter } from './common/filters/service-error.filter';
|
||||
import { ModelHasRelationsFilter } from './common/filters/model-has-relations.filter';
|
||||
import { ValidationPipe } from './common/pipes/ClassValidation.pipe';
|
||||
import { ToJsonInterceptor } from './common/interceptors/to-json.interceptor';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
|
||||
global.__public_dirname = path.join(__dirname, '..', 'public');
|
||||
global.__static_dirname = path.join(__dirname, '../static');
|
||||
@@ -15,17 +12,15 @@ global.__views_dirname = path.join(global.__static_dirname, '/views');
|
||||
global.__images_dirname = path.join(global.__static_dirname, '/images');
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { rawBody: true });
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
rawBody: true,
|
||||
});
|
||||
app.set('query parser', 'extended');
|
||||
app.setGlobalPrefix('/api');
|
||||
|
||||
// create and mount the middleware manually here
|
||||
app.use(new ClsMiddleware({}).use);
|
||||
|
||||
app.useGlobalInterceptors(new ToJsonInterceptor());
|
||||
|
||||
// use the validation pipe globally
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Bigcapital')
|
||||
.setDescription('Financial accounting software')
|
||||
@@ -35,9 +30,6 @@ async function bootstrap() {
|
||||
const documentFactory = () => SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('swagger', app, documentFactory);
|
||||
|
||||
app.useGlobalFilters(new ServiceErrorFilter());
|
||||
app.useGlobalFilters(new ModelHasRelationsFilter());
|
||||
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
59
packages/server/src/middleware/bull-board-auth.middleware.ts
Normal file
59
packages/server/src/middleware/bull-board-auth.middleware.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
@@ -48,14 +48,30 @@ export class PaginationQueryBuilder<
|
||||
// No relations defined
|
||||
return this.delete();
|
||||
}
|
||||
|
||||
// Only check HasManyRelation and ManyToManyRelation relations, as BelongsToOneRelation are just
|
||||
// foreign key references and shouldn't prevent deletion. Only dependent records should block deletion.
|
||||
const dependentRelationNames = relationNames.filter((name) => {
|
||||
const relation = relationMappings[name];
|
||||
return relation && (
|
||||
relation.relation === Model.HasManyRelation ||
|
||||
relation.relation === Model.ManyToManyRelation
|
||||
);
|
||||
});
|
||||
|
||||
if (dependentRelationNames.length === 0) {
|
||||
// No dependent relations defined, safe to delete
|
||||
return this.delete();
|
||||
}
|
||||
|
||||
const recordQuery = this.clone();
|
||||
|
||||
relationNames.forEach((relationName: string) => {
|
||||
dependentRelationNames.forEach((relationName: string) => {
|
||||
recordQuery.withGraphFetched(relationName);
|
||||
});
|
||||
const record = await recordQuery;
|
||||
|
||||
const hasRelations = relationNames.some((name) => {
|
||||
const hasRelations = dependentRelationNames.some((name) => {
|
||||
const val = record[name];
|
||||
return Array.isArray(val) ? val.length > 0 : val != null;
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
Put,
|
||||
HttpCode,
|
||||
} from '@nestjs/common';
|
||||
import { AccountsApplication } from './AccountsApplication.service';
|
||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||
@@ -43,6 +44,7 @@ export class AccountsController {
|
||||
constructor(private readonly accountsApplication: AccountsApplication) { }
|
||||
|
||||
@Post('validate-bulk-delete')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
|
||||
@@ -64,6 +66,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Post('bulk-delete')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -125,6 +128,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Post(':id/activate')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Activate the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -142,6 +146,7 @@ export class AccountsController {
|
||||
}
|
||||
|
||||
@Post(':id/inactivate')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: 'Inactivate the given account.' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
|
||||
@@ -18,7 +18,7 @@ export class DeleteAccount {
|
||||
private eventEmitter: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
private validator: CommandAccountValidators,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Authorize account delete.
|
||||
@@ -57,7 +57,10 @@ export class DeleteAccount {
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
// Retrieve account or not found service error.
|
||||
const oldAccount = await this.accountModel().query().findById(accountId);
|
||||
const oldAccount = await this.accountModel()
|
||||
.query()
|
||||
.findById(accountId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Authorize before delete account.
|
||||
await this.authorize(accountId, oldAccount);
|
||||
|
||||
@@ -156,7 +156,7 @@ export const AccountMeta = {
|
||||
minLength: 3,
|
||||
maxLength: 6,
|
||||
unique: true,
|
||||
importHint: 'Unique number to identify the account.',
|
||||
importHint: 'account.field.code_hint',
|
||||
},
|
||||
accountType: {
|
||||
name: 'account.field.type',
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE, APP_FILTER } from '@nestjs/core';
|
||||
import { join } from 'path';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { RedisModule } from '@liaoliaots/nestjs-redis';
|
||||
@@ -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';
|
||||
@@ -36,6 +39,10 @@ import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
|
||||
import { BranchesModule } from '../Branches/Branches.module';
|
||||
import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
||||
import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor';
|
||||
import { ToJsonInterceptor } from '@/common/interceptors/to-json.interceptor';
|
||||
import { ValidationPipe } from '@/common/pipes/ClassValidation.pipe';
|
||||
import { ServiceErrorFilter } from '@/common/filters/service-error.filter';
|
||||
import { ModelHasRelationsFilter } from '@/common/filters/model-has-relations.filter';
|
||||
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
|
||||
import { CustomersModule } from '../Customers/Customers.module';
|
||||
import { VendorsModule } from '../Vendors/Vendors.module';
|
||||
@@ -133,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: {
|
||||
@@ -154,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],
|
||||
@@ -234,6 +259,10 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useClass: ValidationPipe,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
@@ -242,6 +271,10 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: SerializeInterceptor,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ToJsonInterceptor,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: UserIpInterceptor,
|
||||
@@ -250,6 +283,14 @@ import { AppThrottleModule } from './AppThrottle.module';
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ExcludeNullInterceptor,
|
||||
},
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: ServiceErrorFilter,
|
||||
},
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: ModelHasRelationsFilter,
|
||||
},
|
||||
AppService,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -9,6 +9,27 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => {
|
||||
// Use in-memory storage with very high limits for test environment
|
||||
const isTest = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
|
||||
|
||||
if (isTest) {
|
||||
return {
|
||||
throttlers: [
|
||||
{
|
||||
name: 'default',
|
||||
ttl: 60000,
|
||||
limit: 1000000, // Effectively disable throttling in tests
|
||||
},
|
||||
{
|
||||
name: 'auth',
|
||||
ttl: 60000,
|
||||
limit: 1000000, // Effectively disable throttling in tests
|
||||
},
|
||||
],
|
||||
// No storage specified = uses in-memory storage
|
||||
};
|
||||
}
|
||||
|
||||
const host = configService.get<string>('redis.host') || 'localhost';
|
||||
const port = Number(configService.get<number>('redis.port') || 6379);
|
||||
const password = configService.get<string>('redis.password');
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import { Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { BankAccountsApplication } from './BankAccountsApplication.service';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { ICashflowAccountsFilter } from './types/BankAccounts.types';
|
||||
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { BankAccountsQueryDto } from './dtos/BankAccountsQuery.dto';
|
||||
import { BankAccountResponseDto } from './dtos/BankAccountResponse.dto';
|
||||
|
||||
@Controller('banking/accounts')
|
||||
@ApiTags('Bank Accounts')
|
||||
export class BankAccountsController {
|
||||
constructor(private bankAccountsApplication: BankAccountsApplication) {}
|
||||
constructor(private bankAccountsApplication: BankAccountsApplication) { }
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieve the bank accounts.' })
|
||||
getBankAccounts(@Query() filterDto: ICashflowAccountsFilter) {
|
||||
@ApiQuery({
|
||||
name: 'query',
|
||||
description: 'Query parameters for the bank accounts list.',
|
||||
type: BankAccountsQueryDto,
|
||||
required: false,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'List of bank accounts retrieved successfully.',
|
||||
type: [BankAccountResponseDto],
|
||||
})
|
||||
getBankAccounts(@Query() filterDto: BankAccountsQueryDto) {
|
||||
return this.bankAccountsApplication.getBankAccounts(filterDto);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PauseBankAccountFeeds } from './commands/PauseBankAccountFeeds.service'
|
||||
import { GetBankAccountsService } from './queries/GetBankAccounts';
|
||||
import { ICashflowAccountsFilter } from './types/BankAccounts.types';
|
||||
import { GetBankAccountSummary } from './queries/GetBankAccountSummary';
|
||||
import { BankAccountsQueryDto } from './dtos/BankAccountsQuery.dto';
|
||||
|
||||
@Injectable()
|
||||
export class BankAccountsApplication {
|
||||
@@ -16,13 +17,13 @@ export class BankAccountsApplication {
|
||||
private readonly refreshBankAccountService: RefreshBankAccountService,
|
||||
private readonly resumeBankAccountFeedsService: ResumeBankAccountFeedsService,
|
||||
private readonly pauseBankAccountFeedsService: PauseBankAccountFeeds,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Retrieves the bank accounts.
|
||||
* @param {ICashflowAccountsFilter} filterDto -
|
||||
*/
|
||||
getBankAccounts(filterDto: ICashflowAccountsFilter) {
|
||||
getBankAccounts(filterDto: BankAccountsQueryDto) {
|
||||
return this.getBankAccountsService.getCashflowAccounts(filterDto);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* Bank Account Response DTO
|
||||
* Based on AccountResponseDto but excludes fields that are filtered out by CashflowAccountTransformer:
|
||||
* - predefined
|
||||
* - index
|
||||
* - accountTypeLabel
|
||||
*/
|
||||
export class BankAccountResponseDto {
|
||||
@ApiProperty({
|
||||
description: 'The unique identifier of the account',
|
||||
example: 1,
|
||||
})
|
||||
id: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The name of the account',
|
||||
example: 'Cash Account',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The slug of the account',
|
||||
example: 'cash-account',
|
||||
})
|
||||
slug: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The code of the account',
|
||||
example: '1001',
|
||||
})
|
||||
code: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The type of the account',
|
||||
example: 'bank',
|
||||
})
|
||||
accountType: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The parent account ID',
|
||||
example: null,
|
||||
})
|
||||
parentAccountId: number | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The currency code of the account',
|
||||
example: 'USD',
|
||||
})
|
||||
currencyCode: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the account is active',
|
||||
example: true,
|
||||
})
|
||||
active: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The bank balance of the account',
|
||||
example: 5000.0,
|
||||
})
|
||||
bankBalance: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The formatted bank balance',
|
||||
example: '$5,000.00',
|
||||
})
|
||||
bankBalanceFormatted: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The last feeds update timestamp',
|
||||
example: '2024-03-20T10:30:00Z',
|
||||
})
|
||||
lastFeedsUpdatedAt: string | Date | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The formatted last feeds update timestamp',
|
||||
example: 'Mar 20, 2024 10:30 AM',
|
||||
})
|
||||
lastFeedsUpdatedAtFormatted: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The last feeds updated from now (relative time)',
|
||||
example: '2 hours ago',
|
||||
})
|
||||
lastFeedsUpdatedFromNow: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The amount of the account',
|
||||
example: 5000.0,
|
||||
})
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The formatted amount',
|
||||
example: '$5,000.00',
|
||||
})
|
||||
formattedAmount: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The Plaid item ID',
|
||||
example: 'plaid-item-123',
|
||||
})
|
||||
plaidItemId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The Plaid account ID',
|
||||
example: 'plaid-account-456',
|
||||
})
|
||||
plaidAccountId: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the feeds are active',
|
||||
example: true,
|
||||
})
|
||||
isFeedsActive: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the account is syncing owner',
|
||||
example: true,
|
||||
})
|
||||
isSyncingOwner: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether the feeds are paused',
|
||||
example: false,
|
||||
})
|
||||
isFeedsPaused: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The account normal',
|
||||
example: 'debit',
|
||||
})
|
||||
accountNormal: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The formatted account normal',
|
||||
example: 'Debit',
|
||||
})
|
||||
accountNormalFormatted: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The flatten name with all dependant accounts names',
|
||||
example: 'Assets: Cash Account',
|
||||
})
|
||||
flattenName: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The account level in the hierarchy',
|
||||
example: 2,
|
||||
})
|
||||
accountLevel?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The creation timestamp',
|
||||
example: '2024-03-20T10:00:00Z',
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The update timestamp',
|
||||
example: '2024-03-20T10:30:00Z',
|
||||
})
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, Min } from 'class-validator';
|
||||
import { ToNumber } from '@/common/decorators/Validators';
|
||||
import { IFilterRole, ISortOrder } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
|
||||
export class BankAccountsQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'Custom view ID',
|
||||
type: Number,
|
||||
example: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
customViewId?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Filter roles array',
|
||||
type: Array,
|
||||
isArray: true,
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
filterRoles?: IFilterRole[];
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Column to sort by',
|
||||
type: String,
|
||||
example: 'created_at',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
columnSortBy?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Sort order',
|
||||
enum: ISortOrder,
|
||||
example: ISortOrder.DESC,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(ISortOrder)
|
||||
sortOrder?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Stringified filter roles',
|
||||
type: String,
|
||||
example: '{"fieldKey":"status","value":"active"}',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
stringifiedFilterRoles?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Search keyword',
|
||||
type: String,
|
||||
example: 'bank account',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
searchKeyword?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'View slug',
|
||||
type: String,
|
||||
example: 'active-accounts',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
viewSlug?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Page number',
|
||||
type: Number,
|
||||
example: 1,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Page size',
|
||||
type: Number,
|
||||
example: 25,
|
||||
minimum: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@ToNumber()
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Include inactive accounts',
|
||||
type: Boolean,
|
||||
example: false,
|
||||
default: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsBoolean()
|
||||
inactiveMode?: boolean;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { ICashflowAccountsFilter } from '../types/BankAccounts.types';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
|
||||
import { BankAccountsQueryDto } from '../dtos/BankAccountsQuery.dto';
|
||||
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
|
||||
@Injectable()
|
||||
export class GetBankAccountsService {
|
||||
@@ -15,14 +17,14 @@ export class GetBankAccountsService {
|
||||
|
||||
@Inject(Account.name)
|
||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow accounts.
|
||||
* @param {ICashflowAccountsFilter} filterDTO - Filter DTO.
|
||||
* @returns {ICashflowAccount[]}
|
||||
*/
|
||||
public async getCashflowAccounts(filterDTO: ICashflowAccountsFilter) {
|
||||
public async getCashflowAccounts(filterDTO: BankAccountsQueryDto) {
|
||||
const _filterDto = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
@@ -30,12 +32,14 @@ export class GetBankAccountsService {
|
||||
...filterDTO,
|
||||
};
|
||||
// Parsees accounts list filter DTO.
|
||||
const filter = this.dynamicListService.parseStringifiedFilter(_filterDto);
|
||||
const filter = this.dynamicListService.parseStringifiedFilter<BankAccountsQueryDto>(
|
||||
_filterDto,
|
||||
);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicList = await this.dynamicListService.dynamicList(
|
||||
this.accountModel(),
|
||||
filter,
|
||||
filter as IDynamicListFilter,
|
||||
);
|
||||
// Retrieve accounts model based on the given query.
|
||||
const accounts = await this.accountModel()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import * as yup from 'yup';
|
||||
import uniqid from 'uniqid';
|
||||
import * as uniqid from 'uniqid';
|
||||
import { Importable } from '../../Import/Importable';
|
||||
import { CreateUncategorizedTransactionService } from './CreateUncategorizedTransaction.service';
|
||||
import { ImportableContext } from '../../Import/interfaces';
|
||||
@@ -9,8 +9,10 @@ import { BankTransactionsSampleData } from '../../BankingTransactions/constants'
|
||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||
import { CreateUncategorizedTransactionDTO } from '../types/BankingCategorize.types';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
import { ImportableService } from '../../Import/decorators/Import.decorator';
|
||||
import { UncategorizedBankTransaction } from '../../BankingTransactions/models/UncategorizedBankTransaction';
|
||||
@Injectable()
|
||||
@ImportableService({ name: UncategorizedBankTransaction.name })
|
||||
export class UncategorizedTransactionsImportable extends Importable {
|
||||
constructor(
|
||||
private readonly createUncategorizedTransaction: CreateUncategorizedTransactionService,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common';
|
||||
import { BankingMatchingApplication } from './BankingMatchingApplication';
|
||||
import { GetMatchedTransactionsFilter } from './types';
|
||||
import { MatchBankTransactionDto } from './dtos/MatchBankTransaction.dto';
|
||||
@@ -34,7 +34,7 @@ export class BankingMatchingController {
|
||||
);
|
||||
}
|
||||
|
||||
@Post('/unmatch/:uncategorizedTransactionId')
|
||||
@Patch('/unmatch/:uncategorizedTransactionId')
|
||||
@ApiOperation({ summary: 'Unmatch the given uncategorized transaction.' })
|
||||
async unmatchMatchedTransaction(
|
||||
@Param('uncategorizedTransactionId') uncategorizedTransactionId: number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty, sumBy } from 'lodash';
|
||||
import { isEmpty, round, sumBy } from 'lodash';
|
||||
import { ERRORS, MatchedTransactionPOJO } from './types';
|
||||
import { ServiceError } from '../Items/ServiceError';
|
||||
|
||||
@@ -22,18 +22,24 @@ export const sortClosestMatchTransactions = (
|
||||
};
|
||||
|
||||
export const sumMatchTranasctions = (transactions: Array<any>) => {
|
||||
return transactions.reduce(
|
||||
(total, item) =>
|
||||
total +
|
||||
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
|
||||
const total = transactions.reduce(
|
||||
(sum, item) => {
|
||||
const amount = parseFloat(item.amount) || 0;
|
||||
const multiplier = item.transactionNormal === 'debit' ? 1 : -1;
|
||||
return sum + multiplier * amount;
|
||||
},
|
||||
0
|
||||
);
|
||||
// Round to 2 decimal places to avoid floating-point precision issues
|
||||
return round(total, 2);
|
||||
};
|
||||
|
||||
export const sumUncategorizedTransactions = (
|
||||
uncategorizedTransactions: Array<any>
|
||||
) => {
|
||||
return sumBy(uncategorizedTransactions, 'amount');
|
||||
const total = sumBy(uncategorizedTransactions, 'amount');
|
||||
// Round to 2 decimal places to avoid floating-point precision issues
|
||||
return round(total, 2);
|
||||
};
|
||||
|
||||
export const validateUncategorizedTransactionsNotMatched = (
|
||||
|
||||
@@ -34,7 +34,7 @@ export class MatchBankTransactions {
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Validates the match bank transactions DTO.
|
||||
@@ -100,7 +100,10 @@ export class MatchBankTransactions {
|
||||
);
|
||||
// Validates the total given matching transcations whether is not equal
|
||||
// uncategorized transaction amount.
|
||||
if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
|
||||
// Use tolerance-based comparison to handle floating-point precision issues
|
||||
const tolerance = 0.01; // Allow 0.01 difference for floating-point precision
|
||||
const difference = Math.abs(totalUncategorizedTransactions - totalMatchedTranasctions);
|
||||
if (difference > tolerance) {
|
||||
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,30 @@ export class MatchTransactionsTypes {
|
||||
private static registry: MatchTransactionsTypesRegistry;
|
||||
|
||||
/**
|
||||
* Consttuctor method.
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor() {
|
||||
constructor(
|
||||
private readonly getMatchedInvoicesService: GetMatchedTransactionsByInvoices,
|
||||
private readonly getMatchedBillsService: GetMatchedTransactionsByBills,
|
||||
private readonly getMatchedExpensesService: GetMatchedTransactionsByExpenses,
|
||||
private readonly getMatchedManualJournalsService: GetMatchedTransactionsByManualJournals,
|
||||
private readonly getMatchedCashflowService: GetMatchedTransactionsByCashflow,
|
||||
) {
|
||||
this.boot();
|
||||
}
|
||||
|
||||
get registered() {
|
||||
return [
|
||||
{ type: 'SaleInvoice', service: GetMatchedTransactionsByInvoices },
|
||||
{ type: 'Bill', service: GetMatchedTransactionsByBills },
|
||||
{ type: 'Expense', service: GetMatchedTransactionsByExpenses },
|
||||
{ type: 'SaleInvoice', service: this.getMatchedInvoicesService },
|
||||
{ type: 'Bill', service: this.getMatchedBillsService },
|
||||
{ type: 'Expense', service: this.getMatchedExpensesService },
|
||||
{
|
||||
type: 'ManualJournal',
|
||||
service: GetMatchedTransactionsByManualJournals,
|
||||
service: this.getMatchedManualJournalsService,
|
||||
},
|
||||
{
|
||||
type: 'CashflowTransaction',
|
||||
service: GetMatchedTransactionsByCashflow,
|
||||
service: this.getMatchedCashflowService,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -50,14 +56,13 @@ export class MatchTransactionsTypes {
|
||||
* Boots all the registered importables.
|
||||
*/
|
||||
public boot() {
|
||||
if (!MatchTransactionsTypes.registry) {
|
||||
const instance = MatchTransactionsTypesRegistry.getInstance();
|
||||
const instance = MatchTransactionsTypesRegistry.getInstance();
|
||||
|
||||
this.registered.forEach((registered) => {
|
||||
// const serviceInstanace = Container.get(registered.service);
|
||||
// instance.register(registered.type, serviceInstanace);
|
||||
});
|
||||
MatchTransactionsTypes.registry = instance;
|
||||
}
|
||||
// Always register services to ensure they're available
|
||||
this.registered.forEach((registered) => {
|
||||
instance.register(registered.type, registered.service);
|
||||
});
|
||||
|
||||
MatchTransactionsTypes.registry = instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
RecognizeUncategorizedTransactionsJobPayload,
|
||||
RecognizeUncategorizedTransactionsQueue,
|
||||
} from '../_types';
|
||||
import { Process } from '@nestjs/bull';
|
||||
|
||||
@Processor({
|
||||
name: RecognizeUncategorizedTransactionsQueue,
|
||||
@@ -28,7 +27,6 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
@Process(RecognizeUncategorizedTransactionsQueue)
|
||||
@UseCls()
|
||||
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
|
||||
const { ruleId, transactionsCriteria } = job.data;
|
||||
|
||||
@@ -83,4 +83,4 @@ const models = [
|
||||
CreateBankTransactionService
|
||||
],
|
||||
})
|
||||
export class BankingTransactionsModule {}
|
||||
export class BankingTransactionsModule { }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IsPositive,
|
||||
} from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { parseBoolean } from '@/utils/parse-boolean';
|
||||
|
||||
export class NumberFormatQueryDto {
|
||||
@ApiPropertyOptional({
|
||||
@@ -24,6 +25,7 @@ export class NumberFormatQueryDto {
|
||||
example: false,
|
||||
})
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsOptional()
|
||||
readonly divideOn1000: boolean;
|
||||
|
||||
@@ -32,6 +34,7 @@ export class NumberFormatQueryDto {
|
||||
example: true,
|
||||
})
|
||||
@IsBoolean()
|
||||
@Transform(({ value }) => parseBoolean(value, false))
|
||||
@IsOptional()
|
||||
readonly showZero: boolean;
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
export const UncategorizedBankTransactionMeta = {
|
||||
defaultFilterField: 'createdAt',
|
||||
defaultSort: {
|
||||
sortOrder: 'DESC',
|
||||
sortField: 'created_at',
|
||||
},
|
||||
importable: true,
|
||||
fields: {
|
||||
date: {
|
||||
name: 'Date',
|
||||
column: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
payee: {
|
||||
name: 'Payee',
|
||||
column: 'payee',
|
||||
fieldType: 'text',
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
column: 'description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'Reference No.',
|
||||
column: 'reference_no',
|
||||
fieldType: 'text',
|
||||
},
|
||||
amount: {
|
||||
name: 'Amount',
|
||||
column: 'Amount',
|
||||
fieldType: 'numeric',
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
name: 'Account',
|
||||
column: 'account_id',
|
||||
fieldType: 'relation',
|
||||
to: { model: 'Account', to: 'id' },
|
||||
},
|
||||
createdAt: {
|
||||
name: 'Created At',
|
||||
column: 'createdAt',
|
||||
fieldType: 'date',
|
||||
importable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
date: {
|
||||
name: 'Date',
|
||||
fieldType: 'date',
|
||||
required: true,
|
||||
},
|
||||
payee: {
|
||||
name: 'Payee',
|
||||
fieldType: 'text',
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'Reference No.',
|
||||
fieldType: 'text',
|
||||
},
|
||||
amount: {
|
||||
name: 'Amount',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -2,7 +2,10 @@
|
||||
import * as moment from 'moment';
|
||||
import { Model } from 'objection';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { UncategorizedBankTransactionMeta } from './UncategorizedBankTransaction.meta';
|
||||
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
|
||||
|
||||
@InjectModelMeta(UncategorizedBankTransactionMeta)
|
||||
export class UncategorizedBankTransaction extends TenantBaseModel {
|
||||
readonly amount!: number;
|
||||
readonly date!: Date | string;
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
validateTransactionShouldBeExcluded,
|
||||
} from './utils';
|
||||
import {
|
||||
IBankTransactionExcludedEventPayload,
|
||||
IBankTransactionExcludingEventPayload,
|
||||
IBankTransactionUnexcludedEventPayload,
|
||||
IBankTransactionUnexcludingEventPayload,
|
||||
} from '../types/BankTransactionsExclude.types';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
@@ -24,7 +24,7 @@ export class UnexcludeBankTransactionService {
|
||||
private readonly uncategorizedBankTransactionModel: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Marks the given bank transaction as excluded.
|
||||
@@ -50,7 +50,8 @@ export class UnexcludeBankTransactionService {
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
await this.eventEmitter.emitAsync(events.bankTransactions.onUnexcluding, {
|
||||
uncategorizedTransactionId,
|
||||
} as IBankTransactionExcludingEventPayload);
|
||||
trx,
|
||||
} as IBankTransactionUnexcludingEventPayload);
|
||||
|
||||
await this.uncategorizedBankTransactionModel()
|
||||
.query(trx)
|
||||
@@ -61,7 +62,8 @@ export class UnexcludeBankTransactionService {
|
||||
|
||||
await this.eventEmitter.emitAsync(events.bankTransactions.onUnexcluded, {
|
||||
uncategorizedTransactionId,
|
||||
} as IBankTransactionExcludedEventPayload);
|
||||
trx,
|
||||
} as IBankTransactionUnexcludedEventPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class DecrementUncategorizedTransactionOnExclude {
|
||||
private readonly uncategorizedBankTransaction: TenantModelProxy<
|
||||
typeof UncategorizedBankTransaction
|
||||
>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Validates the cashflow transaction whether matched with bank transaction on deleting.
|
||||
@@ -50,7 +50,7 @@ export class DecrementUncategorizedTransactionOnExclude {
|
||||
trx,
|
||||
}: IBankTransactionUnexcludedEventPayload) {
|
||||
const transaction = await this.uncategorizedBankTransaction()
|
||||
.query()
|
||||
.query(trx)
|
||||
.findById(uncategorizedTransactionId);
|
||||
//
|
||||
await this.account()
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -25,7 +25,7 @@ export class BillAllocateLandedCostController {
|
||||
private billAllocatedCostTransactions: BillAllocatedLandedCostTransactions,
|
||||
private revertAllocatedLandedCost: RevertAllocatedLandedCost,
|
||||
private landedCostTransactions: LandedCostTranasctions,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
@Get('/transactions')
|
||||
@ApiOperation({ summary: 'Get landed cost transactions' })
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export class BillAllocatedLandedCostTransactions {
|
||||
private readonly billLandedCostModel: TenantModelProxy<
|
||||
typeof BillLandedCost
|
||||
>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Retrieve the bill associated landed cost transactions.
|
||||
@@ -77,6 +77,13 @@ export class BillAllocatedLandedCostTransactions {
|
||||
transaction.fromTransactionType,
|
||||
transaction,
|
||||
);
|
||||
const allocationMethodFormattedKey = transaction.allocationMethodFormatted;
|
||||
const allocationMethodFormatted = allocationMethodFormattedKey
|
||||
? this.i18nService.t(allocationMethodFormattedKey, {
|
||||
defaultValue: allocationMethodFormattedKey,
|
||||
})
|
||||
: '';
|
||||
|
||||
return {
|
||||
formattedAmount: formatNumber(transaction.amount, {
|
||||
currencyCode: transaction.currencyCode,
|
||||
@@ -84,12 +91,14 @@ export class BillAllocatedLandedCostTransactions {
|
||||
...omit(transaction, [
|
||||
'allocatedFromBillEntry',
|
||||
'allocatedFromExpenseEntry',
|
||||
'allocationMethodFormatted',
|
||||
]),
|
||||
name,
|
||||
description,
|
||||
formattedLocalAmount: formatNumber(transaction.localAmount, {
|
||||
currencyCode: 'USD',
|
||||
}),
|
||||
allocationMethodFormatted,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { LandedCostTransactionsQueryDto } from '../dtos/LandedCostTransactionsQu
|
||||
|
||||
@Injectable()
|
||||
export class LandedCostTranasctions {
|
||||
constructor(private readonly transactionLandedCost: TransactionLandedCost) {}
|
||||
constructor(private readonly transactionLandedCost: TransactionLandedCost) { }
|
||||
|
||||
/**
|
||||
* Retrieve the landed costs based on the given query.
|
||||
@@ -45,8 +45,8 @@ export class LandedCostTranasctions {
|
||||
)(transactionType);
|
||||
|
||||
return pipe(
|
||||
this.transformLandedCostTransactions,
|
||||
R.map(transformLandedCost),
|
||||
this.transformLandedCostTransactions,
|
||||
)(transactions);
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ export class LandedCostTranasctions {
|
||||
const entries = R.map<
|
||||
ILandedCostTransactionEntry,
|
||||
ILandedCostTransactionEntryDOJO
|
||||
>(transformLandedCostEntry)(transaction.entries);
|
||||
>(transformLandedCostEntry)(transaction.entries ?? []);
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
IsOptional,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
IsDecimal,
|
||||
IsString,
|
||||
IsNumber,
|
||||
} from 'class-validator';
|
||||
@@ -17,8 +16,9 @@ export class AllocateBillLandedCostItemDto {
|
||||
@ToNumber()
|
||||
entryId: number;
|
||||
|
||||
@IsDecimal()
|
||||
cost: string; // Use string for IsDecimal, or use @IsNumber() if you want a number
|
||||
@IsNumber()
|
||||
@ToNumber()
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export class AllocateBillLandedCostDto {
|
||||
|
||||
@@ -60,8 +60,8 @@ export class BillLandedCost extends BaseModel {
|
||||
const allocationMethod = lowerCase(this.allocationMethod);
|
||||
|
||||
const keyLabelsPairs = {
|
||||
value: 'allocation_method.value.label',
|
||||
quantity: 'allocation_method.quantity.label',
|
||||
value: 'bill.allocation_method.value',
|
||||
quantity: 'bill.allocation_method.quantity',
|
||||
};
|
||||
return keyLabelsPairs[allocationMethod] || '';
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { BranchesSettingsService } from '../Branches/BranchesSettings';
|
||||
import { BillPaymentsController } from './BillPayments.controller';
|
||||
import { BillPaymentGLEntries } from './commands/BillPaymentGLEntries';
|
||||
import { BillPaymentGLEntriesSubscriber } from './subscribers/BillPaymentGLEntriesSubscriber';
|
||||
import { BillPaymentBillSyncSubscriber } from './subscribers/BillPaymentBillSyncSubscriber';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||
import { BillPaymentsExportable } from './queries/BillPaymentsExportable';
|
||||
@@ -39,6 +40,7 @@ import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
|
||||
TenancyContext,
|
||||
BillPaymentGLEntries,
|
||||
BillPaymentGLEntriesSubscriber,
|
||||
BillPaymentBillSyncSubscriber,
|
||||
BillPaymentsExportable,
|
||||
BillPaymentsImportable,
|
||||
GetBillPaymentsService,
|
||||
@@ -52,4 +54,4 @@ import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
|
||||
],
|
||||
controllers: [BillPaymentsController],
|
||||
})
|
||||
export class BillPaymentsModule {}
|
||||
export class BillPaymentsModule { }
|
||||
|
||||
@@ -38,7 +38,7 @@ export class CreateBillPaymentService {
|
||||
|
||||
@Inject(BillPayment.name)
|
||||
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Creates a new bill payment transcations and store it to the storage
|
||||
@@ -103,11 +103,19 @@ export class CreateBillPaymentService {
|
||||
} as IBillPaymentCreatingPayload);
|
||||
|
||||
// Writes the bill payment graph to the storage.
|
||||
const billPayment = await this.billPaymentModel()
|
||||
const insertedBillPayment = await this.billPaymentModel()
|
||||
.query(trx)
|
||||
.insertGraphAndFetch({
|
||||
...billPaymentObj,
|
||||
});
|
||||
|
||||
// Fetch the bill payment with entries to ensure they're loaded for the subscriber.
|
||||
const billPayment = await this.billPaymentModel()
|
||||
.query(trx)
|
||||
.withGraphFetched('entries')
|
||||
.findById(insertedBillPayment.id)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Triggers `onBillPaymentCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.billPayment.onCreated, {
|
||||
billPayment,
|
||||
|
||||
@@ -29,7 +29,7 @@ export class EditBillPayment {
|
||||
|
||||
@Inject(Vendor.name)
|
||||
private readonly vendorModel: TenantModelProxy<typeof Vendor>,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Edits the details of the given bill payment.
|
||||
@@ -116,12 +116,20 @@ export class EditBillPayment {
|
||||
} as IBillPaymentEditingPayload);
|
||||
|
||||
// Edits the bill payment transaction graph on the storage.
|
||||
const billPayment = await this.billPaymentModel()
|
||||
await this.billPaymentModel()
|
||||
.query(trx)
|
||||
.upsertGraphAndFetch({
|
||||
.upsertGraph({
|
||||
id: billPaymentId,
|
||||
...billPaymentObj,
|
||||
});
|
||||
|
||||
// Fetch the bill payment with entries to ensure they're loaded for the subscriber.
|
||||
const billPayment = await this.billPaymentModel()
|
||||
.query(trx)
|
||||
.withGraphFetched('entries')
|
||||
.findById(billPaymentId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Triggers `onBillPaymentEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.billPayment.onEdited, {
|
||||
billPaymentId,
|
||||
|
||||
@@ -104,6 +104,12 @@ export class BillPaymentResponseDto {
|
||||
@ApiProperty({ description: 'The formatted amount', example: '100.00 USD' })
|
||||
formattedAmount: string;
|
||||
|
||||
@ApiProperty({ description: 'The formatted total', example: '100.00 USD' })
|
||||
formattedTotal: string;
|
||||
|
||||
@ApiProperty({ description: 'The formatted subtotal', example: '100.00 USD' })
|
||||
formattedSubtotal: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The date when the payment was created',
|
||||
example: '2024-01-01T12:00:00Z',
|
||||
|
||||
@@ -167,7 +167,7 @@ export const BillPaymentMeta = {
|
||||
name: 'bill_payment.field.payment_number',
|
||||
fieldType: 'text',
|
||||
unique: true,
|
||||
importHint: 'The payment number should be unique.',
|
||||
importHint: 'bill_payment.field.payment_number_hint',
|
||||
},
|
||||
paymentAccountId: {
|
||||
name: 'bill_payment.field.payment_account',
|
||||
@@ -175,7 +175,7 @@ export const BillPaymentMeta = {
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
required: true,
|
||||
importHint: 'Matches the account name or code.',
|
||||
importHint: 'account.field.account_hint',
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'bill_payment.field.exchange_rate',
|
||||
@@ -203,7 +203,7 @@ export const BillPaymentMeta = {
|
||||
relationModel: 'Bill',
|
||||
relationImportMatch: 'billNumber',
|
||||
required: true,
|
||||
importHint: 'Matches the bill number.',
|
||||
importHint: 'bill_payment.field.bill_hint',
|
||||
},
|
||||
paymentAmount: {
|
||||
name: 'bill_payment.field.entries.payment_amount',
|
||||
@@ -213,7 +213,7 @@ export const BillPaymentMeta = {
|
||||
},
|
||||
},
|
||||
branchId: {
|
||||
name: 'Branch',
|
||||
name: 'invoice.field.branch',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Branch',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
|
||||
@@ -13,6 +13,8 @@ export class BillPaymentTransformer extends Transformer {
|
||||
'formattedPaymentDate',
|
||||
'formattedCreatedAt',
|
||||
'formattedAmount',
|
||||
'formattedTotal',
|
||||
'formattedSubtotal',
|
||||
'entries',
|
||||
'attachments',
|
||||
];
|
||||
@@ -47,6 +49,29 @@ export class BillPaymentTransformer extends Transformer {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedTotal = (billPayment: BillPayment): string => {
|
||||
return this.formatNumber(billPayment.amount, {
|
||||
currencyCode: billPayment.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted subtotal.
|
||||
* @param {IBillPayment} billPayment
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedSubtotal = (billPayment: BillPayment): string => {
|
||||
return this.formatNumber(billPayment.amount, {
|
||||
currencyCode: billPayment.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retreives the bill payment entries.
|
||||
*/
|
||||
|
||||
@@ -16,8 +16,6 @@ export class BillPaymentsExportable extends Exportable {
|
||||
|
||||
/**
|
||||
* Retrieves the accounts data to exportable sheet.
|
||||
* @param {number} tenantId
|
||||
* @returns
|
||||
*/
|
||||
public async exportable(query: any) {
|
||||
const filterQuery = (builder) => {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
IBillPaymentEventCreatedPayload,
|
||||
IBillPaymentEventDeletedPayload,
|
||||
IBillPaymentEventEditedPayload,
|
||||
} from '../types/BillPayments.types';
|
||||
import { BillPaymentBillSync } from '../commands/BillPaymentBillSync.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class BillPaymentBillSyncSubscriber {
|
||||
/**
|
||||
* @param {BillPaymentBillSync} billPaymentBillSync - Bill payment bill sync service.
|
||||
*/
|
||||
constructor(private readonly billPaymentBillSync: BillPaymentBillSync) { }
|
||||
|
||||
/**
|
||||
* Handle bill increment/decrement payment amount
|
||||
* once created, edited or deleted.
|
||||
*/
|
||||
@OnEvent(events.billPayment.onCreated)
|
||||
async handleBillIncrementPaymentOnceCreated({
|
||||
billPayment,
|
||||
trx,
|
||||
}: IBillPaymentEventCreatedPayload) {
|
||||
// Ensure entries are available - they should be included in insertGraphAndFetch
|
||||
const entries = billPayment.entries || [];
|
||||
await this.billPaymentBillSync.saveChangeBillsPaymentAmount(
|
||||
entries.map((entry) => ({
|
||||
billId: entry.billId,
|
||||
paymentAmount: entry.paymentAmount,
|
||||
})),
|
||||
null,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bill increment/decrement payment amount once edited.
|
||||
*/
|
||||
@OnEvent(events.billPayment.onEdited)
|
||||
async handleBillIncrementPaymentOnceEdited({
|
||||
billPayment,
|
||||
oldBillPayment,
|
||||
trx,
|
||||
}: IBillPaymentEventEditedPayload) {
|
||||
const entries = billPayment.entries || [];
|
||||
const oldEntries = oldBillPayment?.entries || null;
|
||||
|
||||
await this.billPaymentBillSync.saveChangeBillsPaymentAmount(
|
||||
entries.map((entry) => ({
|
||||
billId: entry.billId,
|
||||
paymentAmount: entry.paymentAmount,
|
||||
})),
|
||||
oldEntries,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle revert bills payment amount once bill payment deleted.
|
||||
*/
|
||||
@OnEvent(events.billPayment.onDeleted)
|
||||
async handleBillDecrementPaymentAmount({
|
||||
oldBillPayment,
|
||||
trx,
|
||||
}: IBillPaymentEventDeletedPayload) {
|
||||
const oldEntries = oldBillPayment.entries || [];
|
||||
|
||||
await this.billPaymentBillSync.saveChangeBillsPaymentAmount(
|
||||
oldEntries.map((entry) => ({
|
||||
billId: entry.billId,
|
||||
paymentAmount: 0,
|
||||
})),
|
||||
oldEntries,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
Post,
|
||||
Body,
|
||||
Put,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Get,
|
||||
Query,
|
||||
HttpCode,
|
||||
} from '@nestjs/common';
|
||||
import { BillsApplication } from './Bills.application';
|
||||
import { IBillsFilter } from './Bills.types';
|
||||
@@ -40,6 +42,7 @@ export class BillsController {
|
||||
@ApiOperation({
|
||||
summary: 'Validate which bills can be deleted and return the results.',
|
||||
})
|
||||
@HttpCode(200)
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
@@ -56,6 +59,7 @@ export class BillsController {
|
||||
|
||||
@Post('bulk-delete')
|
||||
@ApiOperation({ summary: 'Deletes multiple bills.' })
|
||||
@HttpCode(200)
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Bills deleted successfully',
|
||||
@@ -160,7 +164,7 @@ export class BillsController {
|
||||
return this.billsApplication.getBill(billId);
|
||||
}
|
||||
|
||||
@Post(':id/open')
|
||||
@Patch(':id/open')
|
||||
@ApiOperation({ summary: 'Open the given bill.' })
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
|
||||
@@ -184,76 +184,76 @@ export const BillMeta = {
|
||||
},
|
||||
fields2: {
|
||||
billNumber: {
|
||||
name: 'Bill No.',
|
||||
name: 'bill.field.bill_number',
|
||||
fieldType: 'text',
|
||||
required: true,
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'Reference No.',
|
||||
name: 'bill.field.reference_no',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billDate: {
|
||||
name: 'Date',
|
||||
name: 'bill.field.bill_date',
|
||||
fieldType: 'date',
|
||||
required: true,
|
||||
},
|
||||
dueDate: {
|
||||
name: 'Due Date',
|
||||
name: 'bill.field.due_date',
|
||||
fieldType: 'date',
|
||||
required: true,
|
||||
},
|
||||
vendorId: {
|
||||
name: 'Vendor',
|
||||
name: 'bill.field.vendor',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Contact',
|
||||
relationImportMatch: 'displayName',
|
||||
required: true,
|
||||
},
|
||||
exchangeRate: {
|
||||
name: 'Exchange Rate',
|
||||
name: 'bill.field.exchange_rate',
|
||||
fieldType: 'number',
|
||||
},
|
||||
note: {
|
||||
name: 'Note',
|
||||
name: 'bill.field.note',
|
||||
fieldType: 'text',
|
||||
},
|
||||
open: {
|
||||
name: 'Open',
|
||||
name: 'bill.field.open',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
entries: {
|
||||
name: 'Entries',
|
||||
name: 'bill.field.entries',
|
||||
fieldType: 'collection',
|
||||
collectionOf: 'object',
|
||||
collectionMinLength: 1,
|
||||
required: true,
|
||||
fields: {
|
||||
itemId: {
|
||||
name: 'Item',
|
||||
name: 'bill.field.item',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Item',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
required: true,
|
||||
importHint: 'Matches the item name or code.',
|
||||
importHint: 'bill.field.item_hint',
|
||||
},
|
||||
rate: {
|
||||
name: 'Rate',
|
||||
name: 'bill.field.rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'Quantity',
|
||||
name: 'bill.field.quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'Line Description',
|
||||
name: 'bill.field.description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
branchId: {
|
||||
name: 'Branch',
|
||||
name: 'invoice.field.branch',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Branch',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
@@ -261,7 +261,7 @@ export const BillMeta = {
|
||||
required: true,
|
||||
},
|
||||
warehouseId: {
|
||||
name: 'Warehouse',
|
||||
name: 'invoice.field.warehouse',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Warehouse',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
|
||||
@@ -51,6 +51,7 @@ export class Bill extends TenantBaseModel {
|
||||
public updatedAt: Date | null;
|
||||
|
||||
public entries?: ItemEntry[];
|
||||
public attachments!: Document[];
|
||||
public locatedLandedCosts?: BillLandedCost[];
|
||||
/**
|
||||
* Timestamps columns.
|
||||
@@ -633,7 +634,7 @@ export class Bill extends TenantBaseModel {
|
||||
|
||||
return this.query(trx)
|
||||
.where('id', billId)
|
||||
[changeMethod]('payment_amount', Math.abs(amount));
|
||||
[changeMethod]('payment_amount', Math.abs(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||
import { Bill } from '../models/Bill';
|
||||
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
|
||||
import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer';
|
||||
|
||||
export class BillTransformer extends Transformer {
|
||||
/**
|
||||
@@ -94,6 +96,7 @@ export class BillTransformer extends Transformer {
|
||||
protected formattedDueAmount = (bill: Bill): string => {
|
||||
return this.formatNumber(bill.dueAmount, {
|
||||
currencyCode: bill.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -169,6 +172,7 @@ export class BillTransformer extends Transformer {
|
||||
protected totalFormatted = (bill: Bill): string => {
|
||||
return this.formatNumber(bill.total, {
|
||||
currencyCode: bill.currencyCode,
|
||||
money: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -229,20 +233,18 @@ export class BillTransformer extends Transformer {
|
||||
/**
|
||||
* Retrieves the entries of the bill.
|
||||
* @param {Bill} credit
|
||||
* @returns {}
|
||||
*/
|
||||
// protected entries = (bill: Bill) => {
|
||||
// return this.item(bill.entries, new ItemEntryTransformer(), {
|
||||
// currencyCode: bill.currencyCode,
|
||||
// });
|
||||
// };
|
||||
protected entries = (bill: Bill) => {
|
||||
return this.item(bill.entries, new ItemEntryTransformer(), {
|
||||
currencyCode: bill.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the bill attachments.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns
|
||||
* @param {Bill} bill
|
||||
*/
|
||||
// protected attachments = (bill: Bill) => {
|
||||
// return this.item(bill.attachments, new AttachmentTransformer());
|
||||
// };
|
||||
protected attachments = (bill: Bill) => {
|
||||
return this.item(bill.attachments, new AttachmentTransformer());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Get,
|
||||
Query,
|
||||
Param,
|
||||
Post,
|
||||
Patch,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger';
|
||||
@@ -27,7 +27,7 @@ export class ContactsController {
|
||||
return this.getAutoCompleteService.autocompleteContacts(query);
|
||||
}
|
||||
|
||||
@Post(':id/activate')
|
||||
@Patch(':id/activate')
|
||||
@ApiOperation({ summary: 'Activate a contact' })
|
||||
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
|
||||
async activateContact(@Param('id', ParseIntPipe) contactId: number) {
|
||||
@@ -35,7 +35,7 @@ export class ContactsController {
|
||||
return { id: contactId, activated: true };
|
||||
}
|
||||
|
||||
@Post(':id/inactivate')
|
||||
@Patch(':id/inactivate')
|
||||
@ApiOperation({ summary: 'Inactivate a contact' })
|
||||
@ApiParam({ name: 'id', type: 'number', description: 'Contact ID' })
|
||||
async inactivateContact(@Param('id', ParseIntPipe) contactId: number) {
|
||||
|
||||
@@ -7,9 +7,13 @@ import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.s
|
||||
import { CreditNoteRefundsController } from './CreditNoteRefunds.controller';
|
||||
import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
|
||||
import { GetCreditNoteRefundsService } from './queries/GetCreditNoteRefunds.service';
|
||||
import { RefundCreditNoteGLEntries } from './commands/RefundCreditNoteGLEntries';
|
||||
import { RefundCreditNoteGLEntriesSubscriber } from '../CreditNotes/subscribers/RefundCreditNoteGLEntriesSubscriber';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||
|
||||
@Module({
|
||||
imports: [forwardRef(() => CreditNotesModule)],
|
||||
imports: [forwardRef(() => CreditNotesModule), LedgerModule, AccountsModule],
|
||||
providers: [
|
||||
CreateRefundCreditNoteService,
|
||||
DeleteRefundCreditNoteService,
|
||||
@@ -17,8 +21,10 @@ import { GetCreditNoteRefundsService } from './queries/GetCreditNoteRefunds.serv
|
||||
RefundSyncCreditNoteBalanceService,
|
||||
CreditNotesRefundsApplication,
|
||||
GetCreditNoteRefundsService,
|
||||
RefundCreditNoteGLEntries,
|
||||
RefundCreditNoteGLEntriesSubscriber,
|
||||
],
|
||||
exports: [RefundSyncCreditNoteBalanceService],
|
||||
controllers: [CreditNoteRefundsController],
|
||||
})
|
||||
export class CreditNoteRefundsModule {}
|
||||
export class CreditNoteRefundsModule { }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user