Compare commits

..

1 Commits

Author SHA1 Message Date
a.bouhuolia
813bed3676 chore: add CONTRIBUTING file 2023-04-27 01:50:09 +02:00
831 changed files with 17268 additions and 14178 deletions

View File

@@ -1,62 +0,0 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitType": "docs",
"commitConvention": "angular",
"contributors": [
{
"login": "abouolia",
"name": "Ahmed Bouhuolia",
"avatar_url": "https://avatars.githubusercontent.com/u/2197422?v=4",
"profile": "https://github.com/abouolia",
"contributions": [
"code"
]
},
{
"login": "ameir",
"name": "Ameir Abdeldayem",
"avatar_url": "https://avatars.githubusercontent.com/u/374330?v=4",
"profile": "http://ameir.net",
"contributions": [
"bug"
]
},
{
"login": "elforjani13",
"name": "ElforJani13",
"avatar_url": "https://avatars.githubusercontent.com/u/39470382?v=4",
"profile": "https://github.com/elforjani13",
"contributions": [
"code"
]
},
{
"login": "scheibling",
"name": "Lars Scheibling",
"avatar_url": "https://avatars.githubusercontent.com/u/24367830?v=4",
"profile": "https://scheibling.se",
"contributions": [
"bug"
]
},
{
"login": "suhaibaffan",
"name": "Suhaib Affan",
"avatar_url": "https://avatars.githubusercontent.com/u/18115937?v=4",
"profile": "https://github.com/suhaibaffan",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "bigcapital",
"projectOwner": "bigcapitalhq"
}

View File

@@ -8,45 +8,27 @@ MAIL_FROM_NAME=
MAIL_FROM_ADDRESS= MAIL_FROM_ADDRESS=
# Database # Database
DB_HOST=mysql DB_USER=
DB_USER=bigcapital DB_HOST=
DB_PASSWORD=bigcapital DB_PASSWORD=
DB_ROOT_PASSWORD=root DB_CHARSET=
DB_CHARSET=utf8
# System database # System database
SYSTEM_DB_NAME=bigcapital_system SYSTEM_DB_NAME=bigcapital_system
# SYSTEM_DB_USER=
# SYSTEM_DB_PASSWORD=
# SYSTEM_DB_NAME=
# SYSTEM_DB_CHARSET=
# Tenant databases # Tenants databases
TENANT_DB_NAME_PERFIX=bigcapital_tenant_ TENANT_DB_NAME_PERFIX=bigcapital_tenant_
# TENANT_DB_HOST=
# TENANT_DB_USER=
# TENANT_DB_PASSWORD=
# TENANT_DB_CHARSET=
# Application # MongoDB
BASE_URL=http://example.com
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
# Jobs MongoDB
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
# App proxy # Authentication
PUBLIC_PROXY_PORT=80 JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
PUBLIC_PROXY_SSL_PORT=443
# Application
BASE_URL=https://bigcapital.ly
CONTACT_US_MAIL=support@bigcapital.ly
# Agendash # Agendash
AGENDASH_AUTH_USER=agendash AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123 AGENDASH_AUTH_PASSWORD=123123
# Sign-up restrictions
SIGNUP_DISABLED=false
SIGNUP_ALLOWED_DOMAINS=
SIGNUP_ALLOWED_EMAILS=
# API rate limit (points,duration,block duration).
API_RATE_LIMIT=120,60,600

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
docker/nginx/scripts/build-nginx.sh text eol=lf
docker/mariadb/docker-entrypoint.sh text eol=lf

View File

@@ -2,111 +2,6 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
# [0.9.11] - 23-07-2023
* added: Restart policy to docker compose files. by @suhaibaffan in https://github.com/bigcapitalhq/bigcapital/pull/198
* fix: Expose and expand the rate limit to the env variables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/195
# [0.9.10] - 18-07-2023
* feat(e2e): E2E onboarding process by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/176
* fix(webapp): Show loading message of cost computing job on financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/196
* fix(webapp): Change the currency code of sales and purchases transactions with foreign contacts.
# [0.9.9] - 28-06-2023
* refactor: Customer and vendor select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/171
* chore: Move auto-increment components in separate files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/170
* fix: Style of quick item drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/173
* fix: Should not show the form before loading account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/172
* fix: Payment made form does not handle not unique number an e… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/177
* fix: Internal note of invoice/bill payment does not saving by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/181
* fix: Storing cash flow transaction description by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/180
* fix: No currency in amount field on money in/out dialogs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/179
* fix: No default branch for customer/vendor opening balance branch by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/182
# [0.9.8] - 19-06-2023
`bigcapitalhq/webapp`
* add: Inventory Adjustment option to the item drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/158
* fix: use all drawers names from common enum object by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/157
* fix: adjustment type options do not show up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/159
* fix: change the remove line text to be red to intent as a danger action by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/162
* fix: rename sidebar localization keys names to be keyword path by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/161
* fix: manual journal placeholder text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/160
* fix: warehouses select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/168
`bigcapitalhq/server`
* fix: sending emails on reset password and registration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/167
## [0.9.7] - 14-06-2023
`@bigcapital/webapp`
* fix: change the footer links of onboarding pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/139
`@bigcapital/server`
* fix: expense transaction journal entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/155
## [0.9.6] - 12-06-2023
`@bigcapital/webapp`
* fix: remove duplicated form submitting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/138
* feat: add monorepo version on the application sidebar by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/136
## [0.9.5] - 11-06-2023
`@bigcapital/server`
* fix: filter ledger entries that effect contact balance to AR/AP accounts only by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/132
`@bigcapital/webapp`
* fix: catch journal error when create a journal with accounts have different currency by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/135
* fix: add duplicate icon to context menu of customers and vendors table by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/133
* fix: customer/vendor opening balance with exchange rate by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/134
## [0.9.4] - 08-06-2023
`@bigcapital/monorepo`
- fixed: docker-compose line-ending issue on Windows by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/130
`@bigcapital/server`
- fixed: Disable Webpack minification for JS class name reading.
## [0.9.3] -04-06-2023
`@bigcapital/monorepo`
* Added: Add env variable to customize the proxy public ports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/125
* Added: Migrate the server database to MariaDB by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/128
## [0.9.2] - 31-05-2023
`@bigcapital/webapp`
- fixed: move `packaeg-lock.json` inside docker container.
- fixed: remove Sentry from the web client.
## [0.9.1] - 28-05-2023
`@bigcapital/server`
- fix: deleting ledger entries of manual journal.
- fix: base currency should be enabled.
- fix: delete invoice transaction issue.
`@bigcapital/webapp`
- fix: general, accoutant and items preferences.
- fix: auto-increment sale invoices, estiamtes, credit notes, payments and manual journals.
- refactor: the setup organization form to use binded Formik components.
## [0.9.0] - 06-05-2023
`@bigcapital/server`
- [Sign-up restrictions](https://docs.bigcapital.ly/docs/deployment/signup_restriction) for self-hosting instances to disable signup or control the allowed email addresses and domains that can sign-up.
## [0.8.3] - 06-04-2023 ## [0.8.3] - 06-04-2023
`@bigcaptial/monorepo` `@bigcaptial/monorepo`

View File

@@ -34,7 +34,7 @@ Contributions via pull requests are much appreciated. Once the approach is agree
## Contribute to Backend ## Contribute to Backend
- Clone the `bigcapital` repository and `cd` into `bigcapital` directory. - Clone the `bigcapital` repository and `cd` into `bigcapital` directory.
- Install all npm dependencies of the monorepo, you don't have to change directory to the `backend` package. just hit these command on root directory and it will install dependencies of all packages. - Install all npm dependencies of the monorepo, you don't have to change directory to the `backend` package. just hit the command on root directory and it will install dependencies of all packages.
``` ```
npm install npm install
@@ -47,7 +47,7 @@ npm run bootstrap
docker-compose up -d docker-compose up -d
``` ```
Wait some seconds, and hit `docker-compose ps` and you should see the same result below. Wait some seconds, and hit `docker-compose ps` to see the result and you should see the same result below.
``` ```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
@@ -122,7 +122,7 @@ There are many other ways to get involved with the community and to participate
- Use the product, submitting GitHub issues when a problem is found. - Use the product, submitting GitHub issues when a problem is found.
- Help code review pull requests and participate in issue threads. - Help code review pull requests and participate in issue threads.
- Submit a new feature request as an issue. - Submit a new feature request as an issue.
- Help answer questions on forums such as Bigcapital Community Discord Channel. - Help answer questions on forums such as Stack Overflow and SigNoz Community Slack Channel.
- Tell others about the project on Twitter, your blog, etc. - Tell others about the project on Twitter, your blog, etc.
**[`^top^`](#)** **[`^top^`](#)**

View File

@@ -7,24 +7,6 @@
<p align="center"> <p align="center">
Simple, smart online accounting software for small and medium businesses. Simple, smart online accounting software for small and medium businesses.
</p> </p>
<p align="center">
<a href="https://github.com/bigcapitalhq/bigcapital/commits/develop">
<img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" />
</a>
<a href="https://discord.com/invite/c8nPBJafeb">
<img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" />
</a>
<a href="https://github.com/bigcapitalhq/bigcapital/graphs/contributors">
<img src="https://img.shields.io/github/contributors/bigcapitalhq/bigcapital" alt="" />
</a>
<a href="https://github.com/bigcapitalhq/bigcapital/blob/develop/LICENSE">
<img src="https://img.shields.io/github/license/bigcapitalhq/bigcapital" alt="" />
</a>
<a href="https://twitter.com/bigcapitalhq">
<img src="https://img.shields.io/twitter/follow/bigcapitalhq?style=social" alt="twitter" />
</a>
</p>
</p> </p>
# What's Bigcapital? # What's Bigcapital?
@@ -40,46 +22,10 @@ Bigcapital is a smart and open-source accounting and inventory software, Bigcapi
# Resources # Resources
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use. - [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
- [Contribution](https://github.com/bigcapitalhq/bigcapital/blob/develop/CONTRIBUTING.md) - Welcome to any contributions.
- [Discord](https://discord.com/invite/c8nPBJafeb) - Ask for help. - [Discord](https://discord.com/invite/c8nPBJafeb) - Ask for help.
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs. - [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
- [Source Code](https://github.com/bigcapitalhq/bigcapital) - Github repo.
# Changelog # Changlog
Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently. Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently.
# Recognition
<a href="https://news.ycombinator.com/item?id=36118990">
<img
style="width: 250px; height: 54px;" width="250" height="54"
alt="Featured on Hacker News"
src="https://hackernews-badge.vercel.app/api?id=36118990"
/>
</a>
# Contributors
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abouolia"><img src="https://avatars.githubusercontent.com/u/2197422?v=4?s=100" width="100px;" alt="Ahmed Bouhuolia"/><br /><sub><b>Ahmed Bouhuolia</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=abouolia" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://ameir.net"><img src="https://avatars.githubusercontent.com/u/374330?v=4?s=100" width="100px;" alt="Ameir Abdeldayem"/><br /><sub><b>Ameir Abdeldayem</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aameir" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/elforjani13"><img src="https://avatars.githubusercontent.com/u/39470382?v=4?s=100" width="100px;" alt="ElforJani13"/><br /><sub><b>ElforJani13</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=elforjani13" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://scheibling.se"><img src="https://avatars.githubusercontent.com/u/24367830?v=4?s=100" width="100px;" alt="Lars Scheibling"/><br /><sub><b>Lars Scheibling</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Ascheibling" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suhaibaffan"><img src="https://avatars.githubusercontent.com/u/18115937?v=4?s=100" width="100px;" alt="Suhaib Affan"/><br /><sub><b>Suhaib Affan</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=suhaibaffan" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -15,22 +15,16 @@ services:
- ./data/logs/nginx/:/var/log/nginx - ./data/logs/nginx/:/var/log/nginx
- ./docker/certbot/certs/:/var/certs - ./docker/certbot/certs/:/var/certs
ports: ports:
- "${PUBLIC_PROXY_PORT:-80}:80" - "80:80"
- "${PUBLIC_PROXY_SSL_PORT:-443}:443" - "443:443"
tty: true tty: true
depends_on: depends_on:
- server - server
- webapp - webapp
deploy:
restart_policy:
condition: unless-stopped
webapp: webapp:
container_name: bigcapital-webapp container_name: bigcapital-webapp
image: ghcr.io/bigcapitalhq/webapp:latest image: ghcr.io/bigcapitalhq/webapp:latest
deploy:
restart_policy:
condition: unless-stopped
server: server:
container_name: bigcapital-server container_name: bigcapital-server
@@ -43,9 +37,6 @@ services:
- mysql - mysql
- mongo - mongo
- redis - redis
deploy:
restart_policy:
condition: unless-stopped
environment: environment:
# Mail # Mail
- MAIL_HOST=${MAIL_HOST} - MAIL_HOST=${MAIL_HOST}
@@ -81,11 +72,6 @@ services:
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER} - AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD} - AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
# Sign-up restrictions
- SIGNUP_DISABLED=${SIGNUP_DISABLED}
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
- SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS}
database_migration: database_migration:
container_name: bigcapital-database-migration container_name: bigcapital-database-migration
build: build:
@@ -102,16 +88,13 @@ services:
mysql: mysql:
container_name: bigcapital-mysql container_name: bigcapital-mysql
deploy:
restart_policy:
condition: unless-stopped
build: build:
context: ./docker/mariadb context: ./docker/mysql
environment: environment:
- MYSQL_DATABASE=${SYSTEM_DB_NAME} - MYSQL_DATABASE=${SYSTEM_DB_NAME}
- MYSQL_USER=${DB_USER} - MYSQL_USER=${DB_USER}
- MYSQL_PASSWORD=${DB_PASSWORD} - MYSQL_PASSWORD=${DB_PASSWORD}
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
expose: expose:
@@ -119,9 +102,6 @@ services:
mongo: mongo:
container_name: bigcapital-mongo container_name: bigcapital-mongo
deploy:
restart_policy:
condition: unless-stopped
build: ./docker/mongo build: ./docker/mongo
expose: expose:
- '27017' - '27017'
@@ -130,9 +110,6 @@ services:
redis: redis:
container_name: bigcapital-redis container_name: bigcapital-redis
deploy:
restart_policy:
condition: unless-stopped
build: build:
context: ./docker/redis context: ./docker/redis
expose: expose:

View File

@@ -6,23 +6,20 @@
version: '3.3' version: '3.3'
services: services:
mariadb: mysql:
build: build:
context: ./docker/mariadb context: ./docker/mysql
environment: environment:
- MYSQL_DATABASE=${SYSTEM_DB_NAME} - MYSQL_DATABASE=${SYSTEM_DB_NAME}
- MYSQL_USER=${DB_USER} - MYSQL_USER=${DB_USER}
- MYSQL_PASSWORD=${DB_PASSWORD} - MYSQL_PASSWORD=${DB_PASSWORD}
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
expose: expose:
- '3306' - '3306'
ports: ports:
- '3306:3306' - '3306:3306'
deploy:
restart_policy:
condition: unless-stopped
mongo: mongo:
build: ./docker/mongo build: ./docker/mongo
@@ -32,9 +29,6 @@ services:
- mongo:/var/lib/mongodb - mongo:/var/lib/mongodb
ports: ports:
- '27017:27017' - '27017:27017'
deploy:
restart_policy:
condition: unless-stopped
redis: redis:
build: build:
@@ -43,9 +37,6 @@ services:
- "6379" - "6379"
volumes: volumes:
- redis:/data - redis:/data
deploy:
restart_policy:
condition: unless-stopped
# Volumes # Volumes
volumes: volumes:

View File

@@ -1,4 +1,4 @@
FROM mariadb:10.2 FROM mysql:5.7
USER root USER root
ADD my.cnf /etc/mysql/conf.d/my.cnf ADD my.cnf /etc/mysql/conf.d/my.cnf
@@ -17,7 +17,7 @@ ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
COPY ./init.sql /scripts/init.template.sql COPY ./init.sql /scripts/init.template.sql
COPY ./docker-entrypoint.sh /docker-entrypoint-initdb.d/docker-initialize.sh COPY ./docker-entrypoint.sh /docker-entrypoint-initdb.d/docker-initialize.sh
# The scripts in the `docker-entrypoint-initdb.d/` directory are executed as # The scripts in the docker-entrypoint-initdb.d/ directory are executed as
# the mysql user inside the MySQL Docker container. # the mysql user inside the MySQL Docker container.
RUN chown -R mysql:root /docker-entrypoint-initdb.d RUN chown -R mysql:root /docker-entrypoint-initdb.d
RUN chown -R mysql:root /scripts RUN chown -R mysql:root /scripts

View File

@@ -1,3 +1,2 @@
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION; GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
FLUSH PRIVILEGES; FLUSH PRIVILEGES;

View File

@@ -1,13 +0,0 @@
import { Page } from '@playwright/test';
export const clearLocalStorage = (page: Page) => {
return page.evaluate(`window.localStorage.clear()`);
};
export const defaultPageConfig = () => {
return {
extraHTTPHeaders: {
'ngrok-skip-browser-warning': 'any-value',
},
};
};

View File

@@ -1,23 +1,14 @@
import { test, expect, Page } from '@playwright/test'; import { test, expect, Page } from '@playwright/test';
import { faker } from '@faker-js/faker';
import { clearLocalStorage, defaultPageConfig } from './_utils';
let authPage: Page; let authPage: Page;
test.describe('authentication', () => { test.describe('authentication', () => {
test.beforeAll(async ({ browser }) => { test.beforeAll(async ({ browser }) => {
authPage = await browser.newPage({ ...defaultPageConfig() }); authPage = await browser.newPage();
});
test.afterAll(async () => {
await authPage.close();
});
test.afterEach(async ({ context }) => {
context.clearCookies();
await clearLocalStorage(authPage);
}); });
test.describe('login', () => { test.describe('login', () => {
test.beforeEach(async () => { test.beforeAll(async () => {
await authPage.goto('/auth/login'); await authPage.goto('/auth/login');
}); });
test('should show the login page.', async () => { test('should show the login page.', async () => {
@@ -39,23 +30,10 @@ test.describe('authentication', () => {
await authPage.getByRole('link', { name: 'Sign up' }).click(); await authPage.getByRole('link', { name: 'Sign up' }).click();
await expect(authPage.url()).toContain('/auth/register'); await expect(authPage.url()).toContain('/auth/register');
}); });
test('should the email or password is not correct.', async () => {
await authPage.getByLabel('Email Address').click();
await authPage.getByLabel('Email Address').fill(faker.internet.email());
await authPage.getByLabel('Password').click();
await authPage.getByLabel('Password').fill(faker.internet.password());
await authPage.getByRole('button', { name: 'Log in' }).click();
await expect(authPage.locator('body')).toContainText(
'The email and password you entered did not match our records.'
);
});
}); });
test.describe('register', () => { test.describe('register', () => {
test.beforeEach(async () => { test.beforeAll(async () => {
await authPage.goto('/auth/register'); await authPage.goto('/auth/register');
}); });
test('should first name, last name, email and password be required.', async () => { test('should first name, last name, email and password be required.', async () => {
@@ -74,36 +52,10 @@ test.describe('authentication', () => {
'Password is a required field' 'Password is a required field'
); );
}); });
test('should signup successfully.', async () => {
const form = authPage.locator('form');
await form.getByLabel('First Name').click();
await form.getByLabel('First Name').fill(faker.person.firstName());
await form.getByLabel('Email').click();
await form.getByLabel('Email').fill(faker.internet.email());
await form.getByLabel('Last Name').click();
await form.getByLabel('Last Name').fill(faker.person.lastName());
await form.getByLabel('Password').click();
await form.getByLabel('Password').fill(faker.internet.password());
await authPage.getByRole('button', { name: 'Register' }).click();
await expect(authPage.locator('h1')).toContainText(
'Register a New Organization now!'
);
});
}); });
test.describe('reset password', () => { test.describe('reset password', () => {
test.beforeAll(async ({ browser }) => { test.beforeAll(async () => {
authPage = await browser.newPage({ ...defaultPageConfig() });
});
test.afterAll(async () => {
await authPage.close();
});
test.beforeEach(async () => {
await authPage.goto('/auth/send_reset_password'); await authPage.goto('/auth/send_reset_password');
}); });
test('should email be required.', async () => { test('should email be required.', async () => {

View File

@@ -1,7 +0,0 @@
import { test, expect, Page } from '@playwright/test';
test.describe('item', () => {
test('should validate all required fields.', () => {});
test('should save the item successfully.', () => {});
test('should item code be unqiue.', () => {});
});

View File

@@ -1,86 +0,0 @@
import { test, expect, Page } from '@playwright/test';
import { faker } from '@faker-js/faker';
import { defaultPageConfig } from './_utils';
let authPage: Page;
let businessLegalName: string = faker.company.name();
test.describe('onboarding', () => {
test.beforeAll(async ({ browser }) => {
authPage = await browser.newPage({ ...defaultPageConfig() });
await authPage.goto('/auth/register');
const form = authPage.locator('form');
await form.getByLabel('First Name').fill(faker.person.firstName());
await form.getByLabel('Email').fill(faker.internet.email());
await form.getByLabel('Last Name').fill(faker.person.lastName());
await form.getByLabel('Password').fill(faker.internet.password());
await authPage.getByRole('button', { name: 'Register' }).click();
});
test('should validation catch all required fields', async () => {
const form = authPage.locator('form');
await authPage.getByRole('button', { name: 'Save & Continue' }).click();
await expect(form).toContainText('Organization name is a required field');
await expect(form).toContainText('Location is a required field');
await expect(form).toContainText('Base currency is a required field');
await expect(form).toContainText('Fiscal year is a required field');
await expect(form).toContainText('Time zone is a required field');
});
test.describe('after onboarding', () => {
test.beforeAll(async () => {
await authPage.getByLabel('Legal Organization Name').click();
await authPage
.getByLabel('Legal Organization Name')
.fill(businessLegalName);
// Fill Business Location.
await authPage
.getByRole('button', { name: 'Select Business Location...' })
.click();
await authPage.locator('a').filter({ hasText: 'Albania' }).click();
// Fill Base Currency.
await authPage
.getByRole('button', { name: 'Select Base Currency...' })
.click();
await authPage
.locator('a')
.filter({ hasText: 'AED - United Arab Emirates Dirham' })
.click();
// Fill Fasical Year.
await authPage
.getByRole('button', { name: 'Select Fiscal Year...' })
.click();
await authPage.locator('a').filter({ hasText: 'June - May' }).click();
// Fill Timezone.
await authPage
.getByRole('button', { name: 'Select Time Zone...' })
.click();
await authPage.getByText('Pacific/Marquesas-09:30').click();
// Click on Submit button
await authPage.getByRole('button', { name: 'Save & Continue' }).click();
});
test('should onboarding process success', async () => {
await expect(authPage.locator('body')).toContainText(
'Congrats! You are ready to go',
{
timeout: 30000,
}
);
});
test('should go to the dashboard after clicking on "Go to dashboard" button.', async () => {
await authPage.getByRole('button', { name: 'Go to dashboard' }).click();
await expect(
authPage.locator('[data-testId="dashboard-topbar"] h1')
).toContainText(businessLegalName);
});
});
});

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true, "useWorkspaces": true,
"version": "0.9.6", "version": "0.0.0",
"npmClient": "npm" "npmClient": "npm"
} }

14
package-lock.json generated
View File

@@ -333,12 +333,6 @@
"@jridgewell/trace-mapping": "0.3.9" "@jridgewell/trace-mapping": "0.3.9"
} }
}, },
"@faker-js/faker": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz",
"integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==",
"dev": true
},
"@gar/promisify": { "@gar/promisify": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
@@ -951,7 +945,6 @@
"version": "1.32.3", "version": "1.32.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.3.tgz",
"integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==", "integrity": "sha512-BvWNvK0RfBriindxhLVabi8BRe3X0J9EVjKlcmhxjg4giWBD/xleLcg2dz7Tx0agu28rczjNIPQWznwzDwVsZQ==",
"dev": true,
"requires": { "requires": {
"@types/node": "*", "@types/node": "*",
"fsevents": "2.3.2", "fsevents": "2.3.2",
@@ -961,8 +954,7 @@
"playwright-core": { "playwright-core": {
"version": "1.32.3", "version": "1.32.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.3.tgz",
"integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg==", "integrity": "sha512-SB+cdrnu74ZIn5Ogh/8278ngEh9NEEV0vR4sJFmK04h2iZpybfbqBY0bX6+BLYWVdV12JLLI+JEFtSnYgR+mWg=="
"dev": true
} }
} }
}, },
@@ -1011,8 +1003,7 @@
"@types/node": { "@types/node": {
"version": "18.14.6", "version": "18.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.6.tgz",
"integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==", "integrity": "sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA=="
"dev": true
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.1", "version": "2.4.1",
@@ -2333,7 +2324,6 @@
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true "optional": true
}, },
"function-bind": { "function-bind": {

View File

@@ -18,13 +18,12 @@
"shared/*" "shared/*"
], ],
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.4.2",
"@commitlint/config-conventional": "^17.4.2", "@commitlint/config-conventional": "^17.4.2",
"@commitlint/config-lerna-scopes": "^17.4.2", "@commitlint/config-lerna-scopes": "^17.4.2",
"@faker-js/faker": "^8.0.2",
"@playwright/test": "^1.32.3",
"husky": "^8.0.3", "husky": "^8.0.3",
"lerna": "^6.4.1" "lerna": "^6.4.1",
"@commitlint/cli": "^17.4.2",
"@playwright/test": "^1.32.3"
}, },
"engines": { "engines": {
"node": "14.x" "node": "14.x"

View File

@@ -34,11 +34,7 @@ ARG MAIL_HOST= \
BASE_URL= \ BASE_URL= \
# Agendash # Agendash
AGENDASH_AUTH_USER=agendash \ AGENDASH_AUTH_USER=agendash \
AGENDASH_AUTH_PASSWORD=123123 \ AGENDASH_AUTH_PASSWORD=123123
# Sign-up restriction
SIGNUP_DISABLED= \
SIGNUP_ALLOWED_DOMAINS= \
SIGNUP_ALLOWED_EMAILS=
ENV MAIL_HOST=$MAIL_HOST \ ENV MAIL_HOST=$MAIL_HOST \
MAIL_USERNAME=$MAIL_USERNAME \ MAIL_USERNAME=$MAIL_USERNAME \
@@ -72,11 +68,7 @@ ENV MAIL_HOST=$MAIL_HOST \
# MongoDB # MongoDB
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \ MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
# Application # Application
BASE_URL=$BASE_URL \ BASE_URL=$BASE_URL
# Sign-up restriction
SIGNUP_DISABLED=$SIGNUP_DISABLED \
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
# Create app directory. # Create app directory.
WORKDIR /app WORKDIR /app

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bigcapital/server", "name": "@bigcapital/server",
"version": "0.9.5", "version": "1.7.1",
"description": "", "description": "",
"main": "src/server.ts", "main": "src/server.ts",
"scripts": { "scripts": {

View File

@@ -65,9 +65,6 @@ exports.getCommonWebpackOptions = ({
}, },
], ],
}, },
optimization: {
minimize: false,
},
}; };
if (isDev) { if (isDev) {

View File

@@ -3,12 +3,7 @@ import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
AbilitySubject,
AccountAction,
IAccountDTO,
IAccountsStructureType,
} from '@/interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from '@/data/DataTypes'; import { DATATYPES_LENGTH } from '@/data/DataTypes';
@@ -177,11 +172,6 @@ export default class AccountsController extends BaseController {
query('inactive_mode').optional().isBoolean().toBoolean(), query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim(), query('search_keyword').optional({ nullable: true }).isString().trim(),
query('structure')
.optional()
.isString()
.isIn([IAccountsStructureType.Tree, IAccountsStructureType.Flat]),
]; ];
} }
@@ -351,7 +341,6 @@ export default class AccountsController extends BaseController {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
inactiveMode: false, inactiveMode: false,
structure: IAccountsStructureType.Tree,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };

View File

@@ -49,7 +49,6 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.resetPassword.bind(this)), asyncMiddleware(this.resetPassword.bind(this)),
this.handlerErrors this.handlerErrors
); );
router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this)));
return router; return router;
} }
@@ -92,7 +91,6 @@ export default class AuthenticationController extends BaseController {
check('password') check('password')
.exists() .exists()
.isString() .isString()
.isLength({ min: 6 })
.trim() .trim()
.escape() .escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
@@ -107,7 +105,7 @@ export default class AuthenticationController extends BaseController {
return [ return [
check('password') check('password')
.exists() .exists()
.isLength({ min: 6 }) .isLength({ min: 5 })
.custom((value, { req }) => { .custom((value, { req }) => {
if (value !== req.body.confirm_password) { if (value !== req.body.confirm_password) {
throw new Error("Passwords don't match"); throw new Error("Passwords don't match");
@@ -209,23 +207,6 @@ export default class AuthenticationController extends BaseController {
} }
} }
/**
* Retrieves the authentication meta for SPA.
* @param {Request} req
* @param {Response} res
* @param {Function} next
* @returns {Response|void}
*/
private async getAuthMeta(req: Request, res: Response, next: Function) {
try {
const meta = await this.authApplication.getAuthMeta();
return res.status(200).send({ meta });
} catch (error) {
next(error);
}
}
/** /**
* Handles the service errors. * Handles the service errors.
*/ */
@@ -266,30 +247,6 @@ export default class AuthenticationController extends BaseController {
errors: [{ type: 'EMAIL.EXISTS', code: 600 }], errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
}); });
} }
if (error.errorType === 'SIGNUP_RESTRICTED') {
return res.status(400).send({
errors: [
{
type: 'SIGNUP_RESTRICTED',
message:
'Sign-up is restricted no one can sign-up to the system.',
code: 700,
},
],
});
}
if (error.errorType === 'SIGNUP_RESTRICTED_NOT_ALLOWED') {
return res.status(400).send({
errors: [
{
type: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
message:
'Sign-up is restricted the given email address is not allowed to sign-up.',
code: 710,
},
],
});
}
} }
next(error); next(error);
} }

View File

@@ -41,7 +41,7 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
get balanceSheetValidationSchema(): ValidationChain[] { get balanceSheetValidationSchema(): ValidationChain[] {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('accounting_method').optional().isIn(['cash', 'accrual']), query('accounting_method').optional().isIn(['cash', 'accural']),
query('from_date').optional(), query('from_date').optional(),
query('to_date').optional(), query('to_date').optional(),

View File

@@ -67,7 +67,6 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
try { try {
const { data, query, meta } = const { data, query, meta } =
await this.generalLedgetService.generalLedger(tenantId, filter); await this.generalLedgetService.generalLedger(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
meta: this.transfromToResponse(meta), meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data), data: this.transfromToResponse(data),

View File

@@ -58,7 +58,7 @@ export default class OrganizationController extends BaseController {
private get organizationValidationSchema(): ValidationChain[] { private get organizationValidationSchema(): ValidationChain[] {
return [ return [
check('name').exists().trim(), check('name').exists().trim(),
check('industry').optional({ nullable: true }).isString().trim().escape(), check('industry').optional().isString(),
check('location').exists().isString().isISO31661Alpha2(), check('location').exists().isString().isISO31661Alpha2(),
check('base_currency').exists().isISO4217(), check('base_currency').exists().isISO4217(),
check('timezone').exists().isIn(moment.tz.names()), check('timezone').exists().isIn(moment.tz.names()),

View File

@@ -1,27 +1,26 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator'; import { check, param, query } from 'express-validator';
import { import { Service, Inject } from 'typedi';
AbilitySubject, import { AbilitySubject, BillAction, IBillDTO, IBillEditDTO } from '@/interfaces';
BillAction,
IBillDTO,
IBillEditDTO,
} from '@/interfaces';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BillsService from '@/services/Purchases/Bills';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { BillsApplication } from '@/services/Purchases/Bills/BillsApplication'; import BillPaymentsService from '@/services/Purchases/BillPaymentsService';
@Service() @Service()
export default class BillsController extends BaseController { export default class BillsController extends BaseController {
@Inject() @Inject()
private billsApplication: BillsApplication; private billsService: BillsService;
@Inject() @Inject()
private dynamicListService: DynamicListingService; private dynamicListService: DynamicListingService;
@Inject()
private billPayments: BillPaymentsService;
/** /**
* Router constructor. * Router constructor.
*/ */
@@ -98,7 +97,7 @@ export default class BillsController extends BaseController {
/** /**
* Common validation schema. * Common validation schema.
*/ */
private get billValidationSchema() { get billValidationSchema() {
return [ return [
check('bill_number').exists().trim().escape(), check('bill_number').exists().trim().escape(),
check('reference_no').optional().trim().escape(), check('reference_no').optional().trim().escape(),
@@ -143,7 +142,7 @@ export default class BillsController extends BaseController {
/** /**
* Common validation schema. * Common validation schema.
*/ */
private get billEditValidationSchema() { get billEditValidationSchema() {
return [ return [
check('bill_number').optional().trim().escape(), check('bill_number').optional().trim().escape(),
check('reference_no').optional().trim().escape(), check('reference_no').optional().trim().escape(),
@@ -185,14 +184,14 @@ export default class BillsController extends BaseController {
/** /**
* Bill validation schema. * Bill validation schema.
*/ */
private get specificBillValidationSchema() { get specificBillValidationSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Bills list validation schema. * Bills list validation schema.
*/ */
private get billsListingValidationSchema() { get billsListingValidationSchema() {
return [ return [
query('view_slug').optional().isString().trim(), query('view_slug').optional().isString().trim(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -204,7 +203,7 @@ export default class BillsController extends BaseController {
]; ];
} }
private get dueBillsListingValidationSchema() { get dueBillsListingValidationSchema() {
return [ return [
query('vendor_id').optional().trim().escape(), query('vendor_id').optional().trim().escape(),
query('payment_made_id').optional().trim().escape(), query('payment_made_id').optional().trim().escape(),
@@ -217,16 +216,17 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
private async newBill(req: Request, res: Response, next: NextFunction) { async newBill(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req; const { tenantId, user } = req;
const billDTO: IBillDTO = this.matchedBodyData(req); const billDTO: IBillDTO = this.matchedBodyData(req);
try { try {
const storedBill = await this.billsApplication.createBill( const storedBill = await this.billsService.createBill(
tenantId, tenantId,
billDTO, billDTO,
user user
); );
return res.status(200).send({ return res.status(200).send({
id: storedBill.id, id: storedBill.id,
message: 'The bill has been created successfully.', message: 'The bill has been created successfully.',
@@ -241,13 +241,13 @@ export default class BillsController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async editBill(req: Request, res: Response, next: NextFunction) { async editBill(req: Request, res: Response, next: NextFunction) {
const { id: billId } = req.params; const { id: billId } = req.params;
const { tenantId, user } = req; const { tenantId, user } = req;
const billDTO: IBillEditDTO = this.matchedBodyData(req); const billDTO: IBillEditDTO = this.matchedBodyData(req);
try { try {
await this.billsApplication.editBill(tenantId, billId, billDTO, user); await this.billsService.editBill(tenantId, billId, billDTO, user);
return res.status(200).send({ return res.status(200).send({
id: billId, id: billId,
@@ -263,12 +263,12 @@ export default class BillsController extends BaseController {
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
private async openBill(req: Request, res: Response, next: NextFunction) { async openBill(req: Request, res: Response, next: NextFunction) {
const { id: billId } = req.params; const { id: billId } = req.params;
const { tenantId } = req; const { tenantId } = req;
try { try {
await this.billsApplication.openBill(tenantId, billId); await this.billsService.openBill(tenantId, billId);
return res.status(200).send({ return res.status(200).send({
id: billId, id: billId,
@@ -285,12 +285,12 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
private async getBill(req: Request, res: Response, next: NextFunction) { async getBill(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: billId } = req.params; const { id: billId } = req.params;
try { try {
const bill = await this.billsApplication.getBill(tenantId, billId); const bill = await this.billsService.getBill(tenantId, billId);
return res.status(200).send(this.transfromToResponse({ bill })); return res.status(200).send(this.transfromToResponse({ bill }));
} catch (error) { } catch (error) {
@@ -304,12 +304,12 @@ export default class BillsController extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} * @return {Response}
*/ */
private async deleteBill(req: Request, res: Response, next: NextFunction) { async deleteBill(req: Request, res: Response, next: NextFunction) {
const billId = req.params.id; const billId = req.params.id;
const { tenantId } = req; const { tenantId } = req;
try { try {
await this.billsApplication.deleteBill(tenantId, billId); await this.billsService.deleteBill(tenantId, billId);
return res.status(200).send({ return res.status(200).send({
id: billId, id: billId,
@@ -326,7 +326,7 @@ export default class BillsController extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} * @return {Response}
*/ */
private async billsList(req: Request, res: Response, next: NextFunction) { public async billsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
page: 1, page: 1,
@@ -338,7 +338,7 @@ export default class BillsController extends BaseController {
try { try {
const { bills, pagination, filterMeta } = const { bills, pagination, filterMeta } =
await this.billsApplication.getBills(tenantId, filter); await this.billsService.getBills(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
bills: this.transfromToResponse(bills), bills: this.transfromToResponse(bills),
@@ -356,13 +356,12 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getDueBills(req: Request, res: Response, next: NextFunction) { public async getDueBills(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { vendorId } = this.matchedQueryData(req); const { vendorId } = this.matchedQueryData(req);
try { try {
const bills = await this.billsApplication.getDueBills(tenantId, vendorId); const bills = await this.billsService.getDueBills(tenantId, vendorId);
return res.status(200).send({ bills }); return res.status(200).send({ bills });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -375,7 +374,7 @@ export default class BillsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private getBillPaymentsTransactions = async ( public getBillPaymentsTransactions = async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -384,7 +383,7 @@ export default class BillsController extends BaseController {
const { id: billId } = req.params; const { id: billId } = req.params;
try { try {
const billPayments = await this.billsApplication.getBillPayments( const billPayments = await this.billPayments.getBillPayments(
tenantId, tenantId,
billId billId
); );

View File

@@ -4,7 +4,7 @@ import { check, param, query, ValidationChain } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { BillPaymentsApplication } from '@/services/Purchases/BillPayments/BillPaymentsApplication'; import BillPaymentsService from '@/services/Purchases/BillPayments/BillPayments';
import BillPaymentsPages from '@/services/Purchases/BillPayments/BillPaymentsPages'; import BillPaymentsPages from '@/services/Purchases/BillPayments/BillPaymentsPages';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
@@ -17,18 +17,18 @@ import { AbilitySubject, IPaymentMadeAction } from '@/interfaces';
@Service() @Service()
export default class BillsPayments extends BaseController { export default class BillsPayments extends BaseController {
@Inject() @Inject()
private billPaymentsApplication: BillPaymentsApplication; billPaymentService: BillPaymentsService;
@Inject() @Inject()
private dynamicListService: DynamicListingService; dynamicListService: DynamicListingService;
@Inject() @Inject()
private billPaymentsPages: BillPaymentsPages; billPaymentsPages: BillPaymentsPages;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -106,7 +106,7 @@ export default class BillsPayments extends BaseController {
* Bill payments schema validation. * Bill payments schema validation.
* @return {ValidationChain[]} * @return {ValidationChain[]}
*/ */
private get billPaymentSchemaValidation(): ValidationChain[] { get billPaymentSchemaValidation(): ValidationChain[] {
return [ return [
check('vendor_id').exists().isNumeric().toInt(), check('vendor_id').exists().isNumeric().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
@@ -129,7 +129,7 @@ export default class BillsPayments extends BaseController {
* Specific bill payment schema validation. * Specific bill payment schema validation.
* @returns {ValidationChain[]} * @returns {ValidationChain[]}
*/ */
private get specificBillPaymentValidateSchema(): ValidationChain[] { get specificBillPaymentValidateSchema(): ValidationChain[] {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
@@ -137,7 +137,7 @@ export default class BillsPayments extends BaseController {
* Bills payment list validation schema. * Bills payment list validation schema.
* @returns {ValidationChain[]} * @returns {ValidationChain[]}
*/ */
private get listingValidationSchema(): ValidationChain[] { get listingValidationSchema(): ValidationChain[] {
return [ return [
query('custom_view_id').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -154,7 +154,7 @@ export default class BillsPayments extends BaseController {
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
private async getBillPaymentNewPageEntries(req: Request, res: Response) { async getBillPaymentNewPageEntries(req: Request, res: Response) {
const { tenantId } = req; const { tenantId } = req;
const { vendorId } = this.matchedQueryData(req); const { vendorId } = this.matchedQueryData(req);
@@ -174,7 +174,7 @@ export default class BillsPayments extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async getBillPaymentEditPage( async getBillPaymentEditPage(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -205,19 +205,16 @@ export default class BillsPayments extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Response} res * @param {Response} res
*/ */
private async createBillPayment( async createBillPayment(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const billPaymentDTO = this.matchedBodyData(req); const billPaymentDTO = this.matchedBodyData(req);
try { try {
const billPayment = await this.billPaymentsApplication.createBillPayment( const billPayment = await this.billPaymentService.createBillPayment(
tenantId, tenantId,
billPaymentDTO billPaymentDTO
); );
return res.status(200).send({ return res.status(200).send({
id: billPayment.id, id: billPayment.id,
message: 'Payment made has been created successfully.', message: 'Payment made has been created successfully.',
@@ -232,17 +229,13 @@ export default class BillsPayments extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async editBillPayment( async editBillPayment(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const billPaymentDTO = this.matchedBodyData(req); const billPaymentDTO = this.matchedBodyData(req);
const { id: billPaymentId } = req.params; const { id: billPaymentId } = req.params;
try { try {
const paymentMade = await this.billPaymentsApplication.editBillPayment( const paymentMade = await this.billPaymentService.editBillPayment(
tenantId, tenantId,
billPaymentId, billPaymentId,
billPaymentDTO billPaymentDTO
@@ -263,19 +256,12 @@ export default class BillsPayments extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} res - * @return {Response} res -
*/ */
private async deleteBillPayment( async deleteBillPayment(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const { id: billPaymentId } = req.params; const { id: billPaymentId } = req.params;
try { try {
await this.billPaymentsApplication.deleteBillPayment( await this.billPaymentService.deleteBillPayment(tenantId, billPaymentId);
tenantId,
billPaymentId
);
return res.status(200).send({ return res.status(200).send({
id: billPaymentId, id: billPaymentId,
@@ -291,19 +277,16 @@ export default class BillsPayments extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async getBillPayment( async getBillPayment(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const { id: billPaymentId } = req.params; const { id: billPaymentId } = req.params;
try { try {
const billPayment = await this.billPaymentsApplication.getBillPayment( const billPayment = await this.billPaymentService.getBillPayment(
tenantId, tenantId,
billPaymentId billPaymentId
); );
return res.status(200).send({ return res.status(200).send({
bill_payment: this.transfromToResponse(billPayment), bill_payment: this.transfromToResponse(billPayment),
}); });
@@ -318,16 +301,12 @@ export default class BillsPayments extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getPaymentBills( async getPaymentBills(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const { id: billPaymentId } = req.params; const { id: billPaymentId } = req.params;
try { try {
const bills = await this.billPaymentsApplication.getPaymentBills( const bills = await this.billPaymentService.getPaymentBills(
tenantId, tenantId,
billPaymentId billPaymentId
); );
@@ -343,11 +322,7 @@ export default class BillsPayments extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} * @return {Response}
*/ */
private async getBillsPayments( async getBillsPayments(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const billPaymentsFilter = { const billPaymentsFilter = {
page: 1, page: 1,
@@ -360,7 +335,7 @@ export default class BillsPayments extends BaseController {
try { try {
const { billPayments, pagination, filterMeta } = const { billPayments, pagination, filterMeta } =
await this.billPaymentsApplication.getBillPayments( await this.billPaymentService.listBillPayments(
tenantId, tenantId,
billPaymentsFilter billPaymentsFilter
); );
@@ -382,7 +357,7 @@ export default class BillsPayments extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private handleServiceError( handleServiceError(
error: Error, error: Error,
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -1,29 +1,42 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, ValidationChain } from 'express-validator'; import { check, param, query, ValidationChain } from 'express-validator';
import { Inject, Service } from 'typedi';
import { import {
AbilitySubject, AbilitySubject,
IPaymentReceiveDTO, IPaymentReceiveDTO,
PaymentReceiveAction, PaymentReceiveAction,
SaleInvoiceAction,
} from '@/interfaces'; } from '@/interfaces';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives';
import PaymentReceivesPages from '@/services/Sales/PaymentReceives/PaymentReceivesPages'; import PaymentReceivesPages from '@/services/Sales/PaymentReceives/PaymentReceivesPages';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import PaymentReceiveNotifyBySms from '@/services/Sales/PaymentReceives/PaymentReceiveSmsNotify';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import GetPaymentReceivePdf from '@/services/Sales/PaymentReceives/GetPaymentReeceivePdf';
/**
* Payments receives controller.
* @service
*/
@Service() @Service()
export default class PaymentReceivesController extends BaseController { export default class PaymentReceivesController extends BaseController {
@Inject() @Inject()
private paymentReceiveApplication: PaymentReceivesApplication; paymentReceiveService: PaymentReceiveService;
@Inject() @Inject()
private PaymentReceivesPages: PaymentReceivesPages; PaymentReceivesPages: PaymentReceivesPages;
@Inject() @Inject()
private dynamicListService: DynamicListingService; dynamicListService: DynamicListingService;
@Inject()
paymentReceiveSmsNotify: PaymentReceiveNotifyBySms;
@Inject()
paymentReceivePdf: GetPaymentReceivePdf;
/** /**
* Router constructor. * Router constructor.
@@ -124,7 +137,7 @@ export default class PaymentReceivesController extends BaseController {
* Payment receive schema. * Payment receive schema.
* @return {Array} * @return {Array}
*/ */
private get paymentReceiveSchema(): ValidationChain[] { get paymentReceiveSchema(): ValidationChain[] {
return [ return [
check('customer_id').exists().isNumeric().toInt(), check('customer_id').exists().isNumeric().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
@@ -149,7 +162,7 @@ export default class PaymentReceivesController extends BaseController {
/** /**
* Payment receive list validation schema. * Payment receive list validation schema.
*/ */
private get validatePaymentReceiveList(): ValidationChain[] { get validatePaymentReceiveList(): ValidationChain[] {
return [ return [
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -168,7 +181,7 @@ export default class PaymentReceivesController extends BaseController {
/** /**
* Validate payment receive parameters. * Validate payment receive parameters.
*/ */
private get paymentReceiveValidation() { get paymentReceiveValidation() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
@@ -176,14 +189,14 @@ export default class PaymentReceivesController extends BaseController {
* New payment receive validation schema. * New payment receive validation schema.
* @return {Array} * @return {Array}
*/ */
private get newPaymentReceiveValidation() { get newPaymentReceiveValidation() {
return [...this.paymentReceiveSchema]; return [...this.paymentReceiveSchema];
} }
/** /**
* Edit payment receive validation. * Edit payment receive validation.
*/ */
private get editPaymentReceiveValidation() { get editPaymentReceiveValidation() {
return [ return [
param('id').exists().isNumeric().toInt(), param('id').exists().isNumeric().toInt(),
...this.paymentReceiveSchema, ...this.paymentReceiveSchema,
@@ -196,17 +209,13 @@ export default class PaymentReceivesController extends BaseController {
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
private async newPaymentReceive( async newPaymentReceive(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req; const { tenantId, user } = req;
const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req); const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
try { try {
const storedPaymentReceive = const storedPaymentReceive =
await this.paymentReceiveApplication.createPaymentReceive( await this.paymentReceiveService.createPaymentReceive(
tenantId, tenantId,
paymentReceive, paymentReceive,
user user
@@ -226,18 +235,14 @@ export default class PaymentReceivesController extends BaseController {
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
private async editPaymentReceive( async editPaymentReceive(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req; const { tenantId, user } = req;
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req); const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
try { try {
await this.paymentReceiveApplication.editPaymentReceive( await this.paymentReceiveService.editPaymentReceive(
tenantId, tenantId,
paymentReceiveId, paymentReceiveId,
paymentReceive, paymentReceive,
@@ -257,20 +262,17 @@ export default class PaymentReceivesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async deletePaymentReceive( async deletePaymentReceive(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req; const { tenantId, user } = req;
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
try { try {
await this.paymentReceiveApplication.deletePaymentReceive( await this.paymentReceiveService.deletePaymentReceive(
tenantId, tenantId,
paymentReceiveId, paymentReceiveId,
user user
); );
return res.status(200).send({ return res.status(200).send({
id: paymentReceiveId, id: paymentReceiveId,
message: 'The payment receive has been deleted successfully', message: 'The payment receive has been deleted successfully',
@@ -286,7 +288,7 @@ export default class PaymentReceivesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getPaymentReceiveInvoices( async getPaymentReceiveInvoices(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -296,7 +298,7 @@ export default class PaymentReceivesController extends BaseController {
try { try {
const saleInvoices = const saleInvoices =
await this.paymentReceiveApplication.getPaymentReceiveInvoices( await this.paymentReceiveService.getPaymentReceiveInvoices(
tenantId, tenantId,
paymentReceiveId paymentReceiveId
); );
@@ -313,11 +315,7 @@ export default class PaymentReceivesController extends BaseController {
* @param {Response} res * @param {Response} res
* @return {Response} * @return {Response}
*/ */
private async getPaymentReceiveList( async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
sortOrder: 'desc', sortOrder: 'desc',
@@ -329,10 +327,7 @@ export default class PaymentReceivesController extends BaseController {
try { try {
const { paymentReceives, pagination, filterMeta } = const { paymentReceives, pagination, filterMeta } =
await this.paymentReceiveApplication.getPaymentReceives( await this.paymentReceiveService.listPaymentReceives(tenantId, filter);
tenantId,
filter
);
return res.status(200).send({ return res.status(200).send({
payment_receives: this.transfromToResponse(paymentReceives), payment_receives: this.transfromToResponse(paymentReceives),
@@ -349,7 +344,7 @@ export default class PaymentReceivesController extends BaseController {
* @param {Request} req - Request. * @param {Request} req - Request.
* @param {Response} res - Response. * @param {Response} res - Response.
*/ */
private async getPaymentReceiveNewPageEntries( async getPaymentReceiveNewPageEntries(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -376,7 +371,7 @@ export default class PaymentReceivesController extends BaseController {
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
private async getPaymentReceiveEditPage( async getPaymentReceiveEditPage(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -407,20 +402,15 @@ export default class PaymentReceivesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getPaymentReceive( async getPaymentReceive(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
try { try {
const paymentReceive = const paymentReceive = await this.paymentReceiveService.getPaymentReceive(
await this.paymentReceiveApplication.getPaymentReceive( tenantId,
tenantId, paymentReceiveId
paymentReceiveId );
);
const ACCEPT_TYPE = { const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf', APPLICATION_PDF: 'application/pdf',
@@ -433,11 +423,10 @@ export default class PaymentReceivesController extends BaseController {
}); });
}, },
[ACCEPT_TYPE.APPLICATION_PDF]: async () => { [ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = const pdfContent = await this.paymentReceivePdf.getPaymentReceivePdf(
await this.paymentReceiveApplication.getPaymentReceivePdf( tenantId,
tenantId, paymentReceive
paymentReceive );
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
@@ -465,11 +454,10 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
try { try {
const paymentReceive = const paymentReceive = await this.paymentReceiveSmsNotify.notifyBySms(
await this.paymentReceiveApplication.notifyPaymentBySms( tenantId,
tenantId, paymentReceiveId
paymentReceiveId );
);
return res.status(200).send({ return res.status(200).send({
id: paymentReceive.id, id: paymentReceive.id,
message: 'The payment notification has been sent successfully.', message: 'The payment notification has been sent successfully.',
@@ -494,11 +482,10 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
try { try {
const smsDetails = const smsDetails = await this.paymentReceiveSmsNotify.smsDetails(
await this.paymentReceiveApplication.getPaymentSmsDetails( tenantId,
tenantId, paymentReceiveId
paymentReceiveId );
);
return res.status(200).send({ return res.status(200).send({
data: smsDetails, data: smsDetails,
}); });

View File

@@ -1,17 +1,20 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator'; import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { import {
AbilitySubject, AbilitySubject,
ISaleEstimateDTO, ISaleEstimateDTO,
SaleEstimateAction, SaleEstimateAction,
SaleInvoiceAction,
} from '@/interfaces'; } from '@/interfaces';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import SaleEstimateService from '@/services/Sales/SalesEstimate';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import SaleEstimatesPdfService from '@/services/Sales/Estimates/SaleEstimatesPdf';
import SaleEstimateNotifyBySms from '@/services/Sales/Estimates/SaleEstimateSmsNotify';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { SaleEstimatesApplication } from '@/services/Sales/Estimates/SaleEstimatesApplication';
const ACCEPT_TYPE = { const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf', APPLICATION_PDF: 'application/pdf',
@@ -20,15 +23,21 @@ const ACCEPT_TYPE = {
@Service() @Service()
export default class SalesEstimatesController extends BaseController { export default class SalesEstimatesController extends BaseController {
@Inject() @Inject()
private saleEstimatesApplication: SaleEstimatesApplication; saleEstimateService: SaleEstimateService;
@Inject() @Inject()
private dynamicListService: DynamicListingService; dynamicListService: DynamicListingService;
@Inject()
saleEstimatesPdf: SaleEstimatesPdfService;
@Inject()
saleEstimateNotifySms: SaleEstimateNotifyBySms;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -127,7 +136,7 @@ export default class SalesEstimatesController extends BaseController {
/** /**
* Estimate validation schema. * Estimate validation schema.
*/ */
private get estimateValidationSchema() { get estimateValidationSchema() {
return [ return [
check('customer_id').exists().isNumeric().toInt(), check('customer_id').exists().isNumeric().toInt(),
check('estimate_date').exists().isISO8601().toDate(), check('estimate_date').exists().isISO8601().toDate(),
@@ -168,14 +177,14 @@ export default class SalesEstimatesController extends BaseController {
/** /**
* Specific sale estimate validation schema. * Specific sale estimate validation schema.
*/ */
private get validateSpecificEstimateSchema() { get validateSpecificEstimateSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Sales estimates list validation schema. * Sales estimates list validation schema.
*/ */
private get validateEstimateListSchema() { get validateEstimateListSchema() {
return [ return [
query('view_slug').optional().isString().trim(), query('view_slug').optional().isString().trim(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -193,16 +202,15 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res - * @param {Response} res -
* @return {Response} res - * @return {Response} res -
*/ */
private async newEstimate(req: Request, res: Response, next: NextFunction) { async newEstimate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req); const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req);
try { try {
const storedEstimate = const storedEstimate = await this.saleEstimateService.createEstimate(
await this.saleEstimatesApplication.createSaleEstimate( tenantId,
tenantId, estimateDTO
estimateDTO );
);
return res.status(200).send({ return res.status(200).send({
id: storedEstimate.id, id: storedEstimate.id,
@@ -218,18 +226,19 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async editEstimate(req: Request, res: Response, next: NextFunction) { async editEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
const { tenantId } = req; const { tenantId } = req;
const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req); const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req);
try { try {
// Update estimate with associated estimate entries. // Update estimate with associated estimate entries.
await this.saleEstimatesApplication.editSaleEstimate( await this.saleEstimateService.editEstimate(
tenantId, tenantId,
estimateId, estimateId,
estimateDTO estimateDTO
); );
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
message: 'The sale estimate has been created successfully.', message: 'The sale estimate has been created successfully.',
@@ -244,19 +253,13 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async deleteEstimate( async deleteEstimate(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
const { tenantId } = req; const { tenantId } = req;
try { try {
await this.saleEstimatesApplication.deleteSaleEstimate( await this.saleEstimateService.deleteEstimate(tenantId, estimateId);
tenantId,
estimateId
);
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
message: 'The sale estimate has been deleted successfully.', message: 'The sale estimate has been deleted successfully.',
@@ -271,19 +274,13 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async deliverSaleEstimate( async deliverSaleEstimate(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
const { tenantId } = req; const { tenantId } = req;
try { try {
await this.saleEstimatesApplication.deliverSaleEstimate( await this.saleEstimateService.deliverSaleEstimate(tenantId, estimateId);
tenantId,
estimateId
);
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
message: 'The sale estimate has been delivered successfully.', message: 'The sale estimate has been delivered successfully.',
@@ -299,19 +296,12 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async approveSaleEstimate( async approveSaleEstimate(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
const { tenantId } = req; const { tenantId } = req;
try { try {
await this.saleEstimatesApplication.approveSaleEstimate( await this.saleEstimateService.approveSaleEstimate(tenantId, estimateId);
tenantId,
estimateId
);
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
@@ -328,19 +318,12 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async rejectSaleEstimate( async rejectSaleEstimate(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
const { tenantId } = req; const { tenantId } = req;
try { try {
await this.saleEstimatesApplication.rejectSaleEstimate( await this.saleEstimateService.rejectSaleEstimate(tenantId, estimateId);
tenantId,
estimateId
);
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
@@ -357,12 +340,12 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getEstimate(req: Request, res: Response, next: NextFunction) { async getEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
const { tenantId } = req; const { tenantId } = req;
try { try {
const estimate = await this.saleEstimatesApplication.getSaleEstimate( const estimate = await this.saleEstimateService.getEstimate(
tenantId, tenantId,
estimateId estimateId
); );
@@ -374,11 +357,10 @@ export default class SalesEstimatesController extends BaseController {
}, },
// PDF content type. // PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => { [ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = const pdfContent = await this.saleEstimatesPdf.saleEstimatePdf(
await this.saleEstimatesApplication.getSaleEstimatePdf( tenantId,
tenantId, estimate
estimate );
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
@@ -396,7 +378,7 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async getEstimates(req: Request, res: Response, next: NextFunction) { async getEstimates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
sortOrder: 'desc', sortOrder: 'desc',
@@ -408,7 +390,7 @@ export default class SalesEstimatesController extends BaseController {
try { try {
const { salesEstimates, pagination, filterMeta } = const { salesEstimates, pagination, filterMeta } =
await this.saleEstimatesApplication.getSaleEstimates(tenantId, filter); await this.saleEstimateService.estimatesList(tenantId, filter);
res.format({ res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => { [ACCEPT_TYPE.APPLICATION_JSON]: () => {
@@ -426,7 +408,7 @@ export default class SalesEstimatesController extends BaseController {
} }
} }
private saleEstimateNotifyBySms = async ( public saleEstimateNotifyBySms = async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -435,11 +417,10 @@ export default class SalesEstimatesController extends BaseController {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
try { try {
const saleEstimate = const saleEstimate = await this.saleEstimateNotifySms.notifyBySms(
await this.saleEstimatesApplication.notifySaleEstimateBySms( tenantId,
tenantId, estimateId
estimateId );
);
return res.status(200).send({ return res.status(200).send({
id: saleEstimate.id, id: saleEstimate.id,
message: message:
@@ -456,7 +437,7 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private saleEstimateSmsDetails = async ( public saleEstimateSmsDetails = async (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
@@ -465,11 +446,10 @@ export default class SalesEstimatesController extends BaseController {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
try { try {
const estimateSmsDetails = const estimateSmsDetails = await this.saleEstimateNotifySms.smsDetails(
await this.saleEstimatesApplication.getSaleEstimateSmsDetails( tenantId,
tenantId, estimateId
estimateId );
);
return res.status(200).send({ return res.status(200).send({
data: estimateSmsDetails, data: estimateSmsDetails,
}); });

View File

@@ -3,6 +3,7 @@ import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import SaleInvoiceService from '@/services/Sales/SalesInvoices';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
@@ -11,8 +12,11 @@ import {
SaleInvoiceAction, SaleInvoiceAction,
AbilitySubject, AbilitySubject,
} from '@/interfaces'; } from '@/interfaces';
import SaleInvoicePdf from '@/services/Sales/SaleInvoicePdf';
import SaleInvoiceWriteoff from '@/services/Sales/SaleInvoiceWriteoff';
import SaleInvoiceNotifyBySms from '@/services/Sales/SaleInvoiceNotifyBySms';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication'; import InvoicePaymentsService from '@/services/Sales/Invoices/InvoicePaymentsService';
const ACCEPT_TYPE = { const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf', APPLICATION_PDF: 'application/pdf',
@@ -21,15 +25,27 @@ const ACCEPT_TYPE = {
@Service() @Service()
export default class SaleInvoicesController extends BaseController { export default class SaleInvoicesController extends BaseController {
@Inject() @Inject()
private saleInvoiceApplication: SaleInvoiceApplication; saleInvoiceService: SaleInvoiceService;
@Inject() @Inject()
private dynamicListService: DynamicListingService; dynamicListService: DynamicListingService;
@Inject()
saleInvoicePdf: SaleInvoicePdf;
@Inject()
saleInvoiceWriteoff: SaleInvoiceWriteoff;
@Inject()
saleInvoiceSmsNotify: SaleInvoiceNotifyBySms;
@Inject()
invoicePaymentsSerivce: InvoicePaymentsService;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -151,7 +167,7 @@ export default class SaleInvoicesController extends BaseController {
/** /**
* Sale invoice validation schema. * Sale invoice validation schema.
*/ */
private get saleInvoiceValidationSchema() { get saleInvoiceValidationSchema() {
return [ return [
check('customer_id').exists().isNumeric().toInt(), check('customer_id').exists().isNumeric().toInt(),
check('invoice_date').exists().isISO8601().toDate(), check('invoice_date').exists().isISO8601().toDate(),
@@ -211,14 +227,14 @@ export default class SaleInvoicesController extends BaseController {
/** /**
* Specific sale invoice validation schema. * Specific sale invoice validation schema.
*/ */
private get specificSaleInvoiceValidation() { get specificSaleInvoiceValidation() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Sales invoices list validation schema. * Sales invoices list validation schema.
*/ */
private get saleInvoiceListValidationSchema() { get saleInvoiceListValidationSchema() {
return [ return [
query('view_slug').optional({ nullable: true }).isString().trim(), query('view_slug').optional({ nullable: true }).isString().trim(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -233,7 +249,7 @@ export default class SaleInvoicesController extends BaseController {
/** /**
* Due sale invoice list validation schema. * Due sale invoice list validation schema.
*/ */
private get dueSalesInvoicesListValidationSchema() { get dueSalesInvoicesListValidationSchema() {
return [query('customer_id').optional().isNumeric().toInt()]; return [query('customer_id').optional().isNumeric().toInt()];
} }
@@ -243,22 +259,17 @@ export default class SaleInvoicesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
private async newSaleInvoice( async newSaleInvoice(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req; const { tenantId, user } = req;
const saleInvoiceDTO: ISaleInvoiceCreateDTO = this.matchedBodyData(req); const saleInvoiceDTO: ISaleInvoiceCreateDTO = this.matchedBodyData(req);
try { try {
// Creates a new sale invoice with associated entries. // Creates a new sale invoice with associated entries.
const storedSaleInvoice = const storedSaleInvoice = await this.saleInvoiceService.createSaleInvoice(
await this.saleInvoiceApplication.createSaleInvoice( tenantId,
tenantId, saleInvoiceDTO,
saleInvoiceDTO, user
user );
);
return res.status(200).send({ return res.status(200).send({
id: storedSaleInvoice.id, id: storedSaleInvoice.id,
message: 'The sale invoice has been created successfully.', message: 'The sale invoice has been created successfully.',
@@ -274,18 +285,14 @@ export default class SaleInvoicesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
private async editSaleInvoice( async editSaleInvoice(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req; const { tenantId, user } = req;
const { id: saleInvoiceId } = req.params; const { id: saleInvoiceId } = req.params;
const saleInvoiceOTD: ISaleInvoiceDTO = this.matchedBodyData(req); const saleInvoiceOTD: ISaleInvoiceDTO = this.matchedBodyData(req);
try { try {
// Update the given sale invoice details. // Update the given sale invoice details.
await this.saleInvoiceApplication.editSaleInvoice( await this.saleInvoiceService.editSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
saleInvoiceOTD, saleInvoiceOTD,
@@ -306,16 +313,12 @@ export default class SaleInvoicesController extends BaseController {
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
private async deliverSaleInvoice( async deliverSaleInvoice(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req; const { tenantId, user } = req;
const { id: saleInvoiceId } = req.params; const { id: saleInvoiceId } = req.params;
try { try {
await this.saleInvoiceApplication.deliverSaleInvoice( await this.saleInvoiceService.deliverSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
user user
@@ -335,17 +338,13 @@ export default class SaleInvoicesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
private async deleteSaleInvoice( async deleteSaleInvoice(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { id: saleInvoiceId } = req.params; const { id: saleInvoiceId } = req.params;
const { tenantId, user } = req; const { tenantId, user } = req;
try { try {
// Deletes the sale invoice with associated entries and journal transaction. // Deletes the sale invoice with associated entries and journal transaction.
await this.saleInvoiceApplication.deleteSaleInvoice( await this.saleInvoiceService.deleteSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
user user
@@ -365,16 +364,12 @@ export default class SaleInvoicesController extends BaseController {
* @param {Request} req - Request object. * @param {Request} req - Request object.
* @param {Response} res - Response object. * @param {Response} res - Response object.
*/ */
private async getSaleInvoice( async getSaleInvoice(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { id: saleInvoiceId } = req.params; const { id: saleInvoiceId } = req.params;
const { tenantId, user } = req; const { tenantId, user } = req;
try { try {
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice( const saleInvoice = await this.saleInvoiceService.getSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
user user
@@ -389,7 +384,7 @@ export default class SaleInvoicesController extends BaseController {
}, },
// PDF content type. // PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => { [ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf( const pdfContent = await this.saleInvoicePdf.saleInvoicePdf(
tenantId, tenantId,
saleInvoice saleInvoice
); );
@@ -425,7 +420,7 @@ export default class SaleInvoicesController extends BaseController {
}; };
try { try {
const { salesInvoices, filterMeta, pagination } = const { salesInvoices, filterMeta, pagination } =
await this.saleInvoiceApplication.getSaleInvoices(tenantId, filter); await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices), sales_invoices: this.transfromToResponse(salesInvoices),
@@ -453,11 +448,10 @@ export default class SaleInvoicesController extends BaseController {
const { customerId } = this.matchedQueryData(req); const { customerId } = this.matchedQueryData(req);
try { try {
const salesInvoices = const salesInvoices = await this.saleInvoiceService.getPayableInvoices(
await this.saleInvoiceApplication.getReceivableSaleInvoices( tenantId,
tenantId, customerId
customerId );
);
return res.status(200).send({ return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices), sales_invoices: this.transfromToResponse(salesInvoices),
}); });
@@ -483,7 +477,7 @@ export default class SaleInvoicesController extends BaseController {
const writeoffDTO = this.matchedBodyData(req); const writeoffDTO = this.matchedBodyData(req);
try { try {
const saleInvoice = await this.saleInvoiceApplication.writeOff( const saleInvoice = await this.saleInvoiceWriteoff.writeOff(
tenantId, tenantId,
invoiceId, invoiceId,
writeoffDTO writeoffDTO
@@ -491,7 +485,7 @@ export default class SaleInvoicesController extends BaseController {
return res.status(200).send({ return res.status(200).send({
id: saleInvoice.id, id: saleInvoice.id,
message: 'The given sale invoice has been written-off successfully.', message: 'The given sale invoice has been writte-off successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -513,7 +507,7 @@ export default class SaleInvoicesController extends BaseController {
const { id: invoiceId } = req.params; const { id: invoiceId } = req.params;
try { try {
const saleInvoice = await this.saleInvoiceApplication.cancelWrittenoff( const saleInvoice = await this.saleInvoiceWriteoff.cancelWrittenoff(
tenantId, tenantId,
invoiceId invoiceId
); );
@@ -544,12 +538,11 @@ export default class SaleInvoicesController extends BaseController {
const invoiceNotifySmsDTO = this.matchedBodyData(req); const invoiceNotifySmsDTO = this.matchedBodyData(req);
try { try {
const saleInvoice = const saleInvoice = await this.saleInvoiceSmsNotify.notifyBySms(
await this.saleInvoiceApplication.notifySaleInvoiceBySms( tenantId,
tenantId, invoiceId,
invoiceId, invoiceNotifySmsDTO.notificationKey
invoiceNotifySmsDTO.notificationKey );
);
return res.status(200).send({ return res.status(200).send({
id: saleInvoice.id, id: saleInvoice.id,
message: message:
@@ -576,12 +569,11 @@ export default class SaleInvoicesController extends BaseController {
const smsDetailsDTO = this.matchedQueryData(req); const smsDetailsDTO = this.matchedQueryData(req);
try { try {
const invoiceSmsDetails = const invoiceSmsDetails = await this.saleInvoiceSmsNotify.smsDetails(
await this.saleInvoiceApplication.getSaleInvoiceSmsDetails( tenantId,
tenantId, invoiceId,
invoiceId, smsDetailsDTO
smsDetailsDTO );
);
return res.status(200).send({ return res.status(200).send({
data: invoiceSmsDetails, data: invoiceSmsDetails,
}); });
@@ -607,7 +599,7 @@ export default class SaleInvoicesController extends BaseController {
try { try {
const invoicePayments = const invoicePayments =
await this.saleInvoiceApplication.getInvoicePayments( await this.invoicePaymentsSerivce.getInvoicePayments(
tenantId, tenantId,
invoiceId invoiceId
); );

View File

@@ -2,26 +2,34 @@ import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator'; import { check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import SaleReceiptService from '@/services/Sales/SalesReceipts';
import SaleReceiptsPdfService from '@/services/Sales/Receipts/SaleReceiptsPdfService';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt'; import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import SaleReceiptNotifyBySms from '@/services/Sales/SaleReceiptNotifyBySms';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, SaleReceiptAction } from '@/interfaces'; import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
@Service() @Service()
export default class SalesReceiptsController extends BaseController { export default class SalesReceiptsController extends BaseController {
@Inject() @Inject()
private saleReceiptsApplication: SaleReceiptApplication; saleReceiptService: SaleReceiptService;
@Inject() @Inject()
private dynamicListService: DynamicListingService; saleReceiptsPdf: SaleReceiptsPdfService;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
saleReceiptSmsNotify: SaleReceiptNotifyBySms;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.post( router.post(
@@ -97,7 +105,7 @@ export default class SalesReceiptsController extends BaseController {
* Sales receipt validation schema. * Sales receipt validation schema.
* @return {Array} * @return {Array}
*/ */
private get salesReceiptsValidationSchema() { get salesReceiptsValidationSchema() {
return [ return [
check('customer_id').exists().isNumeric().toInt(), check('customer_id').exists().isNumeric().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
@@ -138,14 +146,14 @@ export default class SalesReceiptsController extends BaseController {
/** /**
* Specific sale receipt validation schema. * Specific sale receipt validation schema.
*/ */
private get specificReceiptValidationSchema() { get specificReceiptValidationSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* List sales receipts validation schema. * List sales receipts validation schema.
*/ */
private get listSalesReceiptsValidationSchema() { get listSalesReceiptsValidationSchema() {
return [ return [
query('view_slug').optional().isString().trim(), query('view_slug').optional().isString().trim(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -162,21 +170,16 @@ export default class SalesReceiptsController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async newSaleReceipt( async newSaleReceipt(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const saleReceiptDTO: ISaleReceiptDTO = this.matchedBodyData(req); const saleReceiptDTO: ISaleReceiptDTO = this.matchedBodyData(req);
try { try {
// Store the given sale receipt details with associated entries. // Store the given sale receipt details with associated entries.
const storedSaleReceipt = const storedSaleReceipt = await this.saleReceiptService.createSaleReceipt(
await this.saleReceiptsApplication.createSaleReceipt( tenantId,
tenantId, saleReceiptDTO
saleReceiptDTO );
);
return res.status(200).send({ return res.status(200).send({
id: storedSaleReceipt.id, id: storedSaleReceipt.id,
message: 'Sale receipt has been created successfully.', message: 'Sale receipt has been created successfully.',
@@ -191,20 +194,13 @@ export default class SalesReceiptsController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async deleteSaleReceipt( async deleteSaleReceipt(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const { id: saleReceiptId } = req.params; const { id: saleReceiptId } = req.params;
try { try {
// Deletes the sale receipt. // Deletes the sale receipt.
await this.saleReceiptsApplication.deleteSaleReceipt( await this.saleReceiptService.deleteSaleReceipt(tenantId, saleReceiptId);
tenantId,
saleReceiptId
);
return res.status(200).send({ return res.status(200).send({
id: saleReceiptId, id: saleReceiptId,
@@ -221,18 +217,14 @@ export default class SalesReceiptsController extends BaseController {
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
private async editSaleReceipt( async editSaleReceipt(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const { id: saleReceiptId } = req.params; const { id: saleReceiptId } = req.params;
const saleReceipt = this.matchedBodyData(req); const saleReceipt = this.matchedBodyData(req);
try { try {
// Update the given sale receipt details. // Update the given sale receipt details.
await this.saleReceiptsApplication.editSaleReceipt( await this.saleReceiptService.editSaleReceipt(
tenantId, tenantId,
saleReceiptId, saleReceiptId,
saleReceipt saleReceipt
@@ -252,20 +244,13 @@ export default class SalesReceiptsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async closeSaleReceipt( async closeSaleReceipt(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const { id: saleReceiptId } = req.params; const { id: saleReceiptId } = req.params;
try { try {
// Update the given sale receipt details. // Update the given sale receipt details.
await this.saleReceiptsApplication.closeSaleReceipt( await this.saleReceiptService.closeSaleReceipt(tenantId, saleReceiptId);
tenantId,
saleReceiptId
);
return res.status(200).send({ return res.status(200).send({
id: saleReceiptId, id: saleReceiptId,
message: 'Sale receipt has been closed successfully.', message: 'Sale receipt has been closed successfully.',
@@ -280,11 +265,7 @@ export default class SalesReceiptsController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async getSalesReceipts( async getSalesReceipts(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
sortOrder: 'desc', sortOrder: 'desc',
@@ -293,9 +274,10 @@ export default class SalesReceiptsController extends BaseController {
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
try { try {
const { data, pagination, filterMeta } = const { data, pagination, filterMeta } =
await this.saleReceiptsApplication.getSaleReceipts(tenantId, filter); await this.saleReceiptService.salesReceiptsList(tenantId, filter);
const response = this.transfromToResponse({ const response = this.transfromToResponse({
data, data,
@@ -319,10 +301,11 @@ export default class SalesReceiptsController extends BaseController {
const { tenantId } = req; const { tenantId } = req;
try { try {
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt( const saleReceipt = await this.saleReceiptService.getSaleReceipt(
tenantId, tenantId,
saleReceiptId saleReceiptId
); );
res.format({ res.format({
'application/json': () => { 'application/json': () => {
return res return res
@@ -330,11 +313,10 @@ export default class SalesReceiptsController extends BaseController {
.send(this.transfromToResponse({ saleReceipt })); .send(this.transfromToResponse({ saleReceipt }));
}, },
'application/pdf': async () => { 'application/pdf': async () => {
const pdfContent = const pdfContent = await this.saleReceiptsPdf.saleReceiptPdf(
await this.saleReceiptsApplication.getSaleReceiptPdf( tenantId,
tenantId, saleReceipt
saleReceipt );
);
res.set({ res.set({
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length, 'Content-Length': pdfContent.length,
@@ -362,11 +344,10 @@ export default class SalesReceiptsController extends BaseController {
const { id: receiptId } = req.params; const { id: receiptId } = req.params;
try { try {
const saleReceipt = const saleReceipt = await this.saleReceiptSmsNotify.notifyBySms(
await this.saleReceiptsApplication.saleReceiptNotifyBySms( tenantId,
tenantId, receiptId
receiptId );
);
return res.status(200).send({ return res.status(200).send({
id: saleReceipt.id, id: saleReceipt.id,
message: message:
@@ -392,11 +373,10 @@ export default class SalesReceiptsController extends BaseController {
const { id: receiptId } = req.params; const { id: receiptId } = req.params;
try { try {
const smsDetails = const smsDetails = await this.saleReceiptSmsNotify.smsDetails(
await this.saleReceiptsApplication.getSaleReceiptSmsDetails( tenantId,
tenantId, receiptId
receiptId );
);
return res.status(200).send({ return res.status(200).send({
data: smsDetails, data: smsDetails,
}); });

View File

@@ -1,10 +1,10 @@
import { Router } from 'express'; import { Router } from 'express';
import { Container, Service } from 'typedi'; import { Container, Service } from 'typedi';
import SalesInvoices from './SalesInvoices'
import SalesEstimates from './SalesEstimates'; import SalesEstimates from './SalesEstimates';
import SalesReceipts from './SalesReceipts'; import SalesReceipts from './SalesReceipts';
import CreditNotes from './CreditNotes'; import SalesInvoices from './SalesInvoices'
import PaymentReceives from './PaymentReceives'; import PaymentReceives from './PaymentReceives';
import CreditNotes from './CreditNotes';
@Service() @Service()
export default class SalesController { export default class SalesController {
/** /**

View File

@@ -4,7 +4,6 @@ import moment from 'moment';
global.__root_dir = path.join(__dirname, '..'); global.__root_dir = path.join(__dirname, '..');
global.__resources_dir = path.join(global.__root_dir, 'resources'); global.__resources_dir = path.join(global.__root_dir, 'resources');
global.__locales_dir = path.join(global.__resources_dir, 'locales'); global.__locales_dir = path.join(global.__resources_dir, 'locales');
global.__views_dir = path.join(global.__root_dir, 'views');
moment.prototype.toMySqlDateTime = function () { moment.prototype.toMySqlDateTime = function () {
return this.format('YYYY-MM-DD HH:mm:ss'); return this.format('YYYY-MM-DD HH:mm:ss');

View File

@@ -1,12 +1,8 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { toInteger } from 'lodash';
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
dotenv.config(); dotenv.config();
const API_RATE_LIMIT = process.env.API_RATE_LIMIT?.split(',') || [];
module.exports = { module.exports = {
/** /**
* Your favorite port * Your favorite port
@@ -98,15 +94,16 @@ module.exports = {
* JWT secret. * JWT secret.
*/ */
jwtSecret: process.env.JWT_SECRET, jwtSecret: process.env.JWT_SECRET,
resetPasswordSeconds: 600,
/** /**
* *
*/ */
resetPasswordSeconds: 600, customerSuccess: {
email: 'success@bigcapital.ly',
phoneNumber: '(218) 92 791 8381',
},
/**
* Application base URL.
*/
baseURL: process.env.BASE_URL, baseURL: process.env.BASE_URL,
/** /**
@@ -133,23 +130,20 @@ module.exports = {
blockDuration: 60 * 15, blockDuration: 60 * 15,
}, },
requests: { requests: {
points: API_RATE_LIMIT[0] ? toInteger(API_RATE_LIMIT[0]) : 120, points: 60,
duration: API_RATE_LIMIT[1] ? toInteger(API_RATE_LIMIT[1]) : 60, duration: 60,
blockDuration: API_RATE_LIMIT[2] ? toInteger(API_RATE_LIMIT[2]) : 60 * 10, blockDuration: 60 * 10,
}, },
}, },
/** /**
* Sign-up restrictions * Users registeration configuration.
*/ */
signupRestrictions: { registration: {
disabled: parseBoolean<boolean>(process.env.SIGNUP_DISABLED, false), countries: {
allowedDomains: castCommaListEnvVarToArray( whitelist: ['LY'],
process.env.SIGNUP_ALLOWED_DOMAINS blacklist: [],
), },
allowedEmails: castCommaListEnvVarToArray(
process.env.SIGNUP_ALLOWED_EMAILS
),
}, },
/** /**
@@ -159,6 +153,8 @@ module.exports = {
browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT, browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT,
}, },
protocol: '',
hostname: '',
scheduleComputeItemCost: 'in 5 seconds', scheduleComputeItemCost: 'in 5 seconds',
/** /**

View File

@@ -6,14 +6,14 @@ export default class SeedAccounts extends TenantSeeder {
* Seeds initial accounts to the organization. * Seeds initial accounts to the organization.
*/ */
up(knex) { up(knex) {
const data = AccountsData.map((account) => ({ const data = AccountsData.map((account) => {
...account, return {
name: this.i18n.__(account.name), ...account,
description: this.i18n.__(account.description), name: this.i18n.__(account.name),
currencyCode: this.tenant.metadata.baseCurrency, description: this.i18n.__(account.description),
seededAt: new Date(), currencyCode: this.tenant.metadata.baseCurrency,
}) };
); });
return knex('accounts').then(async () => { return knex('accounts').then(async () => {
// Inserts seed entries. // Inserts seed entries.
return knex('accounts').insert(data); return knex('accounts').insert(data);

View File

@@ -8,7 +8,7 @@ export default class SeedSettings extends TenantSeeder {
up() { up() {
const settings = [ const settings = [
// Orgnization settings. // Orgnization settings.
{ group: 'organization', key: 'accounting_basis', value: 'accrual' }, { group: 'organization', key: 'accounting_basis', value: 'accural' },
// Accounts settings. // Accounts settings.
{ group: 'accounts', key: 'account_code_unique', value: true }, { group: 'accounts', key: 'account_code_unique', value: true },

View File

@@ -79,15 +79,9 @@ export interface IAccountTransaction {
} }
export interface IAccountResponse extends IAccount {} export interface IAccountResponse extends IAccount {}
export enum IAccountsStructureType {
Tree = 'tree',
Flat = 'flat',
}
export interface IAccountsFilter extends IDynamicListFilterDTO { export interface IAccountsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string; stringifiedFilterRoles?: string;
onlyInactive: boolean; onlyInactive: boolean;
structure?: IAccountsStructureType;
} }
export interface IAccountType { export interface IAccountType {

View File

@@ -75,7 +75,3 @@ export interface IAuthSendedResetPassword {
user: ISystemUser, user: ISystemUser,
token: string; token: string;
} }
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
}

View File

@@ -44,7 +44,7 @@ export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery {
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
noneTransactions: boolean; noneTransactions: boolean;
noneZero: boolean; noneZero: boolean;
basis: 'cash' | 'accrual'; basis: 'cash' | 'accural';
accountIds: number[]; accountIds: number[];
percentageOfColumn: boolean; percentageOfColumn: boolean;

View File

@@ -4,8 +4,6 @@ export interface ILedger {
getEntries(): ILedgerEntry[]; getEntries(): ILedgerEntry[];
filter(cb: (entry: ILedgerEntry) => boolean): ILedger;
whereAccountId(accountId: number): ILedger; whereAccountId(accountId: number): ILedger;
whereContactId(contactId: number): ILedger; whereContactId(contactId: number): ILedger;
whereFromDate(fromDate: Date | string): ILedger; whereFromDate(fromDate: Date | string): ILedger;
@@ -41,8 +39,6 @@ export interface ILedgerEntry {
index: number; index: number;
indexGroup?: number; indexGroup?: number;
note?: string;
userId?: number; userId?: number;
itemId?: number; itemId?: number;
branchId?: number; branchId?: number;

View File

@@ -4,7 +4,7 @@ export interface ITrialBalanceSheetQuery {
fromDate: Date | string; fromDate: Date | string;
toDate: Date | string; toDate: Date | string;
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
basis: 'cash' | 'accrual'; basis: 'cash' | 'accural';
noneZero: boolean; noneZero: boolean;
noneTransactions: boolean; noneTransactions: boolean;
onlyActive: boolean; onlyActive: boolean;

View File

@@ -1,7 +1,6 @@
import { AnyObject } from '@casl/ability/dist/types/types'; import { AnyObject } from '@casl/ability/dist/types/types';
import { ITenant } from '@/interfaces'; import { ITenant } from '@/interfaces';
import { Model } from 'objection'; import { Model } from 'objection';
import { Tenant } from '@/system/models';
export interface ISystemUser extends Model { export interface ISystemUser extends Model {
id: number; id: number;
@@ -55,52 +54,20 @@ export interface IUserInvite {
export interface IInviteUserService { export interface IInviteUserService {
acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise<void>; acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise<void>;
/**
* Re-send user invite.
* @param {number} tenantId -
* @param {string} email -
* @return {Promise<{ invite: IUserInvite }>}
*/
resendInvite( resendInvite(
tenantId: number, tenantId: number,
userId: number, userId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ ): Promise<{
user: ITenantUser; invite: IUserInvite;
}>; }>;
/**
* Sends invite mail to the given email from the given tenant and user.
* @param {number} tenantId -
* @param {string} email -
* @param {IUser} authorizedUser -
* @return {Promise<IUserInvite>}
*/
sendInvite( sendInvite(
tenantId: number, tenantId: number,
sendInviteDTO: IUserSendInviteDTO, email: string,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ ): Promise<{
invitedUser: ITenantUser; invite: IUserInvite;
}>; }>;
}
export interface IAcceptInviteUserService {
/**
* Accept the received invite.
* @param {string} token
* @param {IInviteUserInput} inviteUserInput
* @throws {ServiceErrors}
* @returns {Promise<void>}
*/
acceptInvite(token: string, inviteUserDTO: IInviteUserInput): Promise<void>;
/**
* Validate the given invite token.
* @param {string} token - the given token string.
* @throws {ServiceError}
*/
checkInvite( checkInvite(
token: string token: string
): Promise<{ inviteToken: IUserInvite; orgName: object }>; ): Promise<{ inviteToken: IUserInvite; orgName: object }>;
@@ -154,7 +121,7 @@ export interface IUserInvitedEventPayload {
tenantId: number; tenantId: number;
user: ITenantUser; user: ITenantUser;
} }
export interface IUserInviteTenantSyncedEventPayload { export interface IUserInviteTenantSyncedEventPayload{
invite: IUserInvite; invite: IUserInvite;
authorizedUser: ISystemUser; authorizedUser: ISystemUser;
tenantId: number; tenantId: number;
@@ -176,7 +143,7 @@ export interface IAcceptInviteEventPayload {
export interface ICheckInviteEventPayload { export interface ICheckInviteEventPayload {
inviteToken: IUserInvite; inviteToken: IUserInvite;
tenant: Tenant; tenant: ITenant
} }
export interface IUserSendInviteDTO { export interface IUserSendInviteDTO {

View File

@@ -1,7 +1,7 @@
import { Container } from 'typedi'; import { Container, Inject } from 'typedi';
import AuthenticationMailMesssages from '@/services/Authentication/AuthenticationMailMessages'; import AuthenticationService from '@/services/Authentication/AuthApplication';
export default class ResetPasswordEmailJob { export default class WelcomeEmailJob {
/** /**
* Constructor method. * Constructor method.
* @param {Agenda} agenda * @param {Agenda} agenda
@@ -10,7 +10,7 @@ export default class ResetPasswordEmailJob {
agenda.define( agenda.define(
'reset-password-mail', 'reset-password-mail',
{ priority: 'high' }, { priority: 'high' },
this.handler.bind(this) this.handler.bind(this),
); );
} }
@@ -22,13 +22,17 @@ export default class ResetPasswordEmailJob {
public async handler(job, done: Function): Promise<void> { public async handler(job, done: Function): Promise<void> {
const { data } = job.attrs; const { data } = job.attrs;
const { user, token } = data; const { user, token } = data;
const authService = Container.get(AuthenticationMailMesssages); const Logger = Container.get('logger');
const authService = Container.get(AuthenticationService);
Logger.info(`[send_reset_password] started.`, { data });
try { try {
await authService.sendResetPasswordMessage(user, token); await authService.mailMessages.sendResetPasswordMessage(user, token);
done(); Logger.info(`[send_reset_password] finished.`, { data });
done()
} catch (error) { } catch (error) {
console.log(error); Logger.error(`[send_reset_password] error.`, { data, error });
done(error); done(error);
} }
} }

View File

@@ -0,0 +1,22 @@
import { Container } from 'typedi';
export default class SmsNotification {
constructor(agenda) {
agenda.define('sms-notification', { priority: 'high' }, this.handler);
}
/**
*
* @param {Job}job
*/
async handler(job) {
const { message, to } = job.attrs.data;
const smsClient = Container.get('SMSClient');
try {
await smsClient.sendMessage(to, message);
} catch (error) {
done(e);
}
}
}

View File

@@ -1,6 +1,5 @@
import { Container, Inject } from 'typedi'; import { Container, Inject } from 'typedi';
import InviteUserService from '@/services/InviteUsers/AcceptInviteUser'; import InviteUserService from '@/services/InviteUsers/AcceptInviteUser';
import SendInviteUsersMailMessage from '@/services/InviteUsers/SendInviteUsersMailMessage';
export default class UserInviteMailJob { export default class UserInviteMailJob {
/** /**
@@ -22,17 +21,24 @@ export default class UserInviteMailJob {
*/ */
public async handler(job, done: Function): Promise<void> { public async handler(job, done: Function): Promise<void> {
const { invite, authorizedUser, tenantId } = job.attrs.data; const { invite, authorizedUser, tenantId } = job.attrs.data;
const sendInviteMailMessage = Container.get(SendInviteUsersMailMessage);
const Logger = Container.get('logger');
const inviteUsersService = Container.get(InviteUserService);
Logger.info(`Send invite user mail - started: ${job.attrs.data}`);
try { try {
await sendInviteMailMessage.sendInviteMail( await inviteUsersService.mailMessages.sendInviteMail(
tenantId, tenantId,
authorizedUser, authorizedUser,
invite invite
); );
Logger.info(`Send invite user mail - finished: ${job.attrs.data}`);
done(); done();
} catch (error) { } catch (error) {
console.log(error); Logger.info(
`Send invite user mail - error: ${job.attrs.data}, error: ${error}`
);
done(error); done(error);
} }
} }

View File

@@ -0,0 +1,35 @@
import { Container, Inject } from 'typedi';
import AuthenticationService from '@/services/Authentication/AuthApplication';
export default class WelcomeSMSJob {
/**
* Constructor method.
* @param {Agenda} agenda
*/
constructor(agenda) {
agenda.define('welcome-sms', { priority: 'high' }, this.handler);
}
/**
* Handle send welcome mail job.
* @param {Job} job
* @param {Function} done
*/
public async handler(job, done: Function): Promise<void> {
const { tenant, user } = job.attrs.data;
const Logger = Container.get('logger');
const authService = Container.get(AuthenticationService);
Logger.info(`[welcome_sms] started: ${job.attrs.data}`);
try {
await authService.smsMessages.sendWelcomeMessage(tenant, user);
Logger.info(`[welcome_sms] finished`, { tenant, user });
done();
} catch (error) {
Logger.info(`[welcome_sms] error`, { error, tenant, user });
done(error);
}
}
}

View File

@@ -0,0 +1,39 @@
import { Container } from 'typedi';
import AuthenticationService from '@/services/Authentication/AuthApplication';
export default class WelcomeEmailJob {
/**
* Constructor method.
* @param {Agenda} agenda -
*/
constructor(agenda) {
// Welcome mail and SMS message.
agenda.define(
'welcome-email',
{ priority: 'high' },
this.handler.bind(this),
);
}
/**
* Handle send welcome mail job.
* @param {Job} job
* @param {Function} done
*/
public async handler(job, done: Function): Promise<void> {
const { organizationId, user } = job.attrs.data;
const Logger: any = Container.get('logger');
const authService = Container.get(AuthenticationService);
Logger.info(`[welcome_mail] started: ${job.attrs.data}`);
try {
await authService.mailMessages.sendWelcomeMessage(user, organizationId);
Logger.info(`[welcome_mail] finished: ${job.attrs.data}`);
done();
} catch (error) {
Logger.error(`[welcome_mail] error: ${job.attrs.data}, error: ${error}`);
done(error);
}
}
}

View File

@@ -1,7 +1,7 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import SalesInvoicesCost from '@/services/Sales/SalesInvoicesCost';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost';
export default class WriteInvoicesJournalEntries { export default class WriteInvoicesJournalEntries {
eventPublisher: EventPublisher; eventPublisher: EventPublisher;
@@ -26,7 +26,7 @@ export default class WriteInvoicesJournalEntries {
*/ */
public async handler(job, done: Function): Promise<void> { public async handler(job, done: Function): Promise<void> {
const { startingDate, tenantId } = job.attrs.data; const { startingDate, tenantId } = job.attrs.data;
const salesInvoicesCost = Container.get(SaleInvoicesCost); const salesInvoicesCost = Container.get(SalesInvoicesCost);
try { try {
await salesInvoicesCost.writeCostLotsGLEntries(tenantId, startingDate); await salesInvoicesCost.writeCostLotsGLEntries(tenantId, startingDate);

View File

@@ -109,7 +109,7 @@ export default class Mail {
* Retrieve view content from the view directory. * Retrieve view content from the view directory.
*/ */
private getViewContent(): string { private getViewContent(): string {
const filePath = path.join(global.__views_dir, `/${this.view}`); const filePath = path.join(global.__root_dir, `../views/${this.view}`);
return fs.readFileSync(filePath, 'utf8'); return fs.readFileSync(filePath, 'utf8');
} }
} }

View File

@@ -2,7 +2,6 @@ import moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash'; import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
import { formatNumber } from 'utils'; import { formatNumber } from 'utils';
import { isArrayLikeObject } from 'lodash/fp';
export class Transformer { export class Transformer {
public context: any; public context: any;
@@ -40,33 +39,12 @@ export class Transformer {
return object; return object;
}; };
/**
*
* @param object
* @returns
*/
protected preCollectionTransform = (object: any) => {
return object;
};
/**
*
* @param object
* @returns
*/
protected postCollectionTransform = (object: any) => {
return object;
};
/** /**
* *
*/ */
public work = (object: any) => { public work = (object: any) => {
if (Array.isArray(object)) { if (Array.isArray(object)) {
const preTransformed = this.preCollectionTransform(object); return object.map(this.getTransformation);
const transformed = preTransformed.map(this.getTransformation);
return this.postCollectionTransform(transformed);
} else if (isObject(object)) { } else if (isObject(object)) {
return this.getTransformation(object); return this.getTransformation(object);
} }

View File

@@ -7,11 +7,11 @@ import PaymentSyncBillBalance from '@/subscribers/PaymentMades/PaymentSyncBillBa
import SaleReceiptInventoryTransactionsSubscriber from '@/subscribers/SaleReceipt/WriteInventoryTransactions'; import SaleReceiptInventoryTransactionsSubscriber from '@/subscribers/SaleReceipt/WriteInventoryTransactions';
import SaleInvoiceWriteInventoryTransactions from '@/subscribers/SaleInvoices/WriteInventoryTransactions'; import SaleInvoiceWriteInventoryTransactions from '@/subscribers/SaleInvoices/WriteInventoryTransactions';
import SaleInvoiceWriteGLEntriesSubscriber from '@/subscribers/SaleInvoices/WriteJournalEntries'; import SaleInvoiceWriteGLEntriesSubscriber from '@/subscribers/SaleInvoices/WriteJournalEntries';
import SaleReceiptWriteGLEntriesSubscriber from '@/subscribers/SaleReceipt/WriteJournalEntries';
import PaymentReceiveSyncInvoices from '@/subscribers/PaymentReceive/PaymentReceiveSyncInvoices'; import PaymentReceiveSyncInvoices from '@/subscribers/PaymentReceive/PaymentReceiveSyncInvoices';
import CashflowTransactionSubscriber from '@/services/Cashflow/CashflowTransactionSubscriber'; import CashflowTransactionSubscriber from '@/services/Cashflow/CashflowTransactionSubscriber';
import PaymentReceivesWriteGLEntriesSubscriber from '@/subscribers/PaymentReceive/WriteGLEntries'; import PaymentReceivesWriteGLEntriesSubscriber from '@/subscribers/PaymentReceive/WriteGLEntries';
import InventorySubscriber from '@/subscribers/Inventory/Inventory'; import InventorySubscriber from '@/subscribers/Inventory/Inventory';
import SaleReceiptWriteGLEntriesSubscriber from '@/subscribers/SaleReceipt/WriteJournalEntries';
import { CustomerWriteGLOpeningBalanceSubscriber } from '@/services/Contacts/Customers/Subscribers/CustomerGLEntriesSubscriber'; import { CustomerWriteGLOpeningBalanceSubscriber } from '@/services/Contacts/Customers/Subscribers/CustomerGLEntriesSubscriber';
import { VendorsWriteGLOpeningSubscriber } from '@/services/Contacts/Vendors/Subscribers/VendorGLEntriesSubscriber'; import { VendorsWriteGLOpeningSubscriber } from '@/services/Contacts/Vendors/Subscribers/VendorGLEntriesSubscriber';
import SaleEstimateAutoSerialSubscriber from '@/subscribers/SaleEstimate/AutoIncrementSerial'; import SaleEstimateAutoSerialSubscriber from '@/subscribers/SaleEstimate/AutoIncrementSerial';
@@ -22,7 +22,7 @@ import SaleInvoiceAutoIncrementSubscriber from '@/subscribers/SaleInvoices/AutoI
import SaleInvoiceConvertFromEstimateSubscriber from '@/subscribers/SaleInvoices/ConvertFromEstimate'; import SaleInvoiceConvertFromEstimateSubscriber from '@/subscribers/SaleInvoices/ConvertFromEstimate';
import PaymentReceiveAutoSerialSubscriber from '@/subscribers/PaymentReceive/AutoSerialIncrement'; import PaymentReceiveAutoSerialSubscriber from '@/subscribers/PaymentReceive/AutoSerialIncrement';
import SyncSystemSendInvite from '@/services/InviteUsers/SyncSystemSendInvite'; import SyncSystemSendInvite from '@/services/InviteUsers/SyncSystemSendInvite';
import InviteSendMainNotification from '@/services/InviteUsers/InviteSendMailNotificationSubscribe'; import InviteSendMainNotification from '@/services/InviteUsers/InviteSendMailNotification';
import SyncTenantAcceptInvite from '@/services/InviteUsers/SyncTenantAcceptInvite'; import SyncTenantAcceptInvite from '@/services/InviteUsers/SyncTenantAcceptInvite';
import SyncTenantUserMutate from '@/services/Users/SyncTenantUserSaved'; import SyncTenantUserMutate from '@/services/Users/SyncTenantUserSaved';
import { SyncTenantUserDelete } from '@/services/Users/SyncTenantUserDeleted'; import { SyncTenantUserDelete } from '@/services/Users/SyncTenantUserDeleted';
@@ -31,11 +31,12 @@ import OrgBuildSmsNotificationSubscriber from '@/subscribers/Organization/BuildS
import PurgeUserAbilityCache from '@/services/Users/PurgeUserAbilityCache'; import PurgeUserAbilityCache from '@/services/Users/PurgeUserAbilityCache';
import ResetLoginThrottleSubscriber from '@/subscribers/Authentication/ResetLoginThrottle'; import ResetLoginThrottleSubscriber from '@/subscribers/Authentication/ResetLoginThrottle';
import AuthenticationSubscriber from '@/subscribers/Authentication/SendResetPasswordMail'; import AuthenticationSubscriber from '@/subscribers/Authentication/SendResetPasswordMail';
import AuthSendWelcomeMailSubscriber from '@/subscribers/Authentication/SendWelcomeMail';
import PurgeAuthorizedUserOnceRoleMutate from '@/services/Roles/PurgeAuthorizedUser'; import PurgeAuthorizedUserOnceRoleMutate from '@/services/Roles/PurgeAuthorizedUser';
import SendSmsNotificationToCustomer from '@/subscribers/SaleInvoices/SendSmsNotificationToCustomer'; import SendSmsNotificationToCustomer from '@/subscribers/SaleInvoices/SendSmsNotificationToCustomer';
import SendSmsNotificationSaleReceipt from '@/subscribers/SaleReceipt/SendSmsNotificationToCustomer'; import SendSmsNotificationSaleReceipt from '@/subscribers/SaleReceipt/SendSmsNotificationToCustomer';
import SendSmsNotificationPaymentReceive from '@/subscribers/PaymentReceive/SendSmsNotificationToCustomer'; import SendSmsNotificationPaymentReceive from '@/subscribers/PaymentReceive/SendSmsNotificationToCustomer';
import SaleInvoiceWriteoffSubscriber from '@/services/Sales/Invoices/SaleInvoiceWriteoffSubscriber'; import SaleInvoiceWriteoffSubscriber from '@/services/Sales/SaleInvoiceWriteoffSubscriber';
import LandedCostSyncCostTransactionsSubscriber from '@/services/Purchases/LandedCost/LandedCostSyncCostTransactionsSubscriber'; import LandedCostSyncCostTransactionsSubscriber from '@/services/Purchases/LandedCost/LandedCostSyncCostTransactionsSubscriber';
import LandedCostInventoryTransactionsSubscriber from '@/services/Purchases/LandedCost/LandedCostInventoryTransactionsSubscriber'; import LandedCostInventoryTransactionsSubscriber from '@/services/Purchases/LandedCost/LandedCostInventoryTransactionsSubscriber';
import CreditNoteGLEntriesSubscriber from '@/services/CreditNotes/CreditNoteGLEntriesSubscriber'; import CreditNoteGLEntriesSubscriber from '@/services/CreditNotes/CreditNoteGLEntriesSubscriber';
@@ -66,6 +67,7 @@ import { ActivateWarehousesSubscriber } from '@/services/Warehouses/ActivateWare
import { ManualJournalWriteGLSubscriber } from '@/services/ManualJournals/ManualJournalGLEntriesSubscriber'; import { ManualJournalWriteGLSubscriber } from '@/services/ManualJournals/ManualJournalGLEntriesSubscriber';
import { BillGLEntriesSubscriber } from '@/services/Purchases/Bills/BillGLEntriesSubscriber'; import { BillGLEntriesSubscriber } from '@/services/Purchases/Bills/BillGLEntriesSubscriber';
import { PaymentWriteGLEntriesSubscriber } from '@/services/Purchases/BillPayments/BillPaymentGLEntriesSubscriber'; import { PaymentWriteGLEntriesSubscriber } from '@/services/Purchases/BillPayments/BillPaymentGLEntriesSubscriber';
import BranchesIntegrationsSubscribers from '@/services/Branches/EventsProvider'; import BranchesIntegrationsSubscribers from '@/services/Branches/EventsProvider';
import WarehousesIntegrationsSubscribers from '@/services/Warehouses/EventsProvider'; import WarehousesIntegrationsSubscribers from '@/services/Warehouses/EventsProvider';
import { WarehouseTransferAutoIncrementSubscriber } from '@/services/Warehouses/WarehousesTransfers/WarehouseTransferAutoIncrementSubscriber'; import { WarehouseTransferAutoIncrementSubscriber } from '@/services/Warehouses/WarehousesTransfers/WarehouseTransferAutoIncrementSubscriber';
@@ -118,6 +120,7 @@ export const susbcribers = () => {
PurgeUserAbilityCache, PurgeUserAbilityCache,
ResetLoginThrottleSubscriber, ResetLoginThrottleSubscriber,
AuthenticationSubscriber, AuthenticationSubscriber,
AuthSendWelcomeMailSubscriber,
PurgeAuthorizedUserOnceRoleMutate, PurgeAuthorizedUserOnceRoleMutate,
SendSmsNotificationToCustomer, SendSmsNotificationToCustomer,
SendSmsNotificationSaleReceipt, SendSmsNotificationSaleReceipt,

View File

@@ -1,18 +1,24 @@
import Agenda from 'agenda'; import Agenda from 'agenda';
import WelcomeEmailJob from 'jobs/welcomeEmail';
import WelcomeSMSJob from 'jobs/WelcomeSMS';
import ResetPasswordMailJob from 'jobs/ResetPasswordMail'; import ResetPasswordMailJob from 'jobs/ResetPasswordMail';
import ComputeItemCost from 'jobs/ComputeItemCost'; import ComputeItemCost from 'jobs/ComputeItemCost';
import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries'; import RewriteInvoicesJournalEntries from 'jobs/writeInvoicesJEntries';
import UserInviteMailJob from 'jobs/UserInviteMail'; import UserInviteMailJob from 'jobs/UserInviteMail';
import OrganizationSetupJob from 'jobs/OrganizationSetup'; import OrganizationSetupJob from 'jobs/OrganizationSetup';
import OrganizationUpgrade from 'jobs/OrganizationUpgrade'; import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
import SmsNotification from 'jobs/SmsNotification';
export default ({ agenda }: { agenda: Agenda }) => { export default ({ agenda }: { agenda: Agenda }) => {
new WelcomeEmailJob(agenda);
new ResetPasswordMailJob(agenda); new ResetPasswordMailJob(agenda);
new WelcomeSMSJob(agenda);
new UserInviteMailJob(agenda); new UserInviteMailJob(agenda);
new ComputeItemCost(agenda); new ComputeItemCost(agenda);
new RewriteInvoicesJournalEntries(agenda); new RewriteInvoicesJournalEntries(agenda);
new OrganizationSetupJob(agenda); new OrganizationSetupJob(agenda);
new OrganizationUpgrade(agenda); new OrganizationUpgrade(agenda);
new SmsNotification(agenda);
agenda.start(); agenda.start();
}; };

View File

@@ -5,7 +5,7 @@ import TenantModel from 'models/TenantModel';
import BillSettings from './Bill.Settings'; import BillSettings from './Bill.Settings';
import ModelSetting from './ModelSetting'; import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel'; import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants'; import { DEFAULT_VIEWS } from '@/services/Purchases/constants';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
export default class Bill extends mixin(TenantModel, [ export default class Bill extends mixin(TenantModel, [

View File

@@ -5,7 +5,7 @@ import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting'; import ModelSetting from './ModelSetting';
import SaleInvoiceMeta from './SaleInvoice.Settings'; import SaleInvoiceMeta from './SaleInvoice.Settings';
import CustomViewBaseModel from './CustomViewBaseModel'; import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Invoices/constants'; import { DEFAULT_VIEWS } from '@/services/Sales/constants';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
export default class SaleInvoice extends mixin(TenantModel, [ export default class SaleInvoice extends mixin(TenantModel, [

View File

@@ -1,14 +1,9 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import async from 'async'; import async from 'async';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import { ILedger, ISaleContactsBalanceQueuePayload } from '@/interfaces';
ILedger,
ILedgerEntry,
ISaleContactsBalanceQueuePayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models'; import { TenantMetadata } from '@/system/models';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
@Service() @Service()
export class LedgerContactsBalanceStorage { export class LedgerContactsBalanceStorage {
@@ -54,29 +49,6 @@ export class LedgerContactsBalanceStorage {
await this.saveContactBalance(tenantId, ledger, contactId, trx); await this.saveContactBalance(tenantId, ledger, contactId, trx);
}; };
/**
* Filters AP/AR ledger entries.
* @param {number} tenantId
* @param {Knex.Transaction} trx
* @returns {Promise<(entry: ILedgerEntry) => boolean>}
*/
private filterARAPLedgerEntris = async (
tenantId: number,
trx?: Knex.Transaction
): Promise<(entry: ILedgerEntry) => boolean> => {
const { Account } = this.tenancy.models(tenantId);
const ARAPAcounts = await Account.query(trx).whereIn('accountType', [
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE,
ACCOUNT_TYPE.ACCOUNTS_PAYABLE,
]);
const ARAPAcountsIds = ARAPAcounts.map((a) => a.id);
return (entry: ILedgerEntry) => {
return ARAPAcountsIds.indexOf(entry.accountId) !== -1;
};
};
/** /**
* *
* @param {number} tenantId * @param {number} tenantId
@@ -91,7 +63,7 @@ export class LedgerContactsBalanceStorage {
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<void> => { ): Promise<void> => {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
const contact = await Contact.query(trx).findById(contactId); const contact = await Contact.query().findById(contactId);
// Retrieves the given tenant metadata. // Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId }); const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
@@ -100,15 +72,7 @@ export class LedgerContactsBalanceStorage {
const isForeignContact = contact.currencyCode !== tenantMeta.baseCurrency; const isForeignContact = contact.currencyCode !== tenantMeta.baseCurrency;
// Filters the ledger base on the given contact id. // Filters the ledger base on the given contact id.
const filterARAPLedgerEntris = await this.filterARAPLedgerEntris( const contactLedger = ledger.whereContactId(contactId);
tenantId,
trx
);
const contactLedger = ledger
// Filter entries only that have contact id.
.whereContactId(contactId)
// Filter entries on AR/AP accounts.
.filter(filterARAPLedgerEntris);
const closingBalance = isForeignContact const closingBalance = isForeignContact
? contactLedger ? contactLedger

View File

@@ -10,7 +10,7 @@ export class LedgerRevert {
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject() @Inject()
private ledgerStorage: LedgerStorageService; ledgerStorage: LedgerStorageService;
/** /**
* Reverts the jouranl entries. * Reverts the jouranl entries.

View File

@@ -21,8 +21,6 @@ export const transformLedgerEntryToTransaction = (
transactionNumber: entry.transactionNumber, transactionNumber: entry.transactionNumber,
referenceNumber: entry.referenceNumber, referenceNumber: entry.referenceNumber,
note: entry.note,
index: entry.index, index: entry.index,
indexGroup: entry.indexGroup, indexGroup: entry.indexGroup,

View File

@@ -1,11 +1,6 @@
import { IAccount, IAccountsStructureType } from '@/interfaces'; import { IAccount } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer'; import { Transformer } from '@/lib/Transformer/Transformer';
import { import { formatNumber } from 'utils';
assocDepthLevelToObjectTree,
flatToNestedArray,
formatNumber,
nestedArrayToFlatten,
} from 'utils';
export class AccountTransformer extends Transformer { export class AccountTransformer extends Transformer {
/** /**
@@ -13,23 +8,7 @@ export class AccountTransformer extends Transformer {
* @returns {Array} * @returns {Array}
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return ['formattedAmount', 'flattenName']; return ['formattedAmount'];
};
/**
* Retrieves the flatten name with all dependants accounts names.
* @param {IAccount} account -
* @returns {string}
*/
public flattenName = (account: IAccount): string => {
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
account.id
);
const prefixAccounts = parentDependantsIds.map((dependId) => {
const node = this.options.accountsGraph.getNodeData(dependId);
return `${node.name}: `;
});
return `${prefixAccounts}${account.name}`;
}; };
/** /**
@@ -38,28 +17,8 @@ export class AccountTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected formattedAmount = (account: IAccount): string => { protected formattedAmount = (account: IAccount): string => {
return formatNumber(account.amount, { currencyCode: account.currencyCode }); return formatNumber(account.amount, {
}; currencyCode: account.currencyCode,
/**
* Transformes the accounts collection to flat or nested array.
* @param {IAccount[]}
* @returns {IAccount[]}
*/
protected postCollectionTransform = (accounts: IAccount[]) => {
// Transfom the flatten to accounts tree.
const transformed = flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
}); });
// Associate `accountLevel` attr to indicate object depth.
const transformed2 = assocDepthLevelToObjectTree(
transformed,
1,
'accountLevel'
);
return this.options.structure === IAccountsStructureType.Flat
? nestedArrayToFlatten(transformed2)
: transformed2;
}; };
} }

View File

@@ -22,19 +22,15 @@ export class GetAccount {
*/ */
public getAccount = async (tenantId: number, accountId: number) => { public getAccount = async (tenantId: number, accountId: number) => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Find the given account or throw not found error. // Find the given account or throw not found error.
const account = await Account.query().findById(accountId).throwIfNotFound(); const account = await Account.query().findById(accountId).throwIfNotFound();
const accountsGraph = await accountRepository.getDependencyGraph();
// Transformes the account model to POJO. // Transformes the account model to POJO.
const transformed = await this.transformer.transform( const transformed = await this.transformer.transform(
tenantId, tenantId,
account, account,
new AccountTransformer(), new AccountTransformer()
{ accountsGraph }
); );
return this.i18nService.i18nApply( return this.i18nService.i18nApply(
[['accountTypeLabel'], ['accountNormalFormatted']], [['accountTypeLabel'], ['accountNormalFormatted']],

View File

@@ -1,11 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import { import { IAccountsFilter, IAccountResponse, IFilterMeta } from '@/interfaces';
IAccountsFilter,
IAccountResponse,
IFilterMeta,
IAccountsStructureType,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { AccountTransformer } from './AccountTransform'; import { AccountTransformer } from './AccountTransform';
@@ -43,7 +38,6 @@ export class GetAccounts {
filterDTO: IAccountsFilter filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => { ): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Parses the stringified filter roles. // Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO); const filter = this.parseListFilterDTO(filterDTO);
@@ -59,16 +53,17 @@ export class GetAccounts {
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode); builder.modify('inactiveMode', filter.inactiveMode);
}); });
// Retrievs the formatted accounts collection.
const accountsGraph = await accountRepository.getDependencyGraph(); const preTransformedAccounts = await this.transformer.transform(
// Retrieves the transformed accounts collection.
const transformedAccounts = await this.transformer.transform(
tenantId, tenantId,
accounts, accounts,
new AccountTransformer(), new AccountTransformer()
{ accountsGraph, structure: filterDTO.structure }
); );
// Transform accounts to nested array.
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
return { return {
accounts: transformedAccounts, accounts: transformedAccounts,

View File

@@ -1,14 +1,8 @@
import { Service, Inject, Container } from 'typedi'; import { Service, Inject, Container } from 'typedi';
import { import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces';
IRegisterDTO,
ISystemUser,
IPasswordReset,
IAuthGetMetaPOJO,
} from '@/interfaces';
import { AuthSigninService } from './AuthSignin'; import { AuthSigninService } from './AuthSignin';
import { AuthSignupService } from './AuthSignup'; import { AuthSignupService } from './AuthSignup';
import { AuthSendResetPassword } from './AuthSendResetPassword'; import { AuthSendResetPassword } from './AuthSendResetPassword';
import { GetAuthMeta } from './GetAuthMeta';
@Service() @Service()
export default class AuthenticationApplication { export default class AuthenticationApplication {
@@ -21,9 +15,6 @@ export default class AuthenticationApplication {
@Inject() @Inject()
private authResetPasswordService: AuthSendResetPassword; private authResetPasswordService: AuthSendResetPassword;
@Inject()
private authGetMeta: GetAuthMeta;
/** /**
* Signin and generates JWT token. * Signin and generates JWT token.
* @throws {ServiceError} * @throws {ServiceError}
@@ -62,12 +53,4 @@ export default class AuthenticationApplication {
public async resetPassword(token: string, password: string): Promise<void> { public async resetPassword(token: string, password: string): Promise<void> {
return this.authResetPasswordService.resetPassword(token, password); return this.authResetPasswordService.resetPassword(token, password);
} }
/**
* Retrieves the authentication meta for SPA.
* @returns {Promise<IAuthGetMetaPOJO>}
*/
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return this.authGetMeta.getAuthMeta();
}
} }

View File

@@ -1,4 +1,4 @@
import { isEmpty, omit } from 'lodash'; import { omit } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
@@ -13,7 +13,6 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import TenantsManagerService from '../Tenancy/TenantsManager'; import TenantsManagerService from '../Tenancy/TenantsManager';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { hashPassword } from '@/utils'; import { hashPassword } from '@/utils';
import config from '@/config';
export class AuthSignupService { export class AuthSignupService {
@Inject() @Inject()
@@ -34,9 +33,6 @@ export class AuthSignupService {
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> { public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
// Validates the signup disable restrictions.
await this.validateSignupRestrictions(signupDTO.email);
// Validates the given email uniqiness. // Validates the given email uniqiness.
await this.validateEmailUniqiness(signupDTO.email); await this.validateEmailUniqiness(signupDTO.email);
@@ -78,34 +74,4 @@ export class AuthSignupService {
throw new ServiceError(ERRORS.EMAIL_EXISTS); throw new ServiceError(ERRORS.EMAIL_EXISTS);
} }
} }
/**
* Validate sign-up disable restrictions.
* @param {string} email
*/
private async validateSignupRestrictions(email: string) {
// Can't continue if the signup is not disabled.
if (!config.signupRestrictions.disabled) return;
// Validate the allowed email addresses and domains.
if (
!isEmpty(config.signupRestrictions.allowedEmails) ||
!isEmpty(config.signupRestrictions.allowedDomains)
) {
const emailDomain = email.split('@').pop();
const isAllowedEmail =
config.signupRestrictions.allowedEmails.indexOf(email) !== -1;
const isAllowedDomain = config.signupRestrictions.allowedDomains.some(
(domain) => emailDomain === domain
);
if (!isAllowedEmail && !isAllowedDomain) {
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED);
}
// Throw error if the signup is disabled with no exceptions.
} else {
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED);
}
}
} }

View File

@@ -5,24 +5,59 @@ import Mail from '@/lib/Mail';
@Service() @Service()
export default class AuthenticationMailMesssages { export default class AuthenticationMailMesssages {
/**
* Sends welcome message.
* @param {ISystemUser} user - The system user.
* @param {string} organizationName -
* @return {Promise<void>}
*/
async sendWelcomeMessage(
user: ISystemUser,
organizationId: string
): Promise<void> {
const root = __dirname + '/../../../views/images/bigcapital.png';
const mail = new Mail()
.setView('mail/Welcome.html')
.setSubject('Welcome to Bigcapital')
.setTo(user.email)
.setAttachments([
{
filename: 'bigcapital.png',
path: root,
cid: 'bigcapital_logo',
},
])
.setData({
firstName: user.firstName,
organizationId,
successPhoneNumber: config.customerSuccess.phoneNumber,
successEmail: config.customerSuccess.email,
});
await mail.send();
}
/** /**
* Sends reset password message. * Sends reset password message.
* @param {ISystemUser} user - The system user. * @param {ISystemUser} user - The system user.
* @param {string} token - Reset password token. * @param {string} token - Reset password token.
* @return {Promise<void>} * @return {Promise<void>}
*/ */
public async sendResetPasswordMessage( async sendResetPasswordMessage(
user: ISystemUser, user: ISystemUser,
token: string token: string
): Promise<void> { ): Promise<void> {
await new Mail() const root = __dirname + '/../../../views/images/bigcapital.png';
const mail = new Mail()
.setSubject('Bigcapital - Password Reset') .setSubject('Bigcapital - Password Reset')
.setView('mail/ResetPassword.html') .setView('mail/ResetPassword.html')
.setTo(user.email) .setTo(user.email)
.setAttachments([ .setAttachments([
{ {
filename: 'bigcapital.png', filename: 'bigcapital.png',
path: `${global.__views_dir}/images/bigcapital.png`, path: root,
cid: 'bigcapital_logo', cid: 'bigcapital_logo',
}, },
]) ])
@@ -30,7 +65,9 @@ export default class AuthenticationMailMesssages {
resetPasswordUrl: `${config.baseURL}/auth/reset_password/${token}`, resetPasswordUrl: `${config.baseURL}/auth/reset_password/${token}`,
first_name: user.firstName, first_name: user.firstName,
last_name: user.lastName, last_name: user.lastName,
}) contact_us_email: config.contactUsMail,
.send(); });
await mail.send();
} }
} }

View File

@@ -0,0 +1,19 @@
import { Service, Inject } from 'typedi';
import { ISystemUser, ITenant } from '@/interfaces';
@Service()
export default class AuthenticationSMSMessages {
@Inject('SMSClient')
smsClient: any;
/**
* Sends welcome sms message.
* @param {ITenant} tenant
* @param {ISystemUser} user
*/
sendWelcomeMessage(tenant: ITenant, user: ISystemUser) {
const message: string = `Hi ${user.firstName}, Welcome to Bigcapital, You've joined the new workspace, if you need any help please don't hesitate to contact us.`;
return this.smsClient.sendMessage(user.phoneNumber, message);
}
}

View File

@@ -1,16 +0,0 @@
import { Service } from 'typedi';
import { IAuthGetMetaPOJO } from '@/interfaces';
import config from '@/config';
@Service()
export class GetAuthMeta {
/**
* Retrieves the authentication meta for SPA.
* @returns {Promise<IAuthGetMetaPOJO>}
*/
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return {
signupDisabled: config.signupRestrictions.disabled,
};
}
}

View File

@@ -7,6 +7,4 @@ export const ERRORS = {
TOKEN_EXPIRED: 'TOKEN_EXPIRED', TOKEN_EXPIRED: 'TOKEN_EXPIRED',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS', PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
EMAIL_EXISTS: 'EMAIL_EXISTS', EMAIL_EXISTS: 'EMAIL_EXISTS',
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
}; };

View File

@@ -8,7 +8,6 @@ import { IBranchDeletedPayload, IBranchDeletePayload } from '@/interfaces';
import { CURDBranch } from './CRUDBranch'; import { CURDBranch } from './CRUDBranch';
import { BranchValidator } from './BranchValidate'; import { BranchValidator } from './BranchValidate';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
@Service() @Service()
export class DeleteBranch extends CURDBranch { export class DeleteBranch extends CURDBranch {
@Inject() @Inject()

View File

@@ -40,8 +40,10 @@ export class ValidateBranchExistance {
tenantId: number, tenantId: number,
branchId: number | null branchId: number | null
) => { ) => {
//
this.validateBranchIdExistance(branchId); this.validateBranchIdExistance(branchId);
//
await this.validateBranchExistance(tenantId, branchId); await this.validateBranchExistance(tenantId, branchId);
}; };
@@ -60,10 +62,7 @@ export class ValidateBranchExistance {
* @param tenantId * @param tenantId
* @param branchId * @param branchId
*/ */
public validateBranchExistance = async ( public validateBranchExistance = async (tenantId: number, branchId: number) => {
tenantId: number,
branchId: number
) => {
const { Branch } = this.tenancy.models(tenantId); const { Branch } = this.tenancy.models(tenantId);
const branch = await Branch.query().findById(branchId); const branch = await Branch.query().findById(branchId);

View File

@@ -25,8 +25,8 @@ export default class CashflowTransactionJournalEntries {
/** /**
* Retrieves the common entry of cashflow transaction. * Retrieves the common entry of cashflow transaction.
* @param {ICashflowTransaction} cashflowTransaction * @param {ICashflowTransaction} cashflowTransaction
* @returns {Partial<ILedgerEntry>} * @returns {}
*/ */
private getCommonEntry = (cashflowTransaction: ICashflowTransaction) => { private getCommonEntry = (cashflowTransaction: ICashflowTransaction) => {
const { entries, ...transaction } = cashflowTransaction; const { entries, ...transaction } = cashflowTransaction;
@@ -41,9 +41,7 @@ export default class CashflowTransactionJournalEntries {
), ),
transactionId: transaction.id, transactionId: transaction.id,
transactionNumber: transaction.transactionNumber, transactionNumber: transaction.transactionNumber,
referenceNumber: transaction.referenceNo, referenceNo: transaction.referenceNo,
note: transaction.description,
branchId: cashflowTransaction.branchId, branchId: cashflowTransaction.branchId,
userId: cashflowTransaction.userId, userId: cashflowTransaction.userId,
@@ -78,9 +76,9 @@ export default class CashflowTransactionJournalEntries {
/** /**
* Retrieves the cashflow credit GL entry. * Retrieves the cashflow credit GL entry.
* @param {ICashflowTransaction} cashflowTransaction * @param {ICashflowTransaction} cashflowTransaction
* @param {ICashflowTransactionLine} entry * @param {ICashflowTransactionLine} entry
* @param {number} index * @param {number} index
* @returns {ILedgerEntry} * @returns {ILedgerEntry}
*/ */
private getCashflowCreditGLEntry = ( private getCashflowCreditGLEntry = (
@@ -104,10 +102,10 @@ export default class CashflowTransactionJournalEntries {
/** /**
* Retrieves the cashflow transaction GL entry. * Retrieves the cashflow transaction GL entry.
* @param {ICashflowTransaction} cashflowTransaction * @param {ICashflowTransaction} cashflowTransaction
* @param {ICashflowTransactionLine} entry * @param {ICashflowTransactionLine} entry
* @param {number} index * @param {number} index
* @returns {ILedgerEntry[]} * @returns
*/ */
private getJournalEntries = ( private getJournalEntries = (
cashflowTransaction: ICashflowTransaction cashflowTransaction: ICashflowTransaction
@@ -120,7 +118,7 @@ export default class CashflowTransactionJournalEntries {
/** /**
* Retrieves the cashflow GL ledger. * Retrieves the cashflow GL ledger.
* @param {ICashflowTransaction} cashflowTransaction * @param {ICashflowTransaction} cashflowTransaction
* @returns {Ledger} * @returns {Ledger}
*/ */
private getCashflowLedger = (cashflowTransaction: ICashflowTransaction) => { private getCashflowLedger = (cashflowTransaction: ICashflowTransaction) => {
@@ -132,7 +130,6 @@ export default class CashflowTransactionJournalEntries {
* Write the journal entries of the given cashflow transaction. * Write the journal entries of the given cashflow transaction.
* @param {number} tenantId * @param {number} tenantId
* @param {ICashflowTransaction} cashflowTransaction * @param {ICashflowTransaction} cashflowTransaction
* @return {Promise<void>}
*/ */
public writeJournalEntries = async ( public writeJournalEntries = async (
tenantId: number, tenantId: number,
@@ -156,7 +153,6 @@ export default class CashflowTransactionJournalEntries {
* Delete the journal entries. * Delete the journal entries.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {number} cashflowTransactionId - Cashflow transaction id. * @param {number} cashflowTransactionId - Cashflow transaction id.
* @return {Promise<void>}
*/ */
public revertJournalEntries = async ( public revertJournalEntries = async (
tenantId: number, tenantId: number,

View File

@@ -55,7 +55,7 @@ export class CreateCustomer {
} as ICustomerEventCreatingPayload); } as ICustomerEventCreatingPayload);
// Creates a new contact as customer. // Creates a new contact as customer.
const customer = await Contact.query(trx).insertAndFetch({ const customer = await Contact.query().insertAndFetch({
...customerObj, ...customerObj,
}); });
// Triggers `onCustomerCreated` event. // Triggers `onCustomerCreated` event.

View File

@@ -1,5 +1,5 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Knex } from 'knex'; import Knex from 'knex';
import { sumBy } from 'lodash'; import { sumBy } from 'lodash';
import { import {
ICreditNote, ICreditNote,
@@ -8,31 +8,27 @@ import {
ISaleInvoice, ISaleInvoice,
} from '@/interfaces'; } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives';
import UnitOfWork from '@/services/UnitOfWork'; import UnitOfWork from '@/services/UnitOfWork';
import { PaymentReceiveValidators } from '../Sales/PaymentReceives/PaymentReceiveValidators'; import events from '@/subscribers/events';
import BaseCreditNotes from './CreditNotes'; import BaseCreditNotes from './CreditNotes';
import { import {
IApplyCreditToInvoicesDTO, IApplyCreditToInvoicesDTO,
IApplyCreditToInvoicesCreatedPayload, IApplyCreditToInvoicesCreatedPayload,
} from '@/interfaces'; } from '@/interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import events from '@/subscribers/events';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import HasTenancyService from '../Tenancy/TenancyService';
@Service() @Service()
export default class CreditNoteApplyToInvoices extends BaseCreditNotes { export default class CreditNoteApplyToInvoices extends BaseCreditNotes {
@Inject() @Inject('PaymentReceives')
private tenancy: HasTenancyService; paymentReceive: PaymentReceiveService;
@Inject() @Inject()
private paymentReceiveValidators: PaymentReceiveValidators; uow: UnitOfWork;
@Inject() @Inject()
private uow: UnitOfWork; eventPublisher: EventPublisher;
@Inject()
private eventPublisher: EventPublisher;
/** /**
* Apply credit note to the given invoices. * Apply credit note to the given invoices.
@@ -54,7 +50,7 @@ export default class CreditNoteApplyToInvoices extends BaseCreditNotes {
); );
// Retrieve the applied invoices that associated to the credit note customer. // Retrieve the applied invoices that associated to the credit note customer.
const appliedInvoicesEntries = const appliedInvoicesEntries =
await this.paymentReceiveValidators.validateInvoicesIDsExistance( await this.paymentReceive.validateInvoicesIDsExistance(
tenantId, tenantId,
creditNote.customerId, creditNote.customerId,
applyCreditToInvoicesDTO.entries applyCreditToInvoicesDTO.entries

View File

@@ -5,13 +5,18 @@ import {
ICreditNoteDeletedPayload, ICreditNoteDeletedPayload,
ICreditNoteEditedPayload, ICreditNoteEditedPayload,
ICreditNoteOpenedPayload, ICreditNoteOpenedPayload,
IRefundCreditNoteOpenedPayload,
} from '@/interfaces'; } from '@/interfaces';
import CreditNoteGLEntries from './CreditNoteGLEntries'; import CreditNoteGLEntries from './CreditNoteGLEntries';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export default class CreditNoteGLEntriesSubscriber { export default class CreditNoteGLEntriesSubscriber {
@Inject() @Inject()
private creditNoteGLEntries: CreditNoteGLEntries; creditNoteGLEntries: CreditNoteGLEntries;
@Inject()
tenancy: HasTenancyService;
/** /**
* Attaches events with handlers. * Attaches events with handlers.

View File

@@ -1,24 +1,24 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Knex } from 'knex'; import Knex from 'knex';
import { IApplyCreditToInvoicesDeletedPayload } from '@/interfaces'; import { IApplyCreditToInvoicesDeletedPayload } from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import PaymentReceiveService from '@/services/Sales/PaymentReceives/PaymentsReceives';
import UnitOfWork from '@/services/UnitOfWork'; import UnitOfWork from '@/services/UnitOfWork';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import BaseCreditNotes from './CreditNotes'; import BaseCreditNotes from './CreditNotes';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import HasTenancyService from '../Tenancy/TenancyService';
@Service() @Service()
export default class DeletreCreditNoteApplyToInvoices extends BaseCreditNotes { export default class DeletreCreditNoteApplyToInvoices extends BaseCreditNotes {
@Inject() @Inject('PaymentReceives')
private uow: UnitOfWork; paymentReceive: PaymentReceiveService;
@Inject() @Inject()
private eventPublisher: EventPublisher; uow: UnitOfWork;
@Inject() @Inject()
private tenancy: HasTenancyService; eventPublisher: EventPublisher;
/** /**
* Apply credit note to the given invoices. * Apply credit note to the given invoices.

View File

@@ -9,6 +9,7 @@ import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork'; import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandExpenseValidator } from './CommandExpenseValidator'; import { CommandExpenseValidator } from './CommandExpenseValidator';
import { ExpenseCategory } from 'models';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
@@ -36,7 +37,7 @@ export class DeleteExpense {
expenseId: number, expenseId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<void> => { ): Promise<void> => {
const { Expense, ExpenseCategory } = this.tenancy.models(tenantId); const { Expense } = this.tenancy.models(tenantId);
// Retrieves the expense transaction with associated entries or // Retrieves the expense transaction with associated entries or
// throw not found error. // throw not found error.
@@ -59,7 +60,7 @@ export class DeleteExpense {
} as IExpenseDeletingPayload); } as IExpenseDeletingPayload);
// Deletes expense associated entries. // Deletes expense associated entries.
await ExpenseCategory.query(trx).where('expenseId', expenseId).delete(); await ExpenseCategory.query(trx).findById(expenseId).delete();
// Deletes expense transactions. // Deletes expense transactions.
await Expense.query(trx).findById(expenseId).delete(); await Expense.query(trx).findById(expenseId).delete();

View File

@@ -46,7 +46,7 @@ export class ExpenseGLEntries {
...commonEntry, ...commonEntry,
credit: expense.localAmount, credit: expense.localAmount,
accountId: expense.paymentAccountId, accountId: expense.paymentAccountId,
accountNormal: AccountNormal.DEBIT, accountNormal: AccountNormal.CREDIT,
index: 1, index: 1,
}; };
}; };

View File

@@ -5,7 +5,7 @@ import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces
@Service() @Service()
export default class CashflowAccountTransactionsRepo { export default class CashflowAccountTransactionsRepo {
@Inject() @Inject()
private tenancy: HasTenancyService; tenancy: HasTenancyService;
/** /**
* Retrieve the cashflow account transactions. * Retrieve the cashflow account transactions.

View File

@@ -17,7 +17,7 @@ export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
formatMoney: 'total', formatMoney: 'total',
precision: 2, precision: 2,
}, },
basis: 'accrual', basis: 'accural',
noneZero: false, noneZero: false,
noneTransactions: false, noneTransactions: false,

View File

@@ -35,7 +35,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
formatMoney: 'total', formatMoney: 'total',
precision: 2, precision: 2,
}, },
basis: 'accrual', basis: 'accural',
noneZero: false, noneZero: false,
noneTransactions: true, noneTransactions: true,
onlyActive: false, onlyActive: false,

View File

@@ -12,12 +12,9 @@ import {
} from '@/interfaces'; } from '@/interfaces';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { IAcceptInviteUserService } from '@/interfaces';
@Service() @Service()
export default class AcceptInviteUserService export default class AcceptInviteUserService {
implements IAcceptInviteUserService
{
@Inject() @Inject()
private eventPublisher: EventPublisher; private eventPublisher: EventPublisher;

View File

@@ -1,4 +1,7 @@
import { IUserInviteTenantSyncedEventPayload } from '@/interfaces'; import {
IUserInvitedEventPayload,
IUserInviteTenantSyncedEventPayload,
} from '@/interfaces';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';

View File

@@ -1,12 +1,12 @@
import path from 'path';
import { ISystemUser } from '@/interfaces'; import { ISystemUser } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import Mail from '@/lib/Mail'; import Mail from '@/lib/Mail';
import { Service } from 'typedi'; import { Service, Container } from 'typedi';
import { Tenant } from '@/system/models';
import config from '@/config'; import config from '@/config';
import { Tenant } from '@/system/models';
@Service() @Service()
export default class SendInviteUsersMailMessage { export default class InviteUsersMailMessages {
/** /**
* Sends invite mail to the given email. * Sends invite mail to the given email.
* @param user * @param user
@@ -18,7 +18,7 @@ export default class SendInviteUsersMailMessage {
.findById(tenantId) .findById(tenantId)
.withGraphFetched('metadata'); .withGraphFetched('metadata');
const root = path.join(global.__views_dir, '/images/bigcapital.png'); const root = __dirname + '/../../../views/images/bigcapital.png';
const mail = new Mail() const mail = new Mail()
.setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`) .setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`)

View File

@@ -12,10 +12,10 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service() @Service()
export default class SyncSystemSendInvite { export default class SyncSystemSendInvite {
@Inject() @Inject()
private tenancy: HasTenancyService; tenancy: HasTenancyService;
@Inject() @Inject()
private eventPublisher: EventPublisher; eventPublisher: EventPublisher;
/** /**
* Attaches events with handlers. * Attaches events with handlers.

View File

@@ -8,7 +8,7 @@ import { IAcceptInviteEventPayload } from '@/interfaces';
@Service() @Service()
export default class SyncTenantAcceptInvite { export default class SyncTenantAcceptInvite {
@Inject() @Inject()
private tenancy: HasTenancyService; tenancy: HasTenancyService;
/** /**
* Attaches events with handlers. * Attaches events with handlers.

View File

@@ -74,15 +74,17 @@ export default class InviteTenantUserService implements IInviteUserService {
/** /**
* Re-send user invite. * Re-send user invite.
* @param {number} tenantId - * @param {number} tenantId -
* @param {string} email - * @param {string} email -
* @return {Promise<{ invite: IUserInvite }>} * @return {Promise<{ invite: IUserInvite }>}
*/ */
public async resendInvite( public async resendInvite(
tenantId: number, tenantId: number,
userId: number, userId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ user: ITenantUser }> { ): Promise<{
user: ITenantUser;
}> {
// Retrieve the user by id or throw not found service error. // Retrieve the user by id or throw not found service error.
const user = await this.getUserByIdOrThrowError(tenantId, userId); const user = await this.getUserByIdOrThrowError(tenantId, userId);

View File

@@ -27,7 +27,7 @@ export class ItemInvoicesTransactionsTransformer extends Transformer {
} }
/** /**
* Formatted invoice date. *
* @param item * @param item
* @returns * @returns
*/ */
@@ -36,17 +36,16 @@ export class ItemInvoicesTransactionsTransformer extends Transformer {
}; };
/** /**
* Formatted item quantity. *
* @returns {string}
*/ */
public formattedQuantity = (entry): string => { public formattedQuantity = (entry): string => {
return entry.quantity; return entry.quantity;
}; };
/** /**
* Formatted date. *
* @param entry * @param entry
* @returns {string} * @returns
*/ */
public formattedRate = (entry): string => { public formattedRate = (entry): string => {
return formatNumber(entry.rate, { return formatNumber(entry.rate, {

View File

@@ -3,8 +3,8 @@ import { Inject, Service } from 'typedi';
import { IItemEntry, IItemEntryDTO, IItem } from '@/interfaces'; import { IItemEntry, IItemEntryDTO, IItem } from '@/interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntry } from '@/models';
import { entriesAmountDiff } from 'utils'; import { entriesAmountDiff } from 'utils';
import { ItemEntry } from 'models';
const ERRORS = { const ERRORS = {
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND', ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
@@ -16,7 +16,7 @@ const ERRORS = {
@Service() @Service()
export default class ItemsEntriesService { export default class ItemsEntriesService {
@Inject() @Inject()
private tenancy: TenancyService; tenancy: TenancyService;
/** /**
* Retrieve the inventory items entries of the reference id and type. * Retrieve the inventory items entries of the reference id and type.
@@ -235,7 +235,7 @@ export default class ItemsEntriesService {
/** /**
* Sets the cost/sell accounts to the invoice entries. * Sets the cost/sell accounts to the invoice entries.
*/ */
public setItemsEntriesDefaultAccounts(tenantId: number) { setItemsEntriesDefaultAccounts(tenantId: number) {
return async (entries: IItemEntry[]) => { return async (entries: IItemEntry[]) => {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
@@ -261,7 +261,7 @@ export default class ItemsEntriesService {
* @param entries * @param entries
* @returns * @returns
*/ */
public getTotalItemsEntries(entries: ItemEntry[]): number { getTotalItemsEntries(entries: ItemEntry[]): number {
return sumBy(entries, (e) => ItemEntry.calcAmount(e)); return sumBy(entries, (e) => ItemEntry.calcAmount(e));
} }
} }

View File

@@ -1,10 +1,11 @@
import { difference } from 'lodash'; import { difference, sumBy, omit, map } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
IManualJournalDTO, IManualJournalDTO,
IManualJournalEntry, IManualJournalEntry,
IManualJournal, IManualJournal,
IManualJournalEntryDTO,
} from '@/interfaces'; } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
@@ -285,7 +286,7 @@ export class CommandManualJournalValidators {
public validateJournalCurrencyWithAccountsCurrency = async ( public validateJournalCurrencyWithAccountsCurrency = async (
tenantId: number, tenantId: number,
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO,
baseCurrency: string baseCurrency: string,
) => { ) => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);

View File

@@ -3,20 +3,25 @@ import * as R from 'ramda';
import { import {
IManualJournal, IManualJournal,
IManualJournalEntry, IManualJournalEntry,
IAccount,
ILedgerEntry, ILedgerEntry,
} from '@/interfaces'; } from '@/interfaces';
import { Knex } from 'knex'; import { Knex } from 'knex';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { LedgerRevert } from '@/services/Accounting/LedgerStorageRevert';
@Service() @Service()
export class ManualJournalGLEntries { export class ManualJournalGLEntries {
@Inject() @Inject()
private ledgerStorage: LedgerStorageService; ledgerStorage: LedgerStorageService;
@Inject() @Inject()
private tenancy: HasTenancyService; ledgerRevert: LedgerRevert;
@Inject()
tenancy: HasTenancyService;
/** /**
* Create manual journal GL entries. * Create manual journal GL entries.
@@ -72,7 +77,7 @@ export class ManualJournalGLEntries {
manualJournalId: number, manualJournalId: number,
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<void> => { ): Promise<void> => {
return this.ledgerStorage.deleteByReference( return this.ledgerRevert.revertGLEntries(
tenantId, tenantId,
manualJournalId, manualJournalId,
'Journal', 'Journal',
@@ -81,7 +86,7 @@ export class ManualJournalGLEntries {
}; };
/** /**
* Retrieves the ledger of the given manual journal. *
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {Ledger} * @returns {Ledger}
*/ */
@@ -92,13 +97,11 @@ export class ManualJournalGLEntries {
}; };
/** /**
* Retrieves the common entry details of the manual journal *
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {Partial<ILedgerEntry>} * @returns {}
*/ */
private getManualJournalCommonEntry = ( private getManualJournalCommonEntry = (manualJournal: IManualJournal) => {
manualJournal: IManualJournal
): Partial<ILedgerEntry> => {
return { return {
transactionNumber: manualJournal.journalNumber, transactionNumber: manualJournal.journalNumber,
referenceNumber: manualJournal.reference, referenceNumber: manualJournal.reference,
@@ -115,8 +118,7 @@ export class ManualJournalGLEntries {
}; };
/** /**
* Retrieves the ledger entry of the given manual journal and *
* its associated entry.
* @param {IManualJournal} manualJournal - * @param {IManualJournal} manualJournal -
* @param {IManualJournalEntry} entry - * @param {IManualJournalEntry} entry -
* @returns {ILedgerEntry} * @returns {ILedgerEntry}
@@ -147,7 +149,7 @@ export class ManualJournalGLEntries {
); );
/** /**
* Retrieves the ledger of the given manual journal. *
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {ILedgerEntry[]} * @returns {ILedgerEntry[]}
*/ */

View File

@@ -23,11 +23,8 @@ export class ProjectBillableBillSubscriber {
events.saleInvoice.onCreated, events.saleInvoice.onCreated,
this.handleIncreaseBillableBill this.handleIncreaseBillableBill
); );
bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableBill); bus.subscribe(events.saleInvoice.onEdited, this.handleDecreaseBillableBill);
bus.subscribe( bus.subscribe(events.saleInvoice.onDeleted, this.handleEditBillableBill);
events.saleInvoice.onDeleted,
this.handleDecreaseBillableBill
);
} }
/** /**

View File

@@ -1,11 +1,7 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import async from 'async'; import async from 'async';
import { import { ISaleInvoice, ISaleInvoiceDTO, ProjectLinkRefType } from '@/interfaces';
ISaleInvoice,
ISaleInvoiceDTO,
ProjectLinkRefType,
} from '@/interfaces';
import { ProjectBillableExpense } from './ProjectBillableExpense'; import { ProjectBillableExpense } from './ProjectBillableExpense';
import { filterEntriesByRefType } from './_utils'; import { filterEntriesByRefType } from './_utils';

View File

@@ -21,10 +21,13 @@ export class ProjectBillableExpensesSubscriber {
events.saleInvoice.onCreated, events.saleInvoice.onCreated,
this.handleIncreaseBillableExpenses this.handleIncreaseBillableExpenses
); );
bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableExpenses); bus.subscribe(
events.saleInvoice.onEdited,
this.handleDecreaseBillableExpenses
);
bus.subscribe( bus.subscribe(
events.saleInvoice.onDeleted, events.saleInvoice.onDeleted,
this.handleDecreaseBillableExpenses this.handleEditBillableExpenses
); );
} }

View File

@@ -1,48 +0,0 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IBillPaymentEntryDTO } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { entriesAmountDiff } from '@/utils';
@Service()
export class BillPaymentBillSync {
@Inject()
private tenancy: HasTenancyService;
/**
* Saves bills payment amount changes different.
* @param {number} tenantId -
* @param {IBillPaymentEntryDTO[]} paymentMadeEntries -
* @param {IBillPaymentEntryDTO[]} oldPaymentMadeEntries -
*/
public async saveChangeBillsPaymentAmount(
tenantId: number,
paymentMadeEntries: IBillPaymentEntryDTO[],
oldPaymentMadeEntries?: IBillPaymentEntryDTO[],
trx?: Knex.Transaction
): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
const opers: Promise<void>[] = [];
const diffEntries = entriesAmountDiff(
paymentMadeEntries,
oldPaymentMadeEntries,
'paymentAmount',
'billId'
);
diffEntries.forEach(
(diffEntry: { paymentAmount: number; billId: number }) => {
if (diffEntry.paymentAmount === 0) {
return;
}
const oper = Bill.changePaymentAmount(
diffEntry.billId,
diffEntry.paymentAmount,
trx
);
opers.push(oper);
}
);
await Promise.all(opers);
}
}

View File

@@ -99,7 +99,7 @@ export class BillPaymentGLEntries {
/** /**
* Retrieves the payment common entry. * Retrieves the payment common entry.
* @param {IBillPayment} billPayment * @param {IBillPayment} billPayment
* @returns {} * @returns {}
*/ */
private getPaymentCommonEntry = (billPayment: IBillPayment) => { private getPaymentCommonEntry = (billPayment: IBillPayment) => {

View File

@@ -24,7 +24,7 @@ export class BillPaymentTransactionTransformer extends Transformer {
/** /**
* Retrieve formatted bill payment date. * Retrieve formatted bill payment date.
* @param entry * @param entry
* @returns {string} * @returns
*/ */
protected formattedPaymentDate = (entry): string => { protected formattedPaymentDate = (entry): string => {
return this.formatDate(entry.payment.paymentDate); return this.formatDate(entry.payment.paymentDate);

View File

@@ -1,278 +0,0 @@
import { Inject, Service } from 'typedi';
import { sumBy, difference } from 'lodash';
import {
IBill,
IBillPaymentDTO,
IBillPaymentEntryDTO,
IBillPayment,
IBillPaymentEntry,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
import { BillPayment } from '@/models';
import { ERRORS } from './constants';
@Service()
export class BillPaymentValidators {
@Inject()
private tenancy: TenancyService;
/**
* Validates the payment existance.
* @param {BillPayment | undefined | null} payment
* @throws {ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND)}
*/
public async validateBillPaymentExistance(
payment: BillPayment | undefined | null
) {
if (!payment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
}
/**
* Validates the bill payment existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
public async getPaymentMadeOrThrowError(
tenantid: number,
paymentMadeId: number
) {
const { BillPayment } = this.tenancy.models(tenantid);
const billPayment = await BillPayment.query()
.withGraphFetched('entries')
.findById(paymentMadeId);
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
return billPayment;
}
/**
* Validates the payment account.
* @param {number} tenantId -
* @param {number} paymentAccountId
* @return {Promise<IAccountType>}
*/
public async getPaymentAccountOrThrowError(
tenantId: number,
paymentAccountId: number
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAccount = await accountRepository.findOneById(
paymentAccountId
);
if (!paymentAccount) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
}
// Validate the payment account type.
if (
!paymentAccount.isAccountType([
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
])
) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE);
}
return paymentAccount;
}
/**
* Validates the payment number uniqness.
* @param {number} tenantId -
* @param {string} paymentMadeNumber -
* @return {Promise<IBillPayment>}
*/
public async validatePaymentNumber(
tenantId: number,
paymentMadeNumber: string,
notPaymentMadeId?: number
) {
const { BillPayment } = this.tenancy.models(tenantId);
const foundBillPayment = await BillPayment.query().onBuild(
(builder: any) => {
builder.findOne('payment_number', paymentMadeNumber);
if (notPaymentMadeId) {
builder.whereNot('id', notPaymentMadeId);
}
}
);
if (foundBillPayment) {
throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE);
}
return foundBillPayment;
}
/**
* Validate whether the entries bills ids exist on the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async validateBillsExistance(
tenantId: number,
billPaymentEntries: { billId: number }[],
vendorId: number
) {
const { Bill } = this.tenancy.models(tenantId);
const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId);
const storedBills = await Bill.query()
.whereIn('id', entriesBillsIds)
.where('vendor_id', vendorId);
const storedBillsIds = storedBills.map((t: IBill) => t.id);
const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds);
if (notFoundBillsIds.length > 0) {
throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND);
}
// Validate the not opened bills.
const notOpenedBills = storedBills.filter((bill) => !bill.openedAt);
if (notOpenedBills.length > 0) {
throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, {
notOpenedBills,
});
}
return storedBills;
}
/**
* Validate wether the payment amount bigger than the payable amount.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @return {void}
*/
public async validateBillsDueAmount(
tenantId: number,
billPaymentEntries: IBillPaymentEntryDTO[],
oldPaymentEntries: IBillPaymentEntry[] = []
) {
const { Bill } = this.tenancy.models(tenantId);
const billsIds = billPaymentEntries.map(
(entry: IBillPaymentEntryDTO) => entry.billId
);
const storedBills = await Bill.query().whereIn('id', billsIds);
const storedBillsMap = new Map(
storedBills.map((bill) => {
const oldEntries = oldPaymentEntries.filter(
(entry) => entry.billId === bill.id
);
const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0;
return [
bill.id,
{ ...bill, dueAmount: bill.dueAmount + oldPaymentAmount },
];
})
);
interface invalidPaymentAmountError {
index: number;
due_amount: number;
}
const hasWrongPaymentAmount: invalidPaymentAmountError[] = [];
billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => {
const entryBill = storedBillsMap.get(entry.billId);
const { dueAmount } = entryBill;
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
});
if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT);
}
}
/**
* Validate the payment receive entries IDs existance.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
public async validateEntriesIdsExistance(
tenantId: number,
billPaymentId: number,
billPaymentEntries: IBillPaymentEntry[]
) {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
const entriesIds = billPaymentEntries
.filter((entry: any) => entry.id)
.map((entry: any) => entry.id);
const storedEntries = await BillPaymentEntry.query().where(
'bill_payment_id',
billPaymentId
);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.BILL_PAYMENT_ENTRIES_NOT_FOUND);
}
}
/**
* * Validate the payment vendor whether modified.
* @param {string} billPaymentNo
*/
public validateVendorNotModified(
billPaymentDTO: IBillPaymentDTO,
oldBillPayment: IBillPayment
) {
if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) {
throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY);
}
}
/**
* Validates the payment account currency code. The deposit account curreny
* should be equals the customer currency code or the base currency.
* @param {string} paymentAccountCurrency
* @param {string} customerCurrency
* @param {string} baseCurrency
* @throws {ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID)}
*/
public validateWithdrawalAccountCurrency = (
paymentAccountCurrency: string,
customerCurrency: string,
baseCurrency: string
) => {
if (
paymentAccountCurrency !== customerCurrency &&
paymentAccountCurrency !== baseCurrency
) {
throw new ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID);
}
};
/**
* Validates the given vendor has no associated payments.
* @param {number} tenantId
* @param {number} vendorId
*/
public async validateVendorHasNoPayments(tenantId: number, vendorId: number) {
const { BillPayment } = this.tenancy.models(tenantId);
const payments = await BillPayment.query().where('vendor_id', vendorId);
if (payments.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_PAYMENTS);
}
}
}

View File

@@ -0,0 +1,713 @@
import { Inject, Service } from 'typedi';
import { sumBy, difference } from 'lodash';
import * as R from 'ramda';
import { Knex } from 'knex';
import events from '@/subscribers/events';
import {
IBill,
IBillPaymentDTO,
IBillPaymentEntryDTO,
IBillPayment,
IBillPaymentsFilter,
IPaginationMeta,
IFilterMeta,
IBillPaymentEntry,
IBillPaymentEventCreatedPayload,
IBillPaymentEventEditedPayload,
IBillPaymentEventDeletedPayload,
IBillPaymentCreatingPayload,
IBillPaymentEditingPayload,
IBillPaymentDeletingPayload,
IVendor,
} from '@/interfaces';
import JournalPosterService from '@/services/Sales/JournalPosterService';
import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { entriesAmountDiff, formatDateFields } from 'utils';
import { ServiceError } from '@/exceptions';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
import { BillPaymentTransformer } from './BillPaymentTransformer';
import { ERRORS } from './constants';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { TenantMetadata } from '@/system/models';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
/**
* Bill payments service.
* @service
*/
@Service('BillPayments')
export default class BillPaymentsService implements IBillPaymentsService {
@Inject()
tenancy: TenancyService;
@Inject()
journalService: JournalPosterService;
@Inject()
dynamicListService: DynamicListingService;
@Inject()
eventPublisher: EventPublisher;
@Inject()
private transformer: TransformerInjectable;
@Inject()
uow: UnitOfWork;
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
/**
* Validates the bill payment existance.
* @param {Request} req
* @param {Response} res
* @param {Function} next
*/
private async getPaymentMadeOrThrowError(
tenantid: number,
paymentMadeId: number
) {
const { BillPayment } = this.tenancy.models(tenantid);
const billPayment = await BillPayment.query()
.withGraphFetched('entries')
.findById(paymentMadeId);
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
return billPayment;
}
/**
* Validates the payment account.
* @param {number} tenantId -
* @param {number} paymentAccountId
* @return {Promise<IAccountType>}
*/
private async getPaymentAccountOrThrowError(
tenantId: number,
paymentAccountId: number
) {
const { accountRepository } = this.tenancy.repositories(tenantId);
const paymentAccount = await accountRepository.findOneById(
paymentAccountId
);
if (!paymentAccount) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
}
// Validate the payment account type.
if (
!paymentAccount.isAccountType([
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
])
) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE);
}
return paymentAccount;
}
/**
* Validates the payment number uniqness.
* @param {number} tenantId -
* @param {string} paymentMadeNumber -
* @return {Promise<IBillPayment>}
*/
private async validatePaymentNumber(
tenantId: number,
paymentMadeNumber: string,
notPaymentMadeId?: number
) {
const { BillPayment } = this.tenancy.models(tenantId);
const foundBillPayment = await BillPayment.query().onBuild(
(builder: any) => {
builder.findOne('payment_number', paymentMadeNumber);
if (notPaymentMadeId) {
builder.whereNot('id', notPaymentMadeId);
}
}
);
if (foundBillPayment) {
throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE);
}
return foundBillPayment;
}
/**
* Validate whether the entries bills ids exist on the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async validateBillsExistance(
tenantId: number,
billPaymentEntries: { billId: number }[],
vendorId: number
) {
const { Bill } = this.tenancy.models(tenantId);
const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId);
const storedBills = await Bill.query()
.whereIn('id', entriesBillsIds)
.where('vendor_id', vendorId);
const storedBillsIds = storedBills.map((t: IBill) => t.id);
const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds);
if (notFoundBillsIds.length > 0) {
throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND);
}
// Validate the not opened bills.
const notOpenedBills = storedBills.filter((bill) => !bill.openedAt);
if (notOpenedBills.length > 0) {
throw new ServiceError(ERRORS.BILLS_NOT_OPENED_YET, null, {
notOpenedBills,
});
}
return storedBills;
}
/**
* Validate wether the payment amount bigger than the payable amount.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @return {void}
*/
private async validateBillsDueAmount(
tenantId: number,
billPaymentEntries: IBillPaymentEntryDTO[],
oldPaymentEntries: IBillPaymentEntry[] = []
) {
const { Bill } = this.tenancy.models(tenantId);
const billsIds = billPaymentEntries.map(
(entry: IBillPaymentEntryDTO) => entry.billId
);
const storedBills = await Bill.query().whereIn('id', billsIds);
const storedBillsMap = new Map(
storedBills.map((bill) => {
const oldEntries = oldPaymentEntries.filter(
(entry) => entry.billId === bill.id
);
const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0;
return [
bill.id,
{ ...bill, dueAmount: bill.dueAmount + oldPaymentAmount },
];
})
);
interface invalidPaymentAmountError {
index: number;
due_amount: number;
}
const hasWrongPaymentAmount: invalidPaymentAmountError[] = [];
billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => {
const entryBill = storedBillsMap.get(entry.billId);
const { dueAmount } = entryBill;
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
});
if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT);
}
}
/**
* Validate the payment receive entries IDs existance.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
private async validateEntriesIdsExistance(
tenantId: number,
billPaymentId: number,
billPaymentEntries: IBillPaymentEntry[]
) {
const { BillPaymentEntry } = this.tenancy.models(tenantId);
const entriesIds = billPaymentEntries
.filter((entry: any) => entry.id)
.map((entry: any) => entry.id);
const storedEntries = await BillPaymentEntry.query().where(
'bill_payment_id',
billPaymentId
);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.BILL_PAYMENT_ENTRIES_NOT_FOUND);
}
}
/**
* * Validate the payment vendor whether modified.
* @param {string} billPaymentNo
*/
private validateVendorNotModified(
billPaymentDTO: IBillPaymentDTO,
oldBillPayment: IBillPayment
) {
if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) {
throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY);
}
}
/**
* Validates the payment account currency code. The deposit account curreny
* should be equals the customer currency code or the base currency.
* @param {string} paymentAccountCurrency
* @param {string} customerCurrency
* @param {string} baseCurrency
* @throws {ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID)}
*/
public validateWithdrawalAccountCurrency = (
paymentAccountCurrency: string,
customerCurrency: string,
baseCurrency: string
) => {
if (
paymentAccountCurrency !== customerCurrency &&
paymentAccountCurrency !== baseCurrency
) {
throw new ServiceError(ERRORS.WITHDRAWAL_ACCOUNT_CURRENCY_INVALID);
}
};
/**
* Transforms create/edit DTO to model.
* @param {number} tenantId
* @param {IBillPaymentDTO} billPaymentDTO - Bill payment.
* @param {IBillPayment} oldBillPayment - Old bill payment.
* @return {Promise<IBillPayment>}
*/
async transformDTOToModel(
tenantId: number,
billPaymentDTO: IBillPaymentDTO,
vendor: IVendor,
oldBillPayment?: IBillPayment
): Promise<IBillPayment> {
const initialDTO = {
...formatDateFields(billPaymentDTO, ['paymentDate']),
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
currencyCode: vendor.currencyCode,
exchangeRate: billPaymentDTO.exchangeRate || 1,
entries: billPaymentDTO.entries,
};
return R.compose(
this.branchDTOTransform.transformDTO<IBillPayment>(tenantId)
)(initialDTO);
}
/**
* Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions.
*
* Precedures:-
* ------
* - Records the bill payment transaction.
* - Records the bill payment associated entries.
* - Increment the payment amount of the given vendor bills.
* - Decrement the vendor balance.
* - Records payment journal entries.
* ------
* @param {number} tenantId - Tenant id.
* @param {BillPaymentDTO} billPayment - Bill payment object.
*/
public async createBillPayment(
tenantId: number,
billPaymentDTO: IBillPaymentDTO
): Promise<IBillPayment> {
const { BillPayment, Contact } = this.tenancy.models(tenantId);
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Retrieves the payment vendor or throw not found error.
const vendor = await Contact.query()
.findById(billPaymentDTO.vendorId)
.modify('vendor')
.throwIfNotFound();
// Transform create DTO to model object.
const billPaymentObj = await this.transformDTOToModel(
tenantId,
billPaymentDTO,
vendor
);
// Validate the payment account existance and type.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
billPaymentObj.paymentAccountId
);
// Validate the payment number uniquiness.
if (billPaymentObj.paymentNumber) {
await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber);
}
// Validates the bills existance and associated to the given vendor.
await this.validateBillsExistance(
tenantId,
billPaymentObj.entries,
billPaymentDTO.vendorId
);
// Validates the bills due payment amount.
await this.validateBillsDueAmount(tenantId, billPaymentObj.entries);
// Validates the withdrawal account currency code.
this.validateWithdrawalAccountCurrency(
paymentAccount.currencyCode,
vendor.currencyCode,
tenantMeta.baseCurrency
);
// Writes bill payment transacation with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillPaymentCreating` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreating, {
tenantId,
billPaymentDTO,
trx,
} as IBillPaymentCreatingPayload);
// Writes the bill payment graph to the storage.
const billPayment = await BillPayment.query(trx).insertGraphAndFetch({
...billPaymentObj,
});
// Triggers `onBillPaymentCreated` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreated, {
tenantId,
billPayment,
billPaymentId: billPayment.id,
trx,
} as IBillPaymentEventCreatedPayload);
return billPayment;
});
}
/**
* Edits the details of the given bill payment.
*
* Preceducres:
* ------
* - Update the bill payment transaction.
* - Insert the new bill payment entries that have no ids.
* - Update the bill paymeny entries that have ids.
* - Delete the bill payment entries that not presented.
* - Re-insert the journal transactions and update the diff accounts balance.
* - Update the diff vendor balance.
* - Update the diff bill payment amount.
* ------
* @param {number} tenantId - Tenant id
* @param {Integer} billPaymentId
* @param {BillPaymentDTO} billPayment
* @param {IBillPayment} oldBillPayment
*/
public async editBillPayment(
tenantId: number,
billPaymentId: number,
billPaymentDTO
): Promise<IBillPayment> {
const { BillPayment, Contact } = this.tenancy.models(tenantId);
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
//
const oldBillPayment = await this.getPaymentMadeOrThrowError(
tenantId,
billPaymentId
);
//
const vendor = await Contact.query()
.modify('vendor')
.findById(billPaymentDTO.vendorId)
.throwIfNotFound();
// Transform bill payment DTO to model object.
const billPaymentObj = await this.transformDTOToModel(
tenantId,
billPaymentDTO,
vendor,
oldBillPayment
);
// Validate vendor not modified.
this.validateVendorNotModified(billPaymentDTO, oldBillPayment);
// Validate the payment account existance and type.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
billPaymentObj.paymentAccountId
);
// Validate the items entries IDs existance on the storage.
await this.validateEntriesIdsExistance(
tenantId,
billPaymentId,
billPaymentObj.entries
);
// Validate the bills existance and associated to the given vendor.
await this.validateBillsExistance(
tenantId,
billPaymentObj.entries,
billPaymentDTO.vendorId
);
// Validates the bills due payment amount.
await this.validateBillsDueAmount(
tenantId,
billPaymentObj.entries,
oldBillPayment.entries
);
// Validate the payment number uniquiness.
if (billPaymentObj.paymentNumber) {
await this.validatePaymentNumber(
tenantId,
billPaymentObj.paymentNumber,
billPaymentId
);
}
// Validates the withdrawal account currency code.
this.validateWithdrawalAccountCurrency(
paymentAccount.currencyCode,
vendor.currencyCode,
tenantMeta.baseCurrency
);
// Edits the bill transactions with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillPaymentEditing` event.
await this.eventPublisher.emitAsync(events.billPayment.onEditing, {
tenantId,
oldBillPayment,
billPaymentDTO,
trx,
} as IBillPaymentEditingPayload);
// Deletes the bill payment transaction graph from the storage.
const billPayment = await BillPayment.query(trx).upsertGraphAndFetch({
id: billPaymentId,
...billPaymentObj,
});
// Triggers `onBillPaymentEdited` event.
await this.eventPublisher.emitAsync(events.billPayment.onEdited, {
tenantId,
billPaymentId,
billPayment,
oldBillPayment,
trx,
} as IBillPaymentEventEditedPayload);
return billPayment;
});
}
/**
* Deletes the bill payment and associated transactions.
* @param {number} tenantId - Tenant id.
* @param {Integer} billPaymentId - The given bill payment id.
* @return {Promise}
*/
public async deleteBillPayment(tenantId: number, billPaymentId: number) {
const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId);
// Retrieve the bill payment or throw not found service error.
const oldBillPayment = await this.getPaymentMadeOrThrowError(
tenantId,
billPaymentId
);
// Deletes the bill transactions with associated transactions under
// unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillPaymentDeleting` payload.
await this.eventPublisher.emitAsync(events.billPayment.onDeleting, {
tenantId,
trx,
oldBillPayment,
} as IBillPaymentDeletingPayload);
// Deletes the bill payment assocaited entries.
await BillPaymentEntry.query(trx)
.where('bill_payment_id', billPaymentId)
.delete();
// Deletes the bill payment transaction.
await BillPayment.query(trx).where('id', billPaymentId).delete();
// Triggers `onBillPaymentDeleted` event.
await this.eventPublisher.emitAsync(events.billPayment.onDeleted, {
tenantId,
billPaymentId,
oldBillPayment,
trx,
} as IBillPaymentEventDeletedPayload);
});
}
/**
* Retrieve payment made associated bills.
* @param {number} tenantId -
* @param {number} billPaymentId -
*/
public async getPaymentBills(tenantId: number, billPaymentId: number) {
const { Bill } = this.tenancy.models(tenantId);
const billPayment = await this.getPaymentMadeOrThrowError(
tenantId,
billPaymentId
);
const paymentBillsIds = billPayment.entries.map((entry) => entry.id);
const bills = await Bill.query().whereIn('id', paymentBillsIds);
return bills;
}
/**
* Retrieve bill payment.
* @param {number} tenantId
* @param {number} billPyamentId
* @return {Promise<IBillPayment>}
*/
public async getBillPayment(
tenantId: number,
billPyamentId: number
): Promise<IBillPayment> {
const { BillPayment } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query()
.withGraphFetched('entries.bill')
.withGraphFetched('vendor')
.withGraphFetched('paymentAccount')
.withGraphFetched('transactions')
.withGraphFetched('branch')
.findById(billPyamentId);
if (!billPayment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
return this.transformer.transform(
tenantId,
billPayment,
new BillPaymentTransformer()
);
}
private parseListFilterDTO(filterDTO) {
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
}
/**
* Retrieve bill payment paginted and filterable list.
* @param {number} tenantId
* @param {IBillPaymentsFilter} billPaymentsFilter
*/
public async listBillPayments(
tenantId: number,
filterDTO: IBillPaymentsFilter
): Promise<{
billPayments: IBillPayment;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { BillPayment } = this.tenancy.models(tenantId);
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
BillPayment,
filter
);
const { results, pagination } = await BillPayment.query()
.onBuild((builder) => {
builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount');
dynamicList.buildQuery()(builder);
})
.pagination(filter.page - 1, filter.pageSize);
// Transformes the bill payments models to POJO.
const billPayments = await this.transformer.transform(
tenantId,
results,
new BillPaymentTransformer()
);
return {
billPayments,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
/**
* Saves bills payment amount changes different.
* @param {number} tenantId -
* @param {IBillPaymentEntryDTO[]} paymentMadeEntries -
* @param {IBillPaymentEntryDTO[]} oldPaymentMadeEntries -
*/
public async saveChangeBillsPaymentAmount(
tenantId: number,
paymentMadeEntries: IBillPaymentEntryDTO[],
oldPaymentMadeEntries?: IBillPaymentEntryDTO[],
trx?: Knex.Transaction
): Promise<void> {
const { Bill } = this.tenancy.models(tenantId);
const opers: Promise<void>[] = [];
const diffEntries = entriesAmountDiff(
paymentMadeEntries,
oldPaymentMadeEntries,
'paymentAmount',
'billId'
);
diffEntries.forEach(
(diffEntry: { paymentAmount: number; billId: number }) => {
if (diffEntry.paymentAmount === 0) {
return;
}
const oper = Bill.changePaymentAmount(
diffEntry.billId,
diffEntry.paymentAmount,
trx
);
opers.push(oper);
}
);
await Promise.all(opers);
}
/**
* Validates the given vendor has no associated payments.
* @param {number} tenantId
* @param {number} vendorId
*/
public async validateVendorHasNoPayments(tenantId: number, vendorId: number) {
const { BillPayment } = this.tenancy.models(tenantId);
const payments = await BillPayment.query().where('vendor_id', vendorId);
if (payments.length > 0) {
throw new ServiceError(ERRORS.VENDOR_HAS_PAYMENTS);
}
}
}

View File

@@ -1,109 +0,0 @@
import { Inject, Service } from 'typedi';
import { IBillPaymentDTO, IBillPayment } from '@/interfaces';
import { CreateBillPayment } from './CreateBillPayment';
import { DeleteBillPayment } from './DeleteBillPayment';
import { EditBillPayment } from './EditBillPayment';
import { GetBillPayments } from './GetBillPayments';
import { GetBillPayment } from './GetBillPayment';
import { GetPaymentBills } from './GetPaymentBills';
/**
* Bill payments application.
* @service
*/
@Service()
export class BillPaymentsApplication {
@Inject()
private createBillPaymentService: CreateBillPayment;
@Inject()
private deleteBillPaymentService: DeleteBillPayment;
@Inject()
private editBillPaymentService: EditBillPayment;
@Inject()
private getBillPaymentsService: GetBillPayments;
@Inject()
private getBillPaymentService: GetBillPayment;
@Inject()
private getPaymentBillsService: GetPaymentBills;
/**
* Creates a bill payment with associated GL entries.
* @param {number} tenantId
* @param {IBillPaymentDTO} billPaymentDTO
* @returns {Promise<IBillPayment>}
*/
public createBillPayment(
tenantId: number,
billPaymentDTO: IBillPaymentDTO
): Promise<IBillPayment> {
return this.createBillPaymentService.createBillPayment(
tenantId,
billPaymentDTO
);
}
/**
* Delets the given bill payment with associated GL entries.
* @param {number} tenantId
* @param {number} billPaymentId
*/
public deleteBillPayment(tenantId: number, billPaymentId: number) {
return this.deleteBillPaymentService.deleteBillPayment(
tenantId,
billPaymentId
);
}
/**
* Edits the given bill payment with associated GL entries.
* @param {number} tenantId
* @param {number} billPaymentId
* @param billPaymentDTO
* @returns {Promise<IBillPayment>}
*/
public editBillPayment(
tenantId: number,
billPaymentId: number,
billPaymentDTO
): Promise<IBillPayment> {
return this.editBillPaymentService.editBillPayment(
tenantId,
billPaymentId,
billPaymentDTO
);
}
/**
* Retrieves bill payments list.
* @param {number} tenantId
* @param filterDTO
* @returns
*/
public getBillPayments(tenantId: number, filterDTO: IBillPaymentsFilter) {
return this.getBillPaymentsService.getBillPayments(tenantId, filterDTO);
}
/**
* Retrieve specific bill payment.
* @param {number} tenantId
* @param {number} billPyamentId
* @returns
*/
public getBillPayment(tenantId: number, billPyamentId: number) {
return this.getBillPaymentService.getBillPayment(tenantId, billPyamentId);
}
/**
* Retrieve payment made associated bills.
* @param {number} tenantId -
* @param {number} billPaymentId -
*/
public getPaymentBills(tenantId: number, billPaymentId: number) {
return this.getPaymentBillsService.getPaymentBills(tenantId, billPaymentId);
}
}

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